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