Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/liquidity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CL: Deref + Clone + Sync + Send>(
&self, connection_manager: &ConnectionManager<CL>,
) 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<LspConfig> {
Expand Down
3 changes: 3 additions & 0 deletions src/payment/bolt11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ impl Bolt11Payment {
) -> Result<LdkBolt11Invoice, Error> {
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Medium: on-demand discovery can delay every JIT request

rediscover_undiscovered_lsps runs serially before gather_lsps2_offers, awaiting both the peer connection and protocol discovery for each undiscovered LSP. Since discovery can take up to 5 seconds, even one unreachable LSP can delay requests to healthy LSPs, and multiple dead LSPs can multiply that delay.

There's also no cooldown or rate limit, so concurrent invoice requests can all wait on the same failing discovery attempts.

Could we add a cooldown or deduplicate the entire rediscovery operation so requests don't repeatedly pay this latency? Ideally, healthy LSP offers should still be queried without waiting for unreachable peers.

// 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()
Expand Down
64 changes: 64 additions & 0 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Loading