Skip to content

Commit ea93afe

Browse files
committed
Refactor liquidity source to support multiple LSP nodes
Replace per-protocol single-LSP configuration `LSPS1Client, LSPS2Client` with a unified `Vec<LspNode>` model where users configure LSP nodes via `add_lsp()` and protocol support is discovered at runtime via LSPS0 `list_protocols`. - Replace separate `LSPS1Client/LSPS2Client` with global pending request maps keyed by `LSPSRequestId` - Add LSPS0 protocol discovery `discover_lsp_protocols` with event handling for `ListProtocolsResponse` - Update events to use is_lsps_node() for multi-LSP counterparty checks - Deprecate `set_liquidity_source_lsps1/lsps2` builder methods in favor of `add_lsp()` - LSPS2 JIT channels now query all LSPS2-capable LSPs and automatically select the cheapest fee offer across all of them - Add `request_channel_from_lsp()` for explicit LSPS1 LSP selection - Spawn background discovery task on `Node::start()`
1 parent a555133 commit ea93afe

File tree

13 files changed

+2139
-1614
lines changed

13 files changed

+2139
-1614
lines changed

bindings/ldk_node.udl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ interface Builder {
4545
void set_pathfinding_scores_source(string url);
4646
void set_liquidity_source_lsps1(PublicKey node_id, SocketAddress address, string? token);
4747
void set_liquidity_source_lsps2(PublicKey node_id, SocketAddress address, string? token);
48+
void add_lsp(PublicKey node_id, SocketAddress address, string? token);
4849
void set_storage_dir_path(string storage_dir_path);
4950
void set_filesystem_logger(string? log_file_path, LogLevel? max_log_level);
5051
void set_log_facade_logger();
@@ -138,6 +139,8 @@ interface Node {
138139
boolean verify_signature([ByRef]sequence<u8> msg, [ByRef]string sig, [ByRef]PublicKey pkey);
139140
[Throws=NodeError]
140141
bytes export_pathfinding_scores();
142+
[Throws=NodeError]
143+
void add_lsp(PublicKey node_id, SocketAddress address, string? token);
141144
};
142145

143146
typedef interface Bolt11Payment;

src/builder.rs

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,7 @@ use crate::io::{
6565
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
6666
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
6767
};
68-
use crate::liquidity::{
69-
LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder,
70-
};
68+
use crate::liquidity::{LSPS2ServiceConfig, LiquiditySourceBuilder, LspConfig};
7169
use crate::lnurl_auth::LnurlAuth;
7270
use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger};
7371
use crate::message_handler::NodeCustomMessageHandler;
@@ -120,10 +118,8 @@ struct PathfindingScoresSyncConfig {
120118

121119
#[derive(Debug, Clone, Default)]
122120
struct LiquiditySourceConfig {
123-
// Act as an LSPS1 client connecting to the given service.
124-
lsps1_client: Option<LSPS1ClientConfig>,
125-
// Act as an LSPS2 client connecting to the given service.
126-
lsps2_client: Option<LSPS2ClientConfig>,
121+
// Acts for both LSPS1 and LSPS2 clients connecting to the given service.
122+
lsp_nodes: Vec<LspConfig>,
127123
// Act as an LSPS2 service.
128124
lsps2_service: Option<LSPS2ServiceConfig>,
129125
}
@@ -440,17 +436,12 @@ impl NodeBuilder {
440436
/// The given `token` will be used by the LSP to authenticate the user.
441437
///
442438
/// [bLIP-51 / LSPS1]: https://github.com/lightning/blips/blob/master/blip-0051.md
439+
#[deprecated(note = "Use `add_lsp` instead")]
440+
#[allow(dead_code)]
443441
pub fn set_liquidity_source_lsps1(
444442
&mut self, node_id: PublicKey, address: SocketAddress, token: Option<String>,
445443
) -> &mut Self {
446-
// Mark the LSP as trusted for 0conf
447-
self.config.trusted_peers_0conf.push(node_id.clone());
448-
449-
let liquidity_source_config =
450-
self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default());
451-
let lsps1_client_config = LSPS1ClientConfig { node_id, address, token };
452-
liquidity_source_config.lsps1_client = Some(lsps1_client_config);
453-
self
444+
self.add_lsp(node_id, address, token)
454445
}
455446

456447
/// Configures the [`Node`] instance to source just-in-time inbound liquidity from the given
@@ -461,16 +452,32 @@ impl NodeBuilder {
461452
/// The given `token` will be used by the LSP to authenticate the user.
462453
///
463454
/// [bLIP-52 / LSPS2]: https://github.com/lightning/blips/blob/master/blip-0052.md
455+
#[deprecated(note = "Use `add_lsp` instead")]
456+
#[allow(dead_code)]
464457
pub fn set_liquidity_source_lsps2(
465458
&mut self, node_id: PublicKey, address: SocketAddress, token: Option<String>,
459+
) -> &mut Self {
460+
self.add_lsp(node_id, address, token)
461+
}
462+
463+
/// Configures the [`Node`] instance to source inbound liquidity from the given LSP, without specifying
464+
/// the exact protocol used (e.g., LSPS1 or LSPS2).
465+
///
466+
/// Will mark the LSP as trusted for 0-confirmation channels, see [`Config::trusted_peers_0conf`].
467+
///
468+
/// The given `token` will be used by the LSP to authenticate the user.
469+
/// This method is useful when the user wants to connect to an LSP but does not want to be concerned with
470+
/// the specific protocol used for liquidity provision. The node will automatically detect and use the
471+
/// appropriate protocol supported by the LSP.
472+
pub fn add_lsp(
473+
&mut self, node_id: PublicKey, address: SocketAddress, token: Option<String>,
466474
) -> &mut Self {
467475
// Mark the LSP as trusted for 0conf
468-
self.config.trusted_peers_0conf.push(node_id.clone());
476+
self.config.trusted_peers_0conf.push(node_id);
469477

470478
let liquidity_source_config =
471479
self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default());
472-
let lsps2_client_config = LSPS2ClientConfig { node_id, address, token };
473-
liquidity_source_config.lsps2_client = Some(lsps2_client_config);
480+
liquidity_source_config.lsp_nodes.push(LspConfig { node_id, address, token });
474481
self
475482
}
476483

@@ -964,7 +971,7 @@ impl ArcedNodeBuilder {
964971
pub fn set_liquidity_source_lsps1(
965972
&self, node_id: PublicKey, address: SocketAddress, token: Option<String>,
966973
) {
967-
self.inner.write().unwrap().set_liquidity_source_lsps1(node_id, address, token);
974+
self.inner.write().unwrap().add_lsp(node_id, address, token);
968975
}
969976

970977
/// Configures the [`Node`] instance to source just-in-time inbound liquidity from the given
@@ -978,7 +985,20 @@ impl ArcedNodeBuilder {
978985
pub fn set_liquidity_source_lsps2(
979986
&self, node_id: PublicKey, address: SocketAddress, token: Option<String>,
980987
) {
981-
self.inner.write().unwrap().set_liquidity_source_lsps2(node_id, address, token);
988+
self.inner.write().unwrap().add_lsp(node_id, address, token);
989+
}
990+
991+
/// Configures the [`Node`] instance to source inbound liquidity from the given LSP, without specifying
992+
/// the exact protocol used (e.g., LSPS1 or LSPS2).
993+
///
994+
/// Will mark the LSP as trusted for 0-confirmation channels, see [`Config::trusted_peers_0conf`].
995+
///
996+
/// The given `token` will be used by the LSP to authenticate the user.
997+
/// This method is useful when the user wants to connect to an LSP but does not want to be concerned with
998+
/// the specific protocol used for liquidity provision. The node will automatically detect and use the
999+
/// appropriate protocol supported by the LSP.
1000+
pub fn add_lsp(&self, node_id: PublicKey, address: SocketAddress, token: Option<String>) {
1001+
self.inner.write().unwrap().add_lsp(node_id, address, token);
9821002
}
9831003

9841004
/// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time
@@ -1802,21 +1822,7 @@ fn build_with_store_internal(
18021822
Arc::clone(&logger),
18031823
);
18041824

1805-
lsc.lsps1_client.as_ref().map(|config| {
1806-
liquidity_source_builder.lsps1_client(
1807-
config.node_id,
1808-
config.address.clone(),
1809-
config.token.clone(),
1810-
)
1811-
});
1812-
1813-
lsc.lsps2_client.as_ref().map(|config| {
1814-
liquidity_source_builder.lsps2_client(
1815-
config.node_id,
1816-
config.address.clone(),
1817-
config.token.clone(),
1818-
)
1819-
});
1825+
liquidity_source_builder.set_lsp_nodes(lsc.lsp_nodes.clone());
18201826

18211827
let promise_secret = {
18221828
let lsps_xpriv = derive_xprv(
@@ -1885,7 +1891,9 @@ fn build_with_store_internal(
18851891
}
18861892
}));
18871893

1888-
liquidity_source.as_ref().map(|l| l.set_peer_manager(Arc::downgrade(&peer_manager)));
1894+
liquidity_source
1895+
.as_ref()
1896+
.map(|l| l.lsps2_service().set_peer_manager(Arc::downgrade(&peer_manager)));
18891897

18901898
let connection_manager = Arc::new(ConnectionManager::new(
18911899
Arc::clone(&peer_manager),

src/event.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -581,15 +581,15 @@ where
581581
Ok(final_tx) => {
582582
let needs_manual_broadcast =
583583
self.liquidity_source.as_ref().map_or(false, |ls| {
584-
ls.as_ref().lsps2_channel_needs_manual_broadcast(
584+
ls.as_ref().lsps2_service().lsps2_channel_needs_manual_broadcast(
585585
counterparty_node_id,
586586
user_channel_id,
587587
)
588588
});
589589

590590
let result = if needs_manual_broadcast {
591591
self.liquidity_source.as_ref().map(|ls| {
592-
ls.lsps2_store_funding_transaction(
592+
ls.lsps2_service().lsps2_store_funding_transaction(
593593
user_channel_id,
594594
counterparty_node_id,
595595
final_tx.clone(),
@@ -653,7 +653,8 @@ where
653653
},
654654
LdkEvent::FundingTxBroadcastSafe { user_channel_id, counterparty_node_id, .. } => {
655655
self.liquidity_source.as_ref().map(|ls| {
656-
ls.lsps2_funding_tx_broadcast_safe(user_channel_id, counterparty_node_id);
656+
ls.lsps2_service()
657+
.lsps2_funding_tx_broadcast_safe(user_channel_id, counterparty_node_id);
657658
});
658659
},
659660
LdkEvent::PaymentClaimable {
@@ -1139,7 +1140,10 @@ where
11391140
LdkEvent::ProbeFailed { .. } => {},
11401141
LdkEvent::HTLCHandlingFailed { failure_type, .. } => {
11411142
if let Some(liquidity_source) = self.liquidity_source.as_ref() {
1142-
liquidity_source.handle_htlc_handling_failed(failure_type).await;
1143+
liquidity_source
1144+
.lsps2_service()
1145+
.handle_htlc_handling_failed(failure_type)
1146+
.await;
11431147
}
11441148
},
11451149
LdkEvent::SpendableOutputs { outputs, channel_id, counterparty_node_id } => {
@@ -1238,14 +1242,15 @@ where
12381242
let user_channel_id: u128 = u128::from_ne_bytes(
12391243
self.keys_manager.get_secure_random_bytes()[..16].try_into().unwrap(),
12401244
);
1241-
let allow_0conf = self.config.trusted_peers_0conf.contains(&counterparty_node_id);
1242-
let mut channel_override_config = None;
1243-
if let Some((lsp_node_id, _)) = self
1245+
let is_lsp_node = self
12441246
.liquidity_source
12451247
.as_ref()
1246-
.and_then(|ls| ls.as_ref().get_lsps2_lsp_details())
1247-
{
1248-
if lsp_node_id == counterparty_node_id {
1248+
.map_or(false, |ls| ls.as_ref().is_lsps_node(&counterparty_node_id));
1249+
let allow_0conf =
1250+
self.config.trusted_peers_0conf.contains(&counterparty_node_id) || is_lsp_node;
1251+
let mut channel_override_config = None;
1252+
if let Some(ls) = self.liquidity_source.as_ref() {
1253+
if ls.as_ref().is_lsps_node(&counterparty_node_id) {
12491254
// When we're an LSPS2 client, allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll
12501255
// check that they don't take too much before claiming.
12511256
//
@@ -1390,6 +1395,7 @@ where
13901395
if let Some(liquidity_source) = self.liquidity_source.as_ref() {
13911396
let skimmed_fee_msat = skimmed_fee_msat.unwrap_or(0);
13921397
liquidity_source
1398+
.lsps2_service()
13931399
.handle_payment_forwarded(Some(next_htlc.channel_id), skimmed_fee_msat)
13941400
.await;
13951401
}
@@ -1499,6 +1505,7 @@ where
14991505

15001506
if let Some(liquidity_source) = self.liquidity_source.as_ref() {
15011507
liquidity_source
1508+
.lsps2_service()
15021509
.handle_channel_ready(user_channel_id, &channel_id, &counterparty_node_id)
15031510
.await;
15041511
}
@@ -1570,6 +1577,7 @@ where
15701577
} => {
15711578
if let Some(liquidity_source) = self.liquidity_source.as_ref() {
15721579
liquidity_source
1580+
.lsps2_service()
15731581
.handle_htlc_intercepted(
15741582
requested_next_hop_scid,
15751583
intercept_id,

src/lib.rs

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ use types::{
180180
pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, SyncAndAsyncKVStore, UserChannelId};
181181
pub use vss_client;
182182

183+
use crate::liquidity::LspConfig;
183184
use crate::scoring::setup_background_pathfinding_scores_sync;
184185
use crate::wallet::FundingAmount;
185186

@@ -674,6 +675,29 @@ impl Node {
674675
});
675676
}
676677

678+
if let Some(liquidity_source) = self.liquidity_source.as_ref() {
679+
let discovery_ls = Arc::clone(&liquidity_source);
680+
let discovery_cm = Arc::clone(&self.connection_manager);
681+
let discovery_logger = Arc::clone(&self.logger);
682+
self.runtime.spawn_background_task(async move {
683+
for (node_id, address) in discovery_ls.get_all_lsp_details() {
684+
if let Err(e) =
685+
discovery_cm.connect_peer_if_necessary(node_id, address.clone()).await
686+
{
687+
log_error!(
688+
discovery_logger,
689+
"Failed to connect to LSP {} for protocol discovery: {}",
690+
node_id,
691+
e
692+
);
693+
continue;
694+
}
695+
}
696+
697+
discovery_ls.discover_all_lsp_protocols().await;
698+
});
699+
}
700+
677701
log_info!(self.logger, "Startup complete.");
678702
*is_running_lock = true;
679703
Ok(())
@@ -1048,7 +1072,7 @@ impl Node {
10481072
Arc::clone(&self.runtime),
10491073
Arc::clone(&self.wallet),
10501074
Arc::clone(&self.connection_manager),
1051-
self.liquidity_source.clone(),
1075+
self.liquidity_source.as_ref().map(|ls| ls.lsps1_client()),
10521076
Arc::clone(&self.logger),
10531077
)
10541078
}
@@ -1062,7 +1086,7 @@ impl Node {
10621086
Arc::clone(&self.runtime),
10631087
Arc::clone(&self.wallet),
10641088
Arc::clone(&self.connection_manager),
1065-
self.liquidity_source.clone(),
1089+
self.liquidity_source.as_ref().map(|ls| ls.lsps1_client()),
10661090
Arc::clone(&self.logger),
10671091
))
10681092
}
@@ -1949,6 +1973,39 @@ impl Node {
19491973
Error::PersistenceFailed
19501974
})
19511975
}
1976+
1977+
/// Configures the [`Node`] instance to source inbound liquidity from the given LSP at runtime,
1978+
/// without specifying the exact protocol used (e.g., LSPS1 or LSPS2).
1979+
///
1980+
/// LSP nodes are automatically trusted for 0-confirmation channels.
1981+
///
1982+
/// The given `token` will be used by the LSP to authenticate the user.
1983+
/// This method is useful when the user wants to connect to an LSP but does not want to be concerned with
1984+
/// the specific protocol used for liquidity provision. The node will automatically detect and use the
1985+
/// appropriate protocol supported by the LSP.
1986+
pub fn add_lsp(
1987+
&self, node_id: PublicKey, address: SocketAddress, token: Option<String>,
1988+
) -> Result<(), Error> {
1989+
let liquidity_source =
1990+
self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
1991+
let lsp_config = LspConfig { node_id, address, token };
1992+
liquidity_source.add_lsp_node(lsp_config.clone())?;
1993+
1994+
let con_node_id = lsp_config.node_id;
1995+
let con_addr = lsp_config.address.clone();
1996+
let con_cm = Arc::clone(&self.connection_manager);
1997+
1998+
self.runtime.block_on(async move {
1999+
con_cm.connect_peer_if_necessary(con_node_id, con_addr).await
2000+
})?;
2001+
2002+
log_info!(self.logger, "Connected to LSP {}@{}. ", lsp_config.node_id, lsp_config.address);
2003+
2004+
let node_id = lsp_config.node_id;
2005+
self.runtime
2006+
.block_on(async move { liquidity_source.discover_lsp_protocols(&node_id).await })?;
2007+
Ok(())
2008+
}
19522009
}
19532010

19542011
impl Drop for Node {

0 commit comments

Comments
 (0)