Skip to content

Add BOLT12 support to LSPS2 via custom Router implementation#4463

Open
tnull wants to merge 9 commits into
lightningdevkit:mainfrom
tnull:2026-03-lsps2-bolt12-alt
Open

Add BOLT12 support to LSPS2 via custom Router implementation#4463
tnull wants to merge 9 commits into
lightningdevkit:mainfrom
tnull:2026-03-lsps2-bolt12-alt

Conversation

@tnull

@tnull tnull commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Closes #4272.

This is an alternative approach to #4394 which leverages a custom Router implementation on the client side to inject the respective.

LDK Node integration PR over at lightningdevkit/ldk-node#817

@tnull tnull requested review from TheBlueMatt and jkczyz March 5, 2026 13:36
@ldk-reviews-bot

ldk-reviews-bot commented Mar 5, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @jkczyz as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tnull tnull force-pushed the 2026-03-lsps2-bolt12-alt branch from 2cb0546 to 25ab3bc Compare March 5, 2026 14:05
Comment thread lightning/src/onion_message/messenger.rs Outdated
Comment thread lightning-liquidity/src/lsps2/router.rs Outdated
Comment thread lightning/src/onion_message/messenger.rs Outdated
@tnull tnull moved this to Goal: Merge in Weekly Goals Mar 5, 2026
@tnull tnull self-assigned this Mar 5, 2026
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 2nd Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment thread lightning-liquidity/src/lsps2/router.rs
@tnull tnull force-pushed the 2026-03-lsps2-bolt12-alt branch from 25ab3bc to 5786409 Compare March 24, 2026 14:34
Comment thread lightning-liquidity/src/lsps2/service.rs Outdated
Comment thread lightning-liquidity/src/lsps2/router.rs Outdated
@ldk-claude-review-bot

ldk-claude-review-bot commented Mar 24, 2026

Copy link
Copy Markdown
Collaborator

No issues found.

I re-reviewed the full current state of the PR (commit 26da1dde7). The design has been substantially refined since earlier passes, and the concerns from my prior review comments are now resolved by the current code:

  • router.rscreate_blinded_payment_paths correctly uses continue on path-build failure (not ?), guards against returning an empty path set with Err(()), uses u16 consistently, increases the forward hop's max_cltv_expiry in the correct direction, and the code comment correctly references Event::HTLCIntercepted (this is the payment path). The metadata-decoder approach replaces the prior HashMap register/deregister design that several old comments targeted.
  • events/mod.rsOnionMessageIntercepted (event tag 37, odd) serializes field 0 only for NodeId next hops (backward compat) and the reader accepts both old (peer_node_id) and new (next_hop) encodings. The doc field naming/typo issues from prior passes are fixed. The previously-flagged even-tagged InvoiceRequestReceived event has been removed entirely.
  • messenger.rs — the intercept_for_unknown_scids interception path is correct, and the "messager" typo is fixed.
  • event.rs / msgs.rs / service.rs — the u32u16 change for cltv_expiry_delta / lsp_cltv_expiry_delta is consistent across client, service API, message struct, and aligns with the BOLT cltv_expiry_delta width.
  • pending_changelog/4463-LSPS2-BOLT12.txt — now accurate: it uses the correct OnionMessenger name and correctly caveats that LDK does not persist these events itself, so the downgrade risk only applies to users who manually persist them.

Security: the decoded payment metadata is authored by the recipient and authenticated via the offer's reply-path context HMAC, so a payer cannot inject malicious LSP parameters. Safe.

One low-confidence observation I deliberately did not file: router.rs:233 uses plain MIN_FINAL_CLTV_EXPIRY_DELTA, whereas the BOLT11 route-hint path uses MIN_FINAL_CLTV_EXPIRY_DELTA + 2. The end-to-end test bolt12_lsps2_end_to_end_test completes successfully (client emits PaymentClaimable and claims), demonstrating the plain value is functionally sufficient for the blinded-path case, so this is not a defect.

No inline comments posted this pass.

@tnull tnull force-pushed the 2026-03-lsps2-bolt12-alt branch from 5786409 to 98a9e9d Compare March 24, 2026 14:50
Comment thread lightning-liquidity/src/lsps2/service.rs Outdated
Comment thread lightning-liquidity/tests/lsps2_integration_tests.rs Outdated
@tnull tnull force-pushed the 2026-03-lsps2-bolt12-alt branch 2 times, most recently from 8800d48 to 7ca886d Compare March 24, 2026 15:14
@codecov

