Skip to content

bolt12: add InvoiceRequest codec and structural validators#10832

Draft
bitromortac wants to merge 11 commits into
lightningnetwork:masterfrom
bitromortac:2604-bolt12-1b
Draft

bolt12: add InvoiceRequest codec and structural validators#10832
bitromortac wants to merge 11 commits into
lightningnetwork:masterfrom
bitromortac:2604-bolt12-1b

Conversation

@bitromortac
Copy link
Copy Markdown
Collaborator

Based on #10789 (last three commits), part of #10736.

Adds the BOLT 12 InvoiceRequest message struct with PureTLVMessage-based Encode/Decode and the structural ValidateInvoiceRequestRead/Write validators. Signature verification and offer cross-validation are deferred to the Invoice and bolt12handler layers respectively.

Todo:

  • Will need to do another pass on this before deeper review can start

Add UnsignedRangeFunc and the SerialiseFieldsToSignFn /
ExtraSignedFieldsFromTypeMapFn variants so callers with non-BOLT 7 v2
signed ranges (e.g. BOLT 12, which reserves only 240-1000) can plug in
their own predicate. The existing SerialiseFieldsToSign and
ExtraSignedFieldsFromTypeMap entry points keep their behaviour by
delegating to the Fn variants with InUnsignedRange.
This gives us easier optional tlv field handling, which we will use for
the following message definitions.
Introduce the canonical lnwire.BlindedPath / BlindedPaths codec with a
sealed IntroductionNode sum-type covering both the BOLT 4 pubkey and
sciddir variants. The codec gates every variable-length subfield against
an io.LimitedReader. It fails closed on the encoder side so invalid
input never hits the wire.

This commit is a pure addition: no existing caller changes. Subsequent
commits migrate OnionMessagePayload and the bolt12 message structs to
consume the new codec.
Switch OnionMessagePayload.ReplyPath from *sphinx.BlindedPath to
*lnwire.BlindedPath. The reply-path TLV is now produced and consumed by
(*lnwire.BlindedPath).Record(), which honours the BOLT 4 sciddir_or_pubkey
introduction-node form. The legacy decoder gated on a 67-byte minimum
length and silently rejected reply paths whose introduction node used
the 9-byte sciddir variant.

The legacy replyPathRecord / replyPathSize / encodeReplyPath /
decodeReplyPath / blindedHopSize / encodeBlindedHop / decodeBlindedHop
helpers and the unused ErrNoHops sentinel are deleted.

Consumers update mechanically: routing/route's
OnionMessageBlindedPathToSphinxPath replyPath parameter, the
onionmessage.OnionMessageUpdate field, the rpcserver onion-message
subscription bridge, and the lnwire test utilities now use the lnwire
type directly. The new TestOnionMessagePayloadRoundTrip "sciddir intro
reply path" subtest pins the BOLT 4 spec fix.
Introduce the ChainsRecord subtype used by the offer_chains and
invoice_chains TLV fields. Decoding caps the count at maxOfferChains to
bound allocation.
The Offer struct models a long-lived, reusable BOLT 12 payment template.
It defines TLV fields as optional records and exposes Encode/DecodeOffer
for round-trip serialization. The struct implements
lnwire.PureTLVMessage; AllRecords filters the decoded TypeMap through
bolt12InUnsignedRange to derive any signed-range extras the encoder must
re-emit, keeping offer_id and the Merkle root stable across encoders
that understand a wider set of even/odd extensions.
ValidateOfferRead and ValidateOfferWrite enforce the codec-side portion
of the BOLT 12 offer reader and writer requirements. Reader rules cover
TLV range, even-feature-bit rejection, chain mismatch, dependency rules
between offer_amount/description/currency, missing issuer identity,
zero-hop blinded paths, and offer expiry. Writer rules mirror the same
dependency and identity guards plus a defense-in-depth empty-
offer_chains rejection.

offer_currency is validated against the ISO 4217 registry via
golang.org/x/text/currency (now a direct dependency); offer_issuer_id is
verified to be an on-curve SEC1 compressed point on both read and write
paths. Encode invokes Validate so invalid bytes never reach the wire.
Document that the introduction_node field in an OnionMessageUpdate's
reply_path is passed through verbatim from the wire, potentially
carrying either a 33-byte pubkey or a 9-byte sciddir form. Subscribers
wishing to reply must resolve sciddir forms against their local channel
graph.

