lnwallet+lnrpc: add derivation info for P2TR change addrs

In some situations (for example in Taproot Assets), we need to be able
to prove that an address is a bare BIP-0086 address that doesn't commit
to any script. We can do that by providing the BIP-0032 derivation info
and internal key.
This commit is contained in:
Oliver Gugger 2024-02-09 13:45:36 +01:00
parent 094fdbfa72
commit fb20cd598e
No known key found for this signature in database
GPG key ID: 8E4256593F177720
4 changed files with 250 additions and 1 deletions

View file

@ -1758,6 +1758,34 @@ func (w *WalletKit) handleChange(packet *psbt.Packet, changeIndex int32,
return 0, fmt.Errorf("could not derive change script: %w", err)
}
// We need to add the derivation info for the change address in case it
// is a P2TR address. This is mostly to prove it's a bare BIP-0086
// address, which is required for some protocols (such as Taproot
// Assets).
pOut := psbt.POutput{}
_, isTaprootChangeAddr := changeAddr.(*btcutil.AddressTaproot)
if isTaprootChangeAddr {
changeAddrInfo, err := w.cfg.Wallet.AddressInfo(changeAddr)
if err != nil {
return 0, fmt.Errorf("could not get address info: %w",
err)
}
deriv, trDeriv, _, err := btcwallet.Bip32DerivationFromAddress(
changeAddrInfo,
)
if err != nil {
return 0, fmt.Errorf("could not get derivation info: "+
"%w", err)
}
pOut.TaprootInternalKey = trDeriv.XOnlyPubKey
pOut.Bip32Derivation = []*psbt.Bip32Derivation{deriv}
pOut.TaprootBip32Derivation = []*psbt.TaprootBip32Derivation{
trDeriv,
}
}
newChangeIndex := int32(len(packet.Outputs))
packet.UnsignedTx.TxOut = append(
packet.UnsignedTx.TxOut, &wire.TxOut{
@ -1765,7 +1793,7 @@ func (w *WalletKit) handleChange(packet *psbt.Packet, changeIndex int32,
PkScript: changeScript,
},
)
packet.Outputs = append(packet.Outputs, psbt.POutput{})
packet.Outputs = append(packet.Outputs, pOut)
return newChangeIndex, nil
}

View file

@ -9,12 +9,14 @@ 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/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
@ -640,3 +642,78 @@ func (b *BtcWallet) lookupFirstCustomAccount(
return keyScope, account.AccountNumber, nil
}
// Bip32DerivationFromKeyDesc returns the default and Taproot BIP-0032 key
// derivation information from the given key descriptor information.
func Bip32DerivationFromKeyDesc(keyDesc keychain.KeyDescriptor,
coinType uint32) (*psbt.Bip32Derivation, *psbt.TaprootBip32Derivation,
string) {
bip32Derivation := &psbt.Bip32Derivation{
PubKey: keyDesc.PubKey.SerializeCompressed(),
Bip32Path: []uint32{
keychain.BIP0043Purpose + hdkeychain.HardenedKeyStart,
coinType + hdkeychain.HardenedKeyStart,
uint32(keyDesc.Family) +
uint32(hdkeychain.HardenedKeyStart),
0,
keyDesc.Index,
},
}
derivationPath := fmt.Sprintf(
"m/%d'/%d'/%d'/%d/%d", keychain.BIP0043Purpose, coinType,
keyDesc.Family, 0, keyDesc.Index,
)
return bip32Derivation, &psbt.TaprootBip32Derivation{
XOnlyPubKey: bip32Derivation.PubKey[1:],
MasterKeyFingerprint: bip32Derivation.MasterKeyFingerprint,
Bip32Path: bip32Derivation.Bip32Path,
LeafHashes: make([][]byte, 0),
}, derivationPath
}
// Bip32DerivationFromAddress returns the default and Taproot BIP-0032 key
// derivation information from the given managed address.
func Bip32DerivationFromAddress(
addr waddrmgr.ManagedAddress) (*psbt.Bip32Derivation,
*psbt.TaprootBip32Derivation, string, error) {
pubKeyAddr, ok := addr.(waddrmgr.ManagedPubKeyAddress)
if !ok {
return nil, nil, "", fmt.Errorf("address is not a pubkey " +
"address")
}
scope, derivationInfo, haveInfo := pubKeyAddr.DerivationInfo()
if !haveInfo {
return nil, nil, "", fmt.Errorf("address is an imported " +
"public key, can't derive BIP32 path")
}
bip32Derivation := &psbt.Bip32Derivation{
PubKey: pubKeyAddr.PubKey().SerializeCompressed(),
Bip32Path: []uint32{
scope.Purpose + hdkeychain.HardenedKeyStart,
scope.Coin + hdkeychain.HardenedKeyStart,
derivationInfo.InternalAccount +
hdkeychain.HardenedKeyStart,
derivationInfo.Branch,
derivationInfo.Index,
},
}
derivationPath := fmt.Sprintf(
"m/%d'/%d'/%d'/%d/%d", scope.Purpose, scope.Coin,
derivationInfo.InternalAccount, derivationInfo.Branch,
derivationInfo.Index,
)
return bip32Derivation, &psbt.TaprootBip32Derivation{
XOnlyPubKey: bip32Derivation.PubKey[1:],
MasterKeyFingerprint: bip32Derivation.MasterKeyFingerprint,
Bip32Path: bip32Derivation.Bip32Path,
LeafHashes: make([][]byte, 0),
}, derivationPath, nil
}

