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  }