Skip to content

Commit 4ecc73e

Browse files
feat: BOLT12 receive via LSPS4 JIT channels
Add LSPS4Router that wraps DefaultRouter and creates LSPS4 blinded payment paths when LSPS4 config is set. This enables BOLT12 offers to work with LSPS4 JIT channels for nodes with no existing channels. Also switches MessageRouter from DefaultMessageRouter to NodeIdMessageRouter to avoid compact SCID introduction nodes that mobile wallets (Phoenix etc) can't resolve with filtered gossip.
1 parent b51e5de commit 4ecc73e

7 files changed

Lines changed: 582 additions & 15 deletions

File tree

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(

src/payment/bolt12.rs

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,14 @@ use lightning_types::string::UntrustedString;
2424
use rand::RngCore;
2525

2626
use crate::config::{AsyncPaymentsRole, Config, LDK_PAYMENT_RETRY_TIMEOUT};
27+
use crate::connection::ConnectionManager;
2728
use crate::error::Error;
2829
use crate::ffi::{maybe_deref, maybe_wrap};
30+
use crate::liquidity::LiquiditySource;
2931
use crate::logger::{log_error, log_info, LdkLogger, Logger};
3032
use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus};
33+
use crate::peer_store::{PeerInfo, PeerStore};
34+
use crate::runtime::Runtime;
3135
use crate::types::{ChannelManager, PaymentStore};
3236

3337
#[cfg(not(feature = "uniffi"))]
@@ -52,8 +56,12 @@ type Refund = Arc<crate::ffi::Refund>;
5256
/// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md
5357
/// [`Node::bolt12_payment`]: crate::Node::bolt12_payment
5458
pub struct Bolt12Payment {
59+
runtime: Arc<Runtime>,
5560
channel_manager: Arc<ChannelManager>,
61+
connection_manager: Arc<ConnectionManager<Arc<Logger>>>,
62+
liquidity_source: Option<Arc<LiquiditySource<Arc<Logger>>>>,
5663
payment_store: Arc<PaymentStore>,
64+
peer_store: Arc<PeerStore<Arc<Logger>>>,
5765
config: Arc<Config>,
5866
is_running: Arc<RwLock<bool>>,
5967
logger: Arc<Logger>,
@@ -62,11 +70,25 @@ pub struct Bolt12Payment {
6270

6371
impl Bolt12Payment {
6472
pub(crate) fn new(
65-
channel_manager: Arc<ChannelManager>, payment_store: Arc<PaymentStore>,
73+
runtime: Arc<Runtime>, channel_manager: Arc<ChannelManager>,
74+
connection_manager: Arc<ConnectionManager<Arc<Logger>>>,
75+
liquidity_source: Option<Arc<LiquiditySource<Arc<Logger>>>>,
76+
payment_store: Arc<PaymentStore>, peer_store: Arc<PeerStore<Arc<Logger>>>,
6677
config: Arc<Config>, is_running: Arc<RwLock<bool>>, logger: Arc<Logger>,
6778
async_payments_role: Option<AsyncPaymentsRole>,
6879
) -> Self {
69-
Self { channel_manager, payment_store, config, is_running, logger, async_payments_role }
80+
Self {
81+
runtime,
82+
channel_manager,
83+
connection_manager,
84+
liquidity_source,
85+
payment_store,
86+
peer_store,
87+
config,
88+
is_running,
89+
logger,
90+
async_payments_role,
91+
}
7092
}
7193

7294
/// Send a payment given an offer.
@@ -368,6 +390,93 @@ impl Bolt12Payment {
368390
Ok(maybe_wrap(offer))
369391
}
370392

393+
/// Returns a payable offer that can be used to request and receive a payment of the amount
394+
/// given via a newly created just-in-time (JIT) channel.
395+
///
396+
/// When the returned offer is paid, the configured LSPS4-compliant LSP will open a channel
397+
/// to us, supplying just-in-time inbound liquidity.
398+
///
399+
/// This is useful for nodes that have no channels but want to receive BOLT12 payments.
400+
/// The offer will contain blinded payment paths that route through the LSP.
401+
pub fn receive_via_lsps4_jit_channel(
402+
&self, amount_msat: u64, description: &str, expiry_secs: Option<u32>,
403+
quantity: Option<u64>,
404+
) -> Result<Offer, Error> {
405+
let liquidity_source =
406+
self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
407+
408+
let (node_id, address) =
409+
liquidity_source.get_lsps4_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?;
410+
411+
let peer_info = PeerInfo { node_id, address };
412+
413+
let con_node_id = peer_info.node_id;
414+
let con_addr = peer_info.address.clone();
415+
let con_cm = Arc::clone(&self.connection_manager);
416+
417+
// Connect to LSP
418+
self.runtime.block_on(async move {
419+
con_cm.connect_peer_if_necessary(con_node_id, con_addr).await
420+
})?;
421+
422+
log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address);
423+
424+
// Register with LSPS4 to populate the blinded path config
425+
let liquidity_source = Arc::clone(&liquidity_source);
426+
self.runtime.block_on(async move { liquidity_source.lsps4_register_for_bolt12().await })?;
427+
428+
// Now create the offer - the LSPS4Router will use the config to create blinded payment paths
429+
let offer = self.receive_inner(amount_msat, description, expiry_secs, quantity)?;
430+
431+
// Persist LSP peer to make sure we reconnect on restart.
432+
self.peer_store.add_peer(peer_info)?;
433+
434+
Ok(maybe_wrap(offer))
435+
}
436+
437+
/// Returns a payable offer that can be used to request and receive a payment for which the
438+
/// amount is to be determined by the user via a newly created just-in-time (JIT) channel.
439+
///
440+
/// When the returned offer is paid, the configured LSPS4-compliant LSP will open a channel
441+
/// to us, supplying just-in-time inbound liquidity.
442+
///
443+
/// This is useful for nodes that have no channels but want to receive BOLT12 payments.
444+
/// The offer will contain blinded payment paths that route through the LSP.
445+
pub fn receive_variable_amount_via_lsps4_jit_channel(
446+
&self, description: &str, expiry_secs: Option<u32>,
447+
) -> Result<Offer, Error> {
448+
let liquidity_source =
449+
self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
450+
451+
let (node_id, address) =
452+
liquidity_source.get_lsps4_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?;
453+
454+
let peer_info = PeerInfo { node_id, address };
455+
456+
let con_node_id = peer_info.node_id;
457+
let con_addr = peer_info.address.clone();
458+
let con_cm = Arc::clone(&self.connection_manager);
459+
460+
// Connect to LSP
461+
self.runtime.block_on(async move {
462+
con_cm.connect_peer_if_necessary(con_node_id, con_addr).await
463+
})?;
464+
465+
log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address);
466+
467+
// Register with LSPS4 to populate the blinded path config
468+
let liquidity_source = Arc::clone(&liquidity_source);
469+
self.runtime.block_on(async move { liquidity_source.lsps4_register_for_bolt12().await })?;
470+
471+
// Now create the offer - the LSPS4Router will use the config to create blinded payment paths
472+
let offer = self.receive_variable_amount(description, expiry_secs)?;
473+
474+
// Persist LSP peer to make sure we reconnect on restart.
475+
self.peer_store.add_peer(peer_info)?;
476+
477+
Ok(offer)
478+
}
479+
371480
/// Requests a refund payment for the given [`Refund`].
372481
///
373482
/// The returned [`Bolt12Invoice`] is for informational purposes only (i.e., isn't needed to

0 commit comments

Comments
 (0)