From 78347f044db92689ee1639e088d266800a5361bb Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 2 Jul 2025 15:30:44 -0500 Subject: [PATCH 1/9] Remove FundedChannel::pending_funding persistence FundedChannel::pending_funding is to be moved to PendingSplice. As such, it will be persisted with PendingSplice once persistence is added for the latter. --- lightning/src/ln/channel.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index e44b9eb5bce..12c4bd8e6f7 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -14265,7 +14265,6 @@ where (49, self.context.local_initiated_shutdown, option), // Added in 0.0.122 (51, is_manual_broadcast, option), // Added in 0.0.124 (53, funding_tx_broadcast_safe_event_emitted, option), // Added in 0.0.124 - (54, self.pending_funding, optional_vec), // Added in 0.2 (55, removed_htlc_attribution_data, optional_vec), // Added in 0.2 (57, holding_cell_attribution_data, optional_vec), // Added in 0.2 (58, self.interactive_tx_signing_session, option), // Added in 0.2 @@ -14628,7 +14627,6 @@ where let mut holder_commitment_point_pending_next_opt: Option = None; let mut is_manual_broadcast = None; - let mut pending_funding = Some(Vec::new()); let mut historical_scids = Some(Vec::new()); let mut interactive_tx_signing_session: Option = None; @@ -14672,7 +14670,6 @@ where (49, local_initiated_shutdown, option), (51, is_manual_broadcast, option), (53, funding_tx_broadcast_safe_event_emitted, option), - (54, pending_funding, optional_vec), // Added in 0.2 (55, removed_htlc_attribution_data, optional_vec), // Added in 0.2 (57, holding_cell_attribution_data, optional_vec), // Added in 0.2 (58, interactive_tx_signing_session, option), // Added in 0.2 @@ -14929,7 +14926,7 @@ where short_channel_id, minimum_depth_override, }, - pending_funding: pending_funding.unwrap(), + pending_funding: Vec::new(), context: ChannelContext { user_id, From 6349685e45df8213ed76908d9bd99571273d4a18 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 2 Jul 2025 15:52:33 -0500 Subject: [PATCH 2/9] Move FundedChannel::pending_funding into PendingSplice FundedChannel::pending_funding and FundedChannel::pending_splice were developed independently, but the former will only contain values when the latter is set. --- lightning/src/ln/channel.rs | 124 ++++++++++++++++++++++-------------- 1 file changed, 75 insertions(+), 49 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 12c4bd8e6f7..1e7347db7a5 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2037,7 +2037,6 @@ where }; let mut funded_channel = FundedChannel { funding: chan.funding, - pending_funding: vec![], context: chan.context, interactive_tx_signing_session: chan.interactive_tx_signing_session, holder_commitment_point, @@ -2550,6 +2549,7 @@ impl AddSigned for u64 { /// Info about a pending splice struct PendingSplice { funding_negotiation: Option, + pending_funding: Vec, /// The funding txid used in the `splice_locked` sent to the counterparty. sent_funding_txid: Option, @@ -2576,11 +2576,14 @@ impl FundingNegotiation { impl PendingSplice { fn check_get_splice_locked( - &mut self, context: &ChannelContext, funding: &FundingScope, height: u32, + &mut self, context: &ChannelContext, confirmed_funding_index: usize, height: u32, ) -> Option where SP::Target: SignerProvider, { + debug_assert!(confirmed_funding_index < self.pending_funding.len()); + + let funding = &self.pending_funding[confirmed_funding_index]; if !context.check_funding_meets_minimum_depth(funding, height) { return None; } @@ -6632,7 +6635,6 @@ where SP::Target: SignerProvider, { pub funding: FundingScope, - pending_funding: Vec, pub context: ChannelContext, /// The signing session for the current interactive tx construction, if any. /// @@ -6654,19 +6656,16 @@ where } macro_rules! promote_splice_funding { - ($self: expr, $funding: expr) => {{ + ($self: expr, $pending_splice: expr, $funding: expr) => {{ let prev_funding_txid = $self.funding.get_funding_txid(); if let Some(scid) = $self.funding.short_channel_id { $self.context.historical_scids.push(scid); } + core::mem::swap(&mut $self.funding, $funding); - $self.interactive_tx_signing_session = None; - $self.pending_splice = None; - $self.context.announcement_sigs = None; - $self.context.announcement_sigs_state = AnnouncementSigsState::NotSent; // The swap above places the previous `FundingScope` into `pending_funding`. - let discarded_funding = $self + let discarded_funding = $pending_splice .pending_funding .drain(..) .filter(|funding| funding.get_funding_txid() != prev_funding_txid) @@ -6682,6 +6681,12 @@ macro_rules! promote_splice_funding { }) }) .collect::>(); + + $self.interactive_tx_signing_session = None; + $self.pending_splice = None; + $self.context.announcement_sigs = None; + $self.context.announcement_sigs_state = AnnouncementSigsState::NotSent; + discarded_funding }}; } @@ -6796,6 +6801,24 @@ where }) } + fn pending_funding(&self) -> &[FundingScope] { + if let Some(pending_splice) = &self.pending_splice { + pending_splice.pending_funding.as_slice() + } else { + &[] + } + } + + fn funding_and_pending_funding_iter_mut(&mut self) -> impl Iterator { + core::iter::once(&mut self.funding).chain( + self.pending_splice + .as_mut() + .map(|pending_splice| pending_splice.pending_funding.as_mut_slice()) + .unwrap_or(&mut []) + .iter_mut(), + ) + } + #[rustfmt::skip] fn check_remote_fee( channel_type: &ChannelTypeFeatures, fee_estimator: &LowerBoundedFeeEstimator, @@ -7425,7 +7448,7 @@ where } core::iter::once(&self.funding) - .chain(self.pending_funding.iter()) + .chain(self.pending_funding().iter()) .try_for_each(|funding| self.context.validate_update_add_htlc(funding, msg, fee_estimator))?; // Now update local state: @@ -7680,7 +7703,7 @@ where { self.commitment_signed_check_state()?; - if !self.pending_funding.is_empty() { + if !self.pending_funding().is_empty() { return Err(ChannelError::close( "Got a single commitment_signed message when expecting a batch".to_owned(), )); @@ -7747,9 +7770,9 @@ where // Any commitment_signed not associated with a FundingScope is ignored below if a // pending splice transaction has confirmed since receiving the batch. - let mut commitment_txs = Vec::with_capacity(self.pending_funding.len() + 1); + let mut commitment_txs = Vec::with_capacity(self.pending_funding().len() + 1); let mut htlc_data = None; - for funding in core::iter::once(&self.funding).chain(self.pending_funding.iter()) { + for funding in core::iter::once(&self.funding).chain(self.pending_funding().iter()) { let funding_txid = funding.get_funding_txid().expect("Funding txid must be known for pending scope"); let msg = messages.get(&funding_txid).ok_or_else(|| { @@ -8419,7 +8442,7 @@ where } } - for funding in core::iter::once(&mut self.funding).chain(self.pending_funding.iter_mut()) { + for funding in self.funding_and_pending_funding_iter_mut() { funding.value_to_self_msat = (funding.value_to_self_msat as i64 + value_to_self_msat_diff) as u64; } @@ -8749,7 +8772,7 @@ where debug_assert!(!self.funding.get_channel_type().supports_anchor_zero_fee_commitments()); let can_send_update_fee = core::iter::once(&self.funding) - .chain(self.pending_funding.iter()) + .chain(self.pending_funding().iter()) .all(|funding| self.context.can_send_update_fee(funding, feerate_per_kw, fee_estimator, logger)); if !can_send_update_fee { return None; @@ -9052,14 +9075,14 @@ where } core::iter::once(&self.funding) - .chain(self.pending_funding.iter()) + .chain(self.pending_funding().iter()) .try_for_each(|funding| FundedChannel::::check_remote_fee(funding.get_channel_type(), fee_estimator, msg.feerate_per_kw, Some(self.context.feerate_per_kw), logger))?; self.context.pending_update_fee = Some((msg.feerate_per_kw, FeeUpdateState::RemoteAnnounced)); self.context.update_time_counter += 1; core::iter::once(&self.funding) - .chain(self.pending_funding.iter()) + .chain(self.pending_funding().iter()) .try_for_each(|funding| self.context.validate_update_fee(funding, fee_estimator, msg)) } @@ -9584,7 +9607,7 @@ where // - MUST process `my_current_funding_locked` as if it was receiving `splice_locked` // for this `txid`. let inferred_splice_locked = msg.my_current_funding_locked.as_ref().and_then(|funding_locked| { - self.pending_funding + self.pending_funding() .iter() .find(|funding| funding.get_funding_txid() == Some(funding_locked.txid)) .and_then(|_| { @@ -10358,7 +10381,7 @@ where ); core::iter::once(&self.funding) - .chain(self.pending_funding.iter()) + .chain(self.pending_funding().iter()) .try_for_each(|funding| self.context.can_accept_incoming_htlc(funding, dust_exposure_limiting_feerate, &logger)) } @@ -10692,12 +10715,12 @@ where let discarded_funding = { // Scope `funding` since it is swapped within `promote_splice_funding` and we don't want // to unintentionally use it. - let funding = self + let funding = pending_splice .pending_funding .iter_mut() .find(|funding| funding.get_funding_txid() == Some(splice_txid)) .unwrap(); - promote_splice_funding!(self, funding) + promote_splice_funding!(self, pending_splice, funding) }; let funding_txo = self @@ -10767,18 +10790,20 @@ where let mut confirmed_funding_index = None; let mut funding_already_confirmed = false; - for (index, funding) in self.pending_funding.iter_mut().enumerate() { - if self.context.check_for_funding_tx_confirmed( - funding, block_hash, height, index_in_block, &mut confirmed_tx, logger, - )? { - if funding_already_confirmed || confirmed_funding_index.is_some() { - let err_reason = "splice tx of another pending funding already confirmed"; - return Err(ClosureReason::ProcessingError { err: err_reason.to_owned() }); - } + if let Some(pending_splice) = &mut self.pending_splice { + for (index, funding) in pending_splice.pending_funding.iter_mut().enumerate() { + if self.context.check_for_funding_tx_confirmed( + funding, block_hash, height, index_in_block, &mut confirmed_tx, logger, + )? { + if funding_already_confirmed || confirmed_funding_index.is_some() { + let err_reason = "splice tx of another pending funding already confirmed"; + return Err(ClosureReason::ProcessingError { err: err_reason.to_owned() }); + } - confirmed_funding_index = Some(index); - } else if funding.funding_tx_confirmation_height != 0 { - funding_already_confirmed = true; + confirmed_funding_index = Some(index); + } else if funding.funding_tx_confirmation_height != 0 { + funding_already_confirmed = true; + } } } @@ -10792,9 +10817,8 @@ where return Err(ClosureReason::ProcessingError { err }); }, }; - 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) { + if let Some(splice_locked) = pending_splice.check_get_splice_locked(&self.context, confirmed_funding_index, height) { log_info!( logger, "Sending splice_locked txid {} to our peer for channel {}", @@ -10918,7 +10942,7 @@ where } let mut confirmed_funding_index = None; - for (index, funding) in self.pending_funding.iter().enumerate() { + for (index, funding) in self.pending_funding().iter().enumerate() { if funding.funding_tx_confirmation_height != 0 { if confirmed_funding_index.is_some() { let err_reason = "splice tx of another pending funding already confirmed"; @@ -10939,7 +10963,7 @@ where return Err(ClosureReason::ProcessingError { err }); }, }; - let funding = self.pending_funding.get_mut(confirmed_funding_index).unwrap(); + let funding = &mut pending_splice.pending_funding[confirmed_funding_index]; // Check if the splice funding transaction was unconfirmed if funding.get_funding_tx_confirmations(height) == 0 { @@ -10958,8 +10982,11 @@ where } let pending_splice = self.pending_splice.as_mut().unwrap(); - 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) { + if let Some(splice_locked) = pending_splice.check_get_splice_locked( + &self.context, + confirmed_funding_index, + height, + ) { log_info!(logger, "Sending a splice_locked to our peer for channel {}", &self.context.channel_id); debug_assert!(chain_node_signer.is_some()); @@ -10989,7 +11016,7 @@ where pub fn get_relevant_txids(&self) -> impl Iterator)> + '_ { core::iter::once(&self.funding) - .chain(self.pending_funding.iter()) + .chain(self.pending_funding().iter()) .map(|funding| { ( funding.get_funding_txid(), @@ -11019,8 +11046,8 @@ where where L::Target: Logger, { - let unconfirmed_funding = core::iter::once(&mut self.funding) - .chain(self.pending_funding.iter_mut()) + let unconfirmed_funding = self + .funding_and_pending_funding_iter_mut() .find(|funding| funding.get_funding_txid() == Some(*txid)); if let Some(funding) = unconfirmed_funding { @@ -11515,6 +11542,7 @@ where self.pending_splice = Some(PendingSplice { funding_negotiation: Some(FundingNegotiation::AwaitingAck(funding_negotiation_context)), + pending_funding: vec![], sent_funding_txid: None, received_funding_txid: None, }); @@ -11737,6 +11765,7 @@ where splice_funding, interactive_tx_constructor, )), + pending_funding: Vec::new(), received_funding_txid: None, sent_funding_txid: None, }); @@ -11924,7 +11953,7 @@ where }, }; - if !self + if !pending_splice .pending_funding .iter() .any(|funding| funding.get_funding_txid() == Some(msg.splice_txid)) @@ -12121,7 +12150,7 @@ where F::Target: FeeEstimator, { core::iter::once(&self.funding) - .chain(self.pending_funding.iter()) + .chain(self.pending_funding().iter()) .map(|funding| self.context.get_available_balances_for_scope(funding, fee_estimator)) .reduce(|acc, e| { AvailableBalances { @@ -12168,7 +12197,7 @@ where } self.context.resend_order = RAACommitmentOrder::RevokeAndACKFirst; - let update = if self.pending_funding.is_empty() { + let update = if self.pending_funding().is_empty() { let (htlcs_ref, counterparty_commitment_tx) = self.build_commitment_no_state_update(&self.funding, logger); let htlc_outputs = htlcs_ref.into_iter() @@ -12191,7 +12220,7 @@ where } else { let mut htlc_data = None; let commitment_txs = core::iter::once(&self.funding) - .chain(self.pending_funding.iter()) + .chain(self.pending_funding().iter()) .map(|funding| { let (htlcs_ref, counterparty_commitment_tx) = self.build_commitment_no_state_update(funding, logger); @@ -12264,7 +12293,7 @@ where L::Target: Logger, { core::iter::once(&self.funding) - .chain(self.pending_funding.iter()) + .chain(self.pending_funding().iter()) .map(|funding| self.send_commitment_no_state_update_for_funding(funding, logger)) .collect::, ChannelError>>() } @@ -13064,7 +13093,6 @@ where let mut channel = FundedChannel { funding: self.funding, - pending_funding: vec![], context: self.context, interactive_tx_signing_session: None, holder_commitment_point, @@ -13350,7 +13378,6 @@ where // `ChannelMonitor`. let mut channel = FundedChannel { funding: self.funding, - pending_funding: vec![], context: self.context, interactive_tx_signing_session: None, holder_commitment_point, @@ -14926,7 +14953,6 @@ where short_channel_id, minimum_depth_override, }, - pending_funding: Vec::new(), context: ChannelContext { user_id, From 2c7e49b0c00dfa64e7f2f07734eca64871547582 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 3 Jul 2025 10:15:40 -0500 Subject: [PATCH 3/9] Rename PendingSplice::pending_funding to negotiated_candidates An upcoming commit will rename PendingSplice to PendingFunding. Thus, rename the similarly named field to something more meaningful. It includes FundingScopes that have been negotiated but have not reached enough confirmations by both parties to have exchanged splice_locked. --- lightning/src/ln/channel.rs | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 1e7347db7a5..82df0d2b85f 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2549,7 +2549,10 @@ impl AddSigned for u64 { /// Info about a pending splice struct PendingSplice { funding_negotiation: Option, - pending_funding: Vec, + + /// Funding candidates that have been negotiated but have not reached enough confirmations + /// by both counterparties to have exchanged `splice_locked` and be promoted. + negotiated_candidates: Vec, /// The funding txid used in the `splice_locked` sent to the counterparty. sent_funding_txid: Option, @@ -2581,9 +2584,9 @@ impl PendingSplice { where SP::Target: SignerProvider, { - debug_assert!(confirmed_funding_index < self.pending_funding.len()); + debug_assert!(confirmed_funding_index < self.negotiated_candidates.len()); - let funding = &self.pending_funding[confirmed_funding_index]; + let funding = &self.negotiated_candidates[confirmed_funding_index]; if !context.check_funding_meets_minimum_depth(funding, height) { return None; } @@ -6666,7 +6669,7 @@ macro_rules! promote_splice_funding { // The swap above places the previous `FundingScope` into `pending_funding`. let discarded_funding = $pending_splice - .pending_funding + .negotiated_candidates .drain(..) .filter(|funding| funding.get_funding_txid() != prev_funding_txid) .map(|mut funding| { @@ -6803,7 +6806,7 @@ where fn pending_funding(&self) -> &[FundingScope] { if let Some(pending_splice) = &self.pending_splice { - pending_splice.pending_funding.as_slice() + pending_splice.negotiated_candidates.as_slice() } else { &[] } @@ -6813,7 +6816,7 @@ where core::iter::once(&mut self.funding).chain( self.pending_splice .as_mut() - .map(|pending_splice| pending_splice.pending_funding.as_mut_slice()) + .map(|pending_splice| pending_splice.negotiated_candidates.as_mut_slice()) .unwrap_or(&mut []) .iter_mut(), ) @@ -10716,7 +10719,7 @@ where // Scope `funding` since it is swapped within `promote_splice_funding` and we don't want // to unintentionally use it. let funding = pending_splice - .pending_funding + .negotiated_candidates .iter_mut() .find(|funding| funding.get_funding_txid() == Some(splice_txid)) .unwrap(); @@ -10791,7 +10794,7 @@ where let mut confirmed_funding_index = None; let mut funding_already_confirmed = false; if let Some(pending_splice) = &mut self.pending_splice { - for (index, funding) in pending_splice.pending_funding.iter_mut().enumerate() { + for (index, funding) in pending_splice.negotiated_candidates.iter_mut().enumerate() { if self.context.check_for_funding_tx_confirmed( funding, block_hash, height, index_in_block, &mut confirmed_tx, logger, )? { @@ -10963,7 +10966,7 @@ where return Err(ClosureReason::ProcessingError { err }); }, }; - let funding = &mut pending_splice.pending_funding[confirmed_funding_index]; + let funding = &mut pending_splice.negotiated_candidates[confirmed_funding_index]; // Check if the splice funding transaction was unconfirmed if funding.get_funding_tx_confirmations(height) == 0 { @@ -11542,7 +11545,7 @@ where self.pending_splice = Some(PendingSplice { funding_negotiation: Some(FundingNegotiation::AwaitingAck(funding_negotiation_context)), - pending_funding: vec![], + negotiated_candidates: vec![], sent_funding_txid: None, received_funding_txid: None, }); @@ -11765,7 +11768,7 @@ where splice_funding, interactive_tx_constructor, )), - pending_funding: Vec::new(), + negotiated_candidates: Vec::new(), received_funding_txid: None, sent_funding_txid: None, }); @@ -11954,7 +11957,7 @@ where }; if !pending_splice - .pending_funding + .negotiated_candidates .iter() .any(|funding| funding.get_funding_txid() == Some(msg.splice_txid)) { From 4268120cab9991cfd6b061651a016e7dbab601d9 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 3 Jul 2025 10:32:55 -0500 Subject: [PATCH 4/9] Rename PendingSplice to PendingFunding While PendingSplice is only used for splicing a FundedChannel, it will be useful when supporting RBF for V2 channel establishment. --- lightning/src/ln/channel.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 82df0d2b85f..970d498013f 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2546,8 +2546,10 @@ impl AddSigned for u64 { } } -/// Info about a pending splice -struct PendingSplice { +/// Information about pending attempts at funding a channel. This includes funding currently under +/// negotiation and any negotiated attempts waiting enough on-chain confirmations. More than one +/// such attempt indicates use of RBF to increase the chances of confirmation. +struct PendingFunding { funding_negotiation: Option, /// Funding candidates that have been negotiated but have not reached enough confirmations @@ -2577,7 +2579,7 @@ impl FundingNegotiation { } } -impl PendingSplice { +impl PendingFunding { fn check_get_splice_locked( &mut self, context: &ChannelContext, confirmed_funding_index: usize, height: u32, ) -> Option @@ -6649,7 +6651,7 @@ where pub interactive_tx_signing_session: Option, holder_commitment_point: HolderCommitmentPoint, /// Info about an in-progress, pending splice (if any), on the pre-splice channel - pending_splice: Option, + pending_splice: Option, /// Once we become quiescent, if we're the initiator, there's some action we'll want to take. /// This keeps track of that action. Note that if we become quiescent and we're not the @@ -11543,7 +11545,7 @@ where change_script, }; - self.pending_splice = Some(PendingSplice { + self.pending_splice = Some(PendingFunding { funding_negotiation: Some(FundingNegotiation::AwaitingAck(funding_negotiation_context)), negotiated_candidates: vec![], sent_funding_txid: None, @@ -11763,7 +11765,7 @@ where let funding_pubkey = splice_funding.get_holder_pubkeys().funding_pubkey; - self.pending_splice = Some(PendingSplice { + self.pending_splice = Some(PendingFunding { funding_negotiation: Some(FundingNegotiation::ConstructingTransaction( splice_funding, interactive_tx_constructor, From d0780f71f08f7d02e73283dc9889703b6f039ee0 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 3 Jul 2025 10:47:32 -0500 Subject: [PATCH 5/9] Remove unnecessary FundedChannel::pending_splice checks Now that PendingFunding directly contains the negotiated candidates, some unnecessary checks can be removed. --- lightning/src/ln/channel.rs | 165 +++++++++++++++++------------------- 1 file changed, 77 insertions(+), 88 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 970d498013f..0e38eec2e45 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -10793,9 +10793,10 @@ where } } - let mut confirmed_funding_index = None; - let mut funding_already_confirmed = false; if let Some(pending_splice) = &mut self.pending_splice { + let mut confirmed_funding_index = None; + let mut funding_already_confirmed = false; + for (index, funding) in pending_splice.negotiated_candidates.iter_mut().enumerate() { if self.context.check_for_funding_tx_confirmed( funding, block_hash, height, index_in_block, &mut confirmed_tx, logger, @@ -10810,40 +10811,36 @@ where funding_already_confirmed = true; } } - } - if let Some(confirmed_funding_index) = confirmed_funding_index { - let pending_splice = match self.pending_splice.as_mut() { - Some(pending_splice) => pending_splice, - None => { - // TODO: Move pending_funding into pending_splice - debug_assert!(false); - let err = "expected a pending splice".to_string(); - return Err(ClosureReason::ProcessingError { err }); - }, - }; + if let Some(confirmed_funding_index) = confirmed_funding_index { + if let Some(splice_locked) = pending_splice.check_get_splice_locked( + &self.context, + confirmed_funding_index, + height, + ) { - if let Some(splice_locked) = pending_splice.check_get_splice_locked(&self.context, confirmed_funding_index, height) { - log_info!( - logger, - "Sending splice_locked txid {} to our peer for channel {}", - splice_locked.splice_txid, - &self.context.channel_id, - ); - - let (funding_txo, monitor_update, announcement_sigs, discarded_funding) = - 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, - splice_promotion.discarded_funding, - )).unwrap_or((None, None, None, Vec::new())); + log_info!( + logger, + "Sending splice_locked txid {} to our peer for channel {}", + splice_locked.splice_txid, + &self.context.channel_id, + ); - return Ok((Some(FundingConfirmedMessage::Splice(splice_locked, funding_txo, monitor_update, discarded_funding)), announcement_sigs)); + let (funding_txo, monitor_update, announcement_sigs, discarded_funding) = + 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, + splice_promotion.discarded_funding, + )).unwrap_or((None, None, None, Vec::new())); + + return Ok((Some(FundingConfirmedMessage::Splice(splice_locked, funding_txo, monitor_update, discarded_funding)), announcement_sigs)); + } } } + } Ok((None, None)) @@ -10946,70 +10943,62 @@ where return Err(ClosureReason::FundingTimedOut); } - let mut confirmed_funding_index = None; - for (index, funding) in self.pending_funding().iter().enumerate() { - if funding.funding_tx_confirmation_height != 0 { - if confirmed_funding_index.is_some() { - let err_reason = "splice tx of another pending funding already confirmed"; - return Err(ClosureReason::ProcessingError { err: err_reason.to_owned() }); - } + if let Some(pending_splice) = &mut self.pending_splice { + let mut confirmed_funding_index = None; + + for (index, funding) in pending_splice.negotiated_candidates.iter().enumerate() { + if funding.funding_tx_confirmation_height != 0 { + if confirmed_funding_index.is_some() { + let err_reason = "splice tx of another pending funding already confirmed"; + return Err(ClosureReason::ProcessingError { err: err_reason.to_owned() }); + } - confirmed_funding_index = Some(index); + confirmed_funding_index = Some(index); + } } - } - if let Some(confirmed_funding_index) = confirmed_funding_index { - let pending_splice = match self.pending_splice.as_mut() { - Some(pending_splice) => pending_splice, - None => { - // TODO: Move pending_funding into pending_splice - debug_assert!(false); - let err = "expected a pending splice".to_string(); - return Err(ClosureReason::ProcessingError { err }); - }, - }; - let funding = &mut pending_splice.negotiated_candidates[confirmed_funding_index]; - - // Check if the splice funding transaction was unconfirmed - if funding.get_funding_tx_confirmations(height) == 0 { - funding.funding_tx_confirmation_height = 0; - if let Some(sent_funding_txid) = pending_splice.sent_funding_txid { - if Some(sent_funding_txid) == funding.get_funding_txid() { - log_warn!( - logger, - "Unconfirming sent splice_locked txid {} for channel {}", - sent_funding_txid, - &self.context.channel_id, - ); - pending_splice.sent_funding_txid = None; + if let Some(confirmed_funding_index) = confirmed_funding_index { + let funding = &mut pending_splice.negotiated_candidates[confirmed_funding_index]; + + // Check if the splice funding transaction was unconfirmed + if funding.get_funding_tx_confirmations(height) == 0 { + funding.funding_tx_confirmation_height = 0; + if let Some(sent_funding_txid) = pending_splice.sent_funding_txid { + if Some(sent_funding_txid) == funding.get_funding_txid() { + log_warn!( + logger, + "Unconfirming sent splice_locked txid {} for channel {}", + sent_funding_txid, + &self.context.channel_id, + ); + pending_splice.sent_funding_txid = None; + } } } - } - let pending_splice = self.pending_splice.as_mut().unwrap(); - if let Some(splice_locked) = pending_splice.check_get_splice_locked( - &self.context, - confirmed_funding_index, - 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_txo, monitor_update, announcement_sigs, discarded_funding) = 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, - splice_promotion.discarded_funding, - )) - .unwrap_or((None, None, None, Vec::new())); + if let Some(splice_locked) = pending_splice.check_get_splice_locked( + &self.context, + confirmed_funding_index, + height, + ) { + log_info!(logger, "Sending a splice_locked to our peer for channel {}", &self.context.channel_id); - return Ok((Some(FundingConfirmedMessage::Splice(splice_locked, funding_txo, monitor_update, discarded_funding)), timed_out_htlcs, announcement_sigs)); + let (funding_txo, monitor_update, announcement_sigs, discarded_funding) = 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, + splice_promotion.discarded_funding, + )) + .unwrap_or((None, None, None, Vec::new())); + + return Ok((Some(FundingConfirmedMessage::Splice(splice_locked, funding_txo, monitor_update, discarded_funding)), timed_out_htlcs, announcement_sigs)); + } } } From 4ba6452d140462fc6e5d76ece8752b181cd1677b Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 10 Sep 2025 14:34:25 -0500 Subject: [PATCH 6/9] Inline promote_splice_funding macro --- lightning/src/ln/channel.rs | 70 ++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 0e38eec2e45..312d1c84282 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -6660,42 +6660,6 @@ where quiescent_action: Option, } -macro_rules! promote_splice_funding { - ($self: expr, $pending_splice: expr, $funding: expr) => {{ - let prev_funding_txid = $self.funding.get_funding_txid(); - if let Some(scid) = $self.funding.short_channel_id { - $self.context.historical_scids.push(scid); - } - - core::mem::swap(&mut $self.funding, $funding); - - // The swap above places the previous `FundingScope` into `pending_funding`. - let discarded_funding = $pending_splice - .negotiated_candidates - .drain(..) - .filter(|funding| funding.get_funding_txid() != prev_funding_txid) - .map(|mut funding| { - funding - .funding_transaction - .take() - .map(|tx| FundingInfo::Tx { transaction: tx }) - .unwrap_or_else(|| FundingInfo::OutPoint { - outpoint: funding - .get_funding_txo() - .expect("Negotiated splices must have a known funding outpoint"), - }) - }) - .collect::>(); - - $self.interactive_tx_signing_session = None; - $self.pending_splice = None; - $self.context.announcement_sigs = None; - $self.context.announcement_sigs_state = AnnouncementSigsState::NotSent; - - discarded_funding - }}; -} - #[cfg(any(test, fuzzing))] #[derive(Clone, Copy, Default)] struct PredictedNextFee { @@ -10718,16 +10682,44 @@ where ); let discarded_funding = { - // Scope `funding` since it is swapped within `promote_splice_funding` and we don't want - // to unintentionally use it. + // Scope `funding` to avoid unintentionally using it later since it is swapped below. let funding = pending_splice .negotiated_candidates .iter_mut() .find(|funding| funding.get_funding_txid() == Some(splice_txid)) .unwrap(); - promote_splice_funding!(self, pending_splice, funding) + let prev_funding_txid = self.funding.get_funding_txid(); + + if let Some(scid) = self.funding.short_channel_id { + self.context.historical_scids.push(scid); + } + + core::mem::swap(&mut self.funding, funding); + + // The swap above places the previous `FundingScope` into `pending_funding`. + pending_splice + .negotiated_candidates + .drain(..) + .filter(|funding| funding.get_funding_txid() != prev_funding_txid) + .map(|mut funding| { + funding + .funding_transaction + .take() + .map(|tx| FundingInfo::Tx { transaction: tx }) + .unwrap_or_else(|| FundingInfo::OutPoint { + outpoint: funding + .get_funding_txo() + .expect("Negotiated splices must have a known funding outpoint"), + }) + }) + .collect::>() }; + self.interactive_tx_signing_session = None; + self.pending_splice = None; + self.context.announcement_sigs = None; + self.context.announcement_sigs_state = AnnouncementSigsState::NotSent; + let funding_txo = self .funding .get_funding_txo() From 5fecde5d9a76dbd6a8753b74bd5ae3480a4c589e Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 10 Sep 2025 23:02:26 -0500 Subject: [PATCH 7/9] Use struct syntax for FundingNegotiation variants To use impl_writeable_tlv_based_enum_upgradable with unread_variants, currently tuple syntax can't be used enum variants. Update FundingNegotiation to use this syntax so that it can be used with that macro. --- lightning/src/ln/channel.rs | 69 +++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 312d1c84282..d0369afafba 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1749,10 +1749,10 @@ where .as_mut() .and_then(|pending_splice| pending_splice.funding_negotiation.take()) .and_then(|funding_negotiation| { - if let FundingNegotiation::ConstructingTransaction( - _, + if let FundingNegotiation::ConstructingTransaction { interactive_tx_constructor, - ) = funding_negotiation + .. + } = funding_negotiation { Some(interactive_tx_constructor) } else { @@ -1970,10 +1970,10 @@ where ChannelPhase::Funded(chan) => { if let Some(pending_splice) = chan.pending_splice.as_mut() { if let Some(funding_negotiation) = pending_splice.funding_negotiation.take() { - if let FundingNegotiation::ConstructingTransaction( + if let FundingNegotiation::ConstructingTransaction { mut funding, interactive_tx_constructor, - ) = funding_negotiation + } = funding_negotiation { let mut signing_session = interactive_tx_constructor.into_signing_session(); @@ -1987,7 +1987,7 @@ where chan.interactive_tx_signing_session = Some(signing_session); pending_splice.funding_negotiation = - Some(FundingNegotiation::AwaitingSignatures(funding)); + Some(FundingNegotiation::AwaitingSignatures { funding }); return Ok(commitment_signed); } else { @@ -2058,7 +2058,7 @@ where let has_negotiated_pending_splice = funded_channel.pending_splice.as_ref() .and_then(|pending_splice| pending_splice.funding_negotiation.as_ref()) .filter(|funding_negotiation| { - matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures(_)) + matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. }) }) .map(|funding_negotiation| funding_negotiation.as_funding().is_some()) .unwrap_or(false); @@ -2564,17 +2564,24 @@ struct PendingFunding { } enum FundingNegotiation { - AwaitingAck(FundingNegotiationContext), - ConstructingTransaction(FundingScope, InteractiveTxConstructor), - AwaitingSignatures(FundingScope), + AwaitingAck { + context: FundingNegotiationContext, + }, + ConstructingTransaction { + funding: FundingScope, + interactive_tx_constructor: InteractiveTxConstructor, + }, + AwaitingSignatures { + funding: FundingScope, + }, } impl FundingNegotiation { fn as_funding(&self) -> Option<&FundingScope> { match self { - FundingNegotiation::AwaitingAck(_) => None, - FundingNegotiation::ConstructingTransaction(funding, _) => Some(funding), - FundingNegotiation::AwaitingSignatures(funding) => Some(funding), + FundingNegotiation::AwaitingAck { .. } => None, + FundingNegotiation::ConstructingTransaction { funding, .. } => Some(funding), + FundingNegotiation::AwaitingSignatures { funding } => Some(funding), } } } @@ -6760,8 +6767,10 @@ where .as_mut() .and_then(|pending_splice| pending_splice.funding_negotiation.as_mut()) .and_then(|funding_negotiation| { - if let FundingNegotiation::ConstructingTransaction(_, interactive_tx_constructor) = - funding_negotiation + if let FundingNegotiation::ConstructingTransaction { + interactive_tx_constructor, + .. + } = funding_negotiation { Some(interactive_tx_constructor) } else { @@ -7580,7 +7589,7 @@ where .as_ref() .and_then(|pending_splice| pending_splice.funding_negotiation.as_ref()) .filter(|funding_negotiation| { - matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures(_)) + matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. }) }) .and_then(|funding_negotiation| funding_negotiation.as_funding()) .expect("Funding must exist for negotiated pending splice"); @@ -8571,7 +8580,7 @@ where .funding_negotiation .as_ref() .map(|funding_negotiation| { - matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures(_)) + matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. }) }) .unwrap_or(false) { @@ -9435,7 +9444,7 @@ where .as_ref() .and_then(|pending_splice| pending_splice.funding_negotiation.as_ref()) .and_then(|funding_negotiation| { - if let FundingNegotiation::AwaitingSignatures(funding) = &funding_negotiation { + if let FundingNegotiation::AwaitingSignatures { funding } = &funding_negotiation { Some(funding) } else { None @@ -11515,7 +11524,7 @@ where } let prev_funding_input = self.funding.to_splice_funding_input(); - let funding_negotiation_context = FundingNegotiationContext { + let context = FundingNegotiationContext { is_initiator: true, our_funding_contribution: adjusted_funding_contribution, funding_tx_locktime: LockTime::from_consensus(locktime), @@ -11527,7 +11536,7 @@ where }; self.pending_splice = Some(PendingFunding { - funding_negotiation: Some(FundingNegotiation::AwaitingAck(funding_negotiation_context)), + funding_negotiation: Some(FundingNegotiation::AwaitingAck { context }), negotiated_candidates: vec![], sent_funding_txid: None, received_funding_txid: None, @@ -11747,10 +11756,10 @@ where let funding_pubkey = splice_funding.get_holder_pubkeys().funding_pubkey; self.pending_splice = Some(PendingFunding { - funding_negotiation: Some(FundingNegotiation::ConstructingTransaction( - splice_funding, + funding_negotiation: Some(FundingNegotiation::ConstructingTransaction { + funding: splice_funding, interactive_tx_constructor, - )), + }), negotiated_candidates: Vec::new(), received_funding_txid: None, sent_funding_txid: None, @@ -11785,7 +11794,7 @@ where let pending_splice = self.pending_splice.as_mut().expect("We should have returned an error earlier!"); // TODO: Good candidate for a let else statement once MSRV >= 1.65 - let funding_negotiation_context = if let Some(FundingNegotiation::AwaitingAck(context)) = + let funding_negotiation_context = if let Some(FundingNegotiation::AwaitingAck { context }) = pending_splice.funding_negotiation.take() { context @@ -11811,10 +11820,10 @@ where debug_assert!(self.interactive_tx_signing_session.is_none()); - pending_splice.funding_negotiation = Some(FundingNegotiation::ConstructingTransaction( - splice_funding, + pending_splice.funding_negotiation = Some(FundingNegotiation::ConstructingTransaction { + funding: splice_funding, interactive_tx_constructor, - )); + }); Ok(tx_msg_opt) } @@ -11828,9 +11837,9 @@ where .ok_or(ChannelError::Ignore("Channel is not in pending splice".to_owned()))? .funding_negotiation { - Some(FundingNegotiation::AwaitingAck(context)) => context, - Some(FundingNegotiation::ConstructingTransaction(_, _)) - | Some(FundingNegotiation::AwaitingSignatures(_)) => { + Some(FundingNegotiation::AwaitingAck { context }) => context, + Some(FundingNegotiation::ConstructingTransaction { .. }) + | Some(FundingNegotiation::AwaitingSignatures { .. }) => { return Err(ChannelError::WarnAndDisconnect( "Got unexpected splice_ack; splice negotiation already in progress".to_owned(), )); From 4f0abe97b10005145262f6e18c4b28cf80a9c446 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 10 Sep 2025 23:15:39 -0500 Subject: [PATCH 8/9] Persist FundedChannel::pending_splice Once a splice funding transaction has been constructed, the corresponding state must be persisted so that the process can be continued across restarts. This includes exchanging signatures, waiting for enough confirmations, and RBF'ing. --- lightning/src/ln/channel.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index d0369afafba..893efe738e3 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2563,6 +2563,13 @@ struct PendingFunding { received_funding_txid: Option, } +impl_writeable_tlv_based!(PendingFunding, { + (1, funding_negotiation, upgradable_option), + (3, negotiated_candidates, required_vec), + (5, sent_funding_txid, option), + (7, received_funding_txid, option), +}); + enum FundingNegotiation { AwaitingAck { context: FundingNegotiationContext, @@ -2576,6 +2583,13 @@ enum FundingNegotiation { }, } +impl_writeable_tlv_based_enum_upgradable!(FundingNegotiation, + (0, AwaitingSignatures) => { + (1, funding, required), + }, + unread_variants: AwaitingAck, ConstructingTransaction +); + impl FundingNegotiation { fn as_funding(&self) -> Option<&FundingScope> { match self { @@ -14294,6 +14308,7 @@ where (60, self.context.historical_scids, optional_vec), // Added in 0.2 (61, fulfill_attribution_data, optional_vec), // Added in 0.2 (63, holder_commitment_point_current, option), // Added in 0.2 + (64, self.pending_splice, option), // Added in 0.2 (65, self.quiescent_action, option), // Added in 0.2 }); @@ -14655,6 +14670,7 @@ where let mut minimum_depth_override: Option = None; + let mut pending_splice: Option = None; let mut quiescent_action = None; read_tlv_fields!(reader, { @@ -14699,6 +14715,7 @@ where (60, historical_scids, optional_vec), // Added in 0.2 (61, fulfill_attribution_data, optional_vec), // Added in 0.2 (63, holder_commitment_point_current_opt, option), // Added in 0.2 + (64, pending_splice, option), // Added in 0.2 (65, quiescent_action, upgradable_option), // Added in 0.2 }); @@ -15059,7 +15076,7 @@ where }, interactive_tx_signing_session, holder_commitment_point, - pending_splice: None, + pending_splice, quiescent_action, }) } From 53509320aa08743803230901bc03cdb112600bb0 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 10 Sep 2025 23:22:30 -0500 Subject: [PATCH 9/9] Clean up FundedChannel::pending_splice docs --- lightning/src/ln/channel.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 893efe738e3..ec7824b2bfc 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -6671,7 +6671,8 @@ where /// This field is cleared once our counterparty sends a `channel_ready`. pub interactive_tx_signing_session: Option, holder_commitment_point: HolderCommitmentPoint, - /// Info about an in-progress, pending splice (if any), on the pre-splice channel + + /// Information about any pending splice candidates, including RBF attempts. pending_splice: Option, /// Once we become quiescent, if we're the initiator, there's some action we'll want to take.