decred.org/dcrdex@v1.0.3/client/asset/dcr/coin_selection_test.go (about) 1 //go:build !harness && !vspd 2 3 package dcr 4 5 import ( 6 "math/rand" 7 "reflect" 8 "sort" 9 "testing" 10 "time" 11 12 dexdcr "decred.org/dcrdex/dex/networks/dcr" 13 walletjson "decred.org/dcrwallet/v4/rpc/jsonrpc/types" 14 ) 15 16 func Test_leastOverFund(t *testing.T) { 17 amt := uint64(10e8) 18 newU := func(amt float64) *compositeUTXO { 19 return &compositeUTXO{ 20 rpc: &walletjson.ListUnspentResult{Amount: amt}, 21 input: &dexdcr.SpendInfo{}, 22 } 23 } 24 tests := []struct { 25 name string 26 utxos []*compositeUTXO 27 want []*compositeUTXO 28 }{ 29 { 30 "1,3", 31 []*compositeUTXO{newU(1), newU(8), newU(9)}, 32 []*compositeUTXO{newU(1), newU(9)}, 33 }, 34 { 35 "1,2", 36 []*compositeUTXO{newU(1), newU(9)}, 37 []*compositeUTXO{newU(1), newU(9)}, 38 }, 39 { 40 "1,2++", 41 []*compositeUTXO{newU(2), newU(9)}, 42 []*compositeUTXO{newU(2), newU(9)}, 43 }, 44 { 45 "2,3++", 46 []*compositeUTXO{newU(0), newU(2), newU(9)}, 47 []*compositeUTXO{newU(2), newU(9)}, 48 }, 49 { 50 "3", 51 []*compositeUTXO{newU(0), newU(2), newU(10)}, 52 []*compositeUTXO{newU(10)}, 53 }, 54 { 55 "subset", 56 []*compositeUTXO{newU(1), newU(9), newU(11)}, 57 []*compositeUTXO{newU(1), newU(9)}, 58 }, 59 { 60 "subset small bias", 61 []*compositeUTXO{newU(3), newU(6), newU(7)}, 62 []*compositeUTXO{newU(3), newU(7)}, 63 }, 64 { 65 "single exception", 66 []*compositeUTXO{newU(5), newU(7), newU(11)}, 67 []*compositeUTXO{newU(11)}, 68 }, 69 { 70 "1 of 1", 71 []*compositeUTXO{newU(10)}, 72 []*compositeUTXO{newU(10)}, 73 }, 74 { 75 "ok nil", 76 []*compositeUTXO{newU(1), newU(8)}, 77 nil, 78 }, 79 { 80 "ok", 81 []*compositeUTXO{newU(1)}, 82 nil, 83 }, 84 } 85 for _, tt := range tests { 86 t.Run(tt.name, func(t *testing.T) { 87 got := leastOverFund(reserveEnough(amt), tt.utxos) 88 sort.Slice(got, func(i int, j int) bool { 89 return got[i].rpc.Amount < got[j].rpc.Amount 90 }) 91 if !reflect.DeepEqual(got, tt.want) { 92 t.Errorf("subset() = %v, want %v", got, tt.want) 93 } 94 }) 95 } 96 } 97 98 func Test_leastOverFundWithLimit(t *testing.T) { 99 newU := func(amt float64) *compositeUTXO { 100 return &compositeUTXO{ 101 rpc: &walletjson.ListUnspentResult{Amount: amt}, 102 input: &dexdcr.SpendInfo{}, 103 } 104 } 105 tests := []struct { 106 name string 107 limit uint64 108 utxos []*compositeUTXO 109 want []*compositeUTXO 110 }{ 111 { 112 "1,3", 113 10e8, 114 []*compositeUTXO{newU(1), newU(8), newU(9)}, 115 []*compositeUTXO{newU(1), newU(9)}, 116 }, 117 { 118 "max fund too low", 119 9e8, 120 []*compositeUTXO{newU(1), newU(8), newU(9)}, 121 nil, 122 }, 123 } 124 for _, tt := range tests { 125 t.Run(tt.name, func(t *testing.T) { 126 got := leastOverFundWithLimit(reserveEnough(10e8), tt.limit, tt.utxos) 127 sort.Slice(got, func(i int, j int) bool { 128 return got[i].rpc.Amount < got[j].rpc.Amount 129 }) 130 if !reflect.DeepEqual(got, tt.want) { 131 t.Errorf("subset() = %v, want %v", got, tt.want) 132 } 133 }) 134 } 135 } 136 137 func Fuzz_leastOverFund(f *testing.F) { 138 type seed struct { 139 amt uint64 140 n int 141 } 142 143 rnd := rand.New(rand.NewSource(1)) 144 145 seeds := make([]seed, 0, 40) 146 for i := 0; i < 10; i++ { 147 seeds = append(seeds, seed{ 148 amt: uint64(rnd.Intn(40)), 149 n: rnd.Intn(20000), 150 }) 151 } 152 153 for _, seed := range seeds { 154 f.Add(seed.amt, seed.n) 155 } 156 157 newU := func(amt float64) *compositeUTXO { 158 return &compositeUTXO{ 159 rpc: &walletjson.ListUnspentResult{Amount: amt}, 160 input: &dexdcr.SpendInfo{}, 161 } 162 } 163 164 var totalDuration time.Duration 165 var totalUTXO int64 166 167 f.Fuzz(func(t *testing.T, amt uint64, n int) { 168 if n < 1 || n > 65535 || amt == 0 || amt > 21e6 { 169 t.Skip() 170 } 171 m := 2 * amt / uint64(n) 172 utxos := make([]*compositeUTXO, n) 173 for i := range utxos { 174 var v float64 175 if rand.Intn(2) > 0 { 176 v = rand.Float64() 177 } 178 if m != 0 { 179 v += float64(rand.Int63n(int64(m))) 180 } 181 if v > 40000 { 182 t.Skip() 183 } 184 utxos[i] = newU(v) 185 } 186 startTime := time.Now() 187 leastOverFund(reserveEnough(amt*1e8), utxos) 188 totalDuration += time.Since(startTime) 189 totalUTXO += int64(n) 190 }) 191 192 f.Logf("leastOverFund: average duration: %v, with average number of UTXOs: %v\n", totalDuration/100, totalUTXO/100) 193 } 194 195 func BenchmarkLeastOverFund(b *testing.B) { 196 // Same amounts every time. 197 rnd := rand.New(rand.NewSource(1)) 198 utxos := make([]*compositeUTXO, 2_000) 199 for i := range utxos { 200 utxo := &compositeUTXO{ 201 rpc: &walletjson.ListUnspentResult{ 202 Amount: float64(1+rnd.Int31n(100)) / float64(1e8), 203 }, 204 input: &dexdcr.SpendInfo{}, 205 } 206 utxos[i] = utxo 207 } 208 b.ResetTimer() 209 for n := 0; n < b.N; n++ { 210 leastOverFund(reserveEnough(10_000), utxos) 211 } 212 } 213 214 func Test_utxoSetDiff(t *testing.T) { 215 newU := func(amt float64) *compositeUTXO { 216 return &compositeUTXO{ 217 rpc: &walletjson.ListUnspentResult{Amount: amt}, 218 } 219 } 220 221 all := []*compositeUTXO{newU(1), newU(3), newU(6), newU(7), newU(12)} 222 223 sub := func(inds []int) []*compositeUTXO { 224 out := make([]*compositeUTXO, len(inds)) 225 for i, ind := range inds { 226 out[i] = all[ind] 227 } 228 return out 229 } 230 231 checkPtrs := func(a, b []*compositeUTXO) bool { 232 if len(a) != len(b) { 233 return false 234 } 235 for i := range a { 236 if a[i] != b[i] { 237 return false 238 } 239 } 240 return true 241 } 242 243 tests := []struct { 244 name string 245 set []*compositeUTXO 246 sub []*compositeUTXO 247 want []*compositeUTXO 248 }{ 249 { 250 "one", 251 sub([]int{0, 1}), 252 sub([]int{1}), 253 sub([]int{0}), 254 }, { 255 "none", 256 sub([]int{0, 1}), 257 sub([]int{2}), 258 sub([]int{0, 1}), 259 }, { 260 "some", 261 sub([]int{0, 1}), 262 sub([]int{1, 2}), 263 sub([]int{0}), 264 }, { 265 "both all / nil", 266 sub([]int{0, 1}), 267 sub([]int{0, 1}), 268 nil, 269 }, { 270 "one all / nil", 271 sub([]int{0}), 272 sub([]int{0}), 273 nil, 274 }, { 275 "bigger sub, all", 276 sub([]int{0, 1}), 277 sub([]int{0, 1, 2}), 278 nil, 279 }, { 280 "nil set", 281 nil, 282 sub([]int{0, 1, 2}), 283 nil, 284 }, { 285 "nil sub", 286 sub([]int{0, 1, 2}), 287 nil, 288 sub([]int{0, 1, 2}), 289 }, { 290 "nil both", 291 nil, 292 nil, 293 nil, 294 }, 295 } 296 for _, tt := range tests { 297 t.Run(tt.name, func(t *testing.T) { 298 got := utxoSetDiff(tt.set, tt.sub) 299 if !checkPtrs(got, tt.want) { 300 t.Errorf("utxoSetDiff() = %v, want %v", got, tt.want) 301 } 302 }) 303 } 304 }