Skip to content

Commit

Permalink
client/dcr: MultiTrade
Browse files Browse the repository at this point in the history
Implements FundMultiOrder in the DCR wallet. Also removes the
MultiOrderFunder interface because all of the wallets now implement
MultiTrade.
  • Loading branch information
martonp committed Jul 11, 2023
1 parent 0f8be21 commit b46486f
Show file tree
Hide file tree
Showing 9 changed files with 1,908 additions and 75 deletions.
1 change: 0 additions & 1 deletion client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,6 @@ var _ asset.Bonder = (*baseWallet)(nil)
var _ asset.Authenticator = (*ExchangeWalletSPV)(nil)
var _ asset.Authenticator = (*ExchangeWalletFullNode)(nil)
var _ asset.Authenticator = (*ExchangeWalletAccelerator)(nil)
var _ asset.MultiOrderFunder = (*baseWallet)(nil)
var _ asset.AddressReturner = (*baseWallet)(nil)

// RecoveryCfg is the information that is transferred from the old wallet
Expand Down
107 changes: 84 additions & 23 deletions client/asset/dcr/coin_selection.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package dcr

import (
"math"
"math/rand"
"sort"
"time"
Expand Down Expand Up @@ -63,6 +64,22 @@ func orderEnough(val, lots, feeRate uint64, reportChange bool) func(sum uint64,
}
}

// reserveEnough generates a function that can be used as the enough argument
// to the fund method. The function returns true if sum is greater than equal
// to amt.
func reserveEnough(amt uint64) func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64) {
return func(sum uint64, _ uint32, unspent *compositeUTXO) (bool, uint64) {
return sum+toAtoms(unspent.rpc.Amount) >= amt, 0
}
}

func sumUTXOSize(set []*compositeUTXO) (tot uint32) {
for _, utxo := range set {
tot += utxo.input.Size()
}
return tot
}

