Skip to content

Commit 528b80f

Browse files
jkczyzclaude
andcommitted
Clear disconnect timer when exiting quiescence
Several code paths exit quiescence by calling `clear_quiescent()` directly without also clearing the disconnect timer via `mark_response_received()`. This causes the timer to fire after the splice completes or is aborted, spuriously disconnecting the peer. Replace `clear_quiescent()` with `exit_quiescence()` in `on_tx_signatures_exchange`, `reset_pending_splice_state`, and `peer_connected_get_handshake`, which clears both the quiescent state and the disconnect timer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8a5c6ca commit 528b80f

2 files changed

Lines changed: 200 additions & 4 deletions

File tree

lightning/src/ln/channel.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,7 +1719,10 @@ where
17191719
// We shouldn't be quiescent anymore upon reconnecting if:
17201720
// - We were in quiescence but a splice/RBF was never negotiated or
17211721
// - We were in quiescence but the splice negotiation failed due to disconnecting
1722-
chan.context.channel_state.clear_quiescent();
1722+
//
1723+
// NOTE: While `exit_quiescence` clears the disconnect timer, it should already
1724+
// have been cleared by `remove_uncommitted_htlcs_and_mark_paused`.
1725+
chan.exit_quiescence();
17231726
None
17241727
} else {
17251728
None
@@ -7284,7 +7287,7 @@ where
72847287
self.pending_splice.take();
72857288
}
72867289

7287-
self.context.channel_state.clear_quiescent();
7290+
self.exit_quiescence();
72887291
if current_is_awaiting_signatures {
72897292
self.context.interactive_tx_signing_session.take();
72907293
}
@@ -9341,7 +9344,6 @@ where
93419344
debug_assert!(!self.context.channel_state.is_awaiting_remote_revoke());
93429345

