From 089cf2434bf8f2bc699247ed3da72754656d87ff Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:13:48 +0100 Subject: [PATCH] Sort out PendingSplicePre and -Post, new spliced for ChannelContext and OutboundV2Channel --- lightning/src/ln/channel.rs | 283 ++++++++++++++++++++++++++++++++++-- 1 file changed, 273 insertions(+), 10 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index ea87b58acd9..d84c1314ad6 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -25,6 +25,8 @@ use bitcoin::secp256k1::constants::PUBLIC_KEY_SIZE; use bitcoin::secp256k1::{PublicKey,SecretKey}; use bitcoin::secp256k1::{Secp256k1,ecdsa::Signature}; use bitcoin::{secp256k1, sighash}; +#[cfg(splicing)] +use bitcoin::{Sequence, Witness}; use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash}; @@ -1217,7 +1219,7 @@ impl UnfundedChannelContext { /// Info about a pending splice, used in the pre-splice channel #[cfg(splicing)] #[derive(Clone)] -struct PendingSplice { +struct PendingSplicePre { pub our_funding_contribution: i64, pub funding_feerate_perkw: u32, pub locktime: u32, @@ -1226,7 +1228,7 @@ struct PendingSplice { } #[cfg(splicing)] -impl PendingSplice { +impl PendingSplicePre { #[inline] fn add_checked(base: u64, delta: i64) -> u64 { if delta >= 0 { @@ -1242,6 +1244,73 @@ impl PendingSplice { } } +/// Info about a pending splice, used in the post-splice channel +#[cfg(splicing)] +#[derive(Clone)] +struct PendingSplicePost { + pre_channel_value: u64, + post_channel_value: u64, + + /// Save here the previous funding transaction + pub pre_funding_transaction: Option, + /// Save here the previous funding TXO + pub pre_funding_txo: Option, +} + +#[cfg(splicing)] +impl PendingSplicePost { + pub(crate) fn new( + pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64, + pre_funding_transaction: Option, pre_funding_txo: Option, + ) -> Self { + let post_channel_value = PendingSplicePre::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution); + Self { + pre_channel_value, post_channel_value, + pre_funding_transaction, pre_funding_txo, + } + } + + fn pre_channel_value(&self) -> u64 { self.pre_channel_value } + + fn post_channel_value(&self) -> u64 { self.post_channel_value } + + /// Get a transaction input that is the previous funding transaction + fn get_input_of_previous_funding(&self) -> Result<(TxIn, TransactionU16LenLimited), ChannelError> { + if let Some(pre_funding_transaction) = &self.pre_funding_transaction { + if let Some(pre_funding_txo) = &self.pre_funding_txo { + Ok(( + TxIn { + previous_output: pre_funding_txo.into_bitcoin_outpoint(), + script_sig: ScriptBuf::new(), + sequence: Sequence::ZERO, + witness: Witness::new(), + }, + TransactionU16LenLimited::new(pre_funding_transaction.clone()).unwrap(), // TODO err? + )) + } else { + Err(ChannelError::Warn("Internal error: Missing previous funding transaction outpoint".to_string())) + } + } else { + Err(ChannelError::Warn("Internal error: Missing previous funding transaction".to_string())) + } + } + + /// Within the given transaction, find the input that corresponds to the previous funding transaction + fn find_input_of_previous_funding(&self, tx: &Transaction) -> Result { + if let Some(pre_funding_txo) = &self.pre_funding_txo { + for idx in 0..tx.input.len() { + if tx.input[idx].previous_output == pre_funding_txo.into_bitcoin_outpoint() { + return Ok(idx as u16); + } + } + // Not found + Err(ChannelError::Warn("Internal error: Previous funding transaction not found in the inputs of the new funding transaction".to_string())) + } else { + Err(ChannelError::Warn("Internal error: Missing previous funding transaction outpoint".to_string())) + } + } +} + /// Contains everything about the channel including state, and various flags. pub(super) struct ChannelContext where SP::Target: SignerProvider { config: LegacyChannelConfig, @@ -4402,6 +4471,81 @@ impl ChannelContext where SP::Target: SignerProvider { } } + /// Create channel context for spliced channel, by duplicating and updating the context. + /// relative_satoshis: The change in channel value (sats), + /// positive for increase (splice-in), negative for decrease (splice out). + /// delta_belongs_to_local: + /// The amount from the channel value change that belongs to the local (sats). + /// Its sign has to be the same as the sign of relative_satoshis, and its absolute value + /// less or equal (e.g. for +100 in the range of 0..100, for -100 in the range of -100..0). + #[cfg(splicing)] + fn new_for_splice( + pre_splice_context: &Self, + is_outgoing: bool, + counterparty_funding_pubkey: &PublicKey, + our_funding_contribution: i64, + their_funding_contribution: i64, + holder_signer: ::EcdsaSigner, + logger: &L, + ) -> Result, ChannelError> where L::Target: Logger + { + let pre_channel_value = pre_splice_context.channel_value_satoshis; + let post_channel_value = PendingSplicePre::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution); + + // Compute our new balance + let old_to_self = pre_splice_context.value_to_self_msat; + let delta_in_value_to_self = our_funding_contribution * 1000; + if delta_in_value_to_self < 0 && delta_in_value_to_self.abs() as u64 > old_to_self { + // Change would make our balance negative + return Err(ChannelError::Warn(format!("Cannot decrease channel value to requested amount, too low, {} {} {} {} {}", + pre_channel_value, post_channel_value, our_funding_contribution, their_funding_contribution, old_to_self))); + } + let value_to_self_msat = (old_to_self as i64).saturating_add(delta_in_value_to_self) as u64; + + let mut context = pre_splice_context.clone(holder_signer); + + // New channel value + context.channel_value_satoshis = post_channel_value; + // Update value to self + context.value_to_self_msat = value_to_self_msat; + // Reset funding + context.funding_transaction = None; + context.funding_tx_confirmed_in = None; + context.funding_tx_confirmation_height = 0; + context.channel_transaction_parameters.funding_outpoint = None; + // Reset state + context.channel_state = ChannelState::NegotiatingFunding( + if is_outgoing { NegotiatingFundingFlags::OUR_INIT_SENT } else { NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT } + ); + context.next_funding_txid = None; + // Reset monitor update + context.latest_monitor_update_id = 0; + // Note on commitment transaction numbers and commitment points: + // we could step 'back' here (i.e. increase number by one, set cur to prev), but that does not work, + // because latest commitment point would be lost. + // Instead, we take the previous values in relevant cases when splicing is pending. + // We'll add our counterparty's `funding_satoshis` to these max commitment output assertions + // Clear these state flags, for sending `ChannelPending` and `ChannelReady` again + context.channel_pending_event_emitted = false; + context.channel_ready_event_emitted = false; + #[cfg(debug_assertions)] + { + context.holder_max_commitment_tx_output = Arc::new(Mutex::new((value_to_self_msat, post_channel_value.saturating_sub(value_to_self_msat)))); + context.counterparty_max_commitment_tx_output = Arc::new(Mutex::new((value_to_self_msat, post_channel_value.saturating_sub(value_to_self_msat)))); + } + // Reset + context.blocked_monitor_updates = Vec::new(); + + log_debug!(logger, "Splicing channel context: value {} old {}, dir {}, value to self {}, funding keys local {} cp {}", + context.channel_value_satoshis, pre_channel_value, + if is_outgoing { "outgoing" } else { "incoming" }, + context.value_to_self_msat, + context.channel_transaction_parameters.holder_pubkeys.funding_pubkey, counterparty_funding_pubkey + ); + + Ok(context) + } + /// Splice process starting; update state, log, etc. #[cfg(splicing)] pub(crate) fn splice_start(&mut self, is_outgoing: bool, logger: &L) where L::Target: Logger { @@ -4572,7 +4716,10 @@ pub(super) struct Channel where SP::Target: SignerProvider { pub interactive_tx_signing_session: Option, /// Info about an in-progress, pending splice (if any), on the pre-splice channel #[cfg(splicing)] - pending_splice_pre: Option, + pending_splice_pre: Option, + /// Info about an in-progress, pending splice (if any), on the post-splice channel + #[cfg(splicing)] + pending_splice_post: Option, } #[cfg(any(test, fuzzing))] @@ -8175,6 +8322,15 @@ impl Channel where } } + /// Check is a splice is currently in progress + /// Can be called regardless of `splicing` configuration. TODO: remove this note once `cfg(splicing)` is being removed + pub fn is_splice_pending(&self) -> bool { + #[cfg(splicing)] + return self.pending_splice_post.is_some(); + #[cfg(not(splicing))] + false + } + /// Initiate splicing #[cfg(splicing)] pub fn splice_channel(&mut self, our_funding_contribution_satoshis: i64, @@ -8227,7 +8383,7 @@ impl Channel where ))); } - self.pending_splice_pre = Some(PendingSplice { + self.pending_splice_pre = Some(PendingSplicePre { our_funding_contribution: our_funding_contribution_satoshis, funding_feerate_perkw, locktime, @@ -8278,8 +8434,8 @@ impl Channel where ))); } - let post_channel_value = PendingSplice::compute_post_value(pre_channel_value, their_funding_contribution_satoshis, our_funding_contribution_satoshis); - let post_balance = PendingSplice::add_checked(self.context.value_to_self_msat, our_funding_contribution_satoshis); + let post_channel_value = PendingSplicePre::compute_post_value(pre_channel_value, their_funding_contribution_satoshis, our_funding_contribution_satoshis); + let post_balance = PendingSplicePre::add_checked(self.context.value_to_self_msat, our_funding_contribution_satoshis); // Early check for reserve requirement, assuming maximum balance of full channel value // This will also be checked later at tx_complete let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?; @@ -8315,8 +8471,8 @@ impl Channel where let our_funding_contribution = pending_splice.our_funding_contribution; let pre_channel_value = self.context.get_value_satoshis(); - let post_channel_value = PendingSplice::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution_satoshis); - let post_balance = PendingSplice::add_checked(self.context.value_to_self_msat, our_funding_contribution); + let post_channel_value = PendingSplicePre::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution_satoshis); + let post_balance = PendingSplicePre::add_checked(self.context.value_to_self_msat, our_funding_contribution); // Early check for reserve requirement, assuming maximum balance of full channel value // This will also be checked later at tx_complete let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?; @@ -9008,6 +9164,8 @@ impl OutboundV1Channel where SP::Target: SignerProvider { interactive_tx_signing_session: None, #[cfg(splicing)] pending_splice_pre: None, + #[cfg(splicing)] + pending_splice_post: None, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some(); @@ -9235,6 +9393,8 @@ impl InboundV1Channel where SP::Target: SignerProvider { interactive_tx_signing_session: None, #[cfg(splicing)] pending_splice_pre: None, + #[cfg(splicing)] + pending_splice_post: None, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some(); channel.monitor_updating_paused(false, false, need_channel_ready, Vec::new(), Vec::new(), Vec::new()); @@ -9243,6 +9403,29 @@ impl InboundV1Channel where SP::Target: SignerProvider { } } +/// Calculate funding values for interactive tx for splicing, based on channel value changes +#[cfg(splicing)] +fn calculate_funding_values( + pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64, is_initiator: bool, +) -> Result<(u64, u64), ChannelError> { + // Initiator also adds the previous funding as input + let mut our_contribution_with_prev = our_funding_contribution; + let mut their_contribution_with_prev = their_funding_contribution; + if is_initiator { + our_contribution_with_prev = our_contribution_with_prev.saturating_add(pre_channel_value as i64); + } else { + their_contribution_with_prev = their_contribution_with_prev.saturating_add(pre_channel_value as i64); + } + if our_contribution_with_prev < 0 || their_contribution_with_prev < 0 { + return Err(ChannelError::Warn(format!( + "Funding contribution cannot be negative! ours {} theirs {} pre {} initiator {} acceptor {}", + our_contribution_with_prev, their_contribution_with_prev, pre_channel_value, + our_funding_contribution, their_funding_contribution + ))); + } + Ok((our_contribution_with_prev.abs() as u64, their_contribution_with_prev.abs() as u64)) +} + // A not-yet-funded outbound (from holder) channel using V2 channel establishment. pub(super) struct OutboundV2Channel where SP::Target: SignerProvider { pub context: ChannelContext, @@ -9250,6 +9433,9 @@ pub(super) struct OutboundV2Channel where SP::Target: SignerProvider pub dual_funding_context: DualFundingChannelContext, /// The current interactive transaction construction session under negotiation. interactive_tx_constructor: Option, + /// Info about an in-progress, pending splice (if any), on the post-splice channel + #[cfg(splicing)] + pending_splice_post: Option, } impl OutboundV2Channel where SP::Target: SignerProvider { @@ -9310,10 +9496,81 @@ impl OutboundV2Channel where SP::Target: SignerProvider { our_funding_inputs: funding_inputs, }, interactive_tx_constructor: None, + pending_splice_post: None, }; Ok(chan) } + /// Create new channel for splicing + #[cfg(splicing)] + pub fn new_spliced( + is_outbound: bool, + pre_splice_channel: &mut Channel, + signer_provider: &SP, + counterparty_funding_pubkey: &PublicKey, + our_funding_contribution: i64, + their_funding_contribution: i64, + funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, + funding_tx_locktime: LockTime, + funding_feerate_sat_per_1000_weight: u32, + logger: &L, + ) -> Result where L::Target: Logger + { + if pre_splice_channel.is_splice_pending() { + return Err(ChannelError::Warn(format!("Internal error: Channel is already splicing, channel_id {}", pre_splice_channel.context.channel_id))); + } + + let pre_channel_value = pre_splice_channel.context.get_value_satoshis(); + + // Save the current funding transaction + let pre_funding_transaction = pre_splice_channel.context.funding_transaction.clone(); + let pre_funding_txo = pre_splice_channel.context.get_funding_txo().clone(); + + let pending_splice_post = PendingSplicePost::new( + pre_channel_value, our_funding_contribution, their_funding_contribution, + pre_funding_transaction, pre_funding_txo, + ); + let post_channel_value = pending_splice_post.post_channel_value(); + + // Create new signer, using the new channel value. + // Note: channel_keys_id is not changed + let holder_signer = signer_provider.derive_channel_signer(post_channel_value, pre_splice_channel.context.channel_keys_id); + + let context = ChannelContext::new_for_splice( + &pre_splice_channel.context, + true, + counterparty_funding_pubkey, + our_funding_contribution, + their_funding_contribution, + holder_signer, + logger, + )?; + + let (our_funding_satoshis, their_funding_satoshis) = calculate_funding_values( + pre_channel_value, + our_funding_contribution, + their_funding_contribution, + is_outbound, + )?; + + let dual_funding_context = DualFundingChannelContext { + our_funding_satoshis, + their_funding_satoshis: Some(their_funding_satoshis), + funding_tx_locktime, + funding_feerate_sat_per_1000_weight, + our_funding_inputs: Some(funding_inputs), + }; + let unfunded_context = UnfundedChannelContext::default(); + let post_chan = Self { + context, + dual_funding_context, + unfunded_context, + interactive_tx_constructor: None, + pending_splice_post: Some(pending_splice_post), + }; + Ok(post_chan) + } + /// If we receive an error message, it may only be a rejection of the channel type we tried, /// not of our ability to open any channel at all. Thus, on error, we should first call this /// and see if we get a new `OpenChannelV2` message, otherwise the channel is failed. @@ -9383,6 +9640,8 @@ impl OutboundV2Channel where SP::Target: SignerProvider { interactive_tx_signing_session: Some(signing_session), #[cfg(splicing)] pending_splice_pre: None, + #[cfg(splicing)] + pending_splice_post: self.pending_splice_post, }; Ok(channel) @@ -9580,6 +9839,8 @@ impl InboundV2Channel where SP::Target: SignerProvider { interactive_tx_signing_session: Some(signing_session), #[cfg(splicing)] pending_splice_pre: None, + #[cfg(splicing)] + pending_splice_post: None, }; Ok(channel) @@ -10660,6 +10921,8 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, u32, &'c Ch interactive_tx_signing_session: None, #[cfg(splicing)] pending_splice_pre: None, + #[cfg(splicing)] + pending_splice_post: None, }) } } @@ -12496,9 +12759,9 @@ mod tests { #[cfg(all(test, splicing))] fn get_pre_and_post(pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64) -> (u64, u64) { - use crate::ln::channel::PendingSplice; + use crate::ln::channel::PendingSplicePre; - let post_channel_value = PendingSplice::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution); + let post_channel_value = PendingSplicePre::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution); (pre_channel_value, post_channel_value) }