Skip to content

Commit

Permalink
Use shuffle
Browse files Browse the repository at this point in the history
  • Loading branch information
martonp committed Feb 26, 2023
1 parent 7a08ec4 commit cc28045
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 12 deletions.
117 changes: 107 additions & 10 deletions client/asset/dcr/coin_selection.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,23 +72,86 @@ func sumUTXOs(set []*compositeUTXO) (tot uint64) {
return tot
}

// subsetWithLeastSumGreaterThan attempts to select the subset of UTXOs with
// the smallest total value greater than amt. It does this by making
// 1000 random selections and returning the best one. Each selection
// involves two passes over the UTXOs. The first pass randomly selects
// each UTXO with 50% probability. Then, the second pass selects any
// unused UTXOs until the total value is greater than or equal to amt.
func subsetWithLeastSumGreaterThan(amt uint64, utxos []*compositeUTXO) []*compositeUTXO {
func subsetWithLeastSumGreaterThanShuffle(amt uint64, utxos []*compositeUTXO) []*compositeUTXO {
best := uint64(1 << 62)
var bestIncluded []bool
bestNumIncluded := 0

rnd := rand.New(rand.NewSource(rand.Int63()))
rnd := rand.New(rand.NewSource(12345))

shuffledUTXOs := make([]*compositeUTXO, len(utxos))
copy(shuffledUTXOs, utxos)
rnd.Shuffle(len(shuffledUTXOs), func(i, j int) {
shuffledUTXOs[i], shuffledUTXOs[j] = shuffledUTXOs[j], shuffledUTXOs[i]
})

included := make([]bool, len(utxos))
const iterations = 1000

for nRep := 0; nRep < iterations; nRep++ {
var nTotal uint64
var numIncluded int

for nPass := 0; nPass < 2; nPass++ {
for i := 0; i < len(shuffledUTXOs); i++ {
var use bool
if nPass == 0 {
use = rnd.Int63()&1 == 1
} else {
use = !included[i]
}
if use {
included[i] = true
numIncluded++
nTotal += toAtoms(shuffledUTXOs[i].rpc.Amount)
if nTotal >= amt {
if nTotal < best || (nTotal == best && numIncluded < bestNumIncluded) {
best = nTotal
if bestIncluded == nil {
bestIncluded = make([]bool, len(utxos))
}
copy(bestIncluded, included)
bestNumIncluded = numIncluded
}
included[i] = false
nTotal -= toAtoms(shuffledUTXOs[i].rpc.Amount)
numIncluded--
}
}
}
}
for i := 0; i < len(included); i++ {
included[i] = false
}
}

if bestIncluded == nil {
return nil
}

set := make([]*compositeUTXO, 0, len(shuffledUTXOs))
for i, inc := range bestIncluded {
if inc {
set = append(set, shuffledUTXOs[i])
}
}

return set
}

func subsetWithLeastSumGreaterThanNoShuffle(amt uint64, utxos []*compositeUTXO) []*compositeUTXO {
best := uint64(1 << 62)
var bestIncluded []bool
bestNumIncluded := 0

rnd := rand.New(rand.NewSource(12345))
included := make([]bool, len(utxos))
const iterations = 1000

for nRep := 0; nRep < iterations; nRep++ {
var nTotal uint64
var numIncluded int

passes:
for nPass := 0; nPass < 2; nPass++ {
for i := 0; i < len(utxos); i++ {
Expand Down Expand Up @@ -153,7 +216,41 @@ func subsetWithLeastSumGreaterThan(amt uint64, utxos []*compositeUTXO) []*compos
//
// If the provided UTXO set has less combined value than the requested amount a
// nil slice is returned.
func leastOverFund(amt uint64, utxos []*compositeUTXO) []*compositeUTXO {
func leastOverFundNoShuffle(amt uint64, utxos []*compositeUTXO) []*compositeUTXO {
if amt == 0 || sumUTXOs(utxos) < amt {
return nil
}

// Partition - smallest UTXO that is large enough to fully fund, and the set
// of smaller ones.
idx := sort.Search(len(utxos), func(i int) bool {
return toAtoms(utxos[i].rpc.Amount) >= amt
})
var small []*compositeUTXO
var single *compositeUTXO // only return this if smaller ones would use more
if idx == len(utxos) { // no one is enough
small = utxos
} else {
small = utxos[:idx]
single = utxos[idx]
}

// Find a subset of the small UTXO set with smallest combined amount.
var set []*compositeUTXO
if sumUTXOs(small) >= amt {
set = subsetWithLeastSumGreaterThanNoShuffle(amt, small)
} else if single != nil {
return []*compositeUTXO{single}
}

// Return the small UTXO subset if it is less than the single big UTXO.
if single != nil && toAtoms(single.rpc.Amount) < sumUTXOs(set) {
return []*compositeUTXO{single}
}
return set
}

func leastOverFundShuffle(amt uint64, utxos []*compositeUTXO) []*compositeUTXO {
if amt == 0 || sumUTXOs(utxos) < amt {
return nil
}
Expand All @@ -175,7 +272,7 @@ func leastOverFund(amt uint64, utxos []*compositeUTXO) []*compositeUTXO {
// Find a subset of the small UTXO set with smallest combined amount.
var set []*compositeUTXO
if sumUTXOs(small) >= amt {
set = subsetWithLeastSumGreaterThan(amt, small)
set = subsetWithLeastSumGreaterThanShuffle(amt, small)
} else if single != nil {
return []*compositeUTXO{single}
}
Expand Down
10 changes: 8 additions & 2 deletions client/asset/dcr/coin_selection_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package dcr

import (
"fmt"
"math/rand"
"reflect"
"testing"
"time"

walletjson "decred.org/dcrwallet/v2/rpc/jsonrpc/types"
)
Expand Down Expand Up @@ -95,7 +97,7 @@ func Fuzz_leastOverFund(f *testing.F) {
for i := 0; i < 100; i++ {
seeds = append(seeds, seed{
amt: uint64(rand.Intn(40)),
n: rand.Intn(65000),
n: rand.Intn(20000),
})
}

Expand Down Expand Up @@ -128,7 +130,11 @@ func Fuzz_leastOverFund(f *testing.F) {
}
utxos[i] = newU(v)
}
leastOverFund(amt*1e8, utxos)
startTime := time.Now()
shuffleRes := leastOverFundShuffle(amt*1e8, utxos)
fmt.Printf("shuffle time: %v with #utxo: %d\n", time.Since(startTime), len(utxos))
noShuffleRes := leastOverFundNoShuffle(amt*1e8, utxos)
fmt.Printf("shuffle is better: %v -- shuffle: %d, no shuffle %d\n", sumUTXOs(shuffleRes) < sumUTXOs(noShuffleRes), sumUTXOs(shuffleRes), sumUTXOs(noShuffleRes))
})
}

Expand Down

0 comments on commit cc28045

Please sign in to comment.