From ef08a54759032fcd3bed786f655eb436016a845f Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:42:52 +0100 Subject: [PATCH 01/10] Add begin_interactive_funding_tx_construction() --- lightning/src/ln/channel.rs | 115 ++++++++++++++++- lightning/src/ln/interactivetxs.rs | 201 ++++++++++++++++++++++++++++- 2 files changed, 305 insertions(+), 11 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index d6fbd162388..56a473d671f 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -10,7 +10,7 @@ use bitcoin::amount::Amount; use bitcoin::constants::ChainHash; use bitcoin::script::{Script, ScriptBuf, Builder, WScriptHash}; -use bitcoin::transaction::{Transaction, TxIn}; +use bitcoin::transaction::{Transaction, TxIn, TxOut}; use bitcoin::sighash::EcdsaSighashType; use bitcoin::consensus::encode; use bitcoin::absolute::LockTime; @@ -30,9 +30,9 @@ use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash}; use crate::types::features::{ChannelTypeFeatures, InitFeatures}; use crate::ln::interactivetxs::{ - get_output_weight, HandleTxCompleteValue, HandleTxCompleteResult, InteractiveTxConstructor, - InteractiveTxConstructorArgs, InteractiveTxSigningSession, InteractiveTxMessageSendResult, - TX_COMMON_FIELDS_WEIGHT, + get_output_weight, calculate_change_output_value, HandleTxCompleteValue, HandleTxCompleteResult, InteractiveTxConstructor, + InteractiveTxConstructorArgs, InteractiveTxMessageSend, InteractiveTxSigningSession, InteractiveTxMessageSendResult, + OutputOwned, SharedOwnedOutput, TX_COMMON_FIELDS_WEIGHT, }; use crate::ln::msgs; use crate::ln::msgs::{ClosingSigned, ClosingSignedFeeRange, DecodeError, OnionErrorPacket}; @@ -2220,6 +2220,106 @@ impl InitialRemoteCommitmentReceiver for FundedChannel where } impl PendingV2Channel where SP::Target: SignerProvider { + #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled + pub fn begin_interactive_funding_tx_construction( + &mut self, signer_provider: &SP, entropy_source: &ES, holder_node_id: PublicKey, + prev_funding_input: Option<(TxIn, TransactionU16LenLimited)>, + ) -> Result, APIError> + where ES::Target: EntropySource + { + let mut funding_inputs = Vec::new(); + mem::swap(&mut self.dual_funding_context.our_funding_inputs, &mut funding_inputs); + + if let Some(prev_funding_input) = prev_funding_input { + funding_inputs.push(prev_funding_input); + } + + let mut funding_inputs_prev_outputs: Vec<&TxOut> = Vec::with_capacity(funding_inputs.len()); + // Check that vouts exist for each TxIn in provided transactions. + for (idx, (txin, tx)) in funding_inputs.iter().enumerate() { + if let Some(output) = tx.as_transaction().output.get(txin.previous_output.vout as usize) { + funding_inputs_prev_outputs.push(output); + } else { + return Err(APIError::APIMisuseError { + err: format!("Transaction with txid {} does not have an output with vout of {} corresponding to TxIn at funding_inputs[{}]", + tx.as_transaction().compute_txid(), txin.previous_output.vout, idx) }); + } + } + + let total_input_satoshis: u64 = funding_inputs.iter().map( + |(txin, tx)| tx.as_transaction().output.get(txin.previous_output.vout as usize).map(|out| out.value.to_sat()).unwrap_or(0) + ).sum(); + if total_input_satoshis < self.dual_funding_context.our_funding_satoshis { + return Err(APIError::APIMisuseError { + err: format!("Total value of funding inputs must be at least funding amount. It was {} sats", + total_input_satoshis) }); + } + + // Add output for funding tx + let mut funding_outputs = Vec::new(); + let funding_output_value_satoshis = self.funding.get_value_satoshis(); + let funding_output_script_pubkey = self.funding.get_funding_redeemscript().to_p2wsh(); + let expected_remote_shared_funding_output = if self.funding.is_outbound() { + let tx_out = TxOut { + value: Amount::from_sat(funding_output_value_satoshis), + script_pubkey: funding_output_script_pubkey, + }; + funding_outputs.push( + if self.dual_funding_context.their_funding_satoshis.unwrap_or(0) == 0 { + OutputOwned::SharedControlFullyOwned(tx_out) + } else { + OutputOwned::Shared(SharedOwnedOutput::new( + tx_out, self.dual_funding_context.our_funding_satoshis + )) + } + ); + None + } else { + Some((funding_output_script_pubkey, funding_output_value_satoshis)) + }; + + // Optionally add change output + if let Some(change_value) = calculate_change_output_value( + self.funding.is_outbound(), self.dual_funding_context.our_funding_satoshis, + &funding_inputs_prev_outputs, &funding_outputs, + self.dual_funding_context.funding_feerate_sat_per_1000_weight, + self.context.holder_dust_limit_satoshis, + ) { + let change_script = signer_provider.get_destination_script(self.context.channel_keys_id).map_err( + |err| APIError::APIMisuseError { + err: format!("Failed to get change script as new destination script, {:?}", err), + })?; + let mut change_output = TxOut { + value: Amount::from_sat(change_value), + script_pubkey: change_script, + }; + let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu(); + let change_output_fee = fee_for_weight(self.dual_funding_context.funding_feerate_sat_per_1000_weight, change_output_weight); + change_output.value = Amount::from_sat(change_value.saturating_sub(change_output_fee)); + funding_outputs.push(OutputOwned::Single(change_output)); + } + + let constructor_args = InteractiveTxConstructorArgs { + entropy_source, + holder_node_id, + counterparty_node_id: self.context.counterparty_node_id, + channel_id: self.context.channel_id(), + feerate_sat_per_kw: self.dual_funding_context.funding_feerate_sat_per_1000_weight, + is_initiator: self.funding.is_outbound(), + funding_tx_locktime: self.dual_funding_context.funding_tx_locktime, + inputs_to_contribute: funding_inputs, + outputs_to_contribute: funding_outputs, + expected_remote_shared_funding_output, + }; + let mut tx_constructor = InteractiveTxConstructor::new(constructor_args) + .map_err(|_| APIError::APIMisuseError { err: "Incorrect shared output provided".into() })?; + let msg = tx_constructor.take_initiator_first_message(); + + self.interactive_tx_constructor = Some(tx_constructor); + + Ok(msg) + } + pub fn tx_add_input(&mut self, msg: &msgs::TxAddInput) -> InteractiveTxMessageSendResult { InteractiveTxMessageSendResult(match &mut self.interactive_tx_constructor { Some(ref mut tx_constructor) => tx_constructor.handle_tx_add_input(msg).map_err( @@ -4849,6 +4949,9 @@ fn check_v2_funding_inputs_sufficient( pub(super) struct DualFundingChannelContext { /// The amount in satoshis we will be contributing to the channel. pub our_funding_satoshis: u64, + /// The amount in satoshis our counterparty will be contributing to the channel. + #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled. + pub their_funding_satoshis: Option, /// The funding transaction locktime suggested by the initiator. If set by us, it is always set /// to the current block height to align incentives against fee-sniping. pub funding_tx_locktime: LockTime, @@ -4860,6 +4963,8 @@ pub(super) struct DualFundingChannelContext { /// Note that the `our_funding_satoshis` field is equal to the total value of `our_funding_inputs` /// minus any fees paid for our contributed weight. This means that change will never be generated /// and the maximum value possible will go towards funding the channel. + /// + /// Note that this field may be emptied once the interactive negotiation has been started. #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled. pub our_funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, } @@ -9835,6 +9940,7 @@ impl PendingV2Channel where SP::Target: SignerProvider { unfunded_context, dual_funding_context: DualFundingChannelContext { our_funding_satoshis: funding_satoshis, + their_funding_satoshis: None, funding_tx_locktime, funding_feerate_sat_per_1000_weight, our_funding_inputs: funding_inputs, @@ -9980,6 +10086,7 @@ impl PendingV2Channel where SP::Target: SignerProvider { let dual_funding_context = DualFundingChannelContext { our_funding_satoshis: our_funding_satoshis, + their_funding_satoshis: Some(msg.common_fields.funding_satoshis), funding_tx_locktime: LockTime::from_consensus(msg.locktime), funding_feerate_sat_per_1000_weight: msg.funding_feerate_sat_per_1000_weight, our_funding_inputs: our_funding_inputs.clone(), diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 9fbcaef92d2..392a95197a5 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -1152,13 +1152,13 @@ pub(crate) enum InteractiveTxInput { } #[derive(Clone, Debug, Eq, PartialEq)] -pub struct SharedOwnedOutput { +pub(super) struct SharedOwnedOutput { tx_out: TxOut, local_owned: u64, } impl SharedOwnedOutput { - fn new(tx_out: TxOut, local_owned: u64) -> SharedOwnedOutput { + pub fn new(tx_out: TxOut, local_owned: u64) -> SharedOwnedOutput { debug_assert!( local_owned <= tx_out.value.to_sat(), "SharedOwnedOutput: Inconsistent local_owned value {}, larger than output value {}", @@ -1177,7 +1177,7 @@ impl SharedOwnedOutput { /// its control -- exclusive by the adder or shared --, and /// its ownership -- value fully owned by the adder or jointly #[derive(Clone, Debug, Eq, PartialEq)] -pub enum OutputOwned { +pub(super) enum OutputOwned { /// Belongs to a single party -- controlled exclusively and fully belonging to a single party Single(TxOut), /// Output with shared control, but fully belonging to local node @@ -1187,7 +1187,7 @@ pub enum OutputOwned { } impl OutputOwned { - fn tx_out(&self) -> &TxOut { + pub fn tx_out(&self) -> &TxOut { match self { OutputOwned::Single(tx_out) | OutputOwned::SharedControlFullyOwned(tx_out) => tx_out, OutputOwned::Shared(output) => &output.tx_out, @@ -1663,14 +1663,57 @@ impl InteractiveTxConstructor { } } +/// Determine whether a change output should be added or not, and if so, of what size, +/// considering our given inputs, outputs, and intended contribution. +/// Computes and takes into account fees. +/// Return value is the value computed for the change output (in satoshis), +/// or None if a change is not needed/possible. +#[allow(dead_code)] // TODO(dual_funding): Remove once begin_interactive_funding_tx_construction() is used +pub(super) fn calculate_change_output_value( + is_initiator: bool, our_contribution: u64, funding_inputs_prev_outputs: &Vec<&TxOut>, + funding_outputs: &Vec, funding_feerate_sat_per_1000_weight: u32, + holder_dust_limit_satoshis: u64, +) -> Option { + let our_funding_inputs_weight = + funding_inputs_prev_outputs.iter().fold(0u64, |weight, prev_output| { + weight.saturating_add(estimate_input_weight(prev_output).to_wu()) + }); + let our_funding_outputs_weight = funding_outputs.iter().fold(0u64, |weight, out| { + weight.saturating_add(get_output_weight(&out.tx_out().script_pubkey).to_wu()) + }); + let our_contributed_weight = + our_funding_outputs_weight.saturating_add(our_funding_inputs_weight); + let mut fees_sats = fee_for_weight(funding_feerate_sat_per_1000_weight, our_contributed_weight); + + // If we are the initiator, we must pay for weight of all common fields in the funding transaction. + if is_initiator { + let common_fees = + fee_for_weight(funding_feerate_sat_per_1000_weight, TX_COMMON_FIELDS_WEIGHT); + fees_sats = fees_sats.saturating_add(common_fees); + } + + let total_input_satoshis: u64 = + funding_inputs_prev_outputs.iter().map(|out| out.value.to_sat()).sum(); + + let remaining_value = + total_input_satoshis.saturating_sub(our_contribution).saturating_sub(fees_sats); + + if remaining_value <= holder_dust_limit_satoshis { + None + } else { + Some(remaining_value) + } +} + #[cfg(test)] mod tests { use crate::chain::chaininterface::{fee_for_weight, FEERATE_FLOOR_SATS_PER_KW}; use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; use crate::ln::interactivetxs::{ - generate_holder_serial_id, AbortReason, HandleTxCompleteValue, InteractiveTxConstructor, - InteractiveTxConstructorArgs, InteractiveTxMessageSend, MAX_INPUTS_OUTPUTS_COUNT, - MAX_RECEIVED_TX_ADD_INPUT_COUNT, MAX_RECEIVED_TX_ADD_OUTPUT_COUNT, + calculate_change_output_value, generate_holder_serial_id, AbortReason, + HandleTxCompleteValue, InteractiveTxConstructor, InteractiveTxConstructorArgs, + InteractiveTxMessageSend, MAX_INPUTS_OUTPUTS_COUNT, MAX_RECEIVED_TX_ADD_INPUT_COUNT, + MAX_RECEIVED_TX_ADD_OUTPUT_COUNT, }; use crate::ln::types::ChannelId; use crate::sign::EntropySource; @@ -2595,4 +2638,148 @@ mod tests { assert_eq!(generate_holder_serial_id(&&entropy_source, true) % 2, 0); assert_eq!(generate_holder_serial_id(&&entropy_source, false) % 2, 1) } + + #[test] + fn test_calculate_change_output_value_open() { + let input_prevouts_owned = vec![ + TxOut { value: Amount::from_sat(70_000), script_pubkey: ScriptBuf::new() }, + TxOut { value: Amount::from_sat(60_000), script_pubkey: ScriptBuf::new() }, + ]; + let input_prevouts: Vec<&TxOut> = input_prevouts_owned.iter().collect(); + let our_contributed = 110_000; + let txout = TxOut { value: Amount::from_sat(128_000), script_pubkey: ScriptBuf::new() }; + let outputs = vec![OutputOwned::SharedControlFullyOwned(txout)]; + let funding_feerate_sat_per_1000_weight = 3000; + + let total_inputs: u64 = input_prevouts.iter().map(|o| o.value.to_sat()).sum(); + let gross_change = total_inputs - our_contributed; + let fees = 1746; + let common_fees = 126; + { + // There is leftover for change + let res = calculate_change_output_value( + true, + our_contributed, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 300, + ); + assert_eq!(res.unwrap(), gross_change - fees - common_fees); + } + { + // There is leftover for change, without common fees + let res = calculate_change_output_value( + false, + our_contributed, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 300, + ); + assert_eq!(res.unwrap(), gross_change - fees); + } + { + // Larger fee, smaller change + let res = calculate_change_output_value( + true, + our_contributed, + &input_prevouts, + &outputs, + 9000, + 300, + ); + assert_eq!(res.unwrap(), 14384); + } + { + // Insufficient inputs, no leftover + let res = calculate_change_output_value( + false, + 130_000, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 300, + ); + assert!(res.is_none()); + } + { + // Very small leftover + let res = calculate_change_output_value( + false, + 128_100, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 300, + ); + assert!(res.is_none()); + } + { + // Small leftover, but not dust + let res = calculate_change_output_value( + false, + 128_100, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 100, + ); + assert_eq!(res.unwrap(), 154); + } + } + + #[test] + fn test_calculate_change_output_value_splice() { + let input_prevouts_owned = vec![ + TxOut { value: Amount::from_sat(70_000), script_pubkey: ScriptBuf::new() }, + TxOut { value: Amount::from_sat(60_000), script_pubkey: ScriptBuf::new() }, + ]; + let input_prevouts: Vec<&TxOut> = input_prevouts_owned.iter().collect(); + let our_contributed = 110_000; + let txout = TxOut { value: Amount::from_sat(148_000), script_pubkey: ScriptBuf::new() }; + let outputs = vec![OutputOwned::Shared(SharedOwnedOutput::new(txout, our_contributed))]; + let funding_feerate_sat_per_1000_weight = 3000; + + let total_inputs: u64 = input_prevouts.iter().map(|o| o.value.to_sat()).sum(); + let gross_change = total_inputs - our_contributed; + let fees = 1746; + let common_fees = 126; + { + // There is leftover for change + let res = calculate_change_output_value( + true, + our_contributed, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 300, + ); + assert_eq!(res.unwrap(), gross_change - fees - common_fees); + } + { + // Very small leftover + let res = calculate_change_output_value( + false, + 128_100, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 300, + ); + assert!(res.is_none()); + } + { + // Small leftover, but not dust + let res = calculate_change_output_value( + false, + 128_100, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 100, + ); + assert_eq!(res.unwrap(), 154); + } + } } From a9df01c57bf9972e3d3ae701a07130397e1e0c3f Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Wed, 20 Nov 2024 22:39:49 +0100 Subject: [PATCH 02/10] New splice_channel() for initiating splicing, handle splice_init and splice_ack messages, but fail afterwards --- lightning/src/ln/channel.rs | 157 ++++++++++++++++++++++++++--- lightning/src/ln/channelmanager.rs | 4 +- 2 files changed, 147 insertions(+), 14 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 56a473d671f..ba092578c9d 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1522,7 +1522,7 @@ impl Channel where holder_commitment_point, is_v2_established: true, #[cfg(splicing)] - pending_splice: None, + pending_splice_pre: None, }; let res = funded_channel.commitment_signed_initial_v2(msg, best_block, signer_provider, logger) .map(|monitor| (Some(monitor), None)) @@ -1727,8 +1727,30 @@ impl FundingScope { /// Info about a pending splice, used in the pre-splice channel #[cfg(splicing)] +#[derive(Clone)] struct PendingSplice { pub our_funding_contribution: i64, + pub funding_feerate_per_kw: u32, + pub locktime: u32, + /// The funding inputs that we plan to contributing to the splice. + pub our_funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, +} + +#[cfg(splicing)] +impl PendingSplice { + #[inline] + fn add_checked(base: u64, delta: i64) -> u64 { + if delta >= 0 { + base.saturating_add(delta as u64) + } else { + base.saturating_sub(delta.abs() as u64) + } + } + + /// Compute the post-splice channel value from the pre-splice values and the peer contributions + pub fn compute_post_value(pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64) -> u64 { + Self::add_checked(pre_channel_value, our_funding_contribution.saturating_add(their_funding_contribution)) + } } /// Contains everything about the channel including state, and various flags. @@ -4981,7 +5003,7 @@ pub(super) struct FundedChannel where SP::Target: SignerProvider { is_v2_established: bool, /// Info about an in-progress, pending splice (if any), on the pre-splice channel #[cfg(splicing)] - pending_splice: Option, + pending_splice_pre: Option, } #[cfg(any(test, fuzzing))] @@ -8516,7 +8538,7 @@ impl FundedChannel where ) -> Result { // Check if a splice has been initiated already. // Note: only a single outstanding splice is supported (per spec) - if let Some(splice_info) = &self.pending_splice { + if let Some(splice_info) = &self.pending_splice_pre { return Err(APIError::APIMisuseError { err: format!( "Channel {} cannot be spliced, as it has already a splice pending (contribution {})", self.context.channel_id(), splice_info.our_funding_contribution @@ -8552,8 +8574,20 @@ impl FundedChannel where self.context.channel_id(), err, )})?; - self.pending_splice = Some(PendingSplice { + // convert inputs + let mut funding_inputs = Vec::new(); + for (tx_in, tx, _w) in our_funding_inputs.into_iter() { + let tx16 = TransactionU16LenLimited::new(tx.clone()).map_err( + |e| APIError::APIMisuseError { err: format!("Too large transaction, {:?}", e)} + )?; + funding_inputs.push((tx_in.clone(), tx16)); + } + + self.pending_splice_pre = Some(PendingSplice { our_funding_contribution: our_funding_contribution_satoshis, + funding_feerate_per_kw, + locktime, + our_funding_inputs: funding_inputs, }); let msg = self.get_splice_init(our_funding_contribution_satoshis, funding_feerate_per_kw, locktime); @@ -8580,13 +8614,13 @@ impl FundedChannel where /// Handle splice_init #[cfg(splicing)] - pub fn splice_init(&mut self, msg: &msgs::SpliceInit) -> Result { + pub fn splice_init(&mut self, msg: &msgs::SpliceInit, logger: &L) -> Result where L::Target: Logger { let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; // TODO(splicing): Currently not possible to contribute on the splicing-acceptor side let our_funding_contribution_satoshis = 0i64; // Check if a splice has been initiated already. - if let Some(splice_info) = &self.pending_splice { + if let Some(splice_info) = &self.pending_splice_pre { return Err(ChannelError::Warn(format!( "Channel has already a splice pending, contribution {}", splice_info.our_funding_contribution, ))); @@ -8615,7 +8649,8 @@ impl FundedChannel where // TODO(splicing): Once splice acceptor can contribute, add reserve pre-check, similar to the one in `splice_ack`. // TODO(splicing): Store msg.funding_pubkey - // TODO(splicing): Apply start of splice (splice_start) + // Apply start of splice change in the state + self.splice_start(false, logger); // TODO(splicing): The exisiting pubkey is reused, but a new one should be generated. See #3542. // Note that channel_keys_id is supposed NOT to change @@ -8626,22 +8661,55 @@ impl FundedChannel where require_confirmed_inputs: None, }; // TODO(splicing): start interactive funding negotiation + // let _msg = self.begin_interactive_funding_tx_construction(signer_provider, entropy_source, holder_node_id) + // .map_err(|err| ChannelError::Warn(format!("Failed to start interactive transaction construction, {:?}", err)))?; + Ok(splice_ack_msg) } /// Handle splice_ack #[cfg(splicing)] - pub fn splice_ack(&mut self, _msg: &msgs::SpliceAck) -> Result<(), ChannelError> { + pub fn splice_ack(&mut self, msg: &msgs::SpliceAck, logger: &L) -> Result<(), ChannelError> where L::Target: Logger { // check if splice is pending - if self.pending_splice.is_none() { + let pending_splice = if let Some(pending_splice) = &self.pending_splice_pre { + pending_splice + } else { return Err(ChannelError::Warn(format!("Channel is not in pending splice"))); }; + + let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; + let our_funding_contribution = pending_splice.our_funding_contribution; + + let pre_channel_value = self.funding.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.funding.value_to_self_msat, our_funding_contribution); + // TODO(splicing): Pre-check for reserve requirement // (Note: It should also be checked later at tx_complete) + + // Apply start of splice change in the state + self.splice_start(true, logger); + + // TODO(splicing): start interactive funding negotiation + // let tx_msg_opt = self.begin_interactive_funding_tx_construction(signer_provider, entropy_source, holder_node_id) + // .map_err(|err| ChannelError::Warn(format!("V2 channel rejected due to sender error, {:?}", err)))?; + // Ok(tx_msg_opt) Ok(()) } + /// 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 { + // Set state, by this point splice_init/splice_ack handshake is complete + // TODO(splicing) + // self.channel_state = ChannelState::NegotiatingFunding( + // NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT + // ); + log_info!(logger, "Splicing process started, old channel value {}, outgoing {}, channel_id {}", + self.funding.get_value_satoshis(), is_outgoing, self.context.channel_id); + } + // Send stuff to our remote peers: /// Queues up an outbound HTLC to send by placing it in the holding cell. You should call @@ -9565,7 +9633,7 @@ impl OutboundV1Channel where SP::Target: SignerProvider { is_v2_established: false, holder_commitment_point, #[cfg(splicing)] - pending_splice: None, + pending_splice_pre: None, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some() @@ -9841,7 +9909,7 @@ impl InboundV1Channel where SP::Target: SignerProvider { is_v2_established: false, holder_commitment_point, #[cfg(splicing)] - pending_splice: None, + pending_splice_pre: None, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some() || channel.context.signer_pending_channel_ready; @@ -11224,7 +11292,7 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, &'c Channel is_v2_established, holder_commitment_point, #[cfg(splicing)] - pending_splice: None, + pending_splice_pre: None, }) } } @@ -13146,4 +13214,69 @@ 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; + + let post_channel_value = PendingSplice::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution); + (pre_channel_value, post_channel_value) + } + + #[cfg(all(test, splicing))] + #[test] + fn test_splice_compute_post_value() { + { + // increase, small amounts + let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, 6_000, 0); + assert_eq!(pre_channel_value, 9_000); + assert_eq!(post_channel_value, 15_000); + } + { + // increase, small amounts + let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, 4_000, 2_000); + assert_eq!(pre_channel_value, 9_000); + assert_eq!(post_channel_value, 15_000); + } + { + // increase, small amounts + let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, 0, 6_000); + assert_eq!(pre_channel_value, 9_000); + assert_eq!(post_channel_value, 15_000); + } + { + // decrease, small amounts + let (pre_channel_value, post_channel_value) = get_pre_and_post(15_000, -6_000, 0); + assert_eq!(pre_channel_value, 15_000); + assert_eq!(post_channel_value, 9_000); + } + { + // decrease, small amounts + let (pre_channel_value, post_channel_value) = get_pre_and_post(15_000, -4_000, -2_000); + assert_eq!(pre_channel_value, 15_000); + assert_eq!(post_channel_value, 9_000); + } + { + // increase and decrease + let (pre_channel_value, post_channel_value) = get_pre_and_post(15_000, 4_000, -2_000); + assert_eq!(pre_channel_value, 15_000); + assert_eq!(post_channel_value, 17_000); + } + let base2: u64 = 2; + let huge63i3 = (base2.pow(63) - 3) as i64; + assert_eq!(huge63i3, 9223372036854775805); + assert_eq!(-huge63i3, -9223372036854775805); + { + // increase, large amount + let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, huge63i3, 3); + assert_eq!(pre_channel_value, 9_000); + assert_eq!(post_channel_value, 9223372036854784807); + } + { + // increase, large amounts + let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, huge63i3, huge63i3); + assert_eq!(pre_channel_value, 9_000); + assert_eq!(post_channel_value, 9223372036854784807); + } + } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index fab15bfea28..a8692f1b2ae 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -9530,7 +9530,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ), msg.channel_id)), hash_map::Entry::Occupied(mut chan_entry) => { if let Some(chan) = chan_entry.get_mut().as_funded_mut() { - let splice_ack_msg = try_channel_entry!(self, peer_state, chan.splice_init(msg), chan_entry); + let splice_ack_msg = try_channel_entry!(self, peer_state, chan.splice_init(msg, &self.logger), chan_entry); peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceAck { node_id: *counterparty_node_id, msg: splice_ack_msg, @@ -9569,7 +9569,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ), msg.channel_id)), hash_map::Entry::Occupied(mut chan_entry) => { if let Some(chan) = chan_entry.get_mut().as_funded_mut() { - try_channel_entry!(self, peer_state, chan.splice_ack(msg), chan_entry); + try_channel_entry!(self, peer_state, chan.splice_ack(msg, &self.logger), chan_entry); } else { return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel is not funded, cannot splice".to_owned(), msg.channel_id)); } From d38ca62687cd04f1228ecf2266f4bc368c0ad88d Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Thu, 6 Mar 2025 02:06:36 +0100 Subject: [PATCH 03/10] Add begin_interactive_funding_tx_construction() --- lightning/src/ln/channel.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index ba092578c9d..7077973d331 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -4972,7 +4972,6 @@ pub(super) struct DualFundingChannelContext { /// The amount in satoshis we will be contributing to the channel. pub our_funding_satoshis: u64, /// The amount in satoshis our counterparty will be contributing to the channel. - #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled. pub their_funding_satoshis: Option, /// The funding transaction locktime suggested by the initiator. If set by us, it is always set /// to the current block height to align incentives against fee-sniping. From fd1c251c6096553e6d7a20b7354215234a6bb35b Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:49:57 +0100 Subject: [PATCH 04/10] Add RefundingV2 phase (for splicing) --- lightning/src/ln/channel.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 7077973d331..cf2fa11e441 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1189,6 +1189,9 @@ enum ChannelPhase where SP::Target: SignerProvider { UnfundedInboundV1(InboundV1Channel), UnfundedV2(PendingV2Channel), Funded(FundedChannel), + /// Used during splicing, channel is funded but a new funding is being renegotiated. + #[cfg(splicing)] + RefundingV2(FundedChannel), } impl Channel where @@ -1202,6 +1205,8 @@ impl Channel where ChannelPhase::UnfundedOutboundV1(chan) => &chan.context, ChannelPhase::UnfundedInboundV1(chan) => &chan.context, ChannelPhase::UnfundedV2(chan) => &chan.context, + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => &chan.context, } } @@ -1212,6 +1217,8 @@ impl Channel where ChannelPhase::UnfundedOutboundV1(chan) => &mut chan.context, ChannelPhase::UnfundedInboundV1(chan) => &mut chan.context, ChannelPhase::UnfundedV2(chan) => &mut chan.context, + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => &mut chan.context, } } @@ -1222,6 +1229,8 @@ impl Channel where ChannelPhase::UnfundedOutboundV1(chan) => &chan.funding, ChannelPhase::UnfundedInboundV1(chan) => &chan.funding, ChannelPhase::UnfundedV2(chan) => &chan.funding, + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => &chan.funding, } } @@ -1233,6 +1242,8 @@ impl Channel where ChannelPhase::UnfundedOutboundV1(chan) => &mut chan.funding, ChannelPhase::UnfundedInboundV1(chan) => &mut chan.funding, ChannelPhase::UnfundedV2(chan) => &mut chan.funding, + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => &mut chan.funding, } } @@ -1243,6 +1254,8 @@ impl Channel where ChannelPhase::UnfundedOutboundV1(chan) => (&chan.funding, &mut chan.context), ChannelPhase::UnfundedInboundV1(chan) => (&chan.funding, &mut chan.context), ChannelPhase::UnfundedV2(chan) => (&chan.funding, &mut chan.context), + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => (&chan.funding, &mut chan.context), } } @@ -1253,6 +1266,8 @@ impl Channel where ChannelPhase::UnfundedOutboundV1(chan) => Some(&mut chan.unfunded_context), ChannelPhase::UnfundedInboundV1(chan) => Some(&mut chan.unfunded_context), ChannelPhase::UnfundedV2(chan) => Some(&mut chan.unfunded_context), + #[cfg(splicing)] + ChannelPhase::RefundingV2(_) => { debug_assert!(false); None }, } } @@ -1360,6 +1375,8 @@ impl Channel where }) }, ChannelPhase::UnfundedV2(_) => None, + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => Some(chan.signer_maybe_unblocked(logger)), } } @@ -1379,6 +1396,8 @@ impl Channel where ChannelPhase::UnfundedOutboundV1(chan) => chan.is_resumable(), ChannelPhase::UnfundedInboundV1(_) => false, ChannelPhase::UnfundedV2(_) => false, + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => chan.remove_uncommitted_htlcs_and_mark_paused(logger).is_ok(), } } @@ -1416,6 +1435,9 @@ impl Channel where ReconnectionMsg::None } }, + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => + ReconnectionMsg::Reestablish(chan.get_channel_reestablish(logger)), } } @@ -1443,6 +1465,8 @@ impl Channel where Ok(None) } }, + #[cfg(splicing)] + ChannelPhase::RefundingV2(_) => Ok(None), } } From b1a96a4088b3a0a670f19169cfbffc322d6a58e3 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Mon, 23 Sep 2024 23:52:07 +0200 Subject: [PATCH 05/10] Clone for ChannelContext --- lightning/src/ln/channel.rs | 177 +++++++++++++++++++++++++++++++++++- 1 file changed, 172 insertions(+), 5 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index cf2fa11e441..12df70cb8f4 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -115,6 +115,7 @@ enum FeeUpdateState { Outbound, } +#[derive(Clone)] enum InboundHTLCRemovalReason { FailRelay(msgs::OnionErrorPacket), FailMalformed(([u8; 32], u16)), @@ -149,6 +150,7 @@ impl_writeable_tlv_based_enum!(InboundHTLCResolution, }, ); +#[derive(Clone)] enum InboundHTLCState { /// Offered by remote, to be included in next local commitment tx. I.e., the remote sent an /// update_add_htlc message for this HTLC. @@ -223,6 +225,7 @@ impl From<&InboundHTLCState> for Option { } } +#[derive(Clone)] struct InboundHTLCOutput { htlc_id: u64, amount_msat: u64, @@ -231,7 +234,8 @@ struct InboundHTLCOutput { state: InboundHTLCState, } -#[cfg_attr(test, derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(test, derive(Debug, PartialEq))] enum OutboundHTLCState { /// Added by us and included in a commitment_signed (if we were AwaitingRemoteRevoke when we /// created it we would have put it in the holding cell instead). When they next revoke_and_ack @@ -313,7 +317,8 @@ impl<'a> Into> for &'a OutboundHTLCOutcome { } } -#[cfg_attr(test, derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(test, derive(Debug, PartialEq))] struct OutboundHTLCOutput { htlc_id: u64, amount_msat: u64, @@ -326,7 +331,8 @@ struct OutboundHTLCOutput { } /// See AwaitingRemoteRevoke ChannelState for more info -#[cfg_attr(test, derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(test, derive(Debug, PartialEq))] enum HTLCUpdateAwaitingACK { AddHTLC { // TODO: Time out if we're getting close to cltv_expiry // always outbound @@ -836,7 +842,7 @@ pub(super) enum ChannelUpdateStatus { } /// We track when we sent an `AnnouncementSignatures` to our peer in a few states, described here. -#[derive(PartialEq)] +#[derive(Clone, PartialEq)] pub enum AnnouncementSigsState { /// We have not sent our peer an `AnnouncementSignatures` yet, or our peer disconnected since /// we sent the last `AnnouncementSignatures`. @@ -1167,6 +1173,7 @@ 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; +#[derive(Clone)] struct PendingChannelMonitorUpdate { update: ChannelMonitorUpdate, } @@ -4851,6 +4858,112 @@ impl ChannelContext where SP::Target: SignerProvider { self.counterparty_cur_commitment_point = Some(counterparty_cur_commitment_point_override); self.get_initial_counterparty_commitment_signature(funding, logger) } + + /// Clone, each field, with the exception of the channel signer. + #[allow(unused)] + fn clone(&self, holder_signer: ::EcdsaSigner) -> Self { + Self { + // Use provided channel signer + holder_signer: ChannelSignerType::Ecdsa(holder_signer), + + config: self.config, + prev_config: self.prev_config, + inbound_handshake_limits_override: self.inbound_handshake_limits_override, + user_id: self.user_id, + channel_id: self.channel_id, + temporary_channel_id: self.temporary_channel_id, + channel_state: self.channel_state, + announcement_sigs_state: self.announcement_sigs_state.clone(), + secp_ctx: self.secp_ctx.clone(), + // channel_value_satoshis: self.channel_value_satoshis, + latest_monitor_update_id: self.latest_monitor_update_id, + shutdown_scriptpubkey: self.shutdown_scriptpubkey.clone(), + destination_script: self.destination_script.clone(), + // holder_commitment_point: self.holder_commitment_point, + cur_counterparty_commitment_transaction_number: self.cur_counterparty_commitment_transaction_number, + // value_to_self_msat: self.value_to_self_msat, + pending_inbound_htlcs: self.pending_inbound_htlcs.clone(), + pending_outbound_htlcs: self.pending_outbound_htlcs.clone(), + holding_cell_htlc_updates: self.holding_cell_htlc_updates.clone(), + resend_order: self.resend_order.clone(), + monitor_pending_channel_ready: self.monitor_pending_channel_ready, + monitor_pending_revoke_and_ack: self.monitor_pending_revoke_and_ack, + monitor_pending_commitment_signed: self.monitor_pending_commitment_signed, + monitor_pending_forwards: self.monitor_pending_forwards.clone(), + monitor_pending_failures: self.monitor_pending_failures.clone(), + monitor_pending_finalized_fulfills: self.monitor_pending_finalized_fulfills.clone(), + monitor_pending_update_adds: self.monitor_pending_update_adds.clone(), + monitor_pending_tx_signatures: self.monitor_pending_tx_signatures.clone(), + signer_pending_revoke_and_ack: self.signer_pending_revoke_and_ack, + signer_pending_commitment_update: self.signer_pending_commitment_update, + signer_pending_funding: self.signer_pending_funding, + signer_pending_closing: self.signer_pending_closing, + signer_pending_channel_ready: self.signer_pending_channel_ready, + pending_update_fee: self.pending_update_fee, + holding_cell_update_fee: self.holding_cell_update_fee, + next_holder_htlc_id: self.next_holder_htlc_id, + next_counterparty_htlc_id: self.next_counterparty_htlc_id, + feerate_per_kw: self.feerate_per_kw, + update_time_counter: self.update_time_counter, + // Create new mutex with copied values + // #[cfg(debug_assertions)] + // holder_max_commitment_tx_output: self.holder_max_commitment_tx_output.clone(), + // #[cfg(debug_assertions)] + // counterparty_max_commitment_tx_output: self.counterparty_max_commitment_tx_output.clone(), + last_sent_closing_fee: self.last_sent_closing_fee.clone(), + last_received_closing_sig: self.last_received_closing_sig, + target_closing_feerate_sats_per_kw: self.target_closing_feerate_sats_per_kw, + pending_counterparty_closing_signed: self.pending_counterparty_closing_signed.clone(), + closing_fee_limits: self.closing_fee_limits, + expecting_peer_commitment_signed: self.expecting_peer_commitment_signed, + funding_tx_confirmed_in: self.funding_tx_confirmed_in, + funding_tx_confirmation_height: self.funding_tx_confirmation_height, + short_channel_id: self.short_channel_id, + channel_creation_height: self.channel_creation_height, + counterparty_dust_limit_satoshis: self.counterparty_dust_limit_satoshis, + holder_dust_limit_satoshis: self.holder_dust_limit_satoshis, + counterparty_max_htlc_value_in_flight_msat: self.counterparty_max_htlc_value_in_flight_msat, + holder_max_htlc_value_in_flight_msat: self.holder_max_htlc_value_in_flight_msat, + // counterparty_selected_channel_reserve_satoshis: self.counterparty_selected_channel_reserve_satoshis, + // holder_selected_channel_reserve_satoshis: self.holder_selected_channel_reserve_satoshis, + counterparty_htlc_minimum_msat: self.counterparty_htlc_minimum_msat, + holder_htlc_minimum_msat: self.holder_htlc_minimum_msat, + counterparty_max_accepted_htlcs: self.counterparty_max_accepted_htlcs, + holder_max_accepted_htlcs: self.holder_max_accepted_htlcs, + minimum_depth: self.minimum_depth, + counterparty_forwarding_info: self.counterparty_forwarding_info.clone(), + // channel_transaction_parameters: self.channel_transaction_parameters.clone(), + // funding_transaction: self.funding_transaction.clone(), + is_manual_broadcast: self.is_manual_broadcast, + is_batch_funding: self.is_batch_funding, + counterparty_cur_commitment_point: self.counterparty_cur_commitment_point, + counterparty_prev_commitment_point: self.counterparty_prev_commitment_point, + counterparty_node_id: self.counterparty_node_id, + counterparty_shutdown_scriptpubkey: self.counterparty_shutdown_scriptpubkey.clone(), + commitment_secrets: self.commitment_secrets.clone(), + channel_update_status: self.channel_update_status, + closing_signed_in_flight: self.closing_signed_in_flight, + announcement_sigs: self.announcement_sigs, + // Create new mutex with copied values + // #[cfg(any(test, fuzzing))] + // next_local_commitment_tx_fee_info_cached: self.next_local_commitment_tx_fee_info_cached.clone(), + // #[cfg(any(test, fuzzing))] + // next_remote_commitment_tx_fee_info_cached: self.next_remote_commitment_tx_fee_info_cached.clone(), + workaround_lnd_bug_4006: self.workaround_lnd_bug_4006.clone(), + sent_message_awaiting_response: self.sent_message_awaiting_response, + channel_type: self.channel_type.clone(), + latest_inbound_scid_alias: self.latest_inbound_scid_alias, + outbound_scid_alias: self.outbound_scid_alias, + channel_pending_event_emitted: self.channel_pending_event_emitted, + funding_tx_broadcast_safe_event_emitted: self.funding_tx_broadcast_safe_event_emitted, + channel_ready_event_emitted: self.channel_ready_event_emitted, + local_initiated_shutdown: self.local_initiated_shutdown.clone(), + channel_keys_id: self.channel_keys_id, + blocked_monitor_updates: self.blocked_monitor_updates.clone(), + next_funding_txid: self.next_funding_txid.clone(), + is_holder_quiescence_initiator: self.is_holder_quiescence_initiator, + } + } } // Internal utility functions for channels @@ -5030,6 +5143,7 @@ pub(super) struct FundedChannel where SP::Target: SignerProvider { } #[cfg(any(test, fuzzing))] +#[derive(Clone)] struct CommitmentTxInfoCached { fee: u64, total_pending_htlcs: usize, @@ -11338,7 +11452,7 @@ mod tests { use crate::ln::channel_keys::{RevocationKey, RevocationBasepoint}; use crate::ln::channelmanager::{self, HTLCSource, PaymentId}; use crate::ln::channel::InitFeatures; - use crate::ln::channel::{AwaitingChannelReadyFlags, ChannelState, FundedChannel, InboundHTLCOutput, OutboundV1Channel, InboundV1Channel, OutboundHTLCOutput, InboundHTLCState, OutboundHTLCState, HTLCCandidate, HTLCInitiator, HTLCUpdateAwaitingACK, commit_tx_fee_sat}; + use crate::ln::channel::{AwaitingChannelReadyFlags, ChannelContext, ChannelId, ChannelPublicKeys, ChannelState, FundedChannel, InboundHTLCOutput, OutboundV1Channel, InboundV1Channel, OutboundHTLCOutput, InboundHTLCState, OutboundHTLCState, HTLCCandidate, HTLCInitiator, HTLCUpdateAwaitingACK, commit_tx_fee_sat}; use crate::ln::channel::{MAX_FUNDING_SATOSHIS_NO_WUMBO, TOTAL_BITCOIN_SUPPLY_SATOSHIS, MIN_THEIR_CHAN_RESERVE_SATOSHIS}; use crate::types::features::{ChannelFeatures, ChannelTypeFeatures, NodeFeatures}; use crate::ln::msgs; @@ -13238,6 +13352,59 @@ mod tests { } } + #[test] + fn channel_context_clone() { + let fee_estimator = TestFeeEstimator {fee_est: 253 }; + let bounded_fee_estimator = LowerBoundedFeeEstimator::new(&fee_estimator); + let seed = [42; 32]; + let network = Network::Testnet; + let keys_provider = test_utils::TestKeysInterface::new(&seed, network); + let secp_ctx = Secp256k1::new(); + let node_a_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let config = UserConfig::default(); + + let signer_provider: &TestKeysInterface = &&keys_provider; + let channel_value_satoshis = 10000000; + let user_id = 42; + let channel_keys_id = signer_provider.generate_channel_keys_id(false, user_id); + let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); + let logger = test_utils::TestLogger::new(); + + let temporary_channel_id_fn = Some(|pubkeys: &ChannelPublicKeys| { + ChannelId::temporary_v2_from_revocation_basepoint(&pubkeys.revocation_basepoint) + }); + + // Create a context + let context = ChannelContext::<&TestKeysInterface>::new_for_outbound_channel( + &bounded_fee_estimator, + &&keys_provider, + &signer_provider, + node_a_node_id, + &channelmanager::provided_init_features(&config), + channel_value_satoshis, + 100000, + user_id, + &config, + 0, + 42, + temporary_channel_id_fn, + 100000, + [42; 32], + holder_signer, + &logger, + ).unwrap().1; + + // Clone it + let holder_signer2 = signer_provider.derive_channel_signer(channel_keys_id); + let context_cloned = context.clone(holder_signer2); + + // Compare some fields + // assert_eq!(context_cloned.channel_value_satoshis, context.channel_value_satoshis); + assert_eq!(context_cloned.channel_id, context.channel_id); + assert_eq!(context_cloned.funding_tx_broadcast_safe_event_emitted, context.funding_tx_broadcast_safe_event_emitted); + assert_eq!(context_cloned.channel_keys_id, context.channel_keys_id); + } + #[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; From e5ace7e12c2997fff545f1c9649c006f4c5267ea 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 06/10] Sort out PendingSplicePre and -Post, new_spliced for ChannelContext and OutboundV2Channel --- lightning/src/ln/channel.rs | 308 ++++++++++++++++++++++++++++++++++-- 1 file changed, 293 insertions(+), 15 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 12df70cb8f4..8925885353c 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}; @@ -1554,6 +1556,8 @@ impl Channel where is_v2_established: true, #[cfg(splicing)] pending_splice_pre: None, + #[cfg(splicing)] + pending_splice_post: None, }; let res = funded_channel.commitment_signed_initial_v2(msg, best_block, signer_provider, logger) .map(|monitor| (Some(monitor), None)) @@ -1759,7 +1763,7 @@ impl FundingScope { /// 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_per_kw: u32, pub locktime: u32, @@ -1768,7 +1772,7 @@ struct PendingSplice { } #[cfg(splicing)] -impl PendingSplice { +impl PendingSplicePre { #[inline] fn add_checked(base: u64, delta: i64) -> u64 { if delta >= 0 { @@ -1784,6 +1788,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, @@ -4879,7 +4950,6 @@ impl ChannelContext where SP::Target: SignerProvider { latest_monitor_update_id: self.latest_monitor_update_id, shutdown_scriptpubkey: self.shutdown_scriptpubkey.clone(), destination_script: self.destination_script.clone(), - // holder_commitment_point: self.holder_commitment_point, cur_counterparty_commitment_transaction_number: self.cur_counterparty_commitment_transaction_number, // value_to_self_msat: self.value_to_self_msat, pending_inbound_htlcs: self.pending_inbound_htlcs.clone(), @@ -4964,6 +5034,92 @@ impl ChannelContext where SP::Target: SignerProvider { is_holder_quiescence_initiator: self.is_holder_quiescence_initiator, } } + + /// 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, + pre_splice_funding: &FundingScope, + is_outgoing: bool, + // counterparty_funding_pubkey: &PublicKey, + our_funding_contribution: i64, + their_funding_contribution: i64, + holder_signer: ::EcdsaSigner, + logger: &L, + ) -> Result<(ChannelContext, FundingScope), ChannelError> where L::Target: Logger + { + let pre_channel_value = pre_splice_funding.get_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_funding.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); + + // Reset funding + context.funding_tx_confirmed_in = None; + context.funding_tx_confirmation_height = 0; + // 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; + // Reset + context.blocked_monitor_updates = Vec::new(); + + let value_to_cp = post_channel_value.saturating_sub(value_to_self_msat); + let mut post_channel_transaction_parameters = pre_splice_funding.channel_transaction_parameters.clone(); + post_channel_transaction_parameters.channel_value_satoshis = post_channel_value; + post_channel_transaction_parameters.funding_outpoint = None; + let funding = FundingScope { + value_to_self_msat: value_to_self_msat, + + counterparty_selected_channel_reserve_satoshis: pre_splice_funding.counterparty_selected_channel_reserve_satoshis, + holder_selected_channel_reserve_satoshis: pre_splice_funding.holder_selected_channel_reserve_satoshis, + #[cfg(debug_assertions)] + holder_max_commitment_tx_output: Mutex::new((value_to_self_msat, value_to_cp)), + #[cfg(debug_assertions)] + counterparty_max_commitment_tx_output: Mutex::new((value_to_self_msat, value_to_cp)), + #[cfg(any(test, fuzzing))] + next_local_commitment_tx_fee_info_cached: Mutex::new(None), + #[cfg(any(test, fuzzing))] + next_remote_commitment_tx_fee_info_cached: Mutex::new(None), + + channel_transaction_parameters: post_channel_transaction_parameters, + funding_transaction: None, + }; + + log_debug!(logger, "Splicing channel context: value {} old {}, dir {}, value to self {}", + funding.get_value_satoshis(), pre_channel_value, + if is_outgoing { "outgoing" } else { "incoming" }, + funding.value_to_self_msat, + ); + + Ok((context, funding)) + } } // Internal utility functions for channels @@ -5139,7 +5295,10 @@ pub(super) struct FundedChannel where SP::Target: SignerProvider { is_v2_established: bool, /// 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))] @@ -8665,6 +8824,15 @@ impl FundedChannel 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. /// - `our_funding_inputs`: the inputs we contribute to the new funding transaction. /// Includes the witness weight for this input (e.g. P2WPKH_WITNESS_WEIGHT=109 for typical P2WPKH inputs). @@ -8720,7 +8888,7 @@ impl FundedChannel where funding_inputs.push((tx_in.clone(), tx16)); } - self.pending_splice_pre = Some(PendingSplice { + self.pending_splice_pre = Some(PendingSplicePre { our_funding_contribution: our_funding_contribution_satoshis, funding_feerate_per_kw, locktime, @@ -8806,21 +8974,20 @@ impl FundedChannel where /// Handle splice_ack #[cfg(splicing)] - pub fn splice_ack(&mut self, msg: &msgs::SpliceAck, logger: &L) -> Result<(), ChannelError> where L::Target: Logger { + pub fn splice_ack(&mut self, _msg: &msgs::SpliceAck, logger: &L) -> Result<(), ChannelError> where L::Target: Logger { // check if splice is pending - let pending_splice = if let Some(pending_splice) = &self.pending_splice_pre { + let _pending_splice = if let Some(pending_splice) = &self.pending_splice_pre { pending_splice } else { return Err(ChannelError::Warn(format!("Channel is not in pending splice"))); }; - let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; - let our_funding_contribution = pending_splice.our_funding_contribution; - - let pre_channel_value = self.funding.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.funding.value_to_self_msat, our_funding_contribution); + // let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; + // let our_funding_contribution = pending_splice.our_funding_contribution; + // let pre_channel_value = self.funding.get_value_satoshis(); + // 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.funding.value_to_self_msat, our_funding_contribution); // TODO(splicing): Pre-check for reserve requirement // (Note: It should also be checked later at tx_complete) @@ -9771,6 +9938,8 @@ impl OutboundV1Channel where SP::Target: SignerProvider { holder_commitment_point, #[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() @@ -10047,6 +10216,8 @@ impl InboundV1Channel where SP::Target: SignerProvider { holder_commitment_point, #[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.context.signer_pending_channel_ready; @@ -10075,6 +10246,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 channel using V2 channel establishment. pub(super) struct PendingV2Channel where SP::Target: SignerProvider { pub funding: FundingScope, @@ -10085,6 +10279,9 @@ pub(super) struct PendingV2Channel where SP::Target: SignerProvider { pub interactive_tx_constructor: Option, /// The signing session created after `tx_complete` handling pub interactive_tx_signing_session: Option, + /// Info about an in-progress, pending splice (if any), on the post-splice channel + #[cfg(splicing)] + pending_splice_post: Option, } impl PendingV2Channel where SP::Target: SignerProvider { @@ -10152,10 +10349,87 @@ impl PendingV2Channel where SP::Target: SignerProvider { }, interactive_tx_constructor: None, interactive_tx_signing_session: None, + #[cfg(splicing)] + pending_splice_post: None, }; Ok(chan) } + /// Create new channel for splicing + #[cfg(splicing)] + pub fn new_spliced( + is_outbound: bool, + pre_splice_channel: &FundedChannel, + 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.funding.get_value_satoshis(); + + // Save the current funding transaction + let pre_funding_transaction = pre_splice_channel.funding.funding_transaction.clone(); + let pre_funding_txo = pre_splice_channel.funding.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, + ); + + // Create new signer, using the new channel value. + // Note: channel_keys_id is not changed + let holder_signer = signer_provider.derive_channel_signer(pre_splice_channel.context.channel_keys_id); + + let (context, funding) = ChannelContext::new_for_splice( + &pre_splice_channel.context, + &pre_splice_channel.funding, + is_outbound, + // 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: funding_inputs, + }; + let unfunded_context = UnfundedChannelContext { + unfunded_channel_age_ticks: 0, + holder_commitment_point: HolderCommitmentPoint::new(&context.holder_signer, &context.secp_ctx), + }; + let post_chan = Self { + context, + funding, + dual_funding_context, + unfunded_context, + interactive_tx_constructor: None, + interactive_tx_signing_session: 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. @@ -10326,6 +10600,8 @@ impl PendingV2Channel where SP::Target: SignerProvider { interactive_tx_constructor, interactive_tx_signing_session: None, unfunded_context, + #[cfg(splicing)] + pending_splice_post: None, }) } @@ -11430,6 +11706,8 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, &'c Channel holder_commitment_point, #[cfg(splicing)] pending_splice_pre: None, + #[cfg(splicing)] + pending_splice_post: None, }) } } @@ -13407,9 +13685,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) } From 9248d33c4c1d0010967bfef05b1c80f3a17208fd Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Thu, 6 Mar 2025 00:42:39 +0100 Subject: [PATCH 07/10] Perform interactive tx negotiation during splicing --- lightning/src/ln/channel.rs | 431 +++++++++++++++++++++++------ lightning/src/ln/channelmanager.rs | 55 ++-- lightning/src/ln/interactivetxs.rs | 2 +- 3 files changed, 365 insertions(+), 123 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 8925885353c..78cc8a4a44a 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -32,7 +32,7 @@ use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash}; use crate::types::features::{ChannelTypeFeatures, InitFeatures}; use crate::ln::interactivetxs::{ - get_output_weight, calculate_change_output_value, HandleTxCompleteValue, HandleTxCompleteResult, InteractiveTxConstructor, + estimate_input_weight, get_output_weight, calculate_change_output_value, HandleTxCompleteValue, HandleTxCompleteResult, InteractiveTxConstructor, InteractiveTxConstructorArgs, InteractiveTxMessageSend, InteractiveTxSigningSession, InteractiveTxMessageSendResult, OutputOwned, SharedOwnedOutput, TX_COMMON_FIELDS_WEIGHT, }; @@ -1200,13 +1200,13 @@ enum ChannelPhase where SP::Target: SignerProvider { Funded(FundedChannel), /// Used during splicing, channel is funded but a new funding is being renegotiated. #[cfg(splicing)] - RefundingV2(FundedChannel), + RefundingV2(SplicingChannel), } impl Channel where SP::Target: SignerProvider, ::EcdsaSigner: ChannelSigner, -{ +{ // impl Channel pub fn context(&self) -> &ChannelContext { match &self.phase { ChannelPhase::Undefined => unreachable!(), @@ -1215,7 +1215,7 @@ impl Channel where ChannelPhase::UnfundedInboundV1(chan) => &chan.context, ChannelPhase::UnfundedV2(chan) => &chan.context, #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => &chan.context, + ChannelPhase::RefundingV2(chan) => &chan.pre_funded.context, } } @@ -1227,7 +1227,7 @@ impl Channel where ChannelPhase::UnfundedInboundV1(chan) => &mut chan.context, ChannelPhase::UnfundedV2(chan) => &mut chan.context, #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => &mut chan.context, + ChannelPhase::RefundingV2(chan) => &mut chan.pre_funded.context, } } @@ -1239,7 +1239,7 @@ impl Channel where ChannelPhase::UnfundedInboundV1(chan) => &chan.funding, ChannelPhase::UnfundedV2(chan) => &chan.funding, #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => &chan.funding, + ChannelPhase::RefundingV2(chan) => &chan.pre_funded.funding, } } @@ -1252,7 +1252,7 @@ impl Channel where ChannelPhase::UnfundedInboundV1(chan) => &mut chan.funding, ChannelPhase::UnfundedV2(chan) => &mut chan.funding, #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => &mut chan.funding, + ChannelPhase::RefundingV2(chan) => &mut chan.pre_funded.funding, } } @@ -1264,7 +1264,7 @@ impl Channel where ChannelPhase::UnfundedInboundV1(chan) => (&chan.funding, &mut chan.context), ChannelPhase::UnfundedV2(chan) => (&chan.funding, &mut chan.context), #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => (&chan.funding, &mut chan.context), + ChannelPhase::RefundingV2(chan) => (&chan.pre_funded.funding, &mut chan.pre_funded.context), } } @@ -1281,22 +1281,29 @@ impl Channel where } pub fn is_funded(&self) -> bool { - matches!(self.phase, ChannelPhase::Funded(_)) + match &self.phase { + ChannelPhase::Funded(_) => true, + #[cfg(splicing)] + ChannelPhase::RefundingV2(_) => true, + _ => false, + } } pub fn as_funded(&self) -> Option<&FundedChannel> { - if let ChannelPhase::Funded(channel) = &self.phase { - Some(channel) - } else { - None + match &self.phase { + ChannelPhase::Funded(channel) => Some(channel), + #[cfg(splicing)] + ChannelPhase::RefundingV2(channel) => Some(&channel.pre_funded), + _ => None, } } pub fn as_funded_mut(&mut self) -> Option<&mut FundedChannel> { - if let ChannelPhase::Funded(channel) = &mut self.phase { - Some(channel) - } else { - None + match &mut self.phase { + ChannelPhase::Funded(channel) => Some(channel), + #[cfg(splicing)] + ChannelPhase::RefundingV2(channel) => Some(&mut channel.pre_funded), + _ => None, } } @@ -1338,10 +1345,11 @@ impl Channel where } pub fn as_unfunded_v2_mut(&mut self) -> Option<&mut PendingV2Channel> { - if let ChannelPhase::UnfundedV2(channel) = &mut self.phase { - Some(channel) - } else { - None + match &mut self.phase { + ChannelPhase::UnfundedV2(channel) => Some(channel), + #[cfg(splicing)] + ChannelPhase::RefundingV2(channel) => Some(&mut channel.post_pending), + _ => None, } } @@ -1385,7 +1393,7 @@ impl Channel where }, ChannelPhase::UnfundedV2(_) => None, #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => Some(chan.signer_maybe_unblocked(logger)), + ChannelPhase::RefundingV2(chan) => Some(chan.pre_funded.signer_maybe_unblocked(logger)), } } @@ -1406,7 +1414,7 @@ impl Channel where ChannelPhase::UnfundedInboundV1(_) => false, ChannelPhase::UnfundedV2(_) => false, #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => chan.remove_uncommitted_htlcs_and_mark_paused(logger).is_ok(), + ChannelPhase::RefundingV2(chan) => chan.pre_funded.remove_uncommitted_htlcs_and_mark_paused(logger).is_ok(), } } @@ -1446,7 +1454,7 @@ impl Channel where }, #[cfg(splicing)] ChannelPhase::RefundingV2(chan) => - ReconnectionMsg::Reestablish(chan.get_channel_reestablish(logger)), + ReconnectionMsg::Reestablish(chan.pre_funded.get_channel_reestablish(logger)), } } @@ -1516,11 +1524,19 @@ impl Channel where where L::Target: Logger { - if let ChannelPhase::UnfundedV2(chan) = &mut self.phase { - let logger = WithChannelContext::from(logger, &chan.context, None); - chan.funding_tx_constructed(signing_session, &&logger) - } else { - Err(ChannelError::Warn("Got a tx_complete message with no interactive transaction construction expected or in-progress".to_owned())) + match &mut self.phase { + ChannelPhase::UnfundedV2(ref mut chan) => { + let logger = WithChannelContext::from(logger, &chan.context, None); + chan.funding_tx_constructed(signing_session, &&logger) + } + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => { + let logger = WithChannelContext::from(logger, &chan.post_pending.context, None); + chan.post_pending.funding_tx_constructed(signing_session, &&logger) + } + _ => { + Err(ChannelError::Warn("Got a tx_complete message with no interactive transaction construction expected or in-progress".to_owned())) + } } } @@ -1536,7 +1552,7 @@ impl Channel where L::Target: Logger { let phase = core::mem::replace(&mut self.phase, ChannelPhase::Undefined); - match phase { + let res = match phase { ChannelPhase::UnfundedV2(chan) => { let holder_commitment_point = match chan.unfunded_context.holder_commitment_point { Some(point) => point, @@ -1575,11 +1591,143 @@ impl Channel where self.phase = ChannelPhase::Funded(funded_channel); res }, + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => { + let holder_commitment_point = match chan.post_pending.unfunded_context.holder_commitment_point { + Some(point) => point, + None => { + let channel_id = chan.post_pending.context.channel_id(); + // TODO(dual_funding): Add async signing support. + return Err( ChannelError::close( + format!("Expected to have holder commitment points available upon finishing interactive splice tx construction for channel {}", + channel_id))); + } + }; + let mut funded_channel = FundedChannel { + funding: chan.post_pending.funding, + context: chan.post_pending.context, + interactive_tx_signing_session: chan.post_pending.interactive_tx_signing_session, + holder_commitment_point, + is_v2_established: true, + #[cfg(splicing)] + pending_splice_pre: None, + #[cfg(splicing)] + pending_splice_post: chan.post_pending.pending_splice_post, + }; + let res = funded_channel.commitment_signed_initial_v2(msg, best_block, signer_provider, logger) + .map(|monitor| (Some(monitor), None)) + // TODO: Change to `inspect_err` when MSRV is high enough. + .map_err(|err| { + // We always expect a `ChannelError` close. + debug_assert!(matches!(err, ChannelError::Close(_))); + err + }); + self.phase = ChannelPhase::Funded(funded_channel); + res + } _ => { self.phase = phase; debug_assert!(!matches!(self.phase, ChannelPhase::Undefined)); Err(ChannelError::close("Got a commitment_signed message for an unfunded V1 channel!".into())) } + }; + debug_assert!(!matches!(self.phase, ChannelPhase::Undefined)); + res + } + + /// Transition the channel from Funded to SplicingChannel. + /// Done in one go, as the existing ('pre') channel is put in the new channel (alongside a new one). + #[cfg(splicing)] + fn phase_to_splice(&mut self, post_chan: PendingV2Channel) -> Result<(), ChannelError> + { + let phase = core::mem::replace(&mut self.phase, ChannelPhase::Undefined); + let result = if let ChannelPhase::Funded(prev_chan) = phase { + self.phase = ChannelPhase::RefundingV2(SplicingChannel::new(prev_chan, post_chan)); + Ok(()) + } else { + // revert phase + self.phase = phase; + Err(ChannelError::Warn("Got a splice_init message with no funded channel".to_owned())) + }; + debug_assert!(!matches!(self.phase, ChannelPhase::Undefined)); + result + } + + #[cfg(splicing)] + pub fn splice_init( + &mut self, msg: &msgs::SpliceInit, our_funding_contribution: i64, + signer_provider: &SP, entropy_source: &ES, our_node_id: &PublicKey, logger: &L + ) -> Result + where + ES::Target: EntropySource, + L::Target: Logger + { + // Explicit check for Funded, not as_funded; RefundingV2 not allowed + if let ChannelPhase::Funded(prev_chan) = &mut self.phase { + let _res = prev_chan.splice_init_checks(msg)?; + + let post_chan = PendingV2Channel::new_spliced( + false, + prev_chan, + signer_provider, + // &msg.funding_pubkey, + our_funding_contribution, + msg.funding_contribution_satoshis, + Vec::new(), + LockTime::from_consensus(msg.locktime), + msg.funding_feerate_per_kw, + logger, + )?; + + let _res = self.phase_to_splice(post_chan)?; + + if let ChannelPhase::RefundingV2(chan) = &mut self.phase { + let splice_ack_msg = chan.post_pending.splice_init(msg, signer_provider, entropy_source, our_node_id, logger)?; + Ok(splice_ack_msg) + } else { + unreachable!("Must have been transitioned to RefundingV2 in above call if successful"); + } + } else { + Err(ChannelError::Warn("Channel is not funded, cannot be spliced".to_owned())) + } + } + + #[cfg(splicing)] + pub fn splice_ack( + &mut self, msg: &msgs::SpliceAck, + signer_provider: &SP, entropy_source: &ES, our_node_id: &PublicKey, logger: &L + ) -> Result, ChannelError> + where + ES::Target: EntropySource, + L::Target: Logger + { + // Explicit check for Funded, not as_funded; RefundingV2 not allowed + if let ChannelPhase::Funded(prev_chan) = &mut self.phase { + let pending_splice = prev_chan.splice_ack_checks()?; + + let post_chan = PendingV2Channel::new_spliced( + true, + prev_chan, + signer_provider, + // &msg.funding_pubkey, + pending_splice.our_funding_contribution, + msg.funding_contribution_satoshis, + pending_splice.our_funding_inputs.clone(), + LockTime::from_consensus(pending_splice.locktime), + pending_splice.funding_feerate_per_kw, + logger, + )?; + + let _res = self.phase_to_splice(post_chan)?; + + if let ChannelPhase::RefundingV2(chan) = &mut self.phase { + let tx_msg_opt = chan.post_pending.splice_ack(msg, pending_splice.our_funding_contribution, signer_provider, entropy_source, our_node_id, logger)?; + Ok(tx_msg_opt) + } else { + unreachable!("Must have been transitioned to RefundingV2 in above call if successful"); + } + } else { + Err(ChannelError::Warn("Channel is not funded, cannot be spliced".to_owned())) } } } @@ -1632,6 +1780,25 @@ where } } +/// Struct holding together various state dureing splicing negotiation +#[cfg(splicing)] +pub(super) struct SplicingChannel where SP::Target: SignerProvider { + pub pre_funded: FundedChannel, + pub post_pending: PendingV2Channel, + pub post_funded: Option>, +} + +#[cfg(splicing)] +impl SplicingChannel where SP::Target: SignerProvider { + pub(super) fn new(pre_funded: FundedChannel, post_pending: PendingV2Channel) -> Self { + Self { + pre_funded, + post_pending, + post_funded: None, + } + } +} + /// Contains all state common to unfunded inbound/outbound channels. pub(super) struct UnfundedChannelContext { /// A counter tracking how many ticks have elapsed since this unfunded channel was @@ -1763,7 +1930,7 @@ impl FundingScope { /// Info about a pending splice, used in the pre-splice channel #[cfg(splicing)] #[derive(Clone)] -struct PendingSplicePre { +pub(super) struct PendingSplicePre { pub our_funding_contribution: i64, pub funding_feerate_per_kw: u32, pub locktime: u32, @@ -5120,6 +5287,18 @@ impl ChannelContext where SP::Target: SignerProvider { Ok((context, funding)) } + + /// Splice process starting; update state, log, etc. + #[cfg(splicing)] + pub(crate) fn splice_start(&mut self, is_outgoing: bool, channel_value_satoshis: u64, logger: &L) where L::Target: Logger { + // Set state, by this point splice_init/splice_ack handshake is complete + // TODO(splicing) + // self.channel_state = ChannelState::NegotiatingFunding( + // NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT + // ); + log_info!(logger, "Splicing process started, old channel value {}, outgoing {}, channel_id {}", + channel_value_satoshis, is_outgoing, self.channel_id); + } } // Internal utility functions for channels @@ -5177,6 +5356,54 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis)) } +pub(super) fn maybe_add_funding_change_output(signer_provider: &SP, is_initiator: bool, + our_funding_satoshis: u64, funding_inputs_prev_outputs: &Vec, + funding_outputs: &mut Vec, funding_feerate_sat_per_1000_weight: u32, + total_input_satoshis: u64, holder_dust_limit_satoshis: u64, channel_keys_id: [u8; 32], +) -> Result, ChannelError> where + SP::Target: SignerProvider, +{ + let our_funding_inputs_weight = funding_inputs_prev_outputs.iter().fold(0u64, |weight, prev_output| { + weight.saturating_add(estimate_input_weight(prev_output).to_wu()) + }); + let our_funding_outputs_weight = funding_outputs.iter().fold(0u64, |weight, out| { + weight.saturating_add(get_output_weight(&out.tx_out().script_pubkey).to_wu()) + }); + let our_contributed_weight = our_funding_outputs_weight.saturating_add(our_funding_inputs_weight); + let mut fees_sats = fee_for_weight(funding_feerate_sat_per_1000_weight, our_contributed_weight); + + // If we are the initiator, we must pay for weight of all common fields in the funding transaction. + if is_initiator { + let common_fees = fee_for_weight(funding_feerate_sat_per_1000_weight, TX_COMMON_FIELDS_WEIGHT); + fees_sats = fees_sats.saturating_add(common_fees); + } + + let remaining_value = total_input_satoshis + .saturating_sub(our_funding_satoshis) + .saturating_sub(fees_sats); + + if remaining_value < holder_dust_limit_satoshis { + Ok(None) + } else { + let change_script = signer_provider.get_destination_script(channel_keys_id).map_err( + |_| ChannelError::Close(( + "Failed to get change script as new destination script".to_owned(), + ClosureReason::ProcessingError { err: "Failed to get change script as new destination script".to_owned() } + )) + )?; + let mut change_output = TxOut { + value: Amount::from_sat(remaining_value), + script_pubkey: change_script, + }; + let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu(); + + let change_output_fee = fee_for_weight(funding_feerate_sat_per_1000_weight, change_output_weight); + change_output.value = Amount::from_sat(remaining_value.saturating_sub(change_output_fee)); + funding_outputs.push(OutputOwned::Single(change_output.clone())); + Ok(Some(change_output)) + } +} + /// Estimate our part of the fee of the new funding transaction. /// input_count: Number of contributed inputs. /// witness_weight: The witness weight for contributed inputs. @@ -5378,7 +5605,7 @@ impl FailHTLCMessageName for msgs::UpdateFailMalformedHTLC { impl FundedChannel where SP::Target: SignerProvider, ::EcdsaSigner: EcdsaChannelSigner -{ +{ // impl FundedChannel fn check_remote_fee( channel_type: &ChannelTypeFeatures, fee_estimator: &LowerBoundedFeeEstimator, feerate_per_kw: u32, cur_feerate_per_kw: Option, logger: &L @@ -8917,9 +9144,9 @@ impl FundedChannel where } } - /// Handle splice_init + /// Checks during handling splice_init #[cfg(splicing)] - pub fn splice_init(&mut self, msg: &msgs::SpliceInit, logger: &L) -> Result where L::Target: Logger { + pub fn splice_init_checks(&mut self, msg: &msgs::SpliceInit) -> Result<(), ChannelError> { let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; // TODO(splicing): Currently not possible to contribute on the splicing-acceptor side let our_funding_contribution_satoshis = 0i64; @@ -8952,66 +9179,18 @@ impl FundedChannel where // Note on channel reserve requirement pre-check: as the splice acceptor does not contribute, // it can't go below reserve, therefore no pre-check is done here. // TODO(splicing): Once splice acceptor can contribute, add reserve pre-check, similar to the one in `splice_ack`. - - // TODO(splicing): Store msg.funding_pubkey - // Apply start of splice change in the state - self.splice_start(false, logger); - - // TODO(splicing): The exisiting pubkey is reused, but a new one should be generated. See #3542. - // Note that channel_keys_id is supposed NOT to change - let splice_ack_msg = msgs::SpliceAck { - channel_id: self.context.channel_id, - funding_contribution_satoshis: our_funding_contribution_satoshis, - funding_pubkey: self.funding.get_holder_pubkeys().funding_pubkey, - require_confirmed_inputs: None, - }; - // TODO(splicing): start interactive funding negotiation - // let _msg = self.begin_interactive_funding_tx_construction(signer_provider, entropy_source, holder_node_id) - // .map_err(|err| ChannelError::Warn(format!("Failed to start interactive transaction construction, {:?}", err)))?; - - Ok(splice_ack_msg) + Ok(()) } - /// Handle splice_ack + /// Checks during handling splice_ack #[cfg(splicing)] - pub fn splice_ack(&mut self, _msg: &msgs::SpliceAck, logger: &L) -> Result<(), ChannelError> where L::Target: Logger { + pub fn splice_ack_checks(&mut self) -> Result { // check if splice is pending - let _pending_splice = if let Some(pending_splice) = &self.pending_splice_pre { - pending_splice + if let Some(pending_splice) = &self.pending_splice_pre { + Ok(pending_splice.clone()) } else { - return Err(ChannelError::Warn(format!("Channel is not in pending splice"))); - }; - - - // let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; - // let our_funding_contribution = pending_splice.our_funding_contribution; - // let pre_channel_value = self.funding.get_value_satoshis(); - // 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.funding.value_to_self_msat, our_funding_contribution); - - // TODO(splicing): Pre-check for reserve requirement - // (Note: It should also be checked later at tx_complete) - - // Apply start of splice change in the state - self.splice_start(true, logger); - - // TODO(splicing): start interactive funding negotiation - // let tx_msg_opt = self.begin_interactive_funding_tx_construction(signer_provider, entropy_source, holder_node_id) - // .map_err(|err| ChannelError::Warn(format!("V2 channel rejected due to sender error, {:?}", err)))?; - // Ok(tx_msg_opt) - Ok(()) - } - - /// 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 { - // Set state, by this point splice_init/splice_ack handshake is complete - // TODO(splicing) - // self.channel_state = ChannelState::NegotiatingFunding( - // NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT - // ); - log_info!(logger, "Splicing process started, old channel value {}, outgoing {}, channel_id {}", - self.funding.get_value_satoshis(), is_outgoing, self.context.channel_id); + Err(ChannelError::Warn(format!("Channel is not in pending splice"))) + } } // Send stuff to our remote peers: @@ -10497,6 +10676,41 @@ impl PendingV2Channel where SP::Target: SignerProvider { } } + /// Handle splice_ack + #[cfg(splicing)] + pub fn splice_ack( + &mut self, msg: &msgs::SpliceAck, our_funding_contribution: i64, + signer_provider: &SP, entropy_source: &ES, holder_node_id: &PublicKey, logger: &L, + ) -> Result, ChannelError> + where ES::Target: EntropySource, L::Target: Logger + { + let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; + + // check if splice is pending + let pending_splice = if let Some(pending_splice) = &self.pending_splice_post { + pending_splice + } else { + return Err(ChannelError::Warn(format!("Channel is not in pending splice"))); + }; + + let pre_channel_value = self.funding.get_value_satoshis(); + 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.funding.value_to_self_msat, our_funding_contribution); + // TODO(splicing): Pre-check for reserve requirement + // (Note: It should also be checked later at tx_complete) + + // We need the current funding tx as an extra input + let prev_funding_input = pending_splice.get_input_of_previous_funding()?; + + // Apply start of splice change in the state + self.context.splice_start(true, pre_channel_value, logger); + + // Start interactive funding negotiation, with the previous funding transaction as an extra shared input + let tx_msg_opt = self.begin_interactive_funding_tx_construction(signer_provider, entropy_source, holder_node_id.clone(), Some(prev_funding_input)) + .map_err(|err| ChannelError::Warn(format!("V2 channel rejected due to sender error, {:?}", err)))?; + Ok(tx_msg_opt) + } + /// Creates a new dual-funded channel from a remote side's request for one. /// Assumes chain_hash has already been checked and corresponds with what we expect! /// TODO(dual_funding): Allow contributions, pass intended amount and inputs @@ -10678,6 +10892,45 @@ impl PendingV2Channel where SP::Target: SignerProvider { pub fn get_accept_channel_v2_message(&self) -> msgs::AcceptChannelV2 { self.generate_accept_channel_v2_message() } + + /// Handle splice_init + /// See also [`splice_init_checks`] + #[cfg(splicing)] + pub fn splice_init( + &mut self, _msg: &msgs::SpliceInit, + signer_provider: &SP, entropy_source: &ES, holder_node_id: &PublicKey, logger: &L, + ) -> Result + where ES::Target: EntropySource, L::Target: Logger + { + // TODO(splicing): Currently not possible to contribute on the splicing-acceptor side + let our_funding_contribution_satoshis = 0i64; + + // TODO(splicing): Store msg.funding_pubkey + + // Apply start of splice change in the state + self.context.splice_start(false, self.funding.get_value_satoshis(), logger); + + let splice_ack_msg = self.get_splice_ack(our_funding_contribution_satoshis); + + // Start interactive funding negotiation. No extra input, as we are not the splice initiator + let _msg = self.begin_interactive_funding_tx_construction(signer_provider, entropy_source, holder_node_id.clone(), None) + .map_err(|err| ChannelError::Warn(format!("Failed to start interactive transaction construction, {:?}", err)))?; + + Ok(splice_ack_msg) + } + + /// Get the splice_ack message that can be sent in response to splice initiation. + #[cfg(splicing)] + pub fn get_splice_ack(&self, our_funding_contribution_satoshis: i64) -> msgs::SpliceAck { + // Reuse the existing funding pubkey, in spite of the channel value changing + let funding_pubkey = self.funding.get_holder_pubkeys().funding_pubkey; + msgs::SpliceAck { + channel_id: self.context.channel_id, + funding_contribution_satoshis: our_funding_contribution_satoshis, + funding_pubkey, + require_confirmed_inputs: None, + } + } } // Unfunded channel utilities diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index a8692f1b2ae..b09cce358b5 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -9522,6 +9522,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; + // TODO(splicing): Currently not possible to contribute on the splicing-acceptor side + let our_funding_contribution = 0i64; + // 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!( @@ -9529,23 +9532,18 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ counterparty_node_id, msg.channel_id, ), msg.channel_id)), hash_map::Entry::Occupied(mut chan_entry) => { - if let Some(chan) = chan_entry.get_mut().as_funded_mut() { - let splice_ack_msg = try_channel_entry!(self, peer_state, chan.splice_init(msg, &self.logger), chan_entry); - peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceAck { - node_id: *counterparty_node_id, - msg: splice_ack_msg, - }); - } else { - return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel is not funded, cannot be spliced".to_owned(), msg.channel_id)); - } + // Handle inside channel (checks, phase change, state change) + let splice_ack_msg = chan_entry.get_mut().splice_init(msg, our_funding_contribution, &self.signer_provider, + &self.entropy_source, &self.get_our_node_id(), &self.logger) + .map_err(|err| MsgHandleErrInternal::from_chan_no_close(err, msg.channel_id))?; + + peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceAck { + node_id: *counterparty_node_id, + msg: splice_ack_msg, + }); }, }; - // TODO(splicing): - // Change channel, change phase (remove and add) - // Create new post-splice channel - // etc. - Ok(()) } @@ -9561,28 +9559,19 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; - // Look for the channel + // Look for 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::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() { - try_channel_entry!(self, peer_state, chan.splice_ack(msg, &self.logger), chan_entry); - } else { - return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel is not funded, cannot splice".to_owned(), msg.channel_id)); + // Handle inside channel (checks, phase change, state change) + let tx_msg_opt = chan_entry.get_mut().splice_ack(msg, &self.signer_provider, &self.entropy_source, &self.get_our_node_id(), &self.logger) + .map_err(|err| MsgHandleErrInternal::from_chan_no_close(err, msg.channel_id))?; + if let Some(tx_msg) = tx_msg_opt { + peer_state.pending_msg_events.push(tx_msg.into_msg_send_event(counterparty_node_id.clone())); } - }, - }; - - // TODO(splicing): - // Change channel, change phase (remove and add) - // Create new post-splice channel - // Start splice funding transaction negotiation - // etc. - - Err(MsgHandleErrInternal::send_err_msg_no_close("TODO(splicing): Splicing is not implemented (splice_ack)".to_owned(), msg.channel_id)) + Ok(()) + } + } } /// Process pending events from the [`chain::Watch`], returning whether any events were processed. diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 392a95197a5..035189b9178 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -1187,7 +1187,7 @@ pub(super) enum OutputOwned { } impl OutputOwned { - pub fn tx_out(&self) -> &TxOut { + pub(super) fn tx_out(&self) -> &TxOut { match self { OutputOwned::Single(tx_out) | OutputOwned::SharedControlFullyOwned(tx_out) => tx_out, OutputOwned::Shared(output) => &output.tx_out, From 3cca201f22c7d6fd7aa714c9f96a724e81dbe184 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Thu, 20 Mar 2025 00:27:15 +0100 Subject: [PATCH 08/10] Implement logic for handling last tx_complete, signature for shared input --- lightning/src/events/mod.rs | 61 +++++++- lightning/src/ln/channel.rs | 226 ++++++++++++++++++----------- lightning/src/ln/channelmanager.rs | 14 +- lightning/src/ln/interactivetxs.rs | 8 +- lightning/src/ln/splicing_tests.rs | 173 +++++++++++++++++++++- 5 files changed, 388 insertions(+), 94 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index a0f26bfbac0..b2cd22abe56 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1463,6 +1463,58 @@ pub enum Event { /// /// [`ChannelHandshakeConfig::negotiate_anchors_zero_fee_htlc_tx`]: crate::util::config::ChannelHandshakeConfig::negotiate_anchors_zero_fee_htlc_tx BumpTransaction(BumpTransactionEvent), + + /// Indicates that a transaction constructed via interactive transaction construction for a + /// dual-funded (V2) channel is ready to be signed by the client. This event will only be triggered + /// if at least one input was contributed by the holder. + /// + /// The transaction contains all inputs provided by both parties when the channel was + /// created/accepted along with the channel's funding output and a change output if applicable. + /// + /// No part of the transaction should be changed before signing as the content of the transaction + /// has already been negotiated with the counterparty. + /// + /// Each signature MUST use the SIGHASH_ALL flag to avoid invalidation of initial commitment and + /// hence possible loss of funds. + /// + /// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially) signed + /// funding transaction. + /// + /// Generated in [`ChannelManager`] message handling. + /// + /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager + /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed + FundingTransactionReadyForSigning { + /// The channel_id of the V2 channel which you'll need to pass back into + /// [`ChannelManager::funding_transaction_signed`]. + /// + /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed + channel_id: ChannelId, + /// The counterparty's node_id, which you'll need to pass back into + /// [`ChannelManager::funding_transaction_signed`]. + /// + /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed + counterparty_node_id: PublicKey, + /* + /// The `user_channel_id` value passed in to [`ChannelManager::create_dual_funded_channel`] for outbound + /// channels, or to [`ChannelManager::accept_inbound_channel`] or [`ChannelManager::accept_inbound_channel_with_contribution`] + /// for inbound channels if [`UserConfig::manually_accept_inbound_channels`] config flag is set to true. + /// Otherwise `user_channel_id` will be randomized for an inbound channel. + /// This may be zero for objects serialized with LDK versions prior to 0.0.113. + /// + /// [`ChannelManager::create_dual_funded_channel`]: crate::ln::channelmanager::ChannelManager::create_dual_funded_channel + /// [`ChannelManager::accept_inbound_channel`]: crate::ln::channelmanager::ChannelManager::accept_inbound_channel + /// [`ChannelManager::accept_inbound_channel_with_contribution`]: crate::ln::channelmanager::ChannelManager::accept_inbound_channel_with_contribution + /// [`UserConfig::manually_accept_inbound_channels`]: crate::util::config::UserConfig::manually_accept_inbound_channels + user_channel_id: u128, + */ + /// The unsigned transaction to be signed and passed back to + /// [`ChannelManager::funding_transaction_signed`]. + /// + /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed + unsigned_transaction: Transaction, + }, + /// We received an onion message that is intended to be forwarded to a peer /// that is currently offline. This event will only be generated if the /// `OnionMessenger` was initialized with @@ -1768,7 +1820,7 @@ impl Writeable for Event { BumpTransactionEvent::HTLCResolution { .. } => {} } write_tlv_fields!(writer, {}); // Write a length field for forwards compat - } + }, &Event::ChannelReady { ref channel_id, ref user_channel_id, ref counterparty_node_id, ref channel_type } => { 29u8.write(writer)?; write_tlv_fields!(writer, { @@ -1828,6 +1880,13 @@ impl Writeable for Event { (8, former_temporary_channel_id, required), }); }, + &Event::FundingTransactionReadyForSigning { .. } => { + 45u8.write(writer)?; + // We never write out FundingTransactionReadyForSigning events as, upon disconnection, peers + // drop any V2-established channels which have not yet exchanged the initial `commitment_signed`. + // We only exhange the initial `commitment_signed` after the client calls + // `ChannelManager::funding_transaction_signed` and ALWAYS before we send a `tx_signatures` + }, // Note that, going forward, all new events must only write data inside of // `write_tlv_fields`. Versions 0.0.101+ will ignore odd-numbered events that write // data via `write_tlv_fields`. diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 78cc8a4a44a..ceea56e691b 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -32,7 +32,7 @@ use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash}; use crate::types::features::{ChannelTypeFeatures, InitFeatures}; use crate::ln::interactivetxs::{ - estimate_input_weight, get_output_weight, calculate_change_output_value, HandleTxCompleteValue, HandleTxCompleteResult, InteractiveTxConstructor, + get_output_weight, calculate_change_output_value, HandleTxCompleteValue, HandleTxCompleteResult, InteractiveTxConstructor, InteractiveTxConstructorArgs, InteractiveTxMessageSend, InteractiveTxSigningSession, InteractiveTxMessageSendResult, OutputOwned, SharedOwnedOutput, TX_COMMON_FIELDS_WEIGHT, }; @@ -1519,7 +1519,7 @@ impl Channel where } pub fn funding_tx_constructed( - &mut self, signing_session: InteractiveTxSigningSession, logger: &L + &mut self, counterparty_node_id: &PublicKey, signing_session: InteractiveTxSigningSession, logger: &L ) -> Result<(msgs::CommitmentSigned, Option), ChannelError> where L::Target: Logger @@ -1527,12 +1527,12 @@ impl Channel where match &mut self.phase { ChannelPhase::UnfundedV2(ref mut chan) => { let logger = WithChannelContext::from(logger, &chan.context, None); - chan.funding_tx_constructed(signing_session, &&logger) + chan.funding_tx_constructed(counterparty_node_id, signing_session, &&logger) } #[cfg(splicing)] ChannelPhase::RefundingV2(chan) => { let logger = WithChannelContext::from(logger, &chan.post_pending.context, None); - chan.post_pending.funding_tx_constructed(signing_session, &&logger) + chan.post_pending.funding_tx_constructed(counterparty_node_id, signing_session, &&logger) } _ => { Err(ChannelError::Warn("Got a tx_complete message with no interactive transaction construction expected or in-progress".to_owned())) @@ -1785,7 +1785,7 @@ where pub(super) struct SplicingChannel where SP::Target: SignerProvider { pub pre_funded: FundedChannel, pub post_pending: PendingV2Channel, - pub post_funded: Option>, + // pub post_funded: Option>, } #[cfg(splicing)] @@ -1794,7 +1794,7 @@ impl SplicingChannel where SP::Target: SignerProvider { Self { pre_funded, post_pending, - post_funded: None, + // post_funded: None, } } } @@ -1960,7 +1960,7 @@ impl PendingSplicePre { #[derive(Clone)] struct PendingSplicePost { pre_channel_value: u64, - post_channel_value: u64, + // post_channel_value: u64, /// Save here the previous funding transaction pub pre_funding_transaction: Option, @@ -1971,19 +1971,20 @@ struct PendingSplicePost { #[cfg(splicing)] impl PendingSplicePost { pub(crate) fn new( - pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64, + 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); + // 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_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 } + // 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> { @@ -2007,11 +2008,11 @@ impl PendingSplicePost { } /// Within the given transaction, find the input that corresponds to the previous funding transaction - fn find_input_of_previous_funding(&self, tx: &Transaction) -> Result { + 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); + return Ok(idx); } } // Not found @@ -2682,13 +2683,14 @@ impl PendingV2Channel where SP::Target: SignerProvider { } pub fn funding_tx_constructed( - &mut self, mut signing_session: InteractiveTxSigningSession, logger: &L + &mut self, counterparty_node_id: &PublicKey, mut signing_session: InteractiveTxSigningSession, logger: &L ) -> Result<(msgs::CommitmentSigned, Option), ChannelError> where L::Target: Logger { let our_funding_satoshis = self.dual_funding_context.our_funding_satoshis; let transaction_number = self.unfunded_context.transaction_number(); + let is_splice_pending = self.is_splice_pending(); let mut output_index = None; let expected_spk = self.funding.get_funding_redeemscript().to_p2wsh(); @@ -2715,8 +2717,11 @@ impl PendingV2Channel where SP::Target: SignerProvider { }; self.funding.channel_transaction_parameters.funding_outpoint = Some(outpoint); - self.context.assert_no_commitment_advancement(transaction_number, "initial commitment_signed"); - let commitment_signed = self.context.get_initial_commitment_signed(&self.funding, logger); + // Assert for no commitment, unless splicing + if !is_splice_pending { + self.context.assert_no_commitment_advancement(transaction_number, "initial commitment_signed"); + } + let commitment_signed = self.context.get_initial_commitment_signed(&self.funding, is_splice_pending, logger); let commitment_signed = match commitment_signed { Ok(commitment_signed) => { self.funding.funding_transaction = Some(signing_session.unsigned_tx.build_unsigned_tx()); @@ -2728,6 +2733,20 @@ impl PendingV2Channel where SP::Target: SignerProvider { }, }; + #[cfg(not(splicing))] + let partly_signed_transaction =signing_session.unsigned_tx.clone().build_unsigned_tx(); + #[cfg(splicing)] + let mut partly_signed_transaction =signing_session.unsigned_tx.clone().build_unsigned_tx(); + #[cfg(splicing)] + if is_splice_pending { + // Add signature for prev funding input + // Note: here the transaction is used for signing, input&output order matters + let (partly_signed_tx, holder_signature) = self.prev_funding_tx_sign(&partly_signed_transaction, None, logger)?; + signing_session.shared_signature = Some(holder_signature); + partly_signed_transaction = partly_signed_tx; + } + + /* TODO remove let funding_ready_for_sig_event = if signing_session.local_inputs_count() == 0 { debug_assert_eq!(our_funding_satoshis, 0); if signing_session.provide_holder_witnesses(self.context.channel_id, Vec::new()).is_err() { @@ -2765,6 +2784,20 @@ impl PendingV2Channel where SP::Target: SignerProvider { ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) } ))); }; + */ + + let mut funding_ready_for_sig_event = None; + if our_funding_satoshis == 0 { + let _res = signing_session.provide_holder_witnesses(self.context.channel_id, Vec::new(), signing_session.shared_signature.clone()); + } else { + funding_ready_for_sig_event = Some(Event::FundingTransactionReadyForSigning { + channel_id: self.context.channel_id, + counterparty_node_id: *counterparty_node_id, + // user_channel_id: self.context.user_id, + // Note: here the transaction is used for signing, input&output order matters + unsigned_transaction: partly_signed_transaction, + }); + } self.context.channel_state = ChannelState::FundingNegotiated; @@ -2774,6 +2807,76 @@ impl PendingV2Channel where SP::Target: SignerProvider { Ok((commitment_signed, funding_ready_for_sig_event)) } + + + /// Prepare the witness on the current funding tx input (used in the splicing case), + /// containing our holder signature, and optionally the counterparty signature, or its empty placholder. + #[cfg(splicing)] + fn prev_funding_tx_sign( + &self, transaction: &Transaction, counterparty_sig: Option, logger: &L + ) -> Result<(Transaction, Signature), ChannelError> where L::Target: Logger { + let (prev_funding_input_index, pre_channel_value) = if let Some(pending_splice) = &self.pending_splice_post { + ( + pending_splice.find_input_of_previous_funding(&transaction)?, + pending_splice.pre_channel_value() + ) + } else { + return Err(ChannelError::Warn(format!("Cannot sign splice transaction, channel is not in active splice, channel_id {}", self.context.channel_id))) + }; + debug_assert!(prev_funding_input_index < transaction.input.len()); + + // #SPLICE-SIG + // the redeem script + let sig_order_ours_first = self.funding.get_holder_pubkeys().funding_pubkey.serialize() < self.funding.counterparty_funding_pubkey().serialize(); + log_info!(logger, "Pubkeys used for redeem script: {} {} {}", &self.funding.get_holder_pubkeys().funding_pubkey, &self.funding.counterparty_funding_pubkey(), sig_order_ours_first); + + let redeem_script = self.funding.get_funding_redeemscript(); + let holder_signature = self.prev_funding_tx_create_holder_sig(&transaction, prev_funding_input_index, pre_channel_value)?; // , &redeem_script)?; + let mut holder_sig = holder_signature.serialize_der().to_vec(); + holder_sig.push(EcdsaSighashType::All as u8); + // counterparty signature + let cp_sig = match counterparty_sig { + Some(s) => { + let mut sb = s.serialize_der().to_vec(); + sb.push(EcdsaSighashType::All as u8); + sb + }, + None => Vec::new(), // placeholder + }; + // prepare witness stack + let mut witness = Witness::new(); + witness.push(Vec::new()); + if sig_order_ours_first { + witness.push(holder_sig); + witness.push(cp_sig); + } else { + witness.push(cp_sig); + witness.push(holder_sig); + } + witness.push(redeem_script.clone().into_bytes()); + + let mut tx = transaction.clone(); + tx.input[prev_funding_input_index as usize].witness = witness; + Ok((tx, holder_signature)) + } + + /// Create signature for the current funding tx input, used in the splicing case. + #[cfg(splicing)] + fn prev_funding_tx_create_holder_sig(&self, transaction: &Transaction, input_index: usize, input_value: u64/*, _redeem_script: &ScriptBuf*/) -> Result { + // #SPLICE-SIG + match &self.context.holder_signer { + ChannelSignerType::Ecdsa(ecdsa) => { + ecdsa.sign_splicing_funding_input(&self.funding.channel_transaction_parameters, transaction, input_index, input_value, /*&redeem_script, */&self.context.secp_ctx) + .map_err(|_e| { + let msg = "Failed to sign the previous funding input in the new splicing funding tx"; + ChannelError::Close((msg.to_owned(), ClosureReason::ProcessingError { err: msg.to_owned() })) + }) + }, + // TODO (taproot|arik) + #[cfg(taproot)] + _ => todo!() + } + } } impl ChannelContext where SP::Target: SignerProvider { @@ -5047,21 +5150,24 @@ impl ChannelContext where SP::Target: SignerProvider { } fn get_initial_commitment_signed( - &mut self, funding: &FundingScope, logger: &L + &mut self, funding: &FundingScope, is_splice: bool, logger: &L ) -> Result where SP::Target: SignerProvider, L::Target: Logger { - if !matches!( - self.channel_state, ChannelState::NegotiatingFunding(flags) - if flags == (NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT) - ) { - debug_assert!(false); - return Err(ChannelError::Close(("Tried to get an initial commitment_signed messsage at a time other than \ - immediately after initial handshake completion (or tried to get funding_created twice)".to_string(), - ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true) } - ))); + // TODO reset state?, put back check always + if !is_splice { + if !matches!( + self.channel_state, ChannelState::NegotiatingFunding(flags) + if flags == (NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT) + ) { + debug_assert!(false); + return Err(ChannelError::Close(("Tried to get an initial commitment_signed messsage at a time other than \ + immediately after initial handshake completion (or tried to get funding_created twice)".to_string(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true) } + ))); + } } let signature = match self.get_initial_counterparty_commitment_signature(funding, logger) { @@ -5356,54 +5462,6 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis)) } -pub(super) fn maybe_add_funding_change_output(signer_provider: &SP, is_initiator: bool, - our_funding_satoshis: u64, funding_inputs_prev_outputs: &Vec, - funding_outputs: &mut Vec, funding_feerate_sat_per_1000_weight: u32, - total_input_satoshis: u64, holder_dust_limit_satoshis: u64, channel_keys_id: [u8; 32], -) -> Result, ChannelError> where - SP::Target: SignerProvider, -{ - let our_funding_inputs_weight = funding_inputs_prev_outputs.iter().fold(0u64, |weight, prev_output| { - weight.saturating_add(estimate_input_weight(prev_output).to_wu()) - }); - let our_funding_outputs_weight = funding_outputs.iter().fold(0u64, |weight, out| { - weight.saturating_add(get_output_weight(&out.tx_out().script_pubkey).to_wu()) - }); - let our_contributed_weight = our_funding_outputs_weight.saturating_add(our_funding_inputs_weight); - let mut fees_sats = fee_for_weight(funding_feerate_sat_per_1000_weight, our_contributed_weight); - - // If we are the initiator, we must pay for weight of all common fields in the funding transaction. - if is_initiator { - let common_fees = fee_for_weight(funding_feerate_sat_per_1000_weight, TX_COMMON_FIELDS_WEIGHT); - fees_sats = fees_sats.saturating_add(common_fees); - } - - let remaining_value = total_input_satoshis - .saturating_sub(our_funding_satoshis) - .saturating_sub(fees_sats); - - if remaining_value < holder_dust_limit_satoshis { - Ok(None) - } else { - let change_script = signer_provider.get_destination_script(channel_keys_id).map_err( - |_| ChannelError::Close(( - "Failed to get change script as new destination script".to_owned(), - ClosureReason::ProcessingError { err: "Failed to get change script as new destination script".to_owned() } - )) - )?; - let mut change_output = TxOut { - value: Amount::from_sat(remaining_value), - script_pubkey: change_script, - }; - let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu(); - - let change_output_fee = fee_for_weight(funding_feerate_sat_per_1000_weight, change_output_weight); - change_output.value = Amount::from_sat(remaining_value.saturating_sub(change_output_fee)); - funding_outputs.push(OutputOwned::Single(change_output.clone())); - Ok(Some(change_output)) - } -} - /// Estimate our part of the fee of the new funding transaction. /// input_count: Number of contributed inputs. /// witness_weight: The witness weight for contributed inputs. @@ -9051,15 +9109,6 @@ impl FundedChannel 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. /// - `our_funding_inputs`: the inputs we contribute to the new funding transaction. /// Includes the witness weight for this input (e.g. P2WPKH_WITNESS_WEIGHT=109 for typical P2WPKH inputs). @@ -10549,7 +10598,7 @@ impl PendingV2Channel where SP::Target: SignerProvider { logger: &L, ) -> Result where L::Target: Logger { - if pre_splice_channel.is_splice_pending() { + if pre_splice_channel.pending_splice_post.is_some() { return Err(ChannelError::Warn(format!("Internal error: Channel is already splicing, channel_id {}", pre_splice_channel.context.channel_id))); } @@ -10931,6 +10980,15 @@ impl PendingV2Channel where SP::Target: SignerProvider { require_confirmed_inputs: None, } } + + /// 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 + fn is_splice_pending(&self) -> bool { + #[cfg(splicing)] + return self.pending_splice_post.is_some(); + #[cfg(not(splicing))] + false + } } // Unfunded channel utilities diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index b09cce358b5..569b9a4d0e9 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5526,6 +5526,18 @@ where result } + /// Handles a signed funding transaction generated by interactive transaction construction and + /// provided by the client. + /// + /// Do NOT broadcast the funding transaction yourself. When we have safely received our + /// counterparty's signature(s) the funding transaction will automatically be broadcast via the + /// [`BroadcasterInterface`] provided when this `ChannelManager` was constructed. + pub fn funding_transaction_signed(&self, _channel_id: &ChannelId, _counterparty_node_id: &PublicKey, + _transaction: Transaction) -> Result<(), APIError> { + // TODO(splicing) TODO(dual_funding) + Ok(()) + } + /// Atomically applies partial updates to the [`ChannelConfig`] of the given channels. /// /// Once the updates are applied, each eligible channel (advertised with a known short channel @@ -8536,7 +8548,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ if let Some(signing_session) = signing_session_opt { let (commitment_signed, funding_ready_for_sig_event_opt) = chan_entry .get_mut() - .funding_tx_constructed(signing_session, &self.logger) + .funding_tx_constructed(&counterparty_node_id, signing_session, &self.logger) .map_err(|err| MsgHandleErrInternal::send_err_msg_no_close(format!("{}", err), msg.channel_id))?; if let Some(funding_ready_for_sig_event) = funding_ready_for_sig_event_opt { let mut pending_events = self.pending_events.lock().unwrap(); diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 035189b9178..b02a187499e 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -15,6 +15,7 @@ use bitcoin::amount::Amount; use bitcoin::consensus::Encodable; use bitcoin::constants::WITNESS_SCALE_FACTOR; use bitcoin::policy::MAX_STANDARD_TX_WEIGHT; +use bitcoin::secp256k1::ecdsa::Signature; use bitcoin::secp256k1::PublicKey; use bitcoin::transaction::Version; use bitcoin::{OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Weight, Witness}; @@ -293,6 +294,9 @@ pub(crate) struct InteractiveTxSigningSession { received_commitment_signed: bool, holder_tx_signatures: Option, counterparty_sent_tx_signatures: bool, + /// Signature for optional shared input -- in case of splicing the previous funding transaction. + /// Note: field included regardless of splicing feature. + pub(crate) shared_signature: Option, } impl InteractiveTxSigningSession { @@ -357,6 +361,7 @@ impl InteractiveTxSigningSession { /// unsigned transaction. pub fn provide_holder_witnesses( &mut self, channel_id: ChannelId, witnesses: Vec, + shared_input_signature: Option, ) -> Result<(), ()> { if self.local_inputs_count() != witnesses.len() { return Err(()); @@ -367,7 +372,7 @@ impl InteractiveTxSigningSession { channel_id, tx_hash: self.unsigned_tx.compute_txid(), witnesses: witnesses.into_iter().collect(), - shared_input_signature: None, + shared_input_signature, }); Ok(()) @@ -990,6 +995,7 @@ macro_rules! define_state_transitions { received_commitment_signed: false, holder_tx_signatures: None, counterparty_sent_tx_signatures: false, + shared_signature: None, }; Ok(NegotiationComplete(signing_session)) } diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index 33f5a500789..b50a78448b1 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -7,9 +7,11 @@ // You may not use this file except in accordance with one or both of these // licenses. +use crate::events::Event; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; use crate::util::errors::APIError; +use bitcoin::Witness; /// Splicing test, simple splice-in flow. Starts with opening a V1 channel first. /// Builds on test_channel_open_simple() @@ -45,13 +47,21 @@ fn test_v1_splice_in() { "03c21e841cbc0b48197d060c71e116c185fa0ac281b7d0aa5924f535154437ca3b"; let expected_acceptor_funding_key = "039481c28b904cbe12681e79937373fc76245c1b29871028ae60ba3152162c319b"; + let expect_inputs_in_reverse = true; + let expected_pre_funding_txid = + "4f128bedf1a15baf465ab1bfd6e97c8f82628f4156bf86eb1cbc132cda6733ae"; // ==== Channel is now ready for normal operation + // Expected balances + let mut exp_balance1 = 1000 * channel_value_sat; + let mut exp_balance2 = 0; + // === Start of Splicing // Amount being added to the channel through the splice-in let splice_in_sats = 20_000; + let post_splice_channel_value = channel_value_sat + splice_in_sats; let funding_feerate_per_kw = 1024; // Create additional inputs @@ -104,7 +114,7 @@ fn test_v1_splice_in() { assert!(channel.is_usable); assert!(channel.is_channel_ready); assert_eq!(channel.channel_value_satoshis, channel_value_sat); - assert_eq!(channel.outbound_capacity_msat, 0); + assert_eq!(channel.outbound_capacity_msat, exp_balance2); assert!(channel.funding_txo.is_some()); assert!(channel.confirmations.unwrap() > 0); } @@ -121,17 +131,166 @@ fn test_v1_splice_in() { assert!(channel.is_usable); assert!(channel.is_channel_ready); assert_eq!(channel.channel_value_satoshis, channel_value_sat); - assert_eq!( - channel.outbound_capacity_msat, - 1000 * (channel_value_sat - channel_reserve_amnt_sat) - ); + assert_eq!(channel.outbound_capacity_msat, exp_balance1 - 1000 * channel_reserve_amnt_sat); assert!(channel.funding_txo.is_some()); assert!(channel.confirmations.unwrap() > 0); } - let _error_msg = get_err_msg(initiator_node, &acceptor_node.node.get_our_node_id()); + // exp_balance1 += 1000 * splice_in_sats; // increase in balance + + // Negotiate transaction inputs and outputs + + // First input + let tx_add_input_msg = get_event_msg!( + initiator_node, + MessageSendEvent::SendTxAddInput, + acceptor_node.node.get_our_node_id() + ); + let exp_value = + if expect_inputs_in_reverse { extra_splice_funding_input_sats } else { channel_value_sat }; + assert_eq!( + tx_add_input_msg.prevtx.as_transaction().output[tx_add_input_msg.prevtx_out as usize] + .value + .to_sat(), + exp_value + ); + + let _res = acceptor_node + .node + .handle_tx_add_input(initiator_node.node.get_our_node_id(), &tx_add_input_msg); + let tx_complete_msg = get_event_msg!( + acceptor_node, + MessageSendEvent::SendTxComplete, + initiator_node.node.get_our_node_id() + ); + + let _res = initiator_node + .node + .handle_tx_complete(acceptor_node.node.get_our_node_id(), &tx_complete_msg); + // Second input + let exp_value = + if expect_inputs_in_reverse { channel_value_sat } else { extra_splice_funding_input_sats }; + let tx_add_input2_msg = get_event_msg!( + initiator_node, + MessageSendEvent::SendTxAddInput, + acceptor_node.node.get_our_node_id() + ); + assert_eq!( + tx_add_input2_msg.prevtx.as_transaction().output[tx_add_input2_msg.prevtx_out as usize] + .value + .to_sat(), + exp_value + ); + + let _res = acceptor_node + .node + .handle_tx_add_input(initiator_node.node.get_our_node_id(), &tx_add_input2_msg); + let tx_complete_msg = get_event_msg!( + acceptor_node, + MessageSendEvent::SendTxComplete, + initiator_node.node.get_our_node_id() + ); + + let _res = initiator_node + .node + .handle_tx_complete(acceptor_node.node.get_our_node_id(), &tx_complete_msg); + + // TxAddOutput for the change output + let tx_add_output_msg = get_event_msg!( + initiator_node, + MessageSendEvent::SendTxAddOutput, + acceptor_node.node.get_our_node_id() + ); + assert!(tx_add_output_msg.script.is_p2wpkh()); + assert_eq!(tx_add_output_msg.sats, 14093); // extra_splice_input_sats - splice_in_sats + + let _res = acceptor_node + .node + .handle_tx_add_output(initiator_node.node.get_our_node_id(), &tx_add_output_msg); + let tx_complete_msg = get_event_msg!( + acceptor_node, + MessageSendEvent::SendTxComplete, + initiator_node.node.get_our_node_id() + ); + + let _res = initiator_node + .node + .handle_tx_complete(acceptor_node.node.get_our_node_id(), &tx_complete_msg); + // TxAddOutput for the splice funding + let tx_add_output2_msg = get_event_msg!( + initiator_node, + MessageSendEvent::SendTxAddOutput, + acceptor_node.node.get_our_node_id() + ); + assert!(tx_add_output2_msg.script.is_p2wsh()); + assert_eq!(tx_add_output2_msg.sats, post_splice_channel_value); + + let _res = acceptor_node + .node + .handle_tx_add_output(initiator_node.node.get_our_node_id(), &tx_add_output2_msg); + let _tx_complete_msg = get_event_msg!( + acceptor_node, + MessageSendEvent::SendTxComplete, + initiator_node.node.get_our_node_id() + ); + + // The last tx_complete + let _res = initiator_node + .node + .handle_tx_complete(acceptor_node.node.get_our_node_id(), &tx_complete_msg); + + let msg_events = initiator_node.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2); + let _tx_complete_msg = match msg_events[0] { + MessageSendEvent::SendTxComplete { ref node_id, ref msg } => { + assert_eq!(*node_id, acceptor_node.node.get_our_node_id()); + (*msg).clone() + }, + _ => panic!("Unexpected event"), + }; + let _msg_commitment_signed_from_0 = match msg_events[1] { + MessageSendEvent::UpdateHTLCs { ref node_id, ref updates } => { + assert_eq!(*node_id, acceptor_node.node.get_our_node_id()); + updates.commitment_signed.clone() + }, + _ => panic!("Unexpected event"), + }; + let (input_idx_prev_fund, input_idx_second_input) = + if expect_inputs_in_reverse { (0, 1) } else { (1, 0) }; + if let Event::FundingTransactionReadyForSigning { + channel_id, + counterparty_node_id, + mut unsigned_transaction, + .. + } = get_event!(initiator_node, Event::FundingTransactionReadyForSigning) + { + assert_eq!(channel_id.to_string(), expected_funded_channel_id); + assert_eq!(counterparty_node_id, acceptor_node.node.get_our_node_id()); + assert_eq!(unsigned_transaction.input.len(), 2); + // Note: input order may vary (based on SerialId) + // This is the previous funding tx input, already signed (partially) + assert_eq!( + unsigned_transaction.input[input_idx_prev_fund].previous_output.txid.to_string(), + expected_pre_funding_txid + ); + assert_eq!(unsigned_transaction.input[input_idx_prev_fund].witness.len(), 4); + // This is the extra input, not yet signed + assert_eq!(unsigned_transaction.input[input_idx_second_input].witness.len(), 0); + + // Placeholder for signature on the contributed input + let mut witness1 = Witness::new(); + witness1.push([7; 72]); + unsigned_transaction.input[input_idx_second_input].witness = witness1; + + let _res = initiator_node + .node + .funding_transaction_signed(&channel_id, &counterparty_node_id, unsigned_transaction) + .unwrap(); + } else { + panic!(); + } - // TODO(splicing): continue with splice transaction negotiation + // TODO(splicing): Continue with commitment flow, new tx confirmation // === Close channel, cooperatively initiator_node.node.close_channel(&channel_id, &acceptor_node.node.get_our_node_id()).unwrap(); From 8a835e5eba9c567da09d1d0bb6fb910d9246ef10 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Thu, 27 Mar 2025 23:24:28 +0100 Subject: [PATCH 09/10] SplicingChannel: make post_pending optional, add post_funded --- lightning/src/ln/channel.rs | 182 ++++++++++++++++++++++++++---------- 1 file changed, 134 insertions(+), 48 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index ceea56e691b..44984b743c0 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1215,7 +1215,7 @@ impl Channel where ChannelPhase::UnfundedInboundV1(chan) => &chan.context, ChannelPhase::UnfundedV2(chan) => &chan.context, #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => &chan.pre_funded.context, + ChannelPhase::RefundingV2(chan) => chan.context(), } } @@ -1227,7 +1227,7 @@ impl Channel where ChannelPhase::UnfundedInboundV1(chan) => &mut chan.context, ChannelPhase::UnfundedV2(chan) => &mut chan.context, #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => &mut chan.pre_funded.context, + ChannelPhase::RefundingV2(chan) => chan.context_mut(), } } @@ -1239,7 +1239,7 @@ impl Channel where ChannelPhase::UnfundedInboundV1(chan) => &chan.funding, ChannelPhase::UnfundedV2(chan) => &chan.funding, #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => &chan.pre_funded.funding, + ChannelPhase::RefundingV2(chan) => chan.funding(), } } @@ -1252,7 +1252,7 @@ impl Channel where ChannelPhase::UnfundedInboundV1(chan) => &mut chan.funding, ChannelPhase::UnfundedV2(chan) => &mut chan.funding, #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => &mut chan.pre_funded.funding, + ChannelPhase::RefundingV2(chan) => chan.funding_mut(), } } @@ -1264,7 +1264,7 @@ impl Channel where ChannelPhase::UnfundedInboundV1(chan) => (&chan.funding, &mut chan.context), ChannelPhase::UnfundedV2(chan) => (&chan.funding, &mut chan.context), #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => (&chan.pre_funded.funding, &mut chan.pre_funded.context), + ChannelPhase::RefundingV2(chan) => chan.funding_and_context_mut(), } } @@ -1348,7 +1348,13 @@ impl Channel where match &mut self.phase { ChannelPhase::UnfundedV2(channel) => Some(channel), #[cfg(splicing)] - ChannelPhase::RefundingV2(channel) => Some(&mut channel.post_pending), + ChannelPhase::RefundingV2(channel) => { + if let Some(ref mut post_pending) = &mut channel.post_pending { + Some(post_pending) + } else { + None + } + } _ => None, } } @@ -1531,8 +1537,8 @@ impl Channel where } #[cfg(splicing)] ChannelPhase::RefundingV2(chan) => { - let logger = WithChannelContext::from(logger, &chan.post_pending.context, None); - chan.post_pending.funding_tx_constructed(counterparty_node_id, signing_session, &&logger) + let logger = WithChannelContext::from(logger, chan.context(), None); + chan.funding_tx_constructed(counterparty_node_id, signing_session, &&logger) } _ => { Err(ChannelError::Warn("Got a tx_complete message with no interactive transaction construction expected or in-progress".to_owned())) @@ -1593,42 +1599,26 @@ impl Channel where }, #[cfg(splicing)] ChannelPhase::RefundingV2(chan) => { - let holder_commitment_point = match chan.post_pending.unfunded_context.holder_commitment_point { - Some(point) => point, - None => { - let channel_id = chan.post_pending.context.channel_id(); - // TODO(dual_funding): Add async signing support. - return Err( ChannelError::close( - format!("Expected to have holder commitment points available upon finishing interactive splice tx construction for channel {}", - channel_id))); - } - }; - let mut funded_channel = FundedChannel { - funding: chan.post_pending.funding, - context: chan.post_pending.context, - interactive_tx_signing_session: chan.post_pending.interactive_tx_signing_session, - holder_commitment_point, - is_v2_established: true, - #[cfg(splicing)] - pending_splice_pre: None, - #[cfg(splicing)] - pending_splice_post: chan.post_pending.pending_splice_post, - }; - let res = funded_channel.commitment_signed_initial_v2(msg, best_block, signer_provider, logger) - .map(|monitor| (Some(monitor), None)) - // TODO: Change to `inspect_err` when MSRV is high enough. - .map_err(|err| { - // We always expect a `ChannelError` close. - debug_assert!(matches!(err, ChannelError::Close(_))); - err - }); - self.phase = ChannelPhase::Funded(funded_channel); - res + if let Some(mut post_funded) = chan.post_funded { + let res = post_funded.commitment_signed_initial_v2(msg, best_block, signer_provider, logger) + .map(|monitor| (Some(monitor), None)) + // TODO: Change to `inspect_err` when MSRV is high enough. + .map_err(|err| { + // We always expect a `ChannelError` close. + debug_assert!(matches!(err, ChannelError::Close(_))); + err + }); + self.phase = ChannelPhase::Funded(post_funded); + res + } else { + self.phase = ChannelPhase::RefundingV2(chan); + Err(ChannelError::close("Got a commitment_signed message for an unfunded channel!".into())) + } } _ => { self.phase = phase; debug_assert!(!matches!(self.phase, ChannelPhase::Undefined)); - Err(ChannelError::close("Got a commitment_signed message for an unfunded V1 channel!".into())) + Err(ChannelError::close("Got a commitment_signed message for an unfunded channel!".into())) } }; debug_assert!(!matches!(self.phase, ChannelPhase::Undefined)); @@ -1682,8 +1672,12 @@ impl Channel where let _res = self.phase_to_splice(post_chan)?; if let ChannelPhase::RefundingV2(chan) = &mut self.phase { - let splice_ack_msg = chan.post_pending.splice_init(msg, signer_provider, entropy_source, our_node_id, logger)?; - Ok(splice_ack_msg) + if let Some(ref mut post_pending) = &mut chan.post_pending { + let splice_ack_msg = post_pending.splice_init(msg, signer_provider, entropy_source, our_node_id, logger)?; + Ok(splice_ack_msg) + } else { + Err(ChannelError::Warn("Internal error: splicing channel is not negotiating after splice_init".to_owned())) + } } else { unreachable!("Must have been transitioned to RefundingV2 in above call if successful"); } @@ -1721,8 +1715,12 @@ impl Channel where let _res = self.phase_to_splice(post_chan)?; if let ChannelPhase::RefundingV2(chan) = &mut self.phase { - let tx_msg_opt = chan.post_pending.splice_ack(msg, pending_splice.our_funding_contribution, signer_provider, entropy_source, our_node_id, logger)?; - Ok(tx_msg_opt) + if let Some(post_pending) = &mut chan.post_pending { + let tx_msg_opt = post_pending.splice_ack(msg, pending_splice.our_funding_contribution, signer_provider, entropy_source, our_node_id, logger)?; + Ok(tx_msg_opt) + } else { + Err(ChannelError::Warn("Internal error: splicing channel is not negotiating after splice_ack".to_owned())) + } } else { unreachable!("Must have been transitioned to RefundingV2 in above call if successful"); } @@ -1784,8 +1782,8 @@ where #[cfg(splicing)] pub(super) struct SplicingChannel where SP::Target: SignerProvider { pub pre_funded: FundedChannel, - pub post_pending: PendingV2Channel, - // pub post_funded: Option>, + pub post_pending: Option>, + pub post_funded: Option>, } #[cfg(splicing)] @@ -1793,8 +1791,95 @@ impl SplicingChannel where SP::Target: SignerProvider { pub(super) fn new(pre_funded: FundedChannel, post_pending: PendingV2Channel) -> Self { Self { pre_funded, - post_pending, - // post_funded: None, + post_pending: Some(post_pending), + post_funded: None, + } + } + + pub(super) fn context(&self) -> &ChannelContext { + // If post is funded, use that, otherwise use pre + if let Some(ref post_funded) = &self.post_funded { + &post_funded.context + } else { + &self.pre_funded.context + } + } + + pub(super) fn context_mut(&mut self) -> &mut ChannelContext { + // If post is funded, use that, otherwise use pre + if let Some(ref mut post_funded) = &mut self.post_funded { + &mut post_funded.context + } else { + &mut self.pre_funded.context + } + } + + pub(super) fn funding(&self) -> &FundingScope { + // If post is funded, use that, otherwise use pre + if let Some(ref post_funded) = &self.post_funded { + &post_funded.funding + } else { + &self.pre_funded.funding + } + } + + #[cfg(any(test, feature = "_externalize_tests"))] + pub(super) fn funding_mut(&mut self) -> &mut FundingScope { + // If post is funded, use that, otherwise use pre + if let Some(ref mut post_funded) = &mut self.post_funded { + &mut post_funded.funding + } else { + &mut self.pre_funded.funding + } + } + + pub(super) fn funding_and_context_mut(&mut self) -> (&FundingScope, &mut ChannelContext) { + // If post is funded, use that, otherwise use pre + if let Some(ref mut post_funded) = &mut self.post_funded { + (&mut post_funded.funding, &mut post_funded.context) + } else { + (&mut self.pre_funded.funding, &mut self.pre_funded.context) + } + } + + pub(super) fn funding_tx_constructed( + &mut self, counterparty_node_id: &PublicKey, signing_session: InteractiveTxSigningSession, logger: &L, + ) -> Result<(msgs::CommitmentSigned, Option), ChannelError> where L::Target: Logger { + if let Some(mut post_pending) = self.post_pending.take() { + let logger = WithChannelContext::from(logger, &post_pending.context, None); + let res = post_pending.funding_tx_constructed(counterparty_node_id, signing_session, &&logger)?; + + // Promote pending channel to Funded + let holder_commitment_point = match post_pending.unfunded_context.holder_commitment_point { + Some(point) => point, + None => { + let channel_id = self.context().channel_id(); + // TODO(dual_funding): Add async signing support. + return Err( ChannelError::close(format!( + "Expected to have holder commitment points available upon finishing interactive tx construction for channel {}", + channel_id, + ))); + } + }; + let funded_channel = FundedChannel { + funding: post_pending.funding, + context: post_pending.context, + interactive_tx_signing_session: post_pending.interactive_tx_signing_session, + holder_commitment_point, + is_v2_established: true, + #[cfg(splicing)] + pending_splice_pre: None, + #[cfg(splicing)] + pending_splice_post: post_pending.pending_splice_post, + }; + self.post_funded = Some(funded_channel); + self.post_pending = None; + + Ok(res) + } else { + Err(ChannelError::Warn( + "Got a tx_complete message with no interactive transaction construction expected or in-progress".to_owned() + )) } } } @@ -2692,6 +2777,7 @@ impl PendingV2Channel where SP::Target: SignerProvider { let transaction_number = self.unfunded_context.transaction_number(); let is_splice_pending = self.is_splice_pending(); + // Find the funding output let mut output_index = None; let expected_spk = self.funding.get_funding_redeemscript().to_p2wsh(); for (idx, outp) in signing_session.unsigned_tx.outputs().enumerate() { From 0c66c80d703f8c18b83b84bf41dcb10fa46ebbee Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Fri, 28 Mar 2025 08:33:34 +0100 Subject: [PATCH 10/10] Add funding_transaction_signed, handle tx_complete --- lightning/src/ln/channel.rs | 80 +++++++++++++++++++++++++++++- lightning/src/ln/channelmanager.rs | 34 +++++++++++-- lightning/src/ln/interactivetxs.rs | 62 ++++++++++++----------- lightning/src/ln/splicing_tests.rs | 42 +++++++++++++++- 4 files changed, 181 insertions(+), 37 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 44984b743c0..a676e4c001a 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -24,9 +24,9 @@ use bitcoin::hash_types::{Txid, BlockHash}; use bitcoin::secp256k1::constants::PUBLIC_KEY_SIZE; use bitcoin::secp256k1::{PublicKey,SecretKey}; use bitcoin::secp256k1::{Secp256k1,ecdsa::Signature}; -use bitcoin::{secp256k1, sighash}; +use bitcoin::{secp256k1, sighash, Witness}; #[cfg(splicing)] -use bitcoin::{Sequence, Witness}; +use bitcoin::Sequence; use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash}; @@ -1546,6 +1546,29 @@ impl Channel where } } + pub fn funding_transaction_signed(&mut self, witnesses: Vec) -> Result, APIError> { + match &mut self.phase { + ChannelPhase::Funded(chan) => { + chan.funding_transaction_signed(witnesses) + } + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => { + if let Some(ref mut post_funded) = &mut chan.post_funded { + post_funded.funding_transaction_signed(witnesses) + } else { + Err(APIError::APIMisuseError { + err: "No negotiation in progress, called in wrong state".to_owned() + }) + } + } + _ => { + return Err(APIError::APIMisuseError { + err: "Got a funding_transaction_signed call with no funding negotiation in-progress".to_owned() + }); + } + } + } + pub fn force_shutdown(&mut self, should_broadcast: bool, closure_reason: ClosureReason) -> ShutdownResult { let (funding, context) = self.funding_and_context_mut(); context.force_shutdown(funding, should_broadcast, closure_reason) @@ -6598,6 +6621,59 @@ impl FundedChannel where return Ok(self.push_ret_blockable_mon_update(monitor_update)); } + /// 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 + fn is_splice_pending(&self) -> bool { + #[cfg(splicing)] + return self.pending_splice_post.is_some(); + #[cfg(not(splicing))] + false + } + + pub(super) fn funding_transaction_signed(&mut self, witnesses: Vec) -> Result, APIError> { + self.verify_interactive_tx_signatures(&witnesses); + let is_splice = self.is_splice_pending(); + if let Some(ref mut signing_session) = self.interactive_tx_signing_session { + // Shared signature (used in splicing): holder signature on the prev funding tx input should have been saved. + // include it in tlvs field + let tlvs = if is_splice { + if let Some(s) = signing_session.shared_signature { + Some(s) + } else { + panic!("TODO error!"); + } + } else { + None + }; + let (tx_signatures_opt, funding_tx_opt) = + signing_session.provide_holder_witnesses(self.context.channel_id(), witnesses, tlvs) + .map_err(|_| APIError::APIMisuseError { + err: "Internal error in funding_transaction_signed, provide_holder_witnesses".to_owned(), + })?; + if funding_tx_opt.is_some() { + self.funding.funding_transaction = funding_tx_opt.clone(); + /* TODO + #[cfg(splicing)] + { + self.context.funding_transaction_saved = funding_tx_opt.clone(); + } + */ + } + Ok(tx_signatures_opt) + } else { + Err(APIError::APIMisuseError { + err: format!("Channel with id {} has no pending signing session, not expecting funding signatures", self.context.channel_id()) + }) + } + } + + fn verify_interactive_tx_signatures(&mut self, _witnesses: &Vec) { + if let Some(ref mut _signing_session) = self.interactive_tx_signing_session { + // Check that sighash_all was used: + // TODO(dual_funding): Check sig for sighash + } + } + /// Public version of the below, checking relevant preconditions first. /// If we're not in a state where freeing the holding cell makes sense, this is a no-op and /// returns `(None, Vec::new())`. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 569b9a4d0e9..ba5815f41f0 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5532,9 +5532,36 @@ where /// Do NOT broadcast the funding transaction yourself. When we have safely received our /// counterparty's signature(s) the funding transaction will automatically be broadcast via the /// [`BroadcasterInterface`] provided when this `ChannelManager` was constructed. - pub fn funding_transaction_signed(&self, _channel_id: &ChannelId, _counterparty_node_id: &PublicKey, - _transaction: Transaction) -> Result<(), APIError> { - // TODO(splicing) TODO(dual_funding) + pub fn funding_transaction_signed( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, transaction: Transaction) + -> Result<(), APIError> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id) + .ok_or_else(|| APIError::ChannelUnavailable { + err: format!("Can't find a peer matching the passed counterparty node_id {}", + counterparty_node_id) })?; + + let witnesses: Vec<_> = transaction.input.into_iter().filter_map(|input| { + if input.witness.is_empty() { None } else { Some(input.witness) } + }).collect(); + + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + + if let Some(channel) = peer_state.channel_by_id.get_mut(channel_id) { + let tx_signatures_opt = channel.funding_transaction_signed(witnesses)?; + if let Some(tx_signatures) = tx_signatures_opt { + peer_state.pending_msg_events.push(MessageSendEvent::SendTxSignatures { + node_id: *counterparty_node_id, + msg: tx_signatures, + }); + } + } else { + return Err(APIError::ChannelUnavailable { err: format!( + "Channel with ID {} and counterparty_node_id {} not found", channel_id, counterparty_node_id + )}); + } + Ok(()) } @@ -8565,6 +8592,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ update_fee: None, }, }); + // channel.set_next_funding_txid(&funding_txid); // TODO ? } Ok(()) }, diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index b02a187499e..1c77f853f75 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -302,19 +302,8 @@ pub(crate) struct InteractiveTxSigningSession { impl InteractiveTxSigningSession { pub fn received_commitment_signed(&mut self) -> Option { self.received_commitment_signed = true; - if self.holder_sends_tx_signatures_first { - self.holder_tx_signatures.clone() - } else { - None - } - } - pub fn get_tx_signatures(&self) -> Option { - if self.received_commitment_signed { - self.holder_tx_signatures.clone() - } else { - None - } + self.get_tx_signatures() } /// Handles a `tx_signatures` message received from the counterparty. @@ -337,22 +326,7 @@ impl InteractiveTxSigningSession { self.unsigned_tx.add_remote_witnesses(tx_signatures.witnesses.clone()); self.counterparty_sent_tx_signatures = true; - let holder_tx_signatures = if !self.holder_sends_tx_signatures_first { - self.holder_tx_signatures.clone() - } else { - None - }; - - // Check if the holder has provided its signatures and if so, - // return the finalized funding transaction. - let funding_tx_opt = if self.holder_tx_signatures.is_some() { - Some(self.finalize_funding_tx()) - } else { - // This means we're still waiting for the holder to provide their signatures. - None - }; - - Ok((holder_tx_signatures, funding_tx_opt)) + Ok((self.get_tx_signatures(), self.get_finalized_funding_tx())) } /// Provides the holder witnesses for the unsigned transaction. @@ -362,7 +336,7 @@ impl InteractiveTxSigningSession { pub fn provide_holder_witnesses( &mut self, channel_id: ChannelId, witnesses: Vec, shared_input_signature: Option, - ) -> Result<(), ()> { + ) -> Result<(Option, Option), ()> { if self.local_inputs_count() != witnesses.len() { return Err(()); } @@ -375,7 +349,35 @@ impl InteractiveTxSigningSession { shared_input_signature, }); - Ok(()) + Ok((self.get_tx_signatures(), self.get_finalized_funding_tx())) + } + + /// Decide if we need to send `TxSignatures` at this stage or not + fn get_tx_signatures(&mut self) -> Option { + if self.holder_tx_signatures.is_none() { + return None; // no local signature yet + } + if !self.received_commitment_signed { + return None; // no counterparty commitment received yet + } + if (!self.holder_sends_tx_signatures_first && self.counterparty_sent_tx_signatures) + || (self.holder_sends_tx_signatures_first && !self.counterparty_sent_tx_signatures) + { + self.holder_tx_signatures.clone() + } else { + None + } + } + + /// Decide if we have the funding transaction signed from both parties + fn get_finalized_funding_tx(&mut self) -> Option { + if self.holder_tx_signatures.is_none() { + return None; // no local signature yet + } + if !self.counterparty_sent_tx_signatures { + return None; // no counterparty signature received yet + } + Some(self.finalize_funding_tx()) } pub fn remote_inputs_count(&self) -> usize { diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index b50a78448b1..36742789b2b 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -50,6 +50,8 @@ fn test_v1_splice_in() { let expect_inputs_in_reverse = true; let expected_pre_funding_txid = "4f128bedf1a15baf465ab1bfd6e97c8f82628f4156bf86eb1cbc132cda6733ae"; + let expected_splice_funding_txid = + "0a6d624a6a6ec0f72e50317856cb8169c1a2272559ba84f20fbb37838c13bf82"; // ==== Channel is now ready for normal operation @@ -257,7 +259,7 @@ fn test_v1_splice_in() { }; let (input_idx_prev_fund, input_idx_second_input) = if expect_inputs_in_reverse { (0, 1) } else { (1, 0) }; - if let Event::FundingTransactionReadyForSigning { + let _channel_id1 = if let Event::FundingTransactionReadyForSigning { channel_id, counterparty_node_id, mut unsigned_transaction, @@ -286,8 +288,44 @@ fn test_v1_splice_in() { .node .funding_transaction_signed(&channel_id, &counterparty_node_id, unsigned_transaction) .unwrap(); + channel_id } else { - panic!(); + panic!("Expected event FundingTransactionReadyForSigning"); + }; + + // check new funding tx + assert_eq!(initiator_node.node.list_channels().len(), 1); + { + let channel = &initiator_node.node.list_channels()[0]; + assert!(!channel.is_channel_ready); + assert_eq!(channel.channel_value_satoshis, post_splice_channel_value); + assert_eq!(channel.funding_txo.unwrap().txid.to_string(), expected_splice_funding_txid); + assert_eq!(channel.confirmations.unwrap(), 0); + } + + let _res = acceptor_node + .node + .handle_tx_complete(initiator_node.node.get_our_node_id(), &tx_complete_msg); + let msg_events = acceptor_node.node.get_and_clear_pending_msg_events(); + // First messsage is commitment_signed, second is tx_signatures (see below for more) + assert_eq!(msg_events.len(), 1); + let _msg_commitment_signed_from_1 = match msg_events[0] { + MessageSendEvent::UpdateHTLCs { ref node_id, ref updates } => { + assert_eq!(*node_id, initiator_node.node.get_our_node_id()); + let res = updates.commitment_signed.clone(); + res + }, + _ => panic!("Unexpected event {:?}", msg_events[0]), + }; + + // check new funding tx (acceptor side) + assert_eq!(acceptor_node.node.list_channels().len(), 1); + { + let channel = &acceptor_node.node.list_channels()[0]; + assert!(!channel.is_channel_ready); + assert_eq!(channel.channel_value_satoshis, post_splice_channel_value); + assert_eq!(channel.funding_txo.unwrap().txid.to_string(), expected_splice_funding_txid); + assert_eq!(channel.confirmations.unwrap(), 0); } // TODO(splicing): Continue with commitment flow, new tx confirmation