Drop stale splice signature on disconnect
What changed, and why it matters
This fix prevents a Lightning channel from being accidentally force-closed. During a splice (a way to resize a payment channel), one side's initial signature could be kept in memory after the peers disconnected. If the peers later reconnected and finished the splice, that old buffered signature could be processed again, triggering a safety shutdown (force close) of the channel. The patch now clears that buffered signature when the connection drops, just like it was already cleared on restart.
Apply the patch. Review other buffered in-memory protocol messages for similar disconnect-vs-restart cleanup gaps. Consider fuzzing the splice reconnect path further.
Security signals we found
State inconsistency: in-memory buffered message not cleared on disconnect
Duplicate message processing after reconnection
Force-close consequence for active Lightning channel
Fuzzer-discovered issue (chanmon_consistency fuzzer)
Splicing protocol edge case
Evidence from the diff
In rust-lightning, an initial splice commitment_signed from the counterparty can be buffered in FundingNegotiation::AwaitingSignatures::initial_commitment_signed_from_counterparty while waiting for local funding signatures. This buffered message was correctly dropped on node restart, but not on a simple peer disconnection. After reconnecting and completing the splice negotiation, the stale commitment_signed could be reprocessed, causing a duplicate-message error and a force close. The patch adds logic in the disconnect/reconnect path to take() (clear) the stale field. A regression test in splicing_tests.rs simulates buffering the message before disconnect and verifies no monitors are added prematurely.
Changed components
lightning/src/ln/channel.rslightning/src/ln/splicing_tests.rsSplicing funding negotiation state machinePeer disconnect/reconnect handlingInspect captured patch +22 / −1
### lightning/src/ln/channel.rs
@@ -1772,6 +1772,16 @@ where
chan.exit_quiescence();
None
} else {
+ if let Some(FundingNegotiation::AwaitingSignatures {
+ initial_commitment_signed_from_counterparty,
+ ..
+ }) = chan
+ .pending_splice
+ .as_mut()
+ .and_then(|pending_splice| pending_splice.funding_negotiation.as_mut())
+ {
+ initial_commitment_signed_from_counterparty.take();
+ }
None
}
} else {
### lightning/src/ln/splicing_tests.rs
@@ -2770,7 +2770,17 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) {
assert!(nodes[1].node.get_and_clear_pending_events().is_empty());
assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());
- let _ = get_htlc_update_msgs(&nodes[1], &node_id_0);
+ let acceptor_commitment_signed = get_htlc_update_msgs(&nodes[1], &node_id_0);
+ if !reload {
+ // Buffer the peer's initial `commitment_signed` before disconnecting. Since we haven't
+ // signed yet, it will be stashed until we do so, though it can be dropped if a disconnect
+ // happens.
+ nodes[0].node.handle_commitment_signed_batch_test(
+ node_id_1,
+ &acceptor_commitment_signed.commitment_signed,
+ );
+ check_added_monitors(&nodes[0], 0);
+ }
// Disconnect them, and handle the signing event on the initiator side.
if reload {
@@ -2830,6 +2840,7 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) {
let tx = nodes[0].wallet_source.sign_tx(unsigned_transaction).unwrap();
nodes[0].node.funding_transaction_signed(&channel_id, &node_id_1, tx).unwrap();
}
+ check_added_monitors(&nodes[0], 0);
// Since they're not connected, no messages should be sent.
assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());Why this scored 58/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.