From 7165827ed43ccc1ed2b58ca35f4e28d25a2dba71 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 12 May 2026 15:13:58 -0700 Subject: [PATCH 1/2] Send missing splice_locked when confirmation precedes reestablishment In most cases, we end up sending our `splice_locked` either implicitly during reestablishment via `ChannelReestablish::my_current_funding_locked`, or explicitly after reestablishment. However, we did not consider that it's possible for the node to be notified of the splice confirmation after connecting to their peer but prior to reestablishing their channel. In such cases, we need to explicitly send the `splice_locked` since it wasn't included in `my_current_funding_locked`, but only after the channel has been reestablished. Found by the chanmon_consistency fuzz target. --- lightning/src/ln/channel.rs | 31 +++++++++- lightning/src/ln/channelmanager.rs | 13 +++-- lightning/src/ln/splicing_tests.rs | 91 ++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 5 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index edcaacfedc6..2341128c74d 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1265,6 +1265,7 @@ pub(super) struct ReestablishResponses { pub shutdown_msg: Option, pub tx_signatures: Option, pub tx_abort: Option, + pub splice_locked: Option, pub inferred_splice_locked: Option, } @@ -3503,6 +3504,12 @@ pub(super) struct ChannelContext { /// See-also pub workaround_lnd_bug_4006: Option, + /// The `my_current_funding_locked` txid included in our `channel_reestablish` for the current + /// reconnect, if any. We track this as we cannot tell what was included after we've already + /// sent it, as it's possible it was unconfirmed at the time we sent it, but confirmed shortly + /// after. + funding_locked_txid_sent_in_reestablish: Option, + /// An option set when we wish to track how many ticks have elapsed while waiting for a response /// from our counterparty after entering specific states. If the peer has yet to respond after /// reaching `DISCONNECT_PEER_AWAITING_RESPONSE_TICKS`, a reconnection should be attempted to @@ -4225,6 +4232,7 @@ impl ChannelContext { announcement_sigs: None, workaround_lnd_bug_4006: None, + funding_locked_txid_sent_in_reestablish: None, sent_message_awaiting_response: None, latest_inbound_scid_alias: None, @@ -4536,6 +4544,7 @@ impl ChannelContext { announcement_sigs: None, workaround_lnd_bug_4006: None, + funding_locked_txid_sent_in_reestablish: None, sent_message_awaiting_response: None, latest_inbound_scid_alias: None, @@ -10512,6 +10521,8 @@ where // remaining cases either succeed or ErrorMessage-fail). self.context.channel_state.clear_peer_disconnected(); self.mark_response_received(); + let funding_locked_txid_sent_in_reestablish = + self.context.funding_locked_txid_sent_in_reestablish.take(); let shutdown_msg = self.get_outbound_shutdown(); @@ -10663,6 +10674,7 @@ where shutdown_msg, announcement_sigs, tx_signatures, tx_abort: None, + splice_locked: None, inferred_splice_locked: None, }); } @@ -10676,6 +10688,7 @@ where shutdown_msg, announcement_sigs, tx_signatures, tx_abort, + splice_locked: None, inferred_splice_locked: None, }); } @@ -10745,6 +10758,15 @@ where splice_txid, }) }); + let splice_locked = self.pending_splice.as_ref().and_then(|pending_splice| { + pending_splice + .sent_funding_txid + .filter(|splice_txid| Some(*splice_txid) != funding_locked_txid_sent_in_reestablish) + .map(|splice_txid| msgs::SpliceLocked { + channel_id: self.context.channel_id, + splice_txid, + }) + }); if msg.next_local_commitment_number == next_counterparty_commitment_number { if required_revoke.is_some() || self.context.signer_pending_revoke_and_ack { @@ -10763,6 +10785,7 @@ where commitment_order: self.context.resend_order.clone(), tx_signatures, tx_abort, + splice_locked, inferred_splice_locked, }) } else if msg.next_local_commitment_number == next_counterparty_commitment_number - 1 { @@ -10788,6 +10811,7 @@ where commitment_order: self.context.resend_order.clone(), tx_signatures: None, tx_abort, + splice_locked, inferred_splice_locked, }) } else { @@ -10815,6 +10839,7 @@ where commitment_order: self.context.resend_order.clone(), tx_signatures: None, tx_abort, + splice_locked, inferred_splice_locked, }) } @@ -12492,6 +12517,9 @@ where log_info!(logger, "Sending a data_loss_protect with no previous remote per_commitment_secret for channel {}", &self.context.channel_id()); [0;32] }; + let my_current_funding_locked = self.maybe_get_my_current_funding_locked(); + self.context.funding_locked_txid_sent_in_reestablish = + my_current_funding_locked.as_ref().map(|funding_locked| funding_locked.txid); msgs::ChannelReestablish { channel_id: self.context.channel_id(), // The protocol has two different commitment number concepts - the "commitment @@ -12515,7 +12543,7 @@ where your_last_per_commitment_secret: remote_last_secret, my_current_per_commitment_point: dummy_pubkey, next_funding: self.maybe_get_next_funding(), - my_current_funding_locked: self.maybe_get_my_current_funding_locked(), + my_current_funding_locked, } } @@ -17196,6 +17224,7 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> announcement_sigs, workaround_lnd_bug_4006: None, + funding_locked_txid_sent_in_reestablish: None, sent_message_awaiting_response: None, latest_inbound_scid_alias, diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 0ff2f19b830..2126cafaadf 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -13285,10 +13285,15 @@ 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 funding_tx_signed = responses.tx_signatures.map(|tx_signatures| FundingTxSigned { - tx_signatures: Some(tx_signatures), - ..Default::default() - }); + let funding_tx_signed = if responses.tx_signatures.is_some() || responses.splice_locked.is_some() { + Some(FundingTxSigned { + tx_signatures: responses.tx_signatures, + splice_locked: responses.splice_locked, + ..Default::default() + }) + } else { + None + }; 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, diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index 35c72509d0b..ca45a39c8cb 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -751,7 +751,21 @@ pub fn lock_splice<'a, 'b, 'c, 'd>( .get_monitor(splice_locked_for_node_b.channel_id) .map(|monitor| monitor.get_funding_txo().txid) .unwrap(); + complete_splice_locked_exchange( + node_a, + node_b, + splice_locked_for_node_b, + is_0conf, + expected_discard_txids, + prev_funding_txid, + ) +} +fn complete_splice_locked_exchange<'a, 'b, 'c, 'd>( + node_a: &'a Node<'b, 'c, 'd>, node_b: &'a Node<'b, 'c, 'd>, + splice_locked_for_node_b: &msgs::SpliceLocked, is_0conf: bool, expected_discard_txids: &[Txid], + prev_funding_txid: Txid, +) -> SpliceLockedResult { let node_id_a = node_a.node.get_our_node_id(); let node_id_b = node_b.node.get_our_node_id(); @@ -2585,6 +2599,83 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) { .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script); } +#[test] +fn test_splice_locked_waits_for_channel_reestablish() { + // 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. + 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]); + 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(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + let prev_funding_txid = get_monitor!(nodes[0], channel_id).get_funding_txo().txid; + + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + + let outputs = vec![ + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ]; + 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); + + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + + connect_nodes(&nodes[0], &nodes[1]); + let reestablish_0 = + get_event_msg!(nodes[0], MessageSendEvent::SendChannelReestablish, node_id_1); + let reestablish_1 = + get_event_msg!(nodes[1], MessageSendEvent::SendChannelReestablish, node_id_0); + + 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); + } else { + panic!(); + } + + confirm_transaction(&nodes[1], &splice_tx); + complete_splice_locked_exchange( + &nodes[0], + &nodes[1], + &splice_locked_0, + false, + &[], + prev_funding_txid, + ); + + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); +} + #[test] fn test_splice_confirms_on_both_sides_while_disconnected() { // Regression test: when a splice transaction confirms on both sides while peers are From 5e14a3fc98457974dd3cb841de2ecf3b1226c57e Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 19 May 2026 15:05:30 -0700 Subject: [PATCH 2/2] Handle inferred splice_locked on reestablish first prior to updates Upon channel reestablishment, we free our holding cells to send any pending updates to our peer. If we happened to implicitly lock a pending splice during reestablishment, we want to make sure any updates we send after the fact are considering the new channel state (post-splice), even if the update was queued while the splice was still pending. Therefore, we must always handle the inferred `splice_locked` first. Found by the `chanmon_consistency` fuzz target. --- lightning/src/ln/channelmanager.rs | 288 +++++++++++++++++------------ lightning/src/ln/splicing_tests.rs | 85 +++++++++ 2 files changed, 257 insertions(+), 116 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 2126cafaadf..4f90b00d727 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -12368,43 +12368,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_entry) => { if let Some(chan) = chan_entry.get_mut().as_funded_mut() { - let logger = WithChannelContext::from(&self.logger, &chan.context, None); - let res = chan.channel_ready( - &msg, - &self.node_signer, - self.chain_hash, - &self.config.read().unwrap(), - &self.best_block.read().unwrap(), - &&logger + let res = self.internal_channel_ready_with_funded_channel( + counterparty_node_id, + msg, + chan, + &mut peer_state.pending_msg_events, ); - let announcement_sigs_opt = - try_channel_entry!(self, peer_state, res, chan_entry); - if let Some(announcement_sigs) = announcement_sigs_opt { - log_trace!(logger, "Sending announcement_signatures"); - peer_state.pending_msg_events.push(MessageSendEvent::SendAnnouncementSignatures { - node_id: counterparty_node_id.clone(), - msg: announcement_sigs, - }); - } else if chan.context.is_usable() { - // If we're sending an announcement_signatures, we'll send the (public) - // channel_update after sending a channel_announcement when we receive our - // counterparty's announcement_signatures. Thus, we only bother to send a - // channel_update here if the channel is not public, i.e. we're not sending an - // announcement_signatures. - log_trace!(logger, "Sending private initial channel_update for our counterparty"); - if let Ok((msg, _, _)) = self.get_channel_update_for_unicast(chan) { - peer_state.pending_msg_events.push(MessageSendEvent::SendChannelUpdate { - node_id: counterparty_node_id.clone(), - msg, - }); - } - } - - { - let mut pending_events = self.pending_events.lock().unwrap(); - emit_initial_channel_ready_event!(pending_events, chan); - } - + try_channel_entry!(self, peer_state, res, chan_entry); Ok(()) } else { try_channel_entry!(self, peer_state, Err(ChannelError::close( @@ -12417,6 +12387,49 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } + #[rustfmt::skip] + fn internal_channel_ready_with_funded_channel( + &self, counterparty_node_id: &PublicKey, msg: &msgs::ChannelReady, + chan: &mut FundedChannel, pending_msg_events: &mut Vec, + ) -> Result<(), ChannelError> { + let logger = WithChannelContext::from(&self.logger, &chan.context, None); + let announcement_sigs_opt = chan.channel_ready( + &msg, + &self.node_signer, + self.chain_hash, + &self.config.read().unwrap(), + &self.best_block.read().unwrap(), + &&logger + )?; + if let Some(announcement_sigs) = announcement_sigs_opt { + log_trace!(logger, "Sending announcement_signatures"); + pending_msg_events.push(MessageSendEvent::SendAnnouncementSignatures { + node_id: counterparty_node_id.clone(), + msg: announcement_sigs, + }); + } else if chan.context.is_usable() { + // If we're sending an announcement_signatures, we'll send the (public) + // channel_update after sending a channel_announcement when we receive our + // counterparty's announcement_signatures. Thus, we only bother to send a + // channel_update here if the channel is not public, i.e. we're not sending an + // announcement_signatures. + log_trace!(logger, "Sending private initial channel_update for our counterparty"); + if let Ok((msg, _, _)) = self.get_channel_update_for_unicast(chan) { + pending_msg_events.push(MessageSendEvent::SendChannelUpdate { + node_id: counterparty_node_id.clone(), + msg, + }); + } + } + + { + let mut pending_events = self.pending_events.lock().unwrap(); + emit_initial_channel_ready_event!(pending_events, chan); + } + + Ok(()) + } + fn internal_shutdown( &self, counterparty_node_id: &PublicKey, msg: &msgs::Shutdown, ) -> Result<(), MsgHandleErrInternal> { @@ -13240,7 +13253,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<(), MsgHandleErrInternal> { - let (inferred_splice_locked, need_lnd_workaround, holding_cell_res) = { + let (post_splice_locked_update, holding_cell_res) = { let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { @@ -13249,7 +13262,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let logger = WithContext::from(&self.logger, Some(*counterparty_node_id), Some(msg.channel_id), None); let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; - match peer_state.channel_by_id.entry(msg.channel_id) { + let post_splice_locked_update = match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_entry) => { if let Some(chan) = chan_entry.get_mut().as_funded_mut() { // Currently, we expect all holding cell update_adds to be dropped on peer @@ -13285,6 +13298,7 @@ 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() { Some(FundingTxSigned { tx_signatures: responses.tx_signatures, @@ -13305,8 +13319,33 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ peer_state.pending_msg_events.push(upd); } - let holding_cell_res = self.check_free_peer_holding_cells(peer_state); - (responses.inferred_splice_locked, need_lnd_workaround, holding_cell_res) + if let Some(channel_ready_msg) = need_lnd_workaround { + let res = self.internal_channel_ready_with_funded_channel( + counterparty_node_id, + &channel_ready_msg, + chan, + &mut peer_state.pending_msg_events, + ); + try_channel_entry!(self, peer_state, res, chan_entry); + } + + // A reestablish may infer a missed `splice_locked`; apply it before freeing + // holding cells so we don't generate commitment updates against stale splice + // state. + if let Some(splice_locked) = inferred_splice_locked { + let result = self.internal_splice_locked_with_funded_channel( + counterparty_node_id, + &splice_locked, + chan, + &mut peer_state.in_flight_monitor_updates, + &mut peer_state.monitor_update_blocked_actions, + &mut peer_state.pending_msg_events, + peer_state.is_connected, + ); + try_channel_entry!(self, peer_state, result, chan_entry) + } else { + None + } } else { return try_channel_entry!(self, peer_state, Err(ChannelError::close( "Got a channel_reestablish message for an unfunded channel!".into())), chan_entry); @@ -13344,18 +13383,16 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ return Err(MsgHandleErrInternal::no_such_channel_for_peer(counterparty_node_id, msg.channel_id) ) } - } - }; - - self.handle_holding_cell_free_result(holding_cell_res); + }; - if let Some(channel_ready_msg) = need_lnd_workaround { - self.internal_channel_ready(counterparty_node_id, &channel_ready_msg)?; - } + let holding_cell_res = self.check_free_peer_holding_cells(peer_state); + (post_splice_locked_update, holding_cell_res) + }; - if let Some(splice_locked) = inferred_splice_locked { - self.internal_splice_locked(counterparty_node_id, &splice_locked)?; + if let Some(data) = post_splice_locked_update { + self.handle_post_monitor_update_chan_resume(data); } + self.handle_holding_cell_free_result(holding_cell_res); Ok(()) } @@ -13585,9 +13622,8 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; - // Look for the channel - match peer_state.channel_by_id.entry(msg.channel_id) { + let post_update_data = match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Vacant(_) => { return Err(MsgHandleErrInternal::no_such_channel_for_peer( counterparty_node_id, @@ -13596,73 +13632,16 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }, hash_map::Entry::Occupied(mut chan_entry) => { if let Some(chan) = chan_entry.get_mut().as_funded_mut() { - let logger = WithChannelContext::from(&self.logger, &chan.context, None); - let result = chan.splice_locked( + let result = self.internal_splice_locked_with_funded_channel( + counterparty_node_id, msg, - &self.node_signer, - self.chain_hash, - &self.config.read().unwrap(), - self.best_block.read().unwrap().height, - &&logger, + chan, + &mut peer_state.in_flight_monitor_updates, + &mut peer_state.monitor_update_blocked_actions, + &mut peer_state.pending_msg_events, + peer_state.is_connected, ); - let splice_promotion = try_channel_entry!(self, peer_state, result, chan_entry); - if let Some(splice_promotion) = splice_promotion { - { - let mut short_to_chan_info = self.short_to_chan_info.write().unwrap(); - insert_short_channel_id!(short_to_chan_info, chan); - } - - { - let mut pending_events = self.pending_events.lock().unwrap(); - pending_events.push_back(( - events::Event::ChannelReady { - channel_id: chan.context.channel_id(), - user_channel_id: chan.context.get_user_id(), - counterparty_node_id: chan.context.get_counterparty_node_id(), - funding_txo: Some( - splice_promotion.funding_txo.into_bitcoin_outpoint(), - ), - channel_type: chan.funding.get_channel_type().clone(), - }, - None, - )); - splice_promotion.discarded_funding.into_iter().for_each( - |funding_info| { - let event = Event::DiscardFunding { - channel_id: chan.context.channel_id(), - funding_info, - }; - pending_events.push_back((event, None)); - }, - ); - } - - if let Some(announcement_sigs) = splice_promotion.announcement_sigs { - log_trace!(logger, "Sending announcement_signatures",); - peer_state.pending_msg_events.push( - MessageSendEvent::SendAnnouncementSignatures { - node_id: counterparty_node_id.clone(), - msg: announcement_sigs, - }, - ); - } - - if let Some(monitor_update) = splice_promotion.monitor_update { - if let Some(data) = self.handle_new_monitor_update( - &mut peer_state.in_flight_monitor_updates, - &mut peer_state.monitor_update_blocked_actions, - &mut peer_state.pending_msg_events, - peer_state.is_connected, - chan, - splice_promotion.funding_txo, - monitor_update, - ) { - mem::drop(peer_state_lock); - mem::drop(per_peer_state); - self.handle_post_monitor_update_chan_resume(data); - } - } - } + try_channel_entry!(self, peer_state, result, chan_entry) } else { return Err(MsgHandleErrInternal::send_err_msg_no_close( "Channel is not funded, cannot splice".to_owned(), @@ -13671,10 +13650,87 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } }, }; + mem::drop(peer_state_lock); + mem::drop(per_peer_state); + + if let Some(data) = post_update_data { + self.handle_post_monitor_update_chan_resume(data); + } Ok(()) } + fn internal_splice_locked_with_funded_channel( + &self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceLocked, + chan: &mut FundedChannel, + in_flight_monitor_updates: &mut BTreeMap)>, + monitor_update_blocked_actions: &mut BTreeMap< + ChannelId, + Vec, + >, + pending_msg_events: &mut Vec, is_connected: bool, + ) -> Result, ChannelError> { + let logger = WithChannelContext::from(&self.logger, &chan.context, None); + let splice_promotion = chan.splice_locked( + msg, + &self.node_signer, + self.chain_hash, + &self.config.read().unwrap(), + self.best_block.read().unwrap().height, + &&logger, + )?; + let mut post_update_data = None; + if let Some(splice_promotion) = splice_promotion { + { + let mut short_to_chan_info = self.short_to_chan_info.write().unwrap(); + insert_short_channel_id!(short_to_chan_info, chan); + } + + { + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back(( + events::Event::ChannelReady { + channel_id: chan.context.channel_id(), + user_channel_id: chan.context.get_user_id(), + counterparty_node_id: chan.context.get_counterparty_node_id(), + funding_txo: Some(splice_promotion.funding_txo.into_bitcoin_outpoint()), + channel_type: chan.funding.get_channel_type().clone(), + }, + None, + )); + splice_promotion.discarded_funding.into_iter().for_each(|funding_info| { + let event = Event::DiscardFunding { + channel_id: chan.context.channel_id(), + funding_info, + }; + pending_events.push_back((event, None)); + }); + } + + if let Some(announcement_sigs) = splice_promotion.announcement_sigs { + log_trace!(logger, "Sending announcement_signatures",); + pending_msg_events.push(MessageSendEvent::SendAnnouncementSignatures { + node_id: counterparty_node_id.clone(), + msg: announcement_sigs, + }); + } + + if let Some(monitor_update) = splice_promotion.monitor_update { + post_update_data = self.handle_new_monitor_update( + in_flight_monitor_updates, + monitor_update_blocked_actions, + pending_msg_events, + is_connected, + chan, + splice_promotion.funding_txo, + monitor_update, + ); + } + } + + Ok(post_update_data) + } + /// Process pending events from the [`chain::Watch`], returning whether any events were processed. fn process_pending_monitor_events(&self) -> bool { debug_assert!(self.total_consistency_lock.try_write().is_err()); // Caller holds read lock diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index ca45a39c8cb..6e6af600faf 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -2794,6 +2794,91 @@ fn test_splice_confirms_on_both_sides_while_disconnected() { .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script); } +#[test] +fn test_holding_cell_claim_freed_after_inferred_splice_locked() { + // If `channel_reestablish` infers a missed `splice_locked`, it must promote the splice before + // freeing holding-cell updates. If the promotion monitor update is asynchronous, holding-cell + // updates must remain held until that monitor update completes. + 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]); + 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(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + let prev_funding_outpoint = get_monitor!(nodes[0], channel_id).get_funding_txo(); + let prev_funding_script = get_monitor!(nodes[0], channel_id).get_funding_script(); + let prev_scid = nodes[0].node.list_channels()[0].short_channel_id; + + let (payment_preimage, payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1]], 1_000_000); + + let outputs = vec![ + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ]; + 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); + + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + + nodes[1].node.claim_funds(payment_preimage); + check_added_monitors(&nodes[1], 1); + expect_payment_claimed!(nodes[1], payment_hash, 1_000_000); + + confirm_transaction(&nodes[0], &splice_tx); + confirm_transaction(&nodes[1], &splice_tx); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + + let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect_args.expect_renegotiated_funding_locked_monitor_update = (true, true); + reconnect_args.send_announcement_sigs = (true, true); + reconnect_nodes(reconnect_args); + + expect_channel_ready_event(&nodes[0], &node_id_1); + expect_channel_ready_event(&nodes[1], &node_id_0); + assert_ne!(prev_scid, nodes[0].node.list_channels()[0].short_channel_id); + + nodes[1].chain_monitor.complete_sole_pending_chan_update(&channel_id); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + + let mut commitment_update = get_htlc_update_msgs(&nodes[1], &node_id_0); + check_added_monitors(&nodes[1], 1); + nodes[0] + .node + .handle_update_fulfill_htlc(node_id_1, commitment_update.update_fulfill_htlcs.remove(0)); + do_commitment_signed_dance( + &nodes[0], + &nodes[1], + &commitment_update.commitment_signed, + false, + false, + ); + + expect_payment_sent!(nodes[0], payment_preimage); + + nodes[0] + .chain_source + .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script.clone()); + nodes[1] + .chain_source + .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script); +} + #[test] fn test_stale_announcement_signatures_ignored_after_splice_lock() { // Regression test: a peer may transmit `announcement_signatures` signed over a pre-splice