Update next_funding_txid logic for channel_reestablish
What changed, and why it matters
This commit updates how the Lightning Dev Kit (LDK) node handles reconnection messages for channels that are in the middle of creating a new funding transaction (a 'splicing' or interactive funding flow). It replaces a simple transaction ID field with a richer structure that also carries flags telling the peer which messages to re-send. The change is a protocol-conformance update for a still-evolving Lightning specification; it does not appear to be a fix for an active security vulnerability, but it prevents possible state mismatches during reconnections.
Treat as a normal protocol-conformance update. Reviewers should verify that the new `NextFunding` serialization/deserialization is backward-compatible or gated appropriately, that the `retransmit_flags` bitfield cannot be abused to force unwanted retransmissions, and that the moved `AwaitingChannelReady` branch does not bypass any existing commitment-number checks. No emergency action is indicated by the supplied materials.
Security signals we found
Protocol state machine change for channel reestablishment during interactive (splicing) funding
Replacement of txid-only field with txid + retransmit flag structure
New `NextFunding` message type and `NextFundingFlag` enum added
Reordering of reestablish response handling to cover `AwaitingChannelReady` state
More consistent propagation of `tx_abort` instead of silently dropping mismatched funding state
No explicit CVE, security advisory, or bug report referenced in commit or materials
Evidence from the diff
The patch changes ChannelReestablish::next_funding_txid: Option<Txid> to next_funding: Option<NextFunding>, where NextFunding contains a txid plus a retransmit_flags bitfield (currently only CommitmentSigned). The receiving logic in FundedChannel::channel_reestablish is rewritten to use the flag instead of comparing next_local_commitment_number to decide whether to retransmit commitment_signed. It also moves the interactive-funding handling earlier so it applies even when the channel is still AwaitingChannelReady, and it propagates tx_abort in more reconnection branches. The commit is framed as following updated splicing spec requirements and explicitly notes the spec is ‘in flux’.
Changed components
lightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/ln/msgs.rsInspect captured patch +195 / −116
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 0d54e4b..3015bd4 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -9121,9 +9121,9 @@ where
}
if msg.next_local_commitment_number >= INITIAL_COMMITMENT_NUMBER || msg.next_remote_commitment_number >= INITIAL_COMMITMENT_NUMBER ||
- (msg.next_local_commitment_number == 0 && msg.next_funding_txid.is_none()) {
+ (msg.next_local_commitment_number == 0 && msg.next_funding.is_none()) {
// Note: This also covers the following case in the V2 channel establishment specification:
- // if `next_funding_txid` is not set, and `next_commitment_number` is zero:
+ // if `next_funding` is not set, and `next_commitment_number` is zero:
// MUST immediately fail the channel and broadcast any relevant latest commitment transaction.
return Err(ChannelError::close("Peer sent an invalid channel_reestablish to force close in a non-standard way".to_owned()));
}
@@ -9174,6 +9174,107 @@ where
let announcement_sigs = self.get_announcement_sigs(node_signer, chain_hash, user_config, best_block.height, logger);
+ let mut commitment_update = None;
+ let mut tx_signatures = None;
+ let mut tx_abort = None;
+
+ // if next_funding is set:
+ if let Some(next_funding) = &msg.next_funding {
+ // - if `next_funding` matches the latest interactive funding transaction
+ // or the current channel funding transaction:
+ if let Some(session) = &self.interactive_tx_signing_session {
+ let our_next_funding_txid = session.unsigned_tx().compute_txid();
+ if our_next_funding_txid != next_funding.txid {
+ return Err(ChannelError::close(format!(
+ "Unexpected next_funding txid: {}; expected: {}",
+ next_funding.txid, our_next_funding_txid,
+ )));
+ }
+
+ if !session.has_received_commitment_signed() {
+ self.context.expecting_peer_commitment_signed = true;
+ }
+
+ // TODO(splicing): Add comment for spec requirements
+ if next_funding.should_retransmit(msgs::NextFundingFlag::CommitmentSigned) {
+ #[cfg(splicing)]
+ let funding = self
+ .pending_splice
+ .as_ref()
+ .and_then(|pending_splice| pending_splice.funding_negotiation.as_ref())
+ .and_then(|funding_negotiation| {
+ if let FundingNegotiation::AwaitingSignatures(funding) = &funding_negotiation {
+ Some(funding)
+ } else {
+ None
+ }
+ })
+ .or_else(|| Some(&self.funding))
+ .filter(|funding| funding.get_funding_txid() == Some(next_funding.txid))
+ .ok_or_else(|| {
+ let message = "Failed to find funding for new commitment_signed".to_owned();
+ ChannelError::Close(
+ (
+ message.clone(),
+ ClosureReason::HolderForceClosed { message, broadcasted_latest_txn: Some(false) },
+ )
+ )
+ })?;
+ #[cfg(not(splicing))]
+ let funding = &self.funding;
+
+ let commitment_signed = self.context.get_initial_commitment_signed_v2(&funding, logger)
+ // TODO(splicing): Support async signing
+ .ok_or_else(|| {
+ let message = "Failed to get signatures for new commitment_signed".to_owned();
+ ChannelError::Close(
+ (
+ message.clone(),
+ ClosureReason::HolderForceClosed { message, broadcasted_latest_txn: Some(false) },
+ )
+ )
+ })?;
+
+ commitment_update = Some(msgs::CommitmentUpdate {
+ commitment_signed: vec![commitment_signed],
+ update_add_htlcs: vec![],
+ update_fulfill_htlcs: vec![],
+ update_fail_htlcs: vec![],
+ update_fail_malformed_htlcs: vec![],
+ update_fee: None,
+ });
+ }
+
+ // - if it has already received `commitment_signed` and it should sign first
+ // - MUST send its `tx_signatures` for that funding transaction.
+ //
+ // - if it has already received `tx_signatures` for that funding transaction:
+ // - MUST send its `tx_signatures` for that funding transaction.
+ if (session.has_received_commitment_signed() && session.holder_sends_tx_signatures_first())
+ || self.context.channel_state.is_their_tx_signatures_sent()
+ {
+ // If `holder_tx_signatures` is `None` here, the `tx_signatures` message will be sent
+ // when the holder provides their witnesses as this will queue a `tx_signatures` if the
+ // holder must send one.
+ if session.holder_tx_signatures().is_none() {
+ log_debug!(logger, "Waiting for funding transaction signatures to be provided");
+ } else if self.context.channel_state.is_monitor_update_in_progress() {
+ log_debug!(logger, "Waiting for monitor update before providing funding transaction signatures");
+ } else {
+ tx_signatures = session.holder_tx_signatures().clone();
+ }
+ }
+ } else {
+ // We'll just send a `tx_abort` here if we don't have a signing session for this channel
+ // on reestablish and tell our peer to just forget about it.
+ // Our peer is doing something strange, but it doesn't warrant closing the channel.
+ tx_abort = Some(msgs::TxAbort {
+ channel_id: self.context.channel_id(),
+ data:
+ "No active signing session. The associated funding transaction may have already been broadcast.".as_bytes().to_vec() });
+ }
+ }
+
if matches!(self.context.channel_state, ChannelState::AwaitingChannelReady(_)) {
// If we're waiting on a monitor update, we shouldn't re-send any channel_ready's.
if !self.context.channel_state.is_our_channel_ready() ||
@@ -9181,14 +9282,14 @@ where
if msg.next_remote_commitment_number != 0 {
return Err(ChannelError::close("Peer claimed they saw a revoke_and_ack but we haven't sent channel_ready yet".to_owned()));
}
- // Short circuit the whole handler as there is nothing we can resend them
+
return Ok(ReestablishResponses {
channel_ready: None,
- channel_ready_order: ChannelReadyOrder::ChannelReadyFirst,
- raa: None, commitment_update: None,
- commitment_order: RAACommitmentOrder::CommitmentFirst,
+ channel_ready_order: ChannelReadyOrder::SignaturesFirst,
+ raa: None, commitment_update,
+ commitment_order: self.context.resend_order.clone(),
shutdown_msg, announcement_sigs,
- tx_signatures: None,
+ tx_signatures,
tx_abort: None,
});
}
@@ -9196,12 +9297,12 @@ where
// We have OurChannelReady set!
return Ok(ReestablishResponses {
channel_ready: self.get_channel_ready(logger),
- channel_ready_order: ChannelReadyOrder::ChannelReadyFirst,
- raa: None, commitment_update: None,
- commitment_order: RAACommitmentOrder::CommitmentFirst,
+ channel_ready_order: ChannelReadyOrder::SignaturesFirst,
+ raa: None, commitment_update,
+ commitment_order: self.context.resend_order.clone(),
shutdown_msg, announcement_sigs,
- tx_signatures: None,
- tx_abort: None,
+ tx_signatures,
+ tx_abort,
});
}
@@ -9244,88 +9345,6 @@ where
log_debug!(logger, "Reconnected channel {} with no loss", &self.context.channel_id());
}
- // if next_funding_txid is set:
- let (commitment_update, tx_signatures, tx_abort) = if let Some(next_funding_txid) = msg.next_funding_txid {
- if let Some(session) = &self.interactive_tx_signing_session {
- // if next_funding_txid matches the latest interactive funding transaction:
- let our_next_funding_txid = session.unsigned_tx().compute_txid();
- if our_next_funding_txid == next_funding_txid {
- debug_assert_eq!(session.unsigned_tx().compute_txid(), self.maybe_get_next_funding_txid().unwrap());
-
- let commitment_update = if !self.context.channel_state.is_their_tx_signatures_sent() && msg.next_local_commitment_number == 0 {
- // if it has not received tx_signatures for that funding transaction AND
- // if next_commitment_number is zero:
- // MUST retransmit its commitment_signed for that funding transaction.
- let commitment_signed = self.context.get_initial_commitment_signed_v2(&self.funding, logger)
- // TODO(splicing): Support async signing
- .ok_or_else(|| {
- let message = "Failed to get signatures for new commitment_signed".to_owned();
- ChannelError::Close(
- (
- message.clone(),
- ClosureReason::HolderForceClosed { message, broadcasted_latest_txn: Some(false) },
- )
- )})?;
- Some(msgs::CommitmentUpdate {
- commitment_signed: vec![commitment_signed],
- update_add_htlcs: vec![],
- update_fulfill_htlcs: vec![],
- update_fail_htlcs: vec![],
- update_fail_malformed_htlcs: vec![],
- update_fee: None,
- })
- } else { None };
- let tx_signatures = if (
- // if it has not received tx_signatures for that funding transaction AND
- // if it has already received commitment_signed AND it should sign first, as specified in the tx_signatures requirements:
- // MUST send its tx_signatures for that funding transaction.
- !self.context.channel_state.is_their_tx_signatures_sent() && session.has_received_commitment_signed() && session.holder_sends_tx_signatures_first()
- // else if it has already received tx_signatures for that funding transaction:
- // MUST send its tx_signatures for that funding transaction.
- ) || self.context.channel_state.is_their_tx_signatures_sent() {
- // If `holder_tx_signatures` is `None` here, the `tx_signatures` message will be sent
- // when the holder provides their witnesses as this will queue a `tx_signatures` if the
- // holder must send one.
- if session.holder_tx_signatures().is_none() {
- log_debug!(logger, "Waiting for funding transaction signatures to be provided");
- None
- } else if self.context.channel_state.is_monitor_update_in_progress() {
- log_debug!(logger, "Waiting for monitor update before providing funding transaction signatures");
- None
- } else {
- session.holder_tx_signatures().clone()
- }
- } else {
- None
- };
- if !session.has_received_commitment_signed() {
- self.context.expecting_peer_commitment_signed = true;
- }
- (commitment_update, tx_signatures, None)
- } else {
- // The `next_funding_txid` does not match the latest interactive funding transaction so we
- // MUST send tx_abort to let the remote know that they can forget this funding transaction.
- (None, None, Some(msgs::TxAbort {
- channel_id: self.context.channel_id(),
- data: format!(
- "next_funding_txid {} does match our latest interactive funding txid {}",
- next_funding_txid, our_next_funding_txid,
- ).into_bytes() }))
- }
- } else {
- // We'll just send a `tx_abort` here if we don't have a signing session for this channel
- // on reestablish and tell our peer to just forget about it.
- // Our peer is doing something strange, but it doesn't warrant closing the channel.
- (None, None, Some(msgs::TxAbort {
- channel_id: self.context.channel_id(),
- data:
- "No active signing session. The associated funding transaction may have already been broadcast.".as_bytes().to_vec() }))
- }
- } else {
- // Don't send anything related to interactive signing if `next_funding_txid` is not set.
- (None, None, None)
- };
-
Ok(ReestablishResponses {
channel_ready,
channel_ready_order: ChannelReadyOrder::SignaturesFirst,
@@ -9338,6 +9357,12 @@ where
tx_abort,
})
} else if msg.next_local_commitment_number == next_counterparty_commitment_number - 1 {
+ debug_assert!(commitment_update.is_none());
+
+ // TODO(splicing): Assert in a test that we don't retransmit tx_signatures instead
+ #[cfg(test)]
+ assert!(tx_signatures.is_none());
+
if required_revoke.is_some() || self.context.signer_pending_revoke_and_ack {
log_debug!(logger, "Reconnected channel {} with lost outbound RAA and lost remote commitment tx", &self.context.channel_id());
} else {
@@ -9353,7 +9378,7 @@ where
commitment_update: None, raa: None,
commitment_order: self.context.resend_order.clone(),
tx_signatures: None,
- tx_abort: None,
+ tx_abort,
})
} else {
let commitment_update = if self.context.resend_order == RAACommitmentOrder::RevokeAndACKFirst
@@ -9379,7 +9404,7 @@ where
raa, commitment_update,
commitment_order: self.context.resend_order.clone(),
tx_signatures: None,
- tx_abort: None,
+ tx_abort,
})
}
} else if msg.next_local_commitment_number < next_counterparty_commitment_number {
@@ -10971,15 +10996,29 @@ where
}
#[rustfmt::skip]
- fn maybe_get_next_funding_txid(&self) -> Option<Txid> {
+ fn maybe_get_next_funding(&self) -> Option<msgs::NextFunding> {
// If we've sent `commtiment_signed` for an interactively constructed transaction
- // during a signing session, but have not received `tx_signatures` we MUST set `next_funding_txid`
+ // during a signing session, but have not received `tx_signatures` we MUST set `next_funding`
// to the txid of that interactive transaction, else we MUST NOT set it.
if self.context.channel_state.is_interactive_signing() {
// Since we have a signing_session, this implies we've sent an initial `commitment_signed`...
if !self.context.channel_state.is_their_tx_signatures_sent() {
// ...but we didn't receive a `tx_signatures` from the counterparty yet.
- self.interactive_tx_signing_session.as_ref().map(|signing_session| signing_session.unsigned_tx().compute_txid())
+ self.interactive_tx_signing_session
+ .as_ref()
+ .map(|signing_session| {
+ let mut next_funding = msgs::NextFunding {
+ txid: signing_session.unsigned_tx().compute_txid(),
+ retransmit_flags: 0,
+ };
+
+ // TODO(splicing): Add comment for spec requirements
+ if !signing_session.has_received_commitment_signed() {
+ next_funding.retransmit(msgs::NextFundingFlag::CommitmentSigned);
+ }
+
+ next_funding
+ })
} else {
// ...and we received a `tx_signatures` from the counterparty.
None
@@ -11056,7 +11095,7 @@ where
next_remote_commitment_number: INITIAL_COMMITMENT_NUMBER - self.context.counterparty_next_commitment_transaction_number - 1,
your_last_per_commitment_secret: remote_last_secret,
my_current_per_commitment_point: dummy_pubkey,
- next_funding_txid: self.maybe_get_next_funding_txid(),
+ next_funding: self.maybe_get_next_funding(),
my_current_funding_locked: self.maybe_get_my_current_funding_locked(),
}
}
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index a450dcc..b74b04e 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -11117,7 +11117,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
next_remote_commitment_number: 0,
your_last_per_commitment_secret: [1u8; 32],
my_current_per_commitment_point: PublicKey::from_slice(&[2u8; 33]).unwrap(),
- next_funding_txid: None,
+ next_funding: None,
my_current_funding_locked: None,
},
});
diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs
index 93107de..b3a8c44 100644
--- a/lightning/src/ln/msgs.rs
+++ b/lightning/src/ln/msgs.rs
@@ -919,13 +919,13 @@ pub struct ChannelReestablish {
/// removed it from its state.
///
/// If we've sent `commtiment_signed` for an interactively constructed transaction
- /// during a signing session, but have not received `tx_signatures` we MUST set `next_funding_txid`
+ /// during a signing session, but have not received `tx_signatures` we MUST set `next_funding`
/// to the txid of that interactive transaction, else we MUST NOT set it.
///
/// See the spec for further details on this:
/// * `channel_reestablish`-sending node: https:///github.com/lightning/bolts/blob/247e83d/02-peer-protocol.md?plain=1#L2466-L2470
/// * `channel_reestablish`-receiving node: https:///github.com/lightning/bolts/blob/247e83d/02-peer-protocol.md?plain=1#L2520-L2531
- pub next_funding_txid: Option<Txid>,
+ pub next_funding: Option<NextFunding>,
/// The last funding txid sent by the sending node, which may be:
/// - the txid of the last `splice_locked` it sent, otherwise
/// - the txid of the funding transaction if it sent `channel_ready`, or else
@@ -935,6 +935,38 @@ pub struct ChannelReestablish {
pub my_current_funding_locked: Option<FundingLocked>,
}
+/// Information exchanged during channel reestablishment about the next funding from interactive
+/// transaction construction.
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub struct NextFunding {
+ /// The txid of the interactive transaction construction.
+ pub txid: Txid,
+
+ /// A bitfield indicating which messages should be retransmitted by the receiving node.
+ ///
+ /// See [`NextFundingFlag`] for details.
+ pub retransmit_flags: u8,
+}
+
+impl NextFunding {
+ /// Sets the bit in `retransmit_flags` for retransmitting the message corresponding to `flag`.
+ pub fn retransmit(&mut self, flag: NextFundingFlag) {
+ self.retransmit_flags |= 1 << flag as u8;
+ }
+
+ /// Returns whether the message corresponding to `flag` should be retransmitted.
+ pub fn should_retransmit(&self, flag: NextFundingFlag) -> bool {
+ self.retransmit_flags & (1 << flag as u8) != 0
+ }
+}
+
+/// Bit positions used in [`NextFunding::retransmit_flags`] for requesting message retransmission.
+#[repr(u8)]
+pub enum NextFundingFlag {
+ /// Retransmit `commitment_signed`.
+ CommitmentSigned = 0,
+}
+
/// Information exchanged during channel reestablishment about the last funding locked.
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct FundingLocked {
@@ -2873,10 +2905,15 @@ impl_writeable_msg!(ChannelReestablish, {
your_last_per_commitment_secret,
my_current_per_commitment_point,
}, {
- (0, next_funding_txid, option),
+ (1, next_funding, option),
(5, my_current_funding_locked, option),
});
+impl_writeable!(NextFunding, {
+ txid,
+ retransmit_flags
+});
+
impl_writeable!(FundingLocked, {
txid,
retransmit_flags
@@ -4348,7 +4385,7 @@ mod tests {
next_remote_commitment_number: 4,
your_last_per_commitment_secret: [9; 32],
my_current_per_commitment_point: public_key,
- next_funding_txid: None,
+ next_funding: None,
my_current_funding_locked: None,
};
@@ -4394,13 +4431,16 @@ mod tests {
next_remote_commitment_number: 4,
your_last_per_commitment_secret: [9; 32],
my_current_per_commitment_point: public_key,
- next_funding_txid: Some(Txid::from_raw_hash(
- bitcoin::hashes::Hash::from_slice(&[
- 48, 167, 250, 69, 152, 48, 103, 172, 164, 99, 59, 19, 23, 11, 92, 84, 15, 80,
- 4, 12, 98, 82, 75, 31, 201, 11, 91, 23, 98, 23, 53, 124,
- ])
- .unwrap(),
- )),
+ next_funding: Some(msgs::NextFunding {
+ txid: Txid::from_raw_hash(
+ bitcoin::hashes::Hash::from_slice(&[
+ 48, 167, 250, 69, 152, 48, 103, 172, 164, 99, 59, 19, 23, 11, 92, 84, 15,
+ 80, 4, 12, 98, 82, 75, 31, 201, 11, 91, 23, 98, 23, 53, 124,
+ ])
+ .unwrap(),
+ ),
+ retransmit_flags: 1,
+ }),
my_current_funding_locked: None,
};
@@ -4417,10 +4457,10 @@ mod tests {
3, 27, 132, 197, 86, 123, 18, 100, 64, 153, 93, 62, 213, 170, 186, 5, 101, 215, 30,
24, 52, 96, 72, 25, 255, 156, 23, 245, 233, 213, 221, 7,
143, // my_current_per_commitment_point
- 0, // Type (next_funding_txid)
- 32, // Length
+ 1, // Type (next_funding)
+ 33, // Length
48, 167, 250, 69, 152, 48, 103, 172, 164, 99, 59, 19, 23, 11, 92, 84, 15, 80, 4,
- 12, 98, 82, 75, 31, 201, 11, 91, 23, 98, 23, 53, 124, // Value
+ 12, 98, 82, 75, 31, 201, 11, 91, 23, 98, 23, 53, 124, 1, // Value
]
);
}
@@ -4450,7 +4490,7 @@ mod tests {
next_remote_commitment_number: 4,
your_last_per_commitment_secret: [9; 32],
my_current_per_commitment_point: public_key,
- next_funding_txid: None,
+ next_funding: None,
my_current_funding_locked: Some(msgs::FundingLocked {
txid: Txid::from_raw_hash(
bitcoin::hashes::Hash::from_slice(&[
Why this scored 43/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.