Skip to content

bolt12: split off review cleanups and test changes from #11191 - #11237

Merged
saubyk merged 13 commits into
lightningnetwork:masterfrom
bitromortac:20260923-9259-dispatch
Sep 24, 2026
Merged

saubyk merged 13 commits into
lightningnetwork:masterfrom
bitromortac:20260923-9259-dispatch

Conversation

@bitromortac

@bitromortac bitromortac commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

This PR takes the commits from #11191 that do not change behavior and puts them in a separate PR. The goal is to make #11191 smaller and to let these changes merge on their own.

Most commits answer review threads on the earlier BOLT 12 PRs. They fix comments and names, and they remove tests that break each time the spec vectors are vendored again.

The round-trip tests for offer, invoice_request and invoice now fill every field. They also compare the decoded struct with the fixture. A byte-identity check alone cannot detect a field that the encoder writes but the decoder does not read. That field comes back as an unknown TLV and encodes again to the same bytes.

The encoder now uses the same strict features record as the decoder. This keeps the Merkle leaf bytes stable because the encoder no longer relies on lnwire to stay minimal. The encoded output does not change, and the spec signature vectors pass without changes.

No release note, because nothing here changes behavior.

The comment standing where the writer-side feature check used to be argued its
case in broken prose and used the word the reviewer found misleading. The file
already has a form for a rule the codec cannot enforce, so use it and name the
thing plainly: the bits are the caller's own and the reader checks them
against the known bits it is given.

Finding F11.
lightningnetwork#10941 (comment)
Two comments on the payer-side checks called the key "the final blinded_node_id
on the arrival path". Arrival is the receiver's view, which is how the spec
states the writer rule, and these run on the payer, where the same key is the
one it sent the request to. The phrase also collides with the separate
reply-path arrival rule. The writer comment keeps its wording, which is correct
there.

Finding F15.
lightningnetwork#10941 (comment)
The package doc claimed no LND dependencies while importing lnwire for the
pure-TLV framing, the blinded-path types and the feature vector.
The continuation error said a '+' must precede a non-whitespace character,
while the code skips whitespace after the marker and then requires a
neighbour. The rule it enforces is that a marker joins two characters, so say
that. The README line ran to 129 columns.

Findings F43 and F44.
lightningnetwork#11001 (comment)
lightningnetwork#11001 (comment)
The invoice path called a map of known feature bits a catalogue, a word that
appears nowhere else in lnd. The other two readers in the same file already
call it knownFeatures, and lnwire calls the same parameter featureNames, so
the invoice path was the outlier. The type becomes InvoiceKnownFeatures,
checkFeatures names its parameter like its callers do, and the word is gone
from the docstrings and the test name.

The struct itself stays: named fields are what stop a caller passing the
blinded-path bits where the invoice bits belong.

Findings F39 and F12.
lightningnetwork#10941 (comment)
The census asserted how many invalid vectors fail at each of the three layers,
counts that break on any re-vendoring of the spec vectors while proving
nothing the table above it does not: that test already requires every invalid
vector to be rejected and every valid one to pass.
@saubyk saubyk added the bolt12 label Sep 23, 2026
@saubyk saubyk added this to lnd v0.22 Sep 23, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in lnd v0.22 Sep 23, 2026
@saubyk saubyk moved this from Backlog to In review in lnd v0.22 Sep 23, 2026
@saubyk saubyk added this to the v0.22.0 milestone Sep 23, 2026

@ViktorT-11 ViktorT-11 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, LGTM 🔥

Comment thread bolt12/subtypes.go Outdated
The round-trip fixture paired erroneous_field 82, invreq_amount, with a
suggested_value of 00 01 86 a0. The leading zero makes that a non-minimal tu64,
so a peer decoding it as the field's own type fails and loses the correction the
message exists to carry. The fixture now uses the canonical encoding.

The writer cannot catch this yet, and the note claiming the schema is caller
context overstated the obstacle: the package defines every field number and
type for both messages, it just has no field-number to type table. That is now
a TODO, and Encode's contract says the value is unchecked.

