decred.org/dcrdex@v1.0.3/client/asset/dcr/coin_selection.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package dcr
     5  
     6  import (
     7  	"math"
     8  	"math/rand"
     9  	"sort"
    10  	"time"
    11  
    12  	"decred.org/dcrdex/dex/calc"
    13  	dexdcr "decred.org/dcrdex/dex/networks/dcr"
    14  )
    15  
    16  // sendEnough generates a function that can be used as the enough argument to
    17  // the fund method when creating transactions to send funds. If fees are to be
    18  // subtracted from the inputs, set subtract so that the required amount excludes
    19  // the transaction fee. If change from the transaction should be considered
    20  // immediately available (not mixing), set reportChange to indicate this and the
    21  // returned enough func will return a non-zero excess value. Otherwise, the
    22  // enough func will always return 0, leaving only unselected UTXOs to cover any
    23  // required reserves.
    24  func sendEnough(amt, feeRate uint64, subtract bool, baseTxSize uint32, reportChange bool) func(sum uint64, inputSize uint32, unspent *compositeUTXO) (bool, uint64) {
    25  	return func(sum uint64, inputSize uint32, unspent *compositeUTXO) (bool, uint64) {
    26  		total := sum + toAtoms(unspent.rpc.Amount)
    27  		txFee := uint64(baseTxSize+inputSize+unspent.input.Size()) * feeRate
    28  		req := amt
    29  		if !subtract { // add the fee to required
    30  			req += txFee
    31  		}
    32  		if total < req {
    33  			return false, 0
    34  		}
    35  		excess := total - req
    36  		if !reportChange || dexdcr.IsDustVal(dexdcr.P2PKHOutputSize, excess, feeRate) {
    37  			excess = 0
    38  		}
    39  		return true, excess
    40  	}
    41  }
    42  
    43  // orderEnough generates a function that can be used as the enough argument to
    44  // the fund method. If change from a split transaction will be created AND
    45  // immediately available (not mixing), set reportChange to indicate this and the
    46  // returned enough func will return a non-zero excess value reflecting this
    47  // potential spit tx change. Otherwise, the enough func will always return 0,
    48  // leaving only unselected UTXOs to cover any required reserves.
    49  func orderEnough(val, lots, feeRate uint64, reportChange bool) func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64) {
    50  	return func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64) {
    51  		reqFunds := calc.RequiredOrderFunds(val, uint64(size+unspent.input.Size()), lots,
    52  			dexdcr.InitTxSizeBase, dexdcr.InitTxSize, feeRate)
    53  		total := sum + toAtoms(unspent.rpc.Amount) // all selected utxos
    54  
    55  		if total >= reqFunds { // that'll do it
    56  			// change = total - (val + swapTxnsFee)
    57  			excess := total - reqFunds // reqFunds = val + swapTxnsFee
    58  			if !reportChange || dexdcr.IsDustVal(dexdcr.P2PKHOutputSize, excess, feeRate) {
    59  				excess = 0
    60  			}
    61  			return true, excess
    62  		}
    63  		return false, 0
    64  	}
    65  }
    66  
    67  // reserveEnough generates a function that can be used as the enough argument
    68  // to the fund method. The function returns true if sum is greater than equal
    69  // to amt.
    70  func reserveEnough(amt uint64) func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64) {
    71  	return func(sum uint64, _ uint32, unspent *compositeUTXO) (bool, uint64) {
    72  		return sum+toAtoms(unspent.rpc.Amount) >= amt, 0
    73  	}
    74  }
    75  
    76  func sumUTXOSize(set []*compositeUTXO) (tot uint32) {
    77  	for _, utxo := range set {
    78  		tot += utxo.input.Size()
    79  	}
    80  	return tot
    81  }
    82  
    83  func sumUTXOs(set []*compositeUTXO) (tot uint64) {
    84  	for _, utxo := range set {
    85  		tot += toAtoms(utxo.rpc.Amount)
    86  	}
    87  	return tot
    88  }
    89  
    90  // subsetWithLeastOverFund attempts to select the subset of UTXOs with
    91  // the smallest total value greater than amt. It does this by making
    92  // 1000 random selections and returning the best one. Each selection
    93  // involves two passes over the UTXOs. The first pass randomly selects
    94  // each UTXO with 50% probability. Then, the second pass selects any
    95  // unused UTXOs until the total value is greater than or equal to amt.
    96  func subsetWithLeastOverFund(enough func(uint64, uint32, *compositeUTXO) (bool, uint64), maxFund uint64, utxos []*compositeUTXO) []*compositeUTXO {
    97  	best := uint64(1 << 62)
    98  	var bestIncluded []bool
    99  	bestNumIncluded := 0
   100  
   101  	rnd := rand.New(rand.NewSource(time.Now().Unix()))
   102  
   103  	shuffledUTXOs := make([]*compositeUTXO, len(utxos))
   104  	copy(shuffledUTXOs, utxos)
   105  	rnd.Shuffle(len(shuffledUTXOs), func(i, j int) {
   106  		shuffledUTXOs[i], shuffledUTXOs[j] = shuffledUTXOs[j], shuffledUTXOs[i]
   107  	})
   108  
   109  	included := make([]bool, len(utxos))
   110  	const iterations = 1000
   111  
   112  	for nRep := 0; nRep < iterations; nRep++ {
   113  		var nTotal uint64
   114  		var totalSize uint32
   115  		var numIncluded int
   116  
   117  		for nPass := 0; nPass < 2; nPass++ {
   118  			for i := 0; i < len(shuffledUTXOs); i++ {
   119  				var use bool
   120  				if nPass == 0 {
   121  					use = rnd.Int63()&1 == 1
   122  				} else {
   123  					use = !included[i]
   124  				}
   125  				if use {
   126  					included[i] = true
   127  					numIncluded++
   128  					totalBefore := nTotal
   129  					sizeBefore := totalSize
   130  					nTotal += toAtoms(shuffledUTXOs[i].rpc.Amount)
   131  					totalSize += shuffledUTXOs[i].input.Size()
   132  
   133  					if e, _ := enough(totalBefore, sizeBefore, shuffledUTXOs[i]); e {
   134  						if (nTotal < best || (nTotal == best && numIncluded < bestNumIncluded)) && nTotal <= maxFund {
   135  							best = nTotal
   136  							if bestIncluded == nil {
   137  								bestIncluded = make([]bool, len(shuffledUTXOs))
   138  							}
   139  							copy(bestIncluded, included)
   140  							bestNumIncluded = numIncluded
   141  						}
   142  
   143  						included[i] = false
   144  						numIncluded--
   145  						nTotal -= toAtoms(shuffledUTXOs[i].rpc.Amount)
   146  						totalSize -= shuffledUTXOs[i].input.Size()
   147  					}
   148  				}
   149  			}
   150  		}
   151  		for i := 0; i < len(included); i++ {
   152  			included[i] = false
   153  		}
   154  	}
   155  
   156  	if bestIncluded == nil {
   157  		return nil
   158  	}
   159  
   160  	set := make([]*compositeUTXO, 0, len(shuffledUTXOs))
   161  	for i, inc := range bestIncluded {
   162  		if inc {
   163  			set = append(set, shuffledUTXOs[i])
   164  		}
   165  	}
   166  
   167  	return set
   168  }
   169  
   170  // leastOverFund attempts to pick a subset of the provided UTXOs to reach the
   171  // required amount with the objective of minimizing the total amount of the
   172  // selected UTXOs. This is different from the objective used when funding
   173  // orders, which is to minimize the number of UTXOs (to minimize fees).
   174  //
   175  // The UTXOs MUST be sorted in ascending order (smallest first, largest last)!
   176  //
   177  // This begins by partitioning the slice before the smallest single UTXO that is
   178  // large enough to fully fund the requested amount, if it exists. If the smaller
   179  // set is insufficient, the single largest UTXO is returned. If instead the set
   180  // of smaller UTXOs has enough total value, it will search for a subset that
   181  // reaches the amount with least over-funding (see subsetWithLeastSumGreaterThan).
   182  // If that subset has less combined value than the single
   183  // sufficiently-large UTXO (if it exists), the subset will be returned,
   184  // otherwise the single UTXO will be returned.
   185  //
   186  // If the provided UTXO set has less combined value than the requested amount a
   187  // nil slice is returned.
   188  func leastOverFund(enough func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64), utxos []*compositeUTXO) []*compositeUTXO {
   189  	return leastOverFundWithLimit(enough, math.MaxUint64, utxos)
   190  }
   191  
   192  // enoughWithoutAdditional is used to utilize an "enough" function with a set
   193  // of UTXOs when we are not looking to add another UTXO to the set, just
   194  // check if the current set of UTXOs is enough.
   195  func enoughWithoutAdditional(enough func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64), utxos []*compositeUTXO) bool {
   196  	if len(utxos) == 0 {
   197  		return false
   198  	}
   199  
   200  	if len(utxos) == 1 {
   201  		e, _ := enough(0, 0, utxos[0])
   202  		return e
   203  	}
   204  
   205  	valueWithoutLast := sumUTXOs(utxos[:len(utxos)-1])
   206  	sizeWithoutLast := sumUTXOSize(utxos[:len(utxos)-1])
   207  
   208  	e, _ := enough(valueWithoutLast, sizeWithoutLast, utxos[len(utxos)-1])
   209  	return e
   210  }
   211  
   212  // leastOverFundWithLimit is the same as leastOverFund, but with an additional
   213  // maxFund parameter. The total value of the returned UTXOs will not exceed
   214  // maxFund.
   215  func leastOverFundWithLimit(enough func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64), maxFund uint64, utxos []*compositeUTXO) []*compositeUTXO {
   216  	// Remove the UTXOs that are larger than maxFund
   217  	var smallEnoughUTXOs []*compositeUTXO
   218  	idx := sort.Search(len(utxos), func(i int) bool {
   219  		utxo := utxos[i]
   220  		return toAtoms(utxo.rpc.Amount) > maxFund
   221  	})
   222  	if idx == len(utxos) {
   223  		smallEnoughUTXOs = utxos
   224  	} else {
   225  		smallEnoughUTXOs = utxos[:idx]
   226  	}
   227  
   228  	// Partition - smallest UTXO that is large enough to fully fund, and the set
   229  	// of smaller ones.
   230  	idx = sort.Search(len(smallEnoughUTXOs), func(i int) bool {
   231  		utxo := smallEnoughUTXOs[i]
   232  		e, _ := enough(0, 0, utxo)
   233  		return e
   234  	})
   235  	var small []*compositeUTXO
   236  	var single *compositeUTXO         // only return this if smaller ones would use more
   237  	if idx == len(smallEnoughUTXOs) { // no one is enough
   238  		small = smallEnoughUTXOs
   239  	} else {
   240  		small = smallEnoughUTXOs[:idx]
   241  		single = smallEnoughUTXOs[idx]
   242  	}
   243  
   244  	var set []*compositeUTXO
   245  	if !enoughWithoutAdditional(enough, small) {
   246  		if single != nil {
   247  			return []*compositeUTXO{single}
   248  		} else {
   249  			return nil
   250  		}
   251  	} else {
   252  		set = subsetWithLeastOverFund(enough, maxFund, small)
   253  	}
   254  
   255  	// Return the small UTXO subset if it is less than the single big UTXO.
   256  	if single != nil && toAtoms(single.rpc.Amount) < sumUTXOs(set) {
   257  		return []*compositeUTXO{single}
   258  	}
   259  
   260  	return set
   261  }
   262  
   263  // utxoSetDiff performs the setdiff(set,sub) of two UTXO sets. That is, any
   264  // UTXOs that are both sets are removed from the first. The comparison is done
   265  // *by pointer*, with no regard to the values of the compositeUTXO elements.
   266  func utxoSetDiff(set, sub []*compositeUTXO) []*compositeUTXO {
   267  	var availUTXOs []*compositeUTXO
   268  avail:
   269  	for _, utxo := range set {
   270  		for _, kept := range sub {
   271  			if utxo == kept { // by pointer
   272  				continue avail
   273  			}
   274  		}
   275  		availUTXOs = append(availUTXOs, utxo)
   276  	}
   277  	return availUTXOs
   278  }