From f4d752577fce50522f944413fe019c4d3c57305b Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 10 Apr 2025 13:28:04 -0500 Subject: [PATCH 01/19] Generalize do_chain_event signature for splicing When processing confirmed transactions and updates to the best block, ChannelManager may be instructed to send a channel_ready message when a channel's funding transaction has confirmed and has met the required number of confirmations. A similar action is needed for sending splice_locked once splice transaction has confirmed with required number of confirmations. Generalize do_chain_event signature to allow for either scenario. --- lightning/src/ln/channel.rs | 16 +++++++------- lightning/src/ln/channelmanager.rs | 35 ++++++++++++++++++------------ 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 3861a7052f1..83df2269da1 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -50,9 +50,9 @@ use crate::ln::channel_state::{ OutboundHTLCDetails, OutboundHTLCStateDetails, }; use crate::ln::channelmanager::{ - self, HTLCFailureMsg, HTLCSource, OpenChannelMessage, PaymentClaimDetails, PendingHTLCInfo, - PendingHTLCStatus, RAACommitmentOrder, SentHTLCId, BREAKDOWN_TIMEOUT, - MAX_LOCAL_BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA, + self, FundingConfirmedMessage, HTLCFailureMsg, HTLCSource, OpenChannelMessage, + PaymentClaimDetails, PendingHTLCInfo, PendingHTLCStatus, RAACommitmentOrder, SentHTLCId, + BREAKDOWN_TIMEOUT, MAX_LOCAL_BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA, }; use crate::ln::interactivetxs::{ calculate_change_output_value, get_output_weight, AbortReason, HandleTxCompleteResult, @@ -5735,7 +5735,7 @@ impl FailHTLCMessageName for msgs::UpdateFailMalformedHTLC { } type BestBlockUpdatedRes = ( - Option, + Option, Vec<(HTLCSource, PaymentHash)>, Option, ); @@ -8858,7 +8858,7 @@ where pub fn transactions_confirmed( &mut self, block_hash: &BlockHash, height: u32, txdata: &TransactionData, chain_hash: ChainHash, node_signer: &NS, user_config: &UserConfig, logger: &L - ) -> Result<(Option, Option), ClosureReason> + ) -> Result<(Option, Option), ClosureReason> where NS::Target: NodeSigner, L::Target: Logger @@ -8919,7 +8919,7 @@ where if let Some(channel_ready) = self.check_get_channel_ready(height, logger) { log_info!(logger, "Sending a channel_ready to our peer for channel {}", &self.context.channel_id); let announcement_sigs = self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger); - msgs = (Some(channel_ready), announcement_sigs); + msgs = (Some(FundingConfirmedMessage::Establishment(channel_ready)), announcement_sigs); } } for inp in tx.input.iter() { @@ -8964,7 +8964,7 @@ where fn do_best_block_updated( &mut self, height: u32, highest_header_time: u32, chain_node_signer: Option<(ChainHash, &NS, &UserConfig)>, logger: &L - ) -> Result<(Option, Vec<(HTLCSource, PaymentHash)>, Option), ClosureReason> + ) -> Result<(Option, Vec<(HTLCSource, PaymentHash)>, Option), ClosureReason> where NS::Target: NodeSigner, L::Target: Logger @@ -8993,7 +8993,7 @@ where self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger) } else { None }; log_info!(logger, "Sending a channel_ready to our peer for channel {}", &self.context.channel_id); - return Ok((Some(channel_ready), timed_out_htlcs, announcement_sigs)); + return Ok((Some(FundingConfirmedMessage::Establishment(channel_ready)), timed_out_htlcs, announcement_sigs)); } if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) || diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 773fda9d769..3afc978f241 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -12118,6 +12118,10 @@ where } } +pub(super) enum FundingConfirmedMessage { + Establishment(msgs::ChannelReady), +} + impl< M: Deref, T: Deref, @@ -12144,7 +12148,7 @@ where /// un/confirmed, etc) on each channel, handling any resulting errors or messages generated by /// the function. #[rustfmt::skip] - fn do_chain_event) -> Result<(Option, Vec<(HTLCSource, PaymentHash)>, Option), ClosureReason>> + fn do_chain_event) -> Result<(Option, Vec<(HTLCSource, PaymentHash)>, Option), ClosureReason>> (&self, height_opt: Option, f: FN) { // Note that we MUST NOT end up calling methods on self.chain_monitor here - we're called // during initialization prior to the chain_monitor being fully configured in some cases. @@ -12165,7 +12169,7 @@ where None => true, Some(funded_channel) => { let res = f(funded_channel); - if let Ok((channel_ready_opt, mut timed_out_pending_htlcs, announcement_sigs)) = res { + if let Ok((funding_confirmed_opt, mut timed_out_pending_htlcs, announcement_sigs)) = res { for (source, payment_hash) in timed_out_pending_htlcs.drain(..) { let reason = LocalHTLCFailureReason::CLTVExpiryTooSoon; let data = self.get_htlc_inbound_temp_fail_data(reason); @@ -12173,19 +12177,22 @@ where HTLCHandlingFailureType::Forward { node_id: Some(funded_channel.context.get_counterparty_node_id()), channel_id: funded_channel.context.channel_id() })); } let logger = WithChannelContext::from(&self.logger, &funded_channel.context, None); - if let Some(channel_ready) = channel_ready_opt { - send_channel_ready!(self, pending_msg_events, funded_channel, channel_ready); - if funded_channel.context.is_usable() { - log_trace!(logger, "Sending channel_ready with private initial channel_update for our counterparty on channel {}", funded_channel.context.channel_id()); - if let Ok(msg) = self.get_channel_update_for_unicast(funded_channel) { - pending_msg_events.push(MessageSendEvent::SendChannelUpdate { - node_id: funded_channel.context.get_counterparty_node_id(), - msg, - }); + match funding_confirmed_opt { + Some(FundingConfirmedMessage::Establishment(channel_ready)) => { + send_channel_ready!(self, pending_msg_events, funded_channel, channel_ready); + if funded_channel.context.is_usable() { + log_trace!(logger, "Sending channel_ready with private initial channel_update for our counterparty on channel {}", funded_channel.context.channel_id()); + if let Ok(msg) = self.get_channel_update_for_unicast(funded_channel) { + pending_msg_events.push(MessageSendEvent::SendChannelUpdate { + node_id: funded_channel.context.get_counterparty_node_id(), + msg, + }); + } + } else { + log_trace!(logger, "Sending channel_ready WITHOUT channel_update for {}", funded_channel.context.channel_id()); } - } else { - log_trace!(logger, "Sending channel_ready WITHOUT channel_update for {}", funded_channel.context.channel_id()); - } + }, + None => {}, } { From e3addaa8751c0b6b903698e71ff8867d3a5a0150 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 15 Apr 2025 15:26:37 -0500 Subject: [PATCH 02/19] Move ChannelContext::funding_tx_confirmed_in to FundingScope When processing confirmed transactions, if the funding transaction is found then information about it in the ChannelContext is updated. In preparation for splicing, move this data to FundingScope. --- lightning/src/ln/channel.rs | 32 +++++++++++++++++------------- lightning/src/ln/channelmanager.rs | 2 +- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 83df2269da1..33a431bf857 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1960,6 +1960,8 @@ pub(super) struct FundingScope { /// The transaction which funds this channel. Note that for manually-funded channels (i.e., /// [`ChannelContext::is_manual_broadcast`] is true) this will be a dummy empty transaction. funding_transaction: Option, + /// The hash of the block in which the funding transaction was included. + funding_tx_confirmed_in: Option, } impl Writeable for FundingScope { @@ -1970,6 +1972,7 @@ impl Writeable for FundingScope { (5, self.holder_selected_channel_reserve_satoshis, required), (7, self.channel_transaction_parameters, (required: ReadableArgs, None)), (9, self.funding_transaction, option), + (11, self.funding_tx_confirmed_in, option), }); Ok(()) } @@ -1983,6 +1986,7 @@ impl Readable for FundingScope { let mut holder_selected_channel_reserve_satoshis = RequiredWrapper(None); let mut channel_transaction_parameters = RequiredWrapper(None); let mut funding_transaction = None; + let mut funding_tx_confirmed_in = None; read_tlv_fields!(reader, { (1, value_to_self_msat, required), @@ -1990,6 +1994,7 @@ impl Readable for FundingScope { (5, holder_selected_channel_reserve_satoshis, required), (7, channel_transaction_parameters, (required: ReadableArgs, None)), (9, funding_transaction, option), + (11, funding_tx_confirmed_in, option), }); Ok(Self { @@ -2002,6 +2007,7 @@ impl Readable for FundingScope { counterparty_max_commitment_tx_output: Mutex::new((0, 0)), channel_transaction_parameters: channel_transaction_parameters.0.unwrap(), funding_transaction, + funding_tx_confirmed_in, #[cfg(any(test, fuzzing))] next_local_commitment_tx_fee_info_cached: Mutex::new(None), #[cfg(any(test, fuzzing))] @@ -2239,8 +2245,6 @@ where /// milliseconds, so any accidental force-closes here should be exceedingly rare. expecting_peer_commitment_signed: bool, - /// The hash of the block in which the funding transaction was included. - funding_tx_confirmed_in: Option, funding_tx_confirmation_height: u32, short_channel_id: Option, /// Either the height at which this channel was created or the height at which it was last @@ -3088,6 +3092,7 @@ where channel_value_satoshis, }, funding_transaction: None, + funding_tx_confirmed_in: None, }; let channel_context = ChannelContext { user_id, @@ -3151,7 +3156,6 @@ where closing_fee_limits: None, target_closing_feerate_sats_per_kw: None, - funding_tx_confirmed_in: None, funding_tx_confirmation_height: 0, short_channel_id: None, channel_creation_height: current_chain_height, @@ -3329,6 +3333,7 @@ where channel_value_satoshis, }, funding_transaction: None, + funding_tx_confirmed_in: None, }; let channel_context = Self { user_id, @@ -3390,7 +3395,6 @@ where closing_fee_limits: None, target_closing_feerate_sats_per_kw: None, - funding_tx_confirmed_in: None, funding_tx_confirmation_height: 0, short_channel_id: None, channel_creation_height: current_chain_height, @@ -3798,11 +3802,6 @@ where Ok(()) } - /// Returns the block hash in which our funding transaction was confirmed. - pub fn get_funding_tx_confirmed_in(&self) -> Option { - self.funding_tx_confirmed_in - } - /// Returns the current number of confirmations on the funding transaction. pub fn get_funding_tx_confirmations(&self, height: u32) -> u32 { if self.funding_tx_confirmation_height == 0 { @@ -8899,7 +8898,7 @@ where } } self.context.funding_tx_confirmation_height = height; - self.context.funding_tx_confirmed_in = Some(*block_hash); + self.funding.funding_tx_confirmed_in = Some(*block_hash); self.context.short_channel_id = match scid_from_parts(height as u64, index_in_block as u64, txo_idx as u64) { Ok(scid) => Some(scid), Err(_) => panic!("Block was bogus - either height was > 16 million, had > 16 million transactions, or had > 65k outputs"), @@ -9015,12 +9014,12 @@ where // 0-conf channel, but not doing so may lead to the // `ChannelManager::short_to_chan_info` map being inconsistent, so we currently have // to. - if funding_tx_confirmations == 0 && self.context.funding_tx_confirmed_in.is_some() { + if funding_tx_confirmations == 0 && self.funding.funding_tx_confirmed_in.is_some() { let err_reason = format!("Funding transaction was un-confirmed. Locked at {} confs, now have {} confs.", self.context.minimum_depth.unwrap(), funding_tx_confirmations); return Err(ClosureReason::ProcessingError { err: err_reason }); } - } else if !self.funding.is_outbound() && self.context.funding_tx_confirmed_in.is_none() && + } else if !self.funding.is_outbound() && self.funding.funding_tx_confirmed_in.is_none() && height >= self.context.channel_creation_height + FUNDING_CONF_DEADLINE_BLOCKS { log_info!(logger, "Closing channel {} due to funding timeout", &self.context.channel_id); // If funding_tx_confirmed_in is unset, the channel must not be active @@ -10200,6 +10199,11 @@ where false } } + + /// Returns the block hash in which our funding transaction was confirmed. + pub fn get_funding_tx_confirmed_in(&self) -> Option { + self.funding.funding_tx_confirmed_in + } } /// A not-yet-funded outbound (from holder) channel using V1 channel establishment. @@ -11501,7 +11505,7 @@ where // consider the stale state on reload. 0u8.write(writer)?; - self.context.funding_tx_confirmed_in.write(writer)?; + self.funding.funding_tx_confirmed_in.write(writer)?; self.context.funding_tx_confirmation_height.write(writer)?; self.context.short_channel_id.write(writer)?; @@ -12160,6 +12164,7 @@ where channel_transaction_parameters: channel_parameters, funding_transaction, + funding_tx_confirmed_in, }, pending_funding: pending_funding.unwrap(), context: ChannelContext { @@ -12223,7 +12228,6 @@ where closing_fee_limits: None, target_closing_feerate_sats_per_kw, - funding_tx_confirmed_in, funding_tx_confirmation_height, short_channel_id, channel_creation_height, diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 3afc978f241..ddd794cce4b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -12084,7 +12084,7 @@ where for chan in peer_state.channel_by_id.values().filter_map(Channel::as_funded) { let txid_opt = chan.funding.get_funding_txo(); let height_opt = chan.context.get_funding_tx_confirmation_height(); - let hash_opt = chan.context.get_funding_tx_confirmed_in(); + let hash_opt = chan.get_funding_tx_confirmed_in(); if let (Some(funding_txo), Some(conf_height), Some(block_hash)) = (txid_opt, height_opt, hash_opt) { From d80770b20e232ff6d3592244a789bb1fda8aa63e Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 15 Apr 2025 16:07:25 -0500 Subject: [PATCH 03/19] Move ChannelContext::funding_tx_confirmation_height to FundingScope When processing confirmed transactions, if the funding transaction is found then information about it in the ChannelContext is updated. In preparation for splicing, move this data to FundingScope. --- lightning/src/ln/channel.rs | 83 ++++++++++++++++-------------- lightning/src/ln/channel_state.rs | 2 +- lightning/src/ln/channelmanager.rs | 38 ++++++++------ 3 files changed, 67 insertions(+), 56 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 33a431bf857..10d1feabba8 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1962,6 +1962,7 @@ pub(super) struct FundingScope { funding_transaction: Option, /// The hash of the block in which the funding transaction was included. funding_tx_confirmed_in: Option, + funding_tx_confirmation_height: u32, } impl Writeable for FundingScope { @@ -1973,6 +1974,7 @@ impl Writeable for FundingScope { (7, self.channel_transaction_parameters, (required: ReadableArgs, None)), (9, self.funding_transaction, option), (11, self.funding_tx_confirmed_in, option), + (13, self.funding_tx_confirmation_height, required), }); Ok(()) } @@ -1987,6 +1989,7 @@ impl Readable for FundingScope { let mut channel_transaction_parameters = RequiredWrapper(None); let mut funding_transaction = None; let mut funding_tx_confirmed_in = None; + let mut funding_tx_confirmation_height = RequiredWrapper(None); read_tlv_fields!(reader, { (1, value_to_self_msat, required), @@ -1995,6 +1998,7 @@ impl Readable for FundingScope { (7, channel_transaction_parameters, (required: ReadableArgs, None)), (9, funding_transaction, option), (11, funding_tx_confirmed_in, option), + (13, funding_tx_confirmation_height, required), }); Ok(Self { @@ -2008,6 +2012,7 @@ impl Readable for FundingScope { channel_transaction_parameters: channel_transaction_parameters.0.unwrap(), funding_transaction, funding_tx_confirmed_in, + funding_tx_confirmation_height: funding_tx_confirmation_height.0.unwrap(), #[cfg(any(test, fuzzing))] next_local_commitment_tx_fee_info_cached: Mutex::new(None), #[cfg(any(test, fuzzing))] @@ -2084,6 +2089,26 @@ impl FundingScope { pub fn get_channel_type(&self) -> &ChannelTypeFeatures { &self.channel_transaction_parameters.channel_type_features } + + /// Returns the height in which our funding transaction was confirmed. + pub fn get_funding_tx_confirmation_height(&self) -> Option { + let conf_height = self.funding_tx_confirmation_height; + if conf_height > 0 { + Some(conf_height) + } else { + None + } + } + + /// Returns the current number of confirmations on the funding transaction. + pub fn get_funding_tx_confirmations(&self, height: u32) -> u32 { + if self.funding_tx_confirmation_height == 0 { + // We either haven't seen any confirmation yet, or observed a reorg. + return 0; + } + + height.checked_sub(self.funding_tx_confirmation_height).map_or(0, |c| c + 1) + } } /// Info about a pending splice, used in the pre-splice channel @@ -2245,7 +2270,6 @@ where /// milliseconds, so any accidental force-closes here should be exceedingly rare. expecting_peer_commitment_signed: bool, - funding_tx_confirmation_height: u32, short_channel_id: Option, /// Either the height at which this channel was created or the height at which it was last /// serialized if it was serialized by versions prior to 0.0.103. @@ -3093,6 +3117,7 @@ where }, funding_transaction: None, funding_tx_confirmed_in: None, + funding_tx_confirmation_height: 0, }; let channel_context = ChannelContext { user_id, @@ -3156,7 +3181,6 @@ where closing_fee_limits: None, target_closing_feerate_sats_per_kw: None, - funding_tx_confirmation_height: 0, short_channel_id: None, channel_creation_height: current_chain_height, @@ -3334,6 +3358,7 @@ where }, funding_transaction: None, funding_tx_confirmed_in: None, + funding_tx_confirmation_height: 0, }; let channel_context = Self { user_id, @@ -3395,7 +3420,6 @@ where closing_fee_limits: None, target_closing_feerate_sats_per_kw: None, - funding_tx_confirmation_height: 0, short_channel_id: None, channel_creation_height: current_chain_height, @@ -3651,16 +3675,6 @@ where self.outbound_scid_alias = outbound_scid_alias; } - /// Returns the height in which our funding transaction was confirmed. - pub fn get_funding_tx_confirmation_height(&self) -> Option { - let conf_height = self.funding_tx_confirmation_height; - if conf_height > 0 { - Some(conf_height) - } else { - None - } - } - /// Performs checks against necessary constraints after receiving either an `accept_channel` or /// `accept_channel2` message. #[rustfmt::skip] @@ -3802,16 +3816,6 @@ where Ok(()) } - /// Returns the current number of confirmations on the funding transaction. - pub fn get_funding_tx_confirmations(&self, height: u32) -> u32 { - if self.funding_tx_confirmation_height == 0 { - // We either haven't seen any confirmation yet, or observed a reorg. - return 0; - } - - height.checked_sub(self.funding_tx_confirmation_height).map_or(0, |c| c + 1) - } - /// Allowed in any state (including after shutdown) pub fn get_counterparty_node_id(&self) -> PublicKey { self.counterparty_node_id @@ -7362,7 +7366,7 @@ where matches!(self.context.channel_state, ChannelState::ChannelReady(_))) { // Broadcast only if not yet confirmed - if self.context.get_funding_tx_confirmation_height().is_none() { + if self.funding.get_funding_tx_confirmation_height().is_none() { funding_broadcastable = Some(funding_transaction.clone()) } } @@ -8771,13 +8775,13 @@ where // Called: // * always when a new block/transactions are confirmed with the new height // * when funding is signed with a height of 0 - if self.context.funding_tx_confirmation_height == 0 && self.context.minimum_depth != Some(0) { + if self.funding.funding_tx_confirmation_height == 0 && self.context.minimum_depth != Some(0) { return None; } - let funding_tx_confirmations = height as i64 - self.context.funding_tx_confirmation_height as i64 + 1; + let funding_tx_confirmations = height as i64 - self.funding.funding_tx_confirmation_height as i64 + 1; if funding_tx_confirmations <= 0 { - self.context.funding_tx_confirmation_height = 0; + self.funding.funding_tx_confirmation_height = 0; } if funding_tx_confirmations < self.context.minimum_depth.unwrap_or(0) as i64 { @@ -8797,7 +8801,7 @@ where // We got a reorg but not enough to trigger a force close, just ignore. false } else { - if self.context.funding_tx_confirmation_height != 0 && + if self.funding.funding_tx_confirmation_height != 0 && self.context.channel_state < ChannelState::ChannelReady(ChannelReadyFlags::new()) { // We should never see a funding transaction on-chain until we've received @@ -8867,7 +8871,7 @@ where for &(index_in_block, tx) in txdata.iter() { // Check if the transaction is the expected funding transaction, and if it is, // check that it pays the right amount to the right script. - if self.context.funding_tx_confirmation_height == 0 { + if self.funding.funding_tx_confirmation_height == 0 { if tx.compute_txid() == funding_txo.txid { let txo_idx = funding_txo.index as usize; if txo_idx >= tx.output.len() || tx.output[txo_idx].script_pubkey != self.funding.get_funding_redeemscript().to_p2wsh() || @@ -8897,7 +8901,8 @@ where } } } - self.context.funding_tx_confirmation_height = height; + + self.funding.funding_tx_confirmation_height = height; self.funding.funding_tx_confirmed_in = Some(*block_hash); self.context.short_channel_id = match scid_from_parts(height as u64, index_in_block as u64, txo_idx as u64) { Ok(scid) => Some(scid), @@ -8997,8 +9002,8 @@ where if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) || self.context.channel_state.is_our_channel_ready() { - let mut funding_tx_confirmations = height as i64 - self.context.funding_tx_confirmation_height as i64 + 1; - if self.context.funding_tx_confirmation_height == 0 { + let mut funding_tx_confirmations = height as i64 - self.funding.funding_tx_confirmation_height as i64 + 1; + if self.funding.funding_tx_confirmation_height == 0 { // Note that check_get_channel_ready may reset funding_tx_confirmation_height to // zero if it has been reorged out, however in either case, our state flags // indicate we've already sent a channel_ready @@ -9039,10 +9044,10 @@ where /// before the channel has reached channel_ready and we can just wait for more blocks. #[rustfmt::skip] pub fn funding_transaction_unconfirmed(&mut self, logger: &L) -> Result<(), ClosureReason> where L::Target: Logger { - if self.context.funding_tx_confirmation_height != 0 { + if self.funding.funding_tx_confirmation_height != 0 { // We handle the funding disconnection by calling best_block_updated with a height one // below where our funding was connected, implying a reorg back to conf_height - 1. - let reorg_height = self.context.funding_tx_confirmation_height - 1; + let reorg_height = self.funding.funding_tx_confirmation_height - 1; // We use the time field to bump the current time we set on channel updates if its // larger. If we don't know that time has moved forward, we can just set it to the last // time we saw and it will be ignored. @@ -9117,7 +9122,7 @@ where NS::Target: NodeSigner, L::Target: Logger { - if self.context.funding_tx_confirmation_height == 0 || self.context.funding_tx_confirmation_height + 5 > best_block_height { + if self.funding.funding_tx_confirmation_height == 0 || self.funding.funding_tx_confirmation_height + 5 > best_block_height { return None; } @@ -9240,7 +9245,7 @@ where } self.context.announcement_sigs = Some((msg.node_signature, msg.bitcoin_signature)); - if self.context.funding_tx_confirmation_height == 0 || self.context.funding_tx_confirmation_height + 5 > best_block_height { + if self.funding.funding_tx_confirmation_height == 0 || self.funding.funding_tx_confirmation_height + 5 > best_block_height { return Err(ChannelError::Ignore( "Got announcement_signatures prior to the required six confirmations - we may not have received a block yet that our peer has".to_owned())); } @@ -9254,7 +9259,7 @@ where pub fn get_signed_channel_announcement( &self, node_signer: &NS, chain_hash: ChainHash, best_block_height: u32, user_config: &UserConfig ) -> Option where NS::Target: NodeSigner { - if self.context.funding_tx_confirmation_height == 0 || self.context.funding_tx_confirmation_height + 5 > best_block_height { + if self.funding.funding_tx_confirmation_height == 0 || self.funding.funding_tx_confirmation_height + 5 > best_block_height { return None; } let announcement = match self.get_channel_announcement(node_signer, chain_hash, user_config) { @@ -11506,7 +11511,7 @@ where 0u8.write(writer)?; self.funding.funding_tx_confirmed_in.write(writer)?; - self.context.funding_tx_confirmation_height.write(writer)?; + self.funding.funding_tx_confirmation_height.write(writer)?; self.context.short_channel_id.write(writer)?; self.context.counterparty_dust_limit_satoshis.write(writer)?; @@ -12165,6 +12170,7 @@ where channel_transaction_parameters: channel_parameters, funding_transaction, funding_tx_confirmed_in, + funding_tx_confirmation_height, }, pending_funding: pending_funding.unwrap(), context: ChannelContext { @@ -12228,7 +12234,6 @@ where closing_fee_limits: None, target_closing_feerate_sats_per_kw, - funding_tx_confirmation_height, short_channel_id, channel_creation_height, diff --git a/lightning/src/ln/channel_state.rs b/lightning/src/ln/channel_state.rs index c941e0eb9d0..f822b664166 100644 --- a/lightning/src/ln/channel_state.rs +++ b/lightning/src/ln/channel_state.rs @@ -532,7 +532,7 @@ impl ChannelDetails { next_outbound_htlc_minimum_msat: balance.next_outbound_htlc_minimum_msat, user_channel_id: context.get_user_id(), confirmations_required: context.minimum_depth(), - confirmations: Some(context.get_funding_tx_confirmations(best_block_height)), + confirmations: Some(funding.get_funding_tx_confirmations(best_block_height)), force_close_spend_delay: funding.get_counterparty_selected_contest_delay(), is_outbound: funding.is_outbound(), is_channel_ready: context.is_usable(), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index ddd794cce4b..54addb295c2 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3164,7 +3164,7 @@ macro_rules! handle_error { /// [`ChannelMonitor`]/channel funding transaction) to begin with. #[rustfmt::skip] macro_rules! locked_close_channel { - ($self: ident, $peer_state: expr, $channel_context: expr, $shutdown_res_mut: expr) => {{ + ($self: ident, $peer_state: expr, $channel_context: expr, $channel_funding: expr, $shutdown_res_mut: expr) => {{ if let Some((_, funding_txo, _, update)) = $shutdown_res_mut.monitor_update.take() { handle_new_monitor_update!($self, funding_txo, update, $peer_state, $channel_context, REMAIN_LOCKED_UPDATE_ACTIONS_PROCESSED_LATER); @@ -3174,7 +3174,7 @@ macro_rules! locked_close_channel { // into the map (which prevents the `PeerState` from being cleaned up) for channels that // never even got confirmations (which would open us up to DoS attacks). let update_id = $channel_context.get_latest_monitor_update_id(); - if $channel_context.get_funding_tx_confirmation_height().is_some() || $channel_context.minimum_depth() == Some(0) || update_id > 1 { + if $channel_funding.get_funding_tx_confirmation_height().is_some() || $channel_context.minimum_depth() == Some(0) || update_id > 1 { let chan_id = $channel_context.channel_id(); $peer_state.closed_channel_monitor_update_ids.insert(chan_id, update_id); } @@ -3213,7 +3213,7 @@ macro_rules! convert_channel_err { let logger = WithChannelContext::from(&$self.logger, &$context, None); log_error!(logger, "Closing channel {} due to close-required error: {}", $channel_id, msg); let mut shutdown_res = $context.force_shutdown($funding, true, reason); - locked_close_channel!($self, $peer_state, $context, &mut shutdown_res); + locked_close_channel!($self, $peer_state, $context, $funding, &mut shutdown_res); let err = MsgHandleErrInternal::from_finish_shutdown(msg, *$channel_id, shutdown_res, $channel_update); (true, err) @@ -3279,7 +3279,13 @@ macro_rules! try_channel_entry { macro_rules! remove_channel_entry { ($self: ident, $peer_state: expr, $entry: expr, $shutdown_res_mut: expr) => {{ let channel = $entry.remove_entry().1; - locked_close_channel!($self, $peer_state, &channel.context(), $shutdown_res_mut); + locked_close_channel!( + $self, + $peer_state, + &channel.context(), + channel.funding(), + $shutdown_res_mut + ); channel }}; } @@ -4298,7 +4304,7 @@ where let mut peer_state = peer_state_mutex.lock().unwrap(); if let Some(mut chan) = peer_state.channel_by_id.remove(&channel_id) { let mut close_res = chan.force_shutdown(false, ClosureReason::FundingBatchClosure); - locked_close_channel!(self, &mut *peer_state, chan.context(), close_res); + locked_close_channel!(self, &mut *peer_state, chan.context(), chan.funding(), close_res); shutdown_results.push(close_res); } } @@ -5841,7 +5847,7 @@ where .map(|(mut chan, mut peer_state)| { let closure_reason = ClosureReason::ProcessingError { err: e.clone() }; let mut close_res = chan.force_shutdown(false, closure_reason); - locked_close_channel!(self, peer_state, chan.context(), close_res); + locked_close_channel!(self, peer_state, chan.context(), chan.funding(), close_res); shutdown_results.push(close_res); peer_state.pending_msg_events.push(MessageSendEvent::HandleError { node_id: counterparty_node_id, @@ -7093,8 +7099,8 @@ where "Force-closing pending channel with ID {} for not establishing in a timely manner", context.channel_id()); let mut close_res = chan.force_shutdown(false, ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }); - let context = chan.context_mut(); - locked_close_channel!(self, peer_state, context, close_res); + let (funding, context) = chan.funding_and_context_mut(); + locked_close_channel!(self, peer_state, context, funding, close_res); shutdown_channels.push(close_res); pending_msg_events.push(MessageSendEvent::HandleError { node_id: context.get_counterparty_node_id(), @@ -8444,7 +8450,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ // This covers non-zero-conf inbound `Channel`s that we are currently monitoring, but those // which have not yet had any confirmations on-chain. if !funded_chan.funding.is_outbound() && funded_chan.context.minimum_depth().unwrap_or(1) != 0 && - funded_chan.context.get_funding_tx_confirmations(best_block_height) == 0 + funded_chan.funding.get_funding_tx_confirmations(best_block_height) == 0 { num_unfunded_channels += 1; } @@ -10358,7 +10364,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let context = &chan.context(); let logger = WithChannelContext::from(&self.logger, context, None); log_trace!(logger, "Removing channel {} now that the signer is unblocked", context.channel_id()); - locked_close_channel!(self, peer_state, context, shutdown_result); + locked_close_channel!(self, peer_state, context, chan.funding(), shutdown_result); shutdown_results.push(shutdown_result); false } else { @@ -10401,7 +10407,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } debug_assert_eq!(shutdown_result_opt.is_some(), funded_chan.is_shutdown()); if let Some(mut shutdown_result) = shutdown_result_opt { - locked_close_channel!(self, peer_state, &funded_chan.context, shutdown_result); + locked_close_channel!(self, peer_state, &funded_chan.context, &funded_chan.funding, shutdown_result); shutdown_results.push(shutdown_result); } if let Some(tx) = tx_opt { @@ -11617,8 +11623,8 @@ where } // Clean up for removal. let mut close_res = chan.force_shutdown(false, ClosureReason::DisconnectedPeer); - let context = chan.context_mut(); - locked_close_channel!(self, peer_state, &context, close_res); + let (funding, context) = chan.funding_and_context_mut(); + locked_close_channel!(self, peer_state, &context, funding, close_res); failed_channels.push(close_res); false }); @@ -12083,7 +12089,7 @@ where let peer_state = &mut *peer_state_lock; for chan in peer_state.channel_by_id.values().filter_map(Channel::as_funded) { let txid_opt = chan.funding.get_funding_txo(); - let height_opt = chan.context.get_funding_tx_confirmation_height(); + let height_opt = chan.funding.get_funding_tx_confirmation_height(); let hash_opt = chan.get_funding_tx_confirmed_in(); if let (Some(funding_txo), Some(conf_height), Some(block_hash)) = (txid_opt, height_opt, hash_opt) @@ -12204,7 +12210,7 @@ where // (re-)broadcast signed `channel_announcement`s and // `channel_update`s for any channels less than a week old. let funding_conf_height = - funded_channel.context.get_funding_tx_confirmation_height().unwrap_or(height); + funded_channel.funding.get_funding_tx_confirmation_height().unwrap_or(height); // To avoid broadcast storms after each block, only // re-broadcast every hour (6 blocks) after the initial // broadcast, or if this is the first time we're ready to @@ -12262,7 +12268,7 @@ where // reorged out of the main chain. Close the channel. let reason_message = format!("{}", reason); let mut close_res = funded_channel.context.force_shutdown(&funded_channel.funding, true, reason); - locked_close_channel!(self, peer_state, &funded_channel.context, close_res); + locked_close_channel!(self, peer_state, &funded_channel.context, &funded_channel.funding, close_res); failed_channels.push(close_res); if let Ok(update) = self.get_channel_update_for_broadcast(&funded_channel) { let mut pending_broadcast_messages = self.pending_broadcast_messages.lock().unwrap(); From fa5b2521374664069795d100f4a45c4b47682cde Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 15 Apr 2025 16:20:57 -0500 Subject: [PATCH 04/19] Move ChannelContext::short_channel_id to FundingScope When processing confirmed transactions, if the funding transaction is found then information about it in the ChannelContext is updated. In preparation for splicing, move this data to FundingScope. --- lightning/src/ln/channel.rs | 36 +++++++++++++++++------------- lightning/src/ln/channel_state.rs | 2 +- lightning/src/ln/channelmanager.rs | 20 ++++++++--------- lightning/src/ln/payment_tests.rs | 14 ++++++------ 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 10d1feabba8..6d7275f8530 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1963,6 +1963,7 @@ pub(super) struct FundingScope { /// The hash of the block in which the funding transaction was included. funding_tx_confirmed_in: Option, funding_tx_confirmation_height: u32, + short_channel_id: Option, } impl Writeable for FundingScope { @@ -1975,6 +1976,7 @@ impl Writeable for FundingScope { (9, self.funding_transaction, option), (11, self.funding_tx_confirmed_in, option), (13, self.funding_tx_confirmation_height, required), + (15, self.short_channel_id, option), }); Ok(()) } @@ -1990,6 +1992,7 @@ impl Readable for FundingScope { let mut funding_transaction = None; let mut funding_tx_confirmed_in = None; let mut funding_tx_confirmation_height = RequiredWrapper(None); + let mut short_channel_id = None; read_tlv_fields!(reader, { (1, value_to_self_msat, required), @@ -1999,6 +2002,7 @@ impl Readable for FundingScope { (9, funding_transaction, option), (11, funding_tx_confirmed_in, option), (13, funding_tx_confirmation_height, required), + (15, short_channel_id, option), }); Ok(Self { @@ -2013,6 +2017,7 @@ impl Readable for FundingScope { funding_transaction, funding_tx_confirmed_in, funding_tx_confirmation_height: funding_tx_confirmation_height.0.unwrap(), + short_channel_id, #[cfg(any(test, fuzzing))] next_local_commitment_tx_fee_info_cached: Mutex::new(None), #[cfg(any(test, fuzzing))] @@ -2109,6 +2114,13 @@ impl FundingScope { height.checked_sub(self.funding_tx_confirmation_height).map_or(0, |c| c + 1) } + + /// Gets the channel's `short_channel_id`. + /// + /// Will return `None` if the funding hasn't been confirmed yet. + pub fn get_short_channel_id(&self) -> Option { + self.short_channel_id + } } /// Info about a pending splice, used in the pre-splice channel @@ -2270,7 +2282,6 @@ where /// milliseconds, so any accidental force-closes here should be exceedingly rare. expecting_peer_commitment_signed: bool, - short_channel_id: Option, /// Either the height at which this channel was created or the height at which it was last /// serialized if it was serialized by versions prior to 0.0.103. /// We use this to close if funding is never broadcasted. @@ -3118,6 +3129,7 @@ where funding_transaction: None, funding_tx_confirmed_in: None, funding_tx_confirmation_height: 0, + short_channel_id: None, }; let channel_context = ChannelContext { user_id, @@ -3181,7 +3193,6 @@ where closing_fee_limits: None, target_closing_feerate_sats_per_kw: None, - short_channel_id: None, channel_creation_height: current_chain_height, feerate_per_kw: open_channel_fields.commitment_feerate_sat_per_1000_weight, @@ -3359,6 +3370,7 @@ where funding_transaction: None, funding_tx_confirmed_in: None, funding_tx_confirmation_height: 0, + short_channel_id: None, }; let channel_context = Self { user_id, @@ -3420,7 +3432,6 @@ where closing_fee_limits: None, target_closing_feerate_sats_per_kw: None, - short_channel_id: None, channel_creation_height: current_chain_height, feerate_per_kw: commitment_feerate, @@ -3644,13 +3655,6 @@ where self.user_id } - /// Gets the channel's `short_channel_id`. - /// - /// Will return `None` if the channel hasn't been confirmed yet. - pub fn get_short_channel_id(&self) -> Option { - self.short_channel_id - } - /// Allowed in any state (including after shutdown) pub fn latest_inbound_scid_alias(&self) -> Option { self.latest_inbound_scid_alias @@ -6192,7 +6196,7 @@ where } if let Some(scid_alias) = msg.short_channel_id_alias { - if Some(scid_alias) != self.context.short_channel_id { + if Some(scid_alias) != self.funding.short_channel_id { // The scid alias provided can be used to route payments *from* our counterparty, // i.e. can be used for inbound payments and provided in invoices, but is not used // when routing outbound payments. @@ -8904,7 +8908,7 @@ where self.funding.funding_tx_confirmation_height = height; self.funding.funding_tx_confirmed_in = Some(*block_hash); - self.context.short_channel_id = match scid_from_parts(height as u64, index_in_block as u64, txo_idx as u64) { + self.funding.short_channel_id = match scid_from_parts(height as u64, index_in_block as u64, txo_idx as u64) { Ok(scid) => Some(scid), Err(_) => panic!("Block was bogus - either height was > 16 million, had > 16 million transactions, or had > 65k outputs"), } @@ -9092,7 +9096,7 @@ where return Err(ChannelError::Ignore("Cannot get a ChannelAnnouncement if the channel is not currently usable".to_owned())); } - let short_channel_id = self.context.get_short_channel_id() + let short_channel_id = self.funding.get_short_channel_id() .ok_or(ChannelError::Ignore("Cannot get a ChannelAnnouncement if the channel has not been confirmed yet".to_owned()))?; let node_id = NodeId::from_pubkey(&node_signer.get_node_id(Recipient::Node) .map_err(|_| ChannelError::Ignore("Failed to retrieve own public key".to_owned()))?); @@ -9165,7 +9169,7 @@ where }, Ok(v) => v }; - let short_channel_id = match self.context.get_short_channel_id() { + let short_channel_id = match self.funding.get_short_channel_id() { Some(scid) => scid, None => return None, }; @@ -11512,7 +11516,7 @@ where self.funding.funding_tx_confirmed_in.write(writer)?; self.funding.funding_tx_confirmation_height.write(writer)?; - self.context.short_channel_id.write(writer)?; + self.funding.short_channel_id.write(writer)?; self.context.counterparty_dust_limit_satoshis.write(writer)?; self.context.holder_dust_limit_satoshis.write(writer)?; @@ -12171,6 +12175,7 @@ where funding_transaction, funding_tx_confirmed_in, funding_tx_confirmation_height, + short_channel_id, }, pending_funding: pending_funding.unwrap(), context: ChannelContext { @@ -12234,7 +12239,6 @@ where closing_fee_limits: None, target_closing_feerate_sats_per_kw, - short_channel_id, channel_creation_height, counterparty_dust_limit_satoshis, diff --git a/lightning/src/ln/channel_state.rs b/lightning/src/ln/channel_state.rs index f822b664166..99e2833f644 100644 --- a/lightning/src/ln/channel_state.rs +++ b/lightning/src/ln/channel_state.rs @@ -516,7 +516,7 @@ impl ChannelDetails { } else { None }, - short_channel_id: context.get_short_channel_id(), + short_channel_id: funding.get_short_channel_id(), outbound_scid_alias: if context.is_usable() { Some(context.outbound_scid_alias()) } else { diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 54addb295c2..bab95ad0d90 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3179,7 +3179,7 @@ macro_rules! locked_close_channel { $peer_state.closed_channel_monitor_update_ids.insert(chan_id, update_id); } let mut short_to_chan_info = $self.short_to_chan_info.write().unwrap(); - if let Some(short_id) = $channel_context.get_short_channel_id() { + if let Some(short_id) = $channel_funding.get_short_channel_id() { short_to_chan_info.remove(&short_id); } else { // If the channel was never confirmed on-chain prior to its closure, remove the @@ -3302,7 +3302,7 @@ macro_rules! send_channel_ready { let outbound_alias_insert = short_to_chan_info.insert($channel.context.outbound_scid_alias(), ($channel.context.get_counterparty_node_id(), $channel.context.channel_id())); assert!(outbound_alias_insert.is_none() || outbound_alias_insert.unwrap() == ($channel.context.get_counterparty_node_id(), $channel.context.channel_id()), "SCIDs should never collide - ensure you weren't behind the chain tip by a full month when creating channels"); - if let Some(real_scid) = $channel.context.get_short_channel_id() { + if let Some(real_scid) = $channel.funding.get_short_channel_id() { let scid_insert = short_to_chan_info.insert(real_scid, ($channel.context.get_counterparty_node_id(), $channel.context.channel_id())); assert!(scid_insert.is_none() || scid_insert.unwrap() == ($channel.context.get_counterparty_node_id(), $channel.context.channel_id()), "SCIDs should never collide - ensure you weren't behind the chain tip by a full month when creating channels"); @@ -4797,7 +4797,7 @@ where action: msgs::ErrorAction::IgnoreError, }); } - if chan.context.get_short_channel_id().is_none() { + if chan.funding.get_short_channel_id().is_none() { return Err(LightningError { err: "Channel not yet established".to_owned(), action: msgs::ErrorAction::IgnoreError, @@ -4827,7 +4827,7 @@ where fn get_channel_update_for_unicast(&self, chan: &FundedChannel) -> Result { let logger = WithChannelContext::from(&self.logger, &chan.context, None); log_trace!(logger, "Attempting to generate channel update for channel {}", chan.context.channel_id()); - let short_channel_id = match chan.context.get_short_channel_id().or(chan.context.latest_inbound_scid_alias()) { + let short_channel_id = match chan.funding.get_short_channel_id().or(chan.context.latest_inbound_scid_alias()) { None => return Err(LightningError{err: "Channel not yet established".to_owned(), action: msgs::ErrorAction::IgnoreError}), Some(id) => id, }; @@ -6017,7 +6017,7 @@ where err: format!("Channel with id {} not fully established", next_hop_channel_id) }) } - funded_chan.context.get_short_channel_id().unwrap_or(funded_chan.context.outbound_scid_alias()) + funded_chan.funding.get_short_channel_id().unwrap_or(funded_chan.context.outbound_scid_alias()) } else { return Err(APIError::ChannelUnavailable { err: format!("Channel with id {} for the passed counterparty node_id {} is still opening.", @@ -6466,7 +6466,7 @@ where }; let logger = WithChannelContext::from(&self.logger, &optimal_channel.context, Some(payment_hash)); - let channel_description = if optimal_channel.context.get_short_channel_id() == Some(short_chan_id) { + let channel_description = if optimal_channel.funding.get_short_channel_id() == Some(short_chan_id) { "specified" } else { "alternate" @@ -8058,7 +8058,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ); let counterparty_node_id = channel.context.get_counterparty_node_id(); - let short_channel_id = channel.context.get_short_channel_id().unwrap_or(channel.context.outbound_scid_alias()); + let short_channel_id = channel.funding.get_short_channel_id().unwrap_or(channel.context.outbound_scid_alias()); let mut htlc_forwards = None; if !pending_forwards.is_empty() { @@ -11302,7 +11302,7 @@ where .iter() .filter(|(_, channel)| channel.context().is_usable()) .min_by_key(|(_, channel)| channel.context().channel_creation_height) - .and_then(|(_, channel)| channel.context().get_short_channel_id()), + .and_then(|(_, channel)| channel.funding().get_short_channel_id()), }) .collect::>() } @@ -12249,7 +12249,7 @@ where }); } if funded_channel.is_our_channel_ready() { - if let Some(real_scid) = funded_channel.context.get_short_channel_id() { + if let Some(real_scid) = funded_channel.funding.get_short_channel_id() { // If we sent a 0conf channel_ready, and now have an SCID, we add it // to the short_to_chan_info map here. Note that we check whether we // can relay using the real SCID at relay-time (i.e. @@ -14431,7 +14431,7 @@ where log_info!(logger, "Successfully loaded channel {} at update_id {} against monitor at update id {} with {} blocked updates", &channel.context.channel_id(), channel.context.get_latest_monitor_update_id(), monitor.get_latest_update_id(), channel.blocked_monitor_updates_pending()); - if let Some(short_channel_id) = channel.context.get_short_channel_id() { + if let Some(short_channel_id) = channel.funding.get_short_channel_id() { short_to_chan_info.insert(short_channel_id, (channel.context.get_counterparty_node_id(), channel.context.channel_id())); } per_peer_state.entry(channel.context.get_counterparty_node_id()) diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index 9c8c63208dd..9fde71ad72e 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -1927,7 +1927,7 @@ fn test_trivial_inflight_htlc_tracking() { let chan_1_used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_a_id), &NodeId::from_pubkey(&node_b_id), - channel_1.context().get_short_channel_id().unwrap(), + channel_1.funding().get_short_channel_id().unwrap(), ); assert_eq!(chan_1_used_liquidity, None); } @@ -1940,7 +1940,7 @@ fn test_trivial_inflight_htlc_tracking() { let chan_2_used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_b_id), &NodeId::from_pubkey(&node_c_id), - channel_2.context().get_short_channel_id().unwrap(), + channel_2.funding().get_short_channel_id().unwrap(), ); assert_eq!(chan_2_used_liquidity, None); @@ -1968,7 +1968,7 @@ fn test_trivial_inflight_htlc_tracking() { let chan_1_used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_a_id), &NodeId::from_pubkey(&node_b_id), - channel_1.context().get_short_channel_id().unwrap(), + channel_1.funding().get_short_channel_id().unwrap(), ); // First hop accounts for expected 1000 msat fee assert_eq!(chan_1_used_liquidity, Some(501000)); @@ -1982,7 +1982,7 @@ fn test_trivial_inflight_htlc_tracking() { let chan_2_used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_b_id), &NodeId::from_pubkey(&node_c_id), - channel_2.context().get_short_channel_id().unwrap(), + channel_2.funding().get_short_channel_id().unwrap(), ); assert_eq!(chan_2_used_liquidity, Some(500000)); @@ -2010,7 +2010,7 @@ fn test_trivial_inflight_htlc_tracking() { let chan_1_used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_a_id), &NodeId::from_pubkey(&node_b_id), - channel_1.context().get_short_channel_id().unwrap(), + channel_1.funding().get_short_channel_id().unwrap(), ); assert_eq!(chan_1_used_liquidity, None); } @@ -2023,7 +2023,7 @@ fn test_trivial_inflight_htlc_tracking() { let chan_2_used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_b_id), &NodeId::from_pubkey(&node_c_id), - channel_2.context().get_short_channel_id().unwrap(), + channel_2.funding().get_short_channel_id().unwrap(), ); assert_eq!(chan_2_used_liquidity, None); } @@ -2073,7 +2073,7 @@ fn test_holding_cell_inflight_htlcs() { let used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_a_id), &NodeId::from_pubkey(&node_b_id), - channel.context().get_short_channel_id().unwrap(), + channel.funding().get_short_channel_id().unwrap(), ); assert_eq!(used_liquidity, Some(2000000)); From 19b775a3f421027d88387b50c4ffc0009a31b820 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 17 Apr 2025 10:51:56 -0500 Subject: [PATCH 05/19] Refactor funding tx confirmation check into helper When checking if channel_ready should be sent, the funding transaction must reach minimum_depth confirmations. The same logic is needed for splicing a channel, so refactor it into a helper method. --- lightning/src/ln/channel.rs | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 6d7275f8530..9cb5db9cd50 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -5465,6 +5465,24 @@ where self.counterparty_cur_commitment_point = Some(counterparty_cur_commitment_point_override); self.get_initial_counterparty_commitment_signature(funding, logger) } + + fn check_funding_meets_minimum_depth(&self, funding: &mut FundingScope, height: u32) -> bool { + if funding.funding_tx_confirmation_height == 0 && self.minimum_depth != Some(0) { + return false; + } + + let funding_tx_confirmations = + height as i64 - funding.funding_tx_confirmation_height as i64 + 1; + if funding_tx_confirmations <= 0 { + funding.funding_tx_confirmation_height = 0; + } + + if funding_tx_confirmations < self.minimum_depth.unwrap_or(0) as i64 { + return false; + } + + return true; + } } // Internal utility functions for channels @@ -8779,16 +8797,7 @@ where // Called: // * always when a new block/transactions are confirmed with the new height // * when funding is signed with a height of 0 - if self.funding.funding_tx_confirmation_height == 0 && self.context.minimum_depth != Some(0) { - return None; - } - - let funding_tx_confirmations = height as i64 - self.funding.funding_tx_confirmation_height as i64 + 1; - if funding_tx_confirmations <= 0 { - self.funding.funding_tx_confirmation_height = 0; - } - - if funding_tx_confirmations < self.context.minimum_depth.unwrap_or(0) as i64 { + if !self.context.check_funding_meets_minimum_depth(&mut self.funding, height) { return None; } From 27be59b583b20d88a64284b7e71b278142f56d7e Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 23 Apr 2025 10:37:14 -0700 Subject: [PATCH 06/19] Add FundingScope::minimum_depth_override When a channel is funded using the coinbase transaction, the minimum depth must be at least COINBASE_MATURITY. Instead of overriding it in ChannelContext, add FundingScope::minimum_depth_override for this purpose. Otherwise, if the overridden minimum_depth were reused in a splice's minimum dpeth, then sending splice_locked would be unnecessarily delayed. Later, this can be used to override the minimum depth needed for a splice. --- lightning/src/ln/channel.rs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 9cb5db9cd50..473d4fdbae8 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1964,6 +1964,10 @@ pub(super) struct FundingScope { funding_tx_confirmed_in: Option, funding_tx_confirmation_height: u32, short_channel_id: Option, + + /// The minimum number of confirmations before the funding is locked. If set, this will override + /// [`ChannelContext::minimum_depth`]. + minimum_depth_override: Option, } impl Writeable for FundingScope { @@ -1977,6 +1981,7 @@ impl Writeable for FundingScope { (11, self.funding_tx_confirmed_in, option), (13, self.funding_tx_confirmation_height, required), (15, self.short_channel_id, option), + (17, self.minimum_depth_override, option), }); Ok(()) } @@ -1993,6 +1998,7 @@ impl Readable for FundingScope { let mut funding_tx_confirmed_in = None; let mut funding_tx_confirmation_height = RequiredWrapper(None); let mut short_channel_id = None; + let mut minimum_depth_override = None; read_tlv_fields!(reader, { (1, value_to_self_msat, required), @@ -2003,6 +2009,7 @@ impl Readable for FundingScope { (11, funding_tx_confirmed_in, option), (13, funding_tx_confirmation_height, required), (15, short_channel_id, option), + (17, minimum_depth_override, option), }); Ok(Self { @@ -2018,6 +2025,7 @@ impl Readable for FundingScope { funding_tx_confirmed_in, funding_tx_confirmation_height: funding_tx_confirmation_height.0.unwrap(), short_channel_id, + minimum_depth_override, #[cfg(any(test, fuzzing))] next_local_commitment_tx_fee_info_cached: Mutex::new(None), #[cfg(any(test, fuzzing))] @@ -3130,6 +3138,7 @@ where funding_tx_confirmed_in: None, funding_tx_confirmation_height: 0, short_channel_id: None, + minimum_depth_override: None, }; let channel_context = ChannelContext { user_id, @@ -3371,6 +3380,7 @@ where funding_tx_confirmed_in: None, funding_tx_confirmation_height: 0, short_channel_id: None, + minimum_depth_override: None, }; let channel_context = Self { user_id, @@ -5467,7 +5477,9 @@ where } fn check_funding_meets_minimum_depth(&self, funding: &mut FundingScope, height: u32) -> bool { - if funding.funding_tx_confirmation_height == 0 && self.minimum_depth != Some(0) { + let minimum_depth = funding.minimum_depth_override.or(self.minimum_depth); + + if funding.funding_tx_confirmation_height == 0 && minimum_depth != Some(0) { return false; } @@ -5477,7 +5489,7 @@ where funding.funding_tx_confirmation_height = 0; } - if funding_tx_confirmations < self.minimum_depth.unwrap_or(0) as i64 { + if funding_tx_confirmations < minimum_depth.unwrap_or(0) as i64 { return false; } @@ -8927,7 +8939,7 @@ where if tx.is_coinbase() && self.context.minimum_depth.unwrap_or(0) > 0 && self.context.minimum_depth.unwrap_or(0) < COINBASE_MATURITY { - self.context.minimum_depth = Some(COINBASE_MATURITY); + self.funding.minimum_depth_override = Some(COINBASE_MATURITY); } } // If we allow 1-conf funding, we may need to check for channel_ready here and @@ -10369,7 +10381,7 @@ where if funding_transaction.is_coinbase() && self.context.minimum_depth.unwrap_or(0) > 0 && self.context.minimum_depth.unwrap_or(0) < COINBASE_MATURITY { - self.context.minimum_depth = Some(COINBASE_MATURITY); + self.funding.minimum_depth_override = Some(COINBASE_MATURITY); } debug_assert!(self.funding.funding_transaction.is_none()); @@ -11650,7 +11662,8 @@ where (54, self.pending_funding, optional_vec), // Added in 0.2 (55, removed_htlc_failure_attribution_data, optional_vec), // Added in 0.2 (57, holding_cell_failure_attribution_data, optional_vec), // Added in 0.2 - (58, self.interactive_tx_signing_session, option) // Added in 0.2 + (58, self.interactive_tx_signing_session, option), // Added in 0.2 + (59, self.funding.minimum_depth_override, option), // Added in 0.2 }); Ok(()) @@ -11971,6 +11984,8 @@ where let mut interactive_tx_signing_session: Option = None; + let mut minimum_depth_override: Option = None; + read_tlv_fields!(reader, { (0, announcement_sigs, option), (1, minimum_depth, option), @@ -12010,6 +12025,7 @@ where (55, removed_htlc_failure_attribution_data, optional_vec), (57, holding_cell_failure_attribution_data, optional_vec), (58, interactive_tx_signing_session, option), // Added in 0.2 + (59, minimum_depth_override, option), // Added in 0.2 }); let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); @@ -12185,6 +12201,7 @@ where funding_tx_confirmed_in, funding_tx_confirmation_height, short_channel_id, + minimum_depth_override, }, pending_funding: pending_funding.unwrap(), context: ChannelContext { From ac39850a79c568ded4f12b24007c5865bd13cc05 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 29 Apr 2025 14:21:18 -0500 Subject: [PATCH 07/19] Add FundingScope::get_funding_txid helper --- lightning/src/ln/channel.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 473d4fdbae8..62f77a43db0 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2070,6 +2070,11 @@ impl FundingScope { self.channel_transaction_parameters.funding_outpoint } + #[cfg(splicing)] + fn get_funding_txid(&self) -> Option { + self.channel_transaction_parameters.funding_outpoint.map(|txo| txo.txid) + } + fn get_holder_selected_contest_delay(&self) -> u16 { self.channel_transaction_parameters.holder_selected_contest_delay } From f9ad4a0908571a1e3555cf5d06f60456d2ee226f Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 13 May 2025 15:08:27 -0500 Subject: [PATCH 08/19] Reset FundingScope::funding_tx_confirmation_height FundingScope::funding_tx_confirmation_height is reset as part of calling ChannelContext::check_funding_meets_minimum_depth via FundedChannel::check_get_channel_ready. This side effect requires using mutable references to self when otherwise it would not be needed. Instead of reseting funding_tx_confirmation_height there, do so when unconfirming the funding transaction. --- lightning/src/ln/channel.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 62f77a43db0..365f7f89742 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -5481,7 +5481,7 @@ where self.get_initial_counterparty_commitment_signature(funding, logger) } - fn check_funding_meets_minimum_depth(&self, funding: &mut FundingScope, height: u32) -> bool { + fn check_funding_meets_minimum_depth(&self, funding: &FundingScope, height: u32) -> bool { let minimum_depth = funding.minimum_depth_override.or(self.minimum_depth); if funding.funding_tx_confirmation_height == 0 && minimum_depth != Some(0) { @@ -5490,10 +5490,6 @@ where let funding_tx_confirmations = height as i64 - funding.funding_tx_confirmation_height as i64 + 1; - if funding_tx_confirmations <= 0 { - funding.funding_tx_confirmation_height = 0; - } - if funding_tx_confirmations < minimum_depth.unwrap_or(0) as i64 { return false; } @@ -8814,7 +8810,7 @@ where // Called: // * always when a new block/transactions are confirmed with the new height // * when funding is signed with a height of 0 - if !self.context.check_funding_meets_minimum_depth(&mut self.funding, height) { + if !self.context.check_funding_meets_minimum_depth(&self.funding, height) { return None; } @@ -9022,6 +9018,12 @@ where self.context.update_time_counter = cmp::max(self.context.update_time_counter, highest_header_time); + // Check if the funding transaction was unconfirmed + let funding_tx_confirmations = self.funding.get_funding_tx_confirmations(height); + if funding_tx_confirmations == 0 { + self.funding.funding_tx_confirmation_height = 0; + } + if let Some(channel_ready) = self.check_get_channel_ready(height, logger) { let announcement_sigs = if let Some((chain_hash, node_signer, user_config)) = chain_node_signer { self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger) @@ -9032,13 +9034,6 @@ where if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) || self.context.channel_state.is_our_channel_ready() { - let mut funding_tx_confirmations = height as i64 - self.funding.funding_tx_confirmation_height as i64 + 1; - if self.funding.funding_tx_confirmation_height == 0 { - // Note that check_get_channel_ready may reset funding_tx_confirmation_height to - // zero if it has been reorged out, however in either case, our state flags - // indicate we've already sent a channel_ready - funding_tx_confirmations = 0; - } // If we've sent channel_ready (or have both sent and received channel_ready), and // the funding transaction has become unconfirmed, @@ -9082,6 +9077,7 @@ where // larger. If we don't know that time has moved forward, we can just set it to the last // time we saw and it will be ignored. let best_time = self.context.update_time_counter; + match self.do_best_block_updated(reorg_height, best_time, None::<(ChainHash, &&dyn NodeSigner, &UserConfig)>, logger) { Ok((channel_ready, timed_out_htlcs, announcement_sigs)) => { assert!(channel_ready.is_none(), "We can't generate a funding with 0 confirmations?"); From 14821116b3ed11777322b965c984f3f8b6288e2d Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 14 May 2025 15:24:59 -0500 Subject: [PATCH 09/19] Special case 0-conf in check_funding_meets_minimum_depth 0-conf channels always meet the funding minimum depth once accepted. Special case this in check_funding_meets_minimum_depth such that it isn't implicit in later calculations. Since a minimum depth is always set when the channel is accepted, expect this to be the case in the method since it should only be called on a ChannelContext in a FundedChannel. --- lightning/src/ln/channel.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 365f7f89742..038fe8970b7 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -5482,15 +5482,23 @@ where } fn check_funding_meets_minimum_depth(&self, funding: &FundingScope, height: u32) -> bool { - let minimum_depth = funding.minimum_depth_override.or(self.minimum_depth); + let minimum_depth = funding + .minimum_depth_override + .or(self.minimum_depth) + .expect("ChannelContext::minimum_depth should be set for FundedChannel"); - if funding.funding_tx_confirmation_height == 0 && minimum_depth != Some(0) { + // Zero-conf channels always meet the minimum depth. + if minimum_depth == 0 { + return true; + } + + if funding.funding_tx_confirmation_height == 0 { return false; } let funding_tx_confirmations = height as i64 - funding.funding_tx_confirmation_height as i64 + 1; - if funding_tx_confirmations < minimum_depth.unwrap_or(0) as i64 { + if funding_tx_confirmations < minimum_depth as i64 { return false; } From 05180176728decf17cbe9458835b772b0a75f9e3 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 9 Apr 2025 18:24:48 -0500 Subject: [PATCH 10/19] Check confirmation of pending funding transactions When transactions confirm or the best block is updated, check if any pending splice funding transactions have confirmed to an acceptable depth. If so, send a splice_locked message to the counterparty and -- if the counterparty has exchanged a splice_locked message for the same funding txid -- promote the corresponding FundingScope such that the new funding can be utilized. --- lightning/src/ln/channel.rs | 399 ++++++++++++++++++++++++----- lightning/src/ln/channelmanager.rs | 23 +- lightning/src/util/config.rs | 4 + 3 files changed, 365 insertions(+), 61 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 038fe8970b7..226b4bd5e17 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2140,6 +2140,36 @@ impl FundingScope { #[cfg(splicing)] struct PendingSplice { pub our_funding_contribution: i64, + + /// The funding txid used in the `splice_locked` sent to the counterparty. + sent_funding_txid: Option, + + /// The funding txid used in the `splice_locked` received from the counterparty. + received_funding_txid: Option, +} + +/// Wrapper around a [`Transaction`] useful for caching the result of [`Transaction::compute_txid`]. +struct ConfirmedTransaction<'a> { + tx: &'a Transaction, + txid: Option, +} + +impl<'a> ConfirmedTransaction<'a> { + /// Returns the underlying [`Transaction`]. + pub fn tx(&self) -> &'a Transaction { + self.tx + } + + /// Returns the [`Txid`], computing and caching it if necessary. + pub fn txid(&mut self) -> Txid { + *self.txid.get_or_insert_with(|| self.tx.compute_txid()) + } +} + +impl<'a> From<&'a Transaction> for ConfirmedTransaction<'a> { + fn from(tx: &'a Transaction) -> Self { + ConfirmedTransaction { tx, txid: None } + } } /// Contains everything about the channel including state, and various flags. @@ -5504,6 +5534,97 @@ where return true; } + + #[rustfmt::skip] + fn check_for_funding_tx_confirmed( + &mut self, funding: &mut FundingScope, block_hash: &BlockHash, height: u32, + index_in_block: usize, tx: &mut ConfirmedTransaction, + ) -> Result { + let funding_txo = match funding.get_funding_txo() { + Some(funding_txo) => funding_txo, + None => { + debug_assert!(false); + return Ok(false); + }, + }; + + // Check if the transaction is the expected funding transaction, and if it is, + // check that it pays the right amount to the right script. + if funding.funding_tx_confirmation_height == 0 { + if tx.txid() == funding_txo.txid { + let tx = tx.tx(); + let txo_idx = funding_txo.index as usize; + if txo_idx >= tx.output.len() || tx.output[txo_idx].script_pubkey != funding.get_funding_redeemscript().to_p2wsh() || + tx.output[txo_idx].value.to_sat() != funding.get_value_satoshis() { + if funding.is_outbound() { + // If we generated the funding transaction and it doesn't match what it + // should, the client is really broken and we should just panic and + // tell them off. That said, because hash collisions happen with high + // probability in fuzzing mode, if we're fuzzing we just close the + // channel and move on. + #[cfg(not(fuzzing))] + panic!("Client called ChannelManager::funding_transaction_generated with bogus transaction!"); + } + self.update_time_counter += 1; + let err_reason = "funding tx had wrong script/value or output index"; + return Err(ClosureReason::ProcessingError { err: err_reason.to_owned() }); + } else { + if funding.is_outbound() { + if !tx.is_coinbase() { + for input in tx.input.iter() { + if input.witness.is_empty() { + // We generated a malleable funding transaction, implying we've + // just exposed ourselves to funds loss to our counterparty. + #[cfg(not(fuzzing))] + panic!("Client called ChannelManager::funding_transaction_generated with bogus transaction!"); + } + } + } + } + + funding.funding_tx_confirmation_height = height; + funding.funding_tx_confirmed_in = Some(*block_hash); + funding.short_channel_id = match scid_from_parts(height as u64, index_in_block as u64, txo_idx as u64) { + Ok(scid) => Some(scid), + Err(_) => panic!("Block was bogus - either height was > 16 million, had > 16 million transactions, or had > 65k outputs"), + }; + + return Ok(true); + } + } + } + + Ok(false) + } + + #[rustfmt::skip] + fn check_for_funding_tx_spent( + &mut self, funding: &FundingScope, tx: &Transaction, logger: &L, + ) -> Result<(), ClosureReason> + where + L::Target: Logger, + { + let funding_txo = match funding.get_funding_txo() { + Some(funding_txo) => funding_txo, + None => { + debug_assert!(false); + return Ok(()); + }, + }; + + for input in tx.input.iter() { + if input.previous_output == funding_txo.into_bitcoin_outpoint() { + log_info!( + logger, "Detected channel-closing tx {} spending {}:{}, closing channel {}", + tx.compute_txid(), input.previous_output.txid, input.previous_output.vout, + &self.channel_id(), + ); + return Err(ClosureReason::CommitmentTxConfirmed); + } + } + + Ok(()) + } } // Internal utility functions for channels @@ -5702,6 +5823,16 @@ where pending_splice: Option, } +#[cfg(splicing)] +macro_rules! promote_splice_funding { + ($self: expr, $funding: expr) => { + core::mem::swap(&mut $self.funding, $funding); + $self.pending_splice = None; + $self.pending_funding.clear(); + $self.context.announcement_sigs_state = AnnouncementSigsState::NotSent; + }; +} + #[cfg(any(test, fuzzing))] struct CommitmentTxInfoCached { fee: u64, @@ -8888,6 +9019,76 @@ where } } + #[cfg(splicing)] + fn check_get_splice_locked( + &self, pending_splice: &PendingSplice, funding: &FundingScope, height: u32, + ) -> Option { + if !self.context.check_funding_meets_minimum_depth(funding, height) { + return None; + } + + let confirmed_funding_txid = match funding.get_funding_txid() { + Some(funding_txid) => funding_txid, + None => { + debug_assert!(false); + return None; + }, + }; + + match pending_splice.sent_funding_txid { + Some(sent_funding_txid) if confirmed_funding_txid == sent_funding_txid => None, + _ => Some(msgs::SpliceLocked { + channel_id: self.context.channel_id(), + splice_txid: confirmed_funding_txid, + }), + } + } + + #[cfg(splicing)] + fn maybe_promote_splice_funding( + &mut self, splice_txid: Txid, confirmed_funding_index: usize, logger: &L, + ) -> bool + where + 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(); + pending_splice.sent_funding_txid = Some(splice_txid); + + 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(); + 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, + ); + } else { + log_info!( + logger, + "Waiting on splice_locked txid {} for channel {}", + splice_txid, + &self.context.channel_id, + ); + } + + return false; + } + /// When a transaction is confirmed, we check whether it is or spends the funding transaction /// In the first case, we store the confirmation height and calculating the short channel id. /// In the second, we simply return an Err indicating we need to be force-closed now. @@ -8900,75 +9101,103 @@ where NS::Target: NodeSigner, L::Target: Logger { - let mut msgs = (None, None); - if let Some(funding_txo) = self.funding.get_funding_txo() { - for &(index_in_block, tx) in txdata.iter() { - // Check if the transaction is the expected funding transaction, and if it is, - // check that it pays the right amount to the right script. - if self.funding.funding_tx_confirmation_height == 0 { - if tx.compute_txid() == funding_txo.txid { - let txo_idx = funding_txo.index as usize; - if txo_idx >= tx.output.len() || tx.output[txo_idx].script_pubkey != self.funding.get_funding_redeemscript().to_p2wsh() || - tx.output[txo_idx].value.to_sat() != self.funding.get_value_satoshis() { - if self.funding.is_outbound() { - // If we generated the funding transaction and it doesn't match what it - // should, the client is really broken and we should just panic and - // tell them off. That said, because hash collisions happen with high - // probability in fuzzing mode, if we're fuzzing we just close the - // channel and move on. - #[cfg(not(fuzzing))] - panic!("Client called ChannelManager::funding_transaction_generated with bogus transaction!"); - } - self.context.update_time_counter += 1; - let err_reason = "funding tx had wrong script/value or output index"; - return Err(ClosureReason::ProcessingError { err: err_reason.to_owned() }); - } else { - if self.funding.is_outbound() { - if !tx.is_coinbase() { - for input in tx.input.iter() { - if input.witness.is_empty() { - // We generated a malleable funding transaction, implying we've - // just exposed ourselves to funds loss to our counterparty. - #[cfg(not(fuzzing))] - panic!("Client called ChannelManager::funding_transaction_generated with bogus transaction!"); - } - } - } - } + for &(index_in_block, tx) in txdata.iter() { + let mut confirmed_tx = ConfirmedTransaction::from(tx); + + // If we allow 1-conf funding, we may need to check for channel_ready or splice_locked here + // and send it immediately instead of waiting for a best_block_updated call (which may have + // already happened for this block). + let is_funding_tx_confirmed = self.context.check_for_funding_tx_confirmed( + &mut self.funding, block_hash, height, index_in_block, &mut confirmed_tx, + )?; - self.funding.funding_tx_confirmation_height = height; - self.funding.funding_tx_confirmed_in = Some(*block_hash); - self.funding.short_channel_id = match scid_from_parts(height as u64, index_in_block as u64, txo_idx as u64) { - Ok(scid) => Some(scid), - Err(_) => panic!("Block was bogus - either height was > 16 million, had > 16 million transactions, or had > 65k outputs"), - } - } - // If this is a coinbase transaction and not a 0-conf channel - // we should update our min_depth to 100 to handle coinbase maturity - if tx.is_coinbase() && - self.context.minimum_depth.unwrap_or(0) > 0 && - self.context.minimum_depth.unwrap_or(0) < COINBASE_MATURITY { - self.funding.minimum_depth_override = Some(COINBASE_MATURITY); + if is_funding_tx_confirmed { + // If this is a coinbase transaction and not a 0-conf channel + // we should update our min_depth to 100 to handle coinbase maturity + if tx.is_coinbase() && + self.context.minimum_depth.unwrap_or(0) > 0 && + self.context.minimum_depth.unwrap_or(0) < COINBASE_MATURITY { + self.funding.minimum_depth_override = Some(COINBASE_MATURITY); + } + + if let Some(channel_ready) = self.check_get_channel_ready(height, logger) { + for &(idx, tx) in txdata.iter() { + if idx > index_in_block { + self.context.check_for_funding_tx_spent(&self.funding, tx, logger)?; } } - // If we allow 1-conf funding, we may need to check for channel_ready here and - // send it immediately instead of waiting for a best_block_updated call (which - // may have already happened for this block). - if let Some(channel_ready) = self.check_get_channel_ready(height, logger) { - log_info!(logger, "Sending a channel_ready to our peer for channel {}", &self.context.channel_id); - let announcement_sigs = self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger); - msgs = (Some(FundingConfirmedMessage::Establishment(channel_ready)), announcement_sigs); + + log_info!(logger, "Sending a channel_ready to our peer for channel {}", &self.context.channel_id); + let announcement_sigs = self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger); + return Ok((Some(FundingConfirmedMessage::Establishment(channel_ready)), announcement_sigs)); + } + } + + #[cfg(splicing)] + let mut confirmed_funding_index = None; + #[cfg(splicing)] + let mut funding_already_confirmed = false; + #[cfg(splicing)] + 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, + )? { + 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; } - for inp in tx.input.iter() { - if inp.previous_output == funding_txo.into_bitcoin_outpoint() { - log_info!(logger, "Detected channel-closing tx {} spending {}:{}, closing channel {}", tx.compute_txid(), inp.previous_output.txid, inp.previous_output.vout, &self.context.channel_id()); - return Err(ClosureReason::CommitmentTxConfirmed); + } + + #[cfg(splicing)] + if let Some(confirmed_funding_index) = confirmed_funding_index { + let pending_splice = match self.pending_splice.as_ref() { + 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 = self.pending_funding.get(confirmed_funding_index).unwrap(); + + if let Some(splice_locked) = self.check_get_splice_locked(pending_splice, funding, height) { + for &(idx, tx) in txdata.iter() { + if idx > index_in_block { + self.context.check_for_funding_tx_spent(funding, tx, logger)?; + } } + + log_info!( + logger, + "Sending splice_locked txid {} to our peer for channel {}", + splice_locked.splice_txid, + &self.context.channel_id, + ); + + let announcement_sigs = self + .maybe_promote_splice_funding(splice_locked.splice_txid, confirmed_funding_index, logger) + .then(|| self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger)) + .flatten(); + + return Ok((Some(FundingConfirmedMessage::Splice(splice_locked)), announcement_sigs)); } } + + self.context.check_for_funding_tx_spent(&self.funding, tx, logger)?; + #[cfg(splicing)] + for funding in self.pending_funding.iter() { + self.context.check_for_funding_tx_spent(funding, tx, logger)?; + } + } - Ok(msgs) + + Ok((None, None)) } /// When a new block is connected, we check the height of the block against outbound holding @@ -9066,6 +9295,49 @@ where return Err(ClosureReason::FundingTimedOut); } + #[cfg(splicing)] + let mut confirmed_funding_index = None; + #[cfg(splicing)] + 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() }); + } + + confirmed_funding_index = Some(index); + } + } + + #[cfg(splicing)] + if let Some(confirmed_funding_index) = confirmed_funding_index { + let pending_splice = match self.pending_splice.as_ref() { + 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 = self.pending_funding.get(confirmed_funding_index).unwrap(); + + if let Some(splice_locked) = self.check_get_splice_locked(pending_splice, funding, height) { + log_info!(logger, "Sending a splice_locked to our peer for channel {}", &self.context.channel_id); + + let announcement_sigs = self + .maybe_promote_splice_funding(splice_locked.splice_txid, confirmed_funding_index, logger) + .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(); + + return Ok((Some(FundingConfirmedMessage::Splice(splice_locked)), timed_out_htlcs, announcement_sigs)); + } + } + let announcement_sigs = if let Some((chain_hash, node_signer, user_config)) = chain_node_signer { self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger) } else { None }; @@ -9426,6 +9698,8 @@ where self.pending_splice = Some(PendingSplice { our_funding_contribution: our_funding_contribution_satoshis, + sent_funding_txid: None, + received_funding_txid: None, }); let msg = self.get_splice_init(our_funding_contribution_satoshis, funding_feerate_per_kw, locktime); @@ -10243,6 +10517,11 @@ where pub fn get_funding_tx_confirmed_in(&self) -> Option { self.funding.funding_tx_confirmed_in } + + #[cfg(splicing)] + pub fn has_pending_splice(&self) -> bool { + self.pending_splice.is_some() + } } /// A not-yet-funded outbound (from holder) channel using V1 channel establishment. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index bab95ad0d90..1cda1061d9c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3302,13 +3302,20 @@ macro_rules! send_channel_ready { let outbound_alias_insert = short_to_chan_info.insert($channel.context.outbound_scid_alias(), ($channel.context.get_counterparty_node_id(), $channel.context.channel_id())); assert!(outbound_alias_insert.is_none() || outbound_alias_insert.unwrap() == ($channel.context.get_counterparty_node_id(), $channel.context.channel_id()), "SCIDs should never collide - ensure you weren't behind the chain tip by a full month when creating channels"); + insert_short_channel_id!(short_to_chan_info, $channel); + }} +} + +macro_rules! insert_short_channel_id { + ($short_to_chan_info: ident, $channel: expr) => {{ if let Some(real_scid) = $channel.funding.get_short_channel_id() { - let scid_insert = short_to_chan_info.insert(real_scid, ($channel.context.get_counterparty_node_id(), $channel.context.channel_id())); + let scid_insert = $short_to_chan_info.insert(real_scid, ($channel.context.get_counterparty_node_id(), $channel.context.channel_id())); assert!(scid_insert.is_none() || scid_insert.unwrap() == ($channel.context.get_counterparty_node_id(), $channel.context.channel_id()), "SCIDs should never collide - ensure you weren't behind the chain tip by a full month when creating channels"); } }} } + macro_rules! emit_funding_tx_broadcast_safe_event { ($locked_events: expr, $channel: expr, $funding_txo: expr) => { if !$channel.context.funding_tx_broadcast_safe_event_emitted() { @@ -12126,6 +12133,8 @@ where pub(super) enum FundingConfirmedMessage { Establishment(msgs::ChannelReady), + #[cfg(splicing)] + Splice(msgs::SpliceLocked), } impl< @@ -12198,6 +12207,18 @@ where log_trace!(logger, "Sending channel_ready WITHOUT channel_update for {}", funded_channel.context.channel_id()); } }, + #[cfg(splicing)] + Some(FundingConfirmedMessage::Splice(splice_locked)) => { + if !funded_channel.has_pending_splice() { + let mut short_to_chan_info = self.short_to_chan_info.write().unwrap(); + insert_short_channel_id!(short_to_chan_info, funded_channel); + } + + pending_msg_events.push(MessageSendEvent::SendSpliceLocked { + node_id: funded_channel.context.get_counterparty_node_id(), + msg: splice_locked, + }); + }, None => {}, } diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs index e98b237691c..361fe24bc93 100644 --- a/lightning/src/util/config.rs +++ b/lightning/src/util/config.rs @@ -25,6 +25,10 @@ pub struct ChannelHandshakeConfig { /// Applied only for inbound channels (see [`ChannelHandshakeLimits::max_minimum_depth`] for the /// equivalent limit applied to outbound channels). /// + /// Also used when splicing the channel for the number of confirmations needed before sending a + /// `splice_locked` message to the counterparty. The spliced funds are considered locked in when + /// both parties have exchanged `splice_locked`. + /// /// A lower-bound of `1` is applied, requiring all channels to have a confirmed commitment /// transaction before operation. If you wish to accept channels with zero confirmations, see /// [`UserConfig::manually_accept_inbound_channels`] and From 4325faf3cfd092aa42cf8a00692f209080373287 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 15 May 2025 16:48:35 -0500 Subject: [PATCH 11/19] Move check_funding_meets_minimum_depth to FundedChannel This method is only applicable for FundedChannel, so it shouldn't be accessible from ChannelContext. --- lightning/src/ln/channel.rs | 52 ++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 226b4bd5e17..f47dd1ecf8f 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -5511,30 +5511,6 @@ where self.get_initial_counterparty_commitment_signature(funding, logger) } - fn check_funding_meets_minimum_depth(&self, funding: &FundingScope, height: u32) -> bool { - let minimum_depth = funding - .minimum_depth_override - .or(self.minimum_depth) - .expect("ChannelContext::minimum_depth should be set for FundedChannel"); - - // Zero-conf channels always meet the minimum depth. - if minimum_depth == 0 { - return true; - } - - if funding.funding_tx_confirmation_height == 0 { - return false; - } - - let funding_tx_confirmations = - height as i64 - funding.funding_tx_confirmation_height as i64 + 1; - if funding_tx_confirmations < minimum_depth as i64 { - return false; - } - - return true; - } - #[rustfmt::skip] fn check_for_funding_tx_confirmed( &mut self, funding: &mut FundingScope, block_hash: &BlockHash, height: u32, @@ -8949,7 +8925,7 @@ where // Called: // * always when a new block/transactions are confirmed with the new height // * when funding is signed with a height of 0 - if !self.context.check_funding_meets_minimum_depth(&self.funding, height) { + if !self.check_funding_meets_minimum_depth(&self.funding, height) { return None; } @@ -9023,7 +8999,7 @@ where fn check_get_splice_locked( &self, pending_splice: &PendingSplice, funding: &FundingScope, height: u32, ) -> Option { - if !self.context.check_funding_meets_minimum_depth(funding, height) { + if !self.check_funding_meets_minimum_depth(funding, height) { return None; } @@ -9044,6 +9020,30 @@ where } } + fn check_funding_meets_minimum_depth(&self, funding: &FundingScope, height: u32) -> bool { + let minimum_depth = funding + .minimum_depth_override + .or(self.context.minimum_depth) + .expect("ChannelContext::minimum_depth should be set for FundedChannel"); + + // Zero-conf channels always meet the minimum depth. + if minimum_depth == 0 { + return true; + } + + if funding.funding_tx_confirmation_height == 0 { + return false; + } + + let funding_tx_confirmations = + height as i64 - funding.funding_tx_confirmation_height as i64 + 1; + if funding_tx_confirmations < minimum_depth as i64 { + return false; + } + + return true; + } + #[cfg(splicing)] fn maybe_promote_splice_funding( &mut self, splice_txid: Txid, confirmed_funding_index: usize, logger: &L, From a49e2991aab538bec3d856416eba219a7592e92d Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 15 May 2025 17:21:11 -0500 Subject: [PATCH 12/19] Account for coinbase tx in ChannelContext::minimum_depth Now that FundedScope::minimum_depth_override is used to override the minimum depth with COINBASE_MATURITY when the funding transaction is the coinbase transaction, use this in ChannelContext::minimum_depth method. Also, add a minimum_depth to Channel. The one on ChannelContext can become private once FudningScope doesn't need to be accessed directly from a ChannelManager macro. This fixes ChannelDetails showing an incorrect minimum depth when the coinbase transaction is used to fund the channel. --- lightning/src/ln/channel.rs | 14 +++++++++----- lightning/src/ln/channel_state.rs | 2 +- lightning/src/ln/channelmanager.rs | 8 ++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index f47dd1ecf8f..ca2f8bb4152 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1852,6 +1852,10 @@ where }, } } + + pub fn minimum_depth(&self) -> Option { + self.context().minimum_depth(self.funding()) + } } impl From> for Channel @@ -3690,8 +3694,8 @@ where self.temporary_channel_id } - pub fn minimum_depth(&self) -> Option { - self.minimum_depth + pub(super) fn minimum_depth(&self, funding: &FundingScope) -> Option { + funding.minimum_depth_override.or(self.minimum_depth) } /// Gets the "user_id" value passed into the construction of this channel. It has no special @@ -9021,9 +9025,9 @@ where } fn check_funding_meets_minimum_depth(&self, funding: &FundingScope, height: u32) -> bool { - let minimum_depth = funding - .minimum_depth_override - .or(self.context.minimum_depth) + let minimum_depth = self + .context + .minimum_depth(funding) .expect("ChannelContext::minimum_depth should be set for FundedChannel"); // Zero-conf channels always meet the minimum depth. diff --git a/lightning/src/ln/channel_state.rs b/lightning/src/ln/channel_state.rs index 99e2833f644..c28b4687631 100644 --- a/lightning/src/ln/channel_state.rs +++ b/lightning/src/ln/channel_state.rs @@ -531,7 +531,7 @@ impl ChannelDetails { next_outbound_htlc_limit_msat: balance.next_outbound_htlc_limit_msat, next_outbound_htlc_minimum_msat: balance.next_outbound_htlc_minimum_msat, user_channel_id: context.get_user_id(), - confirmations_required: context.minimum_depth(), + confirmations_required: channel.minimum_depth(), confirmations: Some(funding.get_funding_tx_confirmations(best_block_height)), force_close_spend_delay: funding.get_counterparty_selected_contest_delay(), is_outbound: funding.is_outbound(), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 1cda1061d9c..a0dd5331851 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3174,7 +3174,7 @@ macro_rules! locked_close_channel { // into the map (which prevents the `PeerState` from being cleaned up) for channels that // never even got confirmations (which would open us up to DoS attacks). let update_id = $channel_context.get_latest_monitor_update_id(); - if $channel_funding.get_funding_tx_confirmation_height().is_some() || $channel_context.minimum_depth() == Some(0) || update_id > 1 { + if $channel_funding.get_funding_tx_confirmation_height().is_some() || $channel_context.minimum_depth($channel_funding) == Some(0) || update_id > 1 { let chan_id = $channel_context.channel_id(); $peer_state.closed_channel_monitor_update_ids.insert(chan_id, update_id); } @@ -8375,7 +8375,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ if accept_0conf { // This should have been correctly configured by the call to Inbound(V1/V2)Channel::new. - debug_assert!(channel.context().minimum_depth().unwrap() == 0); + debug_assert!(channel.minimum_depth().unwrap() == 0); } else if channel.funding().get_channel_type().requires_zero_conf() { let send_msg_err_event = MessageSendEvent::HandleError { node_id: channel.context().get_counterparty_node_id(), @@ -8456,7 +8456,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ Some(funded_chan) => { // This covers non-zero-conf inbound `Channel`s that we are currently monitoring, but those // which have not yet had any confirmations on-chain. - if !funded_chan.funding.is_outbound() && funded_chan.context.minimum_depth().unwrap_or(1) != 0 && + if !funded_chan.funding.is_outbound() && chan.minimum_depth().unwrap_or(1) != 0 && funded_chan.funding.get_funding_tx_confirmations(best_block_height) == 0 { num_unfunded_channels += 1; @@ -8469,7 +8469,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } // 0conf channels are not considered unfunded. - if chan.context().minimum_depth().unwrap_or(1) == 0 { + if chan.minimum_depth().unwrap_or(1) == 0 { continue; } From a90ccb6b10996f39ead3447c734d7fe5fbe81c31 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 13 May 2025 14:57:52 -0500 Subject: [PATCH 13/19] Check unconfirmation of pending funding transactions When a splice funding transaction is unconfirmed, update the corresponding FundingScope just as is done when the initial funding transaction is unconfirmed. --- lightning/src/ln/channel.rs | 81 +++++++++++++++++++++--------- lightning/src/ln/channelmanager.rs | 14 +----- 2 files changed, 58 insertions(+), 37 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index ca2f8bb4152..db965fbbd6a 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2074,7 +2074,6 @@ impl FundingScope { self.channel_transaction_parameters.funding_outpoint } - #[cfg(splicing)] fn get_funding_txid(&self) -> Option { self.channel_transaction_parameters.funding_outpoint.map(|txo| txo.txid) } @@ -9315,7 +9314,7 @@ where #[cfg(splicing)] if let Some(confirmed_funding_index) = confirmed_funding_index { - let pending_splice = match self.pending_splice.as_ref() { + let pending_splice = match self.pending_splice.as_mut() { Some(pending_splice) => pending_splice, None => { // TODO: Move pending_funding into pending_splice @@ -9324,8 +9323,26 @@ where return Err(ClosureReason::ProcessingError { err }); }, }; - let funding = self.pending_funding.get(confirmed_funding_index).unwrap(); + let funding = self.pending_funding.get_mut(confirmed_funding_index).unwrap(); + + // 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_ref().unwrap(); + let funding = self.pending_funding.get(confirmed_funding_index).unwrap(); if let Some(splice_locked) = self.check_get_splice_locked(pending_splice, funding, height) { log_info!(logger, "Sending a splice_locked to our peer for channel {}", &self.context.channel_id); @@ -9348,31 +9365,45 @@ where Ok((None, timed_out_htlcs, announcement_sigs)) } - /// Indicates the funding transaction is no longer confirmed in the main chain. This may + /// Checks if any funding transaction is no longer confirmed in the main chain. This may /// force-close the channel, but may also indicate a harmless reorganization of a block or two - /// before the channel has reached channel_ready and we can just wait for more blocks. - #[rustfmt::skip] - pub fn funding_transaction_unconfirmed(&mut self, logger: &L) -> Result<(), ClosureReason> where L::Target: Logger { - if self.funding.funding_tx_confirmation_height != 0 { - // We handle the funding disconnection by calling best_block_updated with a height one - // below where our funding was connected, implying a reorg back to conf_height - 1. - let reorg_height = self.funding.funding_tx_confirmation_height - 1; - // We use the time field to bump the current time we set on channel updates if its - // larger. If we don't know that time has moved forward, we can just set it to the last - // time we saw and it will be ignored. - let best_time = self.context.update_time_counter; - - match self.do_best_block_updated(reorg_height, best_time, None::<(ChainHash, &&dyn NodeSigner, &UserConfig)>, logger) { - Ok((channel_ready, timed_out_htlcs, announcement_sigs)) => { - assert!(channel_ready.is_none(), "We can't generate a funding with 0 confirmations?"); - assert!(timed_out_htlcs.is_empty(), "We can't have accepted HTLCs with a timeout before our funding confirmation?"); - assert!(announcement_sigs.is_none(), "We can't generate an announcement_sigs with 0 confirmations?"); - Ok(()) - }, - Err(e) => Err(e) + /// before the channel has reached channel_ready or splice_locked, and we can just wait for more + /// blocks. + #[rustfmt::skip] + pub fn transaction_unconfirmed( + &mut self, txid: &Txid, logger: &L, + ) -> Result<(), ClosureReason> + where + L::Target: Logger, + { + let unconfirmed_funding = core::iter::once(&mut self.funding) + .chain(self.pending_funding.iter_mut()) + .find(|funding| funding.get_funding_txid() == Some(*txid)); + + if let Some(funding) = unconfirmed_funding { + if funding.funding_tx_confirmation_height != 0 { + // We handle the funding disconnection by calling best_block_updated with a height one + // below where our funding was connected, implying a reorg back to conf_height - 1. + let reorg_height = funding.funding_tx_confirmation_height - 1; + // We use the time field to bump the current time we set on channel updates if its + // larger. If we don't know that time has moved forward, we can just set it to the last + // time we saw and it will be ignored. + let best_time = self.context.update_time_counter; + + match self.do_best_block_updated(reorg_height, best_time, None::<(ChainHash, &&dyn NodeSigner, &UserConfig)>, logger) { + Ok((channel_ready, timed_out_htlcs, announcement_sigs)) => { + assert!(channel_ready.is_none(), "We can't generate a funding with 0 confirmations?"); + assert!(timed_out_htlcs.is_empty(), "We can't have accepted HTLCs with a timeout before our funding confirmation?"); + assert!(announcement_sigs.is_none(), "We can't generate an announcement_sigs with 0 confirmations?"); + Ok(()) + }, + Err(e) => Err(e), + } + } else { + // We never learned about the funding confirmation anyway, just ignore + Ok(()) } } else { - // We never learned about the funding confirmation anyway, just ignore Ok(()) } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index a0dd5331851..05d782d91df 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -12115,18 +12115,8 @@ where || -> NotifyOption { NotifyOption::DoPersist }, ); self.do_chain_event(None, |channel| { - if let Some(funding_txo) = channel.funding.get_funding_txo() { - if funding_txo.txid == *txid { - let chan_context = - WithChannelContext::from(&self.logger, &channel.context, None); - let res = channel.funding_transaction_unconfirmed(&&chan_context); - res.map(|()| (None, Vec::new(), None)) - } else { - Ok((None, Vec::new(), None)) - } - } else { - Ok((None, Vec::new(), None)) - } + let logger = WithChannelContext::from(&self.logger, &channel.context, None); + channel.transaction_unconfirmed(txid, &&logger).map(|()| (None, Vec::new(), None)) }); } } From a92a416c1873b9d449d2b9bca62517b6f9ae70bb Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 13 May 2025 16:43:42 -0500 Subject: [PATCH 14/19] Check all FundingScopes in get_relevant_txids Pending funding transactions for splices should be monitored for appearance on chain. Include these in ChannelManager::get_relevant_txids so that they can be watched. --- lightning/src/ln/channel.rs | 26 +++++++++++++++++++++----- lightning/src/ln/channelmanager.rs | 9 ++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index db965fbbd6a..bddffb30ae4 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -9365,6 +9365,27 @@ where Ok((None, timed_out_htlcs, announcement_sigs)) } + pub fn get_relevant_txids(&self) -> impl Iterator)> + '_ { + core::iter::once(&self.funding) + .chain(self.pending_funding.iter()) + .map(|funding| { + ( + funding.get_funding_txid(), + funding.get_funding_tx_confirmation_height(), + funding.funding_tx_confirmed_in, + ) + }) + .filter_map(|(txid_opt, height_opt, hash_opt)| { + if let (Some(funding_txid), Some(conf_height), Some(block_hash)) = + (txid_opt, height_opt, hash_opt) + { + Some((funding_txid, conf_height, Some(block_hash))) + } else { + None + } + }) + } + /// Checks if any funding transaction is no longer confirmed in the main chain. This may /// force-close the channel, but may also indicate a harmless reorganization of a block or two /// before the channel has reached channel_ready or splice_locked, and we can just wait for more @@ -10548,11 +10569,6 @@ where } } - /// Returns the block hash in which our funding transaction was confirmed. - pub fn get_funding_tx_confirmed_in(&self) -> Option { - self.funding.funding_tx_confirmed_in - } - #[cfg(splicing)] pub fn has_pending_splice(&self) -> bool { self.pending_splice.is_some() diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 05d782d91df..f268c4c59de 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -12095,13 +12095,8 @@ where let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; for chan in peer_state.channel_by_id.values().filter_map(Channel::as_funded) { - let txid_opt = chan.funding.get_funding_txo(); - let height_opt = chan.funding.get_funding_tx_confirmation_height(); - let hash_opt = chan.get_funding_tx_confirmed_in(); - if let (Some(funding_txo), Some(conf_height), Some(block_hash)) = - (txid_opt, height_opt, hash_opt) - { - res.push((funding_txo.txid, conf_height, Some(block_hash))); + for (funding_txid, conf_height, block_hash) in chan.get_relevant_txids() { + res.push((funding_txid, conf_height, block_hash)); } } } From 8a500f203c78465d3d83fb3b307f37eb11e36f0b Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 18 Apr 2025 17:27:13 -0500 Subject: [PATCH 15/19] Handle splice_locked message When receiving a splice_locked message from a counterparty, promote the corresponding FundingScope if the funding txid matches the one sent by us in splice_locked. --- lightning/src/ln/channel.rs | 70 ++++++++++++++++++++++++++++++ lightning/src/ln/channelmanager.rs | 68 +++++++++++++++++++++++++++-- 2 files changed, 135 insertions(+), 3 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index bddffb30ae4..ae783f1cfc6 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -9845,6 +9845,76 @@ where Ok(()) } + #[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, ChannelError> + where + NS::Target: NodeSigner, + L::Target: Logger, + { + log_info!( + logger, + "Received splice_locked txid {} from our peer for channel {}", + msg.splice_txid, + &self.context.channel_id, + ); + + let pending_splice = match self.pending_splice.as_mut() { + Some(pending_splice) => pending_splice, + None => { + return Err(ChannelError::Ignore(format!("Channel is not in pending splice"))); + }, + }; + + 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); + return Ok(self.get_announcement_sigs( + node_signer, + chain_hash, + user_config, + best_block.height, + logger, + )); + } + + 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 { + log_info!( + logger, + "Waiting for enough confirmations to send splice_locked txid {} for channel {}", + msg.splice_txid, + &self.context.channel_id, + ); + } + + pending_splice.received_funding_txid = Some(msg.splice_txid); + Ok(None) + } + // Send stuff to our remote peers: /// Queues up an outbound HTLC to send by placing it in the holding cell. You should call diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index f268c4c59de..2f085db4ee6 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -10121,6 +10121,61 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ Err(MsgHandleErrInternal::send_err_msg_no_close("TODO(splicing): Splicing is not implemented (splice_ack)".to_owned(), msg.channel_id)) } + #[cfg(splicing)] + fn internal_splice_locked( + &self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceLocked, + ) -> Result<(), MsgHandleErrInternal> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + debug_assert!(false); + MsgHandleErrInternal::send_err_msg_no_close( + format!( + "Can't find a peer matching the passed counterparty node_id {}", + counterparty_node_id + ), + msg.channel_id, + ) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + + // Look for the channel + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!( + "Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", + counterparty_node_id + ), msg.channel_id)), + hash_map::Entry::Occupied(mut chan_entry) => { + if let Some(chan) = chan_entry.get_mut().as_funded_mut() { + let logger = WithChannelContext::from(&self.logger, &chan.context, None); + let announcement_sigs_opt = try_channel_entry!( + self, peer_state, chan.splice_locked( + msg, &self.node_signer, self.chain_hash, &self.default_configuration, + &self.best_block.read().unwrap(), &&logger, + ), chan_entry + ); + + if !chan.has_pending_splice() { + let mut short_to_chan_info = self.short_to_chan_info.write().unwrap(); + insert_short_channel_id!(short_to_chan_info, chan); + } + + 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, + }); + } + } else { + return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel is not funded, cannot splice".to_owned(), msg.channel_id)); + } + }, + }; + + Ok(()) + } + /// Process pending events from the [`chain::Watch`], returning whether any events were processed. #[rustfmt::skip] fn process_pending_monitor_events(&self) -> bool { @@ -12612,9 +12667,16 @@ where #[cfg(splicing)] #[rustfmt::skip] fn handle_splice_locked(&self, counterparty_node_id: PublicKey, msg: &msgs::SpliceLocked) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Splicing not supported (splice_locked)".to_owned(), - msg.channel_id)), counterparty_node_id); + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let res = self.internal_splice_locked(&counterparty_node_id, msg); + let persist = match &res { + Err(e) if e.closes_channel() => NotifyOption::DoPersist, + Err(_) => NotifyOption::SkipPersistHandleEvents, + Ok(()) => NotifyOption::DoPersist, + }; + let _ = handle_error!(self, res, counterparty_node_id); + persist + }); } fn handle_shutdown(&self, counterparty_node_id: PublicKey, msg: &msgs::Shutdown) { From 9455aaf74049beac2fbd3a4880eb06408b5a32fc Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 16 May 2025 16:09:14 -0500 Subject: [PATCH 16/19] Track historical SCIDs from previous funding When a splice is locked, the SCID from the previous funding transaction needs to be remembered so that pending HTLCs can be handled properly. Additionally, when they need to be cleaned up once they should no longer be used. Track these SCIDs as splices are locked and clean any up as blocks are connected. --- lightning/src/ln/channel.rs | 49 +++++++++++++++++++++++++++++- lightning/src/ln/channelmanager.rs | 20 ++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index ae783f1cfc6..48f97747a22 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -76,7 +76,7 @@ use crate::util::config::{ }; use crate::util::errors::APIError; use crate::util::logger::{Logger, Record, WithContext}; -use crate::util::scid_utils::scid_from_parts; +use crate::util::scid_utils::{block_from_scid, scid_from_parts}; use crate::util::ser::{ Readable, ReadableArgs, RequiredWrapper, TransactionU16LenLimited, Writeable, Writer, }; @@ -1421,6 +1421,11 @@ pub(crate) const UNFUNDED_CHANNEL_AGE_LIMIT_TICKS: usize = 60; /// Number of blocks needed for an output from a coinbase transaction to be spendable. pub(crate) const COINBASE_MATURITY: u32 = 100; +/// The number of blocks to wait for a channel_announcement to propagate such that payments using an +/// older SCID can still be relayed. Once the spend of the previous funding transaction has reached +/// this number of confirmations, the corresponding SCID will be forgotten. +const CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY: u32 = 12; + struct PendingChannelMonitorUpdate { update: ChannelMonitorUpdate, } @@ -2425,6 +2430,10 @@ where // blinded paths instead of simple scid+node_id aliases. outbound_scid_alias: u64, + /// Short channel ids used by any prior FundingScope. These are maintained such that + /// ChannelManager can look up the channel for any pending HTLCs. + historical_scids: Vec, + // We track whether we already emitted a `ChannelPending` event. channel_pending_event_emitted: bool, @@ -3275,6 +3284,7 @@ where latest_inbound_scid_alias: None, outbound_scid_alias: 0, + historical_scids: Vec::new(), channel_pending_event_emitted: false, funding_tx_broadcast_safe_event_emitted: false, @@ -3517,6 +3527,7 @@ where latest_inbound_scid_alias: None, outbound_scid_alias, + historical_scids: Vec::new(), channel_pending_event_emitted: false, funding_tx_broadcast_safe_event_emitted: false, @@ -5604,6 +5615,11 @@ where Ok(()) } + + /// Returns SCIDs that have been associated with the channel's funding transactions. + pub fn historical_scids(&self) -> &[u64] { + &self.historical_scids[..] + } } // Internal utility functions for channels @@ -5805,6 +5821,9 @@ where #[cfg(splicing)] macro_rules! promote_splice_funding { ($self: expr, $funding: expr) => { + if let Some(scid) = $self.funding.short_channel_id { + $self.context.historical_scids.push(scid); + } core::mem::swap(&mut $self.funding, $funding); $self.pending_splice = None; $self.pending_funding.clear(); @@ -10643,6 +10662,30 @@ where pub fn has_pending_splice(&self) -> bool { self.pending_splice.is_some() } + + pub fn remove_legacy_scids_before_block(&mut self, height: u32) -> alloc::vec::Drain { + let end = self + .funding + .get_short_channel_id() + .and_then(|current_scid| { + let historical_scids = &self.context.historical_scids; + historical_scids + .iter() + .zip(historical_scids.iter().skip(1).chain(core::iter::once(¤t_scid))) + .map(|(_, next_scid)| { + let funding_height = block_from_scid(*next_scid); + let retain_scid = + funding_height + CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY - 1 > height; + retain_scid + }) + .position(|retain_scid| retain_scid) + }) + .unwrap_or(0); + + // Drains the oldest historical SCIDs until reaching one without + // CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY confirmations. + self.context.historical_scids.drain(0..end) + } } /// A not-yet-funded outbound (from holder) channel using V1 channel establishment. @@ -12073,6 +12116,7 @@ where (57, holding_cell_failure_attribution_data, optional_vec), // Added in 0.2 (58, self.interactive_tx_signing_session, option), // Added in 0.2 (59, self.funding.minimum_depth_override, option), // Added in 0.2 + (60, self.context.historical_scids, optional_vec), // Added in 0.2 }); Ok(()) @@ -12390,6 +12434,7 @@ where 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; @@ -12435,6 +12480,7 @@ where (57, holding_cell_failure_attribution_data, optional_vec), (58, interactive_tx_signing_session, option), // Added in 0.2 (59, minimum_depth_override, option), // Added in 0.2 + (60, historical_scids, optional_vec), // Added in 0.2 }); let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); @@ -12708,6 +12754,7 @@ where latest_inbound_scid_alias, // Later in the ChannelManager deserialization phase we scan for channels and assign scid aliases if its missing outbound_scid_alias, + historical_scids: historical_scids.unwrap(), funding_tx_broadcast_safe_event_emitted: funding_tx_broadcast_safe_event_emitted.unwrap_or(false), channel_pending_event_emitted: channel_pending_event_emitted.unwrap_or(true), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 2f085db4ee6..cd2f185c146 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3192,6 +3192,9 @@ macro_rules! locked_close_channel { debug_assert!(alias_removed); } short_to_chan_info.remove(&$channel_context.outbound_scid_alias()); + for scid in $channel_context.historical_scids() { + short_to_chan_info.remove(scid); + } }} } @@ -12120,6 +12123,18 @@ where channel.check_for_stale_feerate(&logger, feerate)?; } } + + // Remove any SCIDs used by older funding transactions + { + let legacy_scids = channel.remove_legacy_scids_before_block(height); + if !legacy_scids.as_slice().is_empty() { + let mut short_to_chan_info = self.short_to_chan_info.write().unwrap(); + for scid in legacy_scids { + short_to_chan_info.remove(&scid); + } + } + } + channel.best_block_updated(height, header.time, self.chain_hash, &self.node_signer, &self.default_configuration, &&WithChannelContext::from(&self.logger, &channel.context, None)) }); @@ -14502,6 +14517,11 @@ where if let Some(short_channel_id) = channel.funding.get_short_channel_id() { short_to_chan_info.insert(short_channel_id, (channel.context.get_counterparty_node_id(), channel.context.channel_id())); } + + for short_channel_id in channel.context.historical_scids() { + short_to_chan_info.insert(*short_channel_id, (channel.context.get_counterparty_node_id(), channel.context.channel_id())); + } + per_peer_state.entry(channel.context.get_counterparty_node_id()) .or_insert_with(|| Mutex::new(empty_peer_state())) .get_mut().unwrap() From 8b0ac013caa0e8fc83bdc199ca5ae2003e7bf18c Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 30 Apr 2025 10:40:45 -0500 Subject: [PATCH 17/19] Emit SpliceLocked event Once both parties have exchanged splice_locked messages, the splice funding is ready for use. Emit an event to the user indicating as much. --- lightning/src/events/mod.rs | 11 +++++++---- lightning/src/ln/channel.rs | 28 ++++++++++++++-------------- lightning/src/ln/channelmanager.rs | 28 ++++++++++++++++++++++------ 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index c16305bcca0..f86ec1927c5 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1353,10 +1353,13 @@ pub enum Event { /// Will be `None` for channels created prior to LDK version 0.0.122. channel_type: Option, }, - /// Used to indicate that a channel with the given `channel_id` is ready to - /// be used. This event is emitted either when the funding transaction has been confirmed - /// on-chain, or, in case of a 0conf channel, when both parties have confirmed the channel - /// establishment. + /// Used to indicate that a channel with the given `channel_id` is ready to be used. This event + /// is emitted when + /// - the initial funding transaction has been confirmed on-chain to an acceptable depth + /// according to both parties (i.e., `channel_ready` messages were exchanged), + /// - a splice funding transaction has been confirmed on-chain to an acceptable depth according + /// to both parties (i.e., `splice_locked` messages were exchanged), or, + /// - in case of a 0conf channel, when both parties have confirmed the channel establishment. /// /// # Failure Behavior and Persistence /// This event will eventually be replayed after failures-to-handle (i.e., the event handler diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 48f97747a22..fc9c4641100 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2440,8 +2440,8 @@ where // We track whether we already emitted a `FundingTxBroadcastSafe` event. funding_tx_broadcast_safe_event_emitted: bool, - // We track whether we already emitted a `ChannelReady` event. - channel_ready_event_emitted: bool, + // We track whether we already emitted an initial `ChannelReady` event. + initial_channel_ready_event_emitted: bool, /// Some if we initiated to shut down the channel. local_initiated_shutdown: Option<()>, @@ -3288,7 +3288,7 @@ where channel_pending_event_emitted: false, funding_tx_broadcast_safe_event_emitted: false, - channel_ready_event_emitted: false, + initial_channel_ready_event_emitted: false, channel_keys_id, @@ -3531,7 +3531,7 @@ where channel_pending_event_emitted: false, funding_tx_broadcast_safe_event_emitted: false, - channel_ready_event_emitted: false, + initial_channel_ready_event_emitted: false, channel_keys_id, @@ -3959,14 +3959,14 @@ where self.channel_pending_event_emitted = true; } - // Checks whether we should emit a `ChannelReady` event. - pub(crate) fn should_emit_channel_ready_event(&mut self) -> bool { - self.is_usable() && !self.channel_ready_event_emitted + // Checks whether we should emit an initial `ChannelReady` event. + pub(crate) fn should_emit_initial_channel_ready_event(&mut self) -> bool { + self.is_usable() && !self.initial_channel_ready_event_emitted } // Remembers that we already emitted a `ChannelReady` event. - pub(crate) fn set_channel_ready_event_emitted(&mut self) { - self.channel_ready_event_emitted = true; + pub(crate) fn set_initial_channel_ready_event_emitted(&mut self) { + self.initial_channel_ready_event_emitted = true; } // Remembers that we already emitted a `FundingTxBroadcastSafe` event. @@ -12050,7 +12050,7 @@ where { Some(self.context.holder_max_htlc_value_in_flight_msat) } else { None }; let channel_pending_event_emitted = Some(self.context.channel_pending_event_emitted); - let channel_ready_event_emitted = Some(self.context.channel_ready_event_emitted); + let initial_channel_ready_event_emitted = Some(self.context.initial_channel_ready_event_emitted); let funding_tx_broadcast_safe_event_emitted = Some(self.context.funding_tx_broadcast_safe_event_emitted); // `user_id` used to be a single u64 value. In order to remain backwards compatible with @@ -12094,7 +12094,7 @@ where (17, self.context.announcement_sigs_state, required), (19, self.context.latest_inbound_scid_alias, option), (21, self.context.outbound_scid_alias, required), - (23, channel_ready_event_emitted, option), + (23, initial_channel_ready_event_emitted, option), (25, user_id_high_opt, option), (27, self.context.channel_keys_id, required), (28, holder_max_accepted_htlcs, option), @@ -12403,7 +12403,7 @@ where let mut latest_inbound_scid_alias = None; let mut outbound_scid_alias = 0u64; let mut channel_pending_event_emitted = None; - let mut channel_ready_event_emitted = None; + let mut initial_channel_ready_event_emitted = None; let mut funding_tx_broadcast_safe_event_emitted = None; let mut user_id_high_opt: Option = None; @@ -12458,7 +12458,7 @@ where (17, announcement_sigs_state, required), (19, latest_inbound_scid_alias, option), (21, outbound_scid_alias, required), - (23, channel_ready_event_emitted, option), + (23, initial_channel_ready_event_emitted, option), (25, user_id_high_opt, option), (27, channel_keys_id, required), (28, holder_max_accepted_htlcs, option), @@ -12758,7 +12758,7 @@ where funding_tx_broadcast_safe_event_emitted: funding_tx_broadcast_safe_event_emitted.unwrap_or(false), channel_pending_event_emitted: channel_pending_event_emitted.unwrap_or(true), - channel_ready_event_emitted: channel_ready_event_emitted.unwrap_or(true), + initial_channel_ready_event_emitted: initial_channel_ready_event_emitted.unwrap_or(true), channel_keys_id, diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index cd2f185c146..c56efa91fa2 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3355,9 +3355,9 @@ macro_rules! emit_channel_pending_event { }; } -macro_rules! emit_channel_ready_event { +macro_rules! emit_initial_channel_ready_event { ($locked_events: expr, $channel: expr) => { - if $channel.context.should_emit_channel_ready_event() { + if $channel.context.should_emit_initial_channel_ready_event() { debug_assert!($channel.context.channel_pending_event_emitted()); $locked_events.push_back(( events::Event::ChannelReady { @@ -3368,7 +3368,7 @@ macro_rules! emit_channel_ready_event { }, None, )); - $channel.context.set_channel_ready_event_emitted(); + $channel.context.set_initial_channel_ready_event_emitted(); } }; } @@ -8155,7 +8155,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ { let mut pending_events = self.pending_events.lock().unwrap(); emit_channel_pending_event!(pending_events, channel); - emit_channel_ready_event!(pending_events, channel); + emit_initial_channel_ready_event!(pending_events, channel); } (htlc_forwards, decode_update_add_htlcs) @@ -9182,7 +9182,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ { let mut pending_events = self.pending_events.lock().unwrap(); - emit_channel_ready_event!(pending_events, chan); + emit_initial_channel_ready_event!(pending_events, chan); } Ok(()) @@ -10161,6 +10161,14 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ if !chan.has_pending_splice() { let mut short_to_chan_info = self.short_to_chan_info.write().unwrap(); insert_short_channel_id!(short_to_chan_info, chan); + + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back((events::Event::ChannelReady { + channel_id: chan.context.channel_id(), + user_channel_id: chan.context.get_user_id(), + counterparty_node_id: chan.context.get_counterparty_node_id(), + channel_type: chan.funding.get_channel_type().clone(), + }, None)); } if let Some(announcement_sigs) = announcement_sigs_opt { @@ -12267,6 +12275,14 @@ where if !funded_channel.has_pending_splice() { let mut short_to_chan_info = self.short_to_chan_info.write().unwrap(); insert_short_channel_id!(short_to_chan_info, funded_channel); + + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back((events::Event::ChannelReady { + channel_id: funded_channel.context.channel_id(), + user_channel_id: funded_channel.context.get_user_id(), + counterparty_node_id: funded_channel.context.get_counterparty_node_id(), + channel_type: funded_channel.funding.get_channel_type().clone(), + }, None)); } pending_msg_events.push(MessageSendEvent::SendSpliceLocked { @@ -12279,7 +12295,7 @@ where { let mut pending_events = self.pending_events.lock().unwrap(); - emit_channel_ready_event!(pending_events, funded_channel); + emit_initial_channel_ready_event!(pending_events, funded_channel); } if let Some(height) = height_opt { From e583ae853e72f612875ac4bfcf1669e259d9a40a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 6 May 2025 15:37:36 -0500 Subject: [PATCH 18/19] Add funding_txo to ChannelReady event A ChannelReady event is used for both channel establishment and splicing to indicate that the funding transaction is confirmed to an acceptable depth and thus the channel can be used with the funding. An upcoming SplicePending event will be emitted for each pending splice (i.e., both the initial splice attempt and any RBF attempts). Thus, when a ChannelReady event is emitted, the funding_txo must be included to differentiate between which ChannelPending -- which also contains the funding_txo -- that the event corresponds to. --- lightning/src/events/mod.rs | 10 ++++++++++ lightning/src/ln/channelmanager.rs | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index f86ec1927c5..632d3a39af4 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1378,6 +1378,11 @@ pub enum Event { user_channel_id: u128, /// The `node_id` of the channel counterparty. counterparty_node_id: PublicKey, + /// The outpoint of the channel's funding transaction. + /// + /// Will be `None` if the channel's funding transaction reached an acceptable depth prior to + /// version 0.2. + funding_txo: Option, /// The features that this channel will operate with. channel_type: ChannelTypeFeatures, }, @@ -1929,11 +1934,13 @@ impl Writeable for Event { ref channel_id, ref user_channel_id, ref counterparty_node_id, + ref funding_txo, ref channel_type, } => { 29u8.write(writer)?; write_tlv_fields!(writer, { (0, channel_id, required), + (1, funding_txo, option), (2, user_channel_id, required), (4, counterparty_node_id, required), (6, channel_type, required), @@ -2440,9 +2447,11 @@ impl MaybeReadable for Event { let mut channel_id = ChannelId::new_zero(); let mut user_channel_id: u128 = 0; let mut counterparty_node_id = RequiredWrapper(None); + let mut funding_txo = None; let mut channel_type = RequiredWrapper(None); read_tlv_fields!(reader, { (0, channel_id, required), + (1, funding_txo, option), (2, user_channel_id, required), (4, counterparty_node_id, required), (6, channel_type, required), @@ -2452,6 +2461,7 @@ impl MaybeReadable for Event { channel_id, user_channel_id, counterparty_node_id: counterparty_node_id.0.unwrap(), + funding_txo, channel_type: channel_type.0.unwrap(), })) }; diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index c56efa91fa2..656135daf84 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3364,6 +3364,10 @@ macro_rules! emit_initial_channel_ready_event { channel_id: $channel.context.channel_id(), user_channel_id: $channel.context.get_user_id(), counterparty_node_id: $channel.context.get_counterparty_node_id(), + funding_txo: $channel + .funding + .get_funding_txo() + .map(|outpoint| outpoint.into_bitcoin_outpoint()), channel_type: $channel.funding.get_channel_type().clone(), }, None, @@ -10167,6 +10171,7 @@ 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: chan.funding.get_funding_txo().map(|outpoint| outpoint.into_bitcoin_outpoint()), channel_type: chan.funding.get_channel_type().clone(), }, None)); } @@ -12281,6 +12286,7 @@ where channel_id: funded_channel.context.channel_id(), user_channel_id: funded_channel.context.get_user_id(), counterparty_node_id: funded_channel.context.get_counterparty_node_id(), + funding_txo: funded_channel.funding.get_funding_txo().map(|outpoint| outpoint.into_bitcoin_outpoint()), channel_type: funded_channel.funding.get_channel_type().clone(), }, None)); } From 5f34c00f9a38ae435e470d6e57d1373893d5bf53 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 6 Jun 2025 17:38:40 -0500 Subject: [PATCH 19/19] Log confirmation of channel funding transaction --- lightning/src/ln/channel.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index fc9c4641100..bb78ab53d40 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -5526,10 +5526,13 @@ where } #[rustfmt::skip] - fn check_for_funding_tx_confirmed( + fn check_for_funding_tx_confirmed( &mut self, funding: &mut FundingScope, block_hash: &BlockHash, height: u32, - index_in_block: usize, tx: &mut ConfirmedTransaction, - ) -> Result { + index_in_block: usize, tx: &mut ConfirmedTransaction, logger: &L, + ) -> Result + where + L::Target: Logger, + { let funding_txo = match funding.get_funding_txo() { Some(funding_txo) => funding_txo, None => { @@ -5579,6 +5582,14 @@ where Err(_) => panic!("Block was bogus - either height was > 16 million, had > 16 million transactions, or had > 65k outputs"), }; + log_info!( + logger, + "Funding txid {} for channel {} confirmed in block {}", + funding_txo.txid, + &self.channel_id(), + block_hash, + ); + return Ok(true); } } @@ -9130,7 +9141,7 @@ where // and send it immediately instead of waiting for a best_block_updated call (which may have // already happened for this block). let is_funding_tx_confirmed = self.context.check_for_funding_tx_confirmed( - &mut self.funding, block_hash, height, index_in_block, &mut confirmed_tx, + &mut self.funding, block_hash, height, index_in_block, &mut confirmed_tx, logger, )?; if is_funding_tx_confirmed { @@ -9162,7 +9173,7 @@ where #[cfg(splicing)] 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, + 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";