diff --git a/lnrpc/walletrpc/walletkit_server.go b/lnrpc/walletrpc/walletkit_server.go index 2309e0ebc..799ed3d9a 100644 --- a/lnrpc/walletrpc/walletkit_server.go +++ b/lnrpc/walletrpc/walletkit_server.go @@ -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 } diff --git a/lnwallet/btcwallet/psbt.go b/lnwallet/btcwallet/psbt.go index ffb8fd2fd..e655300c1 100644 --- a/lnwallet/btcwallet/psbt.go +++ b/lnwallet/btcwallet/psbt.go @@ -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 +} diff --git a/lnwallet/btcwallet/psbt_test.go b/lnwallet/btcwallet/psbt_test.go index 74ec174ad..d3cf3f413 100644 --- a/lnwallet/btcwallet/psbt_test.go +++ b/lnwallet/btcwallet/psbt_test.go @@ -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) + } +} diff --git a/lnwallet/btcwallet/signer_test.go b/lnwallet/btcwallet/signer_test.go index 25e62b4df..d51714ad0 100644 --- a/lnwallet/btcwallet/signer_test.go +++ b/lnwallet/btcwallet/signer_test.go @@ -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",