codecov Bot commented Mar 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.38174% with 56 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.13%. Comparing base (2313bd5) to head (4342483).
⚠️ Report is 169 commits behind head on main.

Files with missing lines Patch % Lines
lightning-liquidity/src/lsps2/router.rs 93.39% 27 Missing ⚠️
lightning/src/ln/channelmanager.rs 32.00% 16 Missing and 1 partial ⚠️
lightning/src/events/mod.rs 33.33% 5 Missing and 1 partial ⚠️
lightning/src/offers/flow.rs 66.66% 2 Missing and 1 partial ⚠️
lightning/src/onion_message/messenger.rs 86.66% 2 Missing ⚠️
lightning/src/util/test_utils.rs 92.30% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4463      +/-   ##
==========================================
+ Coverage   87.08%   87.13%   +0.04%     
==========================================
  Files         161      162       +1     
  Lines      109255   109707     +452     
  Branches   109255   109707     +452     
==========================================
+ Hits        95147    95589     +442     
- Misses      11627    11632       +5     
- Partials     2481     2486       +5     
Flag Coverage Δ
fuzzing-fake-hashes 31.10% <7.35%> (+0.12%) ⬆️
fuzzing-real-hashes 22.79% <4.41%> (+0.13%) ⬆️
tests 86.20% <88.38%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tnull tnull force-pushed the 2026-03-lsps2-bolt12-alt branch from 7ca886d to 2ff16d7 Compare March 25, 2026 08:23
Comment thread lightning-liquidity/src/lsps2/service.rs Outdated
@tnull tnull force-pushed the 2026-03-lsps2-bolt12-alt branch 4 times, most recently from bcc4e10 to 5602e07 Compare March 25, 2026 12:27
Comment thread lightning-liquidity/src/lsps2/service.rs Outdated
@tnull tnull force-pushed the 2026-03-lsps2-bolt12-alt branch 2 times, most recently from ea05389 to 3acf915 Compare March 25, 2026 13:24
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 2nd Reminder

Hey @TheBlueMatt! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 3rd Reminder

Hey @TheBlueMatt! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment thread lightning-liquidity/src/lsps2/router.rs Outdated
pub fn register_intercept_scid(
&self, intercept_scid: u64, invoice_params: LSPS2Bolt12InvoiceParameters,
) -> Option<LSPS2Bolt12InvoiceParameters> {
debug_assert_eq!(intercept_scid, invoice_params.intercept_scid);

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.

lol why on earth are we providing both just to assert that they're the same?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Because I found it preferable to still communicate that the parameters will be keyed by intercept SCID, especially since we also want to remove them by intercept SCID. And dropping the intercept SCID from the parameters is also not great.

Comment thread lightning-liquidity/src/lsps2/router.rs Outdated
Comment thread lightning-liquidity/src/lsps2/router.rs Outdated
Comment thread lightning/src/events/mod.rs Outdated
Comment thread pending_changelog/4463-LSPS2-BOLT12.txt Outdated
Comment thread lightning/src/ln/channelmanager.rs Outdated
Comment thread lightning/src/offers/flow.rs Outdated
Comment thread lightning/src/offers/flow.rs
Comment thread lightning/src/offers/flow.rs Outdated
Comment thread pending_changelog/4463-LSPS2-BOLT12.txt Outdated
@tnull tnull force-pushed the 2026-03-lsps2-bolt12-alt branch from a0bcfcf to 0cef8dc Compare April 24, 2026 09:17
@tnull

tnull commented Apr 24, 2026

Copy link
Copy Markdown
Contributor Author

Addressed pending comments, also had to rebase as we now had a minor conflict. Let me know if I can squash.

@tnull tnull requested a review from TheBlueMatt April 24, 2026 09:18
self.check_refresh_async_receive_offer_cache(false).unwrap();
}

/// Requests fresh async receive offer paths from the configured static invoice server, if any.

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.

