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
38 changes: 37 additions & 1 deletion src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1585,11 +1585,47 @@ 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 all_channels =
self.channel_manager.list_channels_with_counterparty(&counterparty_node_id);

let has_other_channels =
all_channels.iter().any(|c| c.channel_id != channel_id);

let counterparty_initiated = matches!(
reason,
ClosureReason::CounterpartyForceClosed { .. }
| ClosureReason::CounterpartyInitiatedCooperativeClosure
);

if !has_other_channels && counterparty_initiated {
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),
counterparty_node_id,
reason: Some(reason),
reason: Some(reason.clone()),
};

match self.event_queue.add_event(event).await {
Expand Down
8 changes: 7 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1842,7 +1842,13 @@ impl Node {
}

// Check if this was the last open channel, if so, forget the peer.
if open_channels.len() == 1 {
//For force-closes we keep the peer
// in the store so the background reconnection task can fire and
// complete the channel_reestablish recovery flow. This is especially
// important for LND peers
// The peer will be removed by the ChannelClosed event handler once
// the counterparty reconnects and confirms closure.
if open_channels.len() == 1 && !force {
self.peer_store.remove_peer(&counterparty_node_id)?;
}
}
Expand Down
121 changes: 121 additions & 0 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2957,3 +2957,124 @@ async fn splice_in_with_all_balance() {
node_a.stop().unwrap();
node_b.stop().unwrap();
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_peer_removed_on_counterparty_force_close() {
// When the counterparty force-closes the last channel between us, we
// should remove them from the peer store — no need to keep reconnecting.
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
let chain_source = random_chain_source(&bitcoind, &electrsd);
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);

let addr_a = node_a.onchain_payment().new_address().unwrap();
let addr_b = node_b.onchain_payment().new_address().unwrap();

premine_and_distribute_funds(
&bitcoind.client,
&electrsd.client,
vec![addr_a, addr_b],
Amount::from_sat(100_000),
)
.await;

node_a.sync_wallets().unwrap();
node_b.sync_wallets().unwrap();

// Open a channel from A to B and wait for it to be ready.
open_channel(&node_a, &node_b, 50_000, true, &electrsd).await;
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
node_a.sync_wallets().unwrap();
node_b.sync_wallets().unwrap();
expect_event!(node_a, ChannelReady);
expect_event!(node_b, ChannelReady);

// Confirm B is in A's peer store.
let node_b_id = node_b.node_id();
let node_a_id = node_a.node_id();
assert!(
node_a.list_peers().iter().any(|p| p.node_id == node_b_id),
"node_b should be in node_a peer store after channel open"
);

// B force-closes (counterparty-initiated from A's perspective).
let channels_b = node_b.list_channels();
assert_eq!(channels_b.len(), 1);
node_b.force_close_channel(&channels_b[0].user_channel_id, node_a_id, None).unwrap();

generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
node_a.sync_wallets().unwrap();
node_b.sync_wallets().unwrap();

expect_event!(node_a, ChannelClosed);
expect_event!(node_b, ChannelClosed);

// checks if peer is persisted in the store
assert!(
!node_a.list_peers().iter().any(|p| p.node_id == node_b_id && p.is_persisted),
"node_b should not be persisted in node_a peer store after counterparty force-close"
);

node_a.stop().unwrap();
node_b.stop().unwrap();
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_peer_retained_on_local_force_close() {
// When WE force-close, keep the peer in the store so we can reconnect
// and complete channel_reestablish recovery. This matters especially
// for LND counterparties that may not handle force-closure error
// messages correctly.
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
let chain_source = random_chain_source(&bitcoind, &electrsd);
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);

let addr_a = node_a.onchain_payment().new_address().unwrap();
let addr_b = node_b.onchain_payment().new_address().unwrap();

premine_and_distribute_funds(
&bitcoind.client,
&electrsd.client,
vec![addr_a, addr_b],
Amount::from_sat(100_000),
)
.await;

node_a.sync_wallets().unwrap();
node_b.sync_wallets().unwrap();

// Open a channel from A to B and wait for it to be ready.
open_channel(&node_a, &node_b, 50_000, true, &electrsd).await;
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
node_a.sync_wallets().unwrap();
node_b.sync_wallets().unwrap();
expect_event!(node_a, ChannelReady);
expect_event!(node_b, ChannelReady);

// Confirm B is in A's peer store.
let node_b_id = node_b.node_id();
assert!(
node_a.list_peers().iter().any(|p| p.node_id == node_b_id),
"node_b should be in node_a peer store after channel open"
);

// A force-closes (locally-initiated from A's perspective).
let channels_a = node_a.list_channels();
assert_eq!(channels_a.len(), 1);
node_a.force_close_channel(&channels_a[0].user_channel_id, node_b_id, None).unwrap();

generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
node_a.sync_wallets().unwrap();
node_b.sync_wallets().unwrap();

expect_event!(node_a, ChannelClosed);
expect_event!(node_b, ChannelClosed);

// A should STILL have B in peer store — kept for channel_reestablish.
assert!(
node_a.list_peers().iter().any(|p| p.node_id == node_b_id && p.is_persisted),
"node_b should remain persisted in node_a peer store after locally-initiated force-close"
);

node_a.stop().unwrap();
node_b.stop().unwrap();
}
Loading