From c77853444c52b1082c4e31d2af7ab04575471aad 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/17] Add begin_interactive_funding_tx_construction() --- lightning/src/ln/channel.rs | 149 ++++++++++++++++++++-- lightning/src/ln/channelmanager.rs | 2 +- lightning/src/ln/interactivetxs.rs | 197 ++++++++++++++++++++++++++++- 3 files changed, 332 insertions(+), 16 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index f1a97b39c27..73cbba0187f 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; use bitcoin::sighash::EcdsaSighashType; use bitcoin::consensus::encode; @@ -31,9 +31,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, need_to_add_funding_change_output, HandleTxCompleteValue, HandleTxCompleteResult, InteractiveTxConstructor, + InteractiveTxConstructorArgs, InteractiveTxMessageSend, InteractiveTxSigningSession, InteractiveTxMessageSendResult, + OutputOwned, SharedOwnedOutput, TX_COMMON_FIELDS_WEIGHT, }; use crate::ln::msgs; use crate::ln::msgs::{ClosingSigned, ClosingSignedFeeRange, DecodeError}; @@ -1693,8 +1693,105 @@ pub(super) trait InteractivelyFunded where SP::Target: SignerProvider fn dual_funding_context(&self) -> &DualFundingChannelContext; + fn dual_funding_context_mut(&mut self) -> &mut DualFundingChannelContext; + fn unfunded_context(&self) -> &UnfundedChannelContext; + fn is_initiator(&self) -> bool; + + #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled + fn begin_interactive_funding_tx_construction( + &mut self, signer_provider: &SP, entropy_source: &ES, holder_node_id: PublicKey, + extra_input: Option<(TxIn, TransactionU16LenLimited)>, + ) -> Result, APIError> + where ES::Target: EntropySource + { + let mut funding_inputs_with_extra = self.dual_funding_context_mut().our_funding_inputs.take().unwrap_or_else(|| vec![]); + + if let Some(extra_input) = extra_input { + funding_inputs_with_extra.push(extra_input); + } + + let mut funding_inputs_prev_outputs: Vec = Vec::with_capacity(funding_inputs_with_extra.len()); + // Check that vouts exist for each TxIn in provided transactions. + for (idx, input) in funding_inputs_with_extra.iter().enumerate() { + if let Some(output) = input.1.as_transaction().output.get(input.0.previous_output.vout as usize) { + funding_inputs_prev_outputs.push(output.clone()); + } else { + return Err(APIError::APIMisuseError { + err: format!("Transaction with txid {} does not have an output with vout of {} corresponding to TxIn at funding_inputs_with_extra[{}]", + input.1.as_transaction().compute_txid(), input.0.previous_output.vout, idx) }); + } + } + + let total_input_satoshis: u64 = funding_inputs_with_extra.iter().map( + |input| input.1.as_transaction().output.get(input.0.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.context().get_value_satoshis(); + let funding_output_script_pubkey = self.context().get_funding_redeemscript().to_p2wsh(); + let expected_remote_shared_funding_output = if self.is_initiator() { + 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) = need_to_add_funding_change_output( + self.is_initiator(), 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 _res = add_funding_change_output( + change_value, change_script, &mut funding_outputs, self.dual_funding_context().funding_feerate_sat_per_1000_weight); + } + + 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_mut().funding_feerate_sat_per_1000_weight, + is_initiator: self.is_initiator(), + funding_tx_locktime: self.dual_funding_context_mut().funding_tx_locktime, + inputs_to_contribute: funding_inputs_with_extra, + 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_mut().replace(tx_constructor); + + Ok(msg) + } + fn tx_add_input(&mut self, msg: &msgs::TxAddInput) -> InteractiveTxMessageSendResult { InteractiveTxMessageSendResult(match self.interactive_tx_constructor_mut() { Some(ref mut tx_constructor) => tx_constructor.handle_tx_add_input(msg).map_err( @@ -1859,12 +1956,20 @@ impl InteractivelyFunded for OutboundV2Channel where SP::Targ fn dual_funding_context(&self) -> &DualFundingChannelContext { &self.dual_funding_context } + #[allow(dead_code)] // TODO(dual_funding): Remove once begin_interactive_funding_tx_construction() is used + fn dual_funding_context_mut(&mut self) -> &mut DualFundingChannelContext { + &mut self.dual_funding_context + } fn unfunded_context(&self) -> &UnfundedChannelContext { &self.unfunded_context } fn interactive_tx_constructor_mut(&mut self) -> &mut Option { &mut self.interactive_tx_constructor } + #[allow(dead_code)] // TODO(dual_funding): Remove once begin_interactive_funding_tx_construction() is used + fn is_initiator(&self) -> bool { + true + } } impl InteractivelyFunded for InboundV2Channel where SP::Target: SignerProvider { @@ -1877,12 +1982,20 @@ impl InteractivelyFunded for InboundV2Channel where SP::Targe fn dual_funding_context(&self) -> &DualFundingChannelContext { &self.dual_funding_context } + #[allow(dead_code)] // TODO(dual_funding): Remove once begin_interactive_funding_tx_construction() is used + fn dual_funding_context_mut(&mut self) -> &mut DualFundingChannelContext { + &mut self.dual_funding_context + } fn unfunded_context(&self) -> &UnfundedChannelContext { &self.unfunded_context } fn interactive_tx_constructor_mut(&mut self) -> &mut Option { &mut self.interactive_tx_constructor } + #[allow(dead_code)] // TODO(dual_funding): Remove once begin_interactive_funding_tx_construction() is used + fn is_initiator(&self) -> bool { + false + } } impl ChannelContext where SP::Target: SignerProvider { @@ -4154,7 +4267,22 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis)) } -#[allow(dead_code)] // TODO(dual_funding): Remove once V2 channels is enabled. +#[allow(dead_code)] // TODO(dual_funding): Remove once begin_interactive_funding_tx_construction() is used +fn add_funding_change_output( + change_value: u64, change_script: ScriptBuf, + funding_outputs: &mut Vec, funding_feerate_sat_per_1000_weight: u32, +) -> TxOut { + 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(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.clone())); + change_output +} + pub(super) fn calculate_our_funding_satoshis( is_initiator: bool, funding_inputs: &[(TxIn, TransactionU16LenLimited)], total_witness_weight: Weight, funding_feerate_sat_per_1000_weight: u32, @@ -4200,6 +4328,9 @@ pub(super) fn calculate_our_funding_satoshis( 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, @@ -4212,7 +4343,7 @@ pub(super) struct DualFundingChannelContext { /// 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. #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled. - pub our_funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, + pub our_funding_inputs: Option>, } // Holder designates channel data owned for the benefit of the user client. @@ -8885,9 +9016,10 @@ impl OutboundV2Channel 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, + our_funding_inputs: Some(funding_inputs), }, interactive_tx_constructor: None, }; @@ -9053,9 +9185,10 @@ impl InboundV2Channel where SP::Target: SignerProvider { let dual_funding_context = DualFundingChannelContext { our_funding_satoshis: 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: funding_inputs.clone(), + our_funding_inputs: Some(funding_inputs.clone()), }; let interactive_tx_constructor = Some(InteractiveTxConstructor::new( diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index a5ae07eab7f..6f6f1fabd3a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -49,7 +49,7 @@ use crate::ln::inbound_payment; use crate::ln::types::ChannelId; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::ln::channel::{self, Channel, ChannelPhase, ChannelError, ChannelUpdateStatus, ShutdownResult, UpdateFulfillCommitFetch, OutboundV1Channel, InboundV1Channel, WithChannelContext, InteractivelyFunded as _}; -#[cfg(any(dual_funding, splicing))] +#[cfg(dual_funding)] use crate::ln::channel::InboundV2Channel; use crate::ln::channel_state::ChannelDetails; use crate::types::features::{Bolt12InvoiceFeatures, ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures}; diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 2b72133ec09..32536f050af 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -1151,13 +1151,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 {}", @@ -1176,7 +1176,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 @@ -1186,7 +1186,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, @@ -1662,14 +1662,55 @@ 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. +#[allow(dead_code)] // TODO(dual_funding): Remove once begin_interactive_funding_tx_construction() is used +pub(super) fn need_to_add_funding_change_output( + is_initiator: bool, our_contribution: u64, funding_inputs_prev_outputs: &Vec, + 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, + generate_holder_serial_id, need_to_add_funding_change_output, 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; @@ -2594,4 +2635,146 @@ 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_need_to_add_funding_change_output_open() { + let input_prevouts = vec![ + TxOut { value: Amount::from_sat(70_000), script_pubkey: ScriptBuf::new() }, + TxOut { value: Amount::from_sat(60_000), script_pubkey: ScriptBuf::new() }, + ]; + 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 = need_to_add_funding_change_output( + 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 = need_to_add_funding_change_output( + 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 = need_to_add_funding_change_output( + true, + our_contributed, + &input_prevouts, + &outputs, + 9000, + 300, + ); + assert_eq!(res.unwrap(), 14384); + } + { + // Insufficient inputs, no leftover + let res = need_to_add_funding_change_output( + false, + 130_000, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 300, + ); + assert!(res.is_none()); + } + { + // Very small leftover + let res = need_to_add_funding_change_output( + false, + 128_100, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 300, + ); + assert!(res.is_none()); + } + { + // Small leftover, but not dust + let res = need_to_add_funding_change_output( + false, + 128_100, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 100, + ); + assert_eq!(res.unwrap(), 154); + } + } + + #[test] + fn test_need_to_add_funding_change_output_splice() { + let input_prevouts = vec![ + TxOut { value: Amount::from_sat(70_000), script_pubkey: ScriptBuf::new() }, + TxOut { value: Amount::from_sat(60_000), script_pubkey: ScriptBuf::new() }, + ]; + 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 = need_to_add_funding_change_output( + 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 = need_to_add_funding_change_output( + false, + 128_100, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 300, + ); + assert!(res.is_none()); + } + { + // Small leftover, but not dust + let res = need_to_add_funding_change_output( + false, + 128_100, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 100, + ); + assert_eq!(res.unwrap(), 154); + } + } } From fb5a291451dbc293298237b91e082f5d88e27ec4 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:40:43 +0100 Subject: [PATCH 02/17] Rename method, review --- lightning/src/ln/channel.rs | 11 +++++------ lightning/src/ln/interactivetxs.rs | 28 +++++++++++++++------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 73cbba0187f..5c389a8816b 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -31,7 +31,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, need_to_add_funding_change_output, HandleTxCompleteValue, HandleTxCompleteResult, InteractiveTxConstructor, + get_output_weight, calculate_change_output_value, HandleTxCompleteValue, HandleTxCompleteResult, InteractiveTxConstructor, InteractiveTxConstructorArgs, InteractiveTxMessageSend, InteractiveTxSigningSession, InteractiveTxMessageSendResult, OutputOwned, SharedOwnedOutput, TX_COMMON_FIELDS_WEIGHT, }; @@ -1757,7 +1757,7 @@ pub(super) trait InteractivelyFunded where SP::Target: SignerProvider }; // Optionally add change output - if let Some(change_value) = need_to_add_funding_change_output( + if let Some(change_value) = calculate_change_output_value( self.is_initiator(), self.dual_funding_context().our_funding_satoshis, &funding_inputs_prev_outputs, &funding_outputs, self.dual_funding_context().funding_feerate_sat_per_1000_weight, @@ -1767,8 +1767,8 @@ pub(super) trait InteractivelyFunded where SP::Target: SignerProvider |err| APIError::APIMisuseError { err: format!("Failed to get change script as new destination script, {:?}", err), })?; - let _res = add_funding_change_output( - change_value, change_script, &mut funding_outputs, self.dual_funding_context().funding_feerate_sat_per_1000_weight); + add_funding_change_output(change_value, change_script, + &mut funding_outputs, self.dual_funding_context().funding_feerate_sat_per_1000_weight); } let constructor_args = InteractiveTxConstructorArgs { @@ -4271,7 +4271,7 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos fn add_funding_change_output( change_value: u64, change_script: ScriptBuf, funding_outputs: &mut Vec, funding_feerate_sat_per_1000_weight: u32, -) -> TxOut { +) { let mut change_output = TxOut { value: Amount::from_sat(change_value), script_pubkey: change_script, @@ -4280,7 +4280,6 @@ fn add_funding_change_output( let change_output_fee = fee_for_weight(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.clone())); - change_output } pub(super) fn calculate_our_funding_satoshis( diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 32536f050af..63ca011de7f 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -1665,8 +1665,10 @@ 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 need_to_add_funding_change_output( +pub(super) fn calculate_change_output_value( is_initiator: bool, our_contribution: u64, funding_inputs_prev_outputs: &Vec, funding_outputs: &Vec, funding_feerate_sat_per_1000_weight: u32, holder_dust_limit_satoshis: u64, @@ -1707,7 +1709,7 @@ 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, need_to_add_funding_change_output, AbortReason, + 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, @@ -2637,7 +2639,7 @@ mod tests { } #[test] - fn test_need_to_add_funding_change_output_open() { + fn test_calculate_change_output_value_open() { let input_prevouts = vec![ TxOut { value: Amount::from_sat(70_000), script_pubkey: ScriptBuf::new() }, TxOut { value: Amount::from_sat(60_000), script_pubkey: ScriptBuf::new() }, @@ -2653,7 +2655,7 @@ mod tests { let common_fees = 126; { // There is leftover for change - let res = need_to_add_funding_change_output( + let res = calculate_change_output_value( true, our_contributed, &input_prevouts, @@ -2665,7 +2667,7 @@ mod tests { } { // There is leftover for change, without common fees - let res = need_to_add_funding_change_output( + let res = calculate_change_output_value( false, our_contributed, &input_prevouts, @@ -2677,7 +2679,7 @@ mod tests { } { // Larger fee, smaller change - let res = need_to_add_funding_change_output( + let res = calculate_change_output_value( true, our_contributed, &input_prevouts, @@ -2689,7 +2691,7 @@ mod tests { } { // Insufficient inputs, no leftover - let res = need_to_add_funding_change_output( + let res = calculate_change_output_value( false, 130_000, &input_prevouts, @@ -2701,7 +2703,7 @@ mod tests { } { // Very small leftover - let res = need_to_add_funding_change_output( + let res = calculate_change_output_value( false, 128_100, &input_prevouts, @@ -2713,7 +2715,7 @@ mod tests { } { // Small leftover, but not dust - let res = need_to_add_funding_change_output( + let res = calculate_change_output_value( false, 128_100, &input_prevouts, @@ -2726,7 +2728,7 @@ mod tests { } #[test] - fn test_need_to_add_funding_change_output_splice() { + fn test_calculate_change_output_value_splice() { let input_prevouts = vec![ TxOut { value: Amount::from_sat(70_000), script_pubkey: ScriptBuf::new() }, TxOut { value: Amount::from_sat(60_000), script_pubkey: ScriptBuf::new() }, @@ -2742,7 +2744,7 @@ mod tests { let common_fees = 126; { // There is leftover for change - let res = need_to_add_funding_change_output( + let res = calculate_change_output_value( true, our_contributed, &input_prevouts, @@ -2754,7 +2756,7 @@ mod tests { } { // Very small leftover - let res = need_to_add_funding_change_output( + let res = calculate_change_output_value( false, 128_100, &input_prevouts, @@ -2766,7 +2768,7 @@ mod tests { } { // Small leftover, but not dust - let res = need_to_add_funding_change_output( + let res = calculate_change_output_value( false, 128_100, &input_prevouts, From 18d10250042504b3595bb9a54be8ff66633c4fec Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Mon, 9 Dec 2024 08:20:15 +0100 Subject: [PATCH 03/17] Rename extra_input to prev_funding_input --- lightning/src/ln/channel.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 5c389a8816b..59a3f4b3ccd 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1702,29 +1702,29 @@ pub(super) trait InteractivelyFunded where SP::Target: SignerProvider #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled fn begin_interactive_funding_tx_construction( &mut self, signer_provider: &SP, entropy_source: &ES, holder_node_id: PublicKey, - extra_input: Option<(TxIn, TransactionU16LenLimited)>, + prev_funding_input: Option<(TxIn, TransactionU16LenLimited)>, ) -> Result, APIError> where ES::Target: EntropySource { - let mut funding_inputs_with_extra = self.dual_funding_context_mut().our_funding_inputs.take().unwrap_or_else(|| vec![]); + let mut funding_inputs = self.dual_funding_context_mut().our_funding_inputs.take().unwrap_or_else(|| vec![]); - if let Some(extra_input) = extra_input { - funding_inputs_with_extra.push(extra_input); + if let Some(prev_funding_input) = prev_funding_input { + funding_inputs.push(prev_funding_input); } - let mut funding_inputs_prev_outputs: Vec = Vec::with_capacity(funding_inputs_with_extra.len()); + let mut funding_inputs_prev_outputs: Vec = Vec::with_capacity(funding_inputs.len()); // Check that vouts exist for each TxIn in provided transactions. - for (idx, input) in funding_inputs_with_extra.iter().enumerate() { + for (idx, input) in funding_inputs.iter().enumerate() { if let Some(output) = input.1.as_transaction().output.get(input.0.previous_output.vout as usize) { funding_inputs_prev_outputs.push(output.clone()); } else { return Err(APIError::APIMisuseError { - err: format!("Transaction with txid {} does not have an output with vout of {} corresponding to TxIn at funding_inputs_with_extra[{}]", + err: format!("Transaction with txid {} does not have an output with vout of {} corresponding to TxIn at funding_inputs[{}]", input.1.as_transaction().compute_txid(), input.0.previous_output.vout, idx) }); } } - let total_input_satoshis: u64 = funding_inputs_with_extra.iter().map( + let total_input_satoshis: u64 = funding_inputs.iter().map( |input| input.1.as_transaction().output.get(input.0.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 { @@ -1779,7 +1779,7 @@ pub(super) trait InteractivelyFunded where SP::Target: SignerProvider feerate_sat_per_kw: self.dual_funding_context_mut().funding_feerate_sat_per_1000_weight, is_initiator: self.is_initiator(), funding_tx_locktime: self.dual_funding_context_mut().funding_tx_locktime, - inputs_to_contribute: funding_inputs_with_extra, + inputs_to_contribute: funding_inputs, outputs_to_contribute: funding_outputs, expected_remote_shared_funding_output, }; From b8d4303e42cd313ede9d35c5f0ba75d639686350 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:37:18 +0100 Subject: [PATCH 04/17] Minor changes post review --- lightning/src/ln/channel.rs | 54 +++++++++++++----------------- lightning/src/ln/interactivetxs.rs | 8 +++-- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 59a3f4b3ccd..5edaf3b2490 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1706,26 +1706,27 @@ pub(super) trait InteractivelyFunded where SP::Target: SignerProvider ) -> Result, APIError> where ES::Target: EntropySource { - let mut funding_inputs = self.dual_funding_context_mut().our_funding_inputs.take().unwrap_or_else(|| vec![]); + let mut funding_inputs = Vec::new(); + mem::swap(&mut self.dual_funding_context_mut().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 = Vec::with_capacity(funding_inputs.len()); + 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, input) in funding_inputs.iter().enumerate() { - if let Some(output) = input.1.as_transaction().output.get(input.0.previous_output.vout as usize) { - funding_inputs_prev_outputs.push(output.clone()); + 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[{}]", - input.1.as_transaction().compute_txid(), input.0.previous_output.vout, idx) }); + tx.as_transaction().compute_txid(), txin.previous_output.vout, idx) }); } } let total_input_satoshis: u64 = funding_inputs.iter().map( - |input| input.1.as_transaction().output.get(input.0.previous_output.vout as usize).map(|out| out.value.to_sat()).unwrap_or(0) + |(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 { @@ -1767,8 +1768,14 @@ pub(super) trait InteractivelyFunded where SP::Target: SignerProvider |err| APIError::APIMisuseError { err: format!("Failed to get change script as new destination script, {:?}", err), })?; - add_funding_change_output(change_value, change_script, - &mut funding_outputs, self.dual_funding_context().funding_feerate_sat_per_1000_weight); + 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 { @@ -1776,9 +1783,9 @@ pub(super) trait InteractivelyFunded where SP::Target: SignerProvider 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_mut().funding_feerate_sat_per_1000_weight, + feerate_sat_per_kw: self.dual_funding_context().funding_feerate_sat_per_1000_weight, is_initiator: self.is_initiator(), - funding_tx_locktime: self.dual_funding_context_mut().funding_tx_locktime, + 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, @@ -1787,7 +1794,7 @@ pub(super) trait InteractivelyFunded where SP::Target: SignerProvider .map_err(|_| APIError::APIMisuseError { err: "Incorrect shared output provided".into() })?; let msg = tx_constructor.take_initiator_first_message(); - self.interactive_tx_constructor_mut().replace(tx_constructor); + *self.interactive_tx_constructor_mut() = Some(tx_constructor); Ok(msg) } @@ -4267,21 +4274,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)) } -#[allow(dead_code)] // TODO(dual_funding): Remove once begin_interactive_funding_tx_construction() is used -fn add_funding_change_output( - change_value: u64, change_script: ScriptBuf, - funding_outputs: &mut Vec, funding_feerate_sat_per_1000_weight: u32, -) { - 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(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.clone())); -} - pub(super) fn calculate_our_funding_satoshis( is_initiator: bool, funding_inputs: &[(TxIn, TransactionU16LenLimited)], total_witness_weight: Weight, funding_feerate_sat_per_1000_weight: u32, @@ -4341,8 +4333,10 @@ 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: Option>, + pub our_funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, } // Holder designates channel data owned for the benefit of the user client. @@ -9018,7 +9012,7 @@ impl OutboundV2Channel where SP::Target: SignerProvider { their_funding_satoshis: None, funding_tx_locktime, funding_feerate_sat_per_1000_weight, - our_funding_inputs: Some(funding_inputs), + our_funding_inputs: funding_inputs, }, interactive_tx_constructor: None, }; @@ -9187,7 +9181,7 @@ impl InboundV2Channel where SP::Target: SignerProvider { 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: Some(funding_inputs.clone()), + our_funding_inputs: funding_inputs.clone(), }; let interactive_tx_constructor = Some(InteractiveTxConstructor::new( diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 63ca011de7f..accd2ad4c53 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -1669,7 +1669,7 @@ impl InteractiveTxConstructor { /// 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, + 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 { @@ -2640,10 +2640,11 @@ mod tests { #[test] fn test_calculate_change_output_value_open() { - let input_prevouts = vec![ + 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)]; @@ -2729,10 +2730,11 @@ mod tests { #[test] fn test_calculate_change_output_value_splice() { - let input_prevouts = vec![ + 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))]; From 508c1b66bef860cd77d8c64c224d28c427d44ef8 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 05/17] New splice_channel() for initiating splicing, handle splice_init and splice_ack messages, but fail afterwards --- lightning/src/ln/channel.rs | 282 ++++++++++++++++- lightning/src/ln/channelmanager.rs | 177 ++++++++++- lightning/src/ln/functional_test_utils.rs | 2 +- lightning/src/ln/functional_tests_splice.rs | 326 ++++++++++++++++++++ lightning/src/ln/mod.rs | 3 + 5 files changed, 779 insertions(+), 11 deletions(-) create mode 100644 lightning/src/ln/functional_tests_splice.rs diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 5edaf3b2490..837de355c82 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -11,7 +11,6 @@ use bitcoin::amount::Amount; use bitcoin::constants::ChainHash; use bitcoin::script::{Script, ScriptBuf, Builder, WScriptHash}; use bitcoin::transaction::{Transaction, TxIn, TxOut}; -use bitcoin::sighash; use bitcoin::sighash::EcdsaSighashType; use bitcoin::consensus::encode; use bitcoin::absolute::LockTime; @@ -25,7 +24,7 @@ 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; +use bitcoin::{secp256k1, sighash}; use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash}; @@ -1187,6 +1186,30 @@ impl UnfundedChannelContext { } } +/// Info about a pending splice, used in the pre-splice channel +#[cfg(splicing)] +#[derive(Clone)] +struct PendingSpliceInfoPre { + pub our_funding_contribution: i64, +} + +#[cfg(splicing)] +impl PendingSpliceInfoPre { + #[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. pub(super) struct ChannelContext where SP::Target: SignerProvider { config: LegacyChannelConfig, @@ -1222,6 +1245,10 @@ pub(super) struct ChannelContext where SP::Target: SignerProvider { secp_ctx: Secp256k1, channel_value_satoshis: u64, + /// Info about an in-progress, pending splice (if any), on the pre-splice channel + #[cfg(splicing)] + pending_splice_pre: Option, + latest_monitor_update_id: u64, holder_signer: ChannelSignerType, @@ -2343,6 +2370,9 @@ impl ChannelContext where SP::Target: SignerProvider { is_manual_broadcast: false, next_funding_txid: None, + + #[cfg(splicing)] + pending_splice_pre: None, }; Ok(channel_context) @@ -2573,6 +2603,9 @@ impl ChannelContext where SP::Target: SignerProvider { local_initiated_shutdown: None, is_manual_broadcast: false, next_funding_txid: None, + + #[cfg(splicing)] + pending_splice_pre: None, }) } @@ -3755,6 +3788,33 @@ impl ChannelContext where SP::Target: SignerProvider { (context.holder_selected_channel_reserve_satoshis, context.counterparty_selected_channel_reserve_satoshis) } + /// Check that a balance value meets the channel reserve requirements or violates them (below reserve). + /// The channel value is an input as opposed to using from self, so that this can be used in case of splicing + /// to checks with new channel value (before being comitted to it). + #[cfg(splicing)] + pub fn check_balance_meets_reserve_requirements(&self, balance: u64, channel_value: u64) -> Result<(), ChannelError> { + if balance == 0 { + return Ok(()); + } + let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( + channel_value, self.holder_dust_limit_satoshis); + if balance < holder_selected_channel_reserve_satoshis { + return Err(ChannelError::Warn(format!( + "Balance below reserve mandated by holder, {} vs {}", + balance, holder_selected_channel_reserve_satoshis, + ))); + } + let counterparty_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( + channel_value, self.counterparty_dust_limit_satoshis); + if balance < counterparty_selected_channel_reserve_satoshis { + return Err(ChannelError::Warn(format!( + "Balance below reserve mandated by counterparty, {} vs {}", + balance, counterparty_selected_channel_reserve_satoshis, + ))); + } + Ok(()) + } + /// Get the commitment tx fee for the local's (i.e. our) next commitment transaction based on the /// number of pending HTLCs that are on track to be in our next commitment tx. /// @@ -4217,6 +4277,38 @@ impl ChannelContext where SP::Target: SignerProvider { self.channel_transaction_parameters = channel_transaction_parameters; self.get_initial_counterparty_commitment_signature(logger) } + + /// Get the splice message that can be sent during splice initiation. + #[cfg(splicing)] + pub fn get_splice_init(&self, our_funding_contribution_satoshis: i64, + funding_feerate_perkw: u32, locktime: u32, + ) -> msgs::SpliceInit { + // Reuse the existing funding pubkey, in spite of the channel value changing + // (though at this point we don't know the new value yet, due tue the optional counterparty contribution) + // Note that channel_keys_id is supposed NOT to change + let funding_pubkey = self.get_holder_pubkeys().funding_pubkey.clone(); + msgs::SpliceInit { + channel_id: self.channel_id, + funding_contribution_satoshis: our_funding_contribution_satoshis, + funding_feerate_perkw, + locktime, + funding_pubkey, + require_confirmed_inputs: None, + } + } + + /// 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.get_holder_pubkeys().funding_pubkey; + msgs::SpliceAck { + channel_id: self.channel_id, + funding_contribution_satoshis: our_funding_contribution_satoshis, + funding_pubkey, + require_confirmed_inputs: None, + } + } } // Internal utility functions for channels @@ -7956,6 +8048,124 @@ impl Channel where } } + /// Initiate splicing + #[cfg(splicing)] + pub fn splice_channel(&mut self, our_funding_contribution_satoshis: i64, + funding_feerate_perkw: u32, locktime: u32, + ) -> Result { + // Check if a splice has been initiated already. + // Note: this could be handled more nicely, and support multiple outstanding splice's, the incoming splice_ack matters anyways. + if let Some(splice_info) = &self.context.pending_splice_pre { + return Err(ChannelError::Warn(format!( + "Channel has already a splice pending, contribution {}", splice_info.our_funding_contribution + ))); + } + + if !matches!(self.context.channel_state, ChannelState::ChannelReady(_)) { + return Err(ChannelError::Warn(format!("Cannot initiate splicing, as channel is not Ready"))); + } + + let pre_channel_value = self.context.get_value_satoshis(); + // Sanity check: capacity cannot decrease below 0 + if (pre_channel_value as i64).saturating_add(our_funding_contribution_satoshis) < 0 { + return Err(ChannelError::Warn(format!( + "Post-splicing channel value cannot be negative. It was {} + {}", + pre_channel_value, our_funding_contribution_satoshis + ))); + } + + if our_funding_contribution_satoshis < 0 { + return Err(ChannelError::Warn(format!( + "TODO(splicing): Splice-out not supported, only splice in, contribution {}", + our_funding_contribution_satoshis, + ))); + } + + // Note: post-splice channel value is not yet known at this point, counterpary contribution is not known + // (Cannot test for miminum required post-splice channel value) + + self.context.pending_splice_pre = Some(PendingSpliceInfoPre { + our_funding_contribution: our_funding_contribution_satoshis, + }); + + let msg = self.context.get_splice_init(our_funding_contribution_satoshis, funding_feerate_perkw, locktime); + Ok(msg) + } + + /// Handle splice_init + #[cfg(splicing)] + pub fn splice_init(&mut self, msg: &msgs::SpliceInit) -> Result { + 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. + // Note: this could be handled more nicely, and support multiple outstanding splice's, the incoming splice_ack matters anyways. + if let Some(splice_info) = &self.context.pending_splice_pre { + return Err(ChannelError::Warn(format!( + "Channel has already a splice pending, contribution {}", splice_info.our_funding_contribution, + ))); + } + + if !matches!(self.context.channel_state, ChannelState::ChannelReady(_)) { + return Err(ChannelError::Warn(format!("Splicing requested on a channel that is not Ready"))); + } + + let pre_channel_value = self.context.get_value_satoshis(); + // Sanity check: capacity cannot decrease below 0 + if (pre_channel_value as i64) + .saturating_add(their_funding_contribution_satoshis) + .saturating_add(our_funding_contribution_satoshis) < 0 + { + return Err(ChannelError::Warn(format!( + "Post-splicing channel value cannot be negative. It was {} + {} + {}", + pre_channel_value, their_funding_contribution_satoshis, our_funding_contribution_satoshis, + ))); + } + + if their_funding_contribution_satoshis.saturating_add(our_funding_contribution_satoshis) < 0 { + return Err(ChannelError::Warn(format!( + "Splice-out not supported, only splice in, relative {} + {}", + their_funding_contribution_satoshis, our_funding_contribution_satoshis, + ))); + } + + let post_channel_value = PendingSpliceInfoPre::compute_post_value(pre_channel_value, their_funding_contribution_satoshis, our_funding_contribution_satoshis); + let post_balance = PendingSpliceInfoPre::add_checked(self.context.value_to_self_msat, our_funding_contribution_satoshis); + // Early check for reserve requirement, assuming maximum balance of full channel value + // This will also be checked later at tx_complete + let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?; + + // TODO(splicing): Store msg.funding_pubkey + // TODO(splicing): Apply start of splice (splice_start) + + let splice_ack_msg = self.context.get_splice_ack(our_funding_contribution_satoshis); + // TODO(splicing): start interactive funding negotiation + Ok(splice_ack_msg) + } + + /// Handle splice_ack + #[cfg(splicing)] + pub fn splice_ack(&mut self, msg: &msgs::SpliceAck) -> Result<(), ChannelError> { + let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; + + // check if splice is pending + let pending_splice = if let Some(pending_splice) = &self.context.pending_splice_pre { + pending_splice + } else { + return Err(ChannelError::Warn(format!("Channel is not in pending splice"))); + }; + + let our_funding_contribution = pending_splice.our_funding_contribution; + + let pre_channel_value = self.context.get_value_satoshis(); + let post_channel_value = PendingSpliceInfoPre::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution_satoshis); + let post_balance = PendingSpliceInfoPre::add_checked(self.context.value_to_self_msat, our_funding_contribution); + // Early check for reserve requirement, assuming maximum balance of full channel value + // This will also be checked later at tx_complete + let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?; + Ok(()) + } // Send stuff to our remote peers: @@ -10373,6 +10583,9 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, u32, &'c Ch // during a signing session, but have not received `tx_signatures` we MUST set `next_funding_txid` // to the txid of that interactive transaction, else we MUST NOT set it. next_funding_txid: None, + + #[cfg(splicing)] + pending_splice_pre: None, }, interactive_tx_signing_session: None, holder_commitment_point, @@ -12158,4 +12371,69 @@ mod tests { assert_eq!(node_a_chan.context.channel_state, ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::THEIR_CHANNEL_READY)); assert!(node_a_chan.check_get_channel_ready(0, &&logger).is_some()); } + + #[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::PendingSpliceInfoPre; + + let post_channel_value = PendingSpliceInfoPre::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 6f6f1fabd3a..a02b4765467 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4220,6 +4220,65 @@ where } } + /// Initiate a splice, to change the channel capacity of an existing funded channel. + /// After completion of splicing, the funding transaction will be replaced by a new one, spending the old funding transaction, + /// with optional extra inputs (splice-in) and/or extra outputs (splice-out or change). + /// TODO(splicing): Implementation is currently incomplete. + /// Note: Currently only splice-in is supported (increase in channel capacity), splice-out is not. + /// - our_funding_contribution_satoshis: the amount contributed by us to the channel. This will increase our channel balance. + /// - our_funding_inputs: the funding inputs provided by us. If our contribution is positive, our funding inputs must cover at most that amount. + #[cfg(splicing)] + pub fn splice_channel( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, our_funding_contribution_satoshis: i64, + _our_funding_inputs: Vec<(TxIn, Transaction)>, funding_feerate_perkw: u32, locktime: u32, + ) -> 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 mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + + // Look for the channel + match peer_state.channel_by_id.entry(*channel_id) { + hash_map::Entry::Occupied(mut chan_phase_entry) => { + if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + let msg = match chan.splice_channel(our_funding_contribution_satoshis, funding_feerate_perkw, locktime) { + Ok(msg) => msg, + Err(err) => return Err(APIError::APIMisuseError { + err: format!( + "Cannot initiate Splicing, {}, channel ID {}", err, channel_id + ) + }), + }; + + peer_state.pending_msg_events.push(events::MessageSendEvent::SendSpliceInit { + node_id: *counterparty_node_id, + msg, + }); + + Ok(()) + } else { + Err(APIError::ChannelUnavailable { + err: format!( + "Channel with id {} is not funded, cannot splice it", + channel_id + ) + }) + } + }, + hash_map::Entry::Vacant(_) => { + return Err(APIError::ChannelUnavailable { + err: format!( + "Channel with id {} not found for the passed counterparty node_id {}", + channel_id, counterparty_node_id, + ) + }); + }, + } + } + fn can_forward_htlc_to_outgoing_channel( &self, chan: &mut Channel, msg: &msgs::UpdateAddHTLC, next_packet: &NextPacketDetails ) -> Result<(), (&'static str, u16)> { @@ -9321,6 +9380,94 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ Ok(NotifyOption::SkipPersistHandleEvents) } + /// Handle incoming splice request, transition channel to splice-pending (unless some check fails). + #[cfg(splicing)] + fn internal_splice_init(&self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceInit) -> Result<(), MsgHandleErrInternal> { + // TODO(splicing): if we accept splicing, quiescence + + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id) + .ok_or_else(|| { + debug_assert!(false); + MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id), msg.channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + + // Look for the channel + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!( + "Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}, channel_id {}", + counterparty_node_id, msg.channel_id, + ), msg.channel_id)), + hash_map::Entry::Occupied(mut chan_entry) => { + if let ChannelPhase::Funded(chan) = chan_entry.get_mut() { + match chan.splice_init(msg) { + Ok(splice_ack_msg) => { + peer_state.pending_msg_events.push(events::MessageSendEvent::SendSpliceAck { + node_id: *counterparty_node_id, + msg: splice_ack_msg, + }); + }, + Err(err) => { + return Err(MsgHandleErrInternal::from_chan_no_close(err, msg.channel_id)); + } + } + } else { + return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel is not funded, cannot be spliced".to_owned(), msg.channel_id)); + } + }, + }; + + // TODO(splicing): + // Change channel, change phase (remove and add) + // Create new post-splice channel + // etc. + + Ok(()) + } + + /// Handle incoming splice request ack, transition channel to splice-pending (unless some check fails). + #[cfg(splicing)] + fn internal_splice_ack(&self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceAck) -> Result<(), MsgHandleErrInternal> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id) + .ok_or_else(|| { + debug_assert!(false); + MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id), msg.channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + + // Look for the channel + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!( + "Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", + counterparty_node_id + ), msg.channel_id)), + hash_map::Entry::Occupied(mut chan) => { + if let ChannelPhase::Funded(chan) = chan.get_mut() { + match chan.splice_ack(msg) { + Ok(_) => {} + Err(err) => { + return Err(MsgHandleErrInternal::from_chan_no_close(err, msg.channel_id)); + } + } + } else { + return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel is not funded, cannot splice".to_owned(), msg.channel_id)); + } + }, + }; + + // 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)) + } + /// Process pending events from the [`chain::Watch`], returning whether any events were processed. fn process_pending_monitor_events(&self) -> bool { debug_assert!(self.total_consistency_lock.try_write().is_err()); // Caller holds read lock @@ -11343,28 +11490,42 @@ where fn handle_stfu(&self, counterparty_node_id: PublicKey, msg: &msgs::Stfu) { let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( "Quiescence not supported".to_owned(), - msg.channel_id.clone())), counterparty_node_id); + msg.channel_id)), counterparty_node_id); } #[cfg(splicing)] fn handle_splice_init(&self, counterparty_node_id: PublicKey, msg: &msgs::SpliceInit) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Splicing not supported".to_owned(), - msg.channel_id.clone())), counterparty_node_id); + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let res = self.internal_splice_init(&counterparty_node_id, msg); + let persist = match &res { + Err(e) if e.closes_channel() => NotifyOption::DoPersist, + Err(_) => NotifyOption::SkipPersistHandleEvents, + Ok(()) => NotifyOption::SkipPersistNoEvents, + }; + let _ = handle_error!(self, res, counterparty_node_id); + persist + }); } #[cfg(splicing)] fn handle_splice_ack(&self, counterparty_node_id: PublicKey, msg: &msgs::SpliceAck) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Splicing not supported (splice_ack)".to_owned(), - msg.channel_id.clone())), counterparty_node_id); + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let res = self.internal_splice_ack(&counterparty_node_id, msg); + let persist = match &res { + Err(e) if e.closes_channel() => NotifyOption::DoPersist, + Err(_) => NotifyOption::SkipPersistHandleEvents, + Ok(()) => NotifyOption::SkipPersistNoEvents, + }; + let _ = handle_error!(self, res, counterparty_node_id); + persist + }); } #[cfg(splicing)] fn handle_splice_locked(&self, counterparty_node_id: PublicKey, msg: &msgs::SpliceLocked) { let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( "Splicing not supported (splice_locked)".to_owned(), - msg.channel_id.clone())), counterparty_node_id); + msg.channel_id)), counterparty_node_id); } fn handle_shutdown(&self, counterparty_node_id: PublicKey, msg: &msgs::Shutdown) { diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index b4f172b4a27..477ea7f05fc 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -801,7 +801,7 @@ macro_rules! get_event_msg { assert_eq!(*node_id, $node_id); (*msg).clone() }, - _ => panic!("Unexpected event"), + _ => panic!("Unexpected event {:?}", events[0]), } } } diff --git a/lightning/src/ln/functional_tests_splice.rs b/lightning/src/ln/functional_tests_splice.rs new file mode 100644 index 00000000000..55ba1ea7de4 --- /dev/null +++ b/lightning/src/ln/functional_tests_splice.rs @@ -0,0 +1,326 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Tests that test standing up a network of ChannelManagers, creating channels, sending +//! payments/messages between them, and often checking the resulting ChannelMonitors are able to +//! claim outputs on-chain. + +use crate::events::{Event, MessageSendEvent, MessageSendEventsProvider}; +use crate::ln::functional_test_utils::*; +use crate::ln::msgs::ChannelMessageHandler; +use crate::util::config::{ChannelHandshakeConfig, UserConfig}; + +/// Splicing test, simple splice-in flow. Starts with opening a V1 channel first. +/// Builds on test_channel_open_simple() +#[test] +fn test_v1_splice_in() { + // Set up a network of 2 nodes + let cfg = UserConfig { + channel_handshake_config: ChannelHandshakeConfig { ..Default::default() }, + ..Default::default() + }; + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(cfg), None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + // Initiator and Acceptor nodes + let initiator_node_index = 0; + let acceptor_node_index = 1; + let initiator_node = &nodes[initiator_node_index]; + let acceptor_node = &nodes[acceptor_node_index]; + + // Instantiate channel parameters where we push the maximum msats given our funding satoshis + let channel_value_sat = 100_000; // same as funding satoshis + let push_msat = 0; + let channel_reserve_amnt_sat = 1_000; + + let expected_funded_channel_id = + "ae3367da2c13bc1ceb86bf56418f62828f7ce9d6bfb15a46af5ba1f1ed8b124f"; + + // Have initiator_node initiate a channel to acceptor_node with aforementioned parameters + let channel_id_temp1 = initiator_node + .node + .create_channel( + acceptor_node.node.get_our_node_id(), + channel_value_sat, + push_msat, + 42, + None, + None, + ) + .unwrap(); + + // Extract the channel open message from initiator_node to acceptor_node + let open_channel_message = get_event_msg!( + initiator_node, + MessageSendEvent::SendOpenChannel, + acceptor_node.node.get_our_node_id() + ); + let expected_initiator_funding_key = + "03c21e841cbc0b48197d060c71e116c185fa0ac281b7d0aa5924f535154437ca3b"; + assert_eq!( + open_channel_message.common_fields.funding_pubkey.to_string(), + expected_initiator_funding_key + ); + + let _res = acceptor_node + .node + .handle_open_channel(initiator_node.node.get_our_node_id(), &open_channel_message.clone()); + // Extract the accept channel message from acceptor_node to initiator_node + let accept_channel_message = get_event_msg!( + acceptor_node, + MessageSendEvent::SendAcceptChannel, + initiator_node.node.get_our_node_id() + ); + let expected_acceptor_funding_key = + "039481c28b904cbe12681e79937373fc76245c1b29871028ae60ba3152162c319b"; + assert_eq!( + accept_channel_message.common_fields.funding_pubkey.to_string(), + expected_acceptor_funding_key + ); + + let _res = initiator_node.node.handle_accept_channel( + acceptor_node.node.get_our_node_id(), + &accept_channel_message.clone(), + ); + // Note: FundingGenerationReady emitted, checked and used below + let (_channel_id_temp2, funding_tx, _funding_output) = create_funding_transaction( + &initiator_node, + &acceptor_node.node.get_our_node_id(), + channel_value_sat, + 42, + ); + + // Funding transation created, provide it + let _res = initiator_node + .node + .funding_transaction_generated( + channel_id_temp1, + acceptor_node.node.get_our_node_id(), + funding_tx.clone(), + ) + .unwrap(); + + let funding_created_message = get_event_msg!( + initiator_node, + MessageSendEvent::SendFundingCreated, + acceptor_node.node.get_our_node_id() + ); + + let _res = acceptor_node + .node + .handle_funding_created(initiator_node.node.get_our_node_id(), &funding_created_message); + + assert_eq!(initiator_node.node.list_channels().len(), 1); + { + let channel = &initiator_node.node.list_channels()[0]; + assert!(!channel.is_channel_ready); + } + // do checks on the acceptor node as well (capacity, etc.) + assert_eq!(acceptor_node.node.list_channels().len(), 1); + { + let channel = &acceptor_node.node.list_channels()[0]; + assert!(!channel.is_channel_ready); + } + + let funding_signed_message = get_event_msg!( + acceptor_node, + MessageSendEvent::SendFundingSigned, + initiator_node.node.get_our_node_id() + ); + let _res = initiator_node + .node + .handle_funding_signed(acceptor_node.node.get_our_node_id(), &funding_signed_message); + // Take new channel ID + let channel_id2 = funding_signed_message.channel_id; + assert_eq!(channel_id2.to_string(), expected_funded_channel_id); + + // Check that funding transaction has been broadcasted + assert_eq!( + chanmon_cfgs[initiator_node_index].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), + 1 + ); + let broadcasted_funding_tx = + chanmon_cfgs[initiator_node_index].tx_broadcaster.txn_broadcasted.lock().unwrap()[0] + .clone(); + + check_added_monitors!(initiator_node, 1); + let _ev = get_event!(initiator_node, Event::ChannelPending); + check_added_monitors!(acceptor_node, 1); + let _ev = get_event!(acceptor_node, Event::ChannelPending); + + // Simulate confirmation of the funding tx + confirm_transaction(&initiator_node, &broadcasted_funding_tx); + let channel_ready_message = get_event_msg!( + initiator_node, + MessageSendEvent::SendChannelReady, + acceptor_node.node.get_our_node_id() + ); + + confirm_transaction(&acceptor_node, &broadcasted_funding_tx); + let channel_ready_message2 = get_event_msg!( + acceptor_node, + MessageSendEvent::SendChannelReady, + initiator_node.node.get_our_node_id() + ); + + let _res = acceptor_node + .node + .handle_channel_ready(initiator_node.node.get_our_node_id(), &channel_ready_message); + let _ev = get_event!(acceptor_node, Event::ChannelReady); + let _channel_update = get_event_msg!( + acceptor_node, + MessageSendEvent::SendChannelUpdate, + initiator_node.node.get_our_node_id() + ); + + let _res = initiator_node + .node + .handle_channel_ready(acceptor_node.node.get_our_node_id(), &channel_ready_message2); + let _ev = get_event!(initiator_node, Event::ChannelReady); + let _channel_update = get_event_msg!( + initiator_node, + MessageSendEvent::SendChannelUpdate, + acceptor_node.node.get_our_node_id() + ); + + // check channel capacity and other parameters + assert_eq!(initiator_node.node.list_channels().len(), 1); + { + let channel = &initiator_node.node.list_channels()[0]; + assert_eq!(channel.channel_id.to_string(), expected_funded_channel_id); + 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.funding_txo.unwrap().txid, funding_tx.compute_txid()); + assert_eq!(channel.confirmations.unwrap(), 10); + } + // do checks on the acceptor node as well (capacity, etc.) + assert_eq!(acceptor_node.node.list_channels().len(), 1); + { + let channel = &acceptor_node.node.list_channels()[0]; + assert_eq!(channel.channel_id.to_string(), expected_funded_channel_id); + 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.funding_txo.unwrap().txid, funding_tx.compute_txid()); + assert_eq!(channel.confirmations.unwrap(), 10); + } + + // ==== Channel is now ready for normal operation + + // === Start of Splicing + println!("Start of Splicing ..., channel_id {}", channel_id2); + + // Amount being added to the channel through the splice-in + let splice_in_sats: u64 = 20000; + let funding_feerate_perkw = 1024; // TODO + let locktime = 0; // TODO + + // Initiate splice-in (on initiator_node) + let _res = initiator_node + .node + .splice_channel( + &channel_id2, + &acceptor_node.node.get_our_node_id(), + splice_in_sats as i64, + Vec::new(), + funding_feerate_perkw, + locktime, + ) + .unwrap(); + // Extract the splice message from node0 to node1 + let splice_init_msg = get_event_msg!( + initiator_node, + MessageSendEvent::SendSpliceInit, + acceptor_node.node.get_our_node_id() + ); + assert_eq!(splice_init_msg.funding_contribution_satoshis, splice_in_sats as i64); + assert_eq!(splice_init_msg.funding_feerate_perkw, funding_feerate_perkw); + assert_eq!(splice_init_msg.funding_pubkey.to_string(), expected_initiator_funding_key); + assert!(splice_init_msg.require_confirmed_inputs.is_none()); + + let _res = acceptor_node + .node + .handle_splice_init(initiator_node.node.get_our_node_id(), &splice_init_msg); + // Extract the splice_ack message from node1 to node0 + let splice_ack_msg = get_event_msg!( + acceptor_node, + MessageSendEvent::SendSpliceAck, + initiator_node.node.get_our_node_id() + ); + assert_eq!(splice_ack_msg.funding_contribution_satoshis, 0); + assert_eq!(splice_ack_msg.funding_pubkey.to_string(), expected_acceptor_funding_key); + assert!(splice_ack_msg.require_confirmed_inputs.is_none()); + + // still pre-splice channel: capacity not updated, channel usable, and funding tx set + assert_eq!(acceptor_node.node.list_channels().len(), 1); + { + let channel = &acceptor_node.node.list_channels()[0]; + assert_eq!(channel.channel_id.to_string(), expected_funded_channel_id); + 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!(channel.funding_txo.is_some()); + assert!(channel.confirmations.unwrap() > 0); + } + + let _res = initiator_node + .node + .handle_splice_ack(acceptor_node.node.get_our_node_id(), &splice_ack_msg); + + // still pre-splice channel: capacity not updated, channel usable, and funding tx set + assert_eq!(initiator_node.node.list_channels().len(), 1); + { + let channel = &initiator_node.node.list_channels()[0]; + assert_eq!(channel.channel_id.to_string(), expected_funded_channel_id); + 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!(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()); + + // TODO(splicing): continue with splice transaction negotiation + + // === Close channel, cooperatively + initiator_node.node.close_channel(&channel_id2, &acceptor_node.node.get_our_node_id()).unwrap(); + let node0_shutdown_message = get_event_msg!( + initiator_node, + MessageSendEvent::SendShutdown, + acceptor_node.node.get_our_node_id() + ); + acceptor_node + .node + .handle_shutdown(initiator_node.node.get_our_node_id(), &node0_shutdown_message); + let nodes_1_shutdown = get_event_msg!( + acceptor_node, + MessageSendEvent::SendShutdown, + initiator_node.node.get_our_node_id() + ); + initiator_node.node.handle_shutdown(acceptor_node.node.get_our_node_id(), &nodes_1_shutdown); + let _ = get_event_msg!( + initiator_node, + MessageSendEvent::SendClosingSigned, + acceptor_node.node.get_our_node_id() + ); +} diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index 1f6a1096e11..9f3d5cfaf4b 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -58,6 +58,9 @@ mod blinded_payment_tests; #[cfg(test)] #[allow(unused_mut)] mod functional_tests; +#[cfg(all(test, splicing))] +#[allow(unused_mut)] +mod functional_tests_splice; #[cfg(test)] #[allow(unused_mut)] mod max_payment_path_len_tests; From a0346b681659ebe095b6a72d33e0d977b2d97556 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:31:52 +0100 Subject: [PATCH 06/17] Move PendingSpliceInfoPre to Channel (from ChannelContext) --- lightning/src/ln/channel.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 837de355c82..7c5b474e0f7 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1245,10 +1245,6 @@ pub(super) struct ChannelContext where SP::Target: SignerProvider { secp_ctx: Secp256k1, channel_value_satoshis: u64, - /// Info about an in-progress, pending splice (if any), on the pre-splice channel - #[cfg(splicing)] - pending_splice_pre: Option, - latest_monitor_update_id: u64, holder_signer: ChannelSignerType, @@ -2370,9 +2366,6 @@ impl ChannelContext where SP::Target: SignerProvider { is_manual_broadcast: false, next_funding_txid: None, - - #[cfg(splicing)] - pending_splice_pre: None, }; Ok(channel_context) @@ -2603,9 +2596,6 @@ impl ChannelContext where SP::Target: SignerProvider { local_initiated_shutdown: None, is_manual_broadcast: false, next_funding_txid: None, - - #[cfg(splicing)] - pending_splice_pre: None, }) } @@ -4437,6 +4427,9 @@ pub(super) struct Channel where SP::Target: SignerProvider { pub context: ChannelContext, pub interactive_tx_signing_session: Option, holder_commitment_point: HolderCommitmentPoint, + /// Info about an in-progress, pending splice (if any), on the pre-splice channel + #[cfg(splicing)] + pending_splice_pre: Option, } #[cfg(any(test, fuzzing))] @@ -8055,7 +8048,7 @@ impl Channel where ) -> Result { // Check if a splice has been initiated already. // Note: this could be handled more nicely, and support multiple outstanding splice's, the incoming splice_ack matters anyways. - if let Some(splice_info) = &self.context.pending_splice_pre { + 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 ))); @@ -8084,7 +8077,7 @@ impl Channel where // Note: post-splice channel value is not yet known at this point, counterpary contribution is not known // (Cannot test for miminum required post-splice channel value) - self.context.pending_splice_pre = Some(PendingSpliceInfoPre { + self.pending_splice_pre = Some(PendingSpliceInfoPre { our_funding_contribution: our_funding_contribution_satoshis, }); @@ -8101,7 +8094,7 @@ impl Channel where // Check if a splice has been initiated already. // Note: this could be handled more nicely, and support multiple outstanding splice's, the incoming splice_ack matters anyways. - if let Some(splice_info) = &self.context.pending_splice_pre { + 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, ))); @@ -8150,7 +8143,7 @@ impl Channel where let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; // check if splice is pending - let pending_splice = if let Some(pending_splice) = &self.context.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"))); @@ -8860,6 +8853,8 @@ impl OutboundV1Channel where SP::Target: SignerProvider { context: self.context, interactive_tx_signing_session: None, holder_commitment_point, + #[cfg(splicing)] + pending_splice_pre: None, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some() @@ -9125,6 +9120,8 @@ impl InboundV1Channel where SP::Target: SignerProvider { context: self.context, interactive_tx_signing_session: None, holder_commitment_point, + #[cfg(splicing)] + pending_splice_pre: None, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some() || channel.context.signer_pending_channel_ready; @@ -9300,6 +9297,8 @@ impl OutboundV2Channel where SP::Target: SignerProvider { context: self.context, interactive_tx_signing_session: Some(signing_session), holder_commitment_point, + #[cfg(splicing)] + pending_splice_pre: None, }; Ok(channel) @@ -9506,6 +9505,8 @@ impl InboundV2Channel where SP::Target: SignerProvider { context: self.context, interactive_tx_signing_session: Some(signing_session), holder_commitment_point, + #[cfg(splicing)] + pending_splice_pre: None, }; Ok(channel) @@ -10583,12 +10584,11 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, u32, &'c Ch // during a signing session, but have not received `tx_signatures` we MUST set `next_funding_txid` // to the txid of that interactive transaction, else we MUST NOT set it. next_funding_txid: None, - - #[cfg(splicing)] - pending_splice_pre: None, }, interactive_tx_signing_session: None, holder_commitment_point, + #[cfg(splicing)] + pending_splice_pre: None, }) } } From da22994b8991f4489e770c84e7e7c97ebf161195 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:26:14 +0100 Subject: [PATCH 07/17] Minor review comments --- lightning/src/ln/channelmanager.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index a02b4765467..d3b53a24be4 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4226,7 +4226,7 @@ where /// TODO(splicing): Implementation is currently incomplete. /// Note: Currently only splice-in is supported (increase in channel capacity), splice-out is not. /// - our_funding_contribution_satoshis: the amount contributed by us to the channel. This will increase our channel balance. - /// - our_funding_inputs: the funding inputs provided by us. If our contribution is positive, our funding inputs must cover at most that amount. + /// - our_funding_inputs: the funding inputs provided by us. If our contribution is positive, our funding inputs must cover at least that amount. #[cfg(splicing)] pub fn splice_channel( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, our_funding_contribution_satoshis: i64, @@ -4244,14 +4244,12 @@ where match peer_state.channel_by_id.entry(*channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { - let msg = match chan.splice_channel(our_funding_contribution_satoshis, funding_feerate_perkw, locktime) { - Ok(msg) => msg, - Err(err) => return Err(APIError::APIMisuseError { + let msg = chan.splice_channel(our_funding_contribution_satoshis, funding_feerate_perkw, locktime) + .map_err(|err| APIError::APIMisuseError { err: format!( "Cannot initiate Splicing, {}, channel ID {}", err, channel_id ) - }), - }; + })?; peer_state.pending_msg_events.push(events::MessageSendEvent::SendSpliceInit { node_id: *counterparty_node_id, From a303b9e03479962c3bca5642ecbd99728036219a Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:04:15 +0100 Subject: [PATCH 08/17] In splice_channel() check if provided inputs are sufficient --- lightning/src/ln/channel.rs | 13 ++++++++++++- lightning/src/ln/channelmanager.rs | 4 ++-- lightning/src/ln/functional_tests_splice.rs | 5 ++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 7c5b474e0f7..0c0efdf24d8 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -8044,7 +8044,7 @@ impl Channel where /// Initiate splicing #[cfg(splicing)] pub fn splice_channel(&mut self, our_funding_contribution_satoshis: i64, - funding_feerate_perkw: u32, locktime: u32, + our_funding_inputs: Vec<(TxIn, Transaction)>, funding_feerate_perkw: u32, locktime: u32, ) -> Result { // Check if a splice has been initiated already. // Note: this could be handled more nicely, and support multiple outstanding splice's, the incoming splice_ack matters anyways. @@ -8077,6 +8077,17 @@ impl Channel where // Note: post-splice channel value is not yet known at this point, counterpary contribution is not known // (Cannot test for miminum required post-splice channel value) + // Check that inputs are sufficient to cover our contribution + let sum_input: i64 = our_funding_inputs.into_iter().fold(0, |acc, i| + acc + i.1.output.get(i.0.previous_output.vout as usize).map(|tx| tx.value.to_sat() as i64).unwrap_or(0) + ); + if sum_input < our_funding_contribution_satoshis { + return Err(ChannelError::Warn(format!( + "Provided inputs are insufficient for our contribution, {} {}", + sum_input, our_funding_contribution_satoshis, + ))); + } + self.pending_splice_pre = Some(PendingSpliceInfoPre { our_funding_contribution: our_funding_contribution_satoshis, }); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index d3b53a24be4..825244017c8 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4230,7 +4230,7 @@ where #[cfg(splicing)] pub fn splice_channel( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, our_funding_contribution_satoshis: i64, - _our_funding_inputs: Vec<(TxIn, Transaction)>, funding_feerate_perkw: u32, locktime: u32, + our_funding_inputs: Vec<(TxIn, Transaction)>, funding_feerate_perkw: u32, locktime: u32, ) -> Result<(), APIError> { let per_peer_state = self.per_peer_state.read().unwrap(); @@ -4244,7 +4244,7 @@ where match peer_state.channel_by_id.entry(*channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { - let msg = chan.splice_channel(our_funding_contribution_satoshis, funding_feerate_perkw, locktime) + let msg = chan.splice_channel(our_funding_contribution_satoshis, our_funding_inputs, funding_feerate_perkw, locktime) .map_err(|err| APIError::APIMisuseError { err: format!( "Cannot initiate Splicing, {}, channel ID {}", err, channel_id diff --git a/lightning/src/ln/functional_tests_splice.rs b/lightning/src/ln/functional_tests_splice.rs index 55ba1ea7de4..061d509cdf7 100644 --- a/lightning/src/ln/functional_tests_splice.rs +++ b/lightning/src/ln/functional_tests_splice.rs @@ -229,6 +229,9 @@ fn test_v1_splice_in() { let funding_feerate_perkw = 1024; // TODO let locktime = 0; // TODO + // Create additional inputs + let extra_splice_funding_input_sats = 35_000; + let funding_inputs = create_dual_funding_utxos_with_prev_txs(&initiator_node, &[extra_splice_funding_input_sats]); // Initiate splice-in (on initiator_node) let _res = initiator_node .node @@ -236,7 +239,7 @@ fn test_v1_splice_in() { &channel_id2, &acceptor_node.node.get_our_node_id(), splice_in_sats as i64, - Vec::new(), + funding_inputs, funding_feerate_perkw, locktime, ) From 860a836b226c8695e38caf6b3cf4968a8d831d34 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:57:43 +0100 Subject: [PATCH 09/17] Preparations for splicing transaction negotiation --- lightning/src/ln/channel.rs | 58 ++++++++++++++++++++++++++---- lightning/src/ln/channelmanager.rs | 10 ++++-- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 0c0efdf24d8..094c1268cf1 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -34,6 +34,8 @@ use crate::ln::interactivetxs::{ InteractiveTxConstructorArgs, InteractiveTxMessageSend, InteractiveTxSigningSession, InteractiveTxMessageSendResult, OutputOwned, SharedOwnedOutput, TX_COMMON_FIELDS_WEIGHT, }; +#[cfg(splicing)] +use crate::ln::interactivetxs::InteractiveTxMessageSend; use crate::ln::msgs; use crate::ln::msgs::{ClosingSigned, ClosingSignedFeeRange, DecodeError}; use crate::ln::script::{self, ShutdownScript}; @@ -1191,6 +1193,10 @@ impl UnfundedChannelContext { #[derive(Clone)] struct PendingSpliceInfoPre { pub our_funding_contribution: i64, + pub funding_feerate_perkw: u32, + pub locktime: u32, + /// The funding inputs that we plan to contributing to the splice. + pub our_funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, } #[cfg(splicing)] @@ -4268,6 +4274,18 @@ impl ChannelContext where SP::Target: SignerProvider { self.get_initial_counterparty_commitment_signature(logger) } + /// 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.channel_value_satoshis, is_outgoing, self.channel_id); + } + /// Get the splice message that can be sent during splice initiation. #[cfg(splicing)] pub fn get_splice_init(&self, our_funding_contribution_satoshis: i64, @@ -8077,10 +8095,15 @@ impl Channel where // Note: post-splice channel value is not yet known at this point, counterpary contribution is not known // (Cannot test for miminum required post-splice channel value) + // Sum and convert inputs + let mut sum_input = 0i64; + let mut funding_inputs = Vec::new(); + for (tx_in, tx) in our_funding_inputs.into_iter() { + sum_input += tx.output.get(tx_in.previous_output.vout as usize).map(|tx| tx.value.to_sat() as i64).unwrap_or(0); + let tx16 = TransactionU16LenLimited::new(tx).map_err(|_e| ChannelError::Warn(format!("Too large transaction")))?; + funding_inputs.push((tx_in, tx16)); + } // Check that inputs are sufficient to cover our contribution - let sum_input: i64 = our_funding_inputs.into_iter().fold(0, |acc, i| - acc + i.1.output.get(i.0.previous_output.vout as usize).map(|tx| tx.value.to_sat() as i64).unwrap_or(0) - ); if sum_input < our_funding_contribution_satoshis { return Err(ChannelError::Warn(format!( "Provided inputs are insufficient for our contribution, {} {}", @@ -8090,6 +8113,9 @@ impl Channel where self.pending_splice_pre = Some(PendingSpliceInfoPre { our_funding_contribution: our_funding_contribution_satoshis, + funding_feerate_perkw, + locktime, + our_funding_inputs: funding_inputs, }); let msg = self.context.get_splice_init(our_funding_contribution_satoshis, funding_feerate_perkw, locktime); @@ -8098,7 +8124,9 @@ impl Channel 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, _signer_provider: &SP, _entropy_source: &ES, _holder_node_id: PublicKey, logger: &L, + ) -> Result where ES::Target: EntropySource, 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; @@ -8141,16 +8169,24 @@ impl Channel where let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?; // TODO(splicing): Store msg.funding_pubkey - // TODO(splicing): Apply start of splice (splice_start) + + // Apply start of splice change in the state + self.context.splice_start(false, logger); let splice_ack_msg = self.context.get_splice_ack(our_funding_contribution_satoshis); + // 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, _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 @@ -8168,7 +8204,15 @@ impl Channel where // Early check for reserve requirement, assuming maximum balance of full channel value // This will also be checked later at tx_complete let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?; - Ok(()) + + // Apply start of splice change in the state + self.context.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(None) } // Send stuff to our remote peers: diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 825244017c8..07898d7690a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -9400,7 +9400,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 ChannelPhase::Funded(chan) = chan_entry.get_mut() { - match chan.splice_init(msg) { + match chan.splice_init(msg, &self.signer_provider, &self.entropy_source, self.get_our_node_id(), &self.logger) { Ok(splice_ack_msg) => { peer_state.pending_msg_events.push(events::MessageSendEvent::SendSpliceAck { node_id: *counterparty_node_id, @@ -9445,8 +9445,12 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ), msg.channel_id)), hash_map::Entry::Occupied(mut chan) => { if let ChannelPhase::Funded(chan) = chan.get_mut() { - match chan.splice_ack(msg) { - Ok(_) => {} + match chan.splice_ack(msg, &self.signer_provider, &self.entropy_source, self.get_our_node_id(), &self.logger) { + Ok(tx_msg_opt) => { + if let Some(tx_msg_opt) = tx_msg_opt { + peer_state.pending_msg_events.push(tx_msg_opt.into_msg_send_event(counterparty_node_id.clone())); + } + } Err(err) => { return Err(MsgHandleErrInternal::from_chan_no_close(err, msg.channel_id)); } From cb677ed52d4a2c903594762e6269d1f526a0a713 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Thu, 28 Nov 2024 00:13:22 +0100 Subject: [PATCH 10/17] Rename PendingInfo --- lightning/src/ln/channel.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 094c1268cf1..d2287313d6e 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1191,7 +1191,7 @@ impl UnfundedChannelContext { /// Info about a pending splice, used in the pre-splice channel #[cfg(splicing)] #[derive(Clone)] -struct PendingSpliceInfoPre { +struct PendingSplice { pub our_funding_contribution: i64, pub funding_feerate_perkw: u32, pub locktime: u32, @@ -1200,7 +1200,7 @@ struct PendingSpliceInfoPre { } #[cfg(splicing)] -impl PendingSpliceInfoPre { +impl PendingSplice { #[inline] fn add_checked(base: u64, delta: i64) -> u64 { if delta >= 0 { @@ -4447,7 +4447,7 @@ pub(super) struct Channel where SP::Target: SignerProvider { holder_commitment_point: HolderCommitmentPoint, /// Info about an in-progress, pending splice (if any), on the pre-splice channel #[cfg(splicing)] - pending_splice_pre: Option, + pending_splice_pre: Option, } #[cfg(any(test, fuzzing))] @@ -8111,7 +8111,7 @@ impl Channel where ))); } - self.pending_splice_pre = Some(PendingSpliceInfoPre { + self.pending_splice_pre = Some(PendingSplice { our_funding_contribution: our_funding_contribution_satoshis, funding_feerate_perkw, locktime, @@ -8162,8 +8162,8 @@ impl Channel where ))); } - let post_channel_value = PendingSpliceInfoPre::compute_post_value(pre_channel_value, their_funding_contribution_satoshis, our_funding_contribution_satoshis); - let post_balance = PendingSpliceInfoPre::add_checked(self.context.value_to_self_msat, our_funding_contribution_satoshis); + let post_channel_value = PendingSplice::compute_post_value(pre_channel_value, their_funding_contribution_satoshis, our_funding_contribution_satoshis); + let post_balance = PendingSplice::add_checked(self.context.value_to_self_msat, our_funding_contribution_satoshis); // Early check for reserve requirement, assuming maximum balance of full channel value // This will also be checked later at tx_complete let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?; @@ -8199,8 +8199,8 @@ impl Channel where let our_funding_contribution = pending_splice.our_funding_contribution; let pre_channel_value = self.context.get_value_satoshis(); - let post_channel_value = PendingSpliceInfoPre::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution_satoshis); - let post_balance = PendingSpliceInfoPre::add_checked(self.context.value_to_self_msat, our_funding_contribution); + let post_channel_value = PendingSplice::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution_satoshis); + let post_balance = PendingSplice::add_checked(self.context.value_to_self_msat, our_funding_contribution); // Early check for reserve requirement, assuming maximum balance of full channel value // This will also be checked later at tx_complete let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?; @@ -12429,9 +12429,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::PendingSpliceInfoPre; + use crate::ln::channel::PendingSplice; - let post_channel_value = PendingSpliceInfoPre::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution); + let post_channel_value = PendingSplice::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution); (pre_channel_value, post_channel_value) } From e254fbe2d2578d23e1683290b80c594527e9fdeb Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Thu, 28 Nov 2024 00:17:13 +0100 Subject: [PATCH 11/17] Add begin_interactive_funding_tx_construction() --- lightning/src/ln/channel.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index d2287313d6e..301556ef808 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -34,8 +34,6 @@ use crate::ln::interactivetxs::{ InteractiveTxConstructorArgs, InteractiveTxMessageSend, InteractiveTxSigningSession, InteractiveTxMessageSendResult, OutputOwned, SharedOwnedOutput, TX_COMMON_FIELDS_WEIGHT, }; -#[cfg(splicing)] -use crate::ln::interactivetxs::InteractiveTxMessageSend; use crate::ln::msgs; use crate::ln::msgs::{ClosingSigned, ClosingSignedFeeRange, DecodeError}; use crate::ln::script::{self, ShutdownScript}; @@ -1728,7 +1726,6 @@ pub(super) trait InteractivelyFunded where SP::Target: SignerProvider fn is_initiator(&self) -> bool; - #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled fn begin_interactive_funding_tx_construction( &mut self, signer_provider: &SP, entropy_source: &ES, holder_node_id: PublicKey, prev_funding_input: Option<(TxIn, TransactionU16LenLimited)>, @@ -1992,7 +1989,6 @@ impl InteractivelyFunded for OutboundV2Channel where SP::Targ fn dual_funding_context(&self) -> &DualFundingChannelContext { &self.dual_funding_context } - #[allow(dead_code)] // TODO(dual_funding): Remove once begin_interactive_funding_tx_construction() is used fn dual_funding_context_mut(&mut self) -> &mut DualFundingChannelContext { &mut self.dual_funding_context } @@ -2002,7 +1998,6 @@ impl InteractivelyFunded for OutboundV2Channel where SP::Targ fn interactive_tx_constructor_mut(&mut self) -> &mut Option { &mut self.interactive_tx_constructor } - #[allow(dead_code)] // TODO(dual_funding): Remove once begin_interactive_funding_tx_construction() is used fn is_initiator(&self) -> bool { true } @@ -2018,7 +2013,6 @@ impl InteractivelyFunded for InboundV2Channel where SP::Targe fn dual_funding_context(&self) -> &DualFundingChannelContext { &self.dual_funding_context } - #[allow(dead_code)] // TODO(dual_funding): Remove once begin_interactive_funding_tx_construction() is used fn dual_funding_context_mut(&mut self) -> &mut DualFundingChannelContext { &mut self.dual_funding_context } @@ -2028,7 +2022,6 @@ impl InteractivelyFunded for InboundV2Channel where SP::Targe fn interactive_tx_constructor_mut(&mut self) -> &mut Option { &mut self.interactive_tx_constructor } - #[allow(dead_code)] // TODO(dual_funding): Remove once begin_interactive_funding_tx_construction() is used fn is_initiator(&self) -> bool { false } @@ -4420,7 +4413,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 fb2bbf3c44f0874fc6749421760079116e949015 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 12/17] Add RefundingV2 phase (for splicing) --- lightning/src/ln/channel.rs | 25 + lightning/src/ln/channelmanager.rs | 563 ++++++++++++---------- lightning/src/ln/functional_test_utils.rs | 2 +- 3 files changed, 327 insertions(+), 263 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 301556ef808..3fbefa668b9 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1130,6 +1130,9 @@ pub(super) enum ChannelPhase where SP::Target: SignerProvider { #[allow(dead_code)] // TODO(dual_funding): Remove once accepting V2 channels is enabled. UnfundedInboundV2(InboundV2Channel), Funded(Channel), + #[cfg(splicing)] + /// Used during splicing, channel is funded but a new funding is being renegotiated. + RefundingV2(Channel), } impl<'a, SP: Deref> ChannelPhase where @@ -1143,6 +1146,8 @@ impl<'a, SP: Deref> ChannelPhase where ChannelPhase::UnfundedInboundV1(chan) => &chan.context, ChannelPhase::UnfundedOutboundV2(chan) => &chan.context, ChannelPhase::UnfundedInboundV2(chan) => &chan.context, + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => &chan.context, } } @@ -1153,6 +1158,26 @@ impl<'a, SP: Deref> ChannelPhase where ChannelPhase::UnfundedInboundV1(ref mut chan) => &mut chan.context, ChannelPhase::UnfundedOutboundV2(ref mut chan) => &mut chan.context, ChannelPhase::UnfundedInboundV2(ref mut chan) => &mut chan.context, + #[cfg(splicing)] + ChannelPhase::RefundingV2(ref mut chan) => &mut chan.context, + } + } + + pub fn funded_channel(&self) -> Option<&Channel> { + match self { + ChannelPhase::Funded(chan) => Some(&chan), + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => Some(&chan), + _ => None + } + } + + pub fn funded_channel_mut(&mut self) -> Option<&mut Channel> { + match self { + ChannelPhase::Funded(ref mut chan) => Some(chan), + #[cfg(splicing)] + ChannelPhase::RefundingV2(ref mut chan) => Some(chan), + _ => None } } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 07898d7690a..8c25b49206a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1372,6 +1372,8 @@ impl PeerState where SP::Target: SignerProvider { ChannelPhase::UnfundedInboundV1(_) => false, ChannelPhase::UnfundedOutboundV2(_) => true, ChannelPhase::UnfundedInboundV2(_) => false, + #[cfg(splicing)] + ChannelPhase::RefundingV2(_) => true, } ) && self.monitor_update_blocked_actions.is_empty() @@ -3054,6 +3056,10 @@ macro_rules! convert_chan_phase_err { ChannelPhase::UnfundedInboundV2(channel) => { convert_chan_phase_err!($self, $peer_state, $err, channel, $channel_id, UNFUNDED_CHANNEL) }, + #[cfg(splicing)] + ChannelPhase::RefundingV2(channel) => { + convert_chan_phase_err!($self, $peer_state, $err, channel, $channel_id, FUNDED_CHANNEL) + }, } }; } @@ -3708,11 +3714,7 @@ where let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; res.extend(peer_state.channel_by_id.iter() - .filter_map(|(chan_id, phase)| match phase { - // Only `Channels` in the `ChannelPhase::Funded` phase can be considered funded. - ChannelPhase::Funded(chan) => Some((chan_id, chan)), - _ => None, - }) + .filter_map(|(chan_id, phase)| phase.funded_channel().map(|chan| (chan_id, chan))) .filter(f) .map(|(_channel_id, channel)| { ChannelDetails::from_channel_context(&channel.context, best_block_height, @@ -3840,7 +3842,7 @@ where match peer_state.channel_by_id.entry(channel_id.clone()) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { let funding_txo_opt = chan.context.get_funding_txo(); let their_features = &peer_state.latest_features; let (shutdown_msg, mut monitor_update_opt, htlcs) = @@ -3968,7 +3970,7 @@ where let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(channel_id) { hash_map::Entry::Occupied(mut chan_phase) => { - if let ChannelPhase::Funded(chan) = chan_phase.get_mut() { + if let Some(chan) = chan_phase.get_mut().funded_channel_mut() { handle_new_monitor_update!(self, funding_txo, monitor_update, peer_state_lock, peer_state, per_peer_state, chan); return; @@ -4106,18 +4108,14 @@ where let logger = WithContext::from(&self.logger, Some(*peer_node_id), Some(*channel_id), None); if let hash_map::Entry::Occupied(mut chan_phase_entry) = peer_state.channel_by_id.entry(channel_id.clone()) { log_error!(logger, "Force-closing channel {}", channel_id); - let (mut shutdown_res, update_opt) = match chan_phase_entry.get_mut() { - ChannelPhase::Funded(ref mut chan) => { - ( - chan.context.force_shutdown(broadcast, closure_reason), - self.get_channel_update_for_broadcast(&chan).ok(), - ) - }, - ChannelPhase::UnfundedOutboundV1(_) | ChannelPhase::UnfundedInboundV1(_) | - ChannelPhase::UnfundedOutboundV2(_) | ChannelPhase::UnfundedInboundV2(_) => { - // Unfunded channel has no update - (chan_phase_entry.get_mut().context_mut().force_shutdown(false, closure_reason), None) - }, + let (mut shutdown_res, update_opt) = if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { + ( + chan.context.force_shutdown(broadcast, closure_reason), + self.get_channel_update_for_broadcast(&chan).ok(), + ) + } else { + // Unfunded channel has no update + (chan_phase_entry.get_mut().context_mut().force_shutdown(false, closure_reason), None) }; let chan_phase = remove_channel_phase!(self, peer_state, chan_phase_entry, shutdown_res); mem::drop(peer_state); @@ -4334,7 +4332,7 @@ where let mut peer_state_lock = peer_state_mutex_opt.unwrap().lock().unwrap(); let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.get_mut(&channel_id).and_then( - |chan_phase| if let ChannelPhase::Funded(chan) = chan_phase { Some(chan) } else { None } + |chan_phase| chan_phase.funded_channel_mut(), ) { None => None, Some(chan) => Some(callback(chan)), @@ -4628,40 +4626,39 @@ where let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; if let hash_map::Entry::Occupied(mut chan_phase_entry) = peer_state.channel_by_id.entry(id) { - match chan_phase_entry.get_mut() { - ChannelPhase::Funded(chan) => { - if !chan.context.is_live() { - return Err(APIError::ChannelUnavailable{err: "Peer for first hop currently disconnected".to_owned()}); - } - let funding_txo = chan.context.get_funding_txo().unwrap(); - let logger = WithChannelContext::from(&self.logger, &chan.context, Some(*payment_hash)); - let send_res = chan.send_htlc_and_commit(htlc_msat, payment_hash.clone(), - htlc_cltv, HTLCSource::OutboundRoute { - path: path.clone(), - session_priv: session_priv.clone(), - first_hop_htlc_msat: htlc_msat, - payment_id, - }, onion_packet, None, &self.fee_estimator, &&logger); - match break_chan_phase_entry!(self, peer_state, send_res, chan_phase_entry) { - Some(monitor_update) => { - match handle_new_monitor_update!(self, funding_txo, monitor_update, peer_state_lock, peer_state, per_peer_state, chan) { - false => { - // Note that MonitorUpdateInProgress here indicates (per function - // docs) that we will resend the commitment update once monitor - // updating completes. Therefore, we must return an error - // indicating that it is unsafe to retry the payment wholesale, - // which we do in the send_payment check for - // MonitorUpdateInProgress, below. - return Err(APIError::MonitorUpdateInProgress); - }, - true => {}, - } - }, - None => {}, - } - }, - _ => return Err(APIError::ChannelUnavailable{err: "Channel to first hop is unfunded".to_owned()}), - }; + if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { + if !chan.context.is_live() { + return Err(APIError::ChannelUnavailable{err: "Peer for first hop currently disconnected".to_owned()}); + } + let funding_txo = chan.context.get_funding_txo().unwrap(); + let logger = WithChannelContext::from(&self.logger, &chan.context, Some(*payment_hash)); + let send_res = chan.send_htlc_and_commit(htlc_msat, payment_hash.clone(), + htlc_cltv, HTLCSource::OutboundRoute { + path: path.clone(), + session_priv: session_priv.clone(), + first_hop_htlc_msat: htlc_msat, + payment_id, + }, onion_packet, None, &self.fee_estimator, &&logger); + match break_chan_phase_entry!(self, peer_state, send_res, chan_phase_entry) { + Some(monitor_update) => { + match handle_new_monitor_update!(self, funding_txo, monitor_update, peer_state_lock, peer_state, per_peer_state, chan) { + false => { + // Note that MonitorUpdateInProgress here indicates (per function + // docs) that we will resend the commitment update once monitor + // updating completes. Therefore, we must return an error + // indicating that it is unsafe to retry the payment wholesale, + // which we do in the send_payment check for + // MonitorUpdateInProgress, below. + return Err(APIError::MonitorUpdateInProgress); + }, + true => {}, + } + }, + None => {}, + } + } else { + return Err(APIError::ChannelUnavailable{err: "Channel to first hop is unfunded".to_owned()}); + } } else { // The channel was likely removed after we fetched the id from the // `short_to_chan_info` map, but before we successfully locked the @@ -5474,7 +5471,7 @@ where if !channel_phase.context_mut().update_config(&config) { continue; } - if let ChannelPhase::Funded(channel) = channel_phase { + if let Some(channel) = channel_phase.funded_channel_mut() { if let Ok(msg) = self.get_channel_update_for_broadcast(channel) { let mut pending_broadcast_messages = self.pending_broadcast_messages.lock().unwrap(); pending_broadcast_messages.push(events::MessageSendEvent::BroadcastChannelUpdate { msg }); @@ -5562,18 +5559,21 @@ where let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.get(next_hop_channel_id) { - Some(ChannelPhase::Funded(chan)) => { - if !chan.context.is_usable() { + Some(phase) => { + if let Some(chan) = phase.funded_channel() { + if !chan.context.is_usable() { + return Err(APIError::ChannelUnavailable { + err: format!("Channel with id {} not fully established", next_hop_channel_id) + }) + } + chan.context.get_short_channel_id().unwrap_or(chan.context.outbound_scid_alias()) + } else { return Err(APIError::ChannelUnavailable { - err: format!("Channel with id {} not fully established", next_hop_channel_id) - }) + err: format!("Channel with id {} for the passed counterparty node_id {} is still opening.", + next_hop_channel_id, next_node_id) + }); } - chan.context.get_short_channel_id().unwrap_or(chan.context.outbound_scid_alias()) - }, - Some(_) => return Err(APIError::ChannelUnavailable { - err: format!("Channel with id {} for the passed counterparty node_id {} is still opening.", - next_hop_channel_id, next_node_id) - }), + } None => { let error = format!("Channel with id {} not found for the passed counterparty node_id {}", next_hop_channel_id, next_node_id); @@ -5581,7 +5581,7 @@ where log_error!(logger, "{} when attempting to forward intercepted HTLC", error); return Err(APIError::ChannelUnavailable { err: error - }) + }); } } }; @@ -5973,8 +5973,8 @@ where // applying non-strict forwarding. // The channel with the least amount of outbound liquidity will be used to maximize the // probability of being able to successfully forward a subsequent HTLC. - let maybe_optimal_channel = peer_state.channel_by_id.values_mut().filter_map(|phase| match phase { - ChannelPhase::Funded(chan) => { + let maybe_optimal_channel = peer_state.channel_by_id.values_mut().filter_map(|phase| + if let Some(chan) = phase.funded_channel_mut() { let balances = chan.context.get_available_balances(&self.fee_estimator); if outgoing_amt_msat <= balances.next_outbound_htlc_limit_msat && outgoing_amt_msat >= balances.next_outbound_htlc_minimum_msat && @@ -5983,15 +5983,21 @@ where } else { None } - }, - _ => None, - }).min_by_key(|(_, balances)| balances.next_outbound_htlc_limit_msat).map(|(c, _)| c); + } else { + None + } + ).min_by_key(|(_, balances)| balances.next_outbound_htlc_limit_msat).map(|(c, _)| c); let optimal_channel = match maybe_optimal_channel { Some(chan) => chan, None => { // Fall back to the specified channel to return an appropriate error. - if let Some(ChannelPhase::Funded(ref mut chan)) = peer_state.channel_by_id.get_mut(&forward_chan_id) { - chan + if let Some(phase) = peer_state.channel_by_id.get_mut(&forward_chan_id) { + if let Some(chan) = phase.funded_channel_mut() { + chan + } else { + forwarding_channel_not_found!(core::iter::once(forward_info).chain(draining_pending_forwards)); + break; + } } else { forwarding_channel_not_found!(core::iter::once(forward_info).chain(draining_pending_forwards)); break; @@ -6018,7 +6024,7 @@ where panic!("Stated return value requirements in send_htlc() were not met"); } - if let Some(ChannelPhase::Funded(ref mut chan)) = peer_state.channel_by_id.get_mut(&forward_chan_id) { + if let Some(Some(ref mut chan)) = peer_state.channel_by_id.get_mut(&forward_chan_id).map(|phase| phase.funded_channel_mut()) { let failure_code = 0x1000|7; let data = self.get_htlc_inbound_temp_fail_data(failure_code); failed_forwards.push((htlc_source, payment_hash, @@ -6036,7 +6042,7 @@ where panic!("short_channel_id != 0 should imply any pending_forward entries are of type Forward"); }, HTLCForwardInfo::FailHTLC { htlc_id, ref err_packet } => { - if let Some(ChannelPhase::Funded(ref mut chan)) = peer_state.channel_by_id.get_mut(&forward_chan_id) { + if let Some(Some(ref mut chan)) = peer_state.channel_by_id.get_mut(&forward_chan_id).map(|phase| phase.funded_channel_mut()) { let logger = WithChannelContext::from(&self.logger, &chan.context, None); log_trace!(logger, "Failing HTLC back to channel with short id {} (backward HTLC ID {}) after delay", short_chan_id, htlc_id); Some((chan.queue_fail_htlc(htlc_id, err_packet.clone(), &&logger), htlc_id)) @@ -6046,7 +6052,7 @@ where } }, HTLCForwardInfo::FailMalformedHTLC { htlc_id, failure_code, sha256_of_onion } => { - if let Some(ChannelPhase::Funded(ref mut chan)) = peer_state.channel_by_id.get_mut(&forward_chan_id) { + if let Some(Some(ref mut chan)) = peer_state.channel_by_id.get_mut(&forward_chan_id).map(|phase| phase.funded_channel_mut()) { let logger = WithChannelContext::from(&self.logger, &chan.context, None); log_trace!(logger, "Failing malformed HTLC back to channel with short id {} (backward HTLC ID {}) after delay", short_chan_id, htlc_id); let res = chan.queue_fail_malformed_htlc( @@ -6062,7 +6068,7 @@ where if let Some((queue_fail_htlc_res, htlc_id)) = queue_fail_htlc_res { if let Err(e) = queue_fail_htlc_res { if let ChannelError::Ignore(msg) = e { - if let Some(ChannelPhase::Funded(ref mut chan)) = peer_state.channel_by_id.get_mut(&forward_chan_id) { + if let Some(Some(ref mut chan)) = peer_state.channel_by_id.get_mut(&forward_chan_id).map(|phase| phase.funded_channel_mut()) { let logger = WithChannelContext::from(&self.logger, &chan.context, None); log_trace!(logger, "Failed to fail HTLC with ID {} backwards to short_id {}: {}", htlc_id, short_chan_id, msg); } @@ -6370,7 +6376,7 @@ where if let Some(peer_state_mutex) = per_peer_state.get(&counterparty_node_id) { let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; - if let Some(ChannelPhase::Funded(chan)) = peer_state.channel_by_id.get_mut(&channel_id) { + if let Some(Some(chan)) = peer_state.channel_by_id.get_mut(&channel_id).map(|phase| phase.funded_channel_mut()) { handle_monitor_update_completion!(self, peer_state_lock, peer_state, per_peer_state, chan); } else { let update_actions = peer_state.monitor_update_blocked_actions @@ -6431,7 +6437,7 @@ where let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; for (chan_id, chan) in peer_state.channel_by_id.iter_mut().filter_map( - |(chan_id, phase)| if let ChannelPhase::Funded(chan) = phase { Some((chan_id, chan)) } else { None } + |(chan_id, phase)| phase.funded_channel_mut().map(|chan| ((chan_id, chan))) ) { let new_feerate = if chan.context.get_channel_type().supports_anchors_zero_fee_htlc_tx() { anchor_feerate @@ -6516,93 +6522,95 @@ where let pending_msg_events = &mut peer_state.pending_msg_events; let counterparty_node_id = *counterparty_node_id; peer_state.channel_by_id.retain(|chan_id, phase| { - match phase { - ChannelPhase::Funded(chan) => { - let new_feerate = if chan.context.get_channel_type().supports_anchors_zero_fee_htlc_tx() { - anchor_feerate - } else { - non_anchor_feerate - }; - let chan_needs_persist = self.update_channel_fee(chan_id, chan, new_feerate); - if chan_needs_persist == NotifyOption::DoPersist { should_persist = NotifyOption::DoPersist; } + if let Some(chan) = phase.funded_channel_mut() { + let new_feerate = if chan.context.get_channel_type().supports_anchors_zero_fee_htlc_tx() { + anchor_feerate + } else { + non_anchor_feerate + }; + let chan_needs_persist = self.update_channel_fee(chan_id, chan, new_feerate); + if chan_needs_persist == NotifyOption::DoPersist { should_persist = NotifyOption::DoPersist; } - if let Err(e) = chan.timer_check_closing_negotiation_progress() { - let (needs_close, err) = convert_chan_phase_err!(self, peer_state, e, chan, chan_id, FUNDED_CHANNEL); - handle_errors.push((Err(err), counterparty_node_id)); - if needs_close { return false; } - } + if let Err(e) = chan.timer_check_closing_negotiation_progress() { + let (needs_close, err) = convert_chan_phase_err!(self, peer_state, e, chan, chan_id, FUNDED_CHANNEL); + handle_errors.push((Err(err), counterparty_node_id)); + if needs_close { return false; } + } - match chan.channel_update_status() { - ChannelUpdateStatus::Enabled if !chan.context.is_live() => chan.set_channel_update_status(ChannelUpdateStatus::DisabledStaged(0)), - ChannelUpdateStatus::Disabled if chan.context.is_live() => chan.set_channel_update_status(ChannelUpdateStatus::EnabledStaged(0)), - ChannelUpdateStatus::DisabledStaged(_) if chan.context.is_live() - => chan.set_channel_update_status(ChannelUpdateStatus::Enabled), - ChannelUpdateStatus::EnabledStaged(_) if !chan.context.is_live() - => chan.set_channel_update_status(ChannelUpdateStatus::Disabled), - ChannelUpdateStatus::DisabledStaged(mut n) if !chan.context.is_live() => { - n += 1; - if n >= DISABLE_GOSSIP_TICKS { - chan.set_channel_update_status(ChannelUpdateStatus::Disabled); - if let Ok(update) = self.get_channel_update_for_broadcast(&chan) { - let mut pending_broadcast_messages = self.pending_broadcast_messages.lock().unwrap(); - pending_broadcast_messages.push(events::MessageSendEvent::BroadcastChannelUpdate { - msg: update - }); - } - should_persist = NotifyOption::DoPersist; - } else { - chan.set_channel_update_status(ChannelUpdateStatus::DisabledStaged(n)); + match chan.channel_update_status() { + ChannelUpdateStatus::Enabled if !chan.context.is_live() => chan.set_channel_update_status(ChannelUpdateStatus::DisabledStaged(0)), + ChannelUpdateStatus::Disabled if chan.context.is_live() => chan.set_channel_update_status(ChannelUpdateStatus::EnabledStaged(0)), + ChannelUpdateStatus::DisabledStaged(_) if chan.context.is_live() + => chan.set_channel_update_status(ChannelUpdateStatus::Enabled), + ChannelUpdateStatus::EnabledStaged(_) if !chan.context.is_live() + => chan.set_channel_update_status(ChannelUpdateStatus::Disabled), + ChannelUpdateStatus::DisabledStaged(mut n) if !chan.context.is_live() => { + n += 1; + if n >= DISABLE_GOSSIP_TICKS { + chan.set_channel_update_status(ChannelUpdateStatus::Disabled); + if let Ok(update) = self.get_channel_update_for_broadcast(&chan) { + let mut pending_broadcast_messages = self.pending_broadcast_messages.lock().unwrap(); + pending_broadcast_messages.push(events::MessageSendEvent::BroadcastChannelUpdate { + msg: update + }); } - }, - ChannelUpdateStatus::EnabledStaged(mut n) if chan.context.is_live() => { - n += 1; - if n >= ENABLE_GOSSIP_TICKS { - chan.set_channel_update_status(ChannelUpdateStatus::Enabled); - if let Ok(update) = self.get_channel_update_for_broadcast(&chan) { - let mut pending_broadcast_messages = self.pending_broadcast_messages.lock().unwrap(); - pending_broadcast_messages.push(events::MessageSendEvent::BroadcastChannelUpdate { - msg: update - }); - } - should_persist = NotifyOption::DoPersist; - } else { - chan.set_channel_update_status(ChannelUpdateStatus::EnabledStaged(n)); + should_persist = NotifyOption::DoPersist; + } else { + chan.set_channel_update_status(ChannelUpdateStatus::DisabledStaged(n)); + } + }, + ChannelUpdateStatus::EnabledStaged(mut n) if chan.context.is_live() => { + n += 1; + if n >= ENABLE_GOSSIP_TICKS { + chan.set_channel_update_status(ChannelUpdateStatus::Enabled); + if let Ok(update) = self.get_channel_update_for_broadcast(&chan) { + let mut pending_broadcast_messages = self.pending_broadcast_messages.lock().unwrap(); + pending_broadcast_messages.push(events::MessageSendEvent::BroadcastChannelUpdate { + msg: update + }); } - }, - _ => {}, - } + should_persist = NotifyOption::DoPersist; + } else { + chan.set_channel_update_status(ChannelUpdateStatus::EnabledStaged(n)); + } + }, + _ => {}, + } - chan.context.maybe_expire_prev_config(); + chan.context.maybe_expire_prev_config(); - if chan.should_disconnect_peer_awaiting_response() { - let logger = WithChannelContext::from(&self.logger, &chan.context, None); - log_debug!(logger, "Disconnecting peer {} due to not making any progress on channel {}", - counterparty_node_id, chan_id); - pending_msg_events.push(MessageSendEvent::HandleError { - node_id: counterparty_node_id, - action: msgs::ErrorAction::DisconnectPeerWithWarning { - msg: msgs::WarningMessage { - channel_id: *chan_id, - data: "Disconnecting due to timeout awaiting response".to_owned(), - }, + if chan.should_disconnect_peer_awaiting_response() { + let logger = WithChannelContext::from(&self.logger, &chan.context, None); + log_debug!(logger, "Disconnecting peer {} due to not making any progress on channel {}", + counterparty_node_id, chan_id); + pending_msg_events.push(MessageSendEvent::HandleError { + node_id: counterparty_node_id, + action: msgs::ErrorAction::DisconnectPeerWithWarning { + msg: msgs::WarningMessage { + channel_id: *chan_id, + data: "Disconnecting due to timeout awaiting response".to_owned(), }, - }); - } + }, + }); + } - true - }, - ChannelPhase::UnfundedInboundV1(chan) => { - process_unfunded_channel_tick!(peer_state, chan, pending_msg_events) - }, - ChannelPhase::UnfundedOutboundV1(chan) => { - process_unfunded_channel_tick!(peer_state, chan, pending_msg_events) - }, - ChannelPhase::UnfundedInboundV2(chan) => { - process_unfunded_channel_tick!(peer_state, chan, pending_msg_events) - }, - ChannelPhase::UnfundedOutboundV2(chan) => { - process_unfunded_channel_tick!(peer_state, chan, pending_msg_events) - }, + true + } else { + match phase { + ChannelPhase::UnfundedInboundV1(chan) => { + process_unfunded_channel_tick!(peer_state, chan, pending_msg_events) + }, + ChannelPhase::UnfundedOutboundV1(chan) => { + process_unfunded_channel_tick!(peer_state, chan, pending_msg_events) + }, + ChannelPhase::UnfundedInboundV2(chan) => { + process_unfunded_channel_tick!(peer_state, chan, pending_msg_events) + }, + ChannelPhase::UnfundedOutboundV2(chan) => { + process_unfunded_channel_tick!(peer_state, chan, pending_msg_events) + }, + _ => panic!("unreachable"), + } } }); @@ -6812,7 +6820,7 @@ where let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(channel_id) { hash_map::Entry::Occupied(chan_phase_entry) => { - if let ChannelPhase::Funded(_chan) = chan_phase_entry.get() { + if let Some(_chan) = chan_phase_entry.get().funded_channel() { let failure_code = 0x1000|7; let data = self.get_htlc_inbound_temp_fail_data(failure_code); (failure_code, data) @@ -7145,7 +7153,7 @@ where if let Some(peer_state_lock) = peer_state_opt.as_mut() { let peer_state = &mut **peer_state_lock; if let hash_map::Entry::Occupied(mut chan_phase_entry) = peer_state.channel_by_id.entry(chan_id) { - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { let logger = WithChannelContext::from(&self.logger, &chan.context, None); let fulfill_res = chan.get_update_fulfill_htlc_and_commit(prev_hop.htlc_id, payment_preimage, payment_info, &&logger); @@ -7889,6 +7897,16 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ num_unfunded_channels += 1; } }, + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => { + // This covers non-zero-conf inbound `Channel`s that we are currently monitoring, but those + // which have not yet had any confirmations on-chain. + if !chan.context.is_outbound() && chan.context.minimum_depth().unwrap_or(1) != 0 && + chan.context.get_funding_tx_confirmations(best_block_height) == 0 + { + num_unfunded_channels += 1; + } + }, ChannelPhase::UnfundedInboundV1(chan) => { if chan.context.minimum_depth().unwrap_or(1) != 0 { num_unfunded_channels += 1; @@ -8507,6 +8525,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ establishment".into(), )), chan_phase_entry) }, + #[cfg(splicing)] + ChannelPhase::RefundingV2(_) => { + // TODO(splicing) + try_chan_phase_entry!(self, peer_state, Err(ChannelError::Warn( + "Tx_abort on splicing not supported".into(), + )), chan_phase_entry) + } }; // This checks for and resets the interactive negotiation state by `take()`ing it from the channel. // The existence of the `tx_constructor` indicates that we have not moved into the signing @@ -8550,7 +8575,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { let logger = WithChannelContext::from(&self.logger, &chan.context, None); let announcement_sigs_opt = try_chan_phase_entry!(self, peer_state, chan.channel_ready(&msg, &self.node_signer, self.chain_hash, &self.default_configuration, &self.best_block.read().unwrap(), &&logger), chan_phase_entry); @@ -8606,44 +8631,40 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let peer_state = &mut *peer_state_lock; if let hash_map::Entry::Occupied(mut chan_phase_entry) = peer_state.channel_by_id.entry(msg.channel_id.clone()) { let phase = chan_phase_entry.get_mut(); - match phase { - ChannelPhase::Funded(chan) => { - if !chan.received_shutdown() { - let logger = WithChannelContext::from(&self.logger, &chan.context, None); - log_info!(logger, "Received a shutdown message from our counterparty for channel {}{}.", - msg.channel_id, - if chan.sent_shutdown() { " after we initiated shutdown" } else { "" }); - } + if let Some(chan) = phase.funded_channel_mut() { + if !chan.received_shutdown() { + let logger = WithChannelContext::from(&self.logger, &chan.context, None); + log_info!(logger, "Received a shutdown message from our counterparty for channel {}{}.", + msg.channel_id, + if chan.sent_shutdown() { " after we initiated shutdown" } else { "" }); + } - let funding_txo_opt = chan.context.get_funding_txo(); - let (shutdown, monitor_update_opt, htlcs) = try_chan_phase_entry!(self, peer_state, - chan.shutdown(&self.signer_provider, &peer_state.latest_features, &msg), chan_phase_entry); - dropped_htlcs = htlcs; - - if let Some(msg) = shutdown { - // We can send the `shutdown` message before updating the `ChannelMonitor` - // here as we don't need the monitor update to complete until we send a - // `shutdown_signed`, which we'll delay if we're pending a monitor update. - peer_state.pending_msg_events.push(events::MessageSendEvent::SendShutdown { - node_id: *counterparty_node_id, - msg, - }); - } - // Update the monitor with the shutdown script if necessary. - if let Some(monitor_update) = monitor_update_opt { - handle_new_monitor_update!(self, funding_txo_opt.unwrap(), monitor_update, - peer_state_lock, peer_state, per_peer_state, chan); - } - }, - ChannelPhase::UnfundedInboundV1(_) | ChannelPhase::UnfundedOutboundV1(_) | - ChannelPhase::UnfundedInboundV2(_) | ChannelPhase::UnfundedOutboundV2(_) => { - let context = phase.context_mut(); - let logger = WithChannelContext::from(&self.logger, context, None); - log_error!(logger, "Immediately closing unfunded channel {} as peer asked to cooperatively shut it down (which is unnecessary)", &msg.channel_id); - let mut close_res = phase.context_mut().force_shutdown(false, ClosureReason::CounterpartyCoopClosedUnfundedChannel); - remove_channel_phase!(self, peer_state, chan_phase_entry, close_res); - finish_shutdown = Some(close_res); - }, + let funding_txo_opt = chan.context.get_funding_txo(); + let (shutdown, monitor_update_opt, htlcs) = try_chan_phase_entry!(self, peer_state, + chan.shutdown(&self.signer_provider, &peer_state.latest_features, &msg), chan_phase_entry); + dropped_htlcs = htlcs; + + if let Some(msg) = shutdown { + // We can send the `shutdown` message before updating the `ChannelMonitor` + // here as we don't need the monitor update to complete until we send a + // `shutdown_signed`, which we'll delay if we're pending a monitor update. + peer_state.pending_msg_events.push(events::MessageSendEvent::SendShutdown { + node_id: *counterparty_node_id, + msg, + }); + } + // Update the monitor with the shutdown script if necessary. + if let Some(monitor_update) = monitor_update_opt { + handle_new_monitor_update!(self, funding_txo_opt.unwrap(), monitor_update, + peer_state_lock, peer_state, per_peer_state, chan); + } + } else { + let context = phase.context_mut(); + let logger = WithChannelContext::from(&self.logger, context, None); + log_error!(logger, "Immediately closing unfunded channel {} as peer asked to cooperatively shut it down (which is unnecessary)", &msg.channel_id); + let mut close_res = phase.context_mut().force_shutdown(false, ClosureReason::CounterpartyCoopClosedUnfundedChannel); + remove_channel_phase!(self, peer_state, chan_phase_entry, close_res); + finish_shutdown = Some(close_res); } } else { 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)) @@ -8673,7 +8694,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id.clone()) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + if let Some(ref mut chan) = chan_phase_entry.get_mut().funded_channel_mut() { let logger = WithChannelContext::from(&self.logger, &chan.context, None); let (closing_signed, tx, shutdown_result) = try_chan_phase_entry!(self, peer_state, chan.closing_signed(&self.fee_estimator, &msg, &&logger), chan_phase_entry); debug_assert_eq!(shutdown_result.is_some(), chan.is_shutdown()); @@ -8709,7 +8730,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ log_info!(WithContext::from(&self.logger, Some(*counterparty_node_id), channel_id, None), "Broadcasting {}", log_tx!(broadcast_tx)); self.tx_broadcaster.broadcast_transactions(&[&broadcast_tx]); } - if let Some(ChannelPhase::Funded(chan)) = chan_option { + if let Some(Some(chan)) = chan_option.as_ref().map(|phase| phase.funded_channel()) { if let Ok(update) = self.get_channel_update_for_broadcast(&chan) { let mut pending_broadcast_messages = self.pending_broadcast_messages.lock().unwrap(); pending_broadcast_messages.push(events::MessageSendEvent::BroadcastChannelUpdate { @@ -8748,7 +8769,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { let mut pending_forward_info = match decoded_hop_res { Ok((next_hop, shared_secret, next_packet_pk_opt)) => self.construct_pending_htlc_status( @@ -8820,7 +8841,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { let res = try_chan_phase_entry!(self, peer_state, chan.update_fulfill_htlc(&msg), chan_phase_entry); if let HTLCSource::PreviousHopData(prev_hop) = &res.0 { let logger = WithChannelContext::from(&self.logger, &chan.context, None); @@ -8869,7 +8890,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { try_chan_phase_entry!(self, peer_state, chan.update_fail_htlc(&msg, HTLCFailReason::from_msg(msg)), chan_phase_entry); } else { return try_chan_phase_entry!(self, peer_state, Err(ChannelError::close( @@ -8898,7 +8919,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let chan_err = ChannelError::close("Got update_fail_malformed_htlc with BADONION not set".to_owned()); try_chan_phase_entry!(self, peer_state, Err(chan_err), chan_phase_entry); } - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { try_chan_phase_entry!(self, peer_state, chan.update_fail_malformed_htlc(&msg, HTLCFailReason::reason(msg.failure_code, msg.sha256_of_onion.to_vec())), chan_phase_entry); } else { return try_chan_phase_entry!(self, peer_state, Err(ChannelError::close( @@ -8922,7 +8943,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { let logger = WithChannelContext::from(&self.logger, &chan.context, None); let funding_txo = chan.context.get_funding_txo(); @@ -9139,7 +9160,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { let logger = WithChannelContext::from(&self.logger, &chan.context, None); let funding_txo_opt = chan.context.get_funding_txo(); let mon_update_blocked = if let Some(funding_txo) = funding_txo_opt { @@ -9179,7 +9200,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { let logger = WithChannelContext::from(&self.logger, &chan.context, None); try_chan_phase_entry!(self, peer_state, chan.update_fee(&self.fee_estimator, &msg, &&logger), chan_phase_entry); } else { @@ -9203,7 +9224,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { if !chan.context.is_usable() { return Err(MsgHandleErrInternal::from_no_close(LightningError{err: "Got an announcement_signatures before we were ready for it".to_owned(), action: msgs::ErrorAction::IgnoreError})); } @@ -9245,7 +9266,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(chan_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { if chan.context.get_counterparty_node_id() != *counterparty_node_id { if chan.context.should_announce() { // If the announcement is about a channel of ours which is public, some @@ -9296,7 +9317,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { // Currently, we expect all holding cell update_adds to be dropped on peer // disconnect, so Channel's reestablish will never hand us any holding cell // freed HTLCs to fail backwards. If in the future we no longer drop pending @@ -9581,7 +9602,7 @@ 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 PeerState<_> = &mut *peer_state_lock; for (channel_id, chan) in peer_state.channel_by_id.iter_mut().filter_map( - |(chan_id, phase)| if let ChannelPhase::Funded(chan) = phase { Some((chan_id, chan)) } else { None } + |(chan_id, phase)| phase.funded_channel_mut().map(|chan| (chan_id, chan)) ) { let counterparty_node_id = chan.context.get_counterparty_node_id(); let funding_txo = chan.context.get_funding_txo(); @@ -9706,6 +9727,8 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ None }, ChannelPhase::UnfundedInboundV2(_) | ChannelPhase::UnfundedOutboundV2(_) => None, + #[cfg(splicing)] + ChannelPhase::RefundingV2(_) => None, } }; @@ -9757,46 +9780,44 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let peer_state = &mut *peer_state_lock; let pending_msg_events = &mut peer_state.pending_msg_events; peer_state.channel_by_id.retain(|channel_id, phase| { - match phase { - ChannelPhase::Funded(chan) => { - let logger = WithChannelContext::from(&self.logger, &chan.context, None); - match chan.maybe_propose_closing_signed(&self.fee_estimator, &&logger) { - Ok((msg_opt, tx_opt, shutdown_result_opt)) => { - if let Some(msg) = msg_opt { - has_update = true; - pending_msg_events.push(events::MessageSendEvent::SendClosingSigned { - node_id: chan.context.get_counterparty_node_id(), msg, + if let Some(chan) = phase.funded_channel_mut() { + let logger = WithChannelContext::from(&self.logger, &chan.context, None); + match chan.maybe_propose_closing_signed(&self.fee_estimator, &&logger) { + Ok((msg_opt, tx_opt, shutdown_result_opt)) => { + if let Some(msg) = msg_opt { + has_update = true; + pending_msg_events.push(events::MessageSendEvent::SendClosingSigned { + node_id: chan.context.get_counterparty_node_id(), msg, + }); + } + debug_assert_eq!(shutdown_result_opt.is_some(), chan.is_shutdown()); + if let Some(shutdown_result) = shutdown_result_opt { + shutdown_results.push(shutdown_result); + } + if let Some(tx) = tx_opt { + // We're done with this channel. We got a closing_signed and sent back + // a closing_signed with a closing transaction to broadcast. + if let Ok(update) = self.get_channel_update_for_broadcast(&chan) { + let mut pending_broadcast_messages = self.pending_broadcast_messages.lock().unwrap(); + pending_broadcast_messages.push(events::MessageSendEvent::BroadcastChannelUpdate { + msg: update }); } - debug_assert_eq!(shutdown_result_opt.is_some(), chan.is_shutdown()); - if let Some(mut shutdown_result) = shutdown_result_opt { - locked_close_channel!(self, peer_state, &chan.context, shutdown_result); - shutdown_results.push(shutdown_result); - } - if let Some(tx) = tx_opt { - // We're done with this channel. We got a closing_signed and sent back - // a closing_signed with a closing transaction to broadcast. - if let Ok(update) = self.get_channel_update_for_broadcast(&chan) { - let mut pending_broadcast_messages = self.pending_broadcast_messages.lock().unwrap(); - pending_broadcast_messages.push(events::MessageSendEvent::BroadcastChannelUpdate { - msg: update - }); - } - log_info!(logger, "Broadcasting {}", log_tx!(tx)); - self.tx_broadcaster.broadcast_transactions(&[&tx]); - false - } else { true } - }, - Err(e) => { - has_update = true; - let (close_channel, res) = convert_chan_phase_err!(self, peer_state, e, chan, channel_id, FUNDED_CHANNEL); - handle_errors.push((chan.context.get_counterparty_node_id(), Err(res))); - !close_channel - } + log_info!(logger, "Broadcasting {}", log_tx!(tx)); + self.tx_broadcaster.broadcast_transactions(&[&tx]); + false + } else { true } + }, + Err(e) => { + has_update = true; + let (close_channel, res) = convert_chan_phase_err!(self, peer_state, e, chan, channel_id, FUNDED_CHANNEL); + handle_errors.push((chan.context.get_counterparty_node_id(), Err(res))); + !close_channel } - }, - _ => true, // Retain unfunded channels if present. + } + } else { + true // Retain unfunded channels if present. } }); } @@ -10706,7 +10727,7 @@ where let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; for chan in peer_state.channel_by_id.values().filter_map( - |phase| if let ChannelPhase::Funded(chan) = phase { Some(chan) } else { None } + |phase| phase.funded_channel() ) { for (htlc_source, _) in chan.inflight_htlc_sources() { if let HTLCSource::OutboundRoute { path, .. } = htlc_source { @@ -10786,7 +10807,7 @@ where if let hash_map::Entry::Occupied(mut chan_phase_entry) = peer_state.channel_by_id.entry( channel_id) { - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { + if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { debug_assert_eq!(chan.context.get_funding_txo().unwrap(), channel_funding_outpoint); if let Some((monitor_update, further_update_exists)) = chan.unblock_next_blocked_monitor_update() { log_debug!(logger, "Unlocking monitor updating for channel {} and updating monitor", @@ -11082,7 +11103,7 @@ where for (_cp_id, peer_state_mutex) in self.per_peer_state.read().unwrap().iter() { let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; - for chan in peer_state.channel_by_id.values().filter_map(|phase| if let ChannelPhase::Funded(chan) = phase { Some(chan) } else { None }) { + for chan in peer_state.channel_by_id.values().filter_map(|phase| phase.funded_channel()) { let txid_opt = chan.context.get_funding_txo(); let height_opt = chan.context.get_funding_tx_confirmation_height(); let hash_opt = chan.context.get_funding_tx_confirmed_in(); @@ -11139,11 +11160,7 @@ where let pending_msg_events = &mut peer_state.pending_msg_events; peer_state.channel_by_id.retain(|_, phase| { - match phase { - // Retain unfunded channels. - ChannelPhase::UnfundedOutboundV1(_) | ChannelPhase::UnfundedInboundV1(_) | - ChannelPhase::UnfundedOutboundV2(_) | ChannelPhase::UnfundedInboundV2(_) => true, - ChannelPhase::Funded(channel) => { + if let Some(channel) = phase.funded_channel_mut() { let res = f(channel); if let Ok((channel_ready_opt, mut timed_out_pending_htlcs, announcement_sigs)) = res { for (source, payment_hash) in timed_out_pending_htlcs.drain(..) { @@ -11255,7 +11272,9 @@ where return false; } true - } + } else { + // Retain unfunded channels. + true } }); } @@ -11672,6 +11691,15 @@ where } &mut chan.context }, + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => { + let logger = WithChannelContext::from(&self.logger, &chan.context, None); + if chan.remove_uncommitted_htlcs_and_mark_paused(&&logger).is_ok() { + // We only retain funded channels that are not shutdown. + return true; + } + &mut chan.context + }, // If we get disconnected and haven't yet committed to a funding // transaction, we can replay the `open_channel` on reconnection, so don't // bother dropping the channel here. However, if we already committed to @@ -11842,6 +11870,15 @@ where }); } + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => { + let logger = WithChannelContext::from(&self.logger, &chan.context, None); + pending_msg_events.push(events::MessageSendEvent::SendChannelReestablish { + node_id: chan.context.get_counterparty_node_id(), + msg: chan.get_channel_reestablish(&&logger), + }); + } + ChannelPhase::UnfundedOutboundV1(chan) => { let logger = WithChannelContext::from(&self.logger, &chan.context, None); if let Some(msg) = chan.get_open_channel(self.chain_hash, &&logger) { @@ -11895,7 +11932,7 @@ where let peer_state_mutex_opt = per_peer_state.get(&counterparty_node_id); if peer_state_mutex_opt.is_none() { return NotifyOption::SkipPersistNoEvents; } let mut peer_state = peer_state_mutex_opt.unwrap().lock().unwrap(); - if let Some(ChannelPhase::Funded(chan)) = peer_state.channel_by_id.get(&msg.channel_id) { + if let Some(Some(chan)) = peer_state.channel_by_id.get(&msg.channel_id).as_ref().map(|phase| phase.funded_channel()) { if let Some(msg) = chan.get_outbound_shutdown() { peer_state.pending_msg_events.push(events::MessageSendEvent::SendShutdown { node_id: counterparty_node_id, @@ -11972,6 +12009,8 @@ where } }, None | Some(ChannelPhase::UnfundedInboundV1(_) | ChannelPhase::UnfundedInboundV2(_) | ChannelPhase::Funded(_)) => (), + #[cfg(splicing)] + Some(ChannelPhase::RefundingV2(_)) => (), } } @@ -12923,7 +12962,7 @@ where } number_of_funded_channels += peer_state.channel_by_id.iter().filter( - |(_, phase)| if let ChannelPhase::Funded(chan) = phase { chan.context.is_funding_broadcast() } else { false } + |(_, phase)| if let Some(chan) = phase.funded_channel() { chan.context.is_funding_broadcast() } else { false } ).count(); } @@ -12933,7 +12972,7 @@ where let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; for channel in peer_state.channel_by_id.iter().filter_map( - |(_, phase)| if let ChannelPhase::Funded(channel) = phase { + |(_, phase)| if let Some(channel) = phase.funded_channel() { if channel.context.is_funding_broadcast() { Some(channel) } else { None } } else { None } ) { @@ -13742,7 +13781,7 @@ where let mut peer_state_lock = peer_state_mtx.lock().unwrap(); let peer_state = &mut *peer_state_lock; for phase in peer_state.channel_by_id.values() { - if let ChannelPhase::Funded(chan) = phase { + if let Some(chan) = phase.funded_channel() { let logger = WithChannelContext::from(&args.logger, &chan.context, None); // Channels that were persisted have to be funded, otherwise they should have been @@ -14201,7 +14240,7 @@ where let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; for (chan_id, phase) in peer_state.channel_by_id.iter_mut() { - if let ChannelPhase::Funded(chan) = phase { + if let Some(chan) = phase.funded_channel_mut() { let logger = WithChannelContext::from(&args.logger, &chan.context, None); if chan.context.outbound_scid_alias() == 0 { let mut outbound_scid_alias; @@ -14451,7 +14490,7 @@ where let peer_state_mutex = per_peer_state.get(&peer_node_id).unwrap(); let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; - if let Some(ChannelPhase::Funded(channel)) = peer_state.channel_by_id.get_mut(&previous_channel_id) { + if let Some(Some(channel)) = peer_state.channel_by_id.get_mut(&previous_channel_id).as_mut().map(|phase| phase.funded_channel_mut()) { let logger = WithChannelContext::from(&channel_manager.logger, &channel.context, Some(payment_hash)); channel.claim_htlc_while_disconnected_dropping_mon_update_legacy( claimable_htlc.prev_hop.htlc_id, payment_preimage, &&logger diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 477ea7f05fc..c12feae32a7 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -3606,7 +3606,7 @@ macro_rules! get_channel_value_stat { let peer_state_lock = $node.node.per_peer_state.read().unwrap(); let chan_lock = peer_state_lock.get(&$counterparty_node.node.get_our_node_id()).unwrap().lock().unwrap(); let chan = chan_lock.channel_by_id.get(&$channel_id).map( - |phase| if let ChannelPhase::Funded(chan) = phase { Some(chan) } else { None } + |phase| phase.funded_channel() ).flatten().unwrap(); chan.get_value_stat() }} From 7b4c270404a5bc084f0ba92a1e4a265957ffced7 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 13/17] Clone for ChannelContext --- lightning/src/ln/channel.rs | 176 +++++++++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 5 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 3fbefa668b9..d9602442cb5 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -111,6 +111,7 @@ enum FeeUpdateState { Outbound, } +#[derive(Clone)] enum InboundHTLCRemovalReason { FailRelay(msgs::OnionErrorPacket), FailMalformed(([u8; 32], u16)), @@ -145,6 +146,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. @@ -219,6 +221,7 @@ impl From<&InboundHTLCState> for Option { } } +#[derive(Clone)] struct InboundHTLCOutput { htlc_id: u64, amount_msat: u64, @@ -227,7 +230,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 @@ -309,7 +313,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, @@ -322,7 +327,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 @@ -797,7 +803,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`. @@ -1112,6 +1118,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, } @@ -4292,6 +4299,113 @@ impl ChannelContext where SP::Target: SignerProvider { self.get_initial_counterparty_commitment_signature(logger) } + /// Clone, each field, with a few exceptions, notably the channel signer, and + /// a few non-cloneable fields (such as Secp256k1 context) + #[allow(unused)] + fn clone(&self, holder_signer: ::EcdsaSigner) -> Self { + Self { + 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(), + // Create new Secp256k context + secp_ctx: Secp256k1::new(), + channel_value_satoshis: self.channel_value_satoshis, + latest_monitor_update_id: self.latest_monitor_update_id, + // Use provided channel signer + holder_signer: ChannelSignerType::Ecdsa(holder_signer), + 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, + 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: Mutex::new(*self.holder_max_commitment_tx_output.lock().unwrap()), + #[cfg(debug_assertions)] + counterparty_max_commitment_tx_output: Mutex::new(*self.counterparty_max_commitment_tx_output.lock().unwrap()), + 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: Mutex::new(self.next_local_commitment_tx_fee_info_cached.lock().unwrap().clone()), + #[cfg(any(test, fuzzing))] + next_remote_commitment_tx_fee_info_cached: Mutex::new(self.next_remote_commitment_tx_fee_info_cached.lock().unwrap().clone()), + workaround_lnd_bug_4006: self.workaround_lnd_bug_4006.clone(), + sent_message_awaiting_response: self.sent_message_awaiting_response, + #[cfg(any(test, fuzzing))] + historical_inbound_htlc_fulfills: self.historical_inbound_htlc_fulfills.clone(), + 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(), + } + } + /// 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 { @@ -4468,6 +4582,7 @@ pub(super) struct Channel where SP::Target: SignerProvider { } #[cfg(any(test, fuzzing))] +#[derive(Clone)] struct CommitmentTxInfoCached { fee: u64, total_pending_htlcs: usize, @@ -10679,7 +10794,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, Channel, ChannelState, InboundHTLCOutput, OutboundV1Channel, InboundV1Channel, OutboundHTLCOutput, InboundHTLCState, OutboundHTLCState, HTLCCandidate, HTLCInitiator, HTLCUpdateAwaitingACK, commit_tx_fee_sat}; + use crate::ln::channel::{AwaitingChannelReadyFlags, Channel, ChannelContext, ChannelState, 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; @@ -12444,6 +12559,57 @@ mod tests { assert!(node_a_chan.check_get_channel_ready(0, &&logger).is_some()); } + #[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, channel_value_satoshis, user_id); + let holder_signer = signer_provider.derive_channel_signer(channel_value_satoshis, channel_keys_id); + let logger = test_utils::TestLogger::new(); + let pubkeys = holder_signer.pubkeys().clone(); + + // 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, + None, + 100000, + [42; 32], + holder_signer, + pubkeys, + &logger, + ).unwrap(); + + // Clone it + let holder_signer2 = signer_provider.derive_channel_signer(channel_value_satoshis, 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 cdecb8c4b5b9509696c116bef2ec60fdeda52c0b Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:46:46 +0100 Subject: [PATCH 14/17] Add Arc to Mutex fields to make them cloneable --- lightning/src/ln/channel.rs | 53 ++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index d9602442cb5..2f7859e32cd 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -68,7 +68,7 @@ use crate::prelude::*; use core::{cmp,mem,fmt}; use core::ops::Deref; #[cfg(any(test, fuzzing, debug_assertions))] -use crate::sync::Mutex; +use crate::sync::{Arc, Mutex}; use crate::sign::type_resolver::ChannelSignerType; use super::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBasepoint}; @@ -1371,10 +1371,10 @@ pub(super) struct ChannelContext where SP::Target: SignerProvider { #[cfg(debug_assertions)] /// Max to_local and to_remote outputs in a locally-generated commitment transaction - holder_max_commitment_tx_output: Mutex<(u64, u64)>, + holder_max_commitment_tx_output: Arc>, #[cfg(debug_assertions)] /// Max to_local and to_remote outputs in a remote-generated commitment transaction - counterparty_max_commitment_tx_output: Mutex<(u64, u64)>, + counterparty_max_commitment_tx_output: Arc>, // (fee_sats, skip_remote_output, fee_range, holder_sig) last_sent_closing_fee: Option<(u64, bool, ClosingSignedFeeRange, Option)>, @@ -1486,9 +1486,9 @@ pub(super) struct ChannelContext where SP::Target: SignerProvider { // be, by comparing the cached values to the fee of the tranaction generated by // `build_commitment_transaction`. #[cfg(any(test, fuzzing))] - next_local_commitment_tx_fee_info_cached: Mutex>, + next_local_commitment_tx_fee_info_cached: Arc>>, #[cfg(any(test, fuzzing))] - next_remote_commitment_tx_fee_info_cached: Mutex>, + next_remote_commitment_tx_fee_info_cached: Arc>>, /// lnd has a long-standing bug where, upon reconnection, if the channel is not yet confirmed /// they will not send a channel_reestablish until the channel locks in. Then, they will send a @@ -2310,9 +2310,9 @@ impl ChannelContext where SP::Target: SignerProvider { #[cfg(debug_assertions)] - holder_max_commitment_tx_output: Mutex::new((value_to_self_msat, (channel_value_satoshis * 1000 - msg_push_msat).saturating_sub(value_to_self_msat))), + holder_max_commitment_tx_output: Arc::new(Mutex::new((value_to_self_msat, (channel_value_satoshis * 1000 - msg_push_msat).saturating_sub(value_to_self_msat)))), #[cfg(debug_assertions)] - counterparty_max_commitment_tx_output: Mutex::new((value_to_self_msat, (channel_value_satoshis * 1000 - msg_push_msat).saturating_sub(value_to_self_msat))), + counterparty_max_commitment_tx_output: Arc::new(Mutex::new((value_to_self_msat, (channel_value_satoshis * 1000 - msg_push_msat).saturating_sub(value_to_self_msat)))), last_sent_closing_fee: None, last_received_closing_sig: None, @@ -2370,9 +2370,9 @@ impl ChannelContext where SP::Target: SignerProvider { announcement_sigs: None, #[cfg(any(test, fuzzing))] - next_local_commitment_tx_fee_info_cached: Mutex::new(None), + next_local_commitment_tx_fee_info_cached: Arc::new(Mutex::new(None)), #[cfg(any(test, fuzzing))] - next_remote_commitment_tx_fee_info_cached: Mutex::new(None), + next_remote_commitment_tx_fee_info_cached: Arc::new(Mutex::new(None)), workaround_lnd_bug_4006: None, sent_message_awaiting_response: None, @@ -2545,9 +2545,9 @@ impl ChannelContext where SP::Target: SignerProvider { // We'll add our counterparty's `funding_satoshis` to these max commitment output assertions // when we receive `accept_channel2`. #[cfg(debug_assertions)] - holder_max_commitment_tx_output: Mutex::new((channel_value_satoshis * 1000 - push_msat, push_msat)), + holder_max_commitment_tx_output: Arc::new(Mutex::new((channel_value_satoshis * 1000 - push_msat, push_msat))), #[cfg(debug_assertions)] - counterparty_max_commitment_tx_output: Mutex::new((channel_value_satoshis * 1000 - push_msat, push_msat)), + counterparty_max_commitment_tx_output: Arc::new(Mutex::new((channel_value_satoshis * 1000 - push_msat, push_msat))), last_sent_closing_fee: None, last_received_closing_sig: None, @@ -2603,9 +2603,9 @@ impl ChannelContext where SP::Target: SignerProvider { announcement_sigs: None, #[cfg(any(test, fuzzing))] - next_local_commitment_tx_fee_info_cached: Mutex::new(None), + next_local_commitment_tx_fee_info_cached: Arc::new(Mutex::new(None)), #[cfg(any(test, fuzzing))] - next_remote_commitment_tx_fee_info_cached: Mutex::new(None), + next_remote_commitment_tx_fee_info_cached: Arc::new(Mutex::new(None)), workaround_lnd_bug_4006: None, sent_message_awaiting_response: None, @@ -4299,11 +4299,13 @@ impl ChannelContext where SP::Target: SignerProvider { self.get_initial_counterparty_commitment_signature(logger) } - /// Clone, each field, with a few exceptions, notably the channel signer, and - /// a few non-cloneable fields (such as Secp256k1 context) + /// 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, @@ -4312,12 +4314,9 @@ impl ChannelContext where SP::Target: SignerProvider { temporary_channel_id: self.temporary_channel_id, channel_state: self.channel_state, announcement_sigs_state: self.announcement_sigs_state.clone(), - // Create new Secp256k context - secp_ctx: Secp256k1::new(), + secp_ctx: self.secp_ctx.clone(), channel_value_satoshis: self.channel_value_satoshis, latest_monitor_update_id: self.latest_monitor_update_id, - // Use provided channel signer - holder_signer: ChannelSignerType::Ecdsa(holder_signer), shutdown_scriptpubkey: self.shutdown_scriptpubkey.clone(), destination_script: self.destination_script.clone(), holder_commitment_point: self.holder_commitment_point, @@ -4347,9 +4346,9 @@ impl ChannelContext where SP::Target: SignerProvider { update_time_counter: self.update_time_counter, // Create new mutex with copied values #[cfg(debug_assertions)] - holder_max_commitment_tx_output: Mutex::new(*self.holder_max_commitment_tx_output.lock().unwrap()), + holder_max_commitment_tx_output: self.holder_max_commitment_tx_output.clone(), #[cfg(debug_assertions)] - counterparty_max_commitment_tx_output: Mutex::new(*self.counterparty_max_commitment_tx_output.lock().unwrap()), + 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, @@ -4386,9 +4385,9 @@ impl ChannelContext where SP::Target: SignerProvider { announcement_sigs: self.announcement_sigs, // Create new mutex with copied values #[cfg(any(test, fuzzing))] - next_local_commitment_tx_fee_info_cached: Mutex::new(self.next_local_commitment_tx_fee_info_cached.lock().unwrap().clone()), + 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: Mutex::new(self.next_remote_commitment_tx_fee_info_cached.lock().unwrap().clone()), + 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, #[cfg(any(test, fuzzing))] @@ -10692,9 +10691,9 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, u32, &'c Ch feerate_per_kw, #[cfg(debug_assertions)] - holder_max_commitment_tx_output: Mutex::new((0, 0)), + holder_max_commitment_tx_output: Arc::new(Mutex::new((0, 0))), #[cfg(debug_assertions)] - counterparty_max_commitment_tx_output: Mutex::new((0, 0)), + counterparty_max_commitment_tx_output: Arc::new(Mutex::new((0, 0))), last_sent_closing_fee: None, last_received_closing_sig: None, @@ -10739,9 +10738,9 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, u32, &'c Ch announcement_sigs, #[cfg(any(test, fuzzing))] - next_local_commitment_tx_fee_info_cached: Mutex::new(None), + next_local_commitment_tx_fee_info_cached: Arc::new(Mutex::new(None)), #[cfg(any(test, fuzzing))] - next_remote_commitment_tx_fee_info_cached: Mutex::new(None), + next_remote_commitment_tx_fee_info_cached: Arc::new(Mutex::new(None)), workaround_lnd_bug_4006: None, sent_message_awaiting_response: None, From 0452695150bda58050400e295310a8b3b3656de9 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 15/17] Sort out PendingSplicePre and -Post, new spliced for ChannelContext and OutboundV2Channel --- lightning/src/ln/channel.rs | 283 ++++++++++++++++++++++++++++++++++-- 1 file changed, 273 insertions(+), 10 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 2f7859e32cd..534dfb3ec88 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}; @@ -1221,7 +1223,7 @@ impl UnfundedChannelContext { /// Info about a pending splice, used in the pre-splice channel #[cfg(splicing)] #[derive(Clone)] -struct PendingSplice { +struct PendingSplicePre { pub our_funding_contribution: i64, pub funding_feerate_perkw: u32, pub locktime: u32, @@ -1230,7 +1232,7 @@ struct PendingSplice { } #[cfg(splicing)] -impl PendingSplice { +impl PendingSplicePre { #[inline] fn add_checked(base: u64, delta: i64) -> u64 { if delta >= 0 { @@ -1246,6 +1248,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, @@ -4405,6 +4474,81 @@ impl ChannelContext where SP::Target: SignerProvider { } } + /// Create channel context for spliced channel, by duplicating and updating the context. + /// relative_satoshis: The change in channel value (sats), + /// positive for increase (splice-in), negative for decrease (splice out). + /// delta_belongs_to_local: + /// The amount from the channel value change that belongs to the local (sats). + /// Its sign has to be the same as the sign of relative_satoshis, and its absolute value + /// less or equal (e.g. for +100 in the range of 0..100, for -100 in the range of -100..0). + #[cfg(splicing)] + fn new_for_splice( + pre_splice_context: &Self, + is_outgoing: bool, + counterparty_funding_pubkey: &PublicKey, + our_funding_contribution: i64, + their_funding_contribution: i64, + holder_signer: ::EcdsaSigner, + logger: &L, + ) -> Result, ChannelError> where L::Target: Logger + { + let pre_channel_value = pre_splice_context.channel_value_satoshis; + let post_channel_value = PendingSplicePre::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution); + + // Compute our new balance + let old_to_self = pre_splice_context.value_to_self_msat; + let delta_in_value_to_self = our_funding_contribution * 1000; + if delta_in_value_to_self < 0 && delta_in_value_to_self.abs() as u64 > old_to_self { + // Change would make our balance negative + return Err(ChannelError::Warn(format!("Cannot decrease channel value to requested amount, too low, {} {} {} {} {}", + pre_channel_value, post_channel_value, our_funding_contribution, their_funding_contribution, old_to_self))); + } + let value_to_self_msat = (old_to_self as i64).saturating_add(delta_in_value_to_self) as u64; + + let mut context = pre_splice_context.clone(holder_signer); + + // New channel value + context.channel_value_satoshis = post_channel_value; + // Update value to self + context.value_to_self_msat = value_to_self_msat; + // Reset funding + context.funding_transaction = None; + context.funding_tx_confirmed_in = None; + context.funding_tx_confirmation_height = 0; + context.channel_transaction_parameters.funding_outpoint = None; + // Reset state + context.channel_state = ChannelState::NegotiatingFunding( + if is_outgoing { NegotiatingFundingFlags::OUR_INIT_SENT } else { NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT } + ); + context.next_funding_txid = None; + // Reset monitor update + context.latest_monitor_update_id = 0; + // Note on commitment transaction numbers and commitment points: + // we could step 'back' here (i.e. increase number by one, set cur to prev), but that does not work, + // because latest commitment point would be lost. + // Instead, we take the previous values in relevant cases when splicing is pending. + // We'll add our counterparty's `funding_satoshis` to these max commitment output assertions + // Clear these state flags, for sending `ChannelPending` and `ChannelReady` again + context.channel_pending_event_emitted = false; + context.channel_ready_event_emitted = false; + #[cfg(debug_assertions)] + { + context.holder_max_commitment_tx_output = Arc::new(Mutex::new((value_to_self_msat, post_channel_value.saturating_sub(value_to_self_msat)))); + context.counterparty_max_commitment_tx_output = Arc::new(Mutex::new((value_to_self_msat, post_channel_value.saturating_sub(value_to_self_msat)))); + } + // Reset + context.blocked_monitor_updates = Vec::new(); + + log_debug!(logger, "Splicing channel context: value {} old {}, dir {}, value to self {}, funding keys local {} cp {}", + context.channel_value_satoshis, pre_channel_value, + if is_outgoing { "outgoing" } else { "incoming" }, + context.value_to_self_msat, + context.channel_transaction_parameters.holder_pubkeys.funding_pubkey, counterparty_funding_pubkey + ); + + Ok(context) + } + /// Splice process starting; update state, log, etc. #[cfg(splicing)] pub(crate) fn splice_start(&mut self, is_outgoing: bool, logger: &L) where L::Target: Logger { @@ -4577,7 +4721,10 @@ pub(super) struct Channel where SP::Target: SignerProvider { holder_commitment_point: HolderCommitmentPoint, /// 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))] @@ -8190,6 +8337,15 @@ impl Channel where } } + /// Check is a splice is currently in progress + /// Can be called regardless of `splicing` configuration. TODO: remove this note once `cfg(splicing)` is being removed + pub fn is_splice_pending(&self) -> bool { + #[cfg(splicing)] + return self.pending_splice_post.is_some(); + #[cfg(not(splicing))] + false + } + /// Initiate splicing #[cfg(splicing)] pub fn splice_channel(&mut self, our_funding_contribution_satoshis: i64, @@ -8242,7 +8398,7 @@ impl Channel where ))); } - self.pending_splice_pre = Some(PendingSplice { + self.pending_splice_pre = Some(PendingSplicePre { our_funding_contribution: our_funding_contribution_satoshis, funding_feerate_perkw, locktime, @@ -8293,8 +8449,8 @@ impl Channel where ))); } - let post_channel_value = PendingSplice::compute_post_value(pre_channel_value, their_funding_contribution_satoshis, our_funding_contribution_satoshis); - let post_balance = PendingSplice::add_checked(self.context.value_to_self_msat, our_funding_contribution_satoshis); + let post_channel_value = PendingSplicePre::compute_post_value(pre_channel_value, their_funding_contribution_satoshis, our_funding_contribution_satoshis); + let post_balance = PendingSplicePre::add_checked(self.context.value_to_self_msat, our_funding_contribution_satoshis); // Early check for reserve requirement, assuming maximum balance of full channel value // This will also be checked later at tx_complete let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?; @@ -8330,8 +8486,8 @@ impl Channel where let our_funding_contribution = pending_splice.our_funding_contribution; let pre_channel_value = self.context.get_value_satoshis(); - let post_channel_value = PendingSplice::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution_satoshis); - let post_balance = PendingSplice::add_checked(self.context.value_to_self_msat, our_funding_contribution); + let post_channel_value = PendingSplicePre::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution_satoshis); + let post_balance = PendingSplicePre::add_checked(self.context.value_to_self_msat, our_funding_contribution); // Early check for reserve requirement, assuming maximum balance of full channel value // This will also be checked later at tx_complete let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?; @@ -9041,6 +9197,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() @@ -9308,6 +9466,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; @@ -9336,6 +9496,29 @@ impl InboundV1Channel where SP::Target: SignerProvider { } } +/// Calculate funding values for interactive tx for splicing, based on channel value changes +#[cfg(splicing)] +fn calculate_funding_values( + pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64, is_initiator: bool, +) -> Result<(u64, u64), ChannelError> { + // Initiator also adds the previous funding as input + let mut our_contribution_with_prev = our_funding_contribution; + let mut their_contribution_with_prev = their_funding_contribution; + if is_initiator { + our_contribution_with_prev = our_contribution_with_prev.saturating_add(pre_channel_value as i64); + } else { + their_contribution_with_prev = their_contribution_with_prev.saturating_add(pre_channel_value as i64); + } + if our_contribution_with_prev < 0 || their_contribution_with_prev < 0 { + return Err(ChannelError::Warn(format!( + "Funding contribution cannot be negative! ours {} theirs {} pre {} initiator {} acceptor {}", + our_contribution_with_prev, their_contribution_with_prev, pre_channel_value, + our_funding_contribution, their_funding_contribution + ))); + } + Ok((our_contribution_with_prev.abs() as u64, their_contribution_with_prev.abs() as u64)) +} + // A not-yet-funded outbound (from holder) channel using V2 channel establishment. pub(super) struct OutboundV2Channel where SP::Target: SignerProvider { pub context: ChannelContext, @@ -9343,6 +9526,9 @@ pub(super) struct OutboundV2Channel where SP::Target: SignerProvider pub dual_funding_context: DualFundingChannelContext, /// The current interactive transaction construction session under negotiation. interactive_tx_constructor: Option, + /// Info about an in-progress, pending splice (if any), on the post-splice channel + #[cfg(splicing)] + pending_splice_post: Option, } impl OutboundV2Channel where SP::Target: SignerProvider { @@ -9408,10 +9594,81 @@ impl OutboundV2Channel where SP::Target: SignerProvider { our_funding_inputs: funding_inputs, }, interactive_tx_constructor: None, + pending_splice_post: None, }; Ok(chan) } + /// Create new channel for splicing + #[cfg(splicing)] + pub fn new_spliced( + is_outbound: bool, + pre_splice_channel: &mut Channel, + signer_provider: &SP, + counterparty_funding_pubkey: &PublicKey, + our_funding_contribution: i64, + their_funding_contribution: i64, + funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, + funding_tx_locktime: LockTime, + funding_feerate_sat_per_1000_weight: u32, + logger: &L, + ) -> Result where L::Target: Logger + { + if pre_splice_channel.is_splice_pending() { + return Err(ChannelError::Warn(format!("Internal error: Channel is already splicing, channel_id {}", pre_splice_channel.context.channel_id))); + } + + let pre_channel_value = pre_splice_channel.context.get_value_satoshis(); + + // Save the current funding transaction + let pre_funding_transaction = pre_splice_channel.context.funding_transaction.clone(); + let pre_funding_txo = pre_splice_channel.context.get_funding_txo().clone(); + + let pending_splice_post = PendingSplicePost::new( + pre_channel_value, our_funding_contribution, their_funding_contribution, + pre_funding_transaction, pre_funding_txo, + ); + let post_channel_value = pending_splice_post.post_channel_value(); + + // Create new signer, using the new channel value. + // Note: channel_keys_id is not changed + let holder_signer = signer_provider.derive_channel_signer(post_channel_value, pre_splice_channel.context.channel_keys_id); + + let context = ChannelContext::new_for_splice( + &pre_splice_channel.context, + true, + counterparty_funding_pubkey, + our_funding_contribution, + their_funding_contribution, + holder_signer, + logger, + )?; + + let (our_funding_satoshis, their_funding_satoshis) = calculate_funding_values( + pre_channel_value, + our_funding_contribution, + their_funding_contribution, + is_outbound, + )?; + + let dual_funding_context = DualFundingChannelContext { + our_funding_satoshis, + their_funding_satoshis: Some(their_funding_satoshis), + funding_tx_locktime, + funding_feerate_sat_per_1000_weight, + our_funding_inputs: Some(funding_inputs), + }; + let unfunded_context = UnfundedChannelContext::default(); + let post_chan = Self { + context, + dual_funding_context, + unfunded_context, + interactive_tx_constructor: None, + pending_splice_post: Some(pending_splice_post), + }; + Ok(post_chan) + } + /// If we receive an error message, it may only be a rejection of the channel type we tried, /// not of our ability to open any channel at all. Thus, on error, we should first call this /// and see if we get a new `OpenChannelV2` message, otherwise the channel is failed. @@ -9485,6 +9742,8 @@ impl OutboundV2Channel where SP::Target: SignerProvider { holder_commitment_point, #[cfg(splicing)] pending_splice_pre: None, + #[cfg(splicing)] + pending_splice_post: self.pending_splice_post, }; Ok(channel) @@ -9693,6 +9952,8 @@ impl InboundV2Channel where SP::Target: SignerProvider { holder_commitment_point, #[cfg(splicing)] pending_splice_pre: None, + #[cfg(splicing)] + pending_splice_post: None, }; Ok(channel) @@ -10775,6 +11036,8 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, u32, &'c Ch holder_commitment_point, #[cfg(splicing)] pending_splice_pre: None, + #[cfg(splicing)] + pending_splice_post: None, }) } } @@ -12611,9 +12874,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 725021f7ad3776d4e3d24d422c4eb396b8092315 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:37:23 +0100 Subject: [PATCH 16/17] Sort out PendingSplicePre and -Post, new spliced for ChannelContext and OutboundV2Channel --- lightning/src/ln/channel.rs | 94 +++++++++++++++++++-- lightning/src/ln/functional_tests_splice.rs | 12 ++- 2 files changed, 96 insertions(+), 10 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 534dfb3ec88..d62606e52fa 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -4388,7 +4388,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(), @@ -4407,6 +4406,7 @@ impl ChannelContext where SP::Target: SignerProvider { 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, @@ -4557,7 +4557,7 @@ impl ChannelContext where SP::Target: SignerProvider { // 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 {}", + log_info!(logger, "Splicing process started, new channel value {}, outgoing {}, channel_id {}", self.channel_value_satoshis, is_outgoing, self.channel_id); } @@ -9594,6 +9594,7 @@ impl OutboundV2Channel where SP::Target: SignerProvider { our_funding_inputs: funding_inputs, }, interactive_tx_constructor: None, + #[cfg(splicing)] pending_splice_post: None, }; Ok(chan) @@ -9603,7 +9604,7 @@ impl OutboundV2Channel where SP::Target: SignerProvider { #[cfg(splicing)] pub fn new_spliced( is_outbound: bool, - pre_splice_channel: &mut Channel, + pre_splice_channel: &Channel, signer_provider: &SP, counterparty_funding_pubkey: &PublicKey, our_funding_contribution: i64, @@ -9656,9 +9657,12 @@ impl OutboundV2Channel where SP::Target: SignerProvider { their_funding_satoshis: Some(their_funding_satoshis), funding_tx_locktime, funding_feerate_sat_per_1000_weight, - our_funding_inputs: Some(funding_inputs), + 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 unfunded_context = UnfundedChannelContext::default(); let post_chan = Self { context, dual_funding_context, @@ -9757,6 +9761,9 @@ pub(super) struct InboundV2Channel where SP::Target: SignerProvider { pub dual_funding_context: DualFundingChannelContext, /// The current interactive transaction construction session under negotiation. interactive_tx_constructor: Option, + /// Info about an in-progress, pending splice (if any), on the post-splice channel + #[cfg(splicing)] + pending_splice_post: Option, } impl InboundV2Channel where SP::Target: SignerProvider { @@ -9865,9 +9872,84 @@ impl InboundV2Channel where SP::Target: SignerProvider { dual_funding_context, interactive_tx_constructor, unfunded_context, + #[cfg(splicing)] + pending_splice_post: None, }) } + /// Create new channel for splicing + #[cfg(splicing)] + pub fn new_spliced( + is_outbound: bool, + pre_splice_channel: &Channel, + signer_provider: &SP, + counterparty_funding_pubkey: &PublicKey, + our_funding_contribution: i64, + their_funding_contribution: i64, + funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, + funding_tx_locktime: LockTime, + funding_feerate_sat_per_1000_weight: u32, + logger: &L, + ) -> Result where L::Target: Logger + { + if pre_splice_channel.is_splice_pending() { + return Err(ChannelError::Warn(format!("Internal error: Channel is already splicing, channel_id {}", pre_splice_channel.context.channel_id))); + } + + let pre_channel_value = pre_splice_channel.context.get_value_satoshis(); + + // Save the current funding transaction + let pre_funding_transaction = pre_splice_channel.context.funding_transaction.clone(); + let pre_funding_txo = pre_splice_channel.context.get_funding_txo().clone(); + + let pending_splice_post = PendingSplicePost::new( + pre_channel_value, our_funding_contribution, their_funding_contribution, + pre_funding_transaction, pre_funding_txo, + ); + let post_channel_value = pending_splice_post.post_channel_value(); + + // Create new signer, using the new channel value. + // Note: channel_keys_id is not changed + let holder_signer = signer_provider.derive_channel_signer(post_channel_value, pre_splice_channel.context.channel_keys_id); + + let context = ChannelContext::new_for_splice( + &pre_splice_channel.context, + false, + 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, + dual_funding_context, + unfunded_context, + interactive_tx_constructor: None, + pending_splice_post: Some(pending_splice_post), + }; + Ok(post_chan) + } + /// Marks an inbound channel as accepted and generates a [`msgs::AcceptChannelV2`] message which /// should be sent back to the counterparty node. /// @@ -9953,7 +10035,7 @@ impl InboundV2Channel where SP::Target: SignerProvider { #[cfg(splicing)] pending_splice_pre: None, #[cfg(splicing)] - pending_splice_post: None, + pending_splice_post: self.pending_splice_post, }; Ok(channel) diff --git a/lightning/src/ln/functional_tests_splice.rs b/lightning/src/ln/functional_tests_splice.rs index 061d509cdf7..824a7cfebcc 100644 --- a/lightning/src/ln/functional_tests_splice.rs +++ b/lightning/src/ln/functional_tests_splice.rs @@ -221,6 +221,10 @@ fn test_v1_splice_in() { // ==== 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 println!("Start of Splicing ..., channel_id {}", channel_id2); @@ -276,8 +280,8 @@ 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!(channel.funding_txo.is_some()); + assert_eq!(channel.outbound_capacity_msat, exp_balance2); + assert_eq!(channel.funding_txo.unwrap().txid, funding_tx.compute_txid()); assert!(channel.confirmations.unwrap() > 0); } @@ -295,9 +299,9 @@ fn test_v1_splice_in() { assert_eq!(channel.channel_value_satoshis, channel_value_sat); assert_eq!( channel.outbound_capacity_msat, - 1000 * (channel_value_sat - channel_reserve_amnt_sat) + exp_balance1 - 1000 * channel_reserve_amnt_sat ); - assert!(channel.funding_txo.is_some()); + assert_eq!(channel.funding_txo.unwrap().txid, funding_tx.compute_txid()); assert!(channel.confirmations.unwrap() > 0); } From 8cc45e49d7dc2893f2dc70b6dbb438b5030c1f1d Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:06:20 +0100 Subject: [PATCH 17/17] Perform interactive tx negotiation during splicing --- lightning/src/ln/channel.rs | 313 ++++++++++++++++---- lightning/src/ln/channelmanager.rs | 199 ++++++++++--- lightning/src/ln/functional_tests_splice.rs | 48 ++- lightning/src/ln/interactivetxs.rs | 2 +- 4 files changed, 457 insertions(+), 105 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index d62606e52fa..b8550251a2c 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, }; @@ -1141,7 +1141,7 @@ pub(super) enum ChannelPhase where SP::Target: SignerProvider { Funded(Channel), #[cfg(splicing)] /// Used during splicing, channel is funded but a new funding is being renegotiated. - RefundingV2(Channel), + RefundingV2(SplicingChannel), } impl<'a, SP: Deref> ChannelPhase where @@ -1156,7 +1156,7 @@ impl<'a, SP: Deref> ChannelPhase where ChannelPhase::UnfundedOutboundV2(chan) => &chan.context, ChannelPhase::UnfundedInboundV2(chan) => &chan.context, #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => &chan.context, + ChannelPhase::RefundingV2(chan) => &chan.pre_funded.context, } } @@ -1168,7 +1168,7 @@ impl<'a, SP: Deref> ChannelPhase where ChannelPhase::UnfundedOutboundV2(ref mut chan) => &mut chan.context, ChannelPhase::UnfundedInboundV2(ref mut chan) => &mut chan.context, #[cfg(splicing)] - ChannelPhase::RefundingV2(ref mut chan) => &mut chan.context, + ChannelPhase::RefundingV2(ref mut chan) => &mut chan.pre_funded.context, } } @@ -1176,7 +1176,7 @@ impl<'a, SP: Deref> ChannelPhase where match self { ChannelPhase::Funded(chan) => Some(&chan), #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => Some(&chan), + ChannelPhase::RefundingV2(chan) => Some(&chan.pre_funded), _ => None } } @@ -1185,12 +1185,65 @@ impl<'a, SP: Deref> ChannelPhase where match self { ChannelPhase::Funded(ref mut chan) => Some(chan), #[cfg(splicing)] - ChannelPhase::RefundingV2(ref mut chan) => Some(chan), + ChannelPhase::RefundingV2(ref mut chan) => Some(&mut chan.pre_funded), _ => None } } } +/// Struct holding together various state dureing splicing negotiation +#[cfg(splicing)] +pub(super) struct SplicingChannel where SP::Target: SignerProvider { + pub pre_funded: Channel, + pub post_pending: PendingV2Channel, + pub post_funded: Option>, +} + +#[cfg(splicing)] +impl SplicingChannel where SP::Target: SignerProvider { + pub(super) fn new(pre_funded: Channel, post_pending: PendingV2Channel) -> Self { + Self { + pre_funded, + post_pending, + post_funded: None, + } + } + + 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 { + self.post_pending.splice_init(msg, signer_provider, entropy_source, holder_node_id, logger) + } + + 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 { + self.post_pending.splice_ack(msg, our_funding_contribution, signer_provider, entropy_source, holder_node_id, logger) + } + + pub fn tx_add_input(&mut self, msg: &msgs::TxAddInput) -> InteractiveTxMessageSendResult { + self.post_pending.tx_add_input(msg) + } + + pub fn tx_add_output(&mut self, msg: &msgs::TxAddOutput)-> InteractiveTxMessageSendResult { + self.post_pending.tx_add_output(msg) + } + + pub fn tx_complete(&mut self, msg: &msgs::TxComplete) -> HandleTxCompleteResult { + self.post_pending.tx_complete(msg) + } + + pub fn funding_tx_constructed( + &mut self, signing_session: &mut InteractiveTxSigningSession, logger: &L + ) -> Result<(msgs::CommitmentSigned, Option), ChannelError> where L::Target: Logger { + self.post_pending.funding_tx_constructed(signing_session, logger) + } + + pub fn into_channel(self, signing_session: InteractiveTxSigningSession) -> Result, ChannelError>{ + self.post_pending.into_channel(signing_session) + } +} + /// 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 @@ -1223,7 +1276,7 @@ impl UnfundedChannelContext { /// 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_perkw: u32, pub locktime: u32, @@ -4649,6 +4702,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)) + } +} + pub(super) fn calculate_our_funding_satoshis( is_initiator: bool, funding_inputs: &[(TxIn, TransactionU16LenLimited)], total_witness_weight: Weight, funding_feerate_sat_per_1000_weight: u32, @@ -4721,7 +4822,7 @@ pub(super) struct Channel where SP::Target: SignerProvider { holder_commitment_point: HolderCommitmentPoint, /// Info about an in-progress, pending splice (if any), on the pre-splice channel #[cfg(splicing)] - pending_splice_pre: Option, + pub pending_splice_pre: Option, /// Info about an in-progress, pending splice (if any), on the post-splice channel #[cfg(splicing)] pending_splice_post: Option, @@ -8409,11 +8510,11 @@ impl Channel where Ok(msg) } - /// Handle splice_init + /// Checks during handling splice_init #[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 { + pub fn splice_init_checks( + &mut self, msg: &msgs::SpliceInit, _signer_provider: &SP, _entropy_source: &ES, _holder_node_id: PublicKey, + ) -> Result<(), ChannelError> where ES::Target: EntropySource { 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; @@ -8454,52 +8555,7 @@ impl Channel where // Early check for reserve requirement, assuming maximum balance of full channel value // This will also be checked later at tx_complete let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?; - - // TODO(splicing): Store msg.funding_pubkey - - // Apply start of splice change in the state - self.context.splice_start(false, logger); - - let splice_ack_msg = self.context.get_splice_ack(our_funding_contribution_satoshis); - - // 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, _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_pre { - pending_splice - } else { - return Err(ChannelError::Warn(format!("Channel is not in pending splice"))); - }; - - let our_funding_contribution = pending_splice.our_funding_contribution; - - let pre_channel_value = self.context.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.context.value_to_self_msat, our_funding_contribution); - // Early check for reserve requirement, assuming maximum balance of full channel value - // This will also be checked later at tx_complete - let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?; - - // Apply start of splice change in the state - self.context.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(None) + Ok(()) } // Send stuff to our remote peers: @@ -9752,6 +9808,39 @@ impl OutboundV2Channel where SP::Target: SignerProvider { Ok(channel) } + + /// 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.context.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.context.value_to_self_msat, our_funding_contribution); + // Early check for reserve requirement, assuming maximum balance of full channel value + // This will also be checked later at tx_complete + let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?; + + // 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, 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, Some(prev_funding_input)) + .map_err(|err| ChannelError::Warn(format!("V2 channel rejected due to sender error, {:?}", err)))?; + Ok(tx_msg_opt) + } } // A not-yet-funded inbound (from counterparty) channel using V2 channel establishment. @@ -10040,6 +10129,114 @@ impl InboundV2Channel where SP::Target: SignerProvider { Ok(channel) } + + /// 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, logger); + + let splice_ack_msg = self.context.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, None) + .map_err(|err| ChannelError::Warn(format!("Failed to start interactive transaction construction, {:?}", err)))?; + + Ok(splice_ack_msg) + } +} + +/// Enum to tie together InboundV2Channel and OutboundV2Channel +/// Note: those two structs could be merged into one with an additional is_outbound field. +#[cfg(splicing)] +pub(super) enum PendingV2Channel where SP::Target: SignerProvider { + Inbound(InboundV2Channel), + Outbound(OutboundV2Channel), +} + +#[cfg(splicing)] +impl InteractivelyFunded for PendingV2Channel where SP::Target: SignerProvider { + fn context(&self) -> &ChannelContext { + match self { + PendingV2Channel::Inbound(chan) => &chan.context, + PendingV2Channel::Outbound(chan) => &chan.context, + } + } + fn context_mut(&mut self) -> &mut ChannelContext { + match self { + PendingV2Channel::Inbound(chan) => &mut chan.context, + PendingV2Channel::Outbound(chan) => &mut chan.context, + } + } + fn dual_funding_context(&self) -> &DualFundingChannelContext { + match self { + PendingV2Channel::Inbound(chan) => &chan.dual_funding_context, + PendingV2Channel::Outbound(chan) => &chan.dual_funding_context, + } + } + fn dual_funding_context_mut(&mut self) -> &mut DualFundingChannelContext { + match self { + PendingV2Channel::Inbound(chan) => &mut chan.dual_funding_context, + PendingV2Channel::Outbound(chan) => &mut chan.dual_funding_context, + } + } + fn unfunded_context(&self) -> &UnfundedChannelContext { + match self { + PendingV2Channel::Inbound(chan) => &chan.unfunded_context, + PendingV2Channel::Outbound(chan) => &chan.unfunded_context, + } + } + fn interactive_tx_constructor_mut(&mut self) -> &mut Option { + match self { + PendingV2Channel::Inbound(chan) => &mut chan.interactive_tx_constructor, + PendingV2Channel::Outbound(chan) => &mut chan.interactive_tx_constructor, + } + } + fn is_initiator(&self) -> bool { + match &self { + PendingV2Channel::Inbound(_) => false, + PendingV2Channel::Outbound(_) => true, + } + } +} + +#[cfg(splicing)] +impl PendingV2Channel where SP::Target: SignerProvider { + /// Handle splice_init + /// See also [`splice_init_checks`] + 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 { + match self { + PendingV2Channel::Inbound(chan) => chan.splice_init(msg, signer_provider, entropy_source, holder_node_id, logger), + PendingV2Channel::Outbound(_) => Err(ChannelError::Warn(format!("Splice_init on an outbound channel"))), + } + } + + /// Handle splice_ack + 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 { + match self { + PendingV2Channel::Inbound(_) => Err(ChannelError::Warn(format!("Splice_ack on an inbound channel"))), + PendingV2Channel::Outbound(chan) => chan.splice_ack(msg, our_funding_contribution, signer_provider, entropy_source, holder_node_id, logger), + } + } + + pub fn into_channel(self, signing_session: InteractiveTxSigningSession) -> Result, ChannelError>{ + match self { + PendingV2Channel::Inbound(chan) => chan.into_channel(signing_session), + PendingV2Channel::Outbound(chan) => chan.into_channel(signing_session), + } + } } // Unfunded channel utilities diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 8c25b49206a..04108eae15a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -27,6 +27,8 @@ use bitcoin::hashes::{Hash, HashEngine, HmacEngine}; use bitcoin::hashes::hmac::Hmac; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hash_types::{BlockHash, Txid}; +#[cfg(splicing)] +use bitcoin::locktime::absolute::LockTime; use bitcoin::secp256k1::{SecretKey,PublicKey}; use bitcoin::secp256k1::Secp256k1; @@ -51,6 +53,8 @@ use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::ln::channel::{self, Channel, ChannelPhase, ChannelError, ChannelUpdateStatus, ShutdownResult, UpdateFulfillCommitFetch, OutboundV1Channel, InboundV1Channel, WithChannelContext, InteractivelyFunded as _}; #[cfg(dual_funding)] use crate::ln::channel::InboundV2Channel; +#[cfg(splicing)] +use crate::ln::channel::{InboundV2Channel, OutboundV2Channel, PendingV2Channel, SplicingChannel}; use crate::ln::channel_state::ChannelDetails; use crate::types::features::{Bolt12InvoiceFeatures, ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures}; #[cfg(any(feature = "_test_utils", test))] @@ -3058,7 +3062,7 @@ macro_rules! convert_chan_phase_err { }, #[cfg(splicing)] ChannelPhase::RefundingV2(channel) => { - convert_chan_phase_err!($self, $peer_state, $err, channel, $channel_id, FUNDED_CHANNEL) + convert_chan_phase_err!($self, $peer_state, $err, &mut channel.pre_funded, $channel_id, FUNDED_CHANNEL) }, } }; @@ -7898,7 +7902,8 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } }, #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => { + ChannelPhase::RefundingV2(channel) => { + let chan = &channel.pre_funded; // This covers non-zero-conf inbound `Channel`s that we are currently monitoring, but those // which have not yet had any confirmations on-chain. if !chan.context.is_outbound() && chan.context.minimum_depth().unwrap_or(1) != 0 && @@ -8320,6 +8325,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ChannelPhase::UnfundedOutboundV2(ref mut channel) => { Ok(channel.tx_add_input(msg).into_msg_send_event(counterparty_node_id)) }, + #[cfg(splicing)] + ChannelPhase::RefundingV2(ref mut channel) => { + Ok(channel.tx_add_input(msg).into_msg_send_event(counterparty_node_id)) + } _ => Err("tx_add_input"), } }) @@ -8334,6 +8343,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ChannelPhase::UnfundedOutboundV2(ref mut channel) => { Ok(channel.tx_add_output(msg).into_msg_send_event(counterparty_node_id)) }, + #[cfg(splicing)] + ChannelPhase::RefundingV2(ref mut channel) => { + Ok(channel.tx_add_output(msg).into_msg_send_event(counterparty_node_id)) + } _ => Err("tx_add_output"), } }) @@ -8386,6 +8399,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ .into_msg_send_event_or_signing_session(counterparty_node_id), ChannelPhase::UnfundedOutboundV2(channel) => channel.tx_complete(msg) .into_msg_send_event_or_signing_session(counterparty_node_id), + #[cfg(splicing)] + ChannelPhase::RefundingV2(channel) => channel.tx_complete(msg) + .into_msg_send_event_or_signing_session(counterparty_node_id), _ => try_chan_phase_entry!(self, peer_state, Err(ChannelError::Close( ( "Got a tx_complete message with no interactive transaction construction expected or in-progress".into(), @@ -8403,6 +8419,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ChannelPhase::UnfundedInboundV2(chan) => { chan.funding_tx_constructed(&mut signing_session, &self.logger) }, + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => { + chan.funding_tx_constructed(&mut signing_session, &self.logger) + } _ => Err(ChannelError::Warn( "Got a tx_complete message with no interactive transaction construction expected or in-progress" .into())), @@ -8411,6 +8431,8 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let channel = match channel_phase { ChannelPhase::UnfundedOutboundV2(chan) => chan.into_channel(signing_session), ChannelPhase::UnfundedInboundV2(chan) => chan.into_channel(signing_session), + #[cfg(splicing)] + ChannelPhase::RefundingV2(chan) => chan.into_channel(signing_session), _ => { debug_assert!(false); // It cannot be another variant as we are in the `Ok` branch of the above match. Err(ChannelError::Warn( @@ -8943,7 +8965,14 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - if let Some(chan) = chan_phase_entry.get_mut().funded_channel_mut() { + let just_funded_channel_opt = match chan_phase_entry.get_mut() { + // Note: here we take the funded post-splice channel, not the pre channel + #[cfg(splicing)] + ChannelPhase::RefundingV2(ref mut chan) => chan.post_funded.as_mut(), + ChannelPhase::Funded(ref mut chan) => Some(chan), + _ => None, + }; + if let Some(chan) = just_funded_channel_opt { let logger = WithChannelContext::from(&self.logger, &chan.context, None); let funding_txo = chan.context.get_funding_txo(); @@ -9413,6 +9442,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!( @@ -9421,27 +9453,63 @@ 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 ChannelPhase::Funded(chan) = chan_entry.get_mut() { - match chan.splice_init(msg, &self.signer_provider, &self.entropy_source, self.get_our_node_id(), &self.logger) { - Ok(splice_ack_msg) => { - peer_state.pending_msg_events.push(events::MessageSendEvent::SendSpliceAck { - node_id: *counterparty_node_id, - msg: splice_ack_msg, - }); - }, - Err(err) => { - return Err(MsgHandleErrInternal::from_chan_no_close(err, msg.channel_id)); - } - } + chan.splice_init_checks(msg, &self.signer_provider, &self.entropy_source, self.get_our_node_id()) + .map_err(|err| MsgHandleErrInternal::from_chan_no_close(err, msg.channel_id))?; } else { return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel is not funded, cannot be spliced".to_owned(), msg.channel_id)); } }, }; - // TODO(splicing): - // Change channel, change phase (remove and add) - // Create new post-splice channel - // etc. + // Change channel, phase changes, remove and add + // Remove the pre channel + // Note: this remove-and-add would not be needed if channel phase was wrapped (see #3418) + let prev_chan = match peer_state.channel_by_id.remove(&msg.channel_id) { + None => 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 {}, channel_id {}", counterparty_node_id, msg.channel_id), msg.channel_id)), + Some(chan_phase) => { + if let ChannelPhase::Funded(chan) = chan_phase { + chan + } else { + return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel in wrong state".to_owned(), msg.channel_id.clone())); + } + } + }; + + let post_chan = InboundV2Channel::new_spliced( + false, + &prev_chan, + &self.signer_provider, + &msg.funding_pubkey, + our_funding_contribution, + msg.funding_contribution_satoshis, + Vec::new(), + LockTime::from_consensus(msg.locktime), + msg.funding_feerate_perkw, + &self.logger, + ).map_err(|e| MsgHandleErrInternal::from_chan_no_close(e, msg.channel_id))?; + + // Add the modified channel + let post_chan_id = post_chan.context.channel_id(); + peer_state.channel_by_id.insert(post_chan_id, ChannelPhase::RefundingV2( + SplicingChannel::new(prev_chan, PendingV2Channel::Inbound(post_chan)) + )); + + // Perform state changes + match peer_state.channel_by_id.entry(post_chan_id) { + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close("Internal consistency error".to_string(), post_chan_id)), + hash_map::Entry::Occupied(mut chan_entry) => { + if let ChannelPhase::RefundingV2(chan) = chan_entry.get_mut() { + let splice_ack_msg = chan.splice_init(msg, &self.signer_provider, &self.entropy_source, self.get_our_node_id(), &self.logger) + .map_err(|err| MsgHandleErrInternal::from_chan_no_close(err, post_chan_id))?; + peer_state.pending_msg_events.push(events::MessageSendEvent::SendSpliceAck { + node_id: *counterparty_node_id, + msg: splice_ack_msg, + }); + } else { + return Err(MsgHandleErrInternal::send_err_msg_no_close("Internal consistency error: splice_init while not renegotiating".to_string(), post_chan_id)); + } + } + } Ok(()) } @@ -9458,37 +9526,78 @@ 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 - match peer_state.channel_by_id.entry(msg.channel_id) { - hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!( - "Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", - counterparty_node_id - ), msg.channel_id)), - hash_map::Entry::Occupied(mut chan) => { - if let ChannelPhase::Funded(chan) = chan.get_mut() { - match chan.splice_ack(msg, &self.signer_provider, &self.entropy_source, self.get_our_node_id(), &self.logger) { - Ok(tx_msg_opt) => { - if let Some(tx_msg_opt) = tx_msg_opt { - peer_state.pending_msg_events.push(tx_msg_opt.into_msg_send_event(counterparty_node_id.clone())); - } - } - Err(err) => { - return Err(MsgHandleErrInternal::from_chan_no_close(err, msg.channel_id)); - } + // Look for channel + let pending_splice = match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)), + hash_map::Entry::Occupied(chan) => { + if let ChannelPhase::Funded(chan) = chan.get() { + // check if splice is pending + if let Some(pending_splice) = &chan.pending_splice_pre { + // Note: this is incomplete (their funding contribution is not set) + pending_splice.clone() + } else { + return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel is not in pending splice".to_owned(), msg.channel_id.clone())); } } else { - return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel is not funded, cannot splice".to_owned(), msg.channel_id)); + return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel in wrong state".to_owned(), msg.channel_id.clone())); } }, }; - // TODO(splicing): - // Change channel, change phase (remove and add) - // Create new post-splice channel - // Start splice funding transaction negotiation - // etc. + // Change channel, phase changes, remove and add + // Remove the pre channel + // Note: this remove-and-add would not be needed if channel phase was wrapped (see #3418) + let prev_chan = match peer_state.channel_by_id.remove(&msg.channel_id) { + None => 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 {}, channel_id {}", counterparty_node_id, msg.channel_id), msg.channel_id)), + Some(chan_phase) => { + if let ChannelPhase::Funded(chan) = chan_phase { + chan + } else { + return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel in wrong state".to_owned(), msg.channel_id.clone())); + } + } + }; - Err(MsgHandleErrInternal::send_err_msg_no_close("TODO(splicing): Splicing is not implemented (splice_ack)".to_owned(), msg.channel_id)) + let post_chan = OutboundV2Channel::new_spliced( + true, + &prev_chan, + &self.signer_provider, + &msg.funding_pubkey, + pending_splice.our_funding_contribution, + msg.funding_contribution_satoshis, + pending_splice.our_funding_inputs, + LockTime::from_consensus(pending_splice.locktime), + pending_splice.funding_feerate_perkw, + &self.logger, + ).map_err(|e| MsgHandleErrInternal::from_chan_no_close(e, msg.channel_id))?; + + // Add the modified channel + let post_chan_id = post_chan.context().channel_id(); + peer_state.channel_by_id.insert(post_chan_id, ChannelPhase::RefundingV2( + SplicingChannel::new(prev_chan, PendingV2Channel::Outbound(post_chan)), + )); + + // Perform state changes + match peer_state.channel_by_id.entry(post_chan_id) { + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close("Internal consistency error".to_string(), post_chan_id)), + hash_map::Entry::Occupied(mut chan_entry) => { + if let ChannelPhase::RefundingV2(chan) = chan_entry.get_mut() { + match chan.splice_ack(msg, pending_splice.our_funding_contribution, &self.signer_provider, &self.entropy_source, self.get_our_node_id(), &self.logger) { + Ok(tx_msg_opt) => { + if let Some(tx_msg) = tx_msg_opt { + peer_state.pending_msg_events.push(tx_msg.into_msg_send_event(counterparty_node_id.clone())); + } + Ok(()) + }, + Err(err) => { + Err(MsgHandleErrInternal::from_chan_no_close(err, post_chan_id)) + }, + } + } else { + Err(MsgHandleErrInternal::send_err_msg_no_close("Internal consistency error: splice_ack while not renegotiating".to_string(), post_chan_id)) + } + } + } } /// Process pending events from the [`chain::Watch`], returning whether any events were processed. @@ -11692,7 +11801,8 @@ where &mut chan.context }, #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => { + ChannelPhase::RefundingV2(channel) => { + let chan = &mut channel.pre_funded; let logger = WithChannelContext::from(&self.logger, &chan.context, None); if chan.remove_uncommitted_htlcs_and_mark_paused(&&logger).is_ok() { // We only retain funded channels that are not shutdown. @@ -11871,7 +11981,8 @@ where } #[cfg(splicing)] - ChannelPhase::RefundingV2(chan) => { + ChannelPhase::RefundingV2(channel) => { + let chan = &mut channel.pre_funded; let logger = WithChannelContext::from(&self.logger, &chan.context, None); pending_msg_events.push(events::MessageSendEvent::SendChannelReestablish { node_id: chan.context.get_counterparty_node_id(), diff --git a/lightning/src/ln/functional_tests_splice.rs b/lightning/src/ln/functional_tests_splice.rs index 824a7cfebcc..48fae40627f 100644 --- a/lightning/src/ln/functional_tests_splice.rs +++ b/lightning/src/ln/functional_tests_splice.rs @@ -40,6 +40,7 @@ fn test_v1_splice_in() { let channel_value_sat = 100_000; // same as funding satoshis let push_msat = 0; let channel_reserve_amnt_sat = 1_000; + let expect_inputs_in_reverse = true; let expected_funded_channel_id = "ae3367da2c13bc1ceb86bf56418f62828f7ce9d6bfb15a46af5ba1f1ed8b124f"; @@ -230,6 +231,7 @@ fn test_v1_splice_in() { // Amount being added to the channel through the splice-in let splice_in_sats: u64 = 20000; + let post_splice_channel_value = channel_value_sat + splice_in_sats; let funding_feerate_perkw = 1024; // TODO let locktime = 0; // TODO @@ -305,9 +307,51 @@ fn test_v1_splice_in() { 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 splice funding + 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 change output + 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()); + + // TODO(splicing) This is the last tx_complete, which triggers the commitment flow, which is not yet implemented + // let _res = initiator_node.node.handle_tx_complete(acceptor_node.node.get_our_node_id(), &tx_complete_msg); + + // TODO(splicing): Continue with commitment flow, new tx confirmation - // TODO(splicing): continue with splice transaction negotiation // === Close channel, cooperatively initiator_node.node.close_channel(&channel_id2, &acceptor_node.node.get_our_node_id()).unwrap(); diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index accd2ad4c53..9c95222e39b 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -1186,7 +1186,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,