Skip to content

Commit fb20cd5

Browse files
committed
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.
1 parent 094fdbf commit fb20cd5

File tree

4 files changed

+250
-1
lines changed

4 files changed

+250
-1
lines changed

lnrpc/walletrpc/walletkit_server.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1758,14 +1758,42 @@ func (w *WalletKit) handleChange(packet *psbt.Packet, changeIndex int32,
17581758
return 0, fmt.Errorf("could not derive change script: %w", err)
17591759
}
17601760

1761+
// We need to add the derivation info for the change address in case it
1762+
// is a P2TR address. This is mostly to prove it's a bare BIP-0086
1763+
// address, which is required for some protocols (such as Taproot
1764+
// Assets).
1765+
pOut := psbt.POutput{}
1766+
_, isTaprootChangeAddr := changeAddr.(*btcutil.AddressTaproot)
1767+
if isTaprootChangeAddr {
1768+
changeAddrInfo, err := w.cfg.Wallet.AddressInfo(changeAddr)
1769+
if err != nil {
1770+
return 0, fmt.Errorf("could not get address info: %w",
1771+
err)
1772+
}
1773+
1774+
deriv, trDeriv, _, err := btcwallet.Bip32DerivationFromAddress(
1775+
changeAddrInfo,
1776+
)
1777+
if err != nil {
1778+
return 0, fmt.Errorf("could not get derivation info: "+
1779+
"%w", err)
1780+
}
1781+
1782+
pOut.TaprootInternalKey = trDeriv.XOnlyPubKey
1783+
pOut.Bip32Derivation = []*psbt.Bip32Derivation{deriv}
1784+
pOut.TaprootBip32Derivation = []*psbt.TaprootBip32Derivation{
1785+
trDeriv,
1786+
}
1787+
}
1788+
17611789
newChangeIndex := int32(len(packet.Outputs))
17621790
packet.UnsignedTx.TxOut = append(
17631791
packet.UnsignedTx.TxOut, &wire.TxOut{
17641792
Value: changeAmount,
17651793
PkScript: changeScript,
17661794
},
17671795
)
1768-
packet.Outputs = append(packet.Outputs, psbt.POutput{})
1796+
packet.Outputs = append(packet.Outputs, pOut)
17691797

17701798
return newChangeIndex, nil
17711799
}

lnwallet/btcwallet/psbt.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import (
99
"github.com/btcsuite/btcd/btcec/v2"
1010
"github.com/btcsuite/btcd/btcec/v2/schnorr"
1111
"github.com/btcsuite/btcd/btcutil"
12+
"github.com/btcsuite/btcd/btcutil/hdkeychain"
1213
"github.com/btcsuite/btcd/btcutil/psbt"
1314
"github.com/btcsuite/btcd/txscript"
1415
"github.com/btcsuite/btcd/wire"
1516
"github.com/btcsuite/btcwallet/waddrmgr"
1617
"github.com/btcsuite/btcwallet/wallet"
1718
"github.com/lightningnetwork/lnd/input"
19+
"github.com/lightningnetwork/lnd/keychain"
1820
"github.com/lightningnetwork/lnd/lnwallet"
1921
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
2022
)
@@ -640,3 +642,78 @@ func (b *BtcWallet) lookupFirstCustomAccount(
640642

641643
return keyScope, account.AccountNumber, nil
642644
}
645+
646+
// Bip32DerivationFromKeyDesc returns the default and Taproot BIP-0032 key
647+
// derivation information from the given key descriptor information.
648+
func Bip32DerivationFromKeyDesc(keyDesc keychain.KeyDescriptor,
649+
coinType uint32) (*psbt.Bip32Derivation, *psbt.TaprootBip32Derivation,
650+
string) {
651+
652+
bip32Derivation := &psbt.Bip32Derivation{
653+
PubKey: keyDesc.PubKey.SerializeCompressed(),
654+
Bip32Path: []uint32{
655+
keychain.BIP0043Purpose + hdkeychain.HardenedKeyStart,
656+
coinType + hdkeychain.HardenedKeyStart,
657+
uint32(keyDesc.Family) +
658+
uint32(hdkeychain.HardenedKeyStart),
659+
0,
660+
keyDesc.Index,
661+
},
662+
}
663+
664+
derivationPath := fmt.Sprintf(
665+
"m/%d'/%d'/%d'/%d/%d", keychain.BIP0043Purpose, coinType,
666+
keyDesc.Family, 0, keyDesc.Index,
667+
)
668+
669+
return bip32Derivation, &psbt.TaprootBip32Derivation{
670+
XOnlyPubKey: bip32Derivation.PubKey[1:],
671+
MasterKeyFingerprint: bip32Derivation.MasterKeyFingerprint,
672+
Bip32Path: bip32Derivation.Bip32Path,
673+
LeafHashes: make([][]byte, 0),
674+
}, derivationPath
675+
}
676+
677+
// Bip32DerivationFromAddress returns the default and Taproot BIP-0032 key
678+
// derivation information from the given managed address.
679+
func Bip32DerivationFromAddress(
680+
addr waddrmgr.ManagedAddress) (*psbt.Bip32Derivation,
681+
*psbt.TaprootBip32Derivation, string, error) {
682+
683+
pubKeyAddr, ok := addr.(waddrmgr.ManagedPubKeyAddress)
684+
if !ok {
685+
return nil, nil, "", fmt.Errorf("address is not a pubkey " +
686+
"address")
687+
}
688+
689+
scope, derivationInfo, haveInfo := pubKeyAddr.DerivationInfo()
690+
if !haveInfo {
691+
return nil, nil, "", fmt.Errorf("address is an imported " +
692+
"public key, can't derive BIP32 path")
693+
}
694+
695+
bip32Derivation := &psbt.Bip32Derivation{
696+
PubKey: pubKeyAddr.PubKey().SerializeCompressed(),
697+
Bip32Path: []uint32{
698+
scope.Purpose + hdkeychain.HardenedKeyStart,
699+
scope.Coin + hdkeychain.HardenedKeyStart,
700+
derivationInfo.InternalAccount +
701+
hdkeychain.HardenedKeyStart,
702+
derivationInfo.Branch,
703+
derivationInfo.Index,
704+
},
705+
}
706+
707+
derivationPath := fmt.Sprintf(
708+
"m/%d'/%d'/%d'/%d/%d", scope.Purpose, scope.Coin,
709+
derivationInfo.InternalAccount, derivationInfo.Branch,
710+
derivationInfo.Index,
711+
)
712+
713+
return bip32Derivation, &psbt.TaprootBip32Derivation{
714+
XOnlyPubKey: bip32Derivation.PubKey[1:],
715+
MasterKeyFingerprint: bip32Derivation.MasterKeyFingerprint,
716+
Bip32Path: bip32Derivation.Bip32Path,
717+
LeafHashes: make([][]byte, 0),
718+
}, derivationPath, nil
719+
}