View file

@ -17,6 +17,7 @@ import (
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/stretchr/testify/require"
)
@ -495,3 +496,136 @@ func TestEstimateInputWeight(t *testing.T) {
})
}
}
// TestBip32DerivationFromKeyDesc tests that we can correctly extract a BIP32
// derivation path from a key descriptor.
func TestBip32DerivationFromKeyDesc(t *testing.T) {
privKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
testCases := []struct {
name string
keyDesc keychain.KeyDescriptor
coinType uint32
expectedPath string
expectedBip32Path []uint32
}{
{
name: "testnet multi-sig family",
keyDesc: keychain.KeyDescriptor{
PubKey: privKey.PubKey(),
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamilyMultiSig,
Index: 123,
},
},
coinType: chaincfg.TestNet3Params.HDCoinType,
expectedPath: "m/1017'/1'/0'/0/123",
expectedBip32Path: []uint32{
hardenedKey(keychain.BIP0043Purpose),
hardenedKey(chaincfg.TestNet3Params.HDCoinType),
hardenedKey(uint32(keychain.KeyFamilyMultiSig)),
0, 123,
},
},
{
name: "mainnet watchtower family",
keyDesc: keychain.KeyDescriptor{
PubKey: privKey.PubKey(),
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamilyTowerSession,
Index: 456,
},
},
coinType: chaincfg.MainNetParams.HDCoinType,
expectedPath: "m/1017'/0'/8'/0/456",
expectedBip32Path: []uint32{
hardenedKey(keychain.BIP0043Purpose),
hardenedKey(chaincfg.MainNetParams.HDCoinType),
hardenedKey(
uint32(keychain.KeyFamilyTowerSession),
),
0, 456,
},
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(tt *testing.T) {
d, trD, path := Bip32DerivationFromKeyDesc(
tc.keyDesc, tc.coinType,
)
require.NoError(tt, err)
require.Equal(tt, tc.expectedPath, path)
require.Equal(tt, tc.expectedBip32Path, d.Bip32Path)
require.Equal(tt, tc.expectedBip32Path, trD.Bip32Path)
serializedKey := tc.keyDesc.PubKey.SerializeCompressed()
require.Equal(tt, serializedKey, d.PubKey)
require.Equal(tt, serializedKey[1:], trD.XOnlyPubKey)
})
}
}
// TestBip32DerivationFromAddress tests that we can correctly extract a BIP32
// derivation path from an address.
func TestBip32DerivationFromAddress(t *testing.T) {
testCases := []struct {
name string
addrType lnwallet.AddressType
expectedAddr string
expectedPath string
expectedBip32Path []uint32
expectedPubKey string
}{
{
name: "p2wkh",
addrType: lnwallet.WitnessPubKey,
expectedAddr: firstAddress,
expectedPath: "m/84'/0'/0'/0/0",
expectedBip32Path: []uint32{
hardenedKey(waddrmgr.KeyScopeBIP0084.Purpose),
hardenedKey(0), hardenedKey(0), 0, 0,
},
expectedPubKey: firstAddressPubKey,
},
{
name: "p2tr",
addrType: lnwallet.TaprootPubkey,
expectedAddr: firstAddressTaproot,
expectedPath: "m/86'/0'/0'/0/0",
expectedBip32Path: []uint32{
hardenedKey(waddrmgr.KeyScopeBIP0086.Purpose),
hardenedKey(0), hardenedKey(0), 0, 0,
},
expectedPubKey: firstAddressTaprootPubKey,
},
}
w, _ := newTestWallet(t, netParams, seedBytes)
for _, tc := range testCases {
tc := tc
addr, err := w.NewAddress(
tc.addrType, false, lnwallet.DefaultAccountName,
)
require.NoError(t, err)
require.Equal(t, tc.expectedAddr, addr.String())
addrInfo, err := w.AddressInfo(addr)
require.NoError(t, err)
managedAddr, ok := addrInfo.(waddrmgr.ManagedPubKeyAddress)
require.True(t, ok)
d, trD, path, err := Bip32DerivationFromAddress(managedAddr)
require.NoError(t, err)
require.Equal(t, tc.expectedPath, path)
require.Equal(t, tc.expectedBip32Path, d.Bip32Path)
require.Equal(t, tc.expectedBip32Path, trD.Bip32Path)
}
}

View file

@ -38,11 +38,21 @@ var (
// which is a special case for the BIP49/84 addresses in btcwallet).
firstAddress = "bcrt1qgdlgjc5ede7fjv350wcjqat80m0zsmfaswsj9p"
// firstAddressPubKey is the public key of the first address that we
// should get from the wallet.
firstAddressPubKey = "02b844aecf8250c29e46894147a7dae02de55a034a533b6" +
"0c6a6469294ee356ce4"
// firstAddressTaproot is the first address that we should get from the
// wallet when deriving a taproot address.
firstAddressTaproot = "bcrt1ps8c222fgysvnsj2m8hxk8khy6wthcrhv9va9z3t4" +
"h3qeyz65sh4qqwvdgc"
// firstAddressTaprootPubKey is the public key of the first address that
// we should get from the wallet when deriving a taproot address.
firstAddressTaprootPubKey = "03004113d6185c955d6e8f5922b50cc0ac3b64fa" +
"0979402604c5b887f07e3b5388"
testPubKeyBytes, _ = hex.DecodeString(
"037a67771635344641d4b56aac33cd5f7a265b59678dce3aec31b89125e3" +
"b8b9b2",