Skip to content
Open
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
96 changes: 85 additions & 11 deletions wallet/address_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,21 @@ import (

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
)

var (
// ErrDerivationPathNotFound is returned when the derivation path for a
// given script cannot be found. This may be because the script does
// not belong to the wallet, is imported, or is not a pubkey-based
// script.
ErrDerivationPathNotFound = errors.New("derivation path not found")

// ErrUnknownAddrType is an error returned when a wallet function is
// called with an unknown address type.
ErrUnknownAddrType = errors.New("unknown address type")
Expand All @@ -39,6 +47,10 @@ var (
"address is not a p2wkh or np2wkh address",
)

// ErrUnableToExtractAddress is returned when an address cannot be
// extracted from a pkscript.
ErrUnableToExtractAddress = errors.New("unable to extract address")

// errStopIteration is a special error used to stop the iteration in
// ForEachAccountAddress.
errStopIteration = errors.New("stop iteration")
Expand All @@ -57,7 +69,7 @@ type AddressProperty struct {
// Script represents the script information required to spend a UTXO.
type Script struct {
// Addr is the managed address of the UTXO.
Addr waddrmgr.ManagedPubKeyAddress
Addr waddrmgr.ManagedAddress

// WitnessProgram is the witness program of the UTXO.
WitnessProgram []byte
Expand Down Expand Up @@ -113,6 +125,11 @@ type AddressManager interface {
// ScriptForOutput returns the address, witness program, and redeem
// script for a given UTXO.
ScriptForOutput(ctx context.Context, output wire.TxOut) (Script, error)

// GetDerivationInfo returns the BIP-32 derivation path for a given
// address.
GetDerivationInfo(ctx context.Context,
addr btcutil.Address) (*psbt.Bip32Derivation, error)
}

// A compile time check to ensure that Wallet implements the interface.
Expand Down Expand Up @@ -678,20 +695,28 @@ func (w *Wallet) ImportTaprootScript(_ context.Context,
// - The operation is dominated by the database lookup for the address, which
// is typically fast (O(log N) or O(1) with indexing). The script
// generation is a constant-time operation.
func (w *Wallet) ScriptForOutput(_ context.Context, output wire.TxOut) (
func (w *Wallet) ScriptForOutput(ctx context.Context, output wire.TxOut) (
Script, error) {

// First make sure we can sign for the input by making sure the script
// in the UTXO belongs to our wallet and we have the private key for it.
walletAddr, err := w.fetchOutputAddr(output.PkScript)
// First, we'll extract the address from the output's pkScript.
addr := extractAddrFromPKScript(output.PkScript, w.chainParams)
if addr == nil {
return Script{}, fmt.Errorf("%w: from pkscript %x",
ErrUnableToExtractAddress, output.PkScript)
}

// We'll then use the address to look up the managed address from the
// database.
managedAddr, err := w.AddressInfo(ctx, addr)
if err != nil {
return Script{}, err
return Script{}, fmt.Errorf("unable to get address info "+
"for %s: %w", addr.String(), err)
}

pubKeyAddr, ok := walletAddr.(waddrmgr.ManagedPubKeyAddress)
pubKeyAddr, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress)
if !ok {
return Script{}, fmt.Errorf("%w: %s", ErrNotPubKeyAddress,
walletAddr.Address())
return Script{}, fmt.Errorf("%w: addr %s",
ErrNotPubKeyAddress, managedAddr.Address())
}

var (
Expand All @@ -702,7 +727,7 @@ func (w *Wallet) ScriptForOutput(_ context.Context, output wire.TxOut) (
switch {
// If we're spending p2wkh output nested within a p2sh output, then
// we'll need to attach a sigScript in addition to witness data.
case walletAddr.AddrType() == waddrmgr.NestedWitnessPubKey:
case managedAddr.AddrType() == waddrmgr.NestedWitnessPubKey:
pubKey := pubKeyAddr.PubKey()
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())

Expand Down Expand Up @@ -740,8 +765,57 @@ func (w *Wallet) ScriptForOutput(_ context.Context, output wire.TxOut) (
}

return Script{
Addr: pubKeyAddr,
Addr: managedAddr,
WitnessProgram: witnessProgram,
RedeemScript: sigScript,
}, nil
}

// GetDerivationInfo returns the BIP-32 derivation path for a given address.
func (w *Wallet) GetDerivationInfo(ctx context.Context,
addr btcutil.Address) (*psbt.Bip32Derivation, error) {

// We'll use the address to look up the derivation path.
managedAddr, err := w.AddressInfo(ctx, addr)
if err != nil {
return nil, err
}

// We only care about pubkey addresses, as they are the only
// ones with derivation paths.
pubKeyAddr, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress)
if !ok {
return nil, fmt.Errorf("%w: addr=%v not found",
ErrDerivationPathNotFound, addr)
}

// Imported addresses don't have derivation paths.
if pubKeyAddr.Imported() {
return nil, fmt.Errorf("%w: addr=%v is imported",
ErrDerivationPathNotFound, addr)
}

// Get the derivation info.
keyScope, derivPath, ok := pubKeyAddr.DerivationInfo()
if !ok {
return nil, fmt.Errorf("%w: derivation info not found for %v",
ErrDerivationPathNotFound, addr)
}

// Get the public key.
pubKey := pubKeyAddr.PubKey()

derivationInfo := &psbt.Bip32Derivation{
PubKey: pubKey.SerializeCompressed(),
MasterKeyFingerprint: derivPath.MasterKeyFingerprint,
Bip32Path: []uint32{
keyScope.Purpose + hdkeychain.HardenedKeyStart,
keyScope.Coin + hdkeychain.HardenedKeyStart,
derivPath.Account,
derivPath.Branch,
derivPath.Index,
},
}

return derivationInfo, nil
}
120 changes: 120 additions & 0 deletions wallet/address_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/waddrmgr"
Expand Down Expand Up @@ -294,6 +295,125 @@ func TestAddressInfo(t *testing.T) {
require.Equal(t, waddrmgr.WitnessPubKey, intInfo.AddrType())
}

// TestGetDerivationInfoExternalAddressSuccess tests that we can successfully
// get the derivation info for an external address.
func TestGetDerivationInfoExternalAddressSuccess(t *testing.T) {
t.Parallel()

// Arrange: Create a new test wallet and a new p2wkh address to test
// with.
w := testWallet(t)
addr, err := w.NewAddress(
t.Context(), "default", waddrmgr.WitnessPubKey, false,
)
require.NoError(t, err)

// Act: Get the derivation info for the address.
derivationInfo, err := w.GetDerivationInfo(t.Context(), addr)

// Assert: Check that the correct derivation info is returned.
require.NoError(t, err)
require.NotNil(t, derivationInfo)

addrInfo, err := w.AddressInfo(t.Context(), addr)
require.NoError(t, err)

pubKeyAddr, ok := addrInfo.(waddrmgr.ManagedPubKeyAddress)
require.True(t, ok)

pubKey := pubKeyAddr.PubKey()
keyScope, derivPath, ok := pubKeyAddr.DerivationInfo()
require.True(t, ok)

expectedPath := []uint32{
keyScope.Purpose + hdkeychain.HardenedKeyStart,
keyScope.Coin + hdkeychain.HardenedKeyStart,
derivPath.Account,
derivPath.Branch,
derivPath.Index,
}

require.Equal(t, pubKey.SerializeCompressed(), derivationInfo.PubKey)
require.Equal(
t, derivPath.MasterKeyFingerprint,
derivationInfo.MasterKeyFingerprint,
)
require.Equal(t, expectedPath, derivationInfo.Bip32Path)
}

// TestGetDerivationInfoInternalAddressSuccess tests that we can successfully
// get the derivation info for an internal address.
func TestGetDerivationInfoInternalAddressSuccess(t *testing.T) {
t.Parallel()

// Arrange: Create a new test wallet and a new p2wkh change address to
// test with.
w := testWallet(t)
addr, err := w.NewAddress(
t.Context(), "default", waddrmgr.WitnessPubKey, true,
)
require.NoError(t, err)

// Act: Get the derivation info for the address.
derivationInfo, err := w.GetDerivationInfo(t.Context(), addr)

// Assert: Check that the correct derivation info is returned.
require.NoError(t, err)
require.NotNil(t, derivationInfo)

addrInfo, err := w.AddressInfo(t.Context(), addr)
require.NoError(t, err)

pubKeyAddr, ok := addrInfo.(waddrmgr.ManagedPubKeyAddress)
require.True(t, ok)
keyScope, derivPath, ok := pubKeyAddr.DerivationInfo()
require.True(t, ok)

expectedPath := []uint32{
keyScope.Purpose + hdkeychain.HardenedKeyStart,
keyScope.Coin + hdkeychain.HardenedKeyStart,
derivPath.Account,
derivPath.Branch,
derivPath.Index,
}
require.Equal(t, expectedPath, derivationInfo.Bip32Path)
require.Equal(t, uint32(1), derivPath.Branch)
}

// TestGetDerivationInfoNoDerivationInfo tests that we get an error when trying
// to get the derivation info for an address that is not in the wallet or is
// imported.
func TestGetDerivationInfoNoDerivationInfo(t *testing.T) {
t.Parallel()

// Arrange: Create a new test wallet and a key and address that is not
// in the wallet.
w := testWallet(t)
privKey, err := btcec.NewPrivateKey()
require.NoError(t, err)

pubKey := privKey.PubKey()
addr, err := btcutil.NewAddressWitnessPubKeyHash(
btcutil.Hash160(pubKey.SerializeCompressed()),
w.chainParams,
)
require.NoError(t, err)

// Act & Assert: Check that we get an error for an address not in the
// wallet.
_, err = w.GetDerivationInfo(t.Context(), addr)
require.Error(t, err)

// Arrange: Import the key as a watch-only address.
err = w.ImportPublicKey(t.Context(), pubKey, waddrmgr.WitnessPubKey)
require.NoError(t, err)

// Act & Assert: Check that we still get an error because it's an
// imported key.
_, err = w.GetDerivationInfo(t.Context(), addr)
require.ErrorIs(t, err, ErrDerivationPathNotFound)
}

// TestListAddresses tests the ListAddresses method to ensure it returns the
// correct addresses and balances for a given account.
func TestListAddresses(t *testing.T) {
Expand Down
85 changes: 85 additions & 0 deletions wallet/deprecated.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
package wallet

import (
"context"
"fmt"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
)
Expand Down Expand Up @@ -150,3 +154,84 @@ func (w *Wallet) RenameAccountDeprecated(scope waddrmgr.KeyScope,

return err
}

// ScriptForOutputDeprecated returns the address, witness program and redeem
// script for a given UTXO. An error is returned if the UTXO does not
// belong to our wallet or it is not a managed pubKey address.
//
// Deprecated: Use AddressManager.ScriptForOutput instead.
func (w *Wallet) ScriptForOutputDeprecated(output *wire.TxOut) (
waddrmgr.ManagedPubKeyAddress, []byte, []byte, error) {

script, err := w.ScriptForOutput(context.Background(), *output)
if err != nil {
return nil, nil, nil, err
}

addr := script.Addr
pubKeyAddr, ok := addr.(waddrmgr.ManagedPubKeyAddress)
if !ok {
return nil, nil, nil, fmt.Errorf("%w: addr %s",
ErrNotPubKeyAddress, addr.Address())
}

return pubKeyAddr, script.WitnessProgram, script.RedeemScript, nil
}

// ComputeInputScript generates a complete InputScript for the passed
// transaction with the signature as defined within the passed
// SignDescriptor. This method is capable of generating the proper input
// script for both regular p2wkh output and p2wkh outputs nested within a
// regular p2sh output.
func (w *Wallet) ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut,
inputIndex int, sigHashes *txscript.TxSigHashes,
hashType txscript.SigHashType, tweaker PrivKeyTweaker) (wire.TxWitness,
[]byte, error) {

walletAddr, witnessProgram, sigScript, err :=
w.ScriptForOutputDeprecated(
output,
)
if err != nil {
return nil, nil, err
}

privKey, err := walletAddr.PrivKey()
if err != nil {
return nil, nil, err
}

// If we need to maybe tweak our private key, do it now.
if tweaker != nil {
privKey, err = tweaker(privKey)
if err != nil {
return nil, nil, err
}
}

// We need to produce a Schnorr signature for p2tr key spend addresses.
if txscript.IsPayToTaproot(output.PkScript) {
// We can now generate a valid witness which will allow us to
// spend this output.
witnessScript, err := txscript.TaprootWitnessSignature(
tx, sigHashes, inputIndex, output.Value,
output.PkScript, hashType, privKey,
)
if err != nil {
return nil, nil, err
}

return witnessScript, nil, nil
}

// Generate a valid witness stack for the input.
witnessScript, err := txscript.WitnessSignature(
tx, sigHashes, inputIndex, output.Value, witnessProgram,
hashType, privKey, true,
)
if err != nil {
return nil, nil, err
}

return witnessScript, sigScript, nil
}
Loading
Loading