Finding F18.
lightningnetwork#10958 (comment)
The round trip covered four of eleven fields and asserted byte identity only.
Byte identity cannot see a field that is wired into the encode path but not
into the decode path: such a field survives as an unknown TLV and re-encodes
cleanly while its typed value disappears. Comparing the decoded struct against
the fixture catches it, verified by dropping offer_quantity_max from the decode
stream. The fixture also carries an unknown odd TLV in the offer range, so the
offer keeps the byte-exact preservation its siblings already pin.

Findings F31 and F2.
lightningnetwork#10832 (comment)
lightningnetwork#10789 (comment)
The round trip covered four of twenty-one fields and asserted byte identity
only, which cannot see a field wired into the encode path but not into the
decode path. Comparing the decoded struct against the fixture catches it,
verified by dropping invreq_quantity from the decode stream. The request is
also signed rather than carrying a placeholder, so the fixture is one a reader
would accept.

Finding F31.
lightningnetwork#10832 (comment)
The round trip populated six of thirty fields despite its name, and asserted
byte identity only, which cannot see a field wired into the encode path but
not into the decode path. Comparing the decoded struct against the fixture
catches it, verified by dropping invoice_relative_expiry from the decode
stream. The invoice is signed and read-validated too, so the fixture is one a
payer would accept.

Finding F31.
lightningnetwork#10832 (comment)
The request side had a test for the offer_amount times quantity overflow, the
invoice side did not, and it was the only uncovered branch in the invoice
amount check. Verified by neutering the guard, which makes the new case accept
an invoice_amount of one against an authorized amount that wrapped to zero.

Finding F9.
lightningnetwork#10941 (comment)
Both decode tables matched error substrings where the decoder returns a
sentinel, so a reworded message would keep passing a test that no longer
proves anything. Four cases now use require.ErrorIs, and the rest stay on
substrings because they assert ad-hoc messages with no sentinel behind them.
The mixed idiom is the one TestDecodeChainsRecord already uses in this file.

Finding F10.
lightningnetwork#10941 (comment)
The vector table decoded each vector and compared every record against the
expected hex, but never fed one back out. So the message-level Encode and the
bech32 writer were only ever tested against fixtures we wrote ourselves. Each
valid vector now has to reproduce its own string, which passes for all of them
today and fails if the writer gains an extra character.

Finding F45.
lightningnetwork#11001 (comment)
@bitromortac
bitromortac force-pushed the 20260923-9259-dispatch branch from cf86678 to 59739d6 Compare September 24, 2026 08:11
@github-actions github-actions Bot added the severity-medium Focused review required label Sep 24, 2026
@github-actions

Copy link
Copy Markdown

🟡 PR Severity: MEDIUM

gh pr view | 12 files | 652 lines changed

🟡 Medium (5 files)
  • bolt12/bech32.go - bolt12 package, not categorized in a higher tier
  • bolt12/doc.go - bolt12 package, not categorized in a higher tier
  • bolt12/invoice.go - bolt12 package, not categorized in a higher tier
  • bolt12/invoice_error.go - bolt12 package, not categorized in a higher tier
  • bolt12/validate.go - bolt12 package, not categorized in a higher tier
🟢 Low (7 files)
  • bolt12/invoice_error_test.go - test-only change
  • bolt12/invoice_request_test.go - test-only change
  • bolt12/invoice_test.go - test-only change
  • bolt12/offer_test.go - test-only change
  • bolt12/subtypes_test.go - test-only change
  • bolt12/validate_test.go - test-only change
  • bolt12/test-vectors/README.md - documentation change

Analysis

All changes are confined to the bolt12/* package (BOLT12 offers/invoices), which is not explicitly listed in the critical or high severity tiers, so it defaults to medium for its non-test source files. Excluding test and doc files, only 5 files and ~56 lines changed, well below the thresholds for a severity bump (>20 files or >500 lines), and no critical packages (lnwallet, htlcswitch, contractcourt, etc.) are touched. No severity bump applies.


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

@GeorgeTsagk GeorgeTsagk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@saubyk
saubyk merged commit ff27d5b into lightningnetwork:master Sep 24, 2026
80 of 81 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants