Skip to content

Add BOLT12 support to LSPS2#4394

Draft
tnull wants to merge 7 commits intolightningdevkit:mainfrom
tnull:2026-02-lsps2-bolt12-support
Draft

Add BOLT12 support to LSPS2#4394
tnull wants to merge 7 commits intolightningdevkit:mainfrom
tnull:2026-02-lsps2-bolt12-support

Conversation

@tnull
Copy link
Contributor

@tnull tnull commented Feb 6, 2026

Finally adding BOLT12 support to the LSPS2 flow.

For now, this is just a draft as basis for further discussion on the approach. (@jkczyz)

tnull added 4 commits February 6, 2026 11:41
Add building blocks for creating BOLT12 invoices with blinded payment
paths through an LSP using intercept SCIDs, enabling LSPS2 JIT channel
opens for BOLT12 payments.

- Add `create_blinded_payment_paths_for_intercept_scid()` on
  `OffersMessageFlow` to construct blinded payment paths through the LSP
  using LSPS2 parameters (intercept SCID, CLTV delta, zero fees)
- Add `create_invoice_builder_from_invoice_request_with_custom_payment_paths()`
  on `OffersMessageFlow` to build BOLT12 invoices with pre-built payment paths
- Add `manually_handle_bolt12_invoice_requests` config option and
  `Event::InvoiceRequestReceived` to allow external handling of invoice
  requests (needed for LSPS2 clients that must construct custom paths)
- Add `send_invoice_for_request()` and `offers_handler()` on
  `ChannelManager` for sending back manually-constructed invoices
- Update `InvoiceParametersReady` docs for BOLT12 usage

Co-Authored-By: HAL 9000
Add `register_peer_for_interception()` and
`deregister_peer_for_interception()` methods to `OnionMessenger`,
allowing specific peers to be registered for onion message interception
without enabling blanket interception for all offline peers.

When a registered peer is offline and an onion message needs to be
forwarded to them, `Event::OnionMessageIntercepted` is emitted. When
a registered peer connects, `Event::OnionMessagePeerConnected` is
emitted. This works alongside the existing global
`new_with_offline_peer_interception()` flag — if either the global flag
is set or the peer is specifically registered, interception occurs.

This enables LSPS2 services to intercept onion messages only for peers
with active JIT channel sessions, rather than intercepting messages for
all offline peers.

Co-Authored-By: HAL 9000
Define the `OnionMessageInterceptor` trait with
`register_peer_for_interception()` and
`deregister_peer_for_interception()` methods, and implement it for
`OnionMessenger`. This allows external components to register peers
for onion message interception via a trait object, without needing
to know the concrete `OnionMessenger` type.

Wire the trait into `LSPS2ServiceHandler` as an optional
`Arc<dyn OnionMessageInterceptor>`. When provided:
- On init, all peers with active intercept SCIDs are registered
- In `invoice_parameters_generated()`, the counterparty is registered
  when a new intercept SCID is assigned

This ensures that onion messages for LSPS2 clients with active JIT
channel sessions are intercepted when those clients are offline,
enabling the LSP to store and forward messages when the client
reconnects.

Co-Authored-By: HAL 9000
When intercept SCIDs are removed during cleanup, also clean up the
handler-level `peer_by_intercept_scid` map and deregister the peer
from onion message interception if they have no remaining active SCIDs.

Cleanup is added in all relevant paths:
- `prune_expired_request_state()` now returns pruned SCIDs
- `peer_disconnected()` cleans up after pruning
- `htlc_intercepted()` error path (fixes existing TODO)
- `channel_open_abandoned()` cleans up after removing the SCID
- `persist()` cleans up both `peer_by_intercept_scid` and
  `peer_by_channel_id` when removing a peer entry entirely

Co-Authored-By: HAL 9000
@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Feb 6, 2026

👋 Hi! This PR is now in draft status.
I'll wait to assign reviewers until you mark it as ready for review.
Just convert it out of draft status when you're ready for review!

