Skip to content

Commit 5e241f7

Browse files
committed
Remove force-close peers after reconnect
Retain a force-closed peer until one recovery reconnect completes, then stop persisting it. This gives channel_reestablish a chance to drive recovery without retrying a peer indefinitely after its last channel has closed. AI tools were used in preparing this commit. Co-Authored-By: HAL 9000
1 parent dc237f3 commit 5e241f7

4 files changed

Lines changed: 96 additions & 25 deletions

File tree

src/connection.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ where
5353
self.do_connect_peer(node_id, addr).await
5454
}
5555

56+
pub(crate) fn disconnect_peer(&self, node_id: PublicKey) {
57+
self.peer_manager.disconnect_by_node_id(node_id);
58+
}
59+
5660
pub(crate) async fn do_connect_peer(
5761
&self, node_id: PublicKey, addr: SocketAddress,
5862
) -> Result<(), Error> {

src/event.rs

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use lightning::{impl_writeable_tlv_based, impl_writeable_tlv_based_enum};
3434
use lightning_liquidity::lsps2::utils::compute_opening_fee;
3535
use lightning_types::payment::{PaymentHash, PaymentPreimage};
3636

37-
use crate::config::{may_announce_channel, Config};
37+
use crate::config::{may_announce_channel, Config, PEER_RECONNECTION_INTERVAL};
3838
use crate::connection::ConnectionManager;
3939
use crate::data_store::DataStoreUpdateResult;
4040
use crate::fee_estimator::ConfirmationTarget;
@@ -583,6 +583,68 @@ where
583583
}
584584
}
585585