func sumUTXOs(set []*compositeUTXO) (tot uint64) {
for _, utxo := range set {
tot += toAtoms(utxo.rpc.Amount)
Expand All @@ -76,7 +93,7 @@ func sumUTXOs(set []*compositeUTXO) (tot uint64) {
// 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 subsetWithLeastOverFund(enough func(uint64, uint32, *compositeUTXO) (bool, uint64), maxFund uint64, utxos []*compositeUTXO) []*compositeUTXO {
best := uint64(1 << 62)
var bestIncluded []bool
bestNumIncluded := 0
Expand All @@ -92,9 +109,9 @@ func subsetWithLeastSumGreaterThan(amt uint64, utxos []*compositeUTXO) []*compos
included := make([]bool, len(utxos))
const iterations = 1000

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

for nPass := 0; nPass < 2; nPass++ {
Expand All @@ -108,22 +125,25 @@ searchLoop:
if use {
included[i] = true
numIncluded++
totalBefore := nTotal
sizeBefore := totalSize
nTotal += toAtoms(shuffledUTXOs[i].rpc.Amount)
if nTotal >= amt {
if nTotal < best || (nTotal == best && numIncluded < bestNumIncluded) {
totalSize += shuffledUTXOs[i].input.Size()

if e, _ := enough(totalBefore, sizeBefore, shuffledUTXOs[i]); e {
if nTotal < best || (nTotal == best && numIncluded < bestNumIncluded) && nTotal <= maxFund {
best = nTotal
if bestIncluded == nil {
bestIncluded = make([]bool, len(shuffledUTXOs))
}
copy(bestIncluded, included)
bestNumIncluded = numIncluded
}
if nTotal == amt {
break searchLoop
}

included[i] = false
nTotal -= toAtoms(shuffledUTXOs[i].rpc.Amount)
numIncluded--
nTotal -= toAtoms(shuffledUTXOs[i].rpc.Amount)
totalSize -= shuffledUTXOs[i].input.Size()
}
}
}
Expand Down Expand Up @@ -165,37 +185,78 @@ searchLoop:
//
// 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 {
if amt == 0 || sumUTXOs(utxos) < amt {
return nil
func leastOverFund(enough func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64), utxos []*compositeUTXO) []*compositeUTXO {
return leastOverFundWithLimit(enough, math.MaxUint64, utxos)
}

// enoughWithoutAdditional is used to utilize an "enough" function with a set
// of UTXOs when we are not looking to add another UTXO to the set, just
// check if the current set if UTXOs is enough.
func enoughWithoutAdditional(enough func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64), utxos []*compositeUTXO) bool {
if len(utxos) == 0 {
return false
}

if len(utxos) == 1 {
e, _ := enough(0, 0, utxos[0])
return e
}

valueWithoutLast := sumUTXOs(utxos[:len(utxos)-1])
sizeWithoutLast := sumUTXOSize(utxos[:len(utxos)-1])

e, _ := enough(valueWithoutLast, sizeWithoutLast, utxos[len(utxos)-1])
return e
}

// leastOverFundWithLimit is the same as leastOverFund, but with an additional
// maxFund parameter. The total value of the returned UTXOs will not exceed
// maxFund.
func leastOverFundWithLimit(enough func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64), maxFund uint64, utxos []*compositeUTXO) []*compositeUTXO {
// Remove the UTXOs that are larger than maxFund
var smallEnoughUTXOs []*compositeUTXO
idx := sort.Search(len(utxos), func(i int) bool {
utxo := utxos[i]
return toAtoms(utxo.rpc.Amount) > maxFund
})
if idx == len(utxos) {
smallEnoughUTXOs = utxos
} else {
smallEnoughUTXOs = utxos[:idx]
}

// 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
idx = sort.Search(len(smallEnoughUTXOs), func(i int) bool {
utxo := smallEnoughUTXOs[i]
e, _ := enough(0, 0, utxo)
return e
})
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
var single *compositeUTXO // only return this if smaller ones would use more
if idx == len(smallEnoughUTXOs) { // no one is enough
small = smallEnoughUTXOs
} else {
small = utxos[:idx]
single = utxos[idx]
small = smallEnoughUTXOs[:idx]
single = smallEnoughUTXOs[idx]
}

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

// 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
}

Expand Down
14 changes: 9 additions & 5 deletions client/asset/dcr/coin_selection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import (
"testing"
"time"

dexdcr "decred.org/dcrdex/dex/networks/dcr"
walletjson "decred.org/dcrwallet/v3/rpc/jsonrpc/types"
)

func Test_leastOverFund(t *testing.T) {
amt := uint64(10e8)
newU := func(amt float64) *compositeUTXO {
return &compositeUTXO{
rpc: &walletjson.ListUnspentResult{Amount: amt},
rpc: &walletjson.ListUnspentResult{Amount: amt},
input: &dexdcr.SpendInfo{},
}
}
tests := []struct {
Expand Down Expand Up @@ -80,7 +82,7 @@ func Test_leastOverFund(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := leastOverFund(amt, tt.utxos)
got := leastOverFund(reserveEnough(amt), tt.utxos)
sort.Slice(got, func(i int, j int) bool {
return got[i].rpc.Amount < got[j].rpc.Amount
})
Expand Down Expand Up @@ -113,7 +115,8 @@ func Fuzz_leastOverFund(f *testing.F) {

newU := func(amt float64) *compositeUTXO {
return &compositeUTXO{
rpc: &walletjson.ListUnspentResult{Amount: amt},
rpc: &walletjson.ListUnspentResult{Amount: amt},
input: &dexdcr.SpendInfo{},
}
}

Expand All @@ -140,7 +143,7 @@ func Fuzz_leastOverFund(f *testing.F) {
utxos[i] = newU(v)
}
startTime := time.Now()
leastOverFund(amt*1e8, utxos)
leastOverFund(reserveEnough(amt*1e8), utxos)
totalDuration += time.Since(startTime)
totalUTXO += int64(n)
})
Expand All @@ -157,12 +160,13 @@ func BenchmarkLeastOverFund(b *testing.B) {
rpc: &walletjson.ListUnspentResult{
Amount: float64(1+rnd.Int31n(100)) / float64(1e8),
},
input: &dexdcr.SpendInfo{},
}
utxos[i] = utxo
}
b.ResetTimer()
for n := 0; n < b.N; n++ {
leastOverFund(10_000, utxos)
leastOverFund(reserveEnough(10_000), utxos)
}
}

Expand Down
Loading

0 comments on commit b46486f

Please sign in to comment.