Skip to content

zpay32: enforce BOLT 11 fixed lengths, single s, low-S with n - #11357

Open
Roasbeef wants to merge 3 commits into
lightningnetwork:masterfrom
Roasbeef:zpay32-bolt11-vectors
Open

Roasbeef wants to merge 3 commits into
lightningnetwork:masterfrom
Roasbeef:zpay32-bolt11-vectors

Conversation

@Roasbeef

Copy link
Copy Markdown
Member

In this PR, we tighten the BOLT 11 decoder in three places where it was more lenient than the spec, and add the spec's invoice test vectors from lightning/bolts#1357 as a test that runs every one of them.

Running those vectors against zpay32.Decode showed that we accepted five invoices the spec says a reader must reject. Three of them are fixed in the decoder:

  • A fixed length field (p, h, s, n) with the wrong data_length was treated as absent rather than failing the invoice. For n this meant we'd silently fall back to public key recovery. We now return ErrInvalidFixedFieldLength.
  • A second s field was skipped, which leaves the same first vs last ambiguity for the payment secret that zpay32: reject duplicate payment hash fields #11190 closed for the payment hash. bolts#1357 now requires exactly one p and exactly one s, so we return ErrDuplicatePaymentAddr.
  • A high-S signature was accepted even when the invoice had an explicit n field. Only the public key recovery path may accept high-S, so we now return ErrHighSSignature.

The other two are already enforced, just later on purpose. An unknown required feature bit is rejected by pathfinding by default, so that decodepayreq can still show the invoice. The test uses WithErrorOnUnknownFeatureBit to check it at decode time. A missing s field stays legal for Decode, as we create blinded path invoices without a payment address. The router already rejects a payment request that has neither, and the test documents that exception.

Compatibility

All three new rejections only affect invoices lnd never produces: our encoder always writes the fixed lengths, a single s field, and low-S signatures. Stored invoices that we decode for ListInvoices are our own, so they're unaffected. A third party invoice that relied on one of these leniencies will now fail to decode. Under the spec it was never valid.

The vectors live in zpay32/test-vectors/invoice-test.json, mirroring bolt12/test-vectors. They come from the bolts#1357 branch, so we should resync the file once that PR lands, in case it changes further.

In this commit, we bring the decoder in line with three BOLT 11 reader
requirements it was lenient about.

A fixed length field (`p`, `h`, `s`, `n`) with the wrong `data_length`
was treated as absent, so an invoice with a malformed `n` field would
silently fall back to public key recovery. BOLT 11 says the reader MUST
fail in that case, so `parse32Bytes` and `parseDestination` now return
`ErrInvalidFixedFieldLength`.

A second `s` field was skipped, which leaves the same first vs last
ambiguity for the payment secret that we already closed for the payment
hash. Following lightning/bolts#1357, we now reject it with
`ErrDuplicatePaymentAddr`.

When the invoice carries an explicit `n` field, the signature must be in
low-S form. Only the public key recovery path may accept high-S, so we
now return `ErrHighSSignature` for a high-S signature verified against
`n`.

A missing `s` field is still accepted here. lnd creates blinded path
invoices without a payment address, and the router already rejects a
payment request that has neither.
In this commit, we add the invoice test vectors from lightning/bolts#1357
(`bolt11/invoice-test.json`) and run every one of them through `Decode`,
mirroring how the BOLT 12 vectors live under `bolt12/test-vectors`.

Each invalid vector has a single defect, so a pass means we reject it for
the intended reason. Unknown required feature bits are checked through
`WithErrorOnUnknownFeatureBit`, as by default we leave those for
pathfinding. The missing `s` vector is the one documented exception: we
assert it decodes with no payment address, as that rule is enforced by
the router.
@github-actions github-actions Bot added the severity-medium Focused review required label Oct 19, 2026
@github-actions

Copy link
Copy Markdown

🟡 PR Severity: MEDIUM

gh pr view | 6 files | 350 lines changed

🟡 Medium (2 files)
  • zpay32/decode.go - BOLT-11 invoice decoding logic
  • zpay32/invoice.go - BOLT-11 invoice data structures/helpers
🟢 Low (4 files)
  • docs/release-notes/unreleased/11357.md - release notes
  • zpay32/bolt11_vectors_test.go - new test file
  • zpay32/invoice_internal_test.go - test-only changes
  • zpay32/test-vectors/invoice-test.json - test vector fixture data

Analysis

This PR is confined to the zpay32 package (BOLT-11 invoice encode/decode), which falls under MEDIUM severity. The only non-test code changes are modest (~41 lines across decode.go and invoice.go), well below the thresholds for a severity bump (20 files / 500 lines / multiple critical packages). No critical or high-severity packages (lnwallet, htlcswitch, channeldb, etc.) are touched. A knowledgeable reviewer familiar with the BOLT-11 spec should review the decode/encode logic changes; the accompanying test vectors and release note are routine.


To override, add a severity-override-{critical,high,medium,low} label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

severity-medium Focused review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant