From d7221e0a6684a4c1664a3a5a457a15eb7a5aa97d Mon Sep 17 00:00:00 2001 From: MPins Date: Thu, 23 Oct 2025 08:37:08 -0700 Subject: [PATCH 1/5] zpay32: validate mandatory fields (p, h, s, n) and lengths Decode now fails if a field with a fixed length (p, h, s, n) does not have the correct length (52, 52, 52, 53), as BOLT 11 requires since lightning/bolts#1243. Previously such fields were skipped. Each field is parsed before the duplicate check, so a wrong-length field fails with ErrInvalidFieldLength even if a valid field of the same type came first. A second valid payment hash is still rejected with ErrDuplicatePaymentHash, now detected through invoice.PaymentHash instead of a separate flag, since a parsed payment hash can no longer be nil. Decode now also requires a payment secret unless the invoice carries blinded paths. The new WithSkipPaymentSecretCheck option disables this check for callers that only display an invoice and never pay it. --- lnrpc/routerrpc/router_backend.go | 4 +- zpay32/decode.go | 88 ++++++++++++++++++++----------- zpay32/invoice.go | 4 ++ 3 files changed, 64 insertions(+), 32 deletions(-) diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index ce350ff5e87..e09da47badd 100644 --- a/lnrpc/routerrpc/router_backend.go +++ b/lnrpc/routerrpc/router_backend.go @@ -1038,7 +1038,9 @@ func (r *RouterBackend) extractIntentFromSendRequest( } // An invoice must include either a payment address or - // blinded paths. + // blinded paths. zpay32.Decode already enforces this, but we + // keep the check here as a defense in case the decoder is + // relaxed. if payReq.PaymentAddr.IsNone() && len(payReq.BlindedPaymentPaths) == 0 { diff --git a/zpay32/decode.go b/zpay32/decode.go index 8a2e0bcb8e8..9dd88ac6b01 100644 --- a/zpay32/decode.go +++ b/zpay32/decode.go @@ -64,10 +64,23 @@ func WithErrorOnUnknownFeatureBit() DecodeOption { } } +// WithSkipPaymentSecretCheck is a functional option that allows the Decode +// function to accept an invoice that has neither a payment secret nor blinded +// paths. BOLT 11 requires a reader to fail the payment for such an invoice, so +// this option must only be used when the invoice is not going to be paid, for +// example to display invoices that were stored before payment secrets were +// mandatory. +func WithSkipPaymentSecretCheck() DecodeOption { + return func(options *decodeOptions) { + options.skipPaymentSecretCheck = true + } +} + // decodeOptions holds the set of Decode options. type decodeOptions struct { - knownFeatureBits map[lnwire.FeatureBit]string - errorOnUnknownFeature bool + knownFeatureBits map[lnwire.FeatureBit]string + errorOnUnknownFeature bool + skipPaymentSecretCheck bool } // newDecodeOptions constructs the default decodeOptions struct. @@ -212,6 +225,16 @@ func Decode(invoice string, net *chaincfg.Params, opts ...DecodeOption) ( return nil, err } + // The invoice must include a payment secret, unless it uses blinded + // paths, in which case the PathID in the final hop serves the same + // purpose. + if !options.skipPaymentSecretCheck && + len(decodedInvoice.BlindedPaymentPaths) == 0 && + decodedInvoice.PaymentAddr.IsNone() { + + return nil, ErrPaymentSecretNotFound + } + if options.errorOnUnknownFeature { // Make sure that we understand all the required feature bits // in the invoice. @@ -265,11 +288,7 @@ func parseTimestamp(data []byte) (uint64, error) { // parseTaggedFields takes the base32 encoded tagged fields of the invoice, and // fills the Invoice struct accordingly. func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) error { - var ( - index int - paymentHashSeen bool - ) - + index := 0 for len(fields)-index > 0 { // If there are less than 3 groups to read, there cannot be more // interesting information, as we need the type (1 group) and @@ -298,27 +317,30 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er switch typ { case fieldTypeP: - if paymentHashSeen { + paymentHash, err := parse32Bytes(base32Data) + if err != nil { + return fmt.Errorf("payment hash: %w", err) + } + + if invoice.PaymentHash != nil { return ErrDuplicatePaymentHash } - paymentHashSeen = true - invoice.PaymentHash, err = parse32Bytes(base32Data) + invoice.PaymentHash = paymentHash case fieldTypeS: + addr, err := parse32Bytes(base32Data) + if err != nil { + return fmt.Errorf("payment secret: %w", err) + } + if invoice.PaymentAddr.IsSome() { // We skip the field if we have already seen a // supported one. continue } - addr, err := parse32Bytes(base32Data) - if err != nil { - return err - } - if addr != nil { - invoice.PaymentAddr = fn.Some(*addr) - } + invoice.PaymentAddr = fn.Some(*addr) case fieldTypeD: if invoice.Description != nil { @@ -339,22 +361,32 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er invoice.Metadata, err = parseMetadata(base32Data) case fieldTypeN: + destination, err := parseDestination(base32Data) + if err != nil { + return fmt.Errorf("destination id: %w", err) + } + if invoice.Destination != nil { // We skip the field if we have already seen a // supported one. continue } - invoice.Destination, err = parseDestination(base32Data) + invoice.Destination = destination case fieldTypeH: + descriptionHash, err := parse32Bytes(base32Data) + if err != nil { + return fmt.Errorf("description hash: %w", err) + } + if invoice.DescriptionHash != nil { // We skip the field if we have already seen a // supported one. continue } - invoice.DescriptionHash, err = parse32Bytes(base32Data) + invoice.DescriptionHash = descriptionHash case fieldTypeX: if invoice.expiry != nil { @@ -449,16 +481,10 @@ func parseFieldDataLength(data []byte) (uint16, error) { func parse32Bytes(data []byte) (*[32]byte, error) { var paymentHash [32]byte - // A field with an unexpected length is reported as absent rather - // than as an error, leaving it to the caller to decide whether a - // missing field is fatal. Note that BOLT 11 is stricter, and - // requires a reader to fail on a fixed-length field (p, h, s, n) - // with the wrong length. For the payment hash the end result is the - // same: a lone wrong-length field leaves PaymentHash nil and - // validateInvoice rejects the invoice, and a wrong-length field - // paired with a valid one is rejected as a duplicate. + // As BOLT-11 states, a reader must fail the payment if a 32-byte + // field (p, h, s) does not have a length of 52. if len(data) != hashBase32Len { - return nil, nil + return nil, ErrInvalidFieldLength } hash, err := bech32.ConvertBits(data, 5, 8, false) @@ -497,10 +523,10 @@ func parseMetadata(data []byte) ([]byte, error) { // parseDestination converts the data (encoded in base32) into a 33-byte public // key of the payee node. func parseDestination(data []byte) (*btcec.PublicKey, error) { - // As BOLT-11 states, a reader must skip over the destination field - // if it does not have a length of 53, so avoid returning an error. + // As BOLT-11 states, a reader must fail the payment if the destination + // field does not have a length of 53. if len(data) != pubKeyBase32Len { - return nil, nil + return nil, ErrInvalidFieldLength } base256Data, err := bech32.ConvertBits(data, 5, 8, false) diff --git a/zpay32/invoice.go b/zpay32/invoice.go index bcdaa1ca9de..af7ed96e91d 100644 --- a/zpay32/invoice.go +++ b/zpay32/invoice.go @@ -109,6 +109,10 @@ var ( ErrDuplicatePaymentHash = errors.New( "invoice contains multiple payment hashes", ) + + // ErrPaymentSecretNotFound is returned when a decoded invoice has + // neither a payment secret nor blinded paths. + ErrPaymentSecretNotFound = errors.New("payment secret not found") ) // MessageSigner is passed to the Encode method to provide a signature From 037008f90782d0359525a00778de32ce905cc187 Mon Sep 17 00:00:00 2001 From: MPins Date: Mon, 27 Oct 2025 11:23:01 -0700 Subject: [PATCH 2/5] zpay32: adapt the current tests to the changes Adapt the existing tests to the new behavior of Decode, parseTaggedFields, parse32Bytes and parseDestination. Test vectors without a payment secret gain an s field and are re-signed. Vectors that relied on wrong-length fields being skipped now expect ErrInvalidFieldLength. The routerrpc tests that decode invoices built with NewInvoice now set a payment address. --- lnrpc/routerrpc/router_backend_test.go | 5 +- lnrpc/routerrpc/router_server_test.go | 1 + zpay32/invoice_internal_test.go | 41 ++--- zpay32/invoice_test.go | 231 +++++++++---------------- 4 files changed, 106 insertions(+), 172 deletions(-) diff --git a/lnrpc/routerrpc/router_backend_test.go b/lnrpc/routerrpc/router_backend_test.go index de3a662d8f3..6d97bcdfeec 100644 --- a/lnrpc/routerrpc/router_backend_test.go +++ b/lnrpc/routerrpc/router_backend_test.go @@ -831,9 +831,8 @@ func TestExtractIntentFromSendRequest(t *testing.T) { sendReq: &SendPaymentRequest{ PaymentRequest: paymentReqMissingAddr, }, - valid: false, - expectedErrorMsg: "payment request must contain " + - "either a payment address or blinded paths", + valid: false, + expectedErrorMsg: "payment secret not found", }, { name: "Invalid dest vertex length", diff --git a/lnrpc/routerrpc/router_server_test.go b/lnrpc/routerrpc/router_server_test.go index 12592a72e29..5353dc0768d 100644 --- a/lnrpc/routerrpc/router_server_test.go +++ b/lnrpc/routerrpc/router_server_test.go @@ -838,6 +838,7 @@ func TestProbePaymentRequestUsesUniqueHashPerLSP(t *testing.T) { zpay32.RouteHint([]zpay32.HopHint{bobHint}), zpay32.RouteHint([]zpay32.HopHint{eveHint}), zpay32.RouteHint([]zpay32.HopHint{daveHint}), + zpay32.PaymentAddr([32]byte{4, 5}), ) require.NoError(t, err) diff --git a/zpay32/invoice_internal_test.go b/zpay32/invoice_internal_test.go index 656f8ee5908..78eaf400127 100644 --- a/zpay32/invoice_internal_test.go +++ b/zpay32/invoice_internal_test.go @@ -3,6 +3,7 @@ package zpay32 import ( "bytes" "encoding/binary" + "errors" "math" "reflect" "testing" @@ -317,8 +318,7 @@ func TestParseFieldDataLength(t *testing.T) { } // TestParse32Bytes checks that the payment hash is properly parsed. -// If the data does not have a length of 52 bytes, we skip over parsing the -// field and do not return an error. +// If the data does not have a length of 52 bytes, we return an error. func TestParse32Bytes(t *testing.T) { t.Parallel() @@ -330,14 +330,12 @@ func TestParse32Bytes(t *testing.T) { result *[32]byte }{ { - data: []byte{}, - valid: true, - result: nil, // skip unknown length, not 52 bytes + data: []byte{}, + valid: false, }, { - data: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, - valid: true, - result: nil, // skip unknown length, not 52 bytes + data: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, + valid: false, }, { data: testPaymentHashData, @@ -345,9 +343,8 @@ func TestParse32Bytes(t *testing.T) { result: &testPaymentHash, }, { - data: append(testPaymentHashData, 0x0), - valid: true, - result: nil, // skip unknown length, not 52 bytes + data: append(testPaymentHashData, 0x0), + valid: false, }, } @@ -423,8 +420,7 @@ func TestParseDescription(t *testing.T) { } // TestParseDestination checks that the destination is properly parsed. -// If the data does not have a length of 53 bytes, we skip over parsing the -// field and do not return an error. +// If the data does not have a length of 53 bytes, we return an error. func TestParseDestination(t *testing.T) { t.Parallel() @@ -436,14 +432,12 @@ func TestParseDestination(t *testing.T) { result *btcec.PublicKey }{ { - data: []byte{}, - valid: true, - result: nil, // skip unknown length, not 53 bytes + data: []byte{}, + valid: false, }, { - data: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, - valid: true, - result: nil, // skip unknown length, not 53 bytes + data: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, + valid: false, }, { data: testPubKeyData, @@ -451,9 +445,8 @@ func TestParseDestination(t *testing.T) { result: testPubKey, }, { - data: append(testPubKeyData, 0x0), - valid: true, - result: nil, // skip unknown length, not 53 bytes + data: append(testPubKeyData, 0x0), + valid: false, }, } @@ -837,7 +830,7 @@ func TestParseTaggedFields(t *testing.T) { { name: "malformed then valid payment hash", data: malformedThenValid.Bytes(), - wantErr: ErrDuplicatePaymentHash, + wantErr: ErrInvalidFieldLength, }, { name: "identical payment hashes", @@ -879,7 +872,7 @@ func TestParseTaggedFields(t *testing.T) { t.Run(tc.name, func(t *testing.T) { var invoice Invoice gotErr := parseTaggedFields(&invoice, tc.data, netParams) - if tc.wantErr != gotErr { + if !errors.Is(gotErr, tc.wantErr) { t.Fatalf("Unexpected error. want=%v got=%v", tc.wantErr, gotErr) } diff --git a/zpay32/invoice_test.go b/zpay32/invoice_test.go index 3dca55be73d..6410cdf230c 100644 --- a/zpay32/invoice_test.go +++ b/zpay32/invoice_test.go @@ -257,52 +257,20 @@ func TestDecodeEncode(t *testing.T) { // no payment hash set encodedInvoice: "lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsjv38luh6p6s2xrv3mzvlmzaya43376h0twal5ax0k6p47498hp3hnaymzhsn424rxqjs0q7apn26yrhaxltq3vzwpqj9nc2r3kzwccsplnq470", valid: false, - decodedInvoice: func() *Invoice { - return &Invoice{ - Net: &chaincfg.MainNetParams, - MilliSat: &testMillisat20mBTC, - Timestamp: time.Unix(1496314658, 0), - DescriptionHash: &testDescriptionHash, - Destination: testPubKey, - Features: emptyFeatures, - } - }, }, { // Both Description and DescriptionHash set. - encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs03vghs8y0kuj4ulrzls8ln7fnm9dk7sjsnqmghql6hd6jut36clkqpyuq0s5m6fhureyz0szx2qjc8hkgf4xc2hpw8jpu26jfeyvf4cpga36gt", + encodedInvoice: "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsu6zmmjhtn0uhrqje9x4y2c05mjvvg0ftwg32cnjkzs2vwmuf7ltysjlvkvh2pkgg20ssp4muprn93ezasdgn6aezu5ec5g54nju9kkgqtf6fht", valid: false, - decodedInvoice: func() *Invoice { - return &Invoice{ - Net: &chaincfg.MainNetParams, - MilliSat: &testMillisat20mBTC, - Timestamp: time.Unix(1496314658, 0), - PaymentHash: &testPaymentHash, - Description: &testPleaseConsider, - DescriptionHash: &testDescriptionHash, - Destination: testPubKey, - Features: emptyFeatures, - } - }, }, { // Neither Description nor DescriptionHash set. - encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqn2rne0kagfl4e0xag0w6hqeg2dwgc54hrm9m0auw52dhwhwcu559qav309h598pyzn69wh2nqauneyyesnpmaax0g6acr8lh9559jmcquyq5a9", + encodedInvoice: "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqwul6apumndhf3t4v50sdx0vc9jma3cjfq49reu9a6rsadhs933nsau8vwumegq0scs492xx5s6zp6rmr50gd2pdkv285kzsr7zt5xjsp69w7kw", valid: false, - decodedInvoice: func() *Invoice { - return &Invoice{ - Net: &chaincfg.MainNetParams, - MilliSat: &testMillisat20mBTC, - Timestamp: time.Unix(1496314658, 0), - PaymentHash: &testPaymentHash, - Destination: testPubKey, - Features: emptyFeatures, - } - }, }, { // Has a few unknown fields, should just be ignored. - encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqtq2v93xxer9vczq8v93xxeqv72xr42ca60022jqu6fu73n453tmnr0ukc0pl0t23w7eavtensjz0j2wcu7nkxhfdgp9y37welajh5kw34mq7m4xuay0a72cwec8qwgqt5vqht", + encodedInvoice: "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqtq0w4hxkmn0wahrqvgzq0w4hxkmn0wahrqvselxnzkx7sdr4jjeqz8c022yhhkwjskhg32kcprhc08atq06megxyy8xclrsuc2tpfnayhskjj6dxmy34330rxd32cp3evjcdaqqkmdqprtqpuh", valid: true, decodedInvoice: func() *Invoice { return &Invoice{ @@ -313,20 +281,15 @@ func TestDecodeEncode(t *testing.T) { Description: &testPleaseConsider, Destination: testPubKey, Features: emptyFeatures, + PaymentAddr: fn.Some(specPaymentAddr), } }, skipEncoding: true, // Skip encoding since we don't have the unknown fields to encode. }, { // Ignore unknown witness version in fallback address. - encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqq" + - "qsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan" + - "79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrq" + - "sfp4z6yn92zrp97a6q5hhh8swys7uf4hm9tr8a0xylnk" + - "26fvkg3jx0sdsxvma0zvf2h0pycyyzdrmjncq6lzrfuw" + - "xfhv6gzz4q5303n3up6as4ghe5qthg7x20z7vae8w5rq" + - "u6de3g4jl7kvuap3qedprqsqqmgqqm6s8sl", - valid: true, + encodedInvoice: "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4z6yn92zrp97a6q5hhh8swys7uf4hm9tr8a0xylnk26fvkg3jx0sdsvy8e63r5vpmxxzfh7jm87h0unhsssqfara08llrvz2zphyw2hreqsa89482kprux7pcgvsuxa8rd4vqj6duzvgat40jk8ke7uyed3asqqqa2fg", + valid: true, decodedInvoice: func() *Invoice { return &Invoice{ Net: &chaincfg.MainNetParams, @@ -336,39 +299,21 @@ func TestDecodeEncode(t *testing.T) { DescriptionHash: &testDescriptionHash, Destination: testPubKey, Features: emptyFeatures, + PaymentAddr: fn.Some(specPaymentAddr), } }, skipEncoding: true, // Skip encoding since we don't have the unknown fields to encode. }, { - // Ignore fields with unknown lengths. The wrong-length - // duplicates of the h and n fields are skipped, while - // the valid p, h, and n fields are used. - encodedInvoice: "lnbc241pveeq09pp5qqqsyqcyq5rqwzqf" + - "qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan" + - "79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqshp38yjmd" + - "an79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahnp4q0n326hr8v" + - "9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66np3q0n326hr8v" + - "9zprg8gsvezcch06gfaqqhde2aj730yg0durunfp3ngd7vju6eywrly" + - "v9vu7l797m4x5yxvvhqd4rm8guqw5389vna986py0hkxen8kmtmte4d" + - "gv439wksk2rh4smnm5w43a0e43lecjvqptnpz74", - valid: true, - decodedInvoice: func() *Invoice { - return &Invoice{ - Net: &chaincfg.MainNetParams, - MilliSat: &testMillisat24BTC, - Timestamp: time.Unix(1503429093, 0), - PaymentHash: &testPaymentHash, - Destination: testPubKey, - DescriptionHash: &testDescriptionHash, - Features: emptyFeatures, - } - }, - skipEncoding: true, // Skip encoding since we don't have the unknown fields to encode. + // Reject fields with unknown lengths. + encodedInvoice: "lnbc241pveeq09pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqppsqgpsgpgxquyqjqqpqgpsgpgxquyqjqqpqgpsgpgxquyqjqgznp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66npjz44wxwc2yzxsw3qej933wl5sn6qpwmj4m9az7gs7mc8exnwe45hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqshpskmm8utp5qqmw6ysf8h2yuqmgmudkagnv20d7fqgltr74mwxpsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsspszyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3hgvfdj2zty30gcyl6flpqrdztw56n8klxwrg2yqw3tjwkqy7k3s9av2rf2s4u5wm4k4e44rd2cvjk9cutlaqwv7uklgnd3zvrt79zgqqutvndk", + valid: false, + wantErr: ErrInvalidFieldLength, }, { - // Reject a duplicate payment hash even if it has an - // unknown length. + // A second payment hash with an unknown length is + // rejected by the length check before the duplicate + // check is reached. encodedInvoice: "lnbc241pveeq09pp5qqqsyqcyq5rqwzqf" + "qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqpp3qqqsyqcyq5rq" + "wzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqhp58yjmdan79s6" + @@ -379,11 +324,11 @@ func TestDecodeEncode(t *testing.T) { "wkymx47cszugvzn5v64ahemzzlmm62rpn9l9rm05h35aceq00tkt2" + "96289wepws9jh4499wq2l0vk6xcxffd90dpuqchqqztyayq", valid: false, - wantErr: ErrDuplicatePaymentHash, + wantErr: ErrInvalidFieldLength, }, { // Invoice with no amount. - encodedInvoice: "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jshwlglv23cytkzvq8ld39drs8sq656yh2zn0aevrwu6uqctaklelhtpjnmgjdzmvwsh0kuxuwqf69fjeap9m5mev2qzpp27xfswhs5vgqmn9xzq", + encodedInvoice: "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jssp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsk7mxg4jd0r638awa4kd6n0pzn6wtg33jg6ptdx68vclhlwpwxz7h4kn2tlah4zn3zwsgvn4xrcszhz0qu9nhem7u8lt4tz5vmkd8a5gpe4w8ra", valid: true, decodedInvoice: func() *Invoice { return &Invoice{ @@ -393,6 +338,7 @@ func TestDecodeEncode(t *testing.T) { Description: &testCupOfCoffee, Destination: testPubKey, Features: emptyFeatures, + PaymentAddr: fn.Some(specPaymentAddr), } }, beforeEncoding: func(i *Invoice) { @@ -404,7 +350,7 @@ func TestDecodeEncode(t *testing.T) { }, { // Please make a donation of any amount using rhash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad - encodedInvoice: "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w", + encodedInvoice: "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs3u3lp8kstkqc5h5u2s960n5qjkzzsqasr9rle9z3rw08frm5j62x8qhj66t83qccmkx7zh29puy7zpq0nf7qws0dkq5q6mav7n87rhgp5skgmx", valid: true, decodedInvoice: func() *Invoice { return &Invoice{ @@ -414,6 +360,7 @@ func TestDecodeEncode(t *testing.T) { Description: &testPleaseConsider, Destination: testPubKey, Features: emptyFeatures, + PaymentAddr: fn.Some(specPaymentAddr), } }, beforeEncoding: func(i *Invoice) { @@ -425,7 +372,7 @@ func TestDecodeEncode(t *testing.T) { }, { // Same as above, pubkey set in 'n' field. - encodedInvoice: "lnbc241pveeq09pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66jd3m5klcwhq68vdsmx2rjgxeay5v0tkt2v5sjaky4eqahe4fx3k9sqavvce3capfuwv8rvjng57jrtfajn5dkpqv8yelsewtljwmmycq62k443", + encodedInvoice: "lnbc241pveeq09pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygse2tu9s2lqeved59qef46rl2rsd5tkzdc9pjr6z3299aaaskgnk85zg96ng3xlknry8d9f9ssrqan6uj9dtm9mc00wcsam7awtkdgh6sqpf7nq7", valid: true, decodedInvoice: func() *Invoice { return &Invoice{ @@ -436,12 +383,13 @@ func TestDecodeEncode(t *testing.T) { Destination: testPubKey, Description: &testEmptyString, Features: emptyFeatures, + PaymentAddr: fn.Some(specPaymentAddr), } }, }, { // Please send $3 for a cup of coffee to the same peer, within 1 minute - encodedInvoice: "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp", + encodedInvoice: "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxfj8ru680ls6z3cqpanuvtyqux2xuek3kr9xnfek7p258x4vhhns0hl5uvzryshanhlxnhl0fglhw9klpxl0rzsn6rv63uxwup0sgtspjum4ek", valid: true, decodedInvoice: func() *Invoice { i, _ := NewInvoice( @@ -451,7 +399,8 @@ func TestDecodeEncode(t *testing.T) { Amount(testMillisat2500uBTC), Description(testCupOfCoffee), Destination(testPubKey), - Expiry(testExpiry60)) + Expiry(testExpiry60), + PaymentAddr(specPaymentAddr)) return i }, beforeEncoding: func(i *Invoice) { @@ -463,7 +412,7 @@ func TestDecodeEncode(t *testing.T) { }, { // Please send 0.0025 BTC for a cup of nonsense (ナンセンス 1杯) to the same peer, within 1 minute - encodedInvoice: "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny", + encodedInvoice: "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygslten3f0ydjxklpy86g3sh6yz5ar7f0jxjas9dkdvggwwnvq2q93xle0gsfxmm3qmulh5y5q5gunhj3r2mqn8glden5g7d6n8mzxze7cq4mqn5r", valid: true, decodedInvoice: func() *Invoice { i, _ := NewInvoice( @@ -473,7 +422,8 @@ func TestDecodeEncode(t *testing.T) { Amount(testMillisat2500uBTC), Description(testCupOfNonsense), Destination(testPubKey), - Expiry(testExpiry60)) + Expiry(testExpiry60), + PaymentAddr(specPaymentAddr)) return i }, beforeEncoding: func(i *Invoice) { @@ -485,7 +435,7 @@ func TestDecodeEncode(t *testing.T) { }, { // Now send $24 for an entire list of things (hashed) - encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7khhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7", + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqssp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsz7q6sc79jzdwtegpf58988jufqr7qy0n59me7pc7wk4pl8ym6k94kjzyxccs6cc9kk88djqeac3lprndgtn2whk25xzw4xenr95ldhsq5k7v5a", valid: true, decodedInvoice: func() *Invoice { return &Invoice{ @@ -496,6 +446,7 @@ func TestDecodeEncode(t *testing.T) { DescriptionHash: &testDescriptionHash, Destination: testPubKey, Features: emptyFeatures, + PaymentAddr: fn.Some(specPaymentAddr), } }, beforeEncoding: func(i *Invoice) { @@ -507,7 +458,7 @@ func TestDecodeEncode(t *testing.T) { }, { // The same, on testnet, with a fallback address mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP - encodedInvoice: "lntb20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3x9et2e20v6pu37c5d9vax37wxq72un98k6vcx9fz94w0qf237cm2rqv9pmn5lnexfvf5579slr4zq3u8kmczecytdx0xg9rwzngp7e6guwqpqlhssu04sucpnz4axcv2dstmknqq6jsk2l", + encodedInvoice: "lntb20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3x9et2e20v6pu37c5d9vax37wxq72un98sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs30kjz30tq32mcqcl0scp9dydfk647cfapf9xcnc2e8ea53j6ya5ne96gylvnrldlnq3x9x6vfqzqmpeuyepzsrgdwevtwd74xppnnxcphq9hqz", valid: true, decodedInvoice: func() *Invoice { return &Invoice{ @@ -519,6 +470,7 @@ func TestDecodeEncode(t *testing.T) { Destination: testPubKey, FallbackAddr: testAddrTestnet, Features: emptyFeatures, + PaymentAddr: fn.Some(specPaymentAddr), } }, beforeEncoding: func(i *Invoice) { @@ -530,7 +482,7 @@ func TestDecodeEncode(t *testing.T) { }, { // On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to get to node 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 - encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85frzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqqqqqqq9qqqvncsk57n4v9ehw86wq8fzvjejhv9z3w3q5zh6qkql005x9xl240ch23jk79ujzvr4hsmmafyxghpqe79psktnjl668ntaf4ne7ucs5csqh5mnnk", + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85frzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqqqqqqq9qqqvsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs6jh3xwjhyldezxr7wx6myldtttk34k2q3jaqc4lyxcsfevjeh09kkv8d23pkdpeuf83wmftrwklazsftuwx8zgghn4jxafd3gq0k6vsqvh5ejq", valid: true, decodedInvoice: func() *Invoice { return &Invoice{ @@ -543,6 +495,7 @@ func TestDecodeEncode(t *testing.T) { FallbackAddr: testRustyAddr, RouteHints: [][]HopHint{testSingleHop}, Features: emptyFeatures, + PaymentAddr: fn.Some(specPaymentAddr), } }, beforeEncoding: func(i *Invoice) { @@ -554,7 +507,7 @@ func TestDecodeEncode(t *testing.T) { }, { // On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 - encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj", + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsa492vv7x3elyzpkck3gyex3al2yywz0sm4clf3t4ayxz49005axh7qx3hp72wflvde27h8mp5rfv4ctvcqc8fy48r4mm69gpeucpadcq9vwr6c", valid: true, decodedInvoice: func() *Invoice { return &Invoice{ @@ -567,6 +520,7 @@ func TestDecodeEncode(t *testing.T) { FallbackAddr: testRustyAddr, RouteHints: [][]HopHint{testDoubleHop}, Features: emptyFeatures, + PaymentAddr: fn.Some(specPaymentAddr), } }, beforeEncoding: func(i *Invoice) { @@ -578,7 +532,7 @@ func TestDecodeEncode(t *testing.T) { }, { // On mainnet, with fallback (p2sh) address 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX - encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppj3a24vwu6r8ejrss3axul8rxldph2q7z9kk822r8plup77n9yq5ep2dfpcydrjwzxs0la84v3tfw43t3vqhek7f05m6uf8lmfkjn7zv7enn76sq65d8u9lxav2pl6x3xnc2ww3lqpagnh0u", + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppj3a24vwu6r8ejrss3axul8rxldph2q7z9sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygslp9th3hnp54ppyeh3cef40gv34rm63xdc6cvgn2wjdmxj860453zjjmm0h55spgl78vwjfnd9x8cjfwd0kckgct5r00d7ufm8t3lc0sp5e70g5", valid: true, decodedInvoice: func() *Invoice { return &Invoice{ @@ -590,6 +544,7 @@ func TestDecodeEncode(t *testing.T) { Destination: testPubKey, FallbackAddr: testAddrMainnetP2SH, Features: emptyFeatures, + PaymentAddr: fn.Some(specPaymentAddr), } }, beforeEncoding: func(i *Invoice) { @@ -655,7 +610,7 @@ func TestDecodeEncode(t *testing.T) { }, { // On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 - encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppqw508d6qejxtdg4y5r3zarvary0c5xw7kknt6zz5vxa8yh8jrnlkl63dah48yh6eupakk87fjdcnwqfcyt7snnpuz7vp83txauq4c60sys3xyucesxjf46yqnpplj0saq36a554cp9wt865", + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppqw508d6qejxtdg4y5r3zarvary0c5xw7ksp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs6dc3xx6alc55ypd3fqh6m4u53ksg72dz0grj0l2rzkeap48xt3u9q2m9mwp5gfu60f84zqnnky0rp9etwqkd43ra83mjxxyqnl0r6kcpru52gx", valid: true, decodedInvoice: func() *Invoice { return &Invoice{ @@ -667,6 +622,7 @@ func TestDecodeEncode(t *testing.T) { Destination: testPubKey, FallbackAddr: testAddrMainnetP2WPKH, Features: emptyFeatures, + PaymentAddr: fn.Some(specPaymentAddr), } }, beforeEncoding: func(i *Invoice) { @@ -678,7 +634,7 @@ func TestDecodeEncode(t *testing.T) { }, { // On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3 - encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qvnjha2auylmwrltv2pkp2t22uy8ura2xsdwhq5nm7s574xva47djmnj2xeycsu7u5v8929mvuux43j0cqhhf32wfyn2th0sv4t9x55sppz5we8", + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsl8ea2vka9sd0vue9qp5tzqzgl4sj982pcpzrrgzdh5hlhdf44dayxy9fudw25kz36t5hgm0q8w4xsavsz28mhtxk567qhmmy6x3eprsp0zwaz6", valid: true, decodedInvoice: func() *Invoice { return &Invoice{ @@ -690,6 +646,7 @@ func TestDecodeEncode(t *testing.T) { Destination: testPubKey, FallbackAddr: testAddrMainnetP2WSH, Features: emptyFeatures, + PaymentAddr: fn.Some(specPaymentAddr), } }, beforeEncoding: func(i *Invoice) { @@ -702,49 +659,10 @@ func TestDecodeEncode(t *testing.T) { { // 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, + // and a payment secret, with the tagged fields in the + // order LND encodes them. + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4pptdvg0d2nj99568qn6ssdy4cygnwuxgw2ukmnwgwz7jpqjz2kszssp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqcresgm59s2yvvuc67ep5wah53rv6a22uwx2u3hm70gx7r334g5qsxy30063452qv2nsfuj88khr8xxdhe79p0ukxqh8lhkagwalshwgqwalarg", + valid: true, decodedInvoice: func() *Invoice { return &Invoice{ Net: &chaincfg.MainNetParams, @@ -763,14 +681,17 @@ func TestDecodeEncode(t *testing.T) { PaymentAddr: fn.Some(specPaymentAddr), } }, - // Skip encoding since LND encode the tagged fields - // in a different order. - skipEncoding: true, + 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 + }, }, { // Send 2500uBTC for a cup of coffee with a custom CLTV // expiry value. - encodedInvoice: "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jscqzysnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66ysxkvnxhcvhz48sn72lp77h4fxcur27z0he48u5qvk3sxse9mr9jhkltt962s8arjnzk8rk59yj5nw4p495747gksj30gza0crhzwjcpgxzy00", + encodedInvoice: "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jscqzysnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs4uwcaqm89zz7y0t8xlhyt80r8mc5yy2q33j0wctghfnwjf3j8fgzpe0q0reenxg92eweyxg67c32x4kqa0wt0xaq0t6s307kltapmssqglqfg3", valid: true, decodedInvoice: func() *Invoice { i, _ := NewInvoice( @@ -781,6 +702,7 @@ func TestDecodeEncode(t *testing.T) { Description(testCupOfCoffee), Destination(testPubKey), CLTVExpiry(144), + PaymentAddr(specPaymentAddr), ) return i @@ -1029,6 +951,8 @@ func TestDecodeDuplicatePaymentHashes(t *testing.T) { // TestNewInvoice tests that providing the optional arguments to the NewInvoice // method creates an Invoice that encodes to the expected string. +// +//nolint:ll func TestNewInvoice(t *testing.T) { t.Parallel() @@ -1043,7 +967,9 @@ func TestNewInvoice(t *testing.T) { return NewInvoice(&chaincfg.MainNetParams, testPaymentHash, time.Unix(1496314658, 0), DescriptionHash(testDescriptionHash), - Description(testPleaseConsider)) + Description(testPleaseConsider), + PaymentAddr(specPaymentAddr), + ) }, valid: false, // Both Description and DescriptionHash set. }, @@ -1055,10 +981,11 @@ func TestNewInvoice(t *testing.T) { testPaymentHash, time.Unix(1496314658, 0), Description(testCupOfCoffee), + PaymentAddr(specPaymentAddr), ) }, valid: true, - encodedInvoice: "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jshwlglv23cytkzvq8ld39drs8sq656yh2zn0aevrwu6uqctaklelhtpjnmgjdzmvwsh0kuxuwqf69fjeap9m5mev2qzpp27xfswhs5vgqmn9xzq", + encodedInvoice: "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jssp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsk7mxg4jd0r638awa4kd6n0pzn6wtg33jg6ptdx68vclhlwpwxz7h4kn2tlah4zn3zwsgvn4xrcszhz0qu9nhem7u8lt4tz5vmkd8a5gpe4w8ra", }, { // 'n' field set. @@ -1067,10 +994,12 @@ func TestNewInvoice(t *testing.T) { testPaymentHash, time.Unix(1503429093, 0), Amount(testMillisat24BTC), Description(testEmptyString), - Destination(testPubKey)) + Destination(testPubKey), + PaymentAddr(specPaymentAddr), + ) }, valid: true, - encodedInvoice: "lnbc241pveeq09pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66jd3m5klcwhq68vdsmx2rjgxeay5v0tkt2v5sjaky4eqahe4fx3k9sqavvce3capfuwv8rvjng57jrtfajn5dkpqv8yelsewtljwmmycq62k443", + encodedInvoice: "lnbc241pveeq09pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygse2tu9s2lqeved59qef46rl2rsd5tkzdc9pjr6z3299aaaskgnk85zg96ng3xlknry8d9f9ssrqan6uj9dtm9mc00wcsam7awtkdgh6sqpf7nq7", }, { // On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 @@ -1081,10 +1010,11 @@ func TestNewInvoice(t *testing.T) { DescriptionHash(testDescriptionHash), FallbackAddr(testRustyAddr), RouteHint(testDoubleHop), + PaymentAddr(specPaymentAddr), ) }, valid: true, - encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj", + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsa492vv7x3elyzpkck3gyex3al2yywz0sm4clf3t4ayxz49005axh7qx3hp72wflvde27h8mp5rfv4ctvcqc8fy48r4mm69gpeucpadcq9vwr6c", }, { // On simnet @@ -1093,10 +1023,12 @@ func TestNewInvoice(t *testing.T) { testPaymentHash, time.Unix(1496314658, 0), Amount(testMillisat24BTC), Description(testEmptyString), - Destination(testPubKey)) + Destination(testPubKey), + PaymentAddr(specPaymentAddr), + ) }, valid: true, - encodedInvoice: "lnsb241pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66jdgev3gnwg0aul7unhqlqvrkp23f0negjsw8ac9f6wa8w9nvppgp3updmr5znhze6l5zneztc0alknntn0wv8fkkgvjqwp0jss66cngqcj9tj6", + encodedInvoice: "lnsb241pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs6ypz232f2e8t2u957z8p82h0gkjpzcv4e3jaggs7vje4mqhtyvspl75kv9mkt3ye35rhh904m4mtz665jwkrmu2cdkjdfcz05s6ut5qqke0kry", }, { // On regtest @@ -1105,10 +1037,12 @@ func TestNewInvoice(t *testing.T) { testPaymentHash, time.Unix(1496314658, 0), Amount(testMillisat24BTC), Description(testEmptyString), - Destination(testPubKey)) + Destination(testPubKey), + PaymentAddr(specPaymentAddr), + ) }, valid: true, - encodedInvoice: "lnbcrt241pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66df5c8pqjjt4z4ymmuaxfx8eh5v7hmzs3wrfas8m2sz5qz56rw2lxy8mmgm4xln0ha26qkw6u3vhu22pss2udugr9g74c3x20slpcqjgq0el4h6", + encodedInvoice: "lnbcrt241pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxuyyugag372x5v5ntk8ep3ag2hnk99acwn58yjnqna7htk364n0khlrx0htkvq58d280y9yeqjxpsludequ3sl5xwf9mthkr6dr8f5qp07r5jw", }, { // Mainnet invoice with two blinded paths. @@ -1122,8 +1056,7 @@ func TestNewInvoice(t *testing.T) { WithBlindedPaymentPath(blindedPath2), ) }, - valid: true, - //nolint:ll + valid: true, encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4js5fdqqqqq2qqqqqpgqyzqqqqqqqqqqqqyqqqqqqqqqqqvsqqqqlnxy0ffrlt2y2jgtzw89kgr3zg4dlwtlfycn3yuek8x5eucnuchqps82xf0m2u6sx5wnjw7xxgnxz5kf09quqsv5zvkgj7d5kpzttp4qz7q5qsyqcyq5pzq2feycsemrh7wvendc4kw3tsmkt25a36ev6kfehrv7ecfkrp5zs9q5zqxqspqtr4avek5quzjn427asptzews5wrczfhychr2sq6ue9phmn35tjqcrspqgpsgpgxquyqjzstpsxsu59zqqqqqpqqqqqqyqq2qqqqqqqqqqqqqqqqqqqqqqqqpgqqqqk8t6endgpc99824amqzk9japgu8synwf3wx4qp4ej2r0h8rghypsqsygpf8ynzr8vwleenxdhzke69wrwed2nk8t9n2e8xudnm8pxcvxs2q5qsyqcyq5y4rdlhtf84f8rgdj34275juwls2ftxtcfh035863q3p9k6s94hpxhdmzfn5gxpsazdznxs56j4vt3fdhe00g9v2l3szher50hp4xlggqkxf77f", }, } @@ -1150,6 +1083,8 @@ func TestNewInvoice(t *testing.T) { // TestMaxInvoiceLength tests that attempting to decode an invoice greater than // maxInvoiceLength fails with ErrInvoiceTooLarge. +// +//nolint:ll func TestMaxInvoiceLength(t *testing.T) { t.Parallel() @@ -1159,7 +1094,7 @@ func TestMaxInvoiceLength(t *testing.T) { }{ { // Valid since it is less than maxInvoiceLength. - encodedInvoice: "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeesrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvrzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv660a66stp9cgqfnp8xklqlxm655zp9x435k6px4hnsajsp00dtcgrxygq67f32qrmxvfk9qcs8mwhzxu8gxr24k2s7364jc9rn4xtn3ncq4t8cej", + encodedInvoice: "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeesr75q20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpfuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4gpqgpsgpgxquyqqqqqqyqqqqq5qqps98sr4yqms4f5lu0f93puw3p37l88yprqvr70022uxls53auvwuj4qypqxpq9qcrssqqqqqqsqqqqzsqqxq57qw5srwz4xnl3ayky836yx8muuusyvps0eaaftsm7zj8h33mj25qsyqcyq5rqwzqqqqqqzqqqqq2qqqczncp6jqdc2560785jcs78gscl0nnjq3sxpl8h49wr0c2g77x8wf2szqsrqszsvpcgqqqqqqgqqqqpgqqrq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpfuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4gpqgpsgpgxquyqqqqqqyqqqqq5qqps98sr4yqms4f5lu0f93puw3p37l88yprqvr70022uxls53auvwuj4qypqxpq9qcrssqqqqqqsqqqqzsqqxq57qw5srwz4xnl3ayky836yx8muuusyvps0eaaftsm7zj8h33mj25qsyqcyq5rqwzqqqqqqzqqqqq2qqqczncp6jqdc2560785jcs78gscl0nnjq3sxpl8h49wr0c2g77x8wf2szqsrqszsvpcgqqqqqqgqqqqpgqqrq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpfuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4gpqgpsgpgxquyqqqqqqyqqqqq5qqpssp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshsu4qvmajk64zafgqprkqpk9tj5ljf9n8l4pkqpzunrs4sy7mpy8vsf2lk8532vdzz4sl0kdf07ccp7uz5mq7zxg5ylhvr45w25q9xsqea9kfg", }, { // Invalid since it is greater than maxInvoiceLength. @@ -1184,8 +1119,12 @@ func TestMaxInvoiceLength(t *testing.T) { // checksum in bech32 strings cannot cause a signature to become valid and // therefore cause a wrong destination to be decoded for invoices where the // destination is extracted from the signature. +// +//nolint:ll func TestInvoiceChecksumMalleability(t *testing.T) { - privKeyHex := "a50f3bdf9b6c4b1fdd7c51a8bbf4b5855cf381f413545ed155c0282f4412a1b1" + // The key is chosen so that the checksum of the encoded invoice + // ends in "p", which the test below relies on. + privKeyHex := "a50f3bdf9b6c4b1fdd7c51a8bbf4b5855cf381f413545ed155c0282f4412b1b1" privKeyBytes, _ := hex.DecodeString(privKeyHex) chain := &chaincfg.SimNetParams var payHash [32]byte @@ -1198,7 +1137,9 @@ func TestInvoiceChecksumMalleability(t *testing.T) { return ecdsa.SignCompact(privKey, hash, true), nil }, } - opts := []func(*Invoice){Description("test")} + opts := []func(*Invoice){ + Description("test"), PaymentAddr(specPaymentAddr), + } invoice, err := NewInvoice(chain, payHash, ts, opts...) if err != nil { t.Fatal(err) From 3db855d690f987b098a0bc5a4025f632b8d4af50 Mon Sep 17 00:00:00 2001 From: MPins Date: Mon, 27 Oct 2025 18:25:14 -0700 Subject: [PATCH 3/5] zpay32: add tests for mandatory fields (p, h, s, n) Add decode tests for an invoice without a payment secret and for p, h, s and n fields with an invalid length. --- zpay32/invoice_internal_test.go | 38 +++++++++++++++++++++++ zpay32/invoice_test.go | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/zpay32/invoice_internal_test.go b/zpay32/invoice_internal_test.go index 78eaf400127..9c91b3e3451 100644 --- a/zpay32/invoice_internal_test.go +++ b/zpay32/invoice_internal_test.go @@ -801,6 +801,24 @@ func TestParseTaggedFields(t *testing.T) { &distinctPaymentHashes, fieldTypeP, secondPaymentHash, )) + // validThenMalformed returns a valid field of the given type followed + // by a second field of the same type with an invalid length. + validThenMalformed := func(fieldType byte, valid []byte) []byte { + var b bytes.Buffer + require.NoError(t, writeTaggedField(&b, fieldType, valid)) + require.NoError(t, writeTaggedField(&b, fieldType, []byte{0})) + + return b.Bytes() + } + + hashData, err := bech32.ConvertBits(testPaymentHash[:], 8, 5, true) + require.NoError(t, err) + + pubKeyData, err := bech32.ConvertBits( + testPubKey.SerializeCompressed(), 8, 5, true, + ) + require.NoError(t, err) + tests := []struct { name string data []byte @@ -832,6 +850,26 @@ func TestParseTaggedFields(t *testing.T) { data: malformedThenValid.Bytes(), wantErr: ErrInvalidFieldLength, }, + { + name: "valid then malformed payment hash", + data: validThenMalformed(fieldTypeP, hashData), + wantErr: ErrInvalidFieldLength, + }, + { + name: "valid then malformed payment secret", + data: validThenMalformed(fieldTypeS, hashData), + wantErr: ErrInvalidFieldLength, + }, + { + name: "valid then malformed description hash", + data: validThenMalformed(fieldTypeH, hashData), + wantErr: ErrInvalidFieldLength, + }, + { + name: "valid then malformed destination", + data: validThenMalformed(fieldTypeN, pubKeyData), + wantErr: ErrInvalidFieldLength, + }, { name: "identical payment hashes", data: identicalPaymentHashes.Bytes(), diff --git a/zpay32/invoice_test.go b/zpay32/invoice_test.go index 6410cdf230c..ec3768cf4c1 100644 --- a/zpay32/invoice_test.go +++ b/zpay32/invoice_test.go @@ -253,11 +253,35 @@ func TestDecodeEncode(t *testing.T) { encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85frqg00000000j9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqsj5cgu", // invalid routing info length: not a multiple of 51 valid: false, }, + { + // no payment secret set + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jszjlrkes0mgx4ghum45ha5gkzlac8xmrr4skgweyp6xqxucu4wz4j8uvpg0jznsesezax6hdt0gtyn3tuqpfy2curryn83zygkydmpxcqdfu7k0", + valid: false, + wantErr: ErrPaymentSecretNotFound, + }, + { + // payment secret with invalid length. + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsspszyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zeams9aqn0ar5rz2lu6g29vqlc5rxwg384dnh482wpjpslmx8dfy6l8l2u6d75xe8lj3te4rfg37u0dcatyzxews2skx8a6dkt4tt0qqv548nr", + valid: false, + wantErr: ErrInvalidFieldLength, + }, + { + // pubkey set in 'n' field with invalid length. + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsnpjz44wxwc2yzxsw3qej933wl5sn6qpwmj4m9az7gs7mc8exnwe45sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsa45ms8u8avxsgp0mjrxnf7u888ezl95wse779emq4wawy56en7hpyarvzq8rshgx80luw9m3cxe2mj5kv95pfljpzc3sjw76azyqdvgqpu7a59", + valid: false, + wantErr: ErrInvalidFieldLength, + }, { // no payment hash set encodedInvoice: "lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsjv38luh6p6s2xrv3mzvlmzaya43376h0twal5ax0k6p47498hp3hnaymzhsn424rxqjs0q7apn26yrhaxltq3vzwpqj9nc2r3kzwccsplnq470", valid: false, }, + { + // payment hash set with invalid length. + encodedInvoice: "lnbc20m1pvjluezppsqgpsgpgxquyqjqqpqgpsgpgxquyqjqqpqgpsgpgxquyqjqgzdq5xysxxatsyp3k7enxv4jsnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs43fqhf8np9n9xs6zgepegrmqfkq6crexhvhqp5958mettsrm6mh9jsxkeuc8vx5n0fg53hyjf5shnxzz5p67xljjq9rwt54rwfj5pfgq9ythaz", + valid: false, + wantErr: ErrInvalidFieldLength, + }, { // Both Description and DescriptionHash set. encodedInvoice: "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsu6zmmjhtn0uhrqje9x4y2c05mjvvg0ftwg32cnjkzs2vwmuf7ltysjlvkvh2pkgg20ssp4muprn93ezasdgn6aezu5ec5g54nju9kkgqtf6fht", @@ -268,6 +292,12 @@ func TestDecodeEncode(t *testing.T) { encodedInvoice: "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqwul6apumndhf3t4v50sdx0vc9jma3cjfq49reu9a6rsadhs933nsau8vwumegq0scs492xx5s6zp6rmr50gd2pdkv285kzsr7zt5xjsp69w7kw", valid: false, }, + { + // DescriptionHash set with invalid length. + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhpskmm8utp5qqmw6ysf8h2yuqmgmudkagnv20d7fqgltr74mwxpsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs2ul3cn2n4j3gcl5yxv0mwp7zkslunmv405hlz0uesckxzu5nk60pv6pfq3zkuewfugx28u3vp9y36eutpa0kdwahu2perdlr4cdjclqpl7zwtf", + valid: false, + wantErr: ErrInvalidFieldLength, + }, { // Has a few unknown fields, should just be ignored. encodedInvoice: "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqtq0w4hxkmn0wahrqvgzq0w4hxkmn0wahrqvselxnzkx7sdr4jjeqz8c022yhhkwjskhg32kcprhc08atq06megxyy8xclrsuc2tpfnayhskjj6dxmy34330rxd32cp3evjcdaqqkmdqprtqpuh", @@ -949,6 +979,30 @@ func TestDecodeDuplicatePaymentHashes(t *testing.T) { } } +// TestDecodeSkipPaymentSecretCheck checks that an invoice without a payment +// secret is rejected by default, and accepted when the payment secret check is +// skipped. +func TestDecodeSkipPaymentSecretCheck(t *testing.T) { + t.Parallel() + + encodedInvoice := "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5" + + "rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7en" + + "xv4jszjlrkes0mgx4ghum45ha5gkzlac8xmrr4skgwey" + + "p6xqxucu4wz4j8uvpg0jznsesezax6hdt0gtyn3tuqpf" + + "y2curryn83zygkydmpxcqdfu7k0" + + _, err := Decode(encodedInvoice, &chaincfg.MainNetParams) + require.ErrorIs(t, err, ErrPaymentSecretNotFound) + + invoice, err := Decode( + encodedInvoice, &chaincfg.MainNetParams, + WithSkipPaymentSecretCheck(), + ) + require.NoError(t, err) + require.Equal(t, testPaymentHash, *invoice.PaymentHash) + require.True(t, invoice.PaymentAddr.IsNone()) +} + // TestNewInvoice tests that providing the optional arguments to the NewInvoice // method creates an Invoice that encodes to the expected string. // From ff46b4b5ea36c0a801ccb06547e22ae405a3086b Mon Sep 17 00:00:00 2001 From: MPins Date: Thu, 24 Sep 2026 16:22:45 -0300 Subject: [PATCH 4/5] invoicesrpc: skip payment secret check when decoding stored invoices Invoices created by lnd versions before v0.9.0 have no payment secret. Since zpay32.Decode now rejects such invoices by default, decodePayReq uses WithSkipPaymentSecretCheck so that ListInvoices, LookupInvoice and SubscribeInvoices can still return them. The invoice is only converted for display here, never paid. --- lnrpc/invoicesrpc/utils.go | 9 +++++++-- lnrpc/invoicesrpc/utils_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 lnrpc/invoicesrpc/utils_test.go diff --git a/lnrpc/invoicesrpc/utils.go b/lnrpc/invoicesrpc/utils.go index f59c6dfbb62..f9dd1d641fc 100644 --- a/lnrpc/invoicesrpc/utils.go +++ b/lnrpc/invoicesrpc/utils.go @@ -33,8 +33,13 @@ func decodePayReq(invoice *invoices.Invoice, }, nil } - var err error - decoded, err := zpay32.Decode(paymentRequest, activeNetParams) + // Invoices created by lnd versions before v0.9.0 have no payment + // secret. We skip that check so that they can still be returned by the + // RPCs, since the invoice is only converted for display here. + decoded, err := zpay32.Decode( + paymentRequest, activeNetParams, + zpay32.WithSkipPaymentSecretCheck(), + ) if err != nil { return nil, fmt.Errorf("unable to decode payment "+ "request: %v", err) diff --git a/lnrpc/invoicesrpc/utils_test.go b/lnrpc/invoicesrpc/utils_test.go new file mode 100644 index 00000000000..ca801570b7f --- /dev/null +++ b/lnrpc/invoicesrpc/utils_test.go @@ -0,0 +1,31 @@ +package invoicesrpc + +import ( + "testing" + + "github.com/btcsuite/btcd/chaincfg/v2" + "github.com/lightningnetwork/lnd/invoices" + "github.com/stretchr/testify/require" +) + +// TestCreateRPCInvoiceWithoutPaymentSecret checks that a stored invoice +// without a payment secret, as created by lnd versions before v0.9.0, can +// still be converted to its RPC representation. +func TestCreateRPCInvoiceWithoutPaymentSecret(t *testing.T) { + t.Parallel() + + payReq := "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5" + + "rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7en" + + "xv4jszjlrkes0mgx4ghum45ha5gkzlac8xmrr4skgwey" + + "p6xqxucu4wz4j8uvpg0jznsesezax6hdt0gtyn3tuqpf" + + "y2curryn83zygkydmpxcqdfu7k0" + + invoice := &invoices.Invoice{ + PaymentRequest: []byte(payReq), + State: invoices.ContractOpen, + } + + rpcInvoice, err := CreateRPCInvoice(invoice, &chaincfg.MainNetParams) + require.NoError(t, err) + require.Equal(t, payReq, rpcInvoice.PaymentRequest) +} From 6b9a857759ca5dc5a6204e1aa62db14d166b16f1 Mon Sep 17 00:00:00 2001 From: MPins Date: Wed, 10 Jun 2026 07:10:21 -0300 Subject: [PATCH 5/5] docs: add release note for BOLT 11 field validation --- docs/release-notes/release-notes-0.22.0.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/release-notes/release-notes-0.22.0.md b/docs/release-notes/release-notes-0.22.0.md index 249ca6c8255..521f11f9838 100644 --- a/docs/release-notes/release-notes-0.22.0.md +++ b/docs/release-notes/release-notes-0.22.0.md @@ -119,6 +119,23 @@ later in the reservation flow as a funder-balance-dust error; they now surface a clearer, spec-aligned error string up front. +* [Require an explicit `channel_type` during channel + funding](https://github.com/lightningnetwork/lnd/pull/11064). It is now always + set in `open_channel` and echoed back in `accept_channel`, and an + `open_channel` that omits it is rejected. Implicit commitment type negotiation + is removed; if the RPC caller doesn't request a type, a default is derived + from both peers' features and signaled explicitly. + +* BOLT 11 invoice decoding [now + fails](https://github.com/lightningnetwork/lnd/pull/10326) if any field with + a fixed length (`p`, `h`, `s`, `n`) does not have the correct length (52, 52, + 52, 53), instead of skipping the field, and if the invoice has no payment + secret (`s`) and no blinded paths, as the BOLT 11 reader requirements + demand. The length rule was clarified in + [lightning/bolts#1243](https://github.com/lightning/bolts/pull/1243). + Stored invoices created by lnd versions before v0.9.0, which have no payment + secret, are still returned by the invoice RPCs. + ## BOLT 12 (Offers) * [Initial BOLT 12 Offer codec](https://github.com/lightningnetwork/lnd/pull/10789): @@ -201,3 +218,5 @@ * Jared Tobin * Kevin Cai * Vandit Singh +* Nishant Bansal +* Pins