Skip to content

Commit

Permalink
wallet: export coin selection strategy code
Browse files Browse the repository at this point in the history
  • Loading branch information
guggero committed Jan 15, 2024
1 parent 5df09dd commit a36ae6f
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 57 deletions.
160 changes: 109 additions & 51 deletions wallet/createtx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 []SelectableCoin) 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))
Expand All @@ -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.Output()
outpoint := nextCredit.PreviousOutPoint()
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
}
}

Expand Down Expand Up @@ -139,41 +141,22 @@ 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,
)
}

rand.Shuffle(len(positivelyYielding), func(i, j int) {
positivelyYielding[i], positivelyYielding[j] =
positivelyYielding[j], positivelyYielding[i]
})

inputSource = makeInputSource(positivelyYielding)
// 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([]SelectableCoin, len(eligible))
for i := range eligible {
wrappedEligible[i] = &SelectableCredit{&eligible[i]}
}
arrangedCoins, err := coinSelectionStrategy.ArrangeCoins(
wrappedEligible, feeSatPerKb,
)
if err != nil {
return err
}

inputSource := makeInputSource(arrangedCoins)

tx, err = txauthor.NewUnsignedTransaction(
outputs, feeSatPerKb, inputSource, changeSource,
)
Expand Down Expand Up @@ -337,11 +320,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
Expand Down Expand Up @@ -446,3 +431,76 @@ func validateMsgTx(tx *wire.MsgTx, prevScripts [][]byte,
}
return nil
}

// sortByAmount is a generic sortable type for sorting coins by their amount.
type sortByAmount []SelectableCoin

func (s sortByAmount) Len() int { return len(s) }
func (s sortByAmount) Less(i, j int) bool {
return s[i].Output().Value < s[j].Output().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 []SelectableCoin,
_ btcutil.Amount) ([]SelectableCoin, 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 []SelectableCoin,
feeSatPerKb btcutil.Amount) ([]SelectableCoin, error) {

// Skip inputs that do not raise the total transaction output
// value at the requested fee rate.
positivelyYielding := make([]SelectableCoin, 0, len(eligible))
for _, output := range eligible {
output := output

if !inputYieldsPositively(output.Output(), 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
}

// SelectableCredit is a wrapper around a wtxmgr.Credit that implements the
// SelectableCoin interface.
type SelectableCredit struct {
*wtxmgr.Credit
}

// Output returns the transaction output of the credit.
func (s *SelectableCredit) Output() *wire.TxOut {
return &wire.TxOut{
Value: int64(s.Amount),
PkScript: s.PkScript,
}
}

// PreviousOutPoint returns the previous outpoint of the coin.
func (s *SelectableCredit) PreviousOutPoint() wire.OutPoint {
return s.OutPoint
}
4 changes: 2 additions & 2 deletions wallet/createtx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down
27 changes: 23 additions & 4 deletions wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,36 @@ var (
wtxmgrNamespaceKey = []byte("wtxmgr")
)

type CoinSelectionStrategy int
// SelectableCoin is an interface that represents a coin that can be selected
// for coin selection.
type SelectableCoin interface {
// Output returns the transaction output of the coin.
Output() *wire.TxOut

const (
// PreviousOutPoint returns the previous outpoint of the coin.
PreviousOutPoint() 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 []SelectableCoin,
feeSatPerKb btcutil.Amount) ([]SelectableCoin, 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
Expand Down

0 comments on commit a36ae6f

Please sign in to comment.