From d8619132b4315bfc888ea2cf0983c97825d798e6 Mon Sep 17 00:00:00 2001 From: Hash Money Date: Sun, 11 Oct 2026 19:06:38 -0700 Subject: [PATCH] Retry LSPS0 discovery for LSPs that were unreachable at start Protocol discovery runs once, in the background, when the node starts. If a configured LSP is unreachable at that moment (e.g. it is restarting), its `supported_protocols` stay `None`, `get_lsps2_nodes` filters it out, and every JIT invoice fails with `LiquiditySourceUnavailable` until the node restarts, even once the LSP is reachable and connected. Before an LSPS2 JIT request, re-attempt discovery (connecting first) for configured LSPs that are still undiscovered after start-up discovery has finished. Failures are logged and the request proceeds as before. The new integration test starts the client while its LSP is not listening, then starts the LSP and connects; without the fix the JIT invoice fails with `LiquiditySourceUnavailable`. --- src/liquidity/mod.rs | 51 ++++++++++++++++++++++++++ src/payment/bolt11.rs | 3 ++ tests/integration_tests_rust.rs | 64 +++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) diff --git a/src/liquidity/mod.rs b/src/liquidity/mod.rs index ffc1f878bc..2ad184a353 100644 --- a/src/liquidity/mod.rs +++ b/src/liquidity/mod.rs @@ -521,6 +521,57 @@ where Ok(protocols) } + /// Re-attempts LSPS0 protocol discovery for configured LSPs whose discovery did not succeed. + /// + /// Protocol discovery otherwise runs once, in the background, when the node starts. If an LSP is + /// unreachable at that moment (e.g., it is restarting), its supported protocols stay unknown and + /// it is skipped by every later LSPS2 request until the node restarts. This retries discovery, + /// connecting first, for those LSPs. It does nothing while start-up discovery is still running + /// (callers already wait for it) and logs, rather than returns, failures: the caller then + /// proceeds with whatever LSPs are known, as before. + pub(crate) async fn rediscover_undiscovered_lsps( + &self, connection_manager: &ConnectionManager, + ) where + CL::Target: LdkLogger, + { + if !*self.discovery_done_rx.borrow() { + return; + } + let undiscovered: Vec<(PublicKey, SocketAddress)> = self + .lsp_nodes + .read() + .expect("lock") + .iter() + .filter(|n| n.supported_protocols.is_none()) + .map(|n| (n.node_id, n.address.clone())) + .collect(); + for (node_id, address) in undiscovered { + if let Err(e) = connection_manager.connect_peer_if_necessary(node_id, address).await { + log_error!( + self.logger, + "Failed to connect to LSP {} to retry protocol discovery: {}", + node_id, + e + ); + continue; + } + match self.discover_lsp_protocols(&node_id).await { + Ok(protocols) => log_info!( + self.logger, + "Discovered protocols for LSP {} on retry: {:?}", + node_id, + protocols + ), + Err(e) => log_error!( + self.logger, + "Retried protocol discovery for LSP {} failed: {}", + node_id, + e + ), + } + } + } + pub(crate) async fn get_lsp_config( &self, node_id: &PublicKey, protocol: u16, ) -> Option { diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 5f0a70c2c6..1a7fab261e 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -130,6 +130,9 @@ impl Bolt11Payment { ) -> Result { let connection_manager = Arc::clone(&self.connection_manager); let res = self.runtime.block_on(async move { + // An LSP that was unreachable when the node started was never discovered; retry it + // now rather than skipping it until the next restart. + self.liquidity_source.rediscover_undiscovered_lsps(&connection_manager).await; if let Some(amount_msat) = amount_msat { self.liquidity_source .lsps2_client() diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index f58cad6904..35a730e386 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -5346,3 +5346,67 @@ async fn do_lsps2_multi_lsp_picks_cheapest(reverse_order: bool) { cheap.stop().unwrap(); expensive.stop().unwrap(); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn lsps2_discovery_retried_after_lsp_unreachable_at_start() { + // If the LSP is unreachable while the client node starts, the client's one-off + // LSPS0 protocol discovery fails. It is never retried: the LSP is left out of + // the LSPS2 candidates, so every JIT invoice fails with + // `LiquiditySourceUnavailable` until the node restarts, even once the LSP is + // reachable and connected. + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + let mut sync_config = EsploraSyncConfig::default(); + sync_config.background_sync_config = None; + + let lsps2_service_config = LSPS2ServiceConfig { + require_token: None, + advertise_service: false, + channel_opening_fee_ppm: 10_000, + channel_over_provisioning_ppm: 100_000, + max_payment_size_msat: 1_000_000_000, + min_payment_size_msat: 0, + min_channel_lifetime: 100, + min_channel_opening_fee_msat: 0, + max_client_to_self_delay: 1024, + client_trusts_lsp: false, + disable_client_reserve: false, + }; + + // The LSP is built but not started: its address is known, nothing listens on it. + let service_config = random_config(); + setup_builder!(service_builder, service_config.node_config); + service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + service_builder.enable_liquidity_provider(lsps2_service_config); + let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); + let service_node_id = service_node.node_id(); + let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); + + let client_config = random_config(); + setup_builder!(client_builder, client_config.node_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.add_liquidity_source(service_node_id, service_addr.clone(), None, true); + let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); + client_node.start().unwrap(); + + // Let the client's start-up discovery run against the closed port and give up. + tokio::time::sleep(Duration::from_secs(5)).await; + + // The LSP comes up, and the client is connected to it. + service_node.start().unwrap(); + client_node.connect(service_node_id, service_addr, false).unwrap(); + + let description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("jit")).unwrap()); + let res = client_node.bolt11_payment().receive_via_jit_channel( + 100_000_000, + &description.into(), + 1024, + None, + ); + assert!(res.is_ok(), "JIT invoice with the LSP reachable and connected failed: {:?}", res.err()); + + service_node.stop().unwrap(); + client_node.stop().unwrap(); +}