Skip to content

Commit

Permalink
Merge pull request btcsuite#900 from guggero/export-coin-selection
Browse files Browse the repository at this point in the history
wallet: export coin selection strategy code for re-use
  • Loading branch information
Roasbeef authored and buck54321 committed Apr 20, 2024
1 parent ae5fdc7 commit c072934
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 105 deletions.
2 changes: 1 addition & 1 deletion chain/bitcoind_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func (c *BitcoindClient) GetRawTransactionVerbose(
return c.chainConn.client.GetRawTransactionVerbose(hash)
}

// GetRawTransaction returns a `btcutil.Tx` from the tx hash.
// GetRawTransaction returns a `ltcutil.Tx` from the tx hash.
func (c *BitcoindClient) GetRawTransaction(
hash *chainhash.Hash) (*ltcutil.Tx, error) {

Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ require (
go.etcd.io/bbolt v1.3.5
golang.org/x/crypto v0.7.0
golang.org/x/net v0.8.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/term v0.6.0
google.golang.org/grpc v1.38.0
)
Expand Down
1 change: 0 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -982,7 +982,6 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down
158 changes: 106 additions & 52 deletions wallet/createtx.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package wallet

import (
"errors"
"fmt"
"math/rand"
"sort"
Expand All @@ -21,16 +22,8 @@ import (
"github.com/ltcsuite/ltcd/wire"
)

// 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 := ltcutil.Amount(0)
currentInputs := make([]*wire.TxIn, 0, len(eligible))
Expand All @@ -41,15 +34,25 @@ func makeInputSource(eligible []wtxmgr.Credit) txauthor.InputSource {
[]ltcutil.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 += ltcutil.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,
ltcutil.Amount(prevOut.Value),
)
}
return currentTotal, currentInputs, currentInputValues, currentScripts, nil

return currentTotal, currentInputs, currentInputValues,
currentScripts, nil
}
}

Expand Down Expand Up @@ -123,6 +126,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(
Expand All @@ -139,40 +147,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,
Expand Down Expand Up @@ -261,7 +256,7 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut,

return nil
})
if err != nil && err != walletdb.ErrDryRunRollBack {
if err != nil && !errors.Is(err, walletdb.ErrDryRunRollBack) {
return nil, err
}

Expand Down Expand Up @@ -290,7 +285,7 @@ func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx,
output := &unspent[i]

// Only include this output if it meets the required number of
// confirmations. Coinbase transactions must have have reached
// confirmations. Coinbase transactions must have reached
// maturity before their outputs may be spent.
if !confirmed(minconf, output.Height, bs.Height) {
continue
Expand Down Expand Up @@ -337,11 +332,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 ltcutil.Amount) bool {
func inputYieldsPositively(credit *wire.TxOut,
feeRatePerKb ltcutil.Amount) bool {

inputSize := txsizes.GetMinInputVirtualSize(credit.PkScript)
inputFee := feeRatePerKb * ltcutil.Amount(inputSize) / 1000

return inputFee < credit.Amount
return inputFee < ltcutil.Amount(credit.Value)
}

// addrMgrWithChangeSource returns the address manager bucket and a change
Expand Down Expand Up @@ -386,6 +383,9 @@ func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx,
scriptSize = txsizes.P2WPKHPkScriptSize
case waddrmgr.TaprootPubKey:
scriptSize = txsizes.P2TRPkScriptSize
default:
return nil, nil, fmt.Errorf("unsupported address type: %v",
addrType)
}

newChangeScript := func() ([]byte, error) {
Expand Down Expand Up @@ -440,3 +440,57 @@ func validateMsgTx(tx *wire.MsgTx, prevScripts [][]byte, inputValues []ltcutil.A
}
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,
_ ltcutil.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 ltcutil.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
}
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
Loading

0 comments on commit c072934

Please sign in to comment.