Merge PR 'Checkpoint deferred ChannelManager with AwaitingSignatures splice' (#4911)
What changed, and why it matters
This commit adds a workaround in a fuzz test harness for a known limitation in how Lightning Dev Kit saves its state during a splice operation. When a node is in a 'deferred persistence' test mode and has a splice waiting for signatures, the test now forces a state checkpoint before restarting. Without this, a fuzz test could simulate a restart at a moment where the signing session has not yet been saved, causing the node to disagree with its peer and force-close the channel. The change is in test/fuzz code, not production logic, but it documents a real edge case in production state management.
Treat this as a low-severity test-hardening commit that flags a real production edge case. The core team should resolve the FIXME by ensuring the signing session is checkpointed before commitment_signed is sent, so the fuzz harness workaround can eventually be removed. No immediate emergency patch is warranted because the change is in fuzz code and does not expose a directly exploitable network vulnerability.
Security signals we found
State-consistency edge case during splice with deferred persistence
Potential force-close after restart due to missing signing-session checkpoint
FIXME comment indicating an unresolved production limitation
Change is in fuzz test harness, not core library code
Evidence from the diff
The patch modifies fuzz/src/chanmon_consistency.rs. It imports SpliceCandidateStatus and adds a helper has_awaiting_signature_splice() that scans a node’s channels for any splice candidate in the AwaitingSignatures state. In restart_node(), when a node is ‘deferred’ (persisting state lazily) and has such a splice, it now calls checkpoint_manager_persistence() before proceeding. A FIXME comment explains that signing sessions are not checkpointed before commitment_signed is sent, so a reload could use an outdated ChannelManager, miss the signing session, and then abort/force-close when the counterparty resumes. The change only affects the fuzz test harness; it does not alter the splice or persistence logic in the library itself.
Changed components
fuzz/src/chanmon_consistency.rsFuzz test HarnessNode::restart_nodeChannelManager persistence/checkpoint behavior during splicingInspect captured patch +21 / −1
### fuzz/src/chanmon_consistency.rs
@@ -51,7 +51,9 @@ use lightning::events::{self, EventsProvider};
use lightning::ln::channel::{
FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS,
};
-use lightning::ln::channel_state::{ChannelDetails, InboundHTLCStateDetails, OutboundHTLCSource};
+use lightning::ln::channel_state::{
+ ChannelDetails, InboundHTLCStateDetails, OutboundHTLCSource, SpliceCandidateStatus,
+};
use lightning::ln::channelmanager::{
ChainParameters, ChannelManager, ChannelManagerReadArgs, PaymentId, RecentPaymentDetails,
TrustedChannelFeatures,
@@ -1251,6 +1253,16 @@ impl<'a> HarnessNode<'a> {
self.node.current_best_block().height
}
+ fn has_awaiting_signature_splice(&self) -> bool {
+ self.list_channels().iter().any(|channel| {
+ channel.splice_details.as_ref().map_or(false, |splice_details| {
+ splice_details.candidates.iter().any(|candidate| {
+ matches!(candidate.status, SpliceCandidateStatus::AwaitingSignatures { .. })
+ })
+ })
+ })
+ }
+
// Connects a block range to the ChannelManager, and to the ChainMonitor when
// sync_monitors is set. Reload syncs monitors separately because they can be
// at different heights than the manager, so it leaves them out here.
@@ -3861,6 +3873,14 @@ impl<'a, Out: Output + MaybeSend + MaybeSync> Harness<'a, Out> {
}
fn restart_node(&mut self, node_idx: usize, v: u8, router: &'a FuzzRouter) {
+ if self.nodes[node_idx].deferred && self.nodes[node_idx].has_awaiting_signature_splice() {
+ // FIXME: We don't currently checkpoint our signing session prior to sending
+ // `commitment_signed`, so we may run into a case where we commit to a splice
+ // candidate and reload with an outdated manager prior to the signing session
+ // existing. If the counterparty commits to the splice back, and is expecting to
+ // resume, we'll end up aborting erroneously leading to a force close.
+ self.nodes[node_idx].checkpoint_manager_persistence();
+ }
if !self.nodes[node_idx].deferred {
self.nodes[node_idx].checkpoint_manager_persistence();
}Why this scored 29/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.