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 reflect their state on the channel commitment without having to send or receive
a certain amount of msats. a certain amount of msats.
- Added support for [P2TR Fallback Addresses](
https://github.com/lightningnetwork/lnd/pull/9975) in BOLT-11 invoices.
## Functional Enhancements ## Functional Enhancements
* [Add](https://github.com/lightningnetwork/lnd/pull/9677) * [Add](https://github.com/lightningnetwork/lnd/pull/9677)
`ConfirmationsUntilActive` and `ConfirmationHeight` field to the `ConfirmationsUntilActive` and `ConfirmationHeight` field to the

View File

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

View File

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

View File

@@ -593,19 +593,34 @@ func TestParseFallbackAddr(t *testing.T) {
t.Parallel() t.Parallel()
testAddrTestnetData, _ := bech32.ConvertBits(testAddrTestnet.ScriptAddress(), 8, 5, true) 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) 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) 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) 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) 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 { tests := []struct {
data []byte data []byte
@@ -651,6 +666,17 @@ func TestParseFallbackAddr(t *testing.T) {
valid: true, valid: true,
result: testAddrMainnetP2WSH, 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 { for i, test := range tests {

View File

@@ -71,6 +71,9 @@ var (
testAddrMainnetP2SH, _ = btcutil.DecodeAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX", &chaincfg.MainNetParams) testAddrMainnetP2SH, _ = btcutil.DecodeAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX", &chaincfg.MainNetParams)
testAddrMainnetP2WPKH, _ = btcutil.DecodeAddress("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", &chaincfg.MainNetParams) testAddrMainnetP2WPKH, _ = btcutil.DecodeAddress("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", &chaincfg.MainNetParams)
testAddrMainnetP2WSH, _ = btcutil.DecodeAddress("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", &chaincfg.MainNetParams) testAddrMainnetP2WSH, _ = btcutil.DecodeAddress("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", &chaincfg.MainNetParams)
testAddrMainnetP2TR, _ = btcutil.DecodeAddress("bc1pptdvg0d2nj99568"+
"qn6ssdy4cygnwuxgw2ukmnwgwz7jpqjz2kszse2s3lm",
&chaincfg.MainNetParams)
testHopHintPubkeyBytes1, _ = hex.DecodeString("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") testHopHintPubkeyBytes1, _ = hex.DecodeString("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255")
testHopHintPubkey1, _ = btcec.ParsePubKey(testHopHintPubkeyBytes1) testHopHintPubkey1, _ = btcec.ParsePubKey(testHopHintPubkeyBytes1)
@@ -299,8 +302,14 @@ func TestDecodeEncode(t *testing.T) {
}, },
{ {
// Ignore unknown witness version in fallback address. // Ignore unknown witness version in fallback address.
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpppw508d6qejxtdg4y5r3zarvary0c5xw7k8txqv6x0a75xuzp0zsdzk5hq6tmfgweltvs6jk5nhtyd9uqksvr48zga9mw08667w8264gkspluu66jhtcmct36nx363km6cquhhv2cpc6q43r", encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqq" +
valid: true, "qsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan" +
"79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrq" +
"sfp4z6yn92zrp97a6q5hhh8swys7uf4hm9tr8a0xylnk" +
"26fvkg3jx0sdsxvma0zvf2h0pycyyzdrmjncq6lzrfuw" +
"xfhv6gzz4q5303n3up6as4ghe5qthg7x20z7vae8w5rq" +
"u6de3g4jl7kvuap3qedprqsqqmgqqm6s8sl",
valid: true,
decodedInvoice: func() *Invoice { decodedInvoice: func() *Invoice {
return &Invoice{ return &Invoice{
Net: &chaincfg.MainNetParams, Net: &chaincfg.MainNetParams,
@@ -649,6 +658,74 @@ func TestDecodeEncode(t *testing.T) {
i.Destination = nil 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 // Send 2500uBTC for a cup of coffee with a custom CLTV
// expiry value. // expiry value.