From e746df3caa1e7c3ff21d487742ca86c5ac6d9a99 Mon Sep 17 00:00:00 2001 From: MPins Date: Mon, 8 Sep 2025 16:24:33 -0700 Subject: [PATCH 1/3] zpay32: add support for P2TR fallback addresses --- zpay32/decode.go | 23 ++++++++++++++++++++--- zpay32/encode.go | 6 ++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/zpay32/decode.go b/zpay32/decode.go index f97c77838..577f6a6d1 100644 --- a/zpay32/decode.go +++ b/zpay32/decode.go @@ -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 diff --git a/zpay32/encode.go b/zpay32/encode.go index 43ccd5ecb..50f294e51 100644 --- a/zpay32/encode.go +++ b/zpay32/encode.go @@ -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") } From 9d792505e911374f9805bb2c9e239c4edb9fbc9a Mon Sep 17 00:00:00 2001 From: MPins Date: Fri, 20 Jun 2025 13:48:03 -0300 Subject: [PATCH 2/3] zpay32: add test case for P2TR fallback addresses --- zpay32/invoice_internal_test.go | 36 +++++++++++++-- zpay32/invoice_test.go | 81 ++++++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 7 deletions(-) diff --git a/zpay32/invoice_internal_test.go b/zpay32/invoice_internal_test.go index 0a41b4c4d..22434a99b 100644 --- a/zpay32/invoice_internal_test.go +++ b/zpay32/invoice_internal_test.go @@ -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 { diff --git a/zpay32/invoice_test.go b/zpay32/invoice_test.go index 4cb1a6170..bfa1539f3 100644 --- a/zpay32/invoice_test.go +++ b/zpay32/invoice_test.go @@ -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. From 97f2fbb2b23140d66089b61ebad70b1d3c9150e1 Mon Sep 17 00:00:00 2001 From: MPins Date: Fri, 20 Jun 2025 15:20:50 -0300 Subject: [PATCH 3/3] doc: release-notes-0.20.0 --- docs/release-notes/release-notes-0.20.0.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes/release-notes-0.20.0.md b/docs/release-notes/release-notes-0.20.0.md index ddf85090e..059b1528d 100644 --- a/docs/release-notes/release-notes-0.20.0.md +++ b/docs/release-notes/release-notes-0.20.0.md @@ -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