Add BOLT12 support to LSPS2 via custom Router implementation#4463
Add BOLT12 support to LSPS2 via custom Router implementation#4463tnull wants to merge 13 commits intolightningdevkit:mainfrom
Router implementation#4463Conversation
|
👋 Thanks for assigning @jkczyz as a reviewer! |
2cb0546 to
25ab3bc
Compare
|
🔔 1st Reminder Hey @jkczyz! This PR has been waiting for your review. |
|
🔔 2nd Reminder Hey @jkczyz! This PR has been waiting for your review. |
25ab3bc to
5786409
Compare
Review Summary — PR #4463 (Review pass 12)One new issue found in the changelog file. All previously identified code bugs remain fixed. Inline comments posted
Verified prior fixesAll critical bugs from prior review passes confirmed resolved in this revision:
Previously posted nits still applicable (not reposted)
|
5786409 to
98a9e9d
Compare
8800d48 to
7ca886d
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4463 +/- ##
==========================================
+ Coverage 84.55% 87.08% +2.52%
==========================================
Files 135 162 +27
Lines 76569 109690 +33121
Branches 76569 109690 +33121
==========================================
+ Hits 64745 95519 +30774
- Misses 9783 11688 +1905
- Partials 2041 2483 +442
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
ea05389 to
3acf915
Compare
bf0ead8 to
ff32073
Compare
| /// For **payment** blinded paths (in invoices), it returns the intercept SCID as the forwarding | ||
| /// hop so that the LSP can intercept the HTLC and open a JIT channel. | ||
| /// For **payment** blinded paths (in invoices), it appends paths using the intercept SCID as the | ||
| /// forwarding hop so that the LSP can intercept the HTLC and open a JIT channel. Paths from the | ||
| /// inner router (e.g., through pre-existing channels) are included as well, allowing payers to | ||
| /// use existing inbound liquidity when available. |
There was a problem hiding this comment.
Seeing this in f538153. Should it belong in an earlier commit?
| pub struct TestMessageRouter<'a> { | ||
| pub inner: TestMessageRouterInternal<'a>, | ||
| pub peers_override: Mutex<Vec<PublicKey>>, | ||
| pub forward_node_scid_override: Mutex<HashMap<PublicKey, u64>>, |
There was a problem hiding this comment.
Is the TestMessageRouter changed needed anymore?
There was a problem hiding this comment.
Hmm, we still use it in bolt12_lsps2_compact_message_path_test to test SCID-based interception. Do you have something else in mind?
| /// Override closure type for [`TestRouter::override_create_blinded_payment_paths`]. | ||
| /// | ||
| /// This closure is called instead of the default [`Router::create_blinded_payment_paths`] | ||
| /// implementation when set, receiving the actual [`ReceiveTlvs`] so tests can construct custom | ||
| /// blinded payment paths using the same TLVs the caller generated. | ||
| pub type BlindedPaymentPathOverrideFn = Box< | ||
| dyn Fn( | ||
| PublicKey, | ||
| ReceiveAuthKey, | ||
| Vec<ChannelDetails>, | ||
| ReceiveTlvs, | ||
| Option<u64>, | ||
| ) -> Result<Vec<BlindedPaymentPath>, ()> | ||
| + Send | ||
| + Sync, | ||
| >; |
There was a problem hiding this comment.
Hmm... maybe we can have TestRouter wrap a dyn Router instead and provide a way to override it?
There was a problem hiding this comment.
I had explored this before, but unfortunately Router is not dyn-compatible (due to the generic type-parameter on create_blinded_payment_paths), and making it a parameter seemed like a pretty invasive change to our test code (i.e., many places need changing, as it would bubble up).
That said, maybe it would be preferable to change Router to be dyn compatible, also so that in LDK Node we could determine the used router at build time? Though that seems a bit out-of-scope for this pr?
ff32073 to
1e29ea3
Compare
| pub struct TestMessageRouter<'a> { | ||
| pub inner: TestMessageRouterInternal<'a>, | ||
| pub peers_override: Mutex<Vec<PublicKey>>, | ||
| pub forward_node_scid_override: Mutex<HashMap<PublicKey, u64>>, |
Introduce `LSPS2BOLT12Router` to allow registering LSPS2 invoice parameters and build blinded payment paths through the negotiated intercept SCIDs. All other routing behavior still delegates to the wrapped router. Co-Authored-By: HAL 9000 Signed-off-by: Elias Rohrer <dev@tnull.de>
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 registered LSPS2 parameters produce a blinded payment path whose first forwarding hop uses the negotiated intercept SCID. Co-Authored-By: HAL 9000 Signed-off-by: Elias Rohrer <dev@tnull.de>
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
Test the full end-to-end flow of an async payment through an LSPS2 JIT channel: static invoice server setup, `LSPS2BOLT12Router` registration, async payment onion message exchange (`HeldHtlcAvailable`/`ReleaseHeldHtlc`), HTLC interception, JIT channel open, and payment claim. Co-Authored-By: HAL 9000 Signed-off-by: Elias Rohrer <dev@tnull.de>
To enable suppoort for async payments via LSPS2 JIT channels, we expose explicit async receive-offer refresh and readiness waiting so integrators can sequence external setup before relying on a ready async offer, instead of polling timer ticks. Generated with AI assistance. Co-Authored-By: HAL 9000
The BOLT 7 wire format defines `cltv_expiry_delta` as a 2-byte field, and LDK uses u16 for it everywhere (`RouteHintHop`, `ChannelUpdateInfo`, `UnsignedChannelUpdate`). Align the LSPS2 types accordingly. serde_json will reject values exceeding `u16::MAX` during deserialization, so a counterparty sending an out-of-range value is handled gracefully. Co-Authored-By: HAL 9000 Signed-off-by: Elias Rohrer <dev@tnull.de>
1e29ea3 to
607e924
Compare
Squashed without further changes. |
|
🔔 1st Reminder Hey @TheBlueMatt! This PR has been waiting for your review. |
|
🔔 2nd Reminder Hey @TheBlueMatt! This PR has been waiting for your review. |
|
🔔 3rd Reminder Hey @TheBlueMatt! This PR has been waiting for your review. |
| pub fn register_intercept_scid( | ||
| &self, intercept_scid: u64, invoice_params: LSPS2Bolt12InvoiceParameters, | ||
| ) -> Option<LSPS2Bolt12InvoiceParameters> { | ||
| debug_assert_eq!(intercept_scid, invoice_params.intercept_scid); |
There was a problem hiding this comment.
lol why on earth are we providing both just to assert that they're the same?
There was a problem hiding this comment.
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.
| /// [`MessageRouter`]: lightning::onion_message::messenger::MessageRouter | ||
| /// [`Event::OnionMessageIntercepted`]: lightning::events::Event::OnionMessageIntercepted | ||
| /// [`Event::HTLCIntercepted`]: lightning::events::Event::HTLCIntercepted | ||
| pub struct LSPS2BOLT12Router<R: Router, ES: EntropySource + Send + Sync> { |
There was a problem hiding this comment.
I don't believe the Send + Sync bounds are required.
| })) | ||
| } else { | ||
| debug_assert!( | ||
| false, |
|
|
||
| When downgrading to LDK v0.2 and prior, `OnionMessageIntercepted` events that | ||
| hold the new `NextMessageHop::ShortChannelId` will fail to be deserialized and | ||
| hence will be discarded. |
There was a problem hiding this comment.
No? We'll fail to deserialize and fail to load entirely. This is likely a real issue...Given the number of nodes with offline onion message interception out there its probably not a huge issue so maybe we ignore it but need to carefully document it and probably it should be a new config option.
There was a problem hiding this comment.
No? We'll fail to deserialize and fail to load entirely.
Wait, we don't persist OnionMessageIntercepted events at all in LDK, right? I think I got mislead by the pre-existing serialization logic, which AFAIU just got added as we thought the LSPs might want to persist the whole event? And even in the LDK Node integration, we so far don't persist anything, let alone the whole LDK event? So we should be good just dropping all the serialization logic, as well as the new upgrade/downgrade tests?
EDIT: Ah, actually, we implement the serialization logic just for serialized_length. Discussed this before here: #2995 (comment)
So we may be good to just change it arbitrarily, remove all upgrade/downgrade tests and add a fat note there that it's never actually serialized? Or maybe we should find another way to calculate serialized_length for this variant in enqueue_intercepted_events to avoid further confusion in the future?
| /// Waits for an async receive offer to become ready after the interactive static-invoice | ||
| /// protocol completes. | ||
| #[cfg(feature = "std")] | ||
| pub fn await_async_receive_offer(&self, max_wait: Duration) -> Result<Offer, ()> { |
There was a problem hiding this comment.
Bleh, why are we only exposing a non-async version of this? At a minimum we should expose an async version as well.
|
|
||
| pending_async_payments_messages: Mutex<Vec<(AsyncPaymentsMessage, MessageSendInstructions)>>, | ||
| async_receive_offer_cache: Mutex<AsyncReceiveOfferCache>, | ||
| #[cfg(feature = "std")] |
There was a problem hiding this comment.
Why is this gated on std?
| @@ -1537,7 +1602,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { | |||
| }, | |||
| _ => return None, | |||
| }; | |||
There was a problem hiding this comment.
What's with the unnecessary diff in this file?
| } | ||
|
|
||
| #[cfg(feature = "std")] | ||
| pub(crate) fn wait_for_async_receive_offer_ready(&self, max_wait: Duration) -> bool { |
There was a problem hiding this comment.
Let's just expose the Future?
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