-
Notifications
You must be signed in to change notification settings - Fork 117
client/dcr: Improved UTXO selection algorithm #2169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,115 +3,13 @@ package dcr | |
import ( | ||
"math/rand" | ||
"reflect" | ||
"sort" | ||
"testing" | ||
"time" | ||
|
||
walletjson "decred.org/dcrwallet/v2/rpc/jsonrpc/types" | ||
) | ||
|
||
func Test_subsetLargeBias(t *testing.T) { | ||
amt := uint64(10e8) | ||
newU := func(amt float64) *compositeUTXO { | ||
return &compositeUTXO{ | ||
rpc: &walletjson.ListUnspentResult{Amount: amt}, | ||
} | ||
} | ||
tests := []struct { | ||
name string | ||
utxos []*compositeUTXO | ||
want []*compositeUTXO | ||
}{ | ||
{ | ||
"1,3 exact", | ||
[]*compositeUTXO{ne 8000 wU(1), newU(8), newU(9)}, | ||
[]*compositeUTXO{newU(1), newU(9)}, | ||
}, | ||
{ | ||
"subset large bias", | ||
[]*compositeUTXO{newU(1), newU(3), newU(6), newU(7)}, | ||
[]*compositeUTXO{newU(3), newU(7)}, | ||
}, | ||
{ | ||
"subset large bias", | ||
[]*compositeUTXO{newU(1), newU(3), newU(5), newU(7), newU(8)}, | ||
[]*compositeUTXO{newU(3), newU(8)}, | ||
}, | ||
{ | ||
"insufficient", | ||
[]*compositeUTXO{newU(1), newU(8)}, | ||
nil, | ||
}, | ||
{ | ||
"all exact", | ||
[]*compositeUTXO{newU(1), newU(9)}, | ||
[]*compositeUTXO{newU(1), newU(9)}, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
if got := subsetLargeBias(amt, tt.utxos); !reflect.DeepEqual(got, tt.want) { | ||
t.Errorf("subset() = %v, want %v", got, tt.want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func Test_subsetSmallBias(t *testing.T) { | ||
amt := uint64(10e8) | ||
newU := func(amt float64) *compositeUTXO { | ||
return &compositeUTXO{ | ||
rpc: &walletjson.ListUnspentResult{Amount: amt}, | ||
} | ||
} | ||
tests := []struct { | ||
name string | ||
utxos []*compositeUTXO | ||
want []*compositeUTXO | ||
}{ | ||
{ | ||
"1,3", | ||
[]*compositeUTXO{newU(1), newU(8), newU(9)}, | ||
[]*compositeUTXO{newU(8), newU(9)}, | ||
}, | ||
{ | ||
"subset", | ||
[]*compositeUTXO{newU(1), newU(9), newU(11)}, | ||
[]*compositeUTXO{newU(1), newU(9)}, | ||
}, | ||
{ | ||
"subset small bias", | ||
[]*compositeUTXO{newU(1), newU(3), newU(6), newU(7)}, | ||
[]*compositeUTXO{newU(1), newU(3), newU(6)}, | ||
}, | ||
{ | ||
"subset small bias", | ||
[]*compositeUTXO{newU(1), newU(3), newU(5), newU(7), newU(8)}, | ||
[]*compositeUTXO{newU(5), newU(7)}, | ||
}, | ||
{ | ||
"ok nil", | ||
[]*compositeUTXO{newU(1), newU(8)}, | ||
nil, | ||
}, | ||
{ | ||
"two, over", | ||
[]*compositeUTXO{newU(5), newU(7), newU(11)}, | ||
[]*compositeUTXO{newU(5), newU(7)}, | ||
}, | ||
{ | ||
"insufficient", | ||
[]*compositeUTXO{newU(1), newU(8)}, | ||
nil, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
if got := subsetSmallBias(amt, tt.utxos); !reflect.DeepEqual(got, tt.want) { | ||
t.Errorf("subset() = %v, want %v", got, tt.want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func Test_leastOverFund(t *testing.T) { | ||
amt := uint64(10e8) | ||
newU := func(amt float64) *compositeUTXO { | ||
There was a problem hiding this comment. Choose a reason for hiding this c AE8F ommentThe reason will be displayed to describe this comment to others. Learn more. Thanks for this! I guess the test cases for the dumber leastOverFund were too easy? We chatted a bit about replacing the algo and it sounded like the new one got better results ~1/2 the time. Would it require much larger sets to test a case where it's a better result? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The better results ~1/2 the time was with the dynamic programming solution which only worked with very small amounts of UTXOs. When I tested this one with large amounts of UTXOs and compared it with the old solution, it got better results every single time. The two solutions (this vs small and large bias subset) are equivalent, but the old one tries 2 random possibilities and this one tries 1000. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tests well. ~16ms for a set of 4000 UTXOs between 0 and 2 DCR in search of a subset totaling 10DCR func Test_subsetFund(t *testing.T) {
amt := uint64(10e8)
s := make([]*compositeUTXO, 4000)
newU := func(amt float64) *compositeUTXO {
return &compositeUTXO{
rpc: &walletjson.ListUnspentResult{Amount: amt},
}
}
for i := range s {
v := rand.Float64() + float64(rand.Int63n(2))
v = math.Round(v*1e8) / 1e8
fmt.Println(v)
s[i] = newU(v)
}
sort.Slice(s, func(i, j int) bool {
return s[i].rpc.Amount < s[i].rpc.Amount
})
r := subsetWithLeastSumGreaterThan(amt, s)
fmt.Println(len(r), sumUTXOs(r))
} |
||
|
@@ -156,7 +54,7 @@ func Test_leastOverFund(t *testing.T) { | |
}, | ||
{ | ||
"subset small bias", | ||
[]*compositeUTXO{newU(1), newU(3), newU(6), newU(7)}, | ||
[]*compositeUTXO{newU(3), newU(6), newU(7)}, | ||
[]*compositeUTXO{newU(3), newU(7)}, | ||
}, | ||
{ | ||
|
@@ -182,30 +80,32 @@ func Test_leastOverFund(t *testing.T) { | |
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
if got := leastOverFund(amt, tt.utxos); !reflect.DeepEqual(got, tt.want) { | ||
got := leastOverFund(amt, tt.utxos) | ||
sort.Slice(got, func(i int, j int) bool { | ||
return got[i].rpc.Amount < got[j].rpc.Amount | ||
}) | ||
if !reflect.DeepEqual(got, tt.want) { | ||
t.Errorf("subset() = %v, want %v", got, tt.want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func Fuzz_leastOverFund(f *testing.F) { | ||
seeds := []struct { | ||
type seed struct { | ||
amt uint64 | ||
n int | ||
}{{ | ||
amt: 200, | ||
n: 2, | ||
}, { | ||
amt: 20, | ||
n: 1, | ||
}, { | ||
amt: 20, | ||
n: 20, | ||
}, { | ||
amt: 2, | ||
n: 40, | ||
}} | ||
} | ||
|
||
rnd := rand.New(rand.NewSource(1)) | ||
|
||
seeds := make([]seed, 0, 40) | ||
for i := 0; i < 10; i++ { | ||
seeds = append(seeds, seed{ | ||
amt: uint64(rnd.Intn(40)), | ||
n: rnd.Intn(20000), | ||
}) | ||
} | ||
|
||
for _, seed := range seeds { | ||
f.Add(seed.amt, seed.n) | ||
|
@@ -217,6 +117,9 @@ func Fuzz_leastOverFund(f *testing.F) { | |
} | ||
} | ||
|
||
var totalDuration time.Duration | ||
var totalUTXO int64 | ||
|
||
f.Fuzz(func(t *testing.T, amt uint64, n int) { | ||
if n < 1 || n > 65535 || amt == 0 || amt > 21e6 { | ||
t.Skip() | ||
|
@@ -236,8 +139,31 @@ func Fuzz_leastOverFund(f *testing.F) { | |
} | ||
utxos[i] = newU(v) | ||
} | ||
startTime := time.Now() | ||
leastOverFund(amt*1e8, utxos) | ||
totalDuration += time.Since(startTime) | ||
totalUTXO += int64(n) | ||
}) | ||
|
||
f.Logf("leastOverFund: average duration: %v, with average number of UTXOs: %v\n", totalDuration/100, totalUTXO/100) | ||
} | ||
|
||
func BenchmarkLeastOverFund(b *testing.B) { | ||
// Same amounts every time. | ||
rnd := rand.New(rand.NewSource(1)) | ||
utxos := make([]*compositeUTXO, 2_000) | ||
for i := range utxos { | ||
utxo := &compositeUTXO{ | ||
rpc: &walletjson.ListUnspentResult{ | ||
Amount: float64(1+rnd.Int31n(100)) / float64(1e8), | ||
}, | ||
} | ||
utxos[i] = utxo | ||
} | ||
b.ResetTimer() | ||
for n := 0; n < b.N; n++ { | ||
leastOverFund(10_000, utxos) | ||
} | ||
} | ||
|
||
func Test_utxoSetDiff(t *testing.T) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only other thing from the cpp that might make sense is to
break nreps
(this outermost loop) if we happen to hitnTotal == amt
.