diff --git a/src/event.rs b/src/event.rs index 65fe683ec..5b57fcf4d 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1585,6 +1585,40 @@ where } => { log_info!(self.logger, "Channel {} closed due to: {}", channel_id, reason); + // 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, + 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 + ); + return Err(ReplayEvent()); + } + } + } + } + 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..2c8ac9222 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