What changed, and why it matters
This commit only adds a new test. It does not change any production code. The test decodes a standard BOLT12 invoice request from the official specification's test vectors and checks that re-encoding it produces exactly the same bytes. This is a defensive quality-improvement change meant to catch canonical-encoding bugs before they reach real peers.
No action required. This is a test-only addition that improves confidence in BOLT12 invoice request canonical encoding. Reviewers may optionally run the new test and consider whether similar coverage is needed for other BOLT12 message types.
Security signals we found
Adds canonical-encoding regression test for BOLT12 invoice_request signature verification
Uses official BOLTs signature-test vector as ground-truth wire bytes
Verifies re-encoded bytes match decoded bytes, which is required for valid Merkle-root signatures
Evidence from the diff
The diff adds TestDecodeInvoiceRequestBech32String to bolt12/invoice_request_test.go. It uses the upstream BOLTs signature-test.json invoice_request bech32 string, decodes it through Decode and DecodeInvoiceRequest, asserts expected field values (metadata, currency, amount, description, payer_id, signature), verifies AllRecords is populated, and checks that Encode() is byte-identical to the original TLV bytes. No implementation code is modified.
Changed components
bolt12/invoice_request_test.goInspect captured patch +101 / −0
### bolt12/invoice_request_test.go
@@ -2,6 +2,7 @@ package bolt12
import (
"bytes"
+ "encoding/hex"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
@@ -169,3 +170,103 @@ func TestNewInvoiceRequestFromOfferMirrorsUnknownFields(t *testing.T) {
}
require.True(t, found, "unknown offer TLV not mirrored into request")
}
+
+// TestDecodeInvoiceRequestBech32String decodes the invoice_request string and
+// verifies key fields. This exercises the low-level Decode plus
+// DecodeInvoiceRequest path.
+func TestDecodeInvoiceRequestBech32String(t *testing.T) {
+ t.Parallel()
+
+ // From upstream lightning/bolts signature-test.json: the
+ // invoice_request bolt12 string.
+ lnrStr := "lnr1qqyqqqqqqqqqqqqqqcp4256ypqqkgzshgysy6ct5d" +
+ "pjk6ct5d93kzmpq23ex2ct5d9ek293pqthvwfzadd7jej" +
+ "es8q9lhc4rvjxd022zv5l44g6qah82ru5rdpnpjkppqvj" +
+ "x204vgdzgsqpvcp4mldl3plscny0rt707gvpdh6ndydfac" +
+ "z43euzqhrurageg3n7kafgsek6gz3e9w52parv8gs2hlxz" +
+ "k95tzeswywffxlkeyhml0hh46kndmwf4m6xma3tkq2lu0" +
+ "4qz3slje2rfthc89vss"
+
+ _, tlvBytes, err := Decode(lnrStr)
+ require.NoError(t, err)
+
+ ir, err := DecodeInvoiceRequest(tlvBytes)
+ require.NoError(t, err)
+
+ // Verify invreq_metadata is set (8 zero bytes).
+ var metadata []byte
+ ir.InvreqMetadata.WhenSome(
+ func(r tlv.RecordT[tlv.TlvType0, tlv.Blob]) {
+ metadata = r.Val
+ },
+ )
+ require.Equal(t, make([]byte, 8), metadata)
+
+ // Verify offer_currency is "USD".
+ var currency []byte
+ ir.OfferCurrency.WhenSome(
+ func(r tlv.RecordT[tlv.TlvType6, tlv.Blob]) {
+ currency = r.Val
+ },
+ )
+ require.Equal(t, "USD", string(currency))
+
+ // Verify offer_amount is 100.
+ var amount TUint64
+ ir.OfferAmount.WhenSome(
+ func(r tlv.RecordT[tlv.TlvType8, TUint64]) {
+ amount = r.Val
+ },
+ )
+ require.Equal(t, TUint64(100), amount)
+
+ // Verify offer_description is "A Mathematical Treatise".
+ var desc []byte
+ ir.OfferDescription.WhenSome(
+ func(r tlv.RecordT[tlv.TlvType10, tlv.Blob]) {
+ desc = r.Val
+ },
+ )
+ require.Equal(t, "A Mathematical Treatise", string(desc))
+
+ // Verify invreq_payer_id is Bob's compressed pubkey (0x424242...
+ // privkey).
+ var payerIDSet bool
+ ir.InvreqPayerID.WhenSome(
+ func(r tlv.RecordT[tlv.TlvType88, *btcec.PublicKey]) {
+ payerIDSet = true
+ },
+ )
+ require.True(t, payerIDSet)
+
+ // Verify signature is present.
+ var (
+ sig [64]byte
+ sigSet bool
+ )
+ ir.Signature.WhenSome(
+ func(r tlv.RecordT[tlv.TlvType240, [64]byte]) {
+ sig = r.Val
+ sigSet = true
+ },
+ )
+ require.True(t, sigSet)
+
+ expectedSig := "b8f83ea3288cfd6ea510cdb481472575141e8d87" +
+ "44157f98562d162cc1c472526fdb24befefbdebab4dbb" +
+ "726bbd1b7d8aec057f8fa805187e5950d2bbe0e5642"
+ require.Equal(t, expectedSig, hex.EncodeToString(sig[:]))
+
+ // Verify decode populated the canonical record set used by the Merkle
+ // tree, so every wire TLV must be reachable through AllRecords for
+ // signature verification to find them.
+ require.NotEmpty(t, ir.AllRecords())
+
+ // Re-encode must be byte-identical to the decoded wire bytes: the
+ // signature is over the Merkle root of this canonical encoding, so any
+ // reordering, dropped TLV, or non-canonical integer would invalidate
+ // it.
+ reencoded, err := ir.Encode()
+ require.NoError(t, err)
+ require.Equal(t, tlvBytes, reencoded)
+}Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.