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 }