Why do we need a public version of this? We already do it on timer tick and any time we connect to a peer, istm this is entirely redundant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Exactly because we auto-request offers when connecting to a peer we found that we need this refresh during the LDK Node integration. Otherwise, there would already a refresh without the intercept SCIDs inflight when we start negotiating the LSPS2 flow and start injecting the SCIDs, which fails the flow. The alternative workaround would be a busy-loop that calls timer_tick_occurred until we can successfully generate an offer, but that's way uglier, hence why we added this API.

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.

Hmm, I don't think this currently works? If we build a static invoice when we connect to peers while we're negotiating LSPS2 (ie we have no LSPS2 parameters configured yet) we'll upload the static invoice to our static invoice storage server and there will be a window during which we cannot receive because the static invoice that is stored is bogus. LSPS2BOLT12Router really needs to fail until we have configured parameters so that we won't upload a bogus static invoice.

The docs here really need to mention that this should never really be required unless using LSPS.

return Ok(offer);
}

self.flow.get_async_receive_offer_ready_future().await;

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.

Can't this race? We probably need a bit more smarts in the flow to make sure we always return a pre-completed Future if async offers are already available in a race-free way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Can't this race? We probably need a bit more smarts in the flow to make sure we always return a pre-completed Future if async offers are already available in a race-free way.

I'm not quite sure I understand what you mean, could you expand? What would race what?

@tnull

tnull commented Apr 28, 2026

Copy link
Copy Markdown
Contributor Author

Added back a commit that derives Hash for OfferId, as we need that downstream (slightly orthogonal, but connected to this PR).

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Does this need updating with the changes from #4584?

@tnull

tnull commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Does this need updating with the changes from #4584?

Yes, probably we should now wait for that to land first.

Allow integrations to intercept blinded onion-message hops that identify the next node by short channel id, so LSPS-style protocols can resolve those hops out of band instead of dropping the message.

Co-Authored-By: HAL 9000
@tnull tnull force-pushed the 2026-03-lsps2-bolt12-alt branch from 4342483 to e56ea06 Compare June 9, 2026 10:56
tnull added 8 commits June 9, 2026 13:12
Allow BOLT12 blinded payment paths to be supplemented from LSPS2 parameters decoded out of payment metadata, so recipients can advertise JIT-channel paths without maintaining router-local SCID state.

Co-Authored-By: HAL 9000
Allow tests and callers to key offer metadata by offer id without wrapping the identifier.

Co-Authored-By: HAL 9000
Let async recipients explicitly refresh receive offers, wait for readiness, and preserve payment metadata across static-invoice refreshes.

Co-Authored-By: HAL 9000
Align LSPS2 CLTV deltas with the wire format and the rest of LDK's routing types.

Co-Authored-By: HAL 9000
Clarify how LSPS2 invoice parameters relate to BOLT11 route hints and BOLT12 blinded payment path creation.

Co-Authored-By: HAL 9000
Let integration tests force specific blinded payment paths so LSPS2 BOLT12 routing behavior can be exercised deterministically.

Co-Authored-By: HAL 9000
Exercise decoder-provided LSPS2 parameters across custom-router, end-to-end, compact message path, and async-payment BOLT12 flows.

Co-Authored-By: HAL 9000
Record the new LSPS2 BOLT12 routing support and its related compatibility note for the next release notes.

Co-Authored-By: HAL 9000
@tnull

tnull commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Now updated to use the new payment-metadata-in-BOLT12-context flow. Also updated lightningdevkit/ldk-node#817 to use this.

///
/// The metadata is persisted with the async receive offer cache so future static-invoice
/// refreshes for the same offer continue to include it.
pub fn refresh_async_receive_offers_with_payment_metadata(

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.

Why do we need this? Doesn't our router inject the metadata for us?

self.check_refresh_async_receive_offer_cache(false).unwrap();
}

/// Requests fresh async receive offer paths from the configured static invoice server, if any.

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.

Hmm, I don't think this currently works? If we build a static invoice when we connect to peers while we're negotiating LSPS2 (ie we have no LSPS2 parameters configured yet) we'll upload the static invoice to our static invoice storage server and there will be a window during which we cannot receive because the static invoice that is stored is bogus. LSPS2BOLT12Router really needs to fail until we have configured parameters so that we won't upload a bogus static invoice.

The docs here really need to mention that this should never really be required unless using LSPS.

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

Projects

Status: Goal: Merge

Development

Successfully merging this pull request may close these issues.

BOLT 12 support for bLIP-52/LSPS2

5 participants