The SubscribeOnionMessages bridge is refactored to use a new
marshallBlindedPath helper, ensuring a nil reply path remains nil in the
RPC response rather than being emitted as an empty struct.
The InvoiceRequest struct represents a BOLT 12 payment request as
defined in the offers specification. It mirrors relevant fields from the
original offer and adds payer-specific fields (metadata, amount,
quantity, payer_id, note, reply paths, BIP 353 name) and a Schnorr
signature.

The struct implements lnwire.PureTLVMessage and supports round-trip
serialization via Encode/DecodeInvoiceRequest. getInvoiceRequestOfferChains
and checkInvreqQuantity provide spec-mandated defaults and coupling
checks between the offer and request layers.
ValidateInvoiceRequestRead and ValidateInvoiceRequestWrite enforce the
structural BOLT 12 requirements an invoice request can be checked
against on its own. The reader validates incoming requests. The writer
catches out-of-range types in decoded-then-mutated requests before they
leave the local boundary. Type 240 carries the signature and sits
outside the allowed range by spec design. Both validators skip it
during the range scan.

Two reader MUSTs are deferred. Schnorr signature verification against
the merkle root keyed by invreq_payer_id lands with the Invoice
message, where the merkle and signing primitives are shared. Offer
cross-validation requires an Offer reference the structural validator
does not carry, and lands in the bolt12handler layer where both the
request and the stored Offer are in scope.
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces the foundational codec and validation logic for BOLT 12 InvoiceRequest messages. It establishes a new bolt12 package dedicated to the encoding, decoding, and structural validation of offers and invoice requests. Furthermore, it refactors the existing BlindedPath implementation to support the introduction node variants required by the specification, ensuring that both Pubkey and Sciddir forms are correctly handled throughout the codebase, including RPC surfaces.

Highlights

  • InvoiceRequest Implementation: Added the InvoiceRequest struct along with its associated TLV-based encoding and decoding logic.
  • Structural Validation: Implemented comprehensive structural validators for both Offers and InvoiceRequests to ensure spec compliance.
  • BlindedPath Refactoring: Refactored BlindedPath to support IntroductionNode variants, specifically adding support for both Pubkey and Sciddir forms.
  • RPC and Wire Updates: Updated lnrpc and lnwire packages to accommodate the new BlindedPath structure and ensure consistent handling across the daemon.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements the core BOLT 12 Offer and Invoice Request functionality, introducing the bolt12 package for TLV encoding, decoding, and extensive validation. It also refactors lnwire to include a shared BlindedPath implementation supporting both pubkey and sciddir introduction nodes, while updating the RPC and onion messaging layers to utilize these new types. Review feedback focused on improving documentation and comment maintainability, specifically recommending the removal of fragile line number references, fixing a broken documentation block in the validation logic, and ensuring the release notes accurately reflect the inclusion of the invoice_request codec.

Comment thread bolt12/validate.go
Comment on lines +202 to +204
// rejected (even = must-understand). Type 240 carries the signature and
// sits outside the allowed range by spec design; skip it here as
// ValidateInvoiceRequestWrite does (validate.go:144).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Referencing specific line numbers in comments is fragile as they quickly become stale when the file is modified. It's better to refer to the function name or the logic itself to maintain clarity without risk of obsolescence.

Suggested change
// rejected (even = must-understand). Type 240 carries the signature and
// sits outside the allowed range by spec design; skip it here as
// ValidateInvoiceRequestWrite does (validate.go:144).
// rejected (even = must-understand). Type 240 carries the signature and
// sits outside the allowed range by spec design; skip it here as
// ValidateInvoiceRequestWrite does.

Comment thread bolt12/validate.go
Comment on lines +293 to +295
// offerAllowedRange returns true if the TLV type falls within the allowed

// ranges for offer messages: 1-79 and 1000000000-1999999999.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

There is an accidental blank line in the middle of the function comment, which breaks the documentation block and prevents it from being correctly associated with the function by documentation tools.

Suggested change
// offerAllowedRange returns true if the TLV type falls within the allowed
// ranges for offer messages: 1-79 and 1000000000-1999999999.
// offerAllowedRange returns true if the TLV type falls within the allowed
// ranges for offer messages: 1-79 and 1000000000-1999999999.

