Remove awaiting quiescence channel state flag
What changed, and why it matters
This commit is a code cleanup in a Lightning network implementation. It removes a duplicate internal flag used to track whether a channel is trying to become quiet (quiescent) for operations like splicing. The same information is now tracked by checking whether a pending 'QuiescentAction' exists. The change frees up a bit in the channel state flags for future use and simplifies state management. It does not appear to fix a known security vulnerability, but any change to state-machine logic in payment channel code carries some risk of introducing subtle bugs.
Treat as a normal code review item. Verify that the new try_send_stfu early-return conditions exactly preserve prior behavior, especially around reconnection, shutdown races, and splice reattempts. Run the updated quiescence tests and consider additional edge-case tests for shutdown-during-quiescence and reconnection-during-quiescence.
Security signals we found
State-machine refactor in channel quiescence/splicing logic
Removal of duplicate state flag with behavior-preserving intent
Logic change in stfu initiation conditions
No vendor security disclosure or CVE referenced in commit
No external incident or vulnerability disclosure supplied
Evidence from the diff
The patch removes the AWAITING_QUIESCENCE bit from ChannelState flags in rust-lightning and replaces its usage with checks on FundedChannel::quiescent_action. It shifts remaining flags down one bit, updates try_send_stfu logic to return early when no QuiescentAction is pending and no remote stfu was received, and removes set/clear calls for the deleted flag. A test is added verifying that after a quiescence tie, the losing node reattempts quiescence. The commit is described by its author as a refactor to eliminate duplicate state, not as a security fix.
Changed components
lightning/src/ln/channel.rslightning/src/ln/quiescence_tests.rsChannelState flag definitionsQuiescence handshake / stfu sending logicSplice funding promotion and failure pathsInspect captured patch +14 / −61
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 35d5864..033e2ac 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -679,10 +679,9 @@ mod state_flags {
pub const LOCAL_SHUTDOWN_SENT: u32 = 1 << 11;
pub const SHUTDOWN_COMPLETE: u32 = 1 << 12;
pub const WAITING_FOR_BATCH: u32 = 1 << 13;
- pub const AWAITING_QUIESCENCE: u32 = 1 << 14;
- pub const LOCAL_STFU_SENT: u32 = 1 << 15;
- pub const REMOTE_STFU_SENT: u32 = 1 << 16;
- pub const QUIESCENT: u32 = 1 << 17;
+ pub const LOCAL_STFU_SENT: u32 = 1 << 14;
+ pub const REMOTE_STFU_SENT: u32 = 1 << 15;
+ pub const QUIESCENT: u32 = 1 << 16;
}
define_state_flags!(
@@ -749,13 +748,8 @@ define_state_flags!(
implicit ACK, so instead we have to hold them away temporarily to be sent later.",
AWAITING_REMOTE_REVOKE, state_flags::AWAITING_REMOTE_REVOKE,
is_awaiting_remote_revoke, set_awaiting_remote_revoke, clear_awaiting_remote_revoke),
- ("Indicates a local request has been made for the channel to become quiescent. Both nodes \
- must send `stfu` for the channel to become quiescent. This flag will be cleared and we \
- will no longer attempt quiescence if either node requests a shutdown.",
- AWAITING_QUIESCENCE, state_flags::AWAITING_QUIESCENCE,
- is_awaiting_quiescence, set_awaiting_quiescence, clear_awaiting_quiescence),
("Indicates we have sent a `stfu` message to the counterparty. This message can only be sent \
- if either `AWAITING_QUIESCENCE` or `REMOTE_STFU_SENT` is set. Shutdown requests are \
+ if `REMOTE_STFU_SENT` is set, or a `QuiescentAction` is pending. Shutdown requests are \
rejected if this flag is set.",
LOCAL_STFU_SENT, state_flags::LOCAL_STFU_SENT,
is_local_stfu_sent, set_local_stfu_sent, clear_local_stfu_sent),
@@ -950,12 +944,6 @@ impl ChannelState {
clear_awaiting_remote_revoke,
ChannelReady
);
- impl_state_flag!(
- is_awaiting_quiescence,
- set_awaiting_quiescence,
- clear_awaiting_quiescence,
- ChannelReady
- );
impl_state_flag!(is_local_stfu_sent, set_local_stfu_sent, clear_local_stfu_sent, ChannelReady);
impl_state_flag!(
is_remote_stfu_sent,
@@ -1750,10 +1738,6 @@ where
let splice_funding_failed = if let ChannelPhase::Funded(chan) = &mut self.phase {
// Reset any quiescence-related state as it is implicitly terminated once disconnected.
if matches!(chan.context.channel_state, ChannelState::ChannelReady(_)) {
- if chan.quiescent_action.is_some() {
- // If we were trying to get quiescent, try again after reconnection.
- chan.context.channel_state.set_awaiting_quiescence();
- }
chan.context.channel_state.clear_local_stfu_sent();
chan.context.channel_state.clear_remote_stfu_sent();
if chan.should_reset_pending_splice_state(false) {
@@ -7088,7 +7072,6 @@ where
} else {
match self.quiescent_action.take() {
Some(QuiescentAction::LegacySplice(instructions)) => {
- self.context.channel_state.clear_awaiting_quiescence();
let (inputs, outputs) = instructions.into_contributed_inputs_and_outputs();
Some(SpliceFundingFailed {
funding_txo: None,
@@ -7098,7 +7081,6 @@ where
})
},
Some(QuiescentAction::Splice { contribution, .. }) => {
- self.context.channel_state.clear_awaiting_quiescence();
let (inputs, outputs) = contribution.into_contributed_inputs_and_outputs();
Some(SpliceFundingFailed {
funding_txo: None,
@@ -10747,11 +10729,6 @@ where
// From here on out, we may not fail!
self.context.channel_state.set_remote_shutdown_sent();
- if self.context.channel_state.is_awaiting_quiescence() {
- // We haven't been able to send `stfu` yet, and there's no point in attempting
- // quiescence anymore since the counterparty wishes to close the channel.
- self.context.channel_state.clear_awaiting_quiescence();
- }
self.context.update_time_counter += 1;
let monitor_update = if update_shutdown_script {
@@ -11526,17 +11503,6 @@ where
let announcement_sigs =
self.get_announcement_sigs(node_signer, chain_hash, user_config, block_height, logger);
- if let Some(quiescent_action) = self.quiescent_action.as_ref() {
- // TODO(splicing): If we didn't win quiescence, then we can contribute as an acceptor
- // instead of waiting for the splice to lock.
- if matches!(
- quiescent_action,
- QuiescentAction::Splice { .. } | QuiescentAction::LegacySplice(_)
- ) {
- self.context.channel_state.set_awaiting_quiescence();
- }
- }
-
Some(SpliceFundingPromotion {
funding_txo,
monitor_update,
@@ -13314,9 +13280,6 @@ where
// From here on out, we may not fail!
self.context.target_closing_feerate_sats_per_kw = target_feerate_sats_per_kw;
self.context.channel_state.set_local_shutdown_sent();
- if self.context.channel_state.is_awaiting_quiescence() {
- self.context.channel_state.clear_awaiting_quiescence();
- }
self.context.local_initiated_shutdown = Some(());
self.context.update_time_counter += 1;
@@ -13412,7 +13375,6 @@ where
}
// Since we don't have a pending quiescent action, we should never be in a state where we
// sent `stfu` without already having become quiescent.
- debug_assert!(!self.context.channel_state.is_awaiting_quiescence());
debug_assert!(!self.context.channel_state.is_local_stfu_sent());
self.quiescent_action = Some(action);
@@ -13421,7 +13383,6 @@ where
return Ok(None);
}
- self.context.channel_state.set_awaiting_quiescence();
Ok(self.try_send_stfu(false, logger))
}
@@ -13570,13 +13531,11 @@ where
// We only need to send `stfu` when we're awaiting quiescence and haven't sent it yet, or
// in response to a counterparty one.
- if self.context.channel_state.is_local_stfu_sent()
- || self.context.channel_state.is_quiescent()
- {
+ if self.quiescent_action.is_none() && !self.context.channel_state.is_remote_stfu_sent() {
return None;
}
- if !self.context.channel_state.is_awaiting_quiescence()
- && !self.context.channel_state.is_remote_stfu_sent()
+ if self.context.channel_state.is_local_stfu_sent()
+ || self.context.channel_state.is_quiescent()
{
return None;
}
@@ -13615,18 +13574,13 @@ where
}
let initiator = if self.context.channel_state.is_remote_stfu_sent() {
- // We may have also attempted to initiate quiescence.
- self.context.channel_state.clear_awaiting_quiescence();
+ // Since we may have also attempted to initiate quiescence but the counterparty
+ // initiated first, we'll retry after we're no longer quiescent.
self.context.channel_state.clear_remote_stfu_sent();
self.context.channel_state.set_quiescent();
- // We are sending an stfu in response to our counterparty's stfu, but had not yet sent
- // our own stfu (even if `awaiting_quiescence` was set). Thus, the counterparty is the
- // initiator and they can do "something fundamental".
false
} else {
log_debug!(logger, "Sending stfu as quiescence initiator");
- debug_assert!(self.context.channel_state.is_awaiting_quiescence());
- self.context.channel_state.clear_awaiting_quiescence();
self.context.channel_state.set_local_stfu_sent();
true
};
@@ -13639,7 +13593,6 @@ where
pub fn exit_quiescence(&mut self) -> bool {
// Make sure we either finished the quiescence handshake and are quiescent, or we never
// attempted to initiate quiescence at all.
- debug_assert!(!self.context.channel_state.is_awaiting_quiescence());
debug_assert!(!self.context.channel_state.is_local_stfu_sent());
debug_assert!(!self.context.channel_state.is_remote_stfu_sent());
@@ -14744,11 +14697,6 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
match channel_state {
ChannelState::AwaitingChannelReady(_) => {},
ChannelState::ChannelReady(_) => {
- if self.quiescent_action.is_some() {
- // If we're trying to get quiescent to do something, try again when we
- // reconnect to the peer.
- channel_state.set_awaiting_quiescence();
- }
channel_state.clear_local_stfu_sent();
channel_state.clear_remote_stfu_sent();
if self.should_reset_pending_splice_state(false)
diff --git a/lightning/src/ln/quiescence_tests.rs b/lightning/src/ln/quiescence_tests.rs
index d972fb6..56dc4d4 100644
--- a/lightning/src/ln/quiescence_tests.rs
+++ b/lightning/src/ln/quiescence_tests.rs
@@ -35,6 +35,11 @@ fn test_quiescence_tie() {
assert!(nodes[0].node.exit_quiescence(&nodes[1].node.get_our_node_id(), &chan_id).unwrap());
assert!(nodes[1].node.exit_quiescence(&nodes[0].node.get_our_node_id(), &chan_id).unwrap());
+
+ // Since node 1 lost the tie, they'll attempt quiescence again.
+ let stfu =
+ get_event_msg!(nodes[1], MessageSendEvent::SendStfu, nodes[0].node.get_our_node_id());
+ assert!(stfu.initiator);
}
#[test]
Why this scored 27/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.