From 10e29aa15fb1eef4fb427fc19f15c4aca9bc2335 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 8 Aug 2024 14:59:43 +0800 Subject: [PATCH 1/2] wallet: add new method `FetchOutpointInfo` A new method `FetchOutpointInfo` is added to query a given outpoint without constructing its derivation info. --- wallet/utxos.go | 105 ++++++++++++++++++++++++++----------------- wallet/utxos_test.go | 38 ++++++++++++++++ 2 files changed, 102 insertions(+), 41 deletions(-) diff --git a/wallet/utxos.go b/wallet/utxos.go index 8e7d8d23aa..91487dd842 100644 --- a/wallet/utxos.go +++ b/wallet/utxos.go @@ -104,66 +104,42 @@ func (w *Wallet) UnspentOutputs(policy OutputSelectionPolicy) ([]*TransactionOut // FetchInputInfo queries for the wallet's knowledge of the passed outpoint. If // the wallet determines this output is under its control, then the original -// full transaction, the target txout and the number of confirmations are -// returned. Otherwise, a non-nil error value of ErrNotMine is returned instead. +// full transaction, the target txout, the derivation info and the number of +// confirmations are returned. Otherwise, a non-nil error value of ErrNotMine +// is returned instead. func (w *Wallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, *wire.TxOut, *psbt.Bip32Derivation, int64, error) { - // We manually look up the output within the tx store. - txid := &prevOut.Hash - txDetail, err := UnstableAPI(w).TxDetails(txid) + tx, txOut, confs, err := w.FetchOutpointInfo(prevOut) if err != nil { return nil, nil, nil, 0, err - } else if txDetail == nil { - return nil, nil, nil, 0, ErrNotMine } - // With the output retrieved, we'll make an additional check to ensure - // we actually have control of this output. We do this because the check - // above only guarantees that the transaction is somehow relevant to us, - // like in the event of us being the sender of the transaction. - numOutputs := uint32(len(txDetail.TxRecord.MsgTx.TxOut)) - if prevOut.Index >= numOutputs { - return nil, nil, nil, 0, fmt.Errorf("invalid output index %v for "+ - "transaction with %v outputs", prevOut.Index, - numOutputs) - } - pkScript := txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].PkScript + pkScript := txOut.PkScript addr, err := w.fetchOutputAddr(pkScript) if err != nil { return nil, nil, nil, 0, err } + pubKeyAddr, ok := addr.(waddrmgr.ManagedPubKeyAddress) if !ok { return nil, nil, nil, 0, ErrNotMine } keyScope, derivationPath, _ := pubKeyAddr.DerivationInfo() - // Determine the number of confirmations the output currently has. - _, currentHeight, err := w.chainClient.GetBestBlock() - if err != nil { - return nil, nil, nil, 0, fmt.Errorf("unable to retrieve current "+ - "height: %w", err) - } - confs := int64(0) - if txDetail.Block.Height != -1 { - confs = int64(currentHeight - txDetail.Block.Height) + derivation := &psbt.Bip32Derivation{ + PubKey: pubKeyAddr.PubKey().SerializeCompressed(), + MasterKeyFingerprint: derivationPath.MasterKeyFingerprint, + Bip32Path: []uint32{ + keyScope.Purpose + hdkeychain.HardenedKeyStart, + keyScope.Coin + hdkeychain.HardenedKeyStart, + derivationPath.Account, + derivationPath.Branch, + derivationPath.Index, + }, } - return &txDetail.TxRecord.MsgTx, &wire.TxOut{ - Value: txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].Value, - PkScript: pkScript, - }, &psbt.Bip32Derivation{ - PubKey: pubKeyAddr.PubKey().SerializeCompressed(), - MasterKeyFingerprint: derivationPath.MasterKeyFingerprint, - Bip32Path: []uint32{ - keyScope.Purpose + hdkeychain.HardenedKeyStart, - keyScope.Coin + hdkeychain.HardenedKeyStart, - derivationPath.Account, - derivationPath.Branch, - derivationPath.Index, - }, - }, confs, nil + return tx, txOut, derivation, confs, nil } // fetchOutputAddr attempts to fetch the managed address corresponding to the @@ -187,3 +163,50 @@ func (w *Wallet) fetchOutputAddr(script []byte) (waddrmgr.ManagedAddress, error) return nil, ErrNotMine } + +// FetchOutpointInfo queries for the wallet's knowledge of the passed outpoint. +// If the wallet determines this output is under its control, the original full +// transaction, the target txout and the number of confirmations are returned. +// Otherwise, a non-nil error value of ErrNotMine is returned instead. +func (w *Wallet) FetchOutpointInfo(prevOut *wire.OutPoint) (*wire.MsgTx, + *wire.TxOut, int64, error) { + + // We manually look up the output within the tx store. + txid := &prevOut.Hash + txDetail, err := UnstableAPI(w).TxDetails(txid) + if err != nil { + return nil, nil, 0, err + } else if txDetail == nil { + return nil, nil, 0, ErrNotMine + } + + // With the output retrieved, we'll make an additional check to ensure + // we actually have control of this output. We do this because the + // check above only guarantees that the transaction is somehow relevant + // to us, like in the event of us being the sender of the transaction. + numOutputs := uint32(len(txDetail.TxRecord.MsgTx.TxOut)) + if prevOut.Index >= numOutputs { + return nil, nil, 0, fmt.Errorf("invalid output index %v for "+ + "transaction with %v outputs", prevOut.Index, + numOutputs) + } + + pkScript := txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].PkScript + + // Determine the number of confirmations the output currently has. + _, currentHeight, err := w.chainClient.GetBestBlock() + if err != nil { + return nil, nil, 0, fmt.Errorf("unable to retrieve current "+ + "height: %w", err) + } + + confs := int64(0) + if txDetail.Block.Height != -1 { + confs = int64(currentHeight - txDetail.Block.Height) + } + + return &txDetail.TxRecord.MsgTx, &wire.TxOut{ + Value: txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].Value, + PkScript: pkScript, + }, confs, nil +} diff --git a/wallet/utxos_test.go b/wallet/utxos_test.go index 9290730c73..85cde530f7 100644 --- a/wallet/utxos_test.go +++ b/wallet/utxos_test.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/stretchr/testify/require" ) // TestFetchInputInfo checks that the wallet can gather information about an @@ -90,3 +91,40 @@ func TestFetchInputInfo(t *testing.T) { confirmations, 0-testBlockHeight) } } + +// TestFetchOutpointInfo checks that the wallet can gather information about an +// output based on the outpoint. +func TestFetchOutpointInfo(t *testing.T) { + t.Parallel() + + w, cleanup := testWallet(t) + defer cleanup() + + // Create an address we can use to send some coins to. + addr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0084) + require.NoError(t, err) + p2shAddr, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + + // Add an output paying to the wallet's address to the database. + utxOut := wire.NewTxOut(100000, p2shAddr) + incomingTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{}}, + TxOut: []*wire.TxOut{utxOut}, + } + addUtxo(t, w, incomingTx) + + // Look up the UTXO for the outpoint now and compare it to our + // expectations. + prevOut := &wire.OutPoint{ + Hash: incomingTx.TxHash(), + Index: 0, + } + tx, out, confirmations, err := w.FetchOutpointInfo(prevOut) + require.NoError(t, err) + + require.Equal(t, utxOut.PkScript, out.PkScript) + require.Equal(t, utxOut.Value, out.Value) + require.Equal(t, utxOut.PkScript, tx.TxOut[prevOut.Index].PkScript) + require.Equal(t, int64(0-testBlockHeight), confirmations) +} From 91de5836c104887448be305bee822d37b2de43d0 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 8 Aug 2024 15:13:23 +0800 Subject: [PATCH 2/2] wallet: add new method `FetchDerivationInfo` --- wallet/utxos.go | 54 ++++++++++++++++++++++++++++---------------- wallet/utxos_test.go | 35 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 20 deletions(-) diff --git a/wallet/utxos.go b/wallet/utxos.go index 91487dd842..9365f24f80 100644 --- a/wallet/utxos.go +++ b/wallet/utxos.go @@ -107,6 +107,8 @@ func (w *Wallet) UnspentOutputs(policy OutputSelectionPolicy) ([]*TransactionOut // full transaction, the target txout, the derivation info and the number of // confirmations are returned. Otherwise, a non-nil error value of ErrNotMine // is returned instead. +// +// NOTE: This method is kept for compatibility. func (w *Wallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, *wire.TxOut, *psbt.Bip32Derivation, int64, error) { @@ -115,30 +117,11 @@ func (w *Wallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, return nil, nil, nil, 0, err } - pkScript := txOut.PkScript - addr, err := w.fetchOutputAddr(pkScript) + derivation, err := w.FetchDerivationInfo(txOut.PkScript) if err != nil { return nil, nil, nil, 0, err } - pubKeyAddr, ok := addr.(waddrmgr.ManagedPubKeyAddress) - if !ok { - return nil, nil, nil, 0, ErrNotMine - } - keyScope, derivationPath, _ := pubKeyAddr.DerivationInfo() - - derivation := &psbt.Bip32Derivation{ - PubKey: pubKeyAddr.PubKey().SerializeCompressed(), - MasterKeyFingerprint: derivationPath.MasterKeyFingerprint, - Bip32Path: []uint32{ - keyScope.Purpose + hdkeychain.HardenedKeyStart, - keyScope.Coin + hdkeychain.HardenedKeyStart, - derivationPath.Account, - derivationPath.Branch, - derivationPath.Index, - }, - } - return tx, txOut, derivation, confs, nil } @@ -210,3 +193,34 @@ func (w *Wallet) FetchOutpointInfo(prevOut *wire.OutPoint) (*wire.MsgTx, PkScript: pkScript, }, confs, nil } + +// FetchDerivationInfo queries for the wallet's knowledge of the passed +// pkScript and constructs the derivation info and returns it. +func (w *Wallet) FetchDerivationInfo(pkScript []byte) (*psbt.Bip32Derivation, + error) { + + addr, err := w.fetchOutputAddr(pkScript) + if err != nil { + return nil, err + } + + pubKeyAddr, ok := addr.(waddrmgr.ManagedPubKeyAddress) + if !ok { + return nil, ErrNotMine + } + keyScope, derivationPath, _ := pubKeyAddr.DerivationInfo() + + derivation := &psbt.Bip32Derivation{ + PubKey: pubKeyAddr.PubKey().SerializeCompressed(), + MasterKeyFingerprint: derivationPath.MasterKeyFingerprint, + Bip32Path: []uint32{ + keyScope.Purpose + hdkeychain.HardenedKeyStart, + keyScope.Coin + hdkeychain.HardenedKeyStart, + derivationPath.Account, + derivationPath.Branch, + derivationPath.Index, + }, + } + + return derivation, nil +} diff --git a/wallet/utxos_test.go b/wallet/utxos_test.go index 85cde530f7..28797f4da1 100644 --- a/wallet/utxos_test.go +++ b/wallet/utxos_test.go @@ -128,3 +128,38 @@ func TestFetchOutpointInfo(t *testing.T) { require.Equal(t, utxOut.PkScript, tx.TxOut[prevOut.Index].PkScript) require.Equal(t, int64(0-testBlockHeight), confirmations) } + +// TestFetchDerivationInfo checks that the wallet can gather the derivation +// info about an output based on the pkScript. +func TestFetchDerivationInfo(t *testing.T) { + t.Parallel() + + w, cleanup := testWallet(t) + defer cleanup() + + // Create an address we can use to send some coins to. + addr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0084) + require.NoError(t, err) + p2shAddr, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + + // Add an output paying to the wallet's address to the database. + utxOut := wire.NewTxOut(100000, p2shAddr) + incomingTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{}}, + TxOut: []*wire.TxOut{utxOut}, + } + addUtxo(t, w, incomingTx) + + info, err := w.FetchDerivationInfo(utxOut.PkScript) + require.NoError(t, err) + + require.Len(t, info.Bip32Path, 5) + require.Equal(t, waddrmgr.KeyScopeBIP0084.Purpose+ + hdkeychain.HardenedKeyStart, info.Bip32Path[0]) + require.Equal(t, waddrmgr.KeyScopeBIP0084.Coin+ + hdkeychain.HardenedKeyStart, info.Bip32Path[1]) + require.EqualValues(t, hdkeychain.HardenedKeyStart, info.Bip32Path[2]) + require.Equal(t, uint32(0), info.Bip32Path[3]) + require.Equal(t, uint32(0), info.Bip32Path[4]) +}