Comment on lines +62 to +65
* [Initial BOLT 12 Offer codec](https://github.com/lightningnetwork/lnd/pull/10789):
add a new `bolt12/` package with the BOLT 12 `offer` TLV codec and full
reader/writer validation, plus a typed `lnwire.BlindedPath` introduction-node
codec shared by HTLC routing and onion messaging.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The release notes should be updated to reflect that this pull request adds both the offer and invoice_request codecs and validators, as the current description only mentions the offer codec.

Suggested change
* [Initial BOLT 12 Offer codec](https://github.com/lightningnetwork/lnd/pull/10789):
add a new `bolt12/` package with the BOLT 12 `offer` TLV codec and full
reader/writer validation, plus a typed `lnwire.BlindedPath` introduction-node
codec shared by HTLC routing and onion messaging.
* [Initial BOLT 12 Offer and InvoiceRequest codec](https://github.com/lightningnetwork/lnd/pull/10789):
add a new `bolt12/` package with the BOLT 12 `offer` and `invoice_request` TLV
codecs and full reader/writer validation, plus a typed `lnwire.BlindedPath`
introduction-node codec shared by HTLC routing and onion messaging.

@github-actions github-actions Bot added the severity-critical Requires expert review - security/consensus critical label May 22, 2026
@github-actions
Copy link
Copy Markdown

🔴 PR Severity: CRITICAL

Automated classification | 32 files total (22 non-excluded) | 2,252 non-excluded lines changed

🔴 Critical (8 files)
  • lnwire/blinded_path.go — Lightning wire protocol: new blinded path type definition
  • lnwire/bounds.go — Lightning wire protocol: new TLV bounds declarations
  • lnwire/custom_records.go — Lightning wire protocol: custom records update
  • lnwire/intro_node.go — Lightning wire protocol: new intro node definition
  • lnwire/onion_msg_payload.go — Lightning wire protocol: onion message payload refactor
  • lnwire/pure_tlv.go — Lightning wire protocol: TLV serialization changes
  • lnwire/test_utils.go — Lightning wire protocol: test utilities in critical package
  • rpcserver.go — Core RPC server coordination
🟠 High (1 file)
  • routing/route/blindedroute.go — Payment routing: blinded route update
🟡 Medium (12 files)
  • bolt12/decode.go — New bolt12 package: TLV decode
  • bolt12/doc.go — New bolt12 package: package documentation
  • bolt12/invoice_request.go — New bolt12 package: invoice request codec
  • bolt12/offer.go — New bolt12 package: offer type
  • bolt12/pure_tlv.go — New bolt12 package: TLV primitives
  • bolt12/subtypes.go — New bolt12 package: subtype definitions
  • bolt12/tlv_types.go — New bolt12 package: TLV type constants
  • bolt12/validate.go — New bolt12 package: structural validators
  • go.mod — Dependency update
  • lnrpc/lightning.proto — RPC API definition changes
  • lnrpc/lightning.swagger.json — API swagger changes
  • onionmessage/onion_endpoint.go — Onion message endpoint update
🟢 Low (1 file)
  • docs/release-notes/release-notes-0.22.0.md — Release notes
Excluded from counting (10 files)
  • bolt12/helpers_test.go, bolt12/invoice_request_test.go, bolt12/offer_test.go, bolt12/subtypes_test.go, bolt12/validate_test.go — test files
  • lnwire/blinded_path_test.go, lnwire/custom_records_test.go, lnwire/onion_msg_payload_test.go, lnwire/pure_tlv_test.go — test files
  • lnrpc/lightning.pb.go — auto-generated protobuf

Analysis

This PR introduces BOLT 12 invoice request codec and structural validators, touching the lnwire package (Lightning wire protocol messages) extensively with 7 new/modified files covering blinded paths, onion message payloads, TLV serialization, and related types. Changes to rpcserver.go further anchor this at CRITICAL. Two severity-bump conditions are also satisfied: 22 non-excluded files (>20 threshold) and 2,252 non-excluded lines changed (>500 threshold), though the base classification is already CRITICAL from the lnwire and rpcserver.go changes.

The new bolt12/ package itself would be MEDIUM in isolation, but the deep integration with lnwire wire protocol message structures and core RPC server requires expert review of encoding correctness, TLV type assignments, and compatibility with the existing Lightning protocol implementation.


To override, add a severity-override-{critical,high,medium,low} label.
<!-- pr-severity-bot -->

@saubyk saubyk added the bolt12 label May 22, 2026
@saubyk saubyk added this to the v0.22.0 milestone May 22, 2026
@saubyk saubyk added this to lnd v0.22 May 22, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in lnd v0.22 May 22, 2026
@saubyk saubyk moved this from Backlog to In progress in lnd v0.22 May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bolt12 severity-critical Requires expert review - security/consensus critical

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

2 participants