From c785ad3763da55d46351d4bc4dffc3e4e2cd4593 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 12 Jan 2024 14:55:52 +0100 Subject: [PATCH] wallet: export coin selection strategy code --- wallet/createtx.go | 150 ++++++++++++++++++++++++++-------------- wallet/createtx_test.go | 4 +- wallet/wallet.go | 24 +++++-- 3 files changed, 122 insertions(+), 56 deletions(-) diff --git a/wallet/createtx.go b/wallet/createtx.go index 7f0d75847c..a65cce8db2 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -21,16 +21,8 @@ import ( "github.com/btcsuite/btcwallet/wtxmgr" ) -// byAmount defines the methods needed to satisify sort.Interface to -// sort credits by their output amount. -type byAmount []wtxmgr.Credit - -func (s byAmount) Len() int { return len(s) } -func (s byAmount) Less(i, j int) bool { return s[i].Amount < s[j].Amount } -func (s byAmount) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -func makeInputSource(eligible []wtxmgr.Credit) txauthor.InputSource { - // Current inputs and their total value. These are closed over by the +func makeInputSource(eligible []Coin) txauthor.InputSource { + // Current inputs and their total value. These are closed over by the // returned input source and reused across multiple calls. currentTotal := btcutil.Amount(0) currentInputs := make([]*wire.TxIn, 0, len(eligible)) @@ -41,15 +33,25 @@ func makeInputSource(eligible []wtxmgr.Credit) txauthor.InputSource { []btcutil.Amount, [][]byte, error) { for currentTotal < target && len(eligible) != 0 { - nextCredit := &eligible[0] + nextCredit := eligible[0] + prevOut := nextCredit.TxOut + outpoint := nextCredit.OutPoint eligible = eligible[1:] - nextInput := wire.NewTxIn(&nextCredit.OutPoint, nil, nil) - currentTotal += nextCredit.Amount + + nextInput := wire.NewTxIn(&outpoint, nil, nil) + currentTotal += btcutil.Amount(prevOut.Value) currentInputs = append(currentInputs, nextInput) - currentScripts = append(currentScripts, nextCredit.PkScript) - currentInputValues = append(currentInputValues, nextCredit.Amount) + currentScripts = append( + currentScripts, prevOut.PkScript, + ) + currentInputValues = append( + currentInputValues, + btcutil.Amount(prevOut.Value), + ) } - return currentTotal, currentInputs, currentInputValues, currentScripts, nil + + return currentTotal, currentInputs, currentInputValues, + currentScripts, nil } } @@ -123,6 +125,11 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, return nil, err } + // Fall back to default coin selection strategy if none is supplied. + if coinSelectionStrategy == nil { + coinSelectionStrategy = CoinSelectionLargest + } + var tx *txauthor.AuthoredTx err = walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error { addrmgrNs, changeSource, err := w.addrMgrWithChangeSource( @@ -139,40 +146,27 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, return err } - var inputSource txauthor.InputSource - - switch coinSelectionStrategy { - // Pick largest outputs first. - case CoinSelectionLargest: - sort.Sort(sort.Reverse(byAmount(eligible))) - inputSource = makeInputSource(eligible) - - // Select coins at random. This prevents the creation of ever - // smaller utxos over time that may never become economical to - // spend. - case CoinSelectionRandom: - // Skip inputs that do not raise the total transaction - // output value at the requested fee rate. - var positivelyYielding []wtxmgr.Credit - for _, output := range eligible { - output := output - - if !inputYieldsPositively(&output, feeSatPerKb) { - continue - } - - positivelyYielding = append( - positivelyYielding, output, - ) + // Wrap our coins in a type that implements the SelectableCoin + // interface, so we can arrange them according to the selected + // coin selection strategy. + wrappedEligible := make([]Coin, len(eligible)) + for i := range eligible { + wrappedEligible[i] = Coin{ + TxOut: wire.TxOut{ + Value: int64(eligible[i].Amount), + PkScript: eligible[i].PkScript, + }, + OutPoint: eligible[i].OutPoint, } - - rand.Shuffle(len(positivelyYielding), func(i, j int) { - positivelyYielding[i], positivelyYielding[j] = - positivelyYielding[j], positivelyYielding[i] - }) - - inputSource = makeInputSource(positivelyYielding) } + arrangedCoins, err := coinSelectionStrategy.ArrangeCoins( + wrappedEligible, feeSatPerKb, + ) + if err != nil { + return err + } + + inputSource := makeInputSource(arrangedCoins) tx, err = txauthor.NewUnsignedTransaction( outputs, feeSatPerKb, inputSource, changeSource, @@ -337,11 +331,13 @@ func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, // best-case added virtual size. For edge cases this function can return true // while the input is yielding slightly negative as part of the final // transaction. -func inputYieldsPositively(credit *wtxmgr.Credit, feeRatePerKb btcutil.Amount) bool { +func inputYieldsPositively(credit *wire.TxOut, + feeRatePerKb btcutil.Amount) bool { + inputSize := txsizes.GetMinInputVirtualSize(credit.PkScript) inputFee := feeRatePerKb * btcutil.Amount(inputSize) / 1000 - return inputFee < credit.Amount + return inputFee < btcutil.Amount(credit.Value) } // addrMgrWithChangeSource returns the address manager bucket and a change @@ -446,3 +442,57 @@ func validateMsgTx(tx *wire.MsgTx, prevScripts [][]byte, } return nil } + +// sortByAmount is a generic sortable type for sorting coins by their amount. +type sortByAmount []Coin + +func (s sortByAmount) Len() int { return len(s) } +func (s sortByAmount) Less(i, j int) bool { + return s[i].Value < s[j].Value +} +func (s sortByAmount) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// LargestFirstCoinSelector is an implementation of the CoinSelectionStrategy +// that always selects the largest coins first. +type LargestFirstCoinSelector struct{} + +// ArrangeCoins takes a list of coins and arranges them according to the +// specified coin selection strategy and fee rate. +func (*LargestFirstCoinSelector) ArrangeCoins(eligible []Coin, + _ btcutil.Amount) ([]Coin, error) { + + sort.Sort(sort.Reverse(sortByAmount(eligible))) + + return eligible, nil +} + +// RandomCoinSelector is an implementation of the CoinSelectionStrategy that +// selects coins at random. This prevents the creation of ever smaller UTXOs +// over time that may never become economical to spend. +type RandomCoinSelector struct{} + +// ArrangeCoins takes a list of coins and arranges them according to the +// specified coin selection strategy and fee rate. +func (*RandomCoinSelector) ArrangeCoins(eligible []Coin, + feeSatPerKb btcutil.Amount) ([]Coin, error) { + + // Skip inputs that do not raise the total transaction output + // value at the requested fee rate. + positivelyYielding := make([]Coin, 0, len(eligible)) + for _, output := range eligible { + output := output + + if !inputYieldsPositively(&output.TxOut, feeSatPerKb) { + continue + } + + positivelyYielding = append(positivelyYielding, output) + } + + rand.Shuffle(len(positivelyYielding), func(i, j int) { + positivelyYielding[i], positivelyYielding[j] = + positivelyYielding[j], positivelyYielding[i] + }) + + return positivelyYielding, nil +} diff --git a/wallet/createtx_test.go b/wallet/createtx_test.go index df2cd19bfd..55577604aa 100644 --- a/wallet/createtx_test.go +++ b/wallet/createtx_test.go @@ -216,8 +216,8 @@ func TestInputYield(t *testing.T) { pkScript, err := txscript.PayToAddrScript(addr) require.NoError(t, err) - credit := &wtxmgr.Credit{ - Amount: 1000, + credit := &wire.TxOut{ + Value: 1000, PkScript: pkScript, } diff --git a/wallet/wallet.go b/wallet/wallet.go index efe92e3943..5814f0db4a 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -89,17 +89,33 @@ var ( wtxmgrNamespaceKey = []byte("wtxmgr") ) -type CoinSelectionStrategy int +// Coin represents a spendable UTXO which is available for coin selection. +type Coin struct { + wire.TxOut -const ( + wire.OutPoint +} + +// CoinSelectionStrategy is an interface that represents a coin selection +// strategy. A coin selection strategy is responsible for ordering, shuffling or +// filtering a list of coins before they are passed to the coin selection +// algorithm. +type CoinSelectionStrategy interface { + // ArrangeCoins takes a list of coins and arranges them according to the + // specified coin selection strategy and fee rate. + ArrangeCoins(eligible []Coin, feeSatPerKb btcutil.Amount) ([]Coin, + error) +} + +var ( // CoinSelectionLargest always picks the largest available utxo to add // to the transaction next. - CoinSelectionLargest CoinSelectionStrategy = iota + CoinSelectionLargest CoinSelectionStrategy = &LargestFirstCoinSelector{} // CoinSelectionRandom randomly selects the next utxo to add to the // transaction. This strategy prevents the creation of ever smaller // utxos over time. - CoinSelectionRandom + CoinSelectionRandom CoinSelectionStrategy = &RandomCoinSelector{} ) // Wallet is a structure containing all the components for a