586+
fn remove_peer_after_reconnect(&self, peer_info: PeerInfo, closed_channel_id: ChannelId) {
587+
let channel_manager = Arc::clone(&self.channel_manager);
588+
let connection_manager = Arc::clone(&self.connection_manager);
589+
let peer_store = Arc::clone(&self.peer_store);
590+
let logger = self.logger.clone();
591+
self.runtime.spawn_cancellable_background_task(async move {
592+
let has_other_channels = || {
593+
channel_manager
594+
.list_channels_with_counterparty(&peer_info.node_id)
595+
.iter()
596+
.any(|c| c.channel_id != closed_channel_id)
597+
};
598+
599+
if peer_store.get_peer(&peer_info.node_id).is_none() || has_other_channels() {
600+
return;
601+
}
602+
603+
// Ensure a connected peer cannot be mistaken for a completed recovery reconnect.
604+
// With no other channels left, reconnecting once gives `channel_reestablish` a chance
605+
// to retransmit the force-close error before we stop persisting the peer.
606+
connection_manager.disconnect_peer(peer_info.node_id);
607+
608+
loop {
609+
if peer_store.get_peer(&peer_info.node_id).is_none() || has_other_channels() {
610+
return;
611+
}
612+
613+
match connection_manager
614+
.connect_peer_if_necessary(peer_info.node_id, peer_info.address.clone())
615+
.await
616+
{
617+
Ok(()) => {
618+
if peer_store.get_peer(&peer_info.node_id).is_none() || has_other_channels()
619+
{
620+
return;
621+
}
622+
if let Err(e) = peer_store.remove_peer(&peer_info.node_id).await {
623+
log_error!(
624+
logger,
625+
"Failed to remove peer {} from peer store: {}",
626+
peer_info.node_id,
627+
e
628+
);
629+
} else {
630+
return;
631+
}
632+
},
633+
Err(e) => {
634+
log_debug!(
635+
logger,
636+
"Failed to reconnect peer {} before removing from peer store: {}",
637+
peer_info.node_id,
638+
e
639+
);
640+
},
641+
}
642+
643+
tokio::time::sleep(PEER_RECONNECTION_INTERVAL).await;
644+
}
645+
});
646+
}
647+
586648
async fn fail_claimable_payment(
587649
&self, payment_id: PaymentId, payment_hash: &PaymentHash,
588650
) -> Result<(), ReplayEvent> {
@@ -1627,25 +1689,24 @@ where
16271689
let counterparty_node_id = counterparty_node_id
16281690
.expect("counterparty_node_id is always set since LDK 0.0.117");
16291691

1630-
// Drop the peer once its last channel with us has reached a terminal state
1631-
// that reconnection cannot recover. Every closure reason is terminal except
1632-
// `HolderForceClosed`: when *we* force-close, we keep reconnecting so that
1633-
// `channel_reestablish` can drive recovery (see `Node::close_channel_internal`).
1692+
// Drop the peer once its last channel with us has reached a terminal state.
1693+
// For `HolderForceClosed`, retain it through one recovery reconnect so that
1694+
// `channel_reestablish` can retransmit the force-close error before cleanup.
16341695
// This also cleans up peers persisted for a channel that closed before funding
16351696
// (e.g. `CounterpartyCoopClosedUnfundedChannel`), which would otherwise be
16361697
// retried forever.
16371698
// We exclude `channel_id` from the count because LDK emits `ChannelClosed`
16381699
// before removing it from its internal list.
1639-
let dont_reconnect = !matches!(reason, ClosureReason::HolderForceClosed { .. });
1640-
1641-
if dont_reconnect {
1642-
let has_other_channels = self
1643-
.channel_manager
1644-
.list_channels_with_counterparty(&counterparty_node_id)
1645-
.iter()
1646-
.any(|c| c.channel_id != channel_id);
1647-
1648-
if !has_other_channels {
1700+
let has_other_channels = self
1701+
.channel_manager
1702+
.list_channels_with_counterparty(&counterparty_node_id)
1703+
.iter()
1704+
.any(|c| c.channel_id != channel_id);
1705+
1706+
let peer_to_reconnect = if !has_other_channels {
1707+
if matches!(reason, ClosureReason::HolderForceClosed { .. }) {
1708+
self.peer_store.get_peer(&counterparty_node_id)
1709+
} else {
16491710
if let Err(e) = self.peer_store.remove_peer(&counterparty_node_id).await {
16501711
log_error!(
16511712
self.logger,
@@ -1655,8 +1716,11 @@ where
16551716
);
16561717
return Err(ReplayEvent());
16571718
}
1719+
None
16581720
}
1659-
}
1721+
} else {
1722+
None
1723+
};
16601724

16611725
let event = Event::ChannelClosed {
16621726
channel_id,
@@ -1672,6 +1736,10 @@ where
16721736
return Err(ReplayEvent());
16731737
},
16741738
};
1739+
1740+
if let Some(peer_info) = peer_to_reconnect {
1741+
self.remove_peer_after_reconnect(peer_info, channel_id);
1742+
}
16751743
},
16761744
LdkEvent::DiscardFunding { channel_id, funding_info } => {
16771745
if let FundingInfo::Contribution { inputs: _, outputs } = funding_info {

src/lib.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2030,12 +2030,10 @@ impl Node {
20302030
}
20312031

20322032
// Peer store cleanup is handled centrally in the `ChannelClosed` event handler,
2033-
// which drops the peer once its last channel reaches a terminal state that
2034-
// reconnection cannot recover. We intentionally do nothing here so that a
2035-
// force-closed peer is retained, letting the background reconnection task keep
2036-
// firing and drive the `channel_reestablish` recovery flow. This is especially
2037-
// important against LND peers, which don't always handle force-closure error
2038-
// messages correctly.
2033+
// which retains a force-closed peer through one recovery reconnect before
2034+
// dropping it. This lets `channel_reestablish` drive the recovery flow, which is
2035+
// especially important against LND peers that don't always handle force-closure
2036+
// error messages correctly.
20392037
}
20402038

20412039
Ok(())

tests/common/mod.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1702,10 +1702,11 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
17021702
}
17031703

17041704
if force_close {
1705-
// Peer retained after local force-close to allow channel_reestablish recovery.
1705+
// The recovery reconnect completed while the force-close settled, so the peer no longer
1706+
// needs to remain persisted.
17061707
assert!(
1707-
node_a.list_peers().iter().any(|p| p.node_id == node_b.node_id() && p.is_persisted),
1708-
"node_b should remain persisted in node_a peer store after locally-initiated force-close"
1708+
!node_a.list_peers().iter().any(|p| p.node_id == node_b.node_id() && p.is_persisted),
1709+
"node_b should be removed from node_a peer store after the recovery reconnect"
17091710
);
17101711
assert_any_node_has_onchain_tx_type(
17111712
&[("node_a", &node_a), ("node_b", &node_b)],

0 commit comments

Comments
 (0)