Add BOLT12 support to LSPS2 via custom Router implementation#4463
Add BOLT12 support to LSPS2 via custom Router implementation#4463tnull wants to merge 7 commits intolightningdevkit:mainfrom
Router implementation#4463Conversation
|
👋 Thanks for assigning @jkczyz as a reviewer! |
2cb0546 to
25ab3bc
Compare
| dns_resolver_handler: DRH, | ||
| custom_handler: CMH, | ||
| intercept_messages_for_offline_peers: bool, | ||
| peers_registered_for_interception: Mutex<HashSet<PublicKey>>, |
There was a problem hiding this comment.
Why not just ignore the events for peers that are offline? Not quite sure I get why we need to move the filtering logic into OnionMessenger.
There was a problem hiding this comment.
Wouldn't we need this for example for LSPS5? I.e., we'd need to intercept the messages, then wake up the the peer via notification, then forward the messages?
| &self, payment_context: &PaymentContext, | ||
| ) -> Option<LSPS2Bolt12InvoiceParameters> { | ||
| // We intentionally only match `Bolt12Offer` here and not `AsyncBolt12Offer`, as LSPS2 | ||
| // JIT channels are not applicable to async (always-online) BOLT12 offer flows. |
There was a problem hiding this comment.
I don't think this is true? We need to support JIT opening for async offers as well.
There was a problem hiding this comment.
Yes, should have formulated that better, but IMO that is a next/follow-up step somewhat orthogonal to this PR?
There was a problem hiding this comment.
We can do it in a separate PR indeed, but I'm not really sure LSPS2 support for BOLT12 only for always-online nodes is nearly as useful has for async recipients. ISTM the second part is the more important usecase.
There was a problem hiding this comment.
The big difference is that there are other LSPS2 (client and service) implementations out there that LSPs are running, while async payments isn't deployed at all yet, and will require both sides to be LDK for the time being.
There was a problem hiding this comment.
I mean that's fair but are there other LSPS servers that support intercepting blinded paths and doing a JIT channel? I imagine we'll in practice require LDK for both ends for that as well.
There was a problem hiding this comment.
In any case my point is that both sides are a similar priority, not that they have to happen in one PR.
There was a problem hiding this comment.
Explored this further, but it seems there might be a conflict between approaches here:
No
receive_async_via_jit_channel()API —receive_async()creates offers viaChannelManager::get_async_receive_offer()which bypasses the LSPS2 router entirely. The static invoice's payment paths don't use the intercept SCID.
So to add BOLT12-async-payments-via-LSPS2-JIT support we might need to reconsider how we could inject the respective data into the blinded paths. Not sure if @valentinewallace would have an opinion here.
Also, to quote Claude:
The simplest approach: the LSPS2 buy dance happens on the client side, before the static invoice is created. The client:
- Calls an LSPS2 buy request to get intercept_scid + cltv_expiry_delta
- Calls router.register_offer_nonce(offer_nonce, params)
- Then triggers the static invoice creation flow
Since the LSPS2BOLT12Router is both the payment router and the message router, when create_static_invoice_for_server() calls router.create_blinded_payment_paths() with AsyncBolt12OfferContext { offer_nonce }, the router finds the registered nonce and injects the intercept SCID.
But there's a problem: the static invoice is created on the server side (LSP), not the client side. The server calls create_static_invoice_for_server() which calls its own router. The client's router registration is irrelevant — it's the server's router that builds the payment paths.
So either:
- (A) The server (LSP) needs to know about the LSPS2 intercept SCID for this client and register it on its own router before creating the static invoice. This means the LSPS2 buy flow needs to complete before static invoice creation, and the server must register the result on its router.
- (B) The client creates the static invoice itself (not the server), but that's not how async payments work.
- (C) Add a callback/hook in create_static_invoice_for_server() that lets the server inject custom payment paths.
Option (A) seems most natural: the LSP (as LSPS2 service) already knows about the client's intercept SCIDs. When the server creates the static invoice for a client, it could register the intercept SCID on its router so the payment paths go through the JIT channel. But this requires the LSP
to proactively register offer nonces for each client's async offers.
| .entry(next_node_id) | ||
| .or_insert_with(|| OnionMessageRecipient::ConnectedPeer(VecDeque::new())); | ||
|
|
||
| let should_intercept = self.intercept_messages_for_offline_peers |
There was a problem hiding this comment.
Shoulnd't we also expand interception to unknown SCIDs for blinded message path creation prior to channel open? I guess its not critical for this to work but it would make the generated offers much smaller as we'd be able to use the SCID encoding rather than pubkey encoding.
There was a problem hiding this comment.
Didn't go this way for now, but we do allow registering intercept SCIDs now. Note that a wildcard intercept without having the user pre-register a publickey won't work without breaking the Event::OnionMessageIntercepted API.
|
🔔 1st Reminder Hey @jkczyz! This PR has been waiting for your review. |
|
🔔 2nd Reminder Hey @jkczyz! This PR has been waiting for your review. |
| pub struct LSPS2Bolt12InvoiceParameters { | ||
| /// The LSP node id to use as the blinded path introduction node. | ||
| pub counterparty_node_id: PublicKey, | ||
| /// The LSPS2 intercept short channel id. | ||
| pub intercept_scid: u64, | ||
| /// The CLTV expiry delta the LSP requires for forwarding over `intercept_scid`. | ||
| pub cltv_expiry_delta: u32, | ||
| } |
There was a problem hiding this comment.
Would it be too expensive to store this in the Offer's blinded path? Though I suppose the Router doesn't have access to that, so we'd have to provide it the MessageContext.
There was a problem hiding this comment.
I imagine it would be. Adding yet another 45 bytes might be a bit much w.r.t. to QR encoding?
There was a problem hiding this comment.
Right, that would be and additional 72 bytes more when encoded as bech32.
Maybe a compact representation (SCID and direction) could be used similar to what we do in blinded paths? That would use 9 bytes instead of 33 for the pubkey, so 21 bytes instead of 45. Encoded that would be 33/34 more bytes instead of 72.
There was a problem hiding this comment.
Could you expand on what exactly you imagine we store? And is this mostly around not requiring the client to remember anything outside the offer locally?
25ab3bc to
5786409
Compare
|
I've thoroughly re-reviewed the entire PR diff against the current state of the codebase, including all files that were truncated in the diff (manager.rs sync constructors, integration tests, test common module). I checked every lifecycle point for SCID registration/deregistration, lock ordering consistency, parameter forwarding between sync/async constructors, and the new router implementation. SummaryNo new issues found. All five issues from my prior review passes have been resolved in this version:
Previously flagged (not re-posted)
No new issues found. |
5786409 to
98a9e9d
Compare
8800d48 to
7ca886d
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4463 +/- ##
==========================================
- Coverage 86.20% 86.17% -0.04%
==========================================
Files 160 161 +1
Lines 107545 108041 +496
Branches 107545 108041 +496
==========================================
+ Hits 92707 93101 +394
- Misses 12214 12308 +94
- Partials 2624 2632 +8
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
7ca886d to
2ff16d7
Compare
bcc4e10 to
5602e07
Compare
| /// Cleans up `peer_by_intercept_scid` entries for the given SCIDs, and deregisters the peer | ||
| /// from onion message interception if they have no remaining active intercept SCIDs. |
There was a problem hiding this comment.
Nit: The doc comment says "deregisters the peer from onion message interception if they have no remaining active intercept SCIDs" but the implementation unconditionally deregisters the given SCIDs — it doesn't check whether the peer has remaining SCIDs and doesn't deregister the peer itself. Consider updating to something like:
/// Cleans up `peer_by_intercept_scid` entries for the given SCIDs and deregisters
/// them from onion message interception.
5602e07 to
ea05389
Compare
Let callers register specific peers for offline onion-message interception and emit dedicated events when a message is intercepted or a tracked peer reconnects. This gives LSPS2 service flows a narrower alternative to blanket offline-peer interception. Co-Authored-By: HAL 9000
Expose `OnionMessageInterceptor` through `LiquidityManager` and use it from `LSPS2ServiceHandler` to register peers that have active JIT sessions. This lets the LSP store and forward onion messages only for LSPS2 clients that currently need interception. Co-Authored-By: HAL 9000
Introduce `LSPS2BOLT12Router` to map registered offers to LSPS2 invoice parameters and build blinded payment paths through the negotiated intercept `SCID`. All other routing behavior still delegates to the wrapped router. Co-Authored-By: HAL 9000
Describe how `InvoiceParametersReady` feeds both the existing `BOLT11` route-hint flow and the new `LSPS2BOLT12Router` registration path for `BOLT12` offers. Co-Authored-By: HAL 9000
Exercise the LSPS2 buy flow and assert that a registered `OfferId` produces a blinded payment path whose first forwarding hop uses the negotiated intercept `SCID`. Co-Authored-By: HAL 9000
Allow tests to inject a custom `create_blinded_payment_paths` hook while preserving the normal `ReceiveTlvs` bindings. This makes it possible to exercise LSPS2-specific `BOLT12` path construction in integration tests. Co-Authored-By: HAL 9000
Cover the full offer-payment flow from onion-message invoice exchange through HTLC interception, JIT channel opening, and settlement. This confirms the LSPS2 router and service handler work together in the integrated path. Co-Authored-By: HAL 9000
ea05389 to
3acf915
Compare
|
Alright, this is now rebased, and I also switched to defaulting to inject the SCID to the blinded path, not the pubkey, while the service part of it should still allow for either to be used. Also played some rounds of AI ping-pong on both ends, so this should be good for another look. |
Closes #4272.
This is an alternative approach to #4394 which leverages a custom
Routerimplementation on the client side to inject the respective.LDK Node integration PR over at lightningdevkit/ldk-node#817