Merge pull request #9975 from MPins/issue-9591

Add Support for P2TR Fallback Addresses in BOLT-11
This commit is contained in:
Elle
2025-09-15 10:24:44 +02:00
committed by GitHub
5 changed files with 137 additions and 12 deletions

View File

@@ -55,6 +55,9 @@ when the appropriate TLV flag is set. This allows for HTLCs carrying metadata to
reflect their state on the channel commitment without having to send or receive
a certain amount of msats.
- Added support for [P2TR Fallback Addresses](
https://github.com/lightningnetwork/lnd/pull/9975) in BOLT-11 invoices.
## Functional Enhancements
* [Add](https://github.com/lightningnetwork/lnd/pull/9677)
`ConfirmationsUntilActive` and `ConfirmationHeight` field to the

View File

@@ -15,10 +15,18 @@ import (
"github.com/btcsuite/btcd/btcutil/bech32"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/lnwire"
)
const (
fallbackVersionWitness = txscript.BaseSegwitWitnessVersion
fallbackVersionTaproot = txscript.TaprootWitnessVersion
fallbackVersionPubkeyHash = 17
fallbackVersionScriptHash = 18
)
var (
// ErrInvalidUTF8Description is returned if the invoice description is
// not valid UTF-8.
@@ -529,7 +537,7 @@ func parseFallbackAddr(data []byte, net *chaincfg.Params) (btcutil.Address, erro
version := data[0]
switch version {
case 0:
case fallbackVersionWitness:
witness, err := bech32.ConvertBits(data[1:], 5, 8, false)
if err != nil {
return nil, err
@@ -548,7 +556,16 @@ func parseFallbackAddr(data []byte, net *chaincfg.Params) (btcutil.Address, erro
if err != nil {
return nil, err
}
case 17:
case fallbackVersionTaproot:
witness, err := bech32.ConvertBits(data[1:], 5, 8, false)
if err != nil {
return nil, err
}
addr, err = btcutil.NewAddressTaproot(witness, net)
if err != nil {
return nil, err
}
case fallbackVersionPubkeyHash:
pubKeyHash, err := bech32.ConvertBits(data[1:], 5, 8, false)
if err != nil {
return nil, err
@@ -558,7 +575,7 @@ func parseFallbackAddr(data []byte, net *chaincfg.Params) (btcutil.Address, erro
if err != nil {
return nil, err
}
case 18:
case fallbackVersionScriptHash:
scriptHash, err := bech32.ConvertBits(data[1:], 5, 8, false)
if err != nil {
return nil, err

View File

@@ -202,13 +202,15 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error {
var version byte
switch addr := invoice.FallbackAddr.(type) {
case *btcutil.AddressPubKeyHash:
version = 17
version = fallbackVersionPubkeyHash
case *btcutil.AddressScriptHash:
version = 18
version = fallbackVersionScriptHash
case *btcutil.AddressWitnessPubKeyHash:
version = addr.WitnessVersion()
case *btcutil.AddressWitnessScriptHash:
version = addr.WitnessVersion()
case *btcutil.AddressTaproot:
version = addr.WitnessVersion()
default:
return fmt.Errorf("unknown fallback address type")
}

View File

@@ -593,19 +593,34 @@ func TestParseFallbackAddr(t *testing.T) {
t.Parallel()
testAddrTestnetData, _ := bech32.ConvertBits(testAddrTestnet.ScriptAddress(), 8, 5, true)
testAddrTestnetDataWithVersion := append([]byte{17}, testAddrTestnetData...)
testAddrTestnetDataWithVersion := append(
[]byte{fallbackVersionPubkeyHash}, testAddrTestnetData...,
)
testRustyAddrData, _ := bech32.ConvertBits(testRustyAddr.ScriptAddress(), 8, 5, true)
testRustyAddrDataWithVersion := append([]byte{17}, testRustyAddrData...)
testRustyAddrDataWithVersion := append(
[]byte{fallbackVersionPubkeyHash}, testRustyAddrData...,
)
testAddrMainnetP2SHData, _ := bech32.ConvertBits(testAddrMainnetP2SH.ScriptAddress(), 8, 5, true)
testAddrMainnetP2SHDataWithVersion := append([]byte{18}, testAddrMainnetP2SHData...)
testAddrMainnetP2SHDataWithVersion := append(
[]byte{fallbackVersionScriptHash}, testAddrMainnetP2SHData...,
)
testAddrMainnetP2WPKHData, _ := bech32.ConvertBits(testAddrMainnetP2WPKH.ScriptAddress(), 8, 5, true)
testAddrMainnetP2WPKHDataWithVersion := append([]byte{0}, testAddrMainnetP2WPKHData...)
testAddrMainnetP2WPKHDataWithVersion := append(
[]byte{fallbackVersionWitness}, testAddrMainnetP2WPKHData...,
)
testAddrMainnetP2WSHData, _ := bech32.ConvertBits(testAddrMainnetP2WSH.ScriptAddress(), 8, 5, true)
testAddrMainnetP2WSHDataWithVersion := append([]byte{0}, testAddrMainnetP2WSHData...)
testAddrMainnetP2WSHDataWithVersion := append(
[]byte{fallbackVersionWitness}, testAddrMainnetP2WSHData...,
)
testAddrMainnetP2TRData, _ := bech32.ConvertBits(
testAddrMainnetP2TR.ScriptAddress(), 8, 5, true)
testAddrMainnetP2TRDataWithVersion := append(
[]byte{fallbackVersionTaproot}, testAddrMainnetP2TRData...)
tests := []struct {
data []byte
@@ -651,6 +666,17 @@ func TestParseFallbackAddr(t *testing.T) {
valid: true,
result: testAddrMainnetP2WSH,
},
{
data: testAddrMainnetP2TRDataWithVersion,
net: &chaincfg.MainNetParams,
valid: true,
result: testAddrMainnetP2TR,
},
{
data: testAddrMainnetP2TRDataWithVersion[:10],
net: &chaincfg.MainNetParams,
valid: false, // data too short for P2TR address
},
}
for i, test := range tests {

View File

@@ -71,6 +71,9 @@ var (
testAddrMainnetP2SH, _ = btcutil.DecodeAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX", &chaincfg.MainNetParams)
testAddrMainnetP2WPKH, _ = btcutil.DecodeAddress("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", &chaincfg.MainNetParams)
testAddrMainnetP2WSH, _ = btcutil.DecodeAddress("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", &chaincfg.MainNetParams)
testAddrMainnetP2TR, _ = btcutil.DecodeAddress("bc1pptdvg0d2nj99568"+
"qn6ssdy4cygnwuxgw2ukmnwgwz7jpqjz2kszse2s3lm",
&chaincfg.MainNetParams)
testHopHintPubkeyBytes1, _ = hex.DecodeString("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255")
testHopHintPubkey1, _ = btcec.ParsePubKey(testHopHintPubkeyBytes1)
@@ -299,8 +302,14 @@ func TestDecodeEncode(t *testing.T) {
},
{
// Ignore unknown witness version in fallback address.
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpppw508d6qejxtdg4y5r3zarvary0c5xw7k8txqv6x0a75xuzp0zsdzk5hq6tmfgweltvs6jk5nhtyd9uqksvr48zga9mw08667w8264gkspluu66jhtcmct36nx363km6cquhhv2cpc6q43r",
valid: true,
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqq" +
"qsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan" +
"79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrq" +
"sfp4z6yn92zrp97a6q5hhh8swys7uf4hm9tr8a0xylnk" +
"26fvkg3jx0sdsxvma0zvf2h0pycyyzdrmjncq6lzrfuw" +
"xfhv6gzz4q5303n3up6as4ghe5qthg7x20z7vae8w5rq" +
"u6de3g4jl7kvuap3qedprqsqqmgqqm6s8sl",
valid: true,
decodedInvoice: func() *Invoice {
return &Invoice{
Net: &chaincfg.MainNetParams,
@@ -649,6 +658,74 @@ func TestDecodeEncode(t *testing.T) {
i.Destination = nil
},
},
{
// On mainnet, with fallback (p2tr) address "bc1pptdvg0d
// 2nj99568qn6ssdy4cygnwuxgw2ukmnwgwz7jpqjz2kszse2s3lm"
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqq" +
"qsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan" +
"79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrq" +
"sfp4pptdvg0d2nj99568qn6ssdy4cygnwuxgw2ukmnwg" +
"wz7jpqjz2kszs9zs3tmcpgulwc0ruwc2cm97udy6sdfe" +
"nwvha8qlkfwx49sgk40kze4kwsh706rae3uc30ltpwpw" +
"mjyhc3uan4ljz56wksg5gsnhrrhcqsrq93d",
valid: true,
decodedInvoice: func() *Invoice {
return &Invoice{
Net: &chaincfg.MainNetParams,
MilliSat: &testMillisat20mBTC,
Timestamp: time.Unix(1496314658, 0),
PaymentHash: &testPaymentHash,
DescriptionHash: &testDescriptionHash,
Destination: testPubKey,
FallbackAddr: testAddrMainnetP2TR,
Features: emptyFeatures,
}
},
beforeEncoding: func(i *Invoice) {
// Since this destination pubkey was recovered
// from the signature, we must set it nil before
// encoding to get back the same invoice string.
i.Destination = nil
},
},
{
// On mainnet, with fallback (p2tr) address "bc1pptdvg0d
// 2nj99568qn6ssdy4cygnwuxgw2ukmnwgwz7jpqjz2kszse2s3lm"
// using the test vector payment from BOLT 11
encodedInvoice: "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zy" +
"g3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqc" +
"yq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqyp" +
"qhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98kly" +
"sy043l2ahrqsfp4pptdvg0d2nj99568qn6ssdy4cygnw" +
"uxgw2ukmnwgwz7jpqjz2kszs9qrsgqy606dznq28exny" +
"dt2r4c29y56xjtn3sk4mhgjtl4pg2y4ar3249rq4ajlm" +
"j9jy8zvlzw7cr8mggqzm842xfr0v72rswzq9xvr4hknf" +
"sqwmn6xd",
valid: true,
decodedInvoice: func() *Invoice {
return &Invoice{
Net: &chaincfg.MainNetParams,
MilliSat: &testMillisat20mBTC,
Timestamp: time.Unix(1496314658, 0),
PaymentHash: &testPaymentHash,
DescriptionHash: &testDescriptionHash,
Destination: testPubKey,
FallbackAddr: testAddrMainnetP2TR,
Features: lnwire.NewFeatureVector(
lnwire.NewRawFeatureVector(
8, 14,
),
lnwire.Features,
),
PaymentAddr: fn.Some(specPaymentAddr),
}
},
// Skip encoding since LND encode the tagged fields
// in a different order.
skipEncoding: true,
},
{
// Send 2500uBTC for a cup of coffee with a custom CLTV
// expiry value.