93439346
if let Some(pending_splice) = self.pending_splice.as_mut() {
9344-
self.context.channel_state.clear_quiescent();
93459347
if let Some(FundingNegotiation::AwaitingSignatures {
93469348
mut funding,
93479349
funding_feerate_sat_per_1000_weight,
@@ -9390,6 +9392,8 @@ where
93909392
} else {
93919393
debug_assert!(false);
93929394
}
9395+
9396+
self.exit_quiescence();
93939397
} else {
93949398
self.funding.funding_transaction = Some(funding_tx.clone());
93959399
self.context.channel_state =

lightning/src/ln/splicing_tests.rs

Lines changed: 193 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ use crate::chain::ChannelMonitorUpdateStatus;
1616
use crate::events::{ClosureReason, Event, FundingInfo, HTLCHandlingFailureType};
1717
use crate::ln::chan_utils;
1818
use crate::ln::channel::{
19-
CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY, FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE,
19+
CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY, DISCONNECT_PEER_AWAITING_RESPONSE_TICKS,
20+
FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE,
2021
};
2122
use crate::ln::channelmanager::{provided_init_features, PaymentId, BREAKDOWN_TIMEOUT};
2223
use crate::ln::functional_test_utils::*;
@@ -7082,3 +7083,194 @@ fn test_splice_rbf_rejects_own_low_feerate_after_several_attempts() {
70827083
other => panic!("Expected SpliceFailed, got {:?}", other),
70837084
}
70847085
}
7086+
7087+
#[test]
7088+
fn test_no_disconnect_after_splice_completes() {
7089+
// Test that the disconnect timer is cleared when exiting quiescence after a successful splice
7090+
// negotiation. Previously, `on_tx_signatures_exchange` cleared the quiescent state but not the
7091+
// disconnect timer, causing a spurious disconnect after the splice completed.
7092+
let chanmon_cfgs = create_chanmon_cfgs(2);
7093+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
7094+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
7095+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
7096+
7097+
let initial_channel_value_sat = 100_000;
7098+
let (_, _, channel_id, _) =
7099+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
7100+
7101+
let added_value = Amount::from_sat(50_000);
7102+
provide_utxo_reserves(&nodes, 2, added_value * 2);
7103+
7104+
let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
7105+
let new_funding_script = complete_splice_handshake(&nodes[0], &nodes[1]);
7106+
7107+
// Fire a tick while quiescent to arm the disconnect timer.
7108+
nodes[0].node.timer_tick_occurred();
7109+
nodes[1].node.timer_tick_occurred();
7110+
7111+
// Complete the splice negotiation, which should clear the timer when exiting quiescence.
7112+
complete_interactive_funding_negotiation(
7113+
&nodes[0],
7114+
&nodes[1],
7115+
channel_id,
7116+
funding_contribution,
7117+
new_funding_script,
7118+
);
7119+
let (_, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
7120+
assert!(splice_locked.is_none());
7121+
7122+
let node_id_0 = nodes[0].node.get_our_node_id();
7123+
let node_id_1 = nodes[1].node.get_our_node_id();
7124+
expect_splice_pending_event(&nodes[0], &node_id_1);
7125+
expect_splice_pending_event(&nodes[1], &node_id_0);
7126+
7127+
// Fire enough ticks to trigger a disconnect if the timer wasn't properly cleared.
7128+
for _ in 0..DISCONNECT_PEER_AWAITING_RESPONSE_TICKS {
7129+
nodes[0].node.timer_tick_occurred();
7130+
nodes[1].node.timer_tick_occurred();
7131+
}
7132+
7133+
let has_disconnect = |events: &[MessageSendEvent]| {
7134+
events.iter().any(|event| {
7135+
matches!(
7136+
event,
7137+
MessageSendEvent::HandleError {
7138+
action: msgs::ErrorAction::DisconnectPeerWithWarning { .. },
7139+
..
7140+
}
7141+
)
7142+
})
7143+
};
7144+
assert!(!has_disconnect(&nodes[0].node.get_and_clear_pending_msg_events()));
7145+
assert!(!has_disconnect(&nodes[1].node.get_and_clear_pending_msg_events()));
7146+
}
7147+
7148+
#[test]
7149+
fn test_no_disconnect_after_splice_aborted() {
7150+
// Test that the disconnect timer is cleared when exiting quiescence after a splice negotiation
7151+
// is aborted via tx_abort. Previously, `reset_pending_splice_state` cleared the quiescent
7152+
// state but not the disconnect timer, causing a spurious disconnect after the abort.
7153+
let chanmon_cfgs = create_chanmon_cfgs(2);
7154+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
7155+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
7156+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
7157+
7158+
let node_id_0 = nodes[0].node.get_our_node_id();
7159+
let node_id_1 = nodes[1].node.get_our_node_id();
7160+
7161+
let initial_channel_value_sat = 100_000;
7162+
let (_, _, channel_id, _) =
7163+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
7164+
7165+
let added_value = Amount::from_sat(50_000);
7166+
provide_utxo_reserves(&nodes, 2, added_value * 2);
7167+
7168+
let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
7169+
complete_splice_handshake(&nodes[0], &nodes[1]);
7170+
7171+
// Fire a tick while quiescent to arm the disconnect timer.
7172+
nodes[0].node.timer_tick_occurred();
7173+
nodes[1].node.timer_tick_occurred();
7174+
7175+
// Abort the splice, which should clear the timer when exiting quiescence.
7176+
nodes[0].node.abandon_splice(&channel_id, &node_id_1).unwrap();
7177+
7178+
expect_splice_failed_events(&nodes[0], &channel_id, funding_contribution);
7179+
7180+
let msg_events = nodes[0].node.get_and_clear_pending_msg_events();
7181+
let tx_abort = msg_events
7182+
.iter()
7183+
.find_map(|event| {
7184+
if let MessageSendEvent::SendTxAbort { msg, .. } = event {
7185+
Some(msg.clone())
7186+
} else {
7187+
None
7188+
}
7189+
})
7190+
.expect("Expected SendTxAbort");
7191+
7192+
nodes[1].node.handle_tx_abort(node_id_0, &tx_abort);
7193+
let tx_abort_echo = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0);
7194+
nodes[1].node.get_and_clear_pending_events();
7195+
7196+
nodes[0].node.handle_tx_abort(node_id_1, &tx_abort_echo);
7197+
7198+
// Fire enough ticks to trigger a disconnect if the timer wasn't properly cleared.
7199+
for _ in 0..DISCONNECT_PEER_AWAITING_RESPONSE_TICKS {
7200+
nodes[0].node.timer_tick_occurred();
7201+
nodes[1].node.timer_tick_occurred();
7202+
}
7203+
7204+
let has_disconnect = |events: &[MessageSendEvent]| {
7205+
events.iter().any(|event| {
7206+
matches!(
7207+
event,
7208+
MessageSendEvent::HandleError {
7209+
action: msgs::ErrorAction::DisconnectPeerWithWarning { .. },
7210+
..
7211+
}
7212+
)
7213+
})
7214+
};
7215+
assert!(!has_disconnect(&nodes[0].node.get_and_clear_pending_msg_events()));
7216+
assert!(!has_disconnect(&nodes[1].node.get_and_clear_pending_msg_events()));
7217+
}
7218+
7219+
#[test]
7220+
fn test_no_disconnect_after_quiescence_on_reconnect() {
7221+
// Test that there is no spurious disconnect after reconnecting from a quiescent state. The
7222+
// disconnect timer is cleared by `remove_uncommitted_htlcs_and_mark_paused` during
7223+
// disconnection and by `exit_quiescence` during reconnection.
7224+
let chanmon_cfgs = create_chanmon_cfgs(2);
7225+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
7226+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
7227+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
7228+
7229+
let node_id_0 = nodes[0].node.get_our_node_id();
7230+
let node_id_1 = nodes[1].node.get_our_node_id();
7231+
7232+
let initial_channel_value_sat = 100_000;
7233+
let (_, _, channel_id, _) =
7234+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
7235+
7236+
let added_value = Amount::from_sat(50_000);
7237+
provide_utxo_reserves(&nodes, 2, added_value * 2);
7238+
7239+
let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
7240+
complete_splice_handshake(&nodes[0], &nodes[1]);
7241+
7242+
// Fire a tick while quiescent to arm the disconnect timer.
7243+
nodes[0].node.timer_tick_occurred();
7244+
nodes[1].node.timer_tick_occurred();
7245+
7246+
// Disconnect and reconnect.
7247+
nodes[0].node.peer_disconnected(node_id_1);
7248+
nodes[1].node.peer_disconnected(node_id_0);
7249+
7250+
expect_splice_failed_events(&nodes[0], &channel_id, funding_contribution);
7251+
7252+
let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]);
7253+
reconnect_args.send_channel_ready = (true, true);
7254+
reconnect_args.send_announcement_sigs = (true, true);
7255+
reconnect_nodes(reconnect_args);
7256+
7257+
// Fire enough ticks to trigger a disconnect if the timer wasn't properly cleared.
7258+
for _ in 0..DISCONNECT_PEER_AWAITING_RESPONSE_TICKS {
7259+
nodes[0].node.timer_tick_occurred();
7260+
nodes[1].node.timer_tick_occurred();
7261+
}
7262+
7263+
let has_disconnect = |events: &[MessageSendEvent]| {
7264+
events.iter().any(|event| {
7265+
matches!(
7266+
event,
7267+
MessageSendEvent::HandleError {
7268+
action: msgs::ErrorAction::DisconnectPeerWithWarning { .. },
7269+
..
7270+
}
7271+
)
7272+
})
7273+
};
7274+
assert!(!has_disconnect(&nodes[0].node.get_and_clear_pending_msg_events()));
7275+
assert!(!has_disconnect(&nodes[1].node.get_and_clear_pending_msg_events()));
7276+
}

0 commit comments

Comments
 (0)