From 120a6776327023538744a4e8a987154f5e96265e Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Thu, 26 Jun 2025 10:06:28 -0700 Subject: [PATCH 1/4] Introduce RenegotiatedFundingLocked monitor update variant This is a new `ChannelMonitorUpdateStep` variant intended to be used whenever a new funding transaction that was negotiated and applied via the `RenegotiatedFunding` update reaches its intended confirmation depth and both sides of the channel exchange `channel_ready`/`splice_locked`. This commit primarily focuses on its use for splices, but future work will expand where needed to support RBFs for a dual funded channel. This monitor update ensures that the monitor can safely drop all prior commitment data since it is now considered invalid/unnecessary. Once the update is applied, only state for the new funding transaction is tracked going forward, until the monitor receives another `RenegotiatedFunding` update. --- lightning/src/chain/channelmonitor.rs | 51 ++++++- lightning/src/chain/onchaintx.rs | 9 ++ lightning/src/ln/channel.rs | 203 ++++++++++++++------------ lightning/src/ln/channelmanager.rs | 112 ++++++++++---- 4 files changed, 247 insertions(+), 128 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 616de1f0e3f..ee8253de2ca 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -678,6 +678,9 @@ pub(crate) enum ChannelMonitorUpdateStep { holder_commitment_tx: HolderCommitmentTransaction, counterparty_commitment_tx: CommitmentTransaction, }, + RenegotiatedFundingLocked { + funding_txid: Txid, + }, } impl ChannelMonitorUpdateStep { @@ -693,6 +696,7 @@ impl ChannelMonitorUpdateStep { ChannelMonitorUpdateStep::ChannelForceClosed { .. } => "ChannelForceClosed", ChannelMonitorUpdateStep::ShutdownScript { .. } => "ShutdownScript", ChannelMonitorUpdateStep::RenegotiatedFunding { .. } => "RenegotiatedFunding", + ChannelMonitorUpdateStep::RenegotiatedFundingLocked { .. } => "RenegotiatedFundingLocked", } } } @@ -741,6 +745,9 @@ impl_writeable_tlv_based_enum_upgradable!(ChannelMonitorUpdateStep, (3, holder_commitment_tx, required), (5, counterparty_commitment_tx, required), }, + (12, RenegotiatedFundingLocked) => { + (1, funding_txid, required), + }, ); /// Indicates whether the balance is derived from a cooperative close, a force-close @@ -1086,6 +1093,10 @@ impl FundingScope { fn funding_txid(&self) -> Txid { self.funding_outpoint().txid } + + fn is_splice(&self) -> bool { + self.channel_parameters.splice_parent_funding_txid.is_some() + } } impl_writeable_tlv_based!(FundingScope, { @@ -1181,8 +1192,6 @@ pub(crate) struct ChannelMonitorImpl { // interface knows about the TXOs that we want to be notified of spends of. We could probably // be smart and derive them from the above storage fields, but its much simpler and more // Obviously Correct (tm) if we just keep track of them explicitly. - // - // TODO: Remove entries for stale funding transactions on `splice_locked`. outputs_to_watch: HashMap>, #[cfg(any(test, feature = "_test_utils"))] @@ -3768,6 +3777,10 @@ impl ChannelMonitorImpl { ); return Err(()); } + } else if self.funding.is_splice() { + // If we've already spliced at least once, we're no longer able to RBF the original + // funding transaction. + return Err(()); } let script_pubkey = channel_parameters.make_funding_redeemscript().to_p2wsh(); @@ -3780,6 +3793,30 @@ impl ChannelMonitorImpl { Ok(()) } + fn promote_funding(&mut self, new_funding_txid: Txid) -> Result<(), ()> { + let new_funding = self + .pending_funding + .iter_mut() + .find(|funding| funding.funding_txid() == new_funding_txid); + if new_funding.is_none() { + return Err(()); + } + let mut new_funding = new_funding.unwrap(); + + mem::swap(&mut self.funding, &mut new_funding); + self.onchain_tx_handler.update_after_renegotiated_funding_locked( + self.funding.current_holder_commitment_tx.clone(), + self.funding.prev_holder_commitment_tx.clone(), + ); + + // The swap above places the previous `FundingScope` into `pending_funding`. + for funding in self.pending_funding.drain(..) { + self.outputs_to_watch.remove(&funding.funding_txid()); + } + + Ok(()) + } + #[rustfmt::skip] fn update_monitor( &mut self, updates: &ChannelMonitorUpdate, broadcaster: &B, fee_estimator: &F, logger: &WithChannelMonitor @@ -3898,6 +3935,13 @@ impl ChannelMonitorImpl { ret = Err(()); } }, + ChannelMonitorUpdateStep::RenegotiatedFundingLocked { funding_txid } => { + log_trace!(logger, "Updating ChannelMonitor with locked renegotiated funding txid {}", funding_txid); + if let Err(_) = self.promote_funding(*funding_txid) { + log_error!(logger, "Unknown funding with txid {} became locked", funding_txid); + ret = Err(()); + } + }, ChannelMonitorUpdateStep::ChannelForceClosed { should_broadcast } => { log_trace!(logger, "Updating ChannelMonitor: channel force closed, should broadcast: {}", should_broadcast); self.lockdown_from_offchain = true; @@ -3951,7 +3995,8 @@ impl ChannelMonitorImpl { |ChannelMonitorUpdateStep::LatestCounterpartyCommitment { .. } |ChannelMonitorUpdateStep::ShutdownScript { .. } |ChannelMonitorUpdateStep::CommitmentSecret { .. } - |ChannelMonitorUpdateStep::RenegotiatedFunding { .. } => + |ChannelMonitorUpdateStep::RenegotiatedFunding { .. } + |ChannelMonitorUpdateStep::RenegotiatedFundingLocked { .. } => is_pre_close_update = true, // After a channel is closed, we don't communicate with our peer about it, so the // only things we will update is getting a new preimage (from a different channel) diff --git a/lightning/src/chain/onchaintx.rs b/lightning/src/chain/onchaintx.rs index 0b63f1f47f8..f7c2bac3a39 100644 --- a/lightning/src/chain/onchaintx.rs +++ b/lightning/src/chain/onchaintx.rs @@ -1238,6 +1238,15 @@ impl OnchainTxHandler { self.prev_holder_commitment = Some(replace(&mut self.holder_commitment, tx)); } + /// Replaces the current/prev holder commitment transactions spending the currently confirmed + /// funding outpoint with those spending the new funding outpoint. + pub(crate) fn update_after_renegotiated_funding_locked( + &mut self, current: HolderCommitmentTransaction, prev: Option, + ) { + self.holder_commitment = current; + self.prev_holder_commitment = prev; + } + // Deprecated as of 0.2, only use in cases where it was not previously available. pub(crate) fn channel_parameters(&self) -> &ChannelTransactionParameters { &self.channel_transaction_parameters diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index fbcc4b01954..169ad06f2d3 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -5975,6 +5975,13 @@ type BestBlockUpdatedRes = ( Option, ); +#[cfg(splicing)] +pub struct SpliceFundingPromotion { + pub funding_txo: OutPoint, + pub monitor_update: Option, + pub announcement_sigs: Option, +} + impl FundedChannel where SP::Target: SignerProvider, @@ -9643,46 +9650,38 @@ where self.context.check_funding_meets_minimum_depth(funding, height) } + /// Returns `Some` if a splice [`FundingScope`] was promoted. #[cfg(splicing)] - fn maybe_promote_splice_funding( - &mut self, confirmed_funding_index: usize, logger: &L, - ) -> bool + fn maybe_promote_splice_funding( + &mut self, node_signer: &NS, chain_hash: ChainHash, user_config: &UserConfig, + block_height: u32, logger: &L, + ) -> Option where + NS::Target: NodeSigner, L::Target: Logger, { debug_assert!(self.pending_splice.is_some()); - debug_assert!(confirmed_funding_index < self.pending_funding.len()); let pending_splice = self.pending_splice.as_mut().unwrap(); let splice_txid = match pending_splice.sent_funding_txid { Some(sent_funding_txid) => sent_funding_txid, None => { debug_assert!(false); - return false; + return None; }, }; - if pending_splice.sent_funding_txid == pending_splice.received_funding_txid { - log_info!( - logger, - "Promoting splice funding txid {} for channel {}", - splice_txid, - &self.context.channel_id, - ); - - let funding = self.pending_funding.get_mut(confirmed_funding_index).unwrap(); - debug_assert_eq!(Some(splice_txid), funding.get_funding_txid()); - promote_splice_funding!(self, funding); - - return true; - } else if let Some(received_funding_txid) = pending_splice.received_funding_txid { - log_warn!( - logger, - "Mismatched splice_locked txid for channel {}; sent txid {}; received txid {}", - &self.context.channel_id, - splice_txid, - received_funding_txid, - ); + if let Some(received_funding_txid) = pending_splice.received_funding_txid { + if splice_txid != received_funding_txid { + log_warn!( + logger, + "Mismatched splice_locked txid for channel {}; sent txid {}; received txid {}", + &self.context.channel_id, + splice_txid, + received_funding_txid, + ); + return None; + } } else { log_info!( logger, @@ -9690,9 +9689,47 @@ where splice_txid, &self.context.channel_id, ); + return None; } - return false; + log_info!( + logger, + "Promoting splice funding txid {} for channel {}", + splice_txid, + &self.context.channel_id, + ); + + { + // Scope `funding` since it is swapped within `promote_splice_funding` and we don't want + // to unintentionally use it. + let funding = self + .pending_funding + .iter_mut() + .find(|funding| funding.get_funding_txid() == Some(splice_txid)) + .unwrap(); + promote_splice_funding!(self, funding); + } + + let funding_txo = self + .funding + .get_funding_txo() + .expect("Splice FundingScope should always have a funding_txo"); + + self.context.latest_monitor_update_id += 1; + let monitor_update = ChannelMonitorUpdate { + update_id: self.context.latest_monitor_update_id, + updates: vec![ChannelMonitorUpdateStep::RenegotiatedFundingLocked { + funding_txid: funding_txo.txid, + }], + channel_id: Some(self.context.channel_id()), + }; + self.monitor_updating_paused(false, false, false, Vec::new(), Vec::new(), Vec::new()); + let monitor_update = self.push_ret_blockable_mon_update(monitor_update); + + let announcement_sigs = + self.get_announcement_sigs(node_signer, chain_hash, user_config, block_height, logger); + + Some(SpliceFundingPromotion { funding_txo, monitor_update, announcement_sigs }) } /// When a transaction is confirmed, we check whether it is or spends the funding transaction @@ -9786,18 +9823,16 @@ where &self.context.channel_id, ); - let funding_promoted = - self.maybe_promote_splice_funding(confirmed_funding_index, logger); - let funding_txo = funding_promoted.then(|| { - self.funding - .get_funding_txo() - .expect("Splice FundingScope should always have a funding_txo") - }); - let announcement_sigs = funding_promoted - .then(|| self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger)) - .flatten(); + let (funding_txo, monitor_update, announcement_sigs) = + self.maybe_promote_splice_funding( + node_signer, chain_hash, user_config, height, logger, + ).map(|splice_promotion| ( + Some(splice_promotion.funding_txo), + splice_promotion.monitor_update, + splice_promotion.announcement_sigs, + )).unwrap_or((None, None, None)); - return Ok((Some(FundingConfirmedMessage::Splice(splice_locked, funding_txo)), announcement_sigs)); + return Ok((Some(FundingConfirmedMessage::Splice(splice_locked, funding_txo, monitor_update)), announcement_sigs)); } } @@ -9954,23 +9989,22 @@ where let funding = self.pending_funding.get(confirmed_funding_index).unwrap(); if let Some(splice_locked) = pending_splice.check_get_splice_locked(&self.context, funding, height) { log_info!(logger, "Sending a splice_locked to our peer for channel {}", &self.context.channel_id); + debug_assert!(chain_node_signer.is_some()); - let funding_promoted = - self.maybe_promote_splice_funding(confirmed_funding_index, logger); - let funding_txo = funding_promoted.then(|| { - self.funding - .get_funding_txo() - .expect("Splice FundingScope should always have a funding_txo") - }); - let announcement_sigs = funding_promoted - .then(|| chain_node_signer - .and_then(|(chain_hash, node_signer, user_config)| - self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger) - ) - ) - .flatten(); + let (funding_txo, monitor_update, announcement_sigs) = chain_node_signer + .and_then(|(chain_hash, node_signer, user_config)| { + // We can only promote on blocks connected, which is when we expect + // `chain_node_signer` to be `Some`. + self.maybe_promote_splice_funding(node_signer, chain_hash, user_config, height, logger) + }) + .map(|splice_promotion| ( + Some(splice_promotion.funding_txo), + splice_promotion.monitor_update, + splice_promotion.announcement_sigs, + )) + .unwrap_or((None, None, None)); - return Ok((Some(FundingConfirmedMessage::Splice(splice_locked, funding_txo)), timed_out_htlcs, announcement_sigs)); + return Ok((Some(FundingConfirmedMessage::Splice(splice_locked, funding_txo, monitor_update)), timed_out_htlcs, announcement_sigs)); } } @@ -10464,8 +10498,8 @@ where #[cfg(splicing)] pub fn splice_locked( &mut self, msg: &msgs::SpliceLocked, node_signer: &NS, chain_hash: ChainHash, - user_config: &UserConfig, best_block: &BestBlock, logger: &L, - ) -> Result<(Option, Option), ChannelError> + user_config: &UserConfig, block_height: u32, logger: &L, + ) -> Result, ChannelError> where NS::Target: NodeSigner, L::Target: Logger, @@ -10484,56 +10518,33 @@ where }, }; - if let Some(sent_funding_txid) = pending_splice.sent_funding_txid { - if sent_funding_txid == msg.splice_txid { - if let Some(funding) = self - .pending_funding - .iter_mut() - .find(|funding| funding.get_funding_txid() == Some(sent_funding_txid)) - { - log_info!( - logger, - "Promoting splice funding txid {} for channel {}", - msg.splice_txid, - &self.context.channel_id, - ); - promote_splice_funding!(self, funding); - let funding_txo = self - .funding - .get_funding_txo() - .expect("Splice FundingScope should always have a funding_txo"); - let announcement_sigs = self.get_announcement_sigs( - node_signer, - chain_hash, - user_config, - best_block.height, - logger, - ); - return Ok((Some(funding_txo), announcement_sigs)); - } + if !self + .pending_funding + .iter() + .any(|funding| funding.get_funding_txid() == Some(msg.splice_txid)) + { + let err = "unknown splice funding txid"; + return Err(ChannelError::close(err.to_string())); + } + pending_splice.received_funding_txid = Some(msg.splice_txid); - let err = "unknown splice funding txid"; - return Err(ChannelError::close(err.to_string())); - } else { - log_warn!( - logger, - "Mismatched splice_locked txid for channel {}; sent txid {}; received txid {}", - &self.context.channel_id, - sent_funding_txid, - msg.splice_txid, - ); - } - } else { + if pending_splice.sent_funding_txid.is_none() { log_info!( logger, "Waiting for enough confirmations to send splice_locked txid {} for channel {}", msg.splice_txid, &self.context.channel_id, ); + return Ok(None); } - pending_splice.received_funding_txid = Some(msg.splice_txid); - Ok((None, None)) + Ok(self.maybe_promote_splice_funding( + node_signer, + chain_hash, + user_config, + block_height, + logger, + )) } // Send stuff to our remote peers: diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 2c9c917053b..f0d0384e13a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -10715,15 +10715,15 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &self.node_signer, self.chain_hash, &self.default_configuration, - &self.best_block.read().unwrap(), + self.best_block.read().unwrap().height, &&logger, ); - let (funding_txo, announcement_sigs_opt) = - try_channel_entry!(self, peer_state, result, chan_entry); - - if funding_txo.is_some() { - let mut short_to_chan_info = self.short_to_chan_info.write().unwrap(); - insert_short_channel_id!(short_to_chan_info, chan); + 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(( @@ -10731,26 +10731,39 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ 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: funding_txo - .map(|outpoint| outpoint.into_bitcoin_outpoint()), + funding_txo: Some( + splice_promotion.funding_txo.into_bitcoin_outpoint(), + ), channel_type: chan.funding.get_channel_type().clone(), }, None, )); - } - if let Some(announcement_sigs) = announcement_sigs_opt { - log_trace!( - logger, - "Sending announcement_signatures for channel {}", - chan.context.channel_id() - ); - peer_state.pending_msg_events.push( - MessageSendEvent::SendAnnouncementSignatures { - node_id: counterparty_node_id.clone(), - msg: announcement_sigs, - }, - ); + if let Some(announcement_sigs) = splice_promotion.announcement_sigs { + log_trace!( + logger, + "Sending announcement_signatures for channel {}", + chan.context.channel_id() + ); + 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 { + handle_new_monitor_update!( + self, + splice_promotion.funding_txo, + monitor_update, + peer_state_lock, + peer_state, + per_peer_state, + chan + ); + } } } else { return Err(MsgHandleErrInternal::send_err_msg_no_close( @@ -12901,7 +12914,7 @@ where pub(super) enum FundingConfirmedMessage { Establishment(msgs::ChannelReady), #[cfg(splicing)] - Splice(msgs::SpliceLocked, Option), + Splice(msgs::SpliceLocked, Option, Option), } impl< @@ -12938,6 +12951,8 @@ where let mut failed_channels: Vec<(Result, _)> = Vec::new(); let mut timed_out_htlcs = Vec::new(); + #[cfg(splicing)] + let mut to_process_monitor_update_actions = Vec::new(); { let per_peer_state = self.per_peer_state.read().unwrap(); for (counterparty_node_id, peer_state_mutex) in per_peer_state.iter() { @@ -12975,23 +12990,40 @@ where } }, #[cfg(splicing)] - Some(FundingConfirmedMessage::Splice(splice_locked, funding_txo)) => { - if funding_txo.is_some() { + Some(FundingConfirmedMessage::Splice(splice_locked, funding_txo, monitor_update_opt)) => { + let counterparty_node_id = funded_channel.context.get_counterparty_node_id(); + let channel_id = funded_channel.context.channel_id(); + + if let Some(funding_txo) = funding_txo { let mut short_to_chan_info = self.short_to_chan_info.write().unwrap(); insert_short_channel_id!(short_to_chan_info, funded_channel); + if let Some(monitor_update) = monitor_update_opt { + handle_new_monitor_update!( + self, + funding_txo, + monitor_update, + peer_state, + funded_channel.context, + REMAIN_LOCKED_UPDATE_ACTIONS_PROCESSED_LATER + ); + to_process_monitor_update_actions.push(( + counterparty_node_id, channel_id + )); + } + let mut pending_events = self.pending_events.lock().unwrap(); pending_events.push_back((events::Event::ChannelReady { - channel_id: funded_channel.context.channel_id(), + channel_id, user_channel_id: funded_channel.context.get_user_id(), - counterparty_node_id: funded_channel.context.get_counterparty_node_id(), - funding_txo: funding_txo.map(|outpoint| outpoint.into_bitcoin_outpoint()), + counterparty_node_id, + funding_txo: Some(funding_txo.into_bitcoin_outpoint()), channel_type: funded_channel.funding.get_channel_type().clone(), }, None)); } pending_msg_events.push(MessageSendEvent::SendSpliceLocked { - node_id: funded_channel.context.get_counterparty_node_id(), + node_id: counterparty_node_id, msg: splice_locked, }); }, @@ -13082,6 +13114,28 @@ where } } + #[cfg(splicing)] + for (counterparty_node_id, channel_id) in to_process_monitor_update_actions { + let per_peer_state = self.per_peer_state.read().unwrap(); + if let Some(peer_state_mutex) = per_peer_state.get(&counterparty_node_id) { + let mut peer_state = peer_state_mutex.lock().unwrap(); + if peer_state + .in_flight_monitor_updates + .get(&channel_id) + .map(|(_, updates)| updates.is_empty()) + .unwrap_or(true) + { + let update_actions = peer_state + .monitor_update_blocked_actions + .remove(&channel_id) + .unwrap_or_else(|| Vec::new()); + mem::drop(peer_state); + mem::drop(per_peer_state); + self.handle_monitor_update_completion_actions(update_actions); + } + } + } + if let Some(height) = height_opt { self.claimable_payments.lock().unwrap().claimable_payments.retain(|payment_hash, payment| { payment.htlcs.retain(|htlc| { From 54a6e0d3e437954938f80f409d0dbea8942f6725 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 15 Jul 2025 17:25:59 -0700 Subject: [PATCH 2/4] Rename ChannelMonitor::first_confirmed_funding_txo It's only intended to be set during initialization and used to check if the channel is v1 or v2. We rename it to `first_negotiated_funding_txo` to better reflect its purpose. --- lightning/src/chain/channelmonitor.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index ee8253de2ca..43509a403a3 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -1124,7 +1124,7 @@ pub(crate) struct ChannelMonitorImpl { channel_keys_id: [u8; 32], holder_revocation_basepoint: RevocationBasepoint, channel_id: ChannelId, - first_confirmed_funding_txo: OutPoint, + first_negotiated_funding_txo: OutPoint, counterparty_commitment_params: CounterpartyCommitmentParameters, @@ -1549,7 +1549,7 @@ impl Writeable for ChannelMonitorImpl { (21, self.balances_empty_height, option), (23, self.holder_pays_commitment_tx_fee, option), (25, self.payment_preimages, required), - (27, self.first_confirmed_funding_txo, required), + (27, self.first_negotiated_funding_txo, required), (29, self.initial_counterparty_commitment_tx, option), (31, self.funding.channel_parameters, required), (32, self.pending_funding, optional_vec), @@ -1733,7 +1733,7 @@ impl ChannelMonitor { channel_keys_id, holder_revocation_basepoint, channel_id, - first_confirmed_funding_txo: funding_outpoint, + first_negotiated_funding_txo: funding_outpoint, counterparty_commitment_params, their_cur_per_commitment_points: None, @@ -1792,7 +1792,7 @@ impl ChannelMonitor { /// [`Persist`]: crate::chain::chainmonitor::Persist pub fn persistence_key(&self) -> MonitorName { let inner = self.inner.lock().unwrap(); - let funding_outpoint = inner.first_confirmed_funding_txo; + let funding_outpoint = inner.first_negotiated_funding_txo; let channel_id = inner.channel_id; if ChannelId::v1_from_funding_outpoint(funding_outpoint) == channel_id { MonitorName::V1Channel(funding_outpoint) @@ -5865,7 +5865,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP let mut channel_id = None; let mut holder_pays_commitment_tx_fee = None; let mut payment_preimages_with_info: Option> = None; - let mut first_confirmed_funding_txo = RequiredWrapper(None); + let mut first_negotiated_funding_txo = RequiredWrapper(None); let mut channel_parameters = None; let mut pending_funding = None; read_tlv_fields!(reader, { @@ -5882,7 +5882,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP (21, balances_empty_height, option), (23, holder_pays_commitment_tx_fee, option), (25, payment_preimages_with_info, option), - (27, first_confirmed_funding_txo, (default_value, outpoint)), + (27, first_negotiated_funding_txo, (default_value, outpoint)), (29, initial_counterparty_commitment_tx, option), (31, channel_parameters, (option: ReadableArgs, None)), (32, pending_funding, optional_vec), @@ -6012,7 +6012,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP channel_keys_id, holder_revocation_basepoint, channel_id, - first_confirmed_funding_txo: first_confirmed_funding_txo.0.unwrap(), + first_negotiated_funding_txo: first_negotiated_funding_txo.0.unwrap(), counterparty_commitment_params, their_cur_per_commitment_points, From 0e7e0e7662e00c6d95482a93ca2bd8a83a54457d Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 16 Jul 2025 08:25:31 -0700 Subject: [PATCH 3/4] Detect channel spend by splice transaction A `ChannelMonitor` will always consider a channel closed once a confirmed spend for the funding transaction is detected. This is no longer the case with splicing, as the channel will remain open and capable of accepting updates while its funding transaction is being replaced. --- lightning/src/chain/channelmonitor.rs | 70 +++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 43509a403a3..320b209cea6 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -565,6 +565,12 @@ enum OnchainEvent { /// output (and generate a SpendableOutput event). on_to_local_output_csv: Option, }, + /// The txid of an alternative funding transaction (due to a splice) that has confirmed but is + /// not yet locked, invalidating the previous funding transaction as it now spent. Note that we + /// wait to promote the corresponding `FundingScope` until we see a + /// [`ChannelMonitorUpdateStep::RenegotiatedFundingLocked`] or if the alternative funding + /// transaction is irrevocably confirmed. + AlternativeFundingConfirmation {}, } impl Writeable for OnchainEventEntry { @@ -609,6 +615,7 @@ impl_writeable_tlv_based_enum_upgradable!(OnchainEvent, (1, MaturingOutput) => { (0, descriptor, required), }, + (2, AlternativeFundingConfirmation) => {}, (3, FundingSpendConfirmation) => { (0, on_local_output_csv, option), (1, commitment_tx_to_counterparty_output, option), @@ -618,7 +625,6 @@ impl_writeable_tlv_based_enum_upgradable!(OnchainEvent, (2, preimage, option), (4, on_to_local_output_csv, option), }, - ); #[derive(Clone, Debug, PartialEq, Eq)] @@ -4871,6 +4877,49 @@ impl ChannelMonitorImpl { } } + // A splice transaction has confirmed. We can't promote the splice's scope until we see + // the corresponding monitor update for it, but we track the txid so we know which + // holder commitment transaction we may need to broadcast. + if let Some(alternative_funding) = self.pending_funding.iter() + .find(|funding| funding.funding_txid() == txid) + { + debug_assert!(self.funding_spend_confirmed.is_none()); + debug_assert!( + !self.onchain_events_awaiting_threshold_conf.iter() + .any(|e| matches!(e.event, OnchainEvent::FundingSpendConfirmation { .. })) + ); + debug_assert_eq!( + self.funding.funding_outpoint().into_bitcoin_outpoint(), + tx.input[0].previous_output + ); + + let (desc, msg) = if alternative_funding.channel_parameters.splice_parent_funding_txid.is_some() { + ("Splice", "splice_locked") + } else { + ("RBF", "channel_ready") + }; + let action = if self.no_further_updates_allowed() { + if self.holder_tx_signed { + ", broadcasting post-splice holder commitment transaction".to_string() + } else { + "".to_string() + } + } else { + format!(", waiting for `{}` exchange", msg) + }; + log_info!(logger, "{desc} for channel {} confirmed with txid {txid}{action}", self.channel_id()); + + self.onchain_events_awaiting_threshold_conf.push(OnchainEventEntry { + txid, + transaction: Some((*tx).clone()), + height, + block_hash: Some(block_hash), + event: OnchainEvent::AlternativeFundingConfirmation {}, + }); + + continue 'tx_iter; + } + if tx.input.len() == 1 { // Assuming our keys were not leaked (in which case we're screwed no matter what), // commitment transactions and HTLC transactions will all only ever have one input @@ -5002,7 +5051,7 @@ impl ChannelMonitorImpl { let unmatured_htlcs: Vec<_> = self.onchain_events_awaiting_threshold_conf .iter() .filter_map(|entry| match &entry.event { - OnchainEvent::HTLCUpdate { source, .. } => Some(source), + OnchainEvent::HTLCUpdate { source, .. } => Some(source.clone()), _ => None, }) .collect(); @@ -5017,7 +5066,7 @@ impl ChannelMonitorImpl { #[cfg(debug_assertions)] { debug_assert!( - !unmatured_htlcs.contains(&&source), + !unmatured_htlcs.contains(&source), "An unmature HTLC transaction conflicts with a maturing one; failed to \ call either transaction_unconfirmed for the conflicting transaction \ or block_disconnected for a block containing it."); @@ -5064,6 +5113,21 @@ impl ChannelMonitorImpl { self.funding_spend_confirmed = Some(entry.txid); self.confirmed_commitment_tx_counterparty_output = commitment_tx_to_counterparty_output; }, + OnchainEvent::AlternativeFundingConfirmation {} => { + // An alternative funding transaction has irrevocably confirmed. Locate the + // corresponding scope and promote it if the monitor is no longer allowing + // updates. Otherwise, we expect it to be promoted via + // [`ChannelMonitorUpdateStep::RenegotiatedFundingLocked`]. + if self.no_further_updates_allowed() { + let funding_txid = entry.transaction + .expect("Transactions are always present for AlternativeFundingConfirmation entries") + .compute_txid(); + debug_assert_ne!(self.funding.funding_txid(), funding_txid); + if let Err(_) = self.promote_funding(funding_txid) { + log_error!(logger, "Missing scope for alternative funding confirmation with txid {}", entry.txid); + } + } + }, } } From a8ae4b7e7607036c13a1129e913fe1dd45744658 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 16 Jul 2025 14:25:03 -0700 Subject: [PATCH 4/4] Broadcast holder commitment for currently confirmed funding A splice's `FundingScope` can only be promoted once a `ChannelMonitorUpdateStep::RenegotiatedFundingLocked` is applied, or if the monitor is no longer accepting updates, once the splice transaction is no longer under reorg risk. Because of this, our current `FundingScope` may not reflect the latest confirmed state in the chain. Before making a holder commitment broadcast, we must check which `FundingScope` is currently confirmed to ensure that it can propogate throughout the network. --- lightning/src/chain/channelmonitor.rs | 244 +++++++++++++++++++------- lightning/src/chain/onchaintx.rs | 23 ++- 2 files changed, 196 insertions(+), 71 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 320b209cea6..c07d018aafa 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -1103,6 +1103,10 @@ impl FundingScope { fn is_splice(&self) -> bool { self.channel_parameters.splice_parent_funding_txid.is_some() } + + fn channel_type_features(&self) -> &ChannelTypeFeatures { + &self.channel_parameters.channel_type_features + } } impl_writeable_tlv_based!(FundingScope, { @@ -3600,7 +3604,7 @@ impl ChannelMonitorImpl { // Assume that the broadcasted commitment transaction confirmed in the current best // block. Even if not, its a reasonable metric for the bump criteria on the HTLC // transactions. - let (claim_reqs, _) = self.get_broadcasted_holder_claims(holder_commitment_tx, self.best_block.height); + let (claim_reqs, _) = self.get_broadcasted_holder_claims(&self.funding, holder_commitment_tx, self.best_block.height); let conf_target = self.closure_conf_target(); self.onchain_tx_handler.update_claims_view_from_requests( claim_reqs, self.best_block.height, self.best_block.height, broadcaster, @@ -3611,25 +3615,39 @@ impl ChannelMonitorImpl { } #[rustfmt::skip] - fn generate_claimable_outpoints_and_watch_outputs(&mut self, reason: ClosureReason) -> (Vec, Vec) { - let holder_commitment_tx = &self.funding.current_holder_commitment_tx; + fn generate_claimable_outpoints_and_watch_outputs( + &mut self, generate_monitor_event_with_reason: Option, + ) -> (Vec, Vec) { + let funding = self.onchain_events_awaiting_threshold_conf + .iter() + .find(|entry| matches!(entry.event, OnchainEvent::AlternativeFundingConfirmation {})) + .and_then(|entry| entry.transaction.as_ref().map(|tx| tx.compute_txid())) + .and_then(|alternative_funding_txid| + self.pending_funding + .iter() + .find(|funding| funding.funding_txid() == alternative_funding_txid) + ) + .unwrap_or(&self.funding); + let holder_commitment_tx = &funding.current_holder_commitment_tx; let funding_outp = HolderFundingOutput::build( holder_commitment_tx.clone(), - self.funding.channel_parameters.clone(), + funding.channel_parameters.clone(), ); - let funding_outpoint = self.get_funding_txo(); + let funding_outpoint = funding.funding_outpoint(); let commitment_package = PackageTemplate::build_package( funding_outpoint.txid.clone(), funding_outpoint.index as u32, PackageSolvingData::HolderFundingOutput(funding_outp), self.best_block.height, ); let mut claimable_outpoints = vec![commitment_package]; - let event = MonitorEvent::HolderForceClosedWithInfo { - reason, - outpoint: funding_outpoint, - channel_id: self.channel_id, - }; - self.pending_monitor_events.push(event); + if let Some(reason) = generate_monitor_event_with_reason { + let event = MonitorEvent::HolderForceClosedWithInfo { + reason, + outpoint: funding_outpoint, + channel_id: self.channel_id, + }; + self.pending_monitor_events.push(event); + } // Although we aren't signing the transaction directly here, the transaction will be signed // in the claim that is queued to OnchainTxHandler. We set holder_tx_signed here to reject @@ -3639,12 +3657,12 @@ impl ChannelMonitorImpl { // We can't broadcast our HTLC transactions while the commitment transaction is // unconfirmed. We'll delay doing so until we detect the confirmed commitment in // `transactions_confirmed`. - if !self.channel_type_features().supports_anchors_zero_fee_htlc_tx() { + if !funding.channel_type_features().supports_anchors_zero_fee_htlc_tx() { // Because we're broadcasting a commitment transaction, we should construct the package // assuming it gets confirmed in the next block. Sadly, we have code which considers // "not yet confirmed" things as discardable, so we cannot do that here. let (mut new_outpoints, _) = self.get_broadcasted_holder_claims( - holder_commitment_tx, self.best_block.height, + &funding, holder_commitment_tx, self.best_block.height, ); let new_outputs = self.get_broadcasted_holder_watch_outputs(holder_commitment_tx); if !new_outputs.is_empty() { @@ -3668,7 +3686,7 @@ impl ChannelMonitorImpl { broadcasted_latest_txn: Some(true), message: "ChannelMonitor-initiated commitment transaction broadcast".to_owned(), }; - let (claimable_outpoints, _) = self.generate_claimable_outpoints_and_watch_outputs(reason); + let (claimable_outpoints, _) = self.generate_claimable_outpoints_and_watch_outputs(Some(reason)); let conf_target = self.closure_conf_target(); self.onchain_tx_handler.update_claims_view_from_requests( claimable_outpoints, self.best_block.height, self.best_block.height, broadcaster, @@ -4511,7 +4529,7 @@ impl ChannelMonitorImpl { #[rustfmt::skip] fn get_broadcasted_holder_htlc_descriptors( - &self, holder_tx: &HolderCommitmentTransaction, + &self, funding: &FundingScope, holder_tx: &HolderCommitmentTransaction, ) -> Vec { let tx = holder_tx.trust(); let mut htlcs = Vec::with_capacity(holder_tx.nondust_htlcs().len()); @@ -4529,11 +4547,10 @@ impl ChannelMonitorImpl { }; htlcs.push(HTLCDescriptor { - // TODO(splicing): Consider alternative funding scopes. channel_derivation_parameters: ChannelDerivationParameters { - value_satoshis: self.funding.channel_parameters.channel_value_satoshis, + value_satoshis: funding.channel_parameters.channel_value_satoshis, keys_id: self.channel_keys_id, - transaction_parameters: self.funding.channel_parameters.clone(), + transaction_parameters: funding.channel_parameters.clone(), }, commitment_txid: tx.txid(), per_commitment_number: tx.commitment_number(), @@ -4553,7 +4570,7 @@ impl ChannelMonitorImpl { // script so we can detect whether a holder transaction has been seen on-chain. #[rustfmt::skip] fn get_broadcasted_holder_claims( - &self, holder_tx: &HolderCommitmentTransaction, conf_height: u32, + &self, funding: &FundingScope, holder_tx: &HolderCommitmentTransaction, conf_height: u32, ) -> (Vec, Option<(ScriptBuf, PublicKey, RevocationKey)>) { let tx = holder_tx.trust(); let keys = tx.keys(); @@ -4564,7 +4581,7 @@ impl ChannelMonitorImpl { redeem_script.to_p2wsh(), holder_tx.per_commitment_point(), keys.revocation_key.clone(), )); - let claim_requests = self.get_broadcasted_holder_htlc_descriptors(holder_tx).into_iter() + let claim_requests = self.get_broadcasted_holder_htlc_descriptors(funding, holder_tx).into_iter() .map(|htlc_descriptor| { let counterparty_spendable_height = if htlc_descriptor.htlc.offered { conf_height @@ -4631,7 +4648,8 @@ impl ChannelMonitorImpl { is_holder_tx = true; log_info!(logger, "Got broadcast of latest holder commitment tx {}, searching for available HTLCs to claim", commitment_txid); let holder_commitment_tx = &self.funding.current_holder_commitment_tx; - let res = self.get_broadcasted_holder_claims(holder_commitment_tx, height); + let res = + self.get_broadcasted_holder_claims(&self.funding, holder_commitment_tx, height); let mut to_watch = self.get_broadcasted_holder_watch_outputs(holder_commitment_tx); append_onchain_update!(res, to_watch); fail_unbroadcast_htlcs!( @@ -4648,7 +4666,8 @@ impl ChannelMonitorImpl { if holder_commitment_tx.trust().txid() == commitment_txid { is_holder_tx = true; log_info!(logger, "Got broadcast of previous holder commitment tx {}, searching for available HTLCs to claim", commitment_txid); - let res = self.get_broadcasted_holder_claims(holder_commitment_tx, height); + let res = + self.get_broadcasted_holder_claims(&self.funding, holder_commitment_tx, height); let mut to_watch = self.get_broadcasted_holder_watch_outputs(holder_commitment_tx); append_onchain_update!(res, to_watch); fail_unbroadcast_htlcs!( @@ -4684,45 +4703,63 @@ impl ChannelMonitorImpl { } // If we have generated claims for counterparty_commitment_txid earlier, we can rely on always // having claim related htlcs for counterparty_commitment_txid in counterparty_claimable_outpoints. - for (htlc, _) in self.funding.counterparty_claimable_outpoints.get(counterparty_commitment_txid).unwrap_or(&vec![]) { - log_trace!(logger, "Canceling claims for previously confirmed counterparty commitment {}", - counterparty_commitment_txid); - let mut outpoint = BitcoinOutPoint { txid: *counterparty_commitment_txid, vout: 0 }; - if let Some(vout) = htlc.transaction_output_index { - outpoint.vout = vout; - self.onchain_tx_handler.abandon_claim(&outpoint); + for funding in core::iter::once(&self.funding).chain(self.pending_funding.iter()) { + let mut found_claim = false; + for (htlc, _) in funding.counterparty_claimable_outpoints.get(counterparty_commitment_txid).unwrap_or(&vec![]) { + let mut outpoint = BitcoinOutPoint { txid: *counterparty_commitment_txid, vout: 0 }; + if let Some(vout) = htlc.transaction_output_index { + outpoint.vout = vout; + if self.onchain_tx_handler.abandon_claim(&outpoint) { + found_claim = true; + } + } + } + if found_claim { + log_trace!(logger, "Canceled claims for previously confirmed counterparty commitment with txid {counterparty_commitment_txid}"); } } } // Cancel any pending claims for any holder commitments in case they had previously // confirmed or been signed (in which case we will start attempting to claim without // waiting for confirmation). - if self.funding.current_holder_commitment_tx.trust().txid() != *confirmed_commitment_txid { - let txid = self.funding.current_holder_commitment_tx.trust().txid(); - log_trace!(logger, "Canceling claims for previously broadcast holder commitment {}", txid); - let mut outpoint = BitcoinOutPoint { txid, vout: 0 }; - for htlc in self.funding.current_holder_commitment_tx.nondust_htlcs() { - if let Some(vout) = htlc.transaction_output_index { - outpoint.vout = vout; - self.onchain_tx_handler.abandon_claim(&outpoint); - } else { - debug_assert!(false, "Expected transaction output index for non-dust HTLC"); - } - } - } - if let Some(prev_holder_commitment_tx) = &self.funding.prev_holder_commitment_tx { - let txid = prev_holder_commitment_tx.trust().txid(); - if txid != *confirmed_commitment_txid { - log_trace!(logger, "Canceling claims for previously broadcast holder commitment {}", txid); + for funding in core::iter::once(&self.funding).chain(self.pending_funding.iter()) { + if funding.current_holder_commitment_tx.trust().txid() != *confirmed_commitment_txid { + let mut found_claim = false; + let txid = funding.current_holder_commitment_tx.trust().txid(); let mut outpoint = BitcoinOutPoint { txid, vout: 0 }; - for htlc in prev_holder_commitment_tx.nondust_htlcs() { + for htlc in funding.current_holder_commitment_tx.nondust_htlcs() { if let Some(vout) = htlc.transaction_output_index { outpoint.vout = vout; - self.onchain_tx_handler.abandon_claim(&outpoint); + if self.onchain_tx_handler.abandon_claim(&outpoint) { + found_claim = true; + } } else { debug_assert!(false, "Expected transaction output index for non-dust HTLC"); } } + if found_claim { + log_trace!(logger, "Canceled claims for previously broadcast holder commitment with txid {txid}"); + } + } + if let Some(prev_holder_commitment_tx) = &funding.prev_holder_commitment_tx { + let txid = prev_holder_commitment_tx.trust().txid(); + if txid != *confirmed_commitment_txid { + let mut found_claim = false; + let mut outpoint = BitcoinOutPoint { txid, vout: 0 }; + for htlc in prev_holder_commitment_tx.nondust_htlcs() { + if let Some(vout) = htlc.transaction_output_index { + outpoint.vout = vout; + if self.onchain_tx_handler.abandon_claim(&outpoint) { + found_claim = true; + } + } else { + debug_assert!(false, "Expected transaction output index for non-dust HTLC"); + } + } + if found_claim { + log_trace!(logger, "Canceled claims for previously broadcast holder commitment with txid {txid}"); + } + } } } } @@ -4749,7 +4786,7 @@ impl ChannelMonitorImpl { return holder_transactions; } - self.get_broadcasted_holder_htlc_descriptors(&self.funding.current_holder_commitment_tx) + self.get_broadcasted_holder_htlc_descriptors(&self.funding, &self.funding.current_holder_commitment_tx) .into_iter() .for_each(|htlc_descriptor| { let txid = self.funding.current_holder_commitment_tx.trust().txid(); @@ -4810,7 +4847,7 @@ impl ChannelMonitorImpl { self.onchain_events_awaiting_threshold_conf.retain(|ref entry| entry.height <= height); let conf_target = self.closure_conf_target(); self.onchain_tx_handler.block_disconnected( - height + 1, broadcaster, conf_target, &self.destination_script, fee_estimator, logger, + height + 1, &broadcaster, conf_target, &self.destination_script, fee_estimator, logger, ); Vec::new() } else { Vec::new() } @@ -4845,6 +4882,7 @@ impl ChannelMonitorImpl { let mut watch_outputs = Vec::new(); let mut claimable_outpoints = Vec::new(); + let mut should_broadcast_commitment = false; 'tx_iter: for tx in &txn_matched { let txid = tx.compute_txid(); log_trace!(logger, "Transaction {} confirmed in block {}", txid , block_hash); @@ -4917,6 +4955,26 @@ impl ChannelMonitorImpl { event: OnchainEvent::AlternativeFundingConfirmation {}, }); + if self.holder_tx_signed { + // Cancel any previous claims that are no longer valid as they stemmed from a + // different funding transaction. + let alternative_holder_commitment_txid = + alternative_funding.current_holder_commitment_tx.trust().txid(); + self.cancel_prev_commitment_claims(&logger, &alternative_holder_commitment_txid); + + // Queue claims for the alternative holder commitment since it is the only one + // that can currently confirm so far (until we see a reorg of its funding + // transaction). + // + // It's possible we process a counterparty commitment within this same block + // that would invalidate our holder commitment. If we were to broadcast our + // holder commitment now, we wouldn't be able to cancel it via our usual + // `cancel_prev_commitment_claims` path once we see a confirmed counterparty + // commitment since the claim would still be pending in `claimable_outpoints` + // (i.e., it wouldn't have been registered with the `OnchainTxHandler` yet). + should_broadcast_commitment = true; + } + continue 'tx_iter; } @@ -4953,6 +5011,11 @@ impl ChannelMonitorImpl { commitment_tx_to_counterparty_output = counterparty_output_idx_sats; claimable_outpoints.append(&mut new_outpoints); + + // We've just seen the counterparty commitment confirm, which conflicts + // with our holder commitment, so make sure we no longer attempt to + // broadcast it. + should_broadcast_commitment = false; } } self.onchain_events_awaiting_threshold_conf.push(OnchainEventEntry { @@ -5002,6 +5065,13 @@ impl ChannelMonitorImpl { self.best_block = BestBlock::new(block_hash, height); } + if should_broadcast_commitment { + let (mut claimables, mut outputs) = + self.generate_claimable_outpoints_and_watch_outputs(None); + claimable_outpoints.append(&mut claimables); + watch_outputs.append(&mut outputs); + } + self.block_confirmed(height, block_hash, txn_matched, watch_outputs, claimable_outpoints, &broadcaster, &fee_estimator, logger) } @@ -5035,7 +5105,7 @@ impl ChannelMonitorImpl { let should_broadcast = self.should_broadcast_holder_commitment_txn(logger); if should_broadcast { - let (mut new_outpoints, mut new_outputs) = self.generate_claimable_outpoints_and_watch_outputs(ClosureReason::HTLCsTimedOut); + let (mut new_outpoints, mut new_outputs) = self.generate_claimable_outpoints_and_watch_outputs(Some(ClosureReason::HTLCsTimedOut)); claimable_outpoints.append(&mut new_outpoints); watch_outputs.append(&mut new_outputs); } @@ -5235,17 +5305,38 @@ impl ChannelMonitorImpl { { log_trace!(logger, "Block {} at height {} disconnected", header.block_hash(), height); - //We may discard: - //- htlc update there as failure-trigger tx (revoked commitment tx, non-revoked commitment tx, HTLC-timeout tx) has been disconnected - //- maturing spendable output has transaction paying us has been disconnected - self.onchain_events_awaiting_threshold_conf.retain(|ref entry| entry.height < height); - let bounded_fee_estimator = LowerBoundedFeeEstimator::new(fee_estimator); let conf_target = self.closure_conf_target(); self.onchain_tx_handler.block_disconnected( - height, broadcaster, conf_target, &self.destination_script, &bounded_fee_estimator, logger + height, &broadcaster, conf_target, &self.destination_script, &bounded_fee_estimator, logger ); + //- htlc update there as failure-trigger tx (revoked commitment tx, non-revoked commitment tx, HTLC-timeout tx) has been disconnected + //- maturing spendable output has transaction paying us has been disconnected + let mut queue_new_commitment_claims = false; + self.onchain_events_awaiting_threshold_conf.retain(|ref entry| { + let retain = entry.height < height; + if !retain && matches!(entry.event, OnchainEvent::AlternativeFundingConfirmation {}) + && self.holder_tx_signed + { + queue_new_commitment_claims = true; + } + retain + }); + if queue_new_commitment_claims { + // Cancel any previous claims that are no longer valid as they stemmed from a + // different funding transaction. + let new_holder_commitment_txid = + self.funding.current_holder_commitment_tx.trust().txid(); + self.cancel_prev_commitment_claims(&logger, &new_holder_commitment_txid); + + // Queue claims for the new holder commitment since it is the only one that can + // currently confirm (until we see an alternative funding transaction confirm). + self.queue_latest_holder_commitment_txn_for_broadcast( + &broadcaster, &bounded_fee_estimator, logger, + ); + } + self.best_block = BestBlock::new(header.prev_blockhash, height - 1); } @@ -5261,6 +5352,11 @@ impl ChannelMonitorImpl { F::Target: FeeEstimator, L::Target: Logger, { + let conf_target = self.closure_conf_target(); + self.onchain_tx_handler.transaction_unconfirmed( + txid, &broadcaster, conf_target, &self.destination_script, fee_estimator, logger + ); + let mut removed_height = None; for entry in self.onchain_events_awaiting_threshold_conf.iter() { if entry.txid == *txid { @@ -5271,18 +5367,36 @@ impl ChannelMonitorImpl { if let Some(removed_height) = removed_height { log_info!(logger, "transaction_unconfirmed of txid {} implies height {} was reorg'd out", txid, removed_height); - self.onchain_events_awaiting_threshold_conf.retain(|ref entry| if entry.height >= removed_height { - log_info!(logger, "Transaction {} reorg'd out", entry.txid); - false - } else { true }); + let mut queue_new_commitment_claims = false; + self.onchain_events_awaiting_threshold_conf.retain(|ref entry| { + let retain = entry.height < removed_height; + if !retain && matches!(entry.event, OnchainEvent::AlternativeFundingConfirmation {}) + && self.holder_tx_signed + { + queue_new_commitment_claims = true; + } + if !retain { + log_info!(logger, "Transaction {} reorg'd out", entry.txid); + } + retain + }); + if queue_new_commitment_claims { + // Cancel any previous claims that are no longer valid as they stemmed from a + // different funding transaction. + let new_holder_commitment_txid = + self.funding.current_holder_commitment_tx.trust().txid(); + self.cancel_prev_commitment_claims(&logger, &new_holder_commitment_txid); + + // Queue claims for the new holder commitment since it is the only one that can + // currently confirm (until we see an alternative funding transaction confirm). + self.queue_latest_holder_commitment_txn_for_broadcast( + &broadcaster, fee_estimator, logger, + ); + } } debug_assert!(!self.onchain_events_awaiting_threshold_conf.iter().any(|ref entry| entry.txid == *txid)); - let conf_target = self.closure_conf_target(); - self.onchain_tx_handler.transaction_unconfirmed( - txid, broadcaster, conf_target, &self.destination_script, fee_estimator, logger - ); } /// Filters a block's `txdata` for transactions spending watched outputs or for any child diff --git a/lightning/src/chain/onchaintx.rs b/lightning/src/chain/onchaintx.rs index f7c2bac3a39..67ee01971ee 100644 --- a/lightning/src/chain/onchaintx.rs +++ b/lightning/src/chain/onchaintx.rs @@ -723,7 +723,8 @@ impl OnchainTxHandler { } #[rustfmt::skip] - pub fn abandon_claim(&mut self, outpoint: &BitcoinOutPoint) { + pub fn abandon_claim(&mut self, outpoint: &BitcoinOutPoint) -> bool { + let mut found_claim = false; let claim_id = self.claimable_outpoints.get(outpoint).map(|(claim_id, _)| *claim_id) .or_else(|| { self.pending_claim_requests.iter() @@ -733,13 +734,23 @@ impl OnchainTxHandler { if let Some(claim_id) = claim_id { if let Some(claim) = self.pending_claim_requests.remove(&claim_id) { for outpoint in claim.outpoints() { - self.claimable_outpoints.remove(outpoint); + if self.claimable_outpoints.remove(outpoint).is_some() { + found_claim = true; + } } } } else { - self.locktimed_packages.values_mut().for_each(|claims| - claims.retain(|claim| !claim.outpoints().contains(&outpoint))); + self.locktimed_packages.values_mut().for_each(|claims| { + claims.retain(|claim| { + let includes_outpoint = claim.outpoints().contains(&outpoint); + if includes_outpoint { + found_claim = true; + } + !includes_outpoint + }) + }); } + found_claim } /// Upon channelmonitor.block_connected(..) or upon provision of a preimage on the forward link @@ -1109,7 +1120,7 @@ impl OnchainTxHandler { pub(super) fn transaction_unconfirmed( &mut self, txid: &Txid, - broadcaster: B, + broadcaster: &B, conf_target: ConfirmationTarget, destination_script: &Script, fee_estimator: &LowerBoundedFeeEstimator, @@ -1135,7 +1146,7 @@ impl OnchainTxHandler { #[rustfmt::skip] pub(super) fn block_disconnected( - &mut self, height: u32, broadcaster: B, conf_target: ConfirmationTarget, + &mut self, height: u32, broadcaster: &B, conf_target: ConfirmationTarget, destination_script: &Script, fee_estimator: &LowerBoundedFeeEstimator, logger: &L, ) where B::Target: BroadcasterInterface,