Skip to content

Commit 16fb674

Browse files
add lsps4 bolt12 receive
1 parent b51e5de commit 16fb674

8 files changed

Lines changed: 743 additions & 14 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# LSPS4 + BOLT12 JIT Channel Implementation Plan
2+
3+
## Problem Statement
4+
Enable BOLT12 offers to work with LSPS4 JIT channels for nodes that have no existing channels.
5+
6+
## Background
7+
8+
### How BOLT12 Works
9+
1. Recipient creates an **Offer** (contains blinded MESSAGE paths for receiving invoice requests)
10+
2. Payer sends **InvoiceRequest** via onion message to recipient
11+
3. Recipient responds with **Bolt12Invoice** (contains blinded PAYMENT paths)
12+
4. Payer pays the invoice using the blinded payment paths
13+
14+
### How LSPS4 Works
15+
1. Client registers with LSP, receives `intercept_scid` and `cltv_expiry_delta`
16+
2. Client creates invoices with route hint pointing to LSP via `intercept_scid`
17+
3. When payment arrives at `intercept_scid`, LSP intercepts HTLC and opens JIT channel
18+
4. LSP forwards payment to client
19+
20+
### The Gap
21+
- **Message paths**: Work fine - LSP is a connected peer, can forward onion messages
22+
- **Payment paths**: Fail - `Router::create_blinded_payment_paths` requires usable channels
23+
24+
## Solution
25+
26+
### Architecture
27+
Create a wrapper router (`LSPS4Router`) that:
28+
1. Delegates to `DefaultRouter` for normal operations
29+
2. Falls back to LSPS4 blinded payment paths when no channels exist
30+
31+
### Shared State
32+
```rust
33+
pub struct LSPS4BlindedPathConfig {
34+
pub lsp_node_id: PublicKey,
35+
pub intercept_scid: u64,
36+
pub cltv_expiry_delta: u32,
37+
}
38+
```
39+
40+
This state is:
41+
- Written by: LiquiditySource after LSPS4 registration
42+
- Read by: LSPS4Router when creating blinded payment paths
43+
44+
### LSPS4Router Implementation
45+
```rust
46+
pub struct LSPS4Router<G, L, ES, S, SP, Sc> {
47+
inner: DefaultRouter<G, L, ES, S, SP, Sc>,
48+
lsps4_config: Arc<RwLock<Option<LSPS4BlindedPathConfig>>>,
49+
entropy_source: ES,
50+
}
51+
52+
impl Router for LSPS4Router {
53+
fn create_blinded_payment_paths(...) -> Result<Vec<BlindedPaymentPath>, ()> {
54+
// 1. Try normal path creation
55+
if let Ok(paths) = self.inner.create_blinded_payment_paths(...) {
56+
if !paths.is_empty() {
57+
return Ok(paths);
58+
}
59+
}
60+
61+
// 2. Fall back to LSPS4 if configured
62+
if let Some(config) = self.lsps4_config.read().unwrap().as_ref() {
63+
return self.create_lsps4_blinded_path(config, recipient, tlvs, ...);
64+
}
65+
66+
Err(())
67+
}
68+
}
69+
```
70+
71+
### LSPS4 Blinded Path Creation
72+
```rust
73+
fn create_lsps4_blinded_path(&self, config: &LSPS4BlindedPathConfig, ...) {
74+
let forward_node = PaymentForwardNode {
75+
node_id: config.lsp_node_id,
76+
tlvs: ForwardTlvs {
77+
short_channel_id: config.intercept_scid,
78+
payment_relay: PaymentRelay {
79+
cltv_expiry_delta: config.cltv_expiry_delta as u16,
80+
fee_base_msat: 0, // LSPS4 charges via channel opening
81+
fee_proportional_millionths: 0,
82+
},
83+
payment_constraints: PaymentConstraints {
84+
max_cltv_expiry: tlvs.tlvs().payment_constraints.max_cltv_expiry + config.cltv_expiry_delta,
85+
htlc_minimum_msat: 0,
86+
},
87+
next_blinding_override: None,
88+
features: BlindedHopFeatures::empty(),
89+
},
90+
htlc_maximum_msat: u64::MAX,
91+
};
92+
93+
BlindedPaymentPath::new(
94+
&[forward_node],
95+
recipient,
96+
tlvs,
97+
u64::MAX,
98+
MIN_FINAL_CLTV_EXPIRY_DELTA,
99+
&self.entropy_source,
100+
secp_ctx,
101+
).map(|path| vec![path])
102+
}
103+
```
104+
105+
## Implementation Steps
106+
107+
### Step 1: Create router module (src/router.rs) - DONE
108+
- [x] Define `LSPS4BlindedPathConfig` struct
109+
- [x] Create `LSPS4Router` struct wrapping `DefaultRouter`
110+
- [x] Implement `Router` trait for `LSPS4Router`
111+
- [x] Add LSPS4 blinded path fallback logic
112+
113+
### Step 2: Update types.rs - DONE
114+
- [x] Change `Router` type alias to use `LSPS4Router`
115+
- [x] Export `LSPS4BlindedPathConfig`
116+
117+
### Step 3: Update builder.rs - DONE
118+
- [x] Create `LSPS4BlindedPathConfig` shared state
119+
- [x] Create `LSPS4Router` instead of `DefaultRouter`
120+
- [x] Pass shared state to both router and liquidity source
121+
122+
### Step 4: Update liquidity.rs - DONE
123+
- [x] Add method to set LSPS4 config after registration
124+
- [x] Store reference to shared config
125+
- [x] Update config when LSPS4 registration succeeds
126+
- [x] Add `lsps4_register_for_bolt12()` method for BOLT12 use case
127+
128+
### Step 5: Add receive_via_lsps4_jit_channel to Bolt12Payment - DONE
129+
- [x] Add required fields to Bolt12Payment (runtime, connection_manager, liquidity_source, peer_store)
130+
- [x] Update constructor and call sites in lib.rs
131+
- [x] Add `receive_via_lsps4_jit_channel` method
132+
- [x] Add `receive_variable_amount_via_lsps4_jit_channel` method
133+
134+
### Step 6: Testing - DONE
135+
- [ ] Unit test for LSPS4Router fallback (optional)
136+
- [x] Integration test for full BOLT12 + LSPS4 flow (`lsps4_bolt12_jit_channel` in integration_tests_rust.rs)
137+
138+
## Files Modified
139+
140+
1. **NEW: src/router.rs** - LSPS4Router implementation
141+
2. **src/types.rs** - Update Router type alias
142+
3. **src/builder.rs** - Wire up LSPS4Router and shared state
143+
4. **src/liquidity.rs** - Populate LSPS4 config after registration, add lsps4_register_for_bolt12()
144+
5. **src/payment/bolt12.rs** - Add receive_via_lsps4_jit_channel methods
145+
6. **src/lib.rs** - Add router module, update Bolt12Payment construction
146+
147+
## Dependencies
148+
149+
From rust-lightning:
150+
- `lightning::blinded_path::payment::{BlindedPaymentPath, PaymentForwardNode, ForwardTlvs, PaymentRelay, PaymentConstraints}`
151+
- `lightning::routing::router::Router`
152+
- `lightning::types::features::BlindedHopFeatures`
153+
- `lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA`
154+
155+
## Notes
156+
157+
### Normal BOLT12 still works
158+
The `LSPS4Router` first tries normal path creation via `DefaultRouter`. LSPS4 fallback only triggers when:
159+
1. Normal path creation fails or returns empty, AND
160+
2. LSPS4 config is populated
161+
162+
So existing BOLT12 functionality is completely preserved for nodes with channels.