lnwallet/btcwallet/psbt_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/btcsuite/btcwallet/waddrmgr"
1818
"github.com/lightningnetwork/lnd/input"
1919
"github.com/lightningnetwork/lnd/keychain"
20+
"github.com/lightningnetwork/lnd/lnwallet"
2021
"github.com/stretchr/testify/require"
2122
)
2223

@@ -495,3 +496,136 @@ func TestEstimateInputWeight(t *testing.T) {
495496
})
496497
}
497498
}
499+
500+
// TestBip32DerivationFromKeyDesc tests that we can correctly extract a BIP32
501+
// derivation path from a key descriptor.
502+
func TestBip32DerivationFromKeyDesc(t *testing.T) {
503+
privKey, err := btcec.NewPrivateKey()
504+
require.NoError(t, err)
505+
506+
testCases := []struct {
507+
name string
508+
keyDesc keychain.KeyDescriptor
509+
coinType uint32
510+
expectedPath string
511+
expectedBip32Path []uint32
512+
}{
513+
{
514+
name: "testnet multi-sig family",
515+
keyDesc: keychain.KeyDescriptor{
516+
PubKey: privKey.PubKey(),
517+
KeyLocator: keychain.KeyLocator{
518+
Family: keychain.KeyFamilyMultiSig,
519+
Index: 123,
520+
},
521+
},
522+
coinType: chaincfg.TestNet3Params.HDCoinType,
523+
expectedPath: "m/1017'/1'/0'/0/123",
524+
expectedBip32Path: []uint32{
525+
hardenedKey(keychain.BIP0043Purpose),
526+
hardenedKey(chaincfg.TestNet3Params.HDCoinType),
527+
hardenedKey(uint32(keychain.KeyFamilyMultiSig)),
528+
0, 123,
529+
},
530+
},
531+
{
532+
name: "mainnet watchtower family",
533+
keyDesc: keychain.KeyDescriptor{
534+
PubKey: privKey.PubKey(),
535+
KeyLocator: keychain.KeyLocator{
536+
Family: keychain.KeyFamilyTowerSession,
537+
Index: 456,
538+
},
539+
},
540+
coinType: chaincfg.MainNetParams.HDCoinType,
541+
expectedPath: "m/1017'/0'/8'/0/456",
542+
expectedBip32Path: []uint32{
543+
hardenedKey(keychain.BIP0043Purpose),
544+
hardenedKey(chaincfg.MainNetParams.HDCoinType),
545+
hardenedKey(
546+
uint32(keychain.KeyFamilyTowerSession),
547+
),
548+
0, 456,
549+
},
550+
},
551+
}
552+
553+
for _, tc := range testCases {
554+
tc := tc
555+
556+
t.Run(tc.name, func(tt *testing.T) {
557+
d, trD, path := Bip32DerivationFromKeyDesc(
558+
tc.keyDesc, tc.coinType,
559+
)
560+
require.NoError(tt, err)
561+
562+
require.Equal(tt, tc.expectedPath, path)
563+
require.Equal(tt, tc.expectedBip32Path, d.Bip32Path)
564+
require.Equal(tt, tc.expectedBip32Path, trD.Bip32Path)
565+
566+
serializedKey := tc.keyDesc.PubKey.SerializeCompressed()
567+
require.Equal(tt, serializedKey, d.PubKey)
568+
require.Equal(tt, serializedKey[1:], trD.XOnlyPubKey)
569+
})
570+
}
571+
}
572+
573+
// TestBip32DerivationFromAddress tests that we can correctly extract a BIP32
574+
// derivation path from an address.
575+
func TestBip32DerivationFromAddress(t *testing.T) {
576+
testCases := []struct {
577+
name string
578+
addrType lnwallet.AddressType
579+
expectedAddr string
580+
expectedPath string
581+
expectedBip32Path []uint32
582+
expectedPubKey string
583+
}{
584+
{
585+
name: "p2wkh",
586+
addrType: lnwallet.WitnessPubKey,
587+
expectedAddr: firstAddress,
588+
expectedPath: "m/84'/0'/0'/0/0",
589+
expectedBip32Path: []uint32{
590+
hardenedKey(waddrmgr.KeyScopeBIP0084.Purpose),
591+
hardenedKey(0), hardenedKey(0), 0, 0,
592+
},
593+
expectedPubKey: firstAddressPubKey,
594+
},
595+
{
596+
name: "p2tr",
597+
addrType: lnwallet.TaprootPubkey,
598+
expectedAddr: firstAddressTaproot,
599+
expectedPath: "m/86'/0'/0'/0/0",
600+
expectedBip32Path: []uint32{
601+
hardenedKey(waddrmgr.KeyScopeBIP0086.Purpose),
602+
hardenedKey(0), hardenedKey(0), 0, 0,
603+
},
604+
expectedPubKey: firstAddressTaprootPubKey,
605+
},
606+
}
607+
608+
w, _ := newTestWallet(t, netParams, seedBytes)
609+
for _, tc := range testCases {
610+
tc := tc
611+
612+
addr, err := w.NewAddress(
613+
tc.addrType, false, lnwallet.DefaultAccountName,
614+
)
615+
require.NoError(t, err)
616+
617+
require.Equal(t, tc.expectedAddr, addr.String())
618+
619+
addrInfo, err := w.AddressInfo(addr)
620+
require.NoError(t, err)
621+
managedAddr, ok := addrInfo.(waddrmgr.ManagedPubKeyAddress)
622+
require.True(t, ok)
623+
624+
d, trD, path, err := Bip32DerivationFromAddress(managedAddr)
625+
require.NoError(t, err)
626+
627+
require.Equal(t, tc.expectedPath, path)
628+
require.Equal(t, tc.expectedBip32Path, d.Bip32Path)
629+
require.Equal(t, tc.expectedBip32Path, trD.Bip32Path)
630+
}
631+
}

