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

wallet: break FetchInputInfo into two methods #945

Merged
merged 2 commits into from
Aug 9, 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
127 changes: 82 additions & 45 deletions wallet/utxos.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,86 +104,123 @@ 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.
//
// NOTE: This method is kept for compatibility.
func (w *Wallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx,
*wire.TxOut, *psbt.Bip32Derivation, int64, error) {

tx, txOut, confs, err := w.FetchOutpointInfo(prevOut)
if err != nil {
return nil, nil, nil, 0, err
}

derivation, err := w.FetchDerivationInfo(txOut.PkScript)
if err != nil {
return nil, nil, nil, 0, err
}

return tx, txOut, derivation, confs, nil
}

// fetchOutputAddr attempts to fetch the managed address corresponding to the
// passed output script. This function is used to look up the proper key which
// should be used to sign a specified input.
func (w *Wallet) fetchOutputAddr(script []byte) (waddrmgr.ManagedAddress, error) {
_, addrs, _, err := txscript.ExtractPkScriptAddrs(script, w.chainParams)
if err != nil {
return nil, err
}

// If the case of a multi-sig output, several address may be extracted.
// Therefore, we simply select the key for the first address we know
// of.
for _, addr := range addrs {
addr, err := w.AddressInfo(addr)
if err == nil {
return addr, nil
}
}

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, nil, 0, err
return nil, nil, 0, err
} else if txDetail == nil {
return nil, nil, nil, 0, ErrNotMine
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.
// 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 "+
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
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 "+
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,
}, &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
Value: txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].Value,
PkScript: pkScript,
}, confs, nil
}

// fetchOutputAddr attempts to fetch the managed address corresponding to the
// passed output script. This function is used to look up the proper key which
// should be used to sign a specified input.
func (w *Wallet) fetchOutputAddr(script []byte) (waddrmgr.ManagedAddress, error) {
_, addrs, _, err := txscript.ExtractPkScriptAddrs(script, w.chainParams)
// 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
}

// If the case of a multi-sig output, several address may be extracted.
// Therefore, we simply select the key for the first address we know
// of.
for _, addr := range addrs {
addr, err := w.AddressInfo(addr)
if err == nil {
return addr, nil
}
pubKeyAddr, ok := addr.(waddrmgr.ManagedPubKeyAddress)
if !ok {
return nil, ErrNotMine
}
keyScope, derivationPath, _ := pubKeyAddr.DerivationInfo()

return nil, ErrNotMine
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
}
73 changes: 73 additions & 0 deletions wallet/utxos_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -90,3 +91,75 @@ 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)
}

// 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])
}
Loading