From 7609c26c83245f6bc4249ada711d8ba7a210b564 Mon Sep 17 00:00:00 2001 From: jolah1 Date: Thu, 30 Apr 2026 21:17:43 +0100 Subject: [PATCH 1/3] event: remove peer from store on counterparty-initiated force-close --- src/event.rs | 38 ++++++++++++++++++++++++++++++++++++++ src/lib.rs | 12 ++++++++++-- tests/common/mod.rs | 14 ++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/event.rs b/src/event.rs index 65fe683ec..59e442fc7 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1585,6 +1585,44 @@ where } => { log_info!(self.logger, "Channel {} closed due to: {}", channel_id, reason); + // If the counterparty initiated closure of their last remaining channel + // with us, remove them from the peer store so we stop trying to reconnect. + // + // If we initiated the closure, keep them in the peer store so the + // background reconnection task fires and we can complete the + // channel_reestablish recovery flow. This matters especially for LND + // peers, which need us to reconnect to recover from force-closures. + // + // We exclude `channel_id` from the remaining-channel check because LDK + // fires ChannelClosed before removing the channel from its internal list, + // so list_channels_with_counterparty still includes the closing channel. + if let Some(counterparty_node_id) = counterparty_node_id { + let counterparty_initiated = matches!( + reason, + ClosureReason::CounterpartyForceClosed { .. } + | ClosureReason::CounterpartyInitiatedCooperativeClosure + ); + + if counterparty_initiated { + let has_other_channels = self + .channel_manager + .list_channels_with_counterparty(&counterparty_node_id) + .iter() + .any(|c| c.channel_id != channel_id); + + if !has_other_channels { + if let Err(e) = self.peer_store.remove_peer(&counterparty_node_id) { + log_error!( + self.logger, + "Failed to remove peer {} from peer store: {}", + counterparty_node_id, + e + ); + } + } + } + } + let event = Event::ChannelClosed { channel_id, user_channel_id: UserChannelId(user_channel_id), diff --git a/src/lib.rs b/src/lib.rs index b95e84470..cf0e76ec2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1841,8 +1841,16 @@ impl Node { })?; } - // Check if this was the last open channel, if so, forget the peer. - if open_channels.len() == 1 { + // If this was the last open channel and we're closing cooperatively, forget the peer + // since we have no further reason to reconnect. + + // For force-closes we intentionally keep the peer in the store so the background reconnection + // task keeps firing and can drive the channel_reestablish recovery flow. + // This is especially important against LND peers, which don't always handle force-closure error messages correctly. + + //Note that this means a force-closed peer is retained until the user explicitly calls Node::disconnect. + + if open_channels.len() == 1 && !force { self.peer_store.remove_peer(&counterparty_node_id)?; } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f92e02cc7..9f60817b3 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1545,6 +1545,20 @@ pub(crate) async fn do_channel_full_cycle( node_b.sync_wallets().unwrap(); } + if force_close { + // Peer retained after local force-close to allow channel_reestablish recovery. + assert!( + node_a.list_peers().iter().any(|p| p.node_id == node_b.node_id() && p.is_persisted), + "node_b should remain persisted in node_a peer store after locally-initiated force-close" + ); + } else { + // Peer removed after cooperative close — no further reason to reconnect. + assert!( + !node_a.list_peers().iter().any(|p| p.node_id == node_b.node_id() && p.is_persisted), + "node_b should be removed from node_a peer store after cooperative close" + ); + } + let sum_of_all_payments_sat = (push_msat + invoice_amount_1_msat + overpaid_amount_msat From 6c6e9a70e8e4437f4abcedbc402390f86741d188 Mon Sep 17 00:00:00 2001 From: jolah1 Date: Mon, 4 May 2026 21:25:29 +0100 Subject: [PATCH 2/3] trim duplicate comments in ChannelClosed --- src/event.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/event.rs b/src/event.rs index 59e442fc7..98d9064e0 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1585,17 +1585,12 @@ where } => { log_info!(self.logger, "Channel {} closed due to: {}", channel_id, reason); - // If the counterparty initiated closure of their last remaining channel - // with us, remove them from the peer store so we stop trying to reconnect. - // - // If we initiated the closure, keep them in the peer store so the - // background reconnection task fires and we can complete the - // channel_reestablish recovery flow. This matters especially for LND - // peers, which need us to reconnect to recover from force-closures. - // - // We exclude `channel_id` from the remaining-channel check because LDK - // fires ChannelClosed before removing the channel from its internal list, - // so list_channels_with_counterparty still includes the closing channel. + // If the counterparty initiated closure of their last remaining channel, + // remove them from the peer store so we stop reconnect attempts. + + // We exclude `channel_id` from the check because LDK emits + // ChannelClosed before removing the channel from its internal list. + if let Some(counterparty_node_id) = counterparty_node_id { let counterparty_initiated = matches!( reason, From 25fc22a1670c7d1004e544424e244a9e61d9ad8f Mon Sep 17 00:00:00 2001 From: jolah1 Date: Tue, 5 May 2026 16:35:29 +0100 Subject: [PATCH 3/3] replay ChannelClosed event on peer removal failure --- src/event.rs | 1 + src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/event.rs b/src/event.rs index 98d9064e0..5b57fcf4d 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1613,6 +1613,7 @@ where counterparty_node_id, e ); + return Err(ReplayEvent()); } } } diff --git a/src/lib.rs b/src/lib.rs index cf0e76ec2..2c8ac9222 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1848,7 +1848,7 @@ impl Node { // task keeps firing and can drive the channel_reestablish recovery flow. // This is especially important against LND peers, which don't always handle force-closure error messages correctly. - //Note that this means a force-closed peer is retained until the user explicitly calls Node::disconnect. + // Note that this means a force-closed peer is retained until the user explicitly calls Node::disconnect. if open_channels.len() == 1 && !force { self.peer_store.remove_peer(&counterparty_node_id)?;