Handle implicit splice_locked during channel_reestablish
What changed, and why it matters
This commit fixes a protocol-handling bug in LDK's Lightning splicing feature. When two peers reconnect after a splice, the Lightning spec says a 'splice_locked' message can be implied by the other peer's current funding state. LDK previously did not recognize this implied signal, which could leave a splice stuck or cause the channel to behave incorrectly after reconnection. The change detects the implicit splice_locked and processes it normally.
Review splicing reestablishment test coverage to ensure implicit splice_locked cases are exercised. If running a node with splicing enabled, update to include this fix to avoid splice stalls or incorrect channel state after reconnections.
Security signals we found
Protocol state machine fix for splicing reestablishment
Implicit message inference now handled per spec
Potential channel stall or desynchronization if splice_locked not inferred
Feature-gated behind splicing compile-time flag
Evidence from the diff
The patch updates channel reestablishment handling so that when a channel_reestablish message’s my_current_funding_locked matches a pending splice transaction for which splice_locked has not yet been received, LDK infers a SpliceLocked message and routes it through internal_splice_locked. This aligns with the BOLT spec requirement to process my_current_funding_locked as if splice_locked was received. The change is gated behind the #[cfg(splicing)] feature flag and adds an inferred_splice_locked field to ReestablishResponses.
Changed components
lightning/src/ln/channel.rslightning/src/ln/channelmanager.rsLDK channel reestablishment logicLDK splicing protocol implementationInspect captured patch +42 / −2
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 9de3bd3..2174980 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -1220,6 +1220,7 @@ pub(super) struct ReestablishResponses {
pub shutdown_msg: Option<msgs::Shutdown>,
pub tx_signatures: Option<msgs::TxSignatures>,
pub tx_abort: Option<msgs::TxAbort>,
+ pub inferred_splice_locked: Option<msgs::SpliceLocked>,
}
/// The first message we send to our peer after connection
@@ -9291,6 +9292,7 @@ where
shutdown_msg, announcement_sigs,
tx_signatures,
tx_abort: None,
+ inferred_splice_locked: None,
});
}
@@ -9303,6 +9305,7 @@ where
shutdown_msg, announcement_sigs,
tx_signatures,
tx_abort,
+ inferred_splice_locked: None,
});
}
@@ -9338,6 +9341,30 @@ where
self.get_channel_ready(logger)
} else { None };
+ // A receiving node:
+ // - if splice transactions are pending and `my_current_funding_locked` matches one of
+ // those splice transactions, for which it hasn't received `splice_locked` yet:
+ // - MUST process `my_current_funding_locked` as if it was receiving `splice_locked`
+ // for this `txid`.
+ #[cfg(splicing)]
+ let inferred_splice_locked = msg.my_current_funding_locked.as_ref().and_then(|funding_locked| {
+ self.pending_funding
+ .iter()
+ .find(|funding| funding.get_funding_txid() == Some(funding_locked.txid))
+ .and_then(|_| {
+ self.pending_splice.as_ref().and_then(|pending_splice| {
+ (Some(funding_locked.txid) != pending_splice.received_funding_txid)
+ .then(|| funding_locked.txid)
+ })
+ })
+ .map(|splice_txid| msgs::SpliceLocked {
+ channel_id: self.context.channel_id,
+ splice_txid,
+ })
+ });
+ #[cfg(not(splicing))]
+ let inferred_splice_locked = None;
+
if msg.next_local_commitment_number == next_counterparty_commitment_number {
if required_revoke.is_some() || self.context.signer_pending_revoke_and_ack {
log_debug!(logger, "Reconnected channel {} with only lost outbound RAA", &self.context.channel_id());
@@ -9355,6 +9382,7 @@ where
commitment_order: self.context.resend_order.clone(),
tx_signatures,
tx_abort,
+ inferred_splice_locked,
})
} else if msg.next_local_commitment_number == next_counterparty_commitment_number - 1 {
debug_assert!(commitment_update.is_none());
@@ -9379,6 +9407,7 @@ where
commitment_order: self.context.resend_order.clone(),
tx_signatures: None,
tx_abort,
+ inferred_splice_locked,
})
} else {
let commitment_update = if self.context.resend_order == RAACommitmentOrder::RevokeAndACKFirst
@@ -9405,6 +9434,7 @@ where
commitment_order: self.context.resend_order.clone(),
tx_signatures: None,
tx_abort,
+ inferred_splice_locked,
})
}
} else if msg.next_local_commitment_number < next_counterparty_commitment_number {
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index b74b04e..3a846c5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -11034,7 +11034,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
#[rustfmt::skip]
fn internal_channel_reestablish(&self, counterparty_node_id: &PublicKey, msg: &msgs::ChannelReestablish) -> Result<NotifyOption, MsgHandleErrInternal> {
- let need_lnd_workaround = {
+ let (inferred_splice_locked, need_lnd_workaround) = {
let per_peer_state = self.per_peer_state.read().unwrap();
let peer_state_mutex = per_peer_state.get(counterparty_node_id)
@@ -11086,7 +11086,8 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
if let Some(upd) = channel_update {
peer_state.pending_msg_events.push(upd);
}
- need_lnd_workaround
+
+ (responses.inferred_splice_locked, need_lnd_workaround)
} else {
return try_channel_entry!(self, peer_state, Err(ChannelError::close(
"Got a channel_reestablish message for an unfunded channel!".into())), chan_entry);
@@ -11132,6 +11133,15 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
if let Some(channel_ready_msg) = need_lnd_workaround {
self.internal_channel_ready(counterparty_node_id, &channel_ready_msg)?;
}
+
+ #[cfg(not(splicing))]
+ let _ = inferred_splice_locked;
+ #[cfg(splicing)]
+ if let Some(splice_locked) = inferred_splice_locked {
+ self.internal_splice_locked(counterparty_node_id, &splice_locked)?;
+ return Ok(NotifyOption::DoPersist);
+ }
+
Ok(NotifyOption::SkipPersistHandleEvents)
}
Why this scored 54/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.