Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use selected Utxos while crafting txn #912

Merged
merged 5 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 80 additions & 23 deletions wallet/createtx.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,33 @@ func makeInputSource(eligible []Coin) txauthor.InputSource {
}
}

// constantInputSource creates an input source function that always returns the
// static set of user-selected UTXOs.
func constantInputSource(eligible []wtxmgr.Credit) txauthor.InputSource {
guggero marked this conversation as resolved.
Show resolved Hide resolved
// Current inputs and their total value. These won't change over
// different invocations as we want our inputs to remain static since
// they're selected by the user.
currentTotal := btcutil.Amount(0)
currentInputs := make([]*wire.TxIn, 0, len(eligible))
currentScripts := make([][]byte, 0, len(eligible))
currentInputValues := make([]btcutil.Amount, 0, len(eligible))

for _, credit := range eligible {
nextInput := wire.NewTxIn(&credit.OutPoint, nil, nil)
currentTotal += credit.Amount
currentInputs = append(currentInputs, nextInput)
currentScripts = append(currentScripts, credit.PkScript)
currentInputValues = append(currentInputValues, credit.Amount)
}

return func(target btcutil.Amount) (btcutil.Amount, []*wire.TxIn,
[]btcutil.Amount, [][]byte, error) {

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

// secretSource is an implementation of txauthor.SecretSource for the wallet's
// address manager.
type secretSource struct {
Expand Down Expand Up @@ -112,8 +139,8 @@ func (s secretSource) GetScript(addr btcutil.Address) ([]byte, error) {
func (w *Wallet) txToOutputs(outputs []*wire.TxOut,
coinSelectKeyScope, changeKeyScope *waddrmgr.KeyScope,
account uint32, minconf int32, feeSatPerKb btcutil.Amount,
coinSelectionStrategy CoinSelectionStrategy, dryRun bool) (
*txauthor.AuthoredTx, error) {
strategy CoinSelectionStrategy, dryRun bool,
selectedUtxos []wire.OutPoint) (*txauthor.AuthoredTx, error) {

chainClient, err := w.requireChainClient()
if err != nil {
Expand All @@ -127,8 +154,8 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut,
}

// Fall back to default coin selection strategy if none is supplied.
if coinSelectionStrategy == nil {
coinSelectionStrategy = CoinSelectionLargest
if strategy == nil {
strategy = CoinSelectionLargest
}

var tx *txauthor.AuthoredTx
Expand All @@ -147,27 +174,57 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut,
return err
}

// 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,
var inputSource txauthor.InputSource
if len(selectedUtxos) > 0 {
eligibleByOutpoint := make(
map[wire.OutPoint]wtxmgr.Credit,
)

for _, e := range eligible {
eligibleByOutpoint[e.OutPoint] = e
}
}
arrangedCoins, err := coinSelectionStrategy.ArrangeCoins(
wrappedEligible, feeSatPerKb,
)
if err != nil {
return err
}

inputSource := makeInputSource(arrangedCoins)
var eligibleSelectedUtxo []wtxmgr.Credit
for _, outpoint := range selectedUtxos {
e, ok := eligibleByOutpoint[outpoint]

if !ok {
return fmt.Errorf("selected outpoint "+
"not eligible for "+
"spending: %v", outpoint)
}
eligibleSelectedUtxo = append(
eligibleSelectedUtxo, e,
)
}

inputSource = constantInputSource(eligibleSelectedUtxo)

} else {
// 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,
}
}

arrangedCoins, err := strategy.ArrangeCoins(
wrappedEligible, feeSatPerKb,
)
if err != nil {
return err
}
inputSource = makeInputSource(arrangedCoins)
}

tx, err = txauthor.NewUnsignedTransaction(
outputs, feeSatPerKb, inputSource, changeSource,
Expand Down
93 changes: 90 additions & 3 deletions wallet/createtx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func TestTxToOutputsDryRun(t *testing.T) {
// database us not inflated.
dryRunTx, err := w.txToOutputs(
txOuts, nil, nil, 0, 1, 1000, CoinSelectionLargest, true,
nil,
)
if err != nil {
t.Fatalf("unable to author tx: %v", err)
Expand All @@ -96,6 +97,7 @@ func TestTxToOutputsDryRun(t *testing.T) {

dryRunTx2, err := w.txToOutputs(
txOuts, nil, nil, 0, 1, 1000, CoinSelectionLargest, true,
nil,
)
if err != nil {
t.Fatalf("unable to author tx: %v", err)
Expand Down Expand Up @@ -131,6 +133,7 @@ func TestTxToOutputsDryRun(t *testing.T) {
// to the database.
tx, err := w.txToOutputs(
txOuts, nil, nil, 0, 1, 1000, CoinSelectionLargest, false,
nil,
)
if err != nil {
t.Fatalf("unable to author tx: %v", err)
Expand Down Expand Up @@ -280,7 +283,7 @@ func TestTxToOutputsRandom(t *testing.T) {
createTx := func() *txauthor.AuthoredTx {
tx, err := w.txToOutputs(
txOuts, nil, nil, 0, 1, feeSatPerKb,
CoinSelectionRandom, true,
CoinSelectionRandom, true, nil,
)
require.NoError(t, err)
return tx
Expand Down Expand Up @@ -352,7 +355,7 @@ func TestCreateSimpleCustomChange(t *testing.T) {
}
tx1, err := w.txToOutputs(
[]*wire.TxOut{targetTxOut}, nil, nil, 0, 1, 1000,
CoinSelectionLargest, true,
CoinSelectionLargest, true, nil,
)
require.NoError(t, err)

Expand All @@ -378,7 +381,7 @@ func TestCreateSimpleCustomChange(t *testing.T) {
tx2, err := w.txToOutputs(
[]*wire.TxOut{targetTxOut}, &waddrmgr.KeyScopeBIP0086,
&waddrmgr.KeyScopeBIP0084, 0, 1, 1000, CoinSelectionLargest,
true,
true, nil,
)
require.NoError(t, err)

Expand All @@ -399,3 +402,87 @@ func TestCreateSimpleCustomChange(t *testing.T) {
require.Equal(t, scriptType, txscript.WitnessV0PubKeyHashTy)
}
}

// TestSelectUtxosTxoToOutpoint tests that it is possible to use passed
// selected utxos to craft a transaction in `txToOutpoint`.
func TestSelectUtxosTxoToOutpoint(t *testing.T) {
t.Parallel()

w, cleanup := testWallet(t)
defer cleanup()

// First, we'll make a P2TR and a P2WKH address to send some coins to.
p2wkhAddr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0084)
require.NoError(t, err)

p2trAddr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0086)
require.NoError(t, err)

// We'll now make a transaction that'll send coins to both outputs,
// then "credit" the wallet for that send.
p2wkhScript, err := txscript.PayToAddrScript(p2wkhAddr)
require.NoError(t, err)

p2trScript, err := txscript.PayToAddrScript(p2trAddr)
require.NoError(t, err)

incomingTx := &wire.MsgTx{
TxIn: []*wire.TxIn{
{},
},
TxOut: []*wire.TxOut{
wire.NewTxOut(1_000_000, p2wkhScript),
wire.NewTxOut(2_000_000, p2trScript),
wire.NewTxOut(3_000_000, p2trScript),
wire.NewTxOut(7_000_000, p2trScript),
},
}
addUtxo(t, w, incomingTx)

// We expect 4 unspent utxos.
unspent, err := w.ListUnspent(0, 80, "")
require.NoError(t, err, "unexpected error while calling "+
"list unspent")

require.Len(t, unspent, 4, "expected 4 unspent "+
"utxos")

selectUtxos := []wire.OutPoint{
{
Hash: incomingTx.TxHash(),
Index: 1,
},
{
Hash: incomingTx.TxHash(),
Index: 2,
},
}

// Test by sending 200_000.
targetTxOut := &wire.TxOut{
Value: 200_000,
PkScript: p2trScript,
}
tx1, err := w.txToOutputs(
[]*wire.TxOut{targetTxOut}, nil, nil, 0, 1, 1000,
CoinSelectionLargest, true, selectUtxos,
)
require.NoError(t, err)

// We expect all and only our select utxos to be input in this
// transaction.
require.Len(t, tx1.Tx.TxIn, len(selectUtxos))

lookupSelectUtxos := make(map[wire.OutPoint]struct{})
for _, utxo := range selectUtxos {
lookupSelectUtxos[utxo] = struct{}{}
}

for _, tx := range tx1.Tx.TxIn {
_, ok := lookupSelectUtxos[tx.PreviousOutPoint]
require.True(t, ok, "unexpected outpoint in txin")
}

// Expect two outputs, change and the actual payment to the address.
require.Len(t, tx1.Tx.TxOut, 2)
}
27 changes: 0 additions & 27 deletions wallet/psbt.go
Original file line number Diff line number Diff line change
Expand Up @@ -567,30 +567,3 @@ func PsbtPrevOutputFetcher(packet *psbt.Packet) *txscript.MultiPrevOutFetcher {

return fetcher
}

// constantInputSource creates an input source function that always returns the
// static set of user-selected UTXOs.
func constantInputSource(eligible []wtxmgr.Credit) txauthor.InputSource {
// Current inputs and their total value. These won't change over
// different invocations as we want our inputs to remain static since
// they're selected by the user.
currentTotal := btcutil.Amount(0)
currentInputs := make([]*wire.TxIn, 0, len(eligible))
currentScripts := make([][]byte, 0, len(eligible))
currentInputValues := make([]btcutil.Amount, 0, len(eligible))

for _, credit := range eligible {
nextInput := wire.NewTxIn(&credit.OutPoint, nil, nil)
currentTotal += credit.Amount
currentInputs = append(currentInputs, nextInput)
currentScripts = append(currentScripts, credit.PkScript)
currentInputValues = append(currentInputValues, credit.Amount)
}

return func(target btcutil.Amount) (btcutil.Amount, []*wire.TxIn,
[]btcutil.Amount, [][]byte, error) {

return currentTotal, currentInputs, currentInputValues,
currentScripts, nil
}
}
46 changes: 42 additions & 4 deletions wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,7 @@ type (
coinSelectionStrategy CoinSelectionStrategy
dryRun bool
resp chan createTxResponse
selectUtxos []wire.OutPoint
}
createTxResponse struct {
tx *txauthor.AuthoredTx
Expand Down Expand Up @@ -1240,7 +1241,7 @@ out:
tx, err := w.txToOutputs(
txr.outputs, txr.coinSelectKeyScope, txr.changeKeyScope,
txr.account, txr.minconf, txr.feeSatPerKB,
txr.coinSelectionStrategy, txr.dryRun,
txr.coinSelectionStrategy, txr.dryRun, txr.selectUtxos,
)

release()
Expand All @@ -1257,6 +1258,7 @@ out:
// scope, which otherwise will default to the specified coin selection scope.
type txCreateOptions struct {
changeKeyScope *waddrmgr.KeyScope
selectUtxos []wire.OutPoint
}

// TxCreateOption is a set of optional arguments to modify the tx creation
Expand All @@ -1279,6 +1281,14 @@ func WithCustomChangeScope(changeScope *waddrmgr.KeyScope) TxCreateOption {
}
}

// WithCustomSelectUtxos is used to specify the inputs to be used while
// creating txns.
func WithCustomSelectUtxos(utxos []wire.OutPoint) TxCreateOption {
guggero marked this conversation as resolved.
Show resolved Hide resolved
return func(opts *txCreateOptions) {
opts.selectUtxos = utxos
}
}

// CreateSimpleTx creates a new signed transaction spending unspent outputs with
// at least minconf confirmations spending to any number of address/amount
// pairs. Only unspent outputs belonging to the given key scope and account will
Expand Down Expand Up @@ -1322,6 +1332,7 @@ func (w *Wallet) CreateSimpleTx(coinSelectKeyScope *waddrmgr.KeyScope,
coinSelectionStrategy: coinSelectionStrategy,
dryRun: dryRun,
resp: make(chan createTxResponse),
selectUtxos: opts.selectUtxos,
}
w.createTxRequests <- req
resp := <-req.resp
Expand Down Expand Up @@ -3382,8 +3393,33 @@ func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcu
// returns the transaction upon success.
func (w *Wallet) SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
guggero marked this conversation as resolved.
Show resolved Hide resolved
account uint32, minconf int32, satPerKb btcutil.Amount,
coinSelectionStrategy CoinSelectionStrategy, label string) (
*wire.MsgTx, error) {
coinSelectionStrategy CoinSelectionStrategy, label string) (*wire.MsgTx,
error) {

return w.sendOutputs(
outputs, keyScope, account, minconf, satPerKb,
coinSelectionStrategy, label,
)
}

// SendOutputsWithInput creates and sends payment transactions using the
// provided selected utxos. It returns the transaction upon success.
func (w *Wallet) SendOutputsWithInput(outputs []*wire.TxOut,
keyScope *waddrmgr.KeyScope,
account uint32, minconf int32, satPerKb btcutil.Amount,
coinSelectionStrategy CoinSelectionStrategy, label string,
selectedUtxos []wire.OutPoint) (*wire.MsgTx, error) {

return w.sendOutputs(outputs, keyScope, account, minconf, satPerKb,
coinSelectionStrategy, label, selectedUtxos...)
}

// sendOutputs creates and sends payment transactions. It returns the
// transaction upon success.
func (w *Wallet) sendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
account uint32, minconf int32, satPerKb btcutil.Amount,
coinSelectionStrategy CoinSelectionStrategy, label string,
selectedUtxos ...wire.OutPoint) (*wire.MsgTx, error) {

// Ensure the outputs to be created adhere to the network's consensus
// rules.
Expand All @@ -3402,7 +3438,9 @@ func (w *Wallet) SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
// been confirmed.
createdTx, err := w.CreateSimpleTx(
keyScope, account, outputs, minconf, satPerKb,
coinSelectionStrategy, false,
coinSelectionStrategy, false, WithCustomSelectUtxos(
selectedUtxos,
),
)
if err != nil {
return nil, err
Expand Down
Loading