lnwallet/btcwallet/signer_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,21 @@ var (
3838
// which is a special case for the BIP49/84 addresses in btcwallet).
3939
firstAddress = "bcrt1qgdlgjc5ede7fjv350wcjqat80m0zsmfaswsj9p"
4040

41+
// firstAddressPubKey is the public key of the first address that we
42+
// should get from the wallet.
43+
firstAddressPubKey = "02b844aecf8250c29e46894147a7dae02de55a034a533b6" +
44+
"0c6a6469294ee356ce4"
45+
4146
// firstAddressTaproot is the first address that we should get from the
4247
// wallet when deriving a taproot address.
4348
firstAddressTaproot = "bcrt1ps8c222fgysvnsj2m8hxk8khy6wthcrhv9va9z3t4" +
4449
"h3qeyz65sh4qqwvdgc"
4550

51+
// firstAddressTaprootPubKey is the public key of the first address that
52+
// we should get from the wallet when deriving a taproot address.
53+
firstAddressTaprootPubKey = "03004113d6185c955d6e8f5922b50cc0ac3b64fa" +
54+
"0979402604c5b887f07e3b5388"
55+
4656
testPubKeyBytes, _ = hex.DecodeString(
4757
"037a67771635344641d4b56aac33cd5f7a265b59678dce3aec31b89125e3" +
4858
"b8b9b2",

0 commit comments

Comments
 (0)