src/builder.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger};
7171
use crate::message_handler::NodeCustomMessageHandler;
7272
use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox;
7373
use crate::peer_store::PeerStore;
74+
use crate::router::{LSPS4BlindedPathConfig, LSPS4Router};
7475
use crate::runtime::Runtime;
7576
use crate::tx_broadcaster::TransactionBroadcaster;
7677
use crate::types::{
@@ -1594,12 +1595,22 @@ fn build_with_store_internal(
15941595
}
15951596

15961597
let scoring_fee_params = ProbabilisticScoringFeeParameters::default();
1597-
let router = Arc::new(DefaultRouter::new(
1598+
let inner_router = DefaultRouter::new(
15981599
Arc::clone(&network_graph),
15991600
Arc::clone(&logger),
16001601
Arc::clone(&keys_manager),
16011602
Arc::clone(&scorer),
16021603
scoring_fee_params,
1604+
);
1605+
1606+
// Create shared LSPS4 config state for the router
1607+
let lsps4_blinded_path_config: Arc<RwLock<Option<LSPS4BlindedPathConfig>>> =
1608+
Arc::new(RwLock::new(None));
1609+
1610+
let router = Arc::new(LSPS4Router::new(
1611+
inner_router,
1612+
Arc::clone(&lsps4_blinded_path_config),
1613+
Arc::clone(&keys_manager),
16031614
));
16041615

16051616
let mut user_config = default_user_config(&config);
@@ -1801,6 +1812,7 @@ fn build_with_store_internal(
18011812
Arc::clone(&config),
18021813
Arc::clone(&logger),
18031814
Arc::clone(&event_queue),
1815+
Arc::clone(&lsps4_blinded_path_config),
18041816
);
18051817

18061818
lsc.lsps1_client.as_ref().map(|config| {

src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ pub mod logger;
9696
mod message_handler;
9797
pub mod payment;
9898
mod peer_store;
99+
mod router;
99100
mod runtime;
100101
mod scoring;
101102
mod tx_broadcaster;
@@ -880,8 +881,12 @@ impl Node {
880881
#[cfg(not(feature = "uniffi"))]
881882
pub fn bolt12_payment(&self) -> Bolt12Payment {
882883
Bolt12Payment::new(
884+
Arc::clone(&self.runtime),
883885
Arc::clone(&self.channel_manager),
886+
Arc::clone(&self.connection_manager),
887+
self.liquidity_source.clone(),
884888
Arc::clone(&self.payment_store),
889+
Arc::clone(&self.peer_store),
885890
Arc::clone(&self.config),
886891
Arc::clone(&self.is_running),
887892
Arc::clone(&self.logger),
@@ -895,8 +900,12 @@ impl Node {
895900
#[cfg(feature = "uniffi")]
896901
pub fn bolt12_payment(&self) -> Arc<Bolt12Payment> {
897902
Arc::new(Bolt12Payment::new(
903+
Arc::clone(&self.runtime),
898904
Arc::clone(&self.channel_manager),
905+
Arc::clone(&self.connection_manager),
906+
self.liquidity_source.clone(),
899907
Arc::clone(&self.payment_store),
908+
Arc::clone(&self.peer_store),
900909
Arc::clone(&self.config),
901910
Arc::clone(&self.is_running),
902911
Arc::clone(&self.logger),

src/liquidity.rs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ use tokio::sync::oneshot;
4646
use crate::builder::BuildError;
4747
use crate::chain::ChainSource;
4848
use crate::connection::ConnectionManager;
49+
use crate::router::LSPS4BlindedPathConfig;
4950
use crate::event::EventQueue;
5051
use crate::logger::{log_debug, log_error, log_info, Logger};
5152
use crate::runtime::Runtime;
@@ -199,6 +200,7 @@ where
199200
lsps2_service: Option<LSPS2Service>,
200201
lsps4_client: Option<LSPS4Client>,
201202
lsps4_service: Option<LSPS4Service>,
203+
lsps4_blinded_path_config: Arc<RwLock<Option<LSPS4BlindedPathConfig>>>,
202204
wallet: Arc<Wallet>,
203205
channel_manager: Arc<ChannelManager>,
204206
keys_manager: Arc<KeysManager>,
@@ -218,6 +220,7 @@ where
218220
wallet: Arc<Wallet>, channel_manager: Arc<ChannelManager>, keys_manager: Arc<KeysManager>,
219221
chain_source: Arc<ChainSource>, tx_broadcaster: Arc<Broadcaster>, kv_store: Arc<DynStore>,
220222
config: Arc<Config>, logger: L, event_queue: Arc<EventQueue<L>>,
223+
lsps4_blinded_path_config: Arc<RwLock<Option<LSPS4BlindedPathConfig>>>,
221224
) -> Self {
222225
let lsps1_client = None;
223226
let lsps2_client = None;
@@ -230,6 +233,7 @@ where
230233
lsps2_service,
231234
lsps4_client,
232235
lsps4_service,
236+
lsps4_blinded_path_config,
233237
wallet,
234238
channel_manager,
235239
keys_manager,
@@ -358,6 +362,7 @@ where
358362
lsps2_service: self.lsps2_service,
359363
lsps4_client: self.lsps4_client,
360364
lsps4_service: self.lsps4_service,
365+
lsps4_blinded_path_config: self.lsps4_blinded_path_config,
361366
wallet: self.wallet,
362367
channel_manager: self.channel_manager,
363368
peer_manager: RwLock::new(None),
@@ -379,6 +384,7 @@ where
379384
lsps2_service: Option<LSPS2Service>,
380385
lsps4_client: Option<LSPS4Client>,
381386
lsps4_service: Option<LSPS4Service>,
387+
lsps4_blinded_path_config: Arc<RwLock<Option<LSPS4BlindedPathConfig>>>,
382388
wallet: Arc<Wallet>,
383389
channel_manager: Arc<ChannelManager>,
384390
peer_manager: RwLock<Option<Arc<PeerManager>>>,
@@ -397,6 +403,33 @@ where
397403
*self.peer_manager.write().unwrap() = Some(peer_manager);
398404
}
399405

406+
/// Updates the LSPS4 blinded path config used by the router for BOLT12 offers.
407+
///
408+
/// This is called after successful LSPS4 registration to enable BOLT12 offers
409+
/// to create blinded payment paths through the LSP.
410+
pub(crate) fn set_lsps4_blinded_path_config(
411+
&self, lsp_node_id: PublicKey, intercept_scid: u64, cltv_expiry_delta: u32,
412+
) {
413+
let config = LSPS4BlindedPathConfig { lsp_node_id, intercept_scid, cltv_expiry_delta };
414+
*self.lsps4_blinded_path_config.write().unwrap() = Some(config);
415+
log_info!(
416+
self.logger,
417+
"LSPS4 blinded path config set: lsp_node_id={}, intercept_scid={}, cltv_expiry_delta={}",
418+
lsp_node_id,
419+
intercept_scid,
420+
cltv_expiry_delta
421+
);
422+
}
423+
424+
/// Clears the LSPS4 blinded path config.
425+
///
426+
/// Call this after a JIT channel has been opened so that subsequent BOLT12 offers
427+
/// use normal blinded paths through the existing channel instead of LSPS4.
428+
pub(crate) fn clear_lsps4_blinded_path_config(&self) {
429+
*self.lsps4_blinded_path_config.write().unwrap() = None;
430+
log_info!(self.logger, "LSPS4 blinded path config cleared");
431+
}
432+
400433
pub(crate) fn liquidity_manager(&self) -> Arc<LiquidityManager<L>> {
401434
Arc::clone(&self.liquidity_manager)
402435
}
@@ -1656,7 +1689,7 @@ where
16561689
}
16571690
log_info!(self.logger, "TIMING: lsps4_register_node() sent request, waiting for response...");
16581691

1659-
let result = tokio::time::timeout(
1692+
let response = tokio::time::timeout(
16601693
Duration::from_secs(LIQUIDITY_REQUEST_TIMEOUT_SECS),
16611694
register_node_receiver,
16621695
)
@@ -1668,10 +1701,26 @@ where
16681701
.map_err(|e| {
16691702
log_error!(self.logger, "Failed to handle response from liquidity service: {}", e);
16701703
Error::LiquidityRequestFailed
1671-
});
1704+
})?;
1705+
1706+
// Update the LSPS4 blinded path config for BOLT12 offers
1707+
self.set_lsps4_blinded_path_config(
1708+
lsps4_client.lsp_node_id,
1709+
response.intercept_scid,
1710+
response.cltv_expiry_delta,
1711+
);
16721712

16731713
log_info!(self.logger, "TIMING: lsps4_register_node() TOTAL took {}ms", fn_start.elapsed().as_millis());
1674-
result
1714+
Ok(response)
1715+
}
1716+
1717+
/// Registers with LSPS4 for BOLT12 offer creation.
1718+
///
1719+
/// This populates the LSPS4 blinded path config used by the router when creating
1720+
/// blinded payment paths for BOLT12 offers.
1721+
pub(crate) async fn lsps4_register_for_bolt12(&self) -> Result<(), Error> {
1722+
self.lsps4_register_node().await?;
1723+
Ok(())
16751724
}
16761725

16771726
pub(crate) async fn lsps4_receive_to_jit_channel(

0 commit comments

Comments
 (0)