Include HTLC signatures in initial commitment signed for splices
What changed, and why it matters
This fix corrects a Lightning channel protocol bug during 'splicing,' where a channel's funding transaction is replaced. The code was already computing signatures for pending in-flight payments (HTLCs) when creating the new initial commitment, but it was discarding those HTLC signatures and sending an empty list to the peer. That incomplete 'commitment_signed' message could cause the peer to reject or mis-handle the spliced channel state, potentially leading to channel force-closure or stuck payments. The patch returns and includes the HTLC signatures in the message.
Apply the patch. Nodes that splice channels with pending HTLCs should upgrade, otherwise they may emit invalid commitment_signed messages that peer implementations reject. Monitor for any related channel failures around splice activation.
Security signals we found
Protocol message missing required signatures for pending HTLCs
Splicing-specific initial commitment signature construction
Potential channel desynchronization or force-close between peers
Incomplete BOLT commitment_signed payload
Evidence from the diff
In rust-lightning’s channel.rs, get_initial_counterparty_commitment_signature previously called sign_counterparty_commitment and immediately dropped the returned Vec
Changed components
lightning/src/ln/channel.rsSplicing / channel funding replacementCommitmentSigned message constructionInspect captured patch +9 / −6
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 53ef1d0..1ca067f 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -6115,9 +6115,9 @@ where
}
}
- fn get_initial_counterparty_commitment_signature<L: Deref>(
+ fn get_initial_counterparty_commitment_signatures<L: Deref>(
&self, funding: &FundingScope, logger: &L,
- ) -> Option<Signature>
+ ) -> Option<(Signature, Vec<Signature>)>
where
SP::Target: SignerProvider,
L::Target: Logger,
@@ -6152,7 +6152,6 @@ where
Vec::new(),
&self.secp_ctx,
)
- .map(|(signature, _)| signature)
.ok()
},
// TODO (taproot|arik)
@@ -6170,16 +6169,20 @@ where
{
debug_assert!(self.interactive_tx_signing_session.is_some());
- let signature = self.get_initial_counterparty_commitment_signature(funding, logger);
- if let Some(signature) = signature {
+ let signatures = self.get_initial_counterparty_commitment_signatures(funding, logger);
+ if let Some((signature, htlc_signatures)) = signatures {
log_info!(
logger,
"Generated commitment_signed for peer for channel {}",
&self.channel_id()
);
+ if matches!(self.channel_state, ChannelState::FundingNegotiated(_)) {
+ // We shouldn't expect any HTLCs before `ChannelReady`.
+ debug_assert!(htlc_signatures.is_empty());
+ }
Some(msgs::CommitmentSigned {
channel_id: self.channel_id,
- htlc_signatures: vec![],
+ htlc_signatures,
signature,
funding_txid: funding.get_funding_txo().map(|funding_txo| funding_txo.txid),
#[cfg(taproot)]
Why this scored 56/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.