Avoid sending stfu for quiescent splice action while pending splice
What changed, and why it matters
This change fixes a minor protocol behavior bug in the Lightning Dev Kit's splicing feature. Previously, the code would try to send a 'stfu' (stop, let's become quiescent) message to start a new splice even when another splice was already pending. Since starting a second splice isn't allowed while one is pending, sending the message was pointless and could cause confusion or unnecessary protocol churn. The patch now skips sending 'stfu' in that situation and adds tests covering reconnection scenarios.
Review as a normal correctness/protocol-compliance fix. No immediate security response appears necessary, but downstream users relying on splicing should ensure they pick up this behavior change to avoid edge-case protocol confusion or failed concurrent splice handling.
Security signals we found
Protocol state machine hardening for splicing/quiescence
Prevention of premature `stfu` initiation during pending splice
Test coverage added for reconnection path
Evidence from the diff
In rust-lightning’s channel state machine, the maybe_send_stfu function now checks whether a queued QuiescentAction::Splice or QuiescentAction::LegacySplice exists while pending_splice is already set. If so, it returns Ok(None) and defers sending stfu until the pending splice completes (after splice_locked exchange). The test test_fails_initiating_concurrent_splices is refactored to run with and without a peer reconnect in the middle, and assertions are added to verify no message events are pending during the pending-splice window.
Changed components
lightning/src/ln/channel.rslightning/src/ln/splicing_tests.rsInspect captured patch +29 / −3
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 27ccd1c..5faa784 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -13619,6 +13619,17 @@ where
return Ok(None);
}
+ if let Some(action) = self.quiescent_action.as_ref() {
+ // We can't initiate another splice while ours is pending, so don't bother becoming
+ // quiescent yet.
+ // TODO(splicing): Allow the splice as an RBF once supported.
+ let has_splice_action = matches!(action, QuiescentAction::Splice { .. })
+ || matches!(action, QuiescentAction::LegacySplice(_));
+ if has_splice_action && self.pending_splice.is_some() {
+ return Ok(None);
+ }
+ }
+
// We need to send our `stfu`, either because we're trying to initiate quiescence, or the
// counterparty is and we've yet to send ours.
if self.context.channel_state.is_awaiting_quiescence()
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index 6727437..96b9b13 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -1031,6 +1031,12 @@ fn test_splice_in_and_out() {
#[test]
fn test_fails_initiating_concurrent_splices() {
+ fails_initiating_concurrent_splices(true);
+ fails_initiating_concurrent_splices(false);
+}
+
+#[cfg(test)]
+fn fails_initiating_concurrent_splices(reconnect: bool) {
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let config = test_default_channel_config();
@@ -1043,6 +1049,7 @@ fn test_fails_initiating_concurrent_splices() {
let node_0_id = nodes[0].node.get_our_node_id();
let node_1_id = nodes[1].node.get_our_node_id();
+ send_payment(&nodes[0], &[&nodes[1]], 1_000);
provide_utxo_reserves(&nodes, 2, Amount::ONE_BTC);
let outputs = vec![TxOut {
@@ -1116,15 +1123,23 @@ fn test_fails_initiating_concurrent_splices() {
expect_splice_pending_event(&nodes[0], &node_1_id);
expect_splice_pending_event(&nodes[1], &node_0_id);
- // Now that the splice is pending, another splice may be initiated.
+ // Now that the splice is pending, another splice may be initiated, but we must wait until
+ // the `splice_locked` exchange to send the initiator `stfu`.
assert!(nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate).is_ok());
+ if reconnect {
+ nodes[0].node.peer_disconnected(node_1_id);
+ nodes[1].node.peer_disconnected(node_0_id);
+ reconnect_nodes(ReconnectArgs::new(&nodes[0], &nodes[1]));
+ }
+
+ assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());
+ assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
+
mine_transaction(&nodes[0], &splice_tx);
mine_transaction(&nodes[1], &splice_tx);
let stfu = lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1);
- // However, the acceptor had enqueued a quiescent action while the splice was pending, so it
- // will now attempt to initiate quiescence.
assert!(
matches!(stfu, Some(MessageSendEvent::SendStfu { node_id, .. }) if node_id == node_0_id)
);
Why this scored 35/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.