Handle missing splice tx_signatures on reestablish
What changed, and why it matters
This commit fixes a bug in LDK's Lightning channel reconnection logic after a splice (a way to resize a channel's on-chain funds). If one peer had already received the splice signatures but the other had not, and then they disconnected and reconnected, the recovering peer could drop its own splice signatures while trying to resend a later commitment update. That left the counterparty still waiting for splice signatures and still treating the channel as 'frozen' (quiescent), so it would reject the normal commitment update. The fix ensures both the missing splice signatures and any later commitment update are retransmitted in the correct order after reconnect.
Review the ordering logic in handle_channel_resumption and the new TxSignaturesOrder enum for completeness. Run the new and updated splicing reconnection tests, especially under async monitor updates, and consider fuzzing the reestablish path further. Ensure downstream nodes upgrade to include this fix to avoid channel stalls or force-closes after splice reconnects.
Security signals we found
Protocol-state inconsistency on reconnection after splice signature exchange
Potential channel stall/force-close due to quiescence not being exited before commitment update
Fuzzer-discovered edge case in Lightning splicing retransmission
New explicit ordering enum (TxSignaturesOrder) to prevent future ordering bugs
Test added for retransmitting completed tx_signatures during unrelated async monitor update
Evidence from the diff
In channel.rs, the reestablish path for a lost remote commitment was discarding pending tx_signatures prepared from the peer’s next_funding TLV. The code assumed that if tx_signatures were owed, no pending commitment updates could exist. The fuzzer found a case where a normal commitment update can be generated after the tx_signatures exchange while the counterparty has not yet processed the responding tx_signatures. The patch introduces TxSignaturesOrder (SignaturesFirst vs CommitmentFirst), carries tx_signatures through the lost-remote-commitment branch, and makes message ordering explicit in handle_channel_resumption. Initial splice funding still sends commitment_signed before tx_signatures; post-splice reconnect recovery sends tx_signatures before normal commitment updates so the peer exits quiescence first. Tests were added/updated in splicing_tests.rs, async_signer_tests.rs, and functional_test_utils.rs.
Changed components
lightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/ln/splicing_tests.rslightning/src/ln/async_signer_tests.rslightning/src/ln/functional_test_utils.rsInspect captured patch +600 / −97
diff --git a/lightning/src/ln/async_signer_tests.rs b/lightning/src/ln/async_signer_tests.rs
index f60e63a..05508a4 100644
--- a/lightning/src/ln/async_signer_tests.rs
+++ b/lightning/src/ln/async_signer_tests.rs
@@ -607,7 +607,7 @@ fn test_signer_unblocked_clears_monitor_pending_raa_after_reestablish() {
// completes.
nodes[1].enable_channel_signer_op(&node_c_id, &chan_bc.2, SignerOp::ReleaseCommitmentSecret);
nodes[1].node.signer_unblocked(Some((node_c_id, chan_bc.2)));
- let (_, signer_revoke_and_ack, signer_commitment_update, _, _, _, _, _) =
+ let (_, signer_revoke_and_ack, signer_commitment_update, _, _, _, _, _, _) =
handle_chan_reestablish_msgs!(nodes[1], nodes[2]);
assert!(signer_revoke_and_ack.is_some());
@@ -617,12 +617,12 @@ fn test_signer_unblocked_clears_monitor_pending_raa_after_reestablish() {
let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_bc.2);
nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_bc.2, latest_update);
check_added_monitors(&nodes[1], 0);
- let (_, duplicate_revoke_and_ack, monitor_commitment_update, _, _, _, _, _) =
+ let (_, duplicate_revoke_and_ack, monitor_commitment_update, _, _, _, _, _, _) =
handle_chan_reestablish_msgs!(nodes[1], nodes[2]);
assert!(duplicate_revoke_and_ack.is_none());
nodes[2].node.handle_channel_reestablish(node_b_id, &bs_reestablish[0]);
- let (_, c_revoke_and_ack, c_commitment_update, _, _, _, _, _) =
+ let (_, c_revoke_and_ack, c_commitment_update, _, _, _, _, _, _) =
handle_chan_reestablish_msgs!(nodes[2], nodes[1]);
assert!(c_revoke_and_ack.is_none());
assert!(c_commitment_update.is_none());
@@ -801,7 +801,7 @@ fn do_test_async_raa_peer_disconnect(
}
// Expect the RAA
- let (_, revoke_and_ack, commitment_signed, resend_order, _, _, _, _) =
+ let (_, revoke_and_ack, commitment_signed, resend_order, _, _, _, _, _) =
handle_chan_reestablish_msgs!(dst, src);
if test_case == UnblockSignerAcrossDisconnectCase::AtEnd {
assert!(revoke_and_ack.is_none());
@@ -817,14 +817,14 @@ fn do_test_async_raa_peer_disconnect(
dst.node.signer_unblocked(Some((src_node_id, chan_id)));
if test_case == UnblockSignerAcrossDisconnectCase::AtEnd {
- let (_, revoke_and_ack, commitment_signed, resend_order, _, _, _, _) =
+ let (_, revoke_and_ack, commitment_signed, resend_order, _, _, _, _, _) =
handle_chan_reestablish_msgs!(dst, src);
assert!(revoke_and_ack.is_some());
assert!(commitment_signed.is_some());
assert!(resend_order == RAACommitmentOrder::RevokeAndACKFirst);
} else {
// Make sure we don't double send the RAA.
- let (_, revoke_and_ack, commitment_signed, _, _, _, _, _) =
+ let (_, revoke_and_ack, commitment_signed, _, _, _, _, _, _) =
handle_chan_reestablish_msgs!(dst, src);
assert!(revoke_and_ack.is_none());
assert!(commitment_signed.is_none());
@@ -951,7 +951,7 @@ fn do_test_async_commitment_signature_peer_disconnect(
}
// Expect the RAA
- let (_, revoke_and_ack, commitment_signed, _, _, _, _, _) =
+ let (_, revoke_and_ack, commitment_signed, _, _, _, _, _, _) =
handle_chan_reestablish_msgs!(dst, src);
assert!(revoke_and_ack.is_some());
if test_case == UnblockSignerAcrossDisconnectCase::AtEnd {
@@ -965,11 +965,11 @@ fn do_test_async_commitment_signature_peer_disconnect(
dst.node.signer_unblocked(Some((src_node_id, chan_id)));
if test_case == UnblockSignerAcrossDisconnectCase::AtEnd {
- let (_, _, commitment_signed, _, _, _, _, _) = handle_chan_reestablish_msgs!(dst, src);
+ let (_, _, commitment_signed, _, _, _, _, _, _) = handle_chan_reestablish_msgs!(dst, src);
assert!(commitment_signed.is_some());
} else {
// Make sure we don't double send the CS.
- let (_, _, commitment_signed, _, _, _, _, _) = handle_chan_reestablish_msgs!(dst, src);
+ let (_, _, commitment_signed, _, _, _, _, _, _) = handle_chan_reestablish_msgs!(dst, src);
assert!(commitment_signed.is_none());
}
}
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index a4e79df..d4b6cf3 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -54,8 +54,8 @@ use crate::ln::channel_state::{
use crate::ln::channelmanager::{
self, BlindedFailure, ChannelReadyOrder, FundingConfirmedMessage, HTLCFailureMsg,
HTLCPreviousHopData, HTLCSource, OpenChannelMessage, PaymentClaimDetails, PendingHTLCInfo,
- PendingHTLCStatus, RAACommitmentOrder, SentHTLCId, TrustedChannelFeatures, BREAKDOWN_TIMEOUT,
- MAX_LOCAL_BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA,
+ PendingHTLCStatus, RAACommitmentOrder, SentHTLCId, TrustedChannelFeatures, TxSignaturesOrder,
+ BREAKDOWN_TIMEOUT, MAX_LOCAL_BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA,
};
use crate::ln::funding::{FeeRateAdjustmentError, FundingContribution, FundingTemplate};
use crate::ln::interactivetxs::{
@@ -1267,7 +1267,7 @@ pub(super) struct ReestablishResponses {
pub commitment_order: RAACommitmentOrder,
pub announcement_sigs: Option<msgs::AnnouncementSignatures>,
pub shutdown_msg: Option<msgs::Shutdown>,
- pub tx_signatures: Option<msgs::TxSignatures>,
+ pub tx_signatures: Option<(TxSignaturesOrder, msgs::TxSignatures)>,
pub tx_abort: Option<msgs::TxAbort>,
pub splice_locked: Option<msgs::SpliceLocked>,
pub inferred_splice_locked: Option<msgs::SpliceLocked>,
@@ -10915,7 +10915,20 @@ where
// - if it has already received `tx_signatures` for that funding transaction:
// - MUST send its `tx_signatures` for that funding transaction.
if let Some(holder_tx_signatures) = session.holder_tx_signatures() {
- if self.is_awaiting_monitor_update() {
+ // A completed exchange may precede an unrelated monitor update, so
+ // retransmitting the same signatures does not depend on that update.
+ let splice_signatures_exchange_complete = self
+ .pending_splice
+ .as_ref()
+ .map(|pending_splice| {
+ pending_splice.negotiated_candidates.iter().any(|candidate| {
+ candidate.funding.get_funding_txid() == Some(next_funding.txid)
+ })
+ })
+ .unwrap_or(false);
+ if self.is_awaiting_monitor_update()
+ && !splice_signatures_exchange_complete
+ {
log_debug!(logger, "Waiting for monitor update before providing funding transaction signatures");
} else if self.context.signer_pending_funding {
log_debug!(logger, "Waiting for signer to provide counterparty commitment_signed before releasing funding transaction signatures");
@@ -10990,7 +11003,8 @@ where
raa: None, commitment_update,
commitment_order: self.context.resend_order.clone(),
shutdown_msg, announcement_sigs,
- tx_signatures,
+ tx_signatures: tx_signatures
+ .map(|msg| (TxSignaturesOrder::CommitmentFirst, msg)),
tx_abort: None,
splice_locked: None,
inferred_splice_locked: None,
@@ -11004,7 +11018,8 @@ where
raa: None, commitment_update,
commitment_order: self.context.resend_order.clone(),
shutdown_msg, announcement_sigs,
- tx_signatures,
+ tx_signatures: tx_signatures
+ .map(|msg| (TxSignaturesOrder::CommitmentFirst, msg)),
tx_abort,
splice_locked: None,
inferred_splice_locked: None,
@@ -11111,6 +11126,15 @@ where
log_debug!(logger, "Reconnected with no loss");
}
+ // A commitment update generated above retransmits the initial splice
+ // `commitment_signed` and must precede its funding signatures. Otherwise a completed
+ // exchange's retransmitted signatures must precede any `splice_locked` below.
+ let tx_signatures_order = if commitment_update.is_some() {
+ TxSignaturesOrder::CommitmentFirst
+ } else {
+ TxSignaturesOrder::SignaturesFirst
+ };
+
Ok(ReestablishResponses {
channel_ready,
channel_ready_order: ChannelReadyOrder::SignaturesFirst,
@@ -11119,17 +11143,17 @@ where
raa: required_revoke,
commitment_update,
commitment_order: self.context.resend_order.clone(),
- tx_signatures,
+ tx_signatures: tx_signatures.map(|msg| (tx_signatures_order, msg)),
tx_abort,
splice_locked,
inferred_splice_locked,
})
} 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 retransmit_funding_commit_sig.is_some() {
+ return Err(ChannelError::close(
+ "Peer requested retransmission of an initial commitment_signed while claiming to have lost a later commitment_signed".to_owned(),
+ ));
+ }
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");
@@ -11145,7 +11169,8 @@ where
shutdown_msg, announcement_sigs,
commitment_update: None, raa: None,
commitment_order: self.context.resend_order.clone(),
- tx_signatures: None,
+ tx_signatures: tx_signatures
+ .map(|msg| (TxSignaturesOrder::SignaturesFirst, msg)),
tx_abort,
splice_locked,
inferred_splice_locked,
@@ -11173,7 +11198,8 @@ where
shutdown_msg, announcement_sigs,
raa, commitment_update,
commitment_order: self.context.resend_order.clone(),
- tx_signatures: None,
+ tx_signatures: tx_signatures
+ .map(|msg| (TxSignaturesOrder::SignaturesFirst, msg)),
tx_abort,
splice_locked,
inferred_splice_locked,
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 318b10b..774f41d 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -1241,6 +1241,25 @@ pub(super) enum ChannelReadyOrder {
SignaturesFirst,
}
+/// Determines whether splice `tx_signatures` should be sent before or after other messages when
+/// resuming a channel.
+///
+/// The ordering matters because exchanging `tx_signatures` ends splice quiescence. A normal
+/// commitment update generated after quiescence cannot be processed by the peer until it has
+/// received our `tx_signatures`. Similarly, if the peer's signature exchange is incomplete, it
+/// cannot process `splice_locked` until the exchange adds the splice transaction to its negotiated
+/// candidates. However, an initial `commitment_signed` for the splice funding must itself be
+/// exchanged before the corresponding funding signatures.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub(super) enum TxSignaturesOrder {
+ /// Send `tx_signatures` before a normal commitment update or `splice_locked` so the peer
+ /// completes the signature exchange first.
+ SignaturesFirst,
+ /// Send `tx_signatures` after an initial splice `commitment_signed` establishes the new funding
+ /// state.
+ CommitmentFirst,
+}
+
/// Information about a payment which is currently being claimed.
#[derive(Clone, Debug, PartialEq, Eq)]
struct ClaimingPayment {
@@ -11187,6 +11206,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
updates.funding_tx_signed,
None,
updates.channel_ready_order,
+ TxSignaturesOrder::SignaturesFirst,
);
needs_persist |= !htlc_forwards.is_empty();
@@ -11348,7 +11368,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
funding_broadcastable: Option<Transaction>,
channel_ready: Option<msgs::ChannelReady>, announcement_sigs: Option<msgs::AnnouncementSignatures>,
mut funding_tx_signed: Option<FundingTxSigned>, tx_abort: Option<msgs::TxAbort>,
- channel_ready_order: ChannelReadyOrder,
+ channel_ready_order: ChannelReadyOrder, tx_signatures_order: TxSignaturesOrder,
) -> (Vec<PendingAddHTLCInfo>, Option<(u64, Vec<msgs::UpdateAddHTLC>)>) {
let logger = WithChannelContext::from(&self.logger, &channel.context, None);
log_trace!(logger, "Handling channel resumption with {} RAA, {} commitment update, {} pending forwards, {} pending update_add_htlcs, {}broadcasting funding, {} channel ready, {} announcement, {} tx_signatures, {} tx_abort, {} splice_locked",
@@ -11405,6 +11425,14 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
debug_assert!(funding_tx_signed.commitment_signed.is_none());
debug_assert!(funding_tx_signed.counterparty_initial_commitment_signed_result.is_none());
}
+ if let TxSignaturesOrder::SignaturesFirst = tx_signatures_order {
+ if let Some(msg) = funding_tx_signed.as_mut().and_then(|v| v.tx_signatures.take()) {
+ pending_msg_events.push(MessageSendEvent::SendTxSignatures {
+ node_id: counterparty_node_id,
+ msg,
+ });
+ }
+ }
if let Some(msg) = funding_tx_signed.as_mut().and_then(|v| v.splice_locked.take()) {
pending_msg_events.push(MessageSendEvent::SendSpliceLocked {
node_id: counterparty_node_id,
@@ -11440,11 +11468,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
},
}
- if let Some(msg) = funding_tx_signed.as_mut().and_then(|v| v.tx_signatures.take()) {
- pending_msg_events.push(MessageSendEvent::SendTxSignatures {
- node_id: counterparty_node_id,
- msg,
- });
+ if let TxSignaturesOrder::CommitmentFirst = tx_signatures_order {
+ if let Some(msg) = funding_tx_signed.as_mut().and_then(|v| v.tx_signatures.take()) {
+ pending_msg_events.push(MessageSendEvent::SendTxSignatures {
+ node_id: counterparty_node_id,
+ msg,
+ });
+ }
}
if let Some(msg) = tx_abort {
pending_msg_events.push(MessageSendEvent::SendTxAbort {
@@ -13669,9 +13699,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
}
let need_lnd_workaround = chan.context.workaround_lnd_bug_4006.take();
let inferred_splice_locked = responses.inferred_splice_locked;
- let funding_tx_signed = if responses.tx_signatures.is_some() || responses.splice_locked.is_some() {
+ let (tx_signatures_order, tx_signatures) = responses
+ .tx_signatures
+ .map(|(order, msg)| (order, Some(msg)))
+ .unwrap_or((TxSignaturesOrder::CommitmentFirst, None));
+ let funding_tx_signed = if tx_signatures.is_some() || responses.splice_locked.is_some() {
Some(FundingTxSigned {
- tx_signatures: responses.tx_signatures,
+ tx_signatures,
splice_locked: responses.splice_locked,
..Default::default()
})
@@ -13681,7 +13715,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
let (htlc_forwards, decode_update_add_htlcs) = self.handle_channel_resumption(
&mut peer_state.pending_msg_events, chan, responses.raa, responses.commitment_update, responses.commitment_order,
Vec::new(), Vec::new(), None, responses.channel_ready, responses.announcement_sigs,
- funding_tx_signed, responses.tx_abort, responses.channel_ready_order,
+ funding_tx_signed, responses.tx_abort, responses.channel_ready_order, tx_signatures_order,
);
debug_assert!(htlc_forwards.is_empty());
debug_assert!(decode_update_add_htlcs.is_none());
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index 3601675..26fed9e 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -27,7 +27,7 @@ use crate::ln::chan_utils::{
};
use crate::ln::channelmanager::{
AChannelManager, ChainParameters, ChannelManager, ChannelManagerReadArgs, PaymentId,
- RAACommitmentOrder, TrustedChannelFeatures, MIN_CLTV_EXPIRY_DELTA,
+ RAACommitmentOrder, TrustedChannelFeatures, TxSignaturesOrder, MIN_CLTV_EXPIRY_DELTA,
};
use crate::ln::funding::FundingContribution;
use crate::ln::msgs::{self, OpenChannel};
@@ -5214,6 +5214,18 @@ macro_rules! handle_chan_reestablish_msgs {
stfu = Some(msg.clone());
}
+ let mut tx_signatures = None;
+ let mut tx_signatures_order =
+ $crate::ln::channelmanager::TxSignaturesOrder::CommitmentFirst;
+ if let Some(&MessageSendEvent::SendTxSignatures { ref node_id, ref msg }) =
+ msg_events.get(idx)
+ {
+ assert_eq!(*node_id, $dst_node.node.get_our_node_id());
+ tx_signatures = Some(msg.clone());
+ tx_signatures_order = $crate::ln::channelmanager::TxSignaturesOrder::SignaturesFirst;
+ idx += 1;
+ }
+
let mut revoke_and_ack = None;
let mut commitment_update = None;
let order = if let Some(ev) = msg_events.get(idx) {
@@ -5262,13 +5274,14 @@ macro_rules! handle_chan_reestablish_msgs {
}
}
- let mut tx_signatures = None;
- if let Some(&MessageSendEvent::SendTxSignatures { ref node_id, ref msg }) =
- msg_events.get(idx)
- {
- assert_eq!(*node_id, $dst_node.node.get_our_node_id());
- tx_signatures = Some(msg.clone());
- idx += 1;
+ if tx_signatures.is_none() {
+ if let Some(&MessageSendEvent::SendTxSignatures { ref node_id, ref msg }) =
+ msg_events.get(idx)
+ {
+ assert_eq!(*node_id, $dst_node.node.get_our_node_id());
+ tx_signatures = Some(msg.clone());
+ idx += 1;
+ }
}
if let Some(&MessageSendEvent::SendAnnouncementSignatures { ref node_id, ref msg }) =
@@ -5298,6 +5311,7 @@ macro_rules! handle_chan_reestablish_msgs {
tx_signatures,
stfu,
tx_abort,
+ tx_signatures_order,
)
}};
}
@@ -5453,8 +5467,25 @@ pub fn reconnect_nodes<'a, 'b, 'c, 'd>(args: ReconnectArgs<'a, 'b, 'c, 'd>) {
&& pending_cell_htlc_claims.1 == 0
&& pending_cell_htlc_fails.1 == 0)
);
+ let pending_commitment_update = (
+ pending_htlc_adds.0 != 0
+ || pending_htlc_claims.0 != 0
+ || pending_htlc_fails.0 != 0
+ || pending_cell_htlc_claims.0 != 0
+ || pending_cell_htlc_fails.0 != 0
+ || pending_responding_commitment_signed.0,
+ pending_htlc_adds.1 != 0
+ || pending_htlc_claims.1 != 0
+ || pending_htlc_fails.1 != 0
+ || pending_cell_htlc_claims.1 != 0
+ || pending_cell_htlc_fails.1 != 0
+ || pending_responding_commitment_signed.1,
+ );
for mut chan_msgs in resp_1.drain(..) {
+ if send_interactive_tx_sigs.0 && pending_commitment_update.0 {
+ assert_eq!(chan_msgs.8, TxSignaturesOrder::SignaturesFirst);
+ }
if send_channel_ready.0 {
node_a.node.handle_channel_ready(node_b_id, &chan_msgs.0.unwrap());
let announcement_event = node_a.node.get_and_clear_pending_msg_events();
@@ -5516,13 +5547,7 @@ pub fn reconnect_nodes<'a, 'b, 'c, 'd>(args: ReconnectArgs<'a, 'b, 'c, 'd>) {
} else {
assert!(chan_msgs.1.is_none());
}
- if pending_htlc_adds.0 != 0
- || pending_htlc_claims.0 != 0
- || pending_htlc_fails.0 != 0
- || pending_cell_htlc_claims.0 != 0
- || pending_cell_htlc_fails.0 != 0
- || pending_responding_commitment_signed.0
- {
+ if pending_commitment_update.0 {
let commitment_update = chan_msgs.2.unwrap();
assert_eq!(commitment_update.update_add_htlcs.len(), pending_htlc_adds.0);
assert_eq!(
@@ -5571,6 +5596,9 @@ pub fn reconnect_nodes<'a, 'b, 'c, 'd>(args: ReconnectArgs<'a, 'b, 'c, 'd>) {
}
for mut chan_msgs in resp_2.drain(..) {
+ if send_interactive_tx_sigs.1 && pending_commitment_update.1 {
+ assert_eq!(chan_msgs.8, TxSignaturesOrder::SignaturesFirst);
+ }
if send_channel_ready.1 {
node_b.node.handle_channel_ready(node_a_id, &chan_msgs.0.unwrap());
let announcement_event = node_b.node.get_and_clear_pending_msg_events();
@@ -5632,13 +5660,7 @@ pub fn reconnect_nodes<'a, 'b, 'c, 'd>(args: ReconnectArgs<'a, 'b, 'c, 'd>) {
} else {
assert!(chan_msgs.1.is_none());
}
- if pending_htlc_adds.1 != 0
- || pending_htlc_claims.1 != 0
- || pending_htlc_fails.1 != 0
- || pending_cell_htlc_claims.1 != 0
- || pending_cell_htlc_fails.1 != 0
- || pending_responding_commitment_signed.1
- {
+ if pending_commitment_update.1 {
let commitment_update = chan_msgs.2.unwrap();
assert_eq!(commitment_update.update_add_htlcs.len(), pending_htlc_adds.1);
assert_eq!(
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index 04fe241..c762ca0 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -2740,10 +2740,11 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) {
}
#[test]
-fn test_splice_locked_waits_for_channel_reestablish() {
+fn test_reestablish_sends_tx_signatures_before_splice_locked() {
// If a splice confirms after `peer_connected` but before `channel_reestablish` is handled, the
// peer state is connected while the channel still has its disconnected bit set. We must not send
- // `splice_locked` until the channel is reestablished, but should send it immediately after.
+ // `splice_locked` until the channel is reestablished. If the peer also lost our `tx_signatures`,
+ // we must retransmit them before `splice_locked` so it recognizes the negotiated candidate.
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
@@ -2771,7 +2772,51 @@ fn test_splice_locked_waits_for_channel_reestablish() {
];
let funding_contribution =
initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap();
- let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
+ negotiate_splice_tx(&nodes[0], &nodes[1], channel_id, funding_contribution);
+
+ let event = get_event!(nodes[0], Event::FundingTransactionReadyForSigning);
+ if let Event::FundingTransactionReadyForSigning { unsigned_transaction, .. } = event {
+ let partially_signed_tx = nodes[0].wallet_source.sign_tx(unsigned_transaction).unwrap();
+ nodes[0]
+ .node
+ .funding_transaction_signed(&channel_id, &node_id_1, partially_signed_tx)
+ .unwrap();
+ } else {
+ panic!("Unexpected event {event:?}");
+ }
+
+ let commitment_update_0 = get_htlc_update_msgs(&nodes[0], &node_id_1);
+ nodes[1].node.handle_commitment_signed(node_id_0, &commitment_update_0.commitment_signed[0]);
+ check_added_monitors(&nodes[1], 1);
+
+ let msg_events = nodes[1].node.get_and_clear_pending_msg_events();
+ assert_eq!(msg_events.len(), 2, "{msg_events:?}");
+ if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[0] {
+ assert!(updates.update_add_htlcs.is_empty());
+ assert_eq!(updates.commitment_signed.len(), 1);
+ nodes[0].node.handle_commitment_signed(node_id_1, &updates.commitment_signed[0]);
+ check_added_monitors(&nodes[0], 1);
+ } else {
+ panic!("Unexpected event {:?}", msg_events[0]);
+ }
+ if let MessageSendEvent::SendTxSignatures { msg, .. } = &msg_events[1] {
+ nodes[0].node.handle_tx_signatures(node_id_1, msg);
+ check_added_monitors(&nodes[0], 0);
+ expect_splice_pending_event(&nodes[0], &node_id_1);
+ } else {
+ panic!("Unexpected event {:?}", msg_events[1]);
+ }
+
+ // Node 0 completes the exchange locally and broadcasts the splice, but its responding
+ // `tx_signatures` are lost. Node 1 therefore still has no negotiated candidate for the splice.
+ let tx_signatures_0 = get_event_msg!(nodes[0], MessageSendEvent::SendTxSignatures, node_id_1);
+ let splice_txid = tx_signatures_0.tx_hash;
+ let mut broadcast_transactions = nodes[0].tx_broadcaster.txn_broadcast();
+ assert_eq!(broadcast_transactions.len(), 1, "{broadcast_transactions:?}");
+ let splice_tx = broadcast_transactions.remove(0);
+ assert_eq!(splice_tx.compute_txid(), splice_txid);
+ assert!(nodes[1].tx_broadcaster.txn_broadcast().is_empty());
+ assert!(nodes[1].node.get_and_clear_pending_events().is_empty());
nodes[0].node.peer_disconnected(node_id_1);
nodes[1].node.peer_disconnected(node_id_0);
@@ -2781,27 +2826,40 @@ fn test_splice_locked_waits_for_channel_reestablish() {
get_event_msg!(nodes[0], MessageSendEvent::SendChannelReestablish, node_id_1);
let reestablish_1 =
get_event_msg!(nodes[1], MessageSendEvent::SendChannelReestablish, node_id_0);
+ assert!(reestablish_0.next_funding.is_none());
+ assert_ne!(
+ reestablish_0.my_current_funding_locked.as_ref().map(|funding| funding.txid),
+ Some(splice_txid),
+ );
+ assert_eq!(reestablish_1.next_funding.as_ref().map(|funding| funding.txid), Some(splice_txid));
confirm_transaction(&nodes[0], &splice_tx);
assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());
nodes[1].node.handle_channel_reestablish(node_id_0, &reestablish_0);
let _ = get_event_msg!(nodes[1], MessageSendEvent::SendChannelUpdate, node_id_0);
+
nodes[0].node.handle_channel_reestablish(node_id_1, &reestablish_1);
- let mut msg_events = nodes[0].node.get_and_clear_pending_msg_events();
- assert_eq!(msg_events.len(), 2, "{msg_events:?}");
- let splice_locked_0 =
- if let MessageSendEvent::SendSpliceLocked { node_id, msg } = msg_events.remove(0) {
- assert_eq!(node_id, node_id_1);
- msg
- } else {
- panic!();
- };
- if let MessageSendEvent::SendChannelUpdate { node_id, .. } = msg_events.remove(0) {
- assert_eq!(node_id, node_id_1);
+ let msg_events = nodes[0].node.get_and_clear_pending_msg_events();
+ assert_eq!(msg_events.len(), 3, "{msg_events:?}");
+ if let MessageSendEvent::SendTxSignatures { msg, .. } = &msg_events[0] {
+ nodes[1].node.handle_tx_signatures(node_id_0, &msg);
+ check_added_monitors(&nodes[1], 0);
} else {
- panic!();
+ panic!("Unexpected event {:?}", msg_events[0]);
}
+ let splice_locked_0 = if let MessageSendEvent::SendSpliceLocked { msg, .. } = &msg_events[1] {
+ msg
+ } else {
+ panic!("Unexpected event {:?}", msg_events[1]);
+ };
+ assert!(matches!(msg_events[2], MessageSendEvent::SendChannelUpdate { .. }));
+
+ assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
+ assert!(nodes[1].node.get_and_clear_pending_events().is_empty());
+ let broadcast_transactions = nodes[1].tx_broadcaster.txn_broadcast();
+ assert_eq!(broadcast_transactions.len(), 1, "{broadcast_transactions:?}");
+ assert_eq!(broadcast_transactions[0], splice_tx);
confirm_transaction(&nodes[1], &splice_tx);
complete_splice_locked_exchange(
@@ -4525,6 +4583,12 @@ fn fail_splice_on_tx_complete_error() {
#[test]
fn free_holding_cell_on_tx_signatures_quiescence_exit() {
+ do_test_free_holding_cell_on_tx_signatures_quiescence_exit(true);
+ do_test_free_holding_cell_on_tx_signatures_quiescence_exit(false);
+}
+
+#[cfg(test)]
+fn do_test_free_holding_cell_on_tx_signatures_quiescence_exit(update_from_initiator: bool) {
// Test that if there's an update in the holding cell while we're quiescent, that it gets freed
// upon exiting quiescence via the `tx_signatures` exchange.
let chanmon_cfgs = create_chanmon_cfgs(2);
@@ -4540,21 +4604,63 @@ fn free_holding_cell_on_tx_signatures_quiescence_exit() {
let (_, _, channel_id, _) =
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0);
+ if !update_from_initiator {
+ // Give the acceptor enough balance to queue the mirrored outbound HTLC.
+ send_payment(initiator, &[acceptor], 2_000_000);
+ provide_utxo_reserves(&nodes, 2, Amount::ONE_BTC);
+ }
let outputs = vec![TxOut {
value: Amount::from_sat(1_000),
script_pubkey: initiator.wallet_source.get_change_script().unwrap(),
}];
- let contribution = initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap();
- negotiate_splice_tx(initiator, acceptor, channel_id, contribution);
+ let initiator_contribution =
+ initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap();
+ if update_from_initiator {
+ negotiate_splice_tx(initiator, acceptor, channel_id, initiator_contribution);
+ } else {
+ // Make the acceptor the second signer so receiving the initiator's `tx_signatures` causes it
+ // to send both its own `tx_signatures` and the commitment update held during quiescence.
+ let acceptor_contribution =
+ initiate_splice_in(acceptor, initiator, channel_id, Amount::from_sat(200_000));
+ let stfu_initiator =
+ get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor);
+ let stfu_acceptor = get_event_msg!(acceptor, MessageSendEvent::SendStfu, node_id_initiator);
+ acceptor.node.handle_stfu(node_id_initiator, &stfu_initiator);
+ assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty());
+ initiator.node.handle_stfu(node_id_acceptor, &stfu_acceptor);
+
+ let splice_init =
+ get_event_msg!(initiator, MessageSendEvent::SendSpliceInit, node_id_acceptor);
+ acceptor.node.handle_splice_init(node_id_initiator, &splice_init);
+ let splice_ack =
+ get_event_msg!(acceptor, MessageSendEvent::SendSpliceAck, node_id_initiator);
+ initiator.node.handle_splice_ack(node_id_acceptor, &splice_ack);
+ let new_funding_script = chan_utils::make_funding_redeemscript(
+ &splice_init.funding_pubkey,
+ &splice_ack.funding_pubkey,
+ )
+ .to_p2wsh();
+ complete_interactive_funding_negotiation_for_both(
+ initiator,
+ acceptor,
+ channel_id,
+ initiator_contribution,
+ Some(acceptor_contribution),
+ splice_ack.funding_contribution_satoshis,
+ new_funding_script,
+ );
+ }
// Queue an outgoing HTLC to the holding cell. It should be freed once we exit quiescence.
+ let (update_sender, update_recipient) =
+ if update_from_initiator { (initiator, acceptor) } else { (acceptor, initiator) };
let (route, payment_hash, _payment_preimage, payment_secret) =
- get_route_and_payment_hash!(initiator, acceptor, 1_000_000);
+ get_route_and_payment_hash!(update_sender, update_recipient, 1_000_000);
let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000);
let payment_id = PaymentId(payment_hash.0);
- initiator.node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap();
- assert!(initiator.node.get_and_clear_pending_msg_events().is_empty());
+ update_sender.node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap();
+ assert!(update_sender.node.get_and_clear_pending_msg_events().is_empty());
let event = get_event!(initiator, Event::FundingTransactionReadyForSigning);
if let Event::FundingTransactionReadyForSigning {
@@ -4575,42 +4681,95 @@ fn free_holding_cell_on_tx_signatures_quiescence_exit() {
let update = get_htlc_update_msgs(initiator, &node_id_acceptor);
acceptor.node.handle_commitment_signed(node_id_initiator, &update.commitment_signed[0]);
- check_added_monitors(&acceptor, 1);
+ if !update_from_initiator {
+ // The acceptor's initial commitment_signed is buffered until it signs its contributed input.
+ assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty());
+ let event = get_event!(acceptor, Event::FundingTransactionReadyForSigning);
+ if let Event::FundingTransactionReadyForSigning {
+ channel_id,
+ counterparty_node_id,
+ unsigned_transaction,
+ ..
+ } = event
+ {
+ let partially_signed_tx = acceptor.wallet_source.sign_tx(unsigned_transaction).unwrap();
+ acceptor
+ .node
+ .funding_transaction_signed(&channel_id, &counterparty_node_id, partially_signed_tx)
+ .unwrap();
+ } else {
+ unreachable!();
+ }
+ }
+ check_added_monitors(acceptor, 1);
- let msg_events = acceptor.node.get_and_clear_pending_msg_events();
- assert_eq!(msg_events.len(), 2, "{msg_events:?}");
- if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = &msg_events[0] {
+ let acceptor_msg_events = acceptor.node.get_and_clear_pending_msg_events();
+ assert_eq!(
+ acceptor_msg_events.len(),
+ if update_from_initiator { 2 } else { 1 },
+ "{acceptor_msg_events:?}"
+ );
+ if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = &acceptor_msg_events[0] {
+ assert!(updates.update_add_htlcs.is_empty());
+ assert_eq!(updates.commitment_signed.len(), 1);
let commitment_signed = &updates.commitment_signed[0];
initiator.node.handle_commitment_signed(node_id_acceptor, commitment_signed);
check_added_monitors(&initiator, 1);
} else {
- panic!("Unexpected event {:?}", &msg_events[0]);
- }
- if let MessageSendEvent::SendTxSignatures { ref msg, .. } = &msg_events[1] {
- initiator.node.handle_tx_signatures(node_id_acceptor, msg);
- } else {
- panic!("Unexpected event {:?}", &msg_events[1]);
+ panic!("Unexpected event {:?}", &acceptor_msg_events[0]);
}
- // With `tx_signatures` exchanged, we've exited quiescence and should now see the outgoing HTLC
- // update be sent.
- let msg_events = initiator.node.get_and_clear_pending_msg_events();
- assert_eq!(msg_events.len(), 2, "{msg_events:?}");
- check_added_monitors(initiator, 1); // Outgoing HTLC monitor update
- if let MessageSendEvent::SendTxSignatures { ref msg, .. } = &msg_events[0] {
- acceptor.node.handle_tx_signatures(node_id_initiator, msg);
+ let expect_tx_signatures_then_htlc_update = |msg_events: &[MessageSendEvent]| match msg_events {
+ [MessageSendEvent::SendTxSignatures { .. }, MessageSendEvent::UpdateHTLCs { updates, .. }] =>
+ {
+ assert_eq!(updates.update_add_htlcs.len(), 1);
+ assert_eq!(updates.commitment_signed.len(), 2);
+ },
+ _ => panic!("Unexpected events {msg_events:?}"),
+ };
+ if update_from_initiator {
+ if let MessageSendEvent::SendTxSignatures { node_id, ref msg } = &acceptor_msg_events[1] {
+ assert_eq!(*node_id, node_id_initiator);
+ initiator.node.handle_tx_signatures(node_id_acceptor, msg);
+ } else {
+ panic!("Unexpected event {:?}", &acceptor_msg_events[1]);
+ }
+
+ // With `tx_signatures` exchanged, we've exited quiescence and should now see the outgoing
+ // HTLC update be sent.
+ let initiator_msg_events = initiator.node.get_and_clear_pending_msg_events();
+ check_added_monitors(initiator, 1); // Outgoing HTLC monitor update
+ expect_tx_signatures_then_htlc_update(&initiator_msg_events);
} else {
- panic!("Unexpected event {:?}", &msg_events[0]);
+ let initiator_tx_signatures =
+ get_event_msg!(initiator, MessageSendEvent::SendTxSignatures, node_id_acceptor);
+ acceptor.node.handle_tx_signatures(node_id_initiator, &initiator_tx_signatures);
+
+ let acceptor_msg_events = acceptor.node.get_and_clear_pending_msg_events();
+ check_added_monitors(acceptor, 1); // Outgoing HTLC monitor update
+ expect_tx_signatures_then_htlc_update(&acceptor_msg_events);
}
- if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[1] {
- acceptor.node.handle_update_add_htlc(node_id_initiator, &updates.update_add_htlcs[0]);
- do_commitment_signed_dance(acceptor, initiator, &updates.commitment_signed, false, false);
+
+ // If the messages are dropped and the peers reconnect, the `tx_signatures` need to be
+ // retransmitted before the freed holding-cell update so the peer can leave quiescence before
+ // handling normal commitment updates.
+ initiator.node.peer_disconnected(node_id_acceptor);
+ acceptor.node.peer_disconnected(node_id_initiator);
+ let mut reconnect_args = ReconnectArgs::new(initiator, acceptor);
+ if update_from_initiator {
+ reconnect_args.send_announcement_sigs = (true, true);
+ reconnect_args.send_interactive_tx_sigs = (false, true);
+ reconnect_args.pending_htlc_adds = (0, 1);
} else {
- panic!("Unexpected event {:?}", &msg_events[1]);
+ reconnect_args.send_interactive_tx_sigs = (true, false);
+ reconnect_args.pending_htlc_adds = (1, 0);
}
+ reconnect_nodes(reconnect_args);
expect_splice_pending_event(initiator, &node_id_acceptor);
- assert!(acceptor.node.get_and_clear_pending_events().is_empty());
+ if !update_from_initiator {
+ expect_splice_pending_event(acceptor, &node_id_initiator);
+ }
}
#[test]
@@ -5336,6 +5495,268 @@ fn do_splice_waits_for_initial_commitment_monitor_update_before_releasing_tx_sig
}
}
+#[test]
+fn test_monitor_restore_sends_tx_signatures_before_splice_locked() {
+ // When a 0-conf splice's RenegotiatedFunding monitor update completes asynchronously after
+ // the counterparty already sent its tx_signatures, restoring the channel releases both our
+ // tx_signatures and, with the signatures exchange now being complete, our 0-conf splice_locked.
+ // The tx_signatures must be sent first: the counterparty only learns the new funding txid is
+ // fully signed upon receiving our tx_signatures, and it will close the channel upon receiving
+ // splice_locked for a funding txid outside its negotiated candidates.
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let mut config = test_default_channel_config();
+ config.channel_handshake_limits.trust_own_funding_0conf = true;
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let node_id_0 = nodes[0].node.get_our_node_id();
+ let node_id_1 = nodes[1].node.get_our_node_id();
+
+ // The channel must be 0-conf so that the splice funding, which inherits the channel's
+ // minimum depth, locks as soon as tx_signatures are exchanged.
+ let initial_channel_value_sat = 100_000;
+ let (funding_tx, channel_id) =
+ open_zero_conf_channel_with_value(&nodes[0], &nodes[1], None, initial_channel_value_sat, 0);
+ mine_transaction(&nodes[0], &funding_tx);
+ mine_transaction(&nodes[1], &funding_tx);
+ let prev_funding_txid = funding_tx.compute_txid();
+
+ // Node 1 initiates a splice-in. The shared funding input counts towards the splice
+ // initiator's contributed input value, so node 0 -- contributing nothing -- will send its
+ // tx_signatures first, making node 1 the second signer.
+ provide_utxo_reserves(&nodes, 1, Amount::from_sat(100_000));
+ let splice_in_sat = Amount::from_sat(50_000);
+ let funding_contribution = initiate_splice_in(&nodes[1], &nodes[0], channel_id, splice_in_sat);
+ negotiate_splice_tx(&nodes[1], &nodes[0], channel_id, funding_contribution);
+
+ // Node 1 signs its contributed inputs and sends its commitment_signed for the new funding.
+ assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
+ let event = get_event!(nodes[1], Event::FundingTransactionReadyForSigning);
+ if let Event::FundingTransactionReadyForSigning { unsigned_transaction, .. } = event {
+ let partially_signed_tx = nodes[1].wallet_source.sign_tx(unsigned_transaction).unwrap();
+ nodes[1]
+ .node
+ .funding_transaction_signed(&channel_id, &node_id_0, partially_signed_tx)
+ .unwrap();
+ } else {
+ panic!();
+ }
+
+ let msg_events = nodes[1].node.get_and_clear_pending_msg_events();
+ assert_eq!(msg_events.len(), 1, "{msg_events:?}");
+ if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = &msg_events[0] {
+ nodes[0].node.handle_commitment_signed(node_id_1, &updates.commitment_signed[0]);
+ } else {
+ panic!("Unexpected event {:?}", msg_events[0]);
+ }
+ check_added_monitors(&nodes[0], 1);
+
+ // Node 0 contributed no inputs, so it is the first signer: it sends its tx_signatures
+ // immediately, along with its commitment_signed.
+ let msg_events = nodes[0].node.get_and_clear_pending_msg_events();
+ assert_eq!(msg_events.len(), 2, "{msg_events:?}");
+ if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[0] {
+ // Node 1 processes node 0's commitment_signed while its monitor persistence is async, leaving
+ // the RenegotiatedFunding monitor update in flight.
+ chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress);
+ nodes[1].node.handle_commitment_signed(node_id_0, &updates.commitment_signed[0]);
+ check_added_monitors(&nodes[1], 1);
+ assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
+ } else {
+ panic!("Unexpected event {:?}", msg_events[0]);
+ }
+ if let MessageSendEvent::SendTxSignatures { msg, .. } = &msg_events[1] {
+ // Node 1 receives node 0's tx_signatures while the monitor update is still in flight. Its
+ // responding tx_signatures (and everything resulting from the completed exchange) must be
+ // withheld until the monitor update completes.
+ nodes[1].node.handle_tx_signatures(node_id_0, msg);
+ check_added_monitors(&nodes[1], 0);
+ assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
+ assert!(nodes[1].node.get_and_clear_pending_events().is_empty());
+ assert!(nodes[1].tx_broadcaster.txn_broadcast().is_empty());
+ } else {
+ panic!("Unexpected event {:?}", msg_events[1]);
+ }
+
+ // Complete the monitor update. Node 1 now broadcasts the splice transaction and releases its
+ // tx_signatures. With both sides' signatures in hand and a 0-conf splice, it also generates
+ // splice_locked.
+ nodes[1].chain_monitor.complete_sole_pending_chan_update(&channel_id);
+ chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed);
+
+ expect_splice_pending_event(&nodes[1], &node_id_0);
+ let txn = nodes[1].tx_broadcaster.txn_broadcast();
+ assert_eq!(txn.len(), 1, "{txn:?}");
+ let splice_tx = txn[0].clone();
+
+ let msg_events = nodes[1].node.get_and_clear_pending_msg_events();
+ assert_eq!(msg_events.len(), 2, "{msg_events:?}");
+ if let MessageSendEvent::SendTxSignatures { msg, .. } = &msg_events[0] {
+ nodes[0].node.handle_tx_signatures(node_id_1, msg);
+ } else {
+ panic!("Unexpected event {:?}", msg_events[0]);
+ }
+ if let MessageSendEvent::SendSpliceLocked { msg, .. } = &msg_events[1] {
+ nodes[0].node.handle_splice_locked(node_id_1, msg);
+ } else {
+ panic!("Unexpected event {:?}", msg_events[1]);
+ }
+
+ // Node 0's signing session completed upon receiving node 1's tx_signatures: node 0 broadcasts
+ // the splice transaction and sends its own 0-conf splice_locked. Node 1's splice_locked then
+ // promotes the splice funding on node 0.
+ let txn = nodes[0].tx_broadcaster.txn_broadcast();
+ assert!(!txn.is_empty());
+ assert!(txn.iter().all(|tx| *tx == splice_tx), "{txn:?}");
+ expect_channel_ready_event(&nodes[0], &node_id_1);
+ check_added_monitors(&nodes[0], 1);
+
+ let msg_events = nodes[0].node.get_and_clear_pending_msg_events();
+ assert_eq!(msg_events.len(), 1, "{msg_events:?}");
+ if let MessageSendEvent::SendSpliceLocked { ref msg, .. } = msg_events[0] {
+ nodes[1].node.handle_splice_locked(node_id_0, msg);
+ } else {
+ panic!("Unexpected event {:?}", msg_events[0]);
+ }
+ expect_channel_ready_event(&nodes[1], &node_id_0);
+ check_added_monitors(&nodes[1], 1);
+ let txn = nodes[1].tx_broadcaster.txn_broadcast();
+ assert!(txn.iter().all(|tx| *tx == splice_tx), "{txn:?}");
+
+ // The old funding is no longer tracked once the splice is locked on both sides.
+ nodes[0].chain_source.remove_watched_by_txid(prev_funding_txid);
+ nodes[1].chain_source.remove_watched_by_txid(prev_funding_txid);
+
+ // The channel remains usable over the new funding.
+ send_payment(&nodes[0], &[&nodes[1]], 1_000_000);
+}
+
+#[test]
+fn retransmit_completed_tx_signatures_during_monitor_update_after_reestablish() {
+ // Test that splice `tx_signatures` owed to our peer are retransmitted on reestablish even if
+ // an unrelated monitor update is still in flight. The signature exchange already completed
+ // locally, so retransmitting the signatures does not depend on the pending monitor update and
+ // allows our peer to exit quiescence before the held commitment update is restored.
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let config = test_default_channel_config();
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let initiator = &nodes[0];
+ let acceptor = &nodes[1];
+ let node_id_initiator = initiator.node.get_our_node_id();
+ let node_id_acceptor = acceptor.node.get_our_node_id();
+
+ let (_, _, channel_id, _) =
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0);
+
+ let outputs = vec![TxOut {
+ value: Amount::from_sat(1_000),
+ script_pubkey: initiator.wallet_source.get_change_script().unwrap(),
+ }];
+ let contribution = initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap();
+ negotiate_splice_tx(initiator, acceptor, channel_id, contribution);
+
+ // Queue an outgoing HTLC to the holding cell. It should be freed once we exit quiescence.
+ let (route, payment_hash, _payment_preimage, payment_secret) =
+ get_route_and_payment_hash!(initiator, acceptor, 1_000_000);
+ let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000);
+ let payment_id = PaymentId(payment_hash.0);
+ initiator.node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap();
+ assert!(initiator.node.get_and_clear_pending_msg_events().is_empty());
+
+ let event = get_event!(initiator, Event::FundingTransactionReadyForSigning);
+ if let Event::FundingTransactionReadyForSigning {
+ channel_id,
+ counterparty_node_id,
+ unsigned_transaction,
+ ..
+ } = event
+ {
+ let partially_signed_tx = initiator.wallet_source.sign_tx(unsigned_transaction).unwrap();
+ initiator
+ .node
+ .funding_transaction_signed(&channel_id, &counterparty_node_id, partially_signed_tx)
+ .unwrap();
+ } else {
+ unreachable!();
+ }
+
+ let update = get_htlc_update_msgs(initiator, &node_id_acceptor);
+ acceptor.node.handle_commitment_signed(node_id_initiator, &update.commitment_signed[0]);
+ check_added_monitors(&acceptor, 1);
+
+ // The acceptor sends `tx_signatures` first since it contributed no inputs.
+ let msg_events = acceptor.node.get_and_clear_pending_msg_events();
+ assert_eq!(msg_events.len(), 2, "{msg_events:?}");
+ if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = &msg_events[0] {
+ let commitment_signed = &updates.commitment_signed[0];
+ initiator.node.handle_commitment_signed(node_id_acceptor, commitment_signed);
+ check_added_monitors(&initiator, 1);
+ } else {
+ panic!("Unexpected event {:?}", &msg_events[0]);
+ }
+ assert!(initiator.node.get_and_clear_pending_msg_events().is_empty());
+
+ // Handle the acceptor's `tx_signatures` while the initiator's monitor persistence is async.
+ // This completes the exchange atomically: the initiator releases its `tx_signatures` and
+ // exits quiescence, freeing the holding cell HTLC, which itself results in a new monitor
+ // update that remains in flight.
+ chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress);
+ let splice_txid =
+ if let MessageSendEvent::SendTxSignatures { node_id, ref msg } = &msg_events[1] {
+ assert_eq!(*node_id, node_id_initiator);
+ initiator.node.handle_tx_signatures(node_id_acceptor, msg);
+ msg.tx_hash
+ } else {
+ panic!("Unexpected event {:?}", &msg_events[1]);
+ };
+ check_added_monitors(&initiator, 1);
+ expect_splice_pending_event(initiator, &node_id_acceptor);
+
+ // The initiator's `tx_signatures` goes out immediately, but the freed holding cell update is
+ // withheld while the monitor update is in flight. Drop the `tx_signatures` (lost in
+ // transit), such that the initiator owes the acceptor both its `tx_signatures` and a
+ // commitment update.
+ let msg_events = initiator.node.get_and_clear_pending_msg_events();
+ assert_eq!(msg_events.len(), 1, "{msg_events:?}");
+ if let MessageSendEvent::SendTxSignatures { node_id, ref msg } = &msg_events[0] {
+ assert_eq!(*node_id, node_id_acceptor);
+ assert_eq!(msg.tx_hash, splice_txid);
+ } else {
+ panic!("Unexpected event {:?}", &msg_events[0]);
+ }
+
+ // Reconnect while the initiator's monitor update is still in flight. The acceptor's
+ // signing session is incomplete, so its `channel_reestablish` causes the initiator to
+ // retransmit its completed exchange's `tx_signatures` immediately. The normal commitment
+ // update remains withheld by the in-flight monitor update.
+ initiator.node.peer_disconnected(node_id_acceptor);
+ acceptor.node.peer_disconnected(node_id_initiator);
+ let mut reconnect_args = ReconnectArgs::new(acceptor, initiator);
+ reconnect_args.send_announcement_sigs = (true, true);
+ reconnect_args.send_interactive_tx_sigs = (true, false);
+ reconnect_nodes(reconnect_args);
+ check_added_monitors(acceptor, 0);
+ assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty());
+ assert!(initiator.node.get_and_clear_pending_msg_events().is_empty());
+
+ // Once the monitor update completes, only the freed holding cell update remains to be sent.
+ initiator.chain_monitor.complete_sole_pending_chan_update(&channel_id);
+ chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed);
+
+ let msg_events = initiator.node.get_and_clear_pending_msg_events();
+ assert_eq!(msg_events.len(), 1, "{msg_events:?}");
+ if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[0] {
+ acceptor.node.handle_update_add_htlc(node_id_initiator, &updates.update_add_htlcs[0]);
+ do_commitment_signed_dance(acceptor, initiator, &updates.commitment_signed, false, false);
+ } else {
+ panic!("Unexpected event {:?}", &msg_events[0]);
+ }
+}
+
#[test]
fn test_splice_balance_falls_below_reserve() {
// Test that we're able to proceed with a splice where the acceptor does not contribute
Why this scored 60/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.