@tnull tnull marked this pull request as draft February 6, 2026 11:45
tnull added 3 commits February 6, 2026 14:21
Add a convenience method on `ChannelManager` that combines all the
steps needed to create and send a BOLT12 invoice using LSPS2 JIT
channel parameters:

1. Verify the invoice request
2. Create an inbound payment (payment hash + secret)
3. Build blinded payment paths through the LSP using the intercept SCID
4. Construct the BOLT12 invoice with those paths
5. Send the invoice back via the responder

This simplifies the LSPS2 client flow: instead of manually calling
five separate methods across `OffersMessageFlow` and `ChannelManager`,
the caller can handle `Event::InvoiceRequestReceived` with a single
call, passing the LSPS2 parameters from `InvoiceParametersReady`.

Co-Authored-By: HAL 9000
…aths

Add a test that verifies the `manually_handle_bolt12_invoice_requests`
config flag and `send_bolt12_invoice_for_intercept_scid` convenience
method work correctly together. The test exercises the full LSPS2+BOLT12
flow: creating an offer, receiving an `InvoiceRequest` via the
`Event::InvoiceRequestReceived` event, building a BOLT12 invoice with
blinded payment paths using an intercept SCID, and sending the invoice
back to the payer via onion message.

Co-Authored-By: HAL 9000
Add `bolt12_lsps2_end_to_end_test` that exercises the full BOLT12 + LSPS2
JIT channel lifecycle across three nodes (payer, service/LSP, client):

- LSPS2 ceremony to negotiate JIT channel parameters
- Client creates BOLT12 offer with `manually_handle_bolt12_invoice_requests`
- Payer sends `InvoiceRequest` routed through the service via onion messages
- Client handles `InvoiceRequestReceived` and calls
  `send_bolt12_invoice_for_intercept_scid` to create an invoice with blinded
  payment paths through the service's intercept SCID
- Payer pays the invoice through the blinded path
- Service intercepts the HTLC and opens a JIT channel to the client
- Client claims, service broadcasts the funding transaction

Co-Authored-By: HAL 9000
@tnull tnull force-pushed the 2026-02-lsps2-bolt12-support branch from 2d9c5a0 to c847f08 Compare February 6, 2026 13:21
/// [`OffersMessageFlow::create_blinded_payment_paths_for_intercept_scid`]: crate::offers::flow::OffersMessageFlow::create_blinded_payment_paths_for_intercept_scid
/// [`OffersMessageFlow::create_invoice_builder_from_invoice_request_with_custom_payment_paths`]: crate::offers::flow::OffersMessageFlow::create_invoice_builder_from_invoice_request_with_custom_payment_paths
/// [`ChannelManager::send_invoice_for_request`]: crate::ln::channelmanager::ChannelManager::send_invoice_for_request
InvoiceRequestReceived {
Copy link
Contributor

Choose a reason for hiding this comment

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

We've been discussing for removing these events for awhile now in favor of events in OffersMessageFlow. But maybe we've come full circle on that? See: #3833 (comment). (cc: @TheBlueMatt)

There's definitely a lot of handling logic in ChannelManager's implementation of OffersMessageHandler that wouldn't be great if a custom implementation needed to reproduce.

/// [`Self::create_invoice_builder_from_invoice_request_with_custom_payment_paths`].
///
/// [`ChannelManager::create_inbound_payment`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment
pub fn create_blinded_payment_paths_for_intercept_scid<ES: EntropySource>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be a custom Router implementation instead? For message blinded paths, we took the approach of allowing to pass a custom MessageRouter if we want non-standard blinded path creation.

/// respond to the counterparty.
///
/// [`ChannelManager::create_inbound_payment`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment
pub fn create_invoice_builder_from_invoice_request_with_custom_payment_paths<'a>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Then this wouldn't be needed since you'd just call create_invoice_builder_from_invoice_request_without_keys with a custom Router.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants