From 0b89a363217552bbc63aac186d8a431a341f62f7 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:21:15 +0200 Subject: [PATCH 1/5] Rename DualFundingContext This is a simple rename, DualFundingContext to FundingNegotiationContext, to suggest that this is use not only in dual-funded channel open. Also rename the field dual_funding_context to funding_negotiation_context. --- lightning/src/ln/channel.rs | 38 +++++++++++++++--------------- lightning/src/ln/channelmanager.rs | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index fb58b51d4dc..e5960650532 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2714,7 +2714,7 @@ where debug_assert!(self.interactive_tx_constructor.is_none()); let mut funding_inputs = Vec::new(); - mem::swap(&mut self.dual_funding_context.our_funding_inputs, &mut funding_inputs); + mem::swap(&mut self.funding_negotiation_context.our_funding_inputs, &mut funding_inputs); // TODO(splicing): Add prev funding tx as input, must be provided as a parameter @@ -2732,7 +2732,7 @@ where if self.funding.is_outbound() { funding_outputs.push( OutputOwned::Shared(SharedOwnedOutput::new( - shared_funding_output, self.dual_funding_context.our_funding_satoshis, + shared_funding_output, self.funding_negotiation_context.our_funding_satoshis, )) ); } else { @@ -2748,9 +2748,9 @@ where .map_err(|_err| AbortReason::InternalError("Error getting destination script"))? }; let change_value_opt = calculate_change_output_value( - self.funding.is_outbound(), self.dual_funding_context.our_funding_satoshis, + self.funding.is_outbound(), self.funding_negotiation_context.our_funding_satoshis, &funding_inputs, &funding_outputs, - self.dual_funding_context.funding_feerate_sat_per_1000_weight, + self.funding_negotiation_context.funding_feerate_sat_per_1000_weight, change_script.minimal_non_dust().to_sat(), )?; if let Some(change_value) = change_value_opt { @@ -2759,7 +2759,7 @@ where 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); + let change_output_fee = fee_for_weight(self.funding_negotiation_context.funding_feerate_sat_per_1000_weight, change_output_weight); let change_value_decreased_with_fee = change_value.saturating_sub(change_output_fee); // Check dust limit again if change_value_decreased_with_fee > self.context.holder_dust_limit_satoshis { @@ -2773,9 +2773,9 @@ where holder_node_id, counterparty_node_id: self.context.counterparty_node_id, channel_id: self.context.channel_id(), - feerate_sat_per_kw: self.dual_funding_context.funding_feerate_sat_per_1000_weight, + feerate_sat_per_kw: self.funding_negotiation_context.funding_feerate_sat_per_1000_weight, is_initiator: self.funding.is_outbound(), - funding_tx_locktime: self.dual_funding_context.funding_tx_locktime, + funding_tx_locktime: self.funding_negotiation_context.funding_tx_locktime, inputs_to_contribute: funding_inputs, outputs_to_contribute: funding_outputs, expected_remote_shared_funding_output, @@ -2867,7 +2867,7 @@ where where L::Target: Logger { - let our_funding_satoshis = self.dual_funding_context.our_funding_satoshis; + let our_funding_satoshis = self.funding_negotiation_context.our_funding_satoshis; let transaction_number = self.unfunded_context.transaction_number(); let mut output_index = None; @@ -5782,8 +5782,8 @@ fn check_v2_funding_inputs_sufficient( } } -/// Context for dual-funded channels. -pub(super) struct DualFundingChannelContext { +/// Context for negotiating channels (dual-funded V2 open, splicing) +pub(super) struct FundingNegotiationContext { /// 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. @@ -11364,7 +11364,7 @@ where pub funding: FundingScope, pub context: ChannelContext, pub unfunded_context: UnfundedChannelContext, - pub dual_funding_context: DualFundingChannelContext, + pub funding_negotiation_context: FundingNegotiationContext, /// The current interactive transaction construction session under negotiation. pub interactive_tx_constructor: Option, /// The signing session created after `tx_complete` handling @@ -11427,7 +11427,7 @@ where unfunded_channel_age_ticks: 0, holder_commitment_point: HolderCommitmentPoint::new(&context.holder_signer, &context.secp_ctx), }; - let dual_funding_context = DualFundingChannelContext { + let funding_negotiation_context = FundingNegotiationContext { our_funding_satoshis: funding_satoshis, // TODO(dual_funding) TODO(splicing) Include counterparty contribution, once that's enabled their_funding_satoshis: None, @@ -11439,7 +11439,7 @@ where funding, context, unfunded_context, - dual_funding_context, + funding_negotiation_context, interactive_tx_constructor: None, interactive_tx_signing_session: None, }; @@ -11515,7 +11515,7 @@ where }, funding_feerate_sat_per_1000_weight: self.context.feerate_per_kw, second_per_commitment_point, - locktime: self.dual_funding_context.funding_tx_locktime.to_consensus_u32(), + locktime: self.funding_negotiation_context.funding_tx_locktime.to_consensus_u32(), require_confirmed_inputs: None, } } @@ -11587,7 +11587,7 @@ where &funding.get_counterparty_pubkeys().revocation_basepoint); context.channel_id = channel_id; - let dual_funding_context = DualFundingChannelContext { + let funding_negotiation_context = FundingNegotiationContext { our_funding_satoshis: our_funding_satoshis, their_funding_satoshis: Some(msg.common_fields.funding_satoshis), funding_tx_locktime: LockTime::from_consensus(msg.locktime), @@ -11601,8 +11601,8 @@ where holder_node_id, counterparty_node_id, channel_id: context.channel_id, - feerate_sat_per_kw: dual_funding_context.funding_feerate_sat_per_1000_weight, - funding_tx_locktime: dual_funding_context.funding_tx_locktime, + feerate_sat_per_kw: funding_negotiation_context.funding_feerate_sat_per_1000_weight, + funding_tx_locktime: funding_negotiation_context.funding_tx_locktime, is_initiator: false, inputs_to_contribute: our_funding_inputs, outputs_to_contribute: Vec::new(), @@ -11620,7 +11620,7 @@ where Ok(Self { funding, context, - dual_funding_context, + funding_negotiation_context, interactive_tx_constructor, interactive_tx_signing_session: None, unfunded_context, @@ -11686,7 +11686,7 @@ where }), channel_type: Some(self.funding.get_channel_type().clone()), }, - funding_satoshis: self.dual_funding_context.our_funding_satoshis, + funding_satoshis: self.funding_negotiation_context.our_funding_satoshis, second_per_commitment_point, require_confirmed_inputs: None, } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 656135daf84..99fc91a7cbd 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8482,7 +8482,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ // Inbound V2 channels with contributed inputs are not considered unfunded. if let Some(unfunded_chan) = chan.as_unfunded_v2() { - if unfunded_chan.dual_funding_context.our_funding_satoshis != 0 { + if unfunded_chan.funding_negotiation_context.our_funding_satoshis != 0 { continue; } } From 299c64336d307ab4e71734fc65c5cb81487bb45f Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Tue, 17 Jun 2025 05:30:42 +0200 Subject: [PATCH 2/5] Extend begin_interactive_...() with splicing-specific parameters The begin_interactive_funding_tx_construction() method is extended with `is_initiator` parameter (splice initiator), and `prev_funding_input` optional parameter, containing the previous funding transaction, which will be added to the negotiation as an input by the initiator. --- lightning/src/ln/channel.rs | 50 ++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index e5960650532..4c2985abcc3 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2703,12 +2703,13 @@ where /// default destination address is used. /// If error occurs, it is caused by our side, not the counterparty. #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled - #[rustfmt::skip] fn begin_interactive_funding_tx_construction( &mut self, signer_provider: &SP, entropy_source: &ES, holder_node_id: PublicKey, - change_destination_opt: Option, + is_initiator: bool, change_destination_opt: Option, + prev_funding_input: Option<(TxIn, TransactionU16LenLimited)>, ) -> Result, AbortReason> - where ES::Target: EntropySource + where + ES::Target: EntropySource, { debug_assert!(matches!(self.context.channel_state, ChannelState::NegotiatingFunding(_))); debug_assert!(self.interactive_tx_constructor.is_none()); @@ -2716,7 +2717,11 @@ where let mut funding_inputs = Vec::new(); mem::swap(&mut self.funding_negotiation_context.our_funding_inputs, &mut funding_inputs); - // TODO(splicing): Add prev funding tx as input, must be provided as a parameter + if is_initiator { + if let Some(prev_funding_input) = prev_funding_input { + funding_inputs.push(prev_funding_input); + } + } // Add output for funding tx // Note: For the error case when the inputs are insufficient, it will be handled after @@ -2729,12 +2734,11 @@ where script_pubkey: self.funding.get_funding_redeemscript().to_p2wsh(), }; - if self.funding.is_outbound() { - funding_outputs.push( - OutputOwned::Shared(SharedOwnedOutput::new( - shared_funding_output, self.funding_negotiation_context.our_funding_satoshis, - )) - ); + if is_initiator { + funding_outputs.push(OutputOwned::Shared(SharedOwnedOutput::new( + shared_funding_output, + self.funding_negotiation_context.our_funding_satoshis, + ))); } else { let TxOut { value, script_pubkey } = shared_funding_output; expected_remote_shared_funding_output = Some((script_pubkey, value.to_sat())); @@ -2744,22 +2748,26 @@ where let change_script = if let Some(script) = change_destination_opt { script } else { - signer_provider.get_destination_script(self.context.channel_keys_id) + signer_provider + .get_destination_script(self.context.channel_keys_id) .map_err(|_err| AbortReason::InternalError("Error getting destination script"))? }; let change_value_opt = calculate_change_output_value( - self.funding.is_outbound(), self.funding_negotiation_context.our_funding_satoshis, - &funding_inputs, &funding_outputs, + is_initiator, + self.funding_negotiation_context.our_funding_satoshis, + &funding_inputs, + &funding_outputs, self.funding_negotiation_context.funding_feerate_sat_per_1000_weight, change_script.minimal_non_dust().to_sat(), )?; if let Some(change_value) = change_value_opt { - let mut change_output = TxOut { - value: Amount::from_sat(change_value), - script_pubkey: change_script, - }; + 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.funding_negotiation_context.funding_feerate_sat_per_1000_weight, change_output_weight); + let change_output_fee = fee_for_weight( + self.funding_negotiation_context.funding_feerate_sat_per_1000_weight, + change_output_weight, + ); let change_value_decreased_with_fee = change_value.saturating_sub(change_output_fee); // Check dust limit again if change_value_decreased_with_fee > self.context.holder_dust_limit_satoshis { @@ -2773,8 +2781,10 @@ where holder_node_id, counterparty_node_id: self.context.counterparty_node_id, channel_id: self.context.channel_id(), - feerate_sat_per_kw: self.funding_negotiation_context.funding_feerate_sat_per_1000_weight, - is_initiator: self.funding.is_outbound(), + feerate_sat_per_kw: self + .funding_negotiation_context + .funding_feerate_sat_per_1000_weight, + is_initiator, funding_tx_locktime: self.funding_negotiation_context.funding_tx_locktime, inputs_to_contribute: funding_inputs, outputs_to_contribute: funding_outputs, From 5df8c8c2bc08c7f269f891f5a8913d1e46692312 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Tue, 17 Jun 2025 05:43:37 +0200 Subject: [PATCH 3/5] Introduce NegotiatingChannelView to bridge Funded and PendingV2 Introduce struct NegotiatingChannelView to perform transaction negotiation logic, on top of either PendingV2Channel (dual-funded channel open) or FundedChannel (splicing). --- lightning/src/ln/channel.rs | 163 ++++++++++++++++++++++++----- lightning/src/ln/channelmanager.rs | 68 +++++------- 2 files changed, 161 insertions(+), 70 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 4c2985abcc3..b9701665c62 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1727,6 +1727,19 @@ where } } + pub fn as_negotiating_channel(&mut self) -> Result, ChannelError> { + match &mut self.phase { + ChannelPhase::UnfundedV2(chan) => Ok(chan.as_negotiating_channel()), + #[cfg(splicing)] + ChannelPhase::Funded(chan) => { + Ok(chan.as_renegotiating_channel().map_err(|err| ChannelError::Warn(err.into()))?) + }, + _ => Err(ChannelError::Warn( + "Got a transaction negotiation message in an invalid phase".to_owned(), + )), + } + } + #[rustfmt::skip] pub fn funding_signed( &mut self, msg: &msgs::FundingSigned, best_block: BestBlock, signer_provider: &SP, logger: &L @@ -1765,9 +1778,11 @@ where where L::Target: Logger, { - if let ChannelPhase::UnfundedV2(chan) = &mut self.phase { - let logger = WithChannelContext::from(logger, &chan.context, None); - chan.funding_tx_constructed(signing_session, &&logger) + let logger = WithChannelContext::from(logger, self.context(), None); + if let Ok(mut negotiating_channel) = self.as_negotiating_channel() { + let (commitment_signed, event) = + negotiating_channel.funding_tx_constructed(signing_session, &&logger)?; + Ok((commitment_signed, event)) } else { Err(ChannelError::Warn("Got a tx_complete message with no interactive transaction construction expected or in-progress".to_owned())) } @@ -2147,7 +2162,13 @@ impl FundingScope { /// Info about a pending splice, used in the pre-splice channel #[cfg(splicing)] struct PendingSplice { + /// Intended contributions to the splice from our end pub our_funding_contribution: i64, + funding_scope: Option, + funding_negotiation_context: FundingNegotiationContext, + /// The current interactive transaction construction session under negotiation. + interactive_tx_constructor: Option, + interactive_tx_signing_session: Option, /// The funding txid used in the `splice_locked` sent to the counterparty. sent_funding_txid: Option, @@ -2694,7 +2715,23 @@ where } } -impl PendingV2Channel +/// A short-lived subset view of a channel, used for V2 funding negotiation or re-negotiation. +/// Can be produced by: +/// - [`PendingV2Channel`], at V2 channel open, and +/// - [`FundedChannel`], when splicing. +pub struct NegotiatingChannelView<'a, SP: Deref> +where + SP::Target: SignerProvider, +{ + context: &'a mut ChannelContext, + funding: &'a mut FundingScope, + funding_negotiation_context: &'a mut FundingNegotiationContext, + interactive_tx_constructor: &'a mut Option, + interactive_tx_signing_session: &'a mut Option, + holder_commitment_transaction_number: u64, +} + +impl<'a, SP: Deref> NegotiatingChannelView<'a, SP> where SP::Target: SignerProvider, { @@ -2793,13 +2830,15 @@ where let mut tx_constructor = InteractiveTxConstructor::new(constructor_args)?; let msg = tx_constructor.take_initiator_first_message(); - self.interactive_tx_constructor = Some(tx_constructor); + *self.interactive_tx_constructor = Some(tx_constructor); Ok(msg) } - pub fn tx_add_input(&mut self, msg: &msgs::TxAddInput) -> InteractiveTxMessageSendResult { - InteractiveTxMessageSendResult(match &mut self.interactive_tx_constructor { + pub(super) fn tx_add_input( + &mut self, msg: &msgs::TxAddInput, + ) -> InteractiveTxMessageSendResult { + InteractiveTxMessageSendResult(match self.interactive_tx_constructor { Some(ref mut tx_constructor) => tx_constructor .handle_tx_add_input(msg) .map_err(|reason| reason.into_tx_abort_msg(self.context.channel_id())), @@ -2810,8 +2849,10 @@ where }) } - pub fn tx_add_output(&mut self, msg: &msgs::TxAddOutput) -> InteractiveTxMessageSendResult { - InteractiveTxMessageSendResult(match &mut self.interactive_tx_constructor { + pub(super) fn tx_add_output( + &mut self, msg: &msgs::TxAddOutput, + ) -> InteractiveTxMessageSendResult { + InteractiveTxMessageSendResult(match self.interactive_tx_constructor { Some(ref mut tx_constructor) => tx_constructor .handle_tx_add_output(msg) .map_err(|reason| reason.into_tx_abort_msg(self.context.channel_id())), @@ -2822,8 +2863,10 @@ where }) } - pub fn tx_remove_input(&mut self, msg: &msgs::TxRemoveInput) -> InteractiveTxMessageSendResult { - InteractiveTxMessageSendResult(match &mut self.interactive_tx_constructor { + pub(super) fn tx_remove_input( + &mut self, msg: &msgs::TxRemoveInput, + ) -> InteractiveTxMessageSendResult { + InteractiveTxMessageSendResult(match self.interactive_tx_constructor { Some(ref mut tx_constructor) => tx_constructor .handle_tx_remove_input(msg) .map_err(|reason| reason.into_tx_abort_msg(self.context.channel_id())), @@ -2834,10 +2877,10 @@ where }) } - pub fn tx_remove_output( + pub(super) fn tx_remove_output( &mut self, msg: &msgs::TxRemoveOutput, ) -> InteractiveTxMessageSendResult { - InteractiveTxMessageSendResult(match &mut self.interactive_tx_constructor { + InteractiveTxMessageSendResult(match self.interactive_tx_constructor { Some(ref mut tx_constructor) => tx_constructor .handle_tx_remove_output(msg) .map_err(|reason| reason.into_tx_abort_msg(self.context.channel_id())), @@ -2848,9 +2891,9 @@ where }) } - pub fn tx_complete(&mut self, msg: &msgs::TxComplete) -> HandleTxCompleteResult { - let tx_constructor = match &mut self.interactive_tx_constructor { - Some(ref mut tx_constructor) => tx_constructor, + pub(super) fn tx_complete(&mut self, msg: &msgs::TxComplete) -> HandleTxCompleteResult { + let tx_constructor = match self.interactive_tx_constructor { + Some(tx_constructor) => tx_constructor, None => { let tx_abort = msgs::TxAbort { channel_id: msg.channel_id, @@ -2871,14 +2914,14 @@ where } #[rustfmt::skip] - pub fn funding_tx_constructed( + fn funding_tx_constructed( &mut self, mut signing_session: InteractiveTxSigningSession, logger: &L ) -> Result<(msgs::CommitmentSigned, Option), ChannelError> where L::Target: Logger { - let our_funding_satoshis = self.funding_negotiation_context.our_funding_satoshis; - let transaction_number = self.unfunded_context.transaction_number(); + let our_funding_satoshis = self.funding_negotiation_context + .our_funding_satoshis; let mut output_index = None; let expected_spk = self.funding.get_funding_redeemscript().to_p2wsh(); @@ -2903,14 +2946,16 @@ where ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, ))); }; - self.funding.channel_transaction_parameters.funding_outpoint = Some(outpoint); + self.funding + .channel_transaction_parameters.funding_outpoint = Some(outpoint); - self.context.assert_no_commitment_advancement(transaction_number, "initial commitment_signed"); + self.context.assert_no_commitment_advancement(self.holder_commitment_transaction_number, "initial commitment_signed"); let commitment_signed = self.context.get_initial_commitment_signed(&self.funding, logger); let commitment_signed = match commitment_signed { Ok(commitment_signed) => commitment_signed, Err(err) => { - self.funding.channel_transaction_parameters.funding_outpoint = None; + self.funding + .channel_transaction_parameters.funding_outpoint = None; return Err(ChannelError::Close((err.to_string(), ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }))); }, }; @@ -2958,8 +3003,8 @@ where self.context.channel_state = channel_state; // Clear the interactive transaction constructor - self.interactive_tx_constructor.take(); - self.interactive_tx_signing_session = Some(signing_session); + *self.interactive_tx_constructor = None; + *self.interactive_tx_signing_session = Some(signing_session); Ok((commitment_signed, funding_ready_for_sig_event)) } @@ -5941,6 +5986,42 @@ where SP::Target: SignerProvider, ::EcdsaSigner: EcdsaChannelSigner, { + /// If we are in splicing/refunding, return a short-lived [`NegotiatingChannelView`]. + #[cfg(splicing)] + fn as_renegotiating_channel(&mut self) -> Result, &'static str> { + if let Some(ref mut pending_splice) = &mut self.pending_splice { + if let Some(ref mut funding) = &mut pending_splice.funding_scope { + if pending_splice.funding_negotiation_context.our_funding_satoshis != 0 + || pending_splice + .funding_negotiation_context + .their_funding_satoshis + .unwrap_or_default() != 0 + { + Ok(NegotiatingChannelView { + context: &mut self.context, + funding, + funding_negotiation_context: &mut pending_splice + .funding_negotiation_context, + interactive_tx_constructor: &mut pending_splice.interactive_tx_constructor, + interactive_tx_signing_session: &mut pending_splice + .interactive_tx_signing_session, + holder_commitment_transaction_number: self + .holder_commitment_point + .transaction_number(), + }) + } else { + Err("Received unexpected interactive transaction negotiation message: \ + the channel is splicing, but splice_init/splice_ack has not been exchanged yet") + } + } else { + Err("Received unexpected interactive transaction negotiation message: \ + the channel is splicing, but splice_init/splice_ack has not been exchanged yet") + } + } else { + Err("Received unexpected interactive transaction negotiation message: the channel is funded and not splicing") + } + } + #[rustfmt::skip] fn check_remote_fee( channel_type: &ChannelTypeFeatures, fee_estimator: &LowerBoundedFeeEstimator, @@ -9802,10 +9883,11 @@ where ) -> Result { // Check if a splice has been initiated already. // Note: only a single outstanding splice is supported (per spec) - if let Some(splice_info) = &self.pending_splice { + if let Some(pending_splice) = &self.pending_splice { return Err(APIError::APIMisuseError { err: format!( "Channel {} cannot be spliced, as it has already a splice pending (contribution {})", - self.context.channel_id(), splice_info.our_funding_contribution + self.context.channel_id(), + pending_splice.our_funding_contribution, )}); } @@ -9837,9 +9919,26 @@ where "Insufficient inputs for splicing; channel ID {}, err {}", self.context.channel_id(), err, )})?; + // Convert inputs + let mut funding_inputs = Vec::new(); + for (tx_in, tx, _w) in our_funding_inputs.into_iter() { + let tx16 = TransactionU16LenLimited::new(tx.clone()).map_err(|_e| APIError::APIMisuseError { err: format!("Too large transaction")})?; + funding_inputs.push((tx_in.clone(), tx16)); + } + let funding_negotiation_context = FundingNegotiationContext { + our_funding_satoshis: 0, // set at later phase + their_funding_satoshis: None, // set at later phase + funding_tx_locktime: LockTime::from_consensus(locktime), + funding_feerate_sat_per_1000_weight: funding_feerate_per_kw, + our_funding_inputs: funding_inputs, + }; self.pending_splice = Some(PendingSplice { our_funding_contribution: our_funding_contribution_satoshis, + funding_scope: None, + funding_negotiation_context, + interactive_tx_constructor: None, + interactive_tx_signing_session: None, sent_funding_txid: None, received_funding_txid: None, }); @@ -11711,6 +11810,18 @@ where pub fn get_accept_channel_v2_message(&self) -> msgs::AcceptChannelV2 { self.generate_accept_channel_v2_message() } + + /// Return a short-lived [`NegotiatingChannelView`]. + fn as_negotiating_channel(&mut self) -> NegotiatingChannelView { + NegotiatingChannelView { + context: &mut self.context, + funding: &mut self.funding, + funding_negotiation_context: &mut self.funding_negotiation_context, + interactive_tx_constructor: &mut self.interactive_tx_constructor, + interactive_tx_signing_session: &mut self.interactive_tx_signing_session, + holder_commitment_transaction_number: self.unfunded_context.transaction_number(), + } + } } // Unfunded channel utilities diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 99fc91a7cbd..02361438c26 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8884,7 +8884,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } #[rustfmt::skip] - fn internal_tx_msg) -> Result>( + fn internal_tx_msg) -> Result>( &self, counterparty_node_id: &PublicKey, channel_id: ChannelId, tx_msg_handler: HandleTxMsgFn ) -> Result<(), MsgHandleErrInternal> { let per_peer_state = self.per_peer_state.read().unwrap(); @@ -8902,9 +8902,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let channel = chan_entry.get_mut(); let msg_send_event = match tx_msg_handler(channel) { Ok(msg_send_event) => msg_send_event, - Err(tx_msg_str) => return Err(MsgHandleErrInternal::from_chan_no_close(ChannelError::Warn( - format!("Got a {tx_msg_str} message with no interactive transaction construction expected or in-progress") - ), channel_id)), + Err(err) => return Err(MsgHandleErrInternal::from_chan_no_close(err, channel_id)), }; peer_state.pending_msg_events.push(msg_send_event); Ok(()) @@ -8922,12 +8920,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &self, counterparty_node_id: PublicKey, msg: &msgs::TxAddInput, ) -> Result<(), MsgHandleErrInternal> { self.internal_tx_msg(&counterparty_node_id, msg.channel_id, |channel: &mut Channel| { - match channel.as_unfunded_v2_mut() { - Some(unfunded_channel) => { - Ok(unfunded_channel.tx_add_input(msg).into_msg_send_event(counterparty_node_id)) - }, - None => Err("tx_add_input"), - } + Ok(channel + .as_negotiating_channel()? + .tx_add_input(msg) + .into_msg_send_event(counterparty_node_id)) }) } @@ -8935,15 +8931,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &self, counterparty_node_id: PublicKey, msg: &msgs::TxAddOutput, ) -> Result<(), MsgHandleErrInternal> { self.internal_tx_msg(&counterparty_node_id, msg.channel_id, |channel: &mut Channel| { - match channel.as_unfunded_v2_mut() { - Some(unfunded_channel) => { - let msg_send_event = unfunded_channel - .tx_add_output(msg) - .into_msg_send_event(counterparty_node_id); - Ok(msg_send_event) - }, - None => Err("tx_add_output"), - } + Ok(channel + .as_negotiating_channel()? + .tx_add_output(msg) + .into_msg_send_event(counterparty_node_id)) }) } @@ -8951,15 +8942,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &self, counterparty_node_id: PublicKey, msg: &msgs::TxRemoveInput, ) -> Result<(), MsgHandleErrInternal> { self.internal_tx_msg(&counterparty_node_id, msg.channel_id, |channel: &mut Channel| { - match channel.as_unfunded_v2_mut() { - Some(unfunded_channel) => { - let msg_send_event = unfunded_channel - .tx_remove_input(msg) - .into_msg_send_event(counterparty_node_id); - Ok(msg_send_event) - }, - None => Err("tx_remove_input"), - } + Ok(channel + .as_negotiating_channel()? + .tx_remove_input(msg) + .into_msg_send_event(counterparty_node_id)) }) } @@ -8967,15 +8953,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &self, counterparty_node_id: PublicKey, msg: &msgs::TxRemoveOutput, ) -> Result<(), MsgHandleErrInternal> { self.internal_tx_msg(&counterparty_node_id, msg.channel_id, |channel: &mut Channel| { - match channel.as_unfunded_v2_mut() { - Some(unfunded_channel) => { - let msg_send_event = unfunded_channel - .tx_remove_output(msg) - .into_msg_send_event(counterparty_node_id); - Ok(msg_send_event) - }, - None => Err("tx_remove_output"), - } + Ok(channel + .as_negotiating_channel()? + .tx_remove_output(msg) + .into_msg_send_event(counterparty_node_id)) }) } @@ -8993,14 +8974,13 @@ 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_entry) => { - let (msg_send_event_opt, signing_session_opt) = match chan_entry.get_mut().as_unfunded_v2_mut() { - Some(chan) => chan.tx_complete(msg) + let (msg_send_event_opt, signing_session_opt) = match chan_entry.get_mut().as_negotiating_channel() { + Ok(mut negotiating_channel) => negotiating_channel + .tx_complete(msg) .into_msg_send_event_or_signing_session(counterparty_node_id), - None => try_channel_entry!(self, peer_state, Err(ChannelError::Close( - ( - "Got a tx_complete message with no interactive transaction construction expected or in-progress".into(), - ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, - ))), chan_entry) + Err(err) => { + try_channel_entry!(self, peer_state, Err(err), chan_entry) + } }; if let Some(msg_send_event) = msg_send_event_opt { peer_state.pending_msg_events.push(msg_send_event); From c3ec314fe907fcbbab082ebf552e59df2409dc9e Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Tue, 17 Jun 2025 10:58:42 +0200 Subject: [PATCH 4/5] Implement transaction negotiation during splicing Fill the logic for including transaction negotiation during splicing, implement the functions: splice_channel, splice_init, splice_ack, funding_tx_constructed. Also extend the test case test_v1_splice_in with the steps for funding negotiation during splicing. --- lightning/src/ln/channel.rs | 507 +++++++++++++++++++++++++++-- lightning/src/ln/channelmanager.rs | 53 +-- lightning/src/ln/interactivetxs.rs | 1 - lightning/src/ln/splicing_tests.rs | 129 +++++++- 4 files changed, 638 insertions(+), 52 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index b9701665c62..2892542d7bf 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::{ecdsa::Signature, Secp256k1}; use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::{secp256k1, sighash}; +#[cfg(splicing)] +use bitcoin::{Sequence, Witness}; use crate::chain::chaininterface::{ fee_for_weight, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator, @@ -2157,9 +2159,102 @@ impl FundingScope { pub fn get_short_channel_id(&self) -> Option { self.short_channel_id } + + /// Construct FundingScope for a splicing channel + #[cfg(splicing)] + pub fn for_splice( + prev_funding: &Self, context: &ChannelContext, our_funding_contribution_sats: i64, + post_channel_value: u64, counterparty_funding_pubkey: PublicKey, + ) -> Result + where + SP::Target: SignerProvider, + { + let post_value_to_self_msat_signed = (prev_funding.value_to_self_msat as i64) + .saturating_add(our_funding_contribution_sats * 1000); + if post_value_to_self_msat_signed < 0 { + // Splice out and more than our balance, error + return Err(ChannelError::Warn(format!( + "Cannot splice out more than the current balance, {} sats, {} msats", + post_value_to_self_msat_signed, prev_funding.value_to_self_msat + ))); + } + debug_assert!(post_value_to_self_msat_signed >= 0); + let post_value_to_self_msat = post_value_to_self_msat_signed as u64; + + let prev_funding_txid = prev_funding + .channel_transaction_parameters + .funding_outpoint + .map(|outpoint| outpoint.txid); + let holder_pubkeys = match &context.holder_signer { + ChannelSignerType::Ecdsa(ecdsa) => ecdsa.pubkeys(prev_funding_txid, &context.secp_ctx), + // TODO (taproot|arik) + #[cfg(taproot)] + _ => todo!(), + }; + let mut post_channel_transaction_parameters = ChannelTransactionParameters { + holder_pubkeys, + holder_selected_contest_delay: prev_funding + .channel_transaction_parameters + .holder_selected_contest_delay, + // The 'outbound' attribute doesn't change, even if the splice initiator is the other node + is_outbound_from_holder: prev_funding + .channel_transaction_parameters + .is_outbound_from_holder, + counterparty_parameters: prev_funding + .channel_transaction_parameters + .counterparty_parameters + .clone(), + funding_outpoint: None, // filled later + splice_parent_funding_txid: prev_funding_txid, + channel_type_features: prev_funding + .channel_transaction_parameters + .channel_type_features + .clone(), + channel_value_satoshis: post_channel_value, + }; + // Update the splicing 'tweak', this will rotate the keys in the signer + if let Some(ref mut counterparty_parameters) = + post_channel_transaction_parameters.counterparty_parameters + { + counterparty_parameters.pubkeys.funding_pubkey = counterparty_funding_pubkey; + } + + // New reserve values are based on the new channel value, and v2-specific + let counterparty_selected_channel_reserve_satoshis = Some(get_v2_channel_reserve_satoshis( + post_channel_value, + context.counterparty_dust_limit_satoshis, + )); + let holder_selected_channel_reserve_satoshis = + get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS); + Ok(Self { + channel_transaction_parameters: post_channel_transaction_parameters, + value_to_self_msat: post_value_to_self_msat, + funding_transaction: None, + counterparty_selected_channel_reserve_satoshis, + holder_selected_channel_reserve_satoshis, + #[cfg(debug_assertions)] + holder_max_commitment_tx_output: Mutex::new(( + post_value_to_self_msat, + (post_channel_value * 1000).saturating_sub(post_value_to_self_msat), + )), + #[cfg(debug_assertions)] + counterparty_max_commitment_tx_output: Mutex::new(( + post_value_to_self_msat, + (post_channel_value * 1000).saturating_sub(post_value_to_self_msat), + )), + #[cfg(any(test, fuzzing))] + next_local_commitment_tx_fee_info_cached: Mutex::new(None), + #[cfg(any(test, fuzzing))] + next_remote_commitment_tx_fee_info_cached: Mutex::new(None), + funding_tx_confirmation_height: 0, + funding_tx_confirmed_in: None, + minimum_depth_override: None, + short_channel_id: None, + }) + } } -/// Info about a pending splice, used in the pre-splice channel +/// Info about a pending splice #[cfg(splicing)] struct PendingSplice { /// Intended contributions to the splice from our end @@ -2201,6 +2296,28 @@ impl<'a> From<&'a Transaction> for ConfirmedTransaction<'a> { } } +#[cfg(splicing)] +impl PendingSplice { + #[inline] + fn add_checked(base: u64, delta: i64) -> u64 { + if delta >= 0 { + base.saturating_add(delta as u64) + } else { + base.saturating_sub(delta.abs() as u64) + } + } + + /// Compute the post-splice channel value from the pre-splice values and the peer contributions + pub fn compute_post_value( + pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64, + ) -> u64 { + Self::add_checked( + pre_channel_value, + our_funding_contribution.saturating_add(their_funding_contribution), + ) + } +} + /// Contains everything about the channel including state, and various flags. pub(super) struct ChannelContext where @@ -2729,6 +2846,7 @@ where interactive_tx_constructor: &'a mut Option, interactive_tx_signing_session: &'a mut Option, holder_commitment_transaction_number: u64, + is_splice: bool, } impl<'a, SP: Deref> NegotiatingChannelView<'a, SP> @@ -2748,7 +2866,15 @@ where where ES::Target: EntropySource, { - debug_assert!(matches!(self.context.channel_state, ChannelState::NegotiatingFunding(_))); + if self.is_splice { + debug_assert!(matches!(self.context.channel_state, ChannelState::ChannelReady(_))); + } else { + debug_assert!(matches!( + self.context.channel_state, + ChannelState::NegotiatingFunding(_) + )); + } + debug_assert!(self.interactive_tx_constructor.is_none()); let mut funding_inputs = Vec::new(); @@ -2949,10 +3075,22 @@ where self.funding .channel_transaction_parameters.funding_outpoint = Some(outpoint); + if self.is_splice { + // TODO(splicing) Forced error, as the use case is not complete + return Err(ChannelError::Close(( + "TODO Forced error, incomplete implementation".into(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) } + ))); + } + self.context.assert_no_commitment_advancement(self.holder_commitment_transaction_number, "initial commitment_signed"); let commitment_signed = self.context.get_initial_commitment_signed(&self.funding, logger); let commitment_signed = match commitment_signed { - Ok(commitment_signed) => commitment_signed, + Ok(commitment_signed) => { + self.funding + .funding_transaction = Some(signing_session.unsigned_tx().build_unsigned_tx()); + commitment_signed + }, Err(err) => { self.funding .channel_transaction_parameters.funding_outpoint = None; @@ -6008,6 +6146,7 @@ where holder_commitment_transaction_number: self .holder_commitment_point .transaction_number(), + is_splice: true, }) } else { Err("Received unexpected interactive transaction negotiation message: \ @@ -9878,7 +10017,7 @@ where #[cfg(splicing)] #[rustfmt::skip] pub fn splice_channel(&mut self, our_funding_contribution_satoshis: i64, - our_funding_inputs: &Vec<(TxIn, Transaction, Weight)>, + our_funding_inputs: Vec<(TxIn, Transaction, Weight)>, funding_feerate_per_kw: u32, locktime: u32, ) -> Result { // Check if a splice has been initiated already. @@ -9965,18 +10104,21 @@ where } } - /// Handle splice_init + /// Checks during handling splice_init #[cfg(splicing)] - #[rustfmt::skip] - pub fn splice_init(&mut self, msg: &msgs::SpliceInit) -> Result { + pub fn validate_splice_init(&self, msg: &msgs::SpliceInit) -> Result<(), ChannelError> { let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; // TODO(splicing): Currently not possible to contribute on the splicing-acceptor side let our_funding_contribution_satoshis = 0i64; + // TODO(splicing): Add check that we are the quiescence acceptor + // Check if a splice has been initiated already. - if let Some(splice_info) = &self.pending_splice { + if let Some(pending_splice) = &self.pending_splice { return Err(ChannelError::Warn(format!( - "Channel has already a splice pending, contribution {}", splice_info.our_funding_contribution, + "Channel {} has already a splice pending, contribution {}", + self.context.channel_id(), + pending_splice.our_funding_contribution, ))); } @@ -9984,14 +10126,18 @@ where // MUST send a warning and close the connection or send an error // and fail the channel. if !self.context.is_live() { - return Err(ChannelError::Warn(format!("Splicing requested on a channel that is not live"))); + return Err(ChannelError::Warn(format!( + "Splicing requested on a channel that is not live" + ))); } - if their_funding_contribution_satoshis.saturating_add(our_funding_contribution_satoshis) < 0 { + if their_funding_contribution_satoshis.saturating_add(our_funding_contribution_satoshis) < 0 + { return Err(ChannelError::Warn(format!( "Splice-out not supported, only splice in, contribution is {} ({} + {})", their_funding_contribution_satoshis + our_funding_contribution_satoshis, - their_funding_contribution_satoshis, our_funding_contribution_satoshis, + their_funding_contribution_satoshis, + our_funding_contribution_satoshis, ))); } @@ -10000,11 +10146,116 @@ where // Note on channel reserve requirement pre-check: as the splice acceptor does not contribute, // it can't go below reserve, therefore no pre-check is done here. - // TODO(splicing): Once splice acceptor can contribute, add reserve pre-check, similar to the one in `splice_ack`. - // TODO(splicing): Store msg.funding_pubkey - // TODO(splicing): Apply start of splice (splice_start) + let pre_channel_value = self.funding.value_to_self_msat; + 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.funding.value_to_self_msat, + our_funding_contribution_satoshis, + ); + // TODO: Early check for reserve requirement + Ok(()) + } + + /// See also [`validate_splice_init`] + #[cfg(splicing)] + pub(crate) fn splice_init( + &mut self, msg: &msgs::SpliceInit, our_funding_contribution: i64, signer_provider: &SP, + entropy_source: &ES, holder_node_id: &PublicKey, logger: &L, + ) -> Result + where + ES::Target: EntropySource, + L::Target: Logger, + { + let _res = self.validate_splice_init(msg)?; + + let pre_channel_value = self.funding.get_value_satoshis(); + let their_funding_contribution = msg.funding_contribution_satoshis; + + let post_channel_value = PendingSplice::compute_post_value( + pre_channel_value, + our_funding_contribution, + their_funding_contribution, + ); + + let (our_funding_satoshis, their_funding_satoshis) = calculate_total_funding_contribution( + pre_channel_value, + our_funding_contribution, + msg.funding_contribution_satoshis, + false, // is_outbound + )?; + + let funding_scope = FundingScope::for_splice( + &self.funding, + &self.context, + our_funding_contribution, + post_channel_value, + msg.funding_pubkey, + )?; + + let funding_negotiation_context = FundingNegotiationContext { + our_funding_satoshis, + their_funding_satoshis: Some(their_funding_satoshis), + funding_tx_locktime: LockTime::from_consensus(msg.locktime), + funding_feerate_sat_per_1000_weight: msg.funding_feerate_per_kw, + our_funding_inputs: Vec::new(), + }; + + self.pending_splice = Some(PendingSplice { + our_funding_contribution, + funding_scope: Some(funding_scope), + funding_negotiation_context, + interactive_tx_constructor: None, + interactive_tx_signing_session: None, + received_funding_txid: None, + sent_funding_txid: None, + }); + + log_info!(logger, "Splicing process started after splice_init, new channel value {}, old {}, outgoing {}, channel_id {}", + post_channel_value, pre_channel_value, false, self.context.channel_id); + + let splice_ack_msg = self.get_splice_ack(our_funding_contribution); + + // Build NegotiatingChannelView locally, similar to Channel::as_renegotiating_channel() + let pending_splice_mut = &mut self.pending_splice.as_mut().unwrap(); // set above + let mut negotiating_view = NegotiatingChannelView { + context: &mut self.context, + funding: &mut pending_splice_mut.funding_scope.as_mut().unwrap(), // set above + funding_negotiation_context: &mut pending_splice_mut.funding_negotiation_context, + interactive_tx_constructor: &mut pending_splice_mut.interactive_tx_constructor, + interactive_tx_signing_session: &mut pending_splice_mut.interactive_tx_signing_session, + holder_commitment_transaction_number: self.holder_commitment_point.transaction_number(), + is_splice: true, + }; + + // Start interactive funding negotiation. TODO(splicing): Add current funding as extra input, once shared inputs are supported, see #3842. + let _msg = negotiating_view + .begin_interactive_funding_tx_construction( + signer_provider, + entropy_source, + holder_node_id.clone(), + false, + None, + None, + ) + .map_err(|err| { + ChannelError::Warn(format!( + "Failed to start interactive transaction construction, {:?}", + err + )) + })?; + + Ok(splice_ack_msg) + } + + /// Get the splice_ack message that can be sent in response to splice initiation. + #[cfg(splicing)] + pub fn get_splice_ack(&self, our_funding_contribution_satoshis: i64) -> msgs::SpliceAck { // TODO(splicing): The exisiting pubkey is reused, but a new one should be generated. See #3542. // Note that channel_keys_id is supposed NOT to change let splice_ack_msg = msgs::SpliceAck { @@ -10014,20 +10265,138 @@ where require_confirmed_inputs: None, }; // TODO(splicing): start interactive funding negotiation - Ok(splice_ack_msg) + splice_ack_msg } /// Handle splice_ack #[cfg(splicing)] - pub fn splice_ack(&mut self, _msg: &msgs::SpliceAck) -> Result<(), ChannelError> { + pub(crate) 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, + { // check if splice is pending - if self.pending_splice.is_none() { + let pending_splice = if let Some(ref mut pending_splice) = &mut self.pending_splice { + pending_splice + } else { return Err(ChannelError::Warn(format!("Channel is not in pending splice"))); }; + // TODO(splicing): Add check that we are the splice (quiescence) initiator + + if pending_splice.funding_scope.is_some() + || pending_splice.interactive_tx_constructor.is_some() + { + return Err(ChannelError::Warn(format!( + "Got unexpected splice_ack, splice already negotiating" + ))); + } + + let our_funding_contribution = pending_splice.our_funding_contribution; + let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; + // TODO(splicing): Pre-check for reserve requirement // (Note: It should also be checked later at tx_complete) - Ok(()) + let pre_channel_value = self.funding.get_value_satoshis(); + let post_channel_value = PendingSplice::compute_post_value( + pre_channel_value, + our_funding_contribution, + their_funding_contribution_satoshis, + ); + let _post_balance = + PendingSplice::add_checked(self.funding.value_to_self_msat, our_funding_contribution); + + // TODO(splicing): Pre-check for reserve requirement + // (Note: It should also be checked later at tx_complete) + + let (our_funding_satoshis, their_funding_satoshis) = calculate_total_funding_contribution( + pre_channel_value, + our_funding_contribution, + their_funding_contribution_satoshis, + true, // is_outbound + )?; + + let funding_scope = FundingScope::for_splice( + &self.funding, + &self.context, + our_funding_contribution, + post_channel_value, + msg.funding_pubkey, + )?; + + let pre_funding_transaction = &self.funding.funding_transaction; + let pre_funding_txo = &self.funding.get_funding_txo(); + // We need the current funding tx as an extra input + let prev_funding_input = + Self::get_input_of_previous_funding(pre_funding_transaction, pre_funding_txo)?; + debug_assert!(pending_splice.funding_scope.is_none()); + pending_splice.funding_scope = Some(funding_scope); + // update funding values + pending_splice.funding_negotiation_context.our_funding_satoshis = our_funding_satoshis; + pending_splice.funding_negotiation_context.their_funding_satoshis = + Some(their_funding_satoshis); + debug_assert!(pending_splice.interactive_tx_constructor.is_none()); + debug_assert!(pending_splice.interactive_tx_signing_session.is_none()); + + log_info!(logger, "Splicing process started after splice_ack, new channel value {}, old {}, outgoing {}, channel_id {}", + post_channel_value, pre_channel_value, true, self.context.channel_id); + + // Build NegotiatingChannelView locally, similar to Channel::as_renegotiating_channel() + let mut negotiating_view = NegotiatingChannelView { + context: &mut self.context, + funding: &mut pending_splice.funding_scope.as_mut().unwrap(), // set above + funding_negotiation_context: &mut pending_splice.funding_negotiation_context, + interactive_tx_constructor: &mut pending_splice.interactive_tx_constructor, + interactive_tx_signing_session: &mut pending_splice.interactive_tx_signing_session, + holder_commitment_transaction_number: self.holder_commitment_point.transaction_number(), + is_splice: true, + }; + + // Start interactive funding negotiation, with the previous funding transaction as an extra shared input + let tx_msg_opt = negotiating_view + .begin_interactive_funding_tx_construction( + signer_provider, + entropy_source, + holder_node_id.clone(), + true, + None, + Some(prev_funding_input), + ) + .map_err(|err| { + ChannelError::Warn(format!("V2 channel rejected due to sender error, {:?}", err)) + })?; + Ok(tx_msg_opt) + } + + /// Get a transaction input that is the previous funding transaction + #[cfg(splicing)] + fn get_input_of_previous_funding( + pre_funding_transaction: &Option, pre_funding_txo: &Option, + ) -> Result<(TxIn, TransactionU16LenLimited), ChannelError> { + if let Some(pre_funding_transaction) = pre_funding_transaction { + if let Some(pre_funding_txo) = 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(), + )) + } } #[cfg(splicing)] @@ -11465,6 +11834,33 @@ where } } +/// Calculate total funding contributions, needed for interactive tx for splicing, +/// based on the current channel value and the splice contributions. +#[cfg(splicing)] +fn calculate_total_funding_contribution( + pre_channel_value: u64, our_splice_contribution: i64, their_splice_contribution: i64, + is_initiator: bool, +) -> Result<(u64, u64), ChannelError> { + // Initiator also adds the previous funding as input + let mut our_contribution_with_prev = our_splice_contribution; + let mut their_contribution_with_prev = their_splice_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_splice_contribution, their_splice_contribution + ))); + } + Ok((our_contribution_with_prev.abs() as u64, their_contribution_with_prev.abs() as u64)) +} + // A not-yet-funded channel using V2 channel establishment. pub(super) struct PendingV2Channel where @@ -11820,6 +12216,7 @@ where interactive_tx_constructor: &mut self.interactive_tx_constructor, interactive_tx_signing_session: &mut self.interactive_tx_signing_session, holder_commitment_transaction_number: self.unfunded_context.transaction_number(), + is_splice: false, } } } @@ -14762,4 +15159,76 @@ mod tests { ); } } + + #[cfg(splicing)] + fn get_pre_and_post( + pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64, + ) -> (u64, u64) { + use crate::ln::channel::PendingSplice; + + let post_channel_value = PendingSplice::compute_post_value( + pre_channel_value, + our_funding_contribution, + their_funding_contribution, + ); + (pre_channel_value, post_channel_value) + } + + #[cfg(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 02361438c26..bd31ac6a9a5 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4524,7 +4524,7 @@ where let mut res = Ok(()); PersistenceNotifierGuard::optionally_notify(self, || { let result = self.internal_splice_channel( - channel_id, counterparty_node_id, our_funding_contribution_satoshis, &our_funding_inputs, funding_feerate_per_kw, locktime + channel_id, counterparty_node_id, our_funding_contribution_satoshis, our_funding_inputs.clone(), funding_feerate_per_kw, locktime ); res = result; match res { @@ -4540,7 +4540,7 @@ where #[rustfmt::skip] fn internal_splice_channel( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, our_funding_contribution_satoshis: i64, - our_funding_inputs: &Vec<(TxIn, Transaction, Weight)>, + our_funding_inputs: Vec<(TxIn, Transaction, Weight)>, funding_feerate_per_kw: u32, locktime: Option, ) -> Result<(), APIError> { let per_peer_state = self.per_peer_state.read().unwrap(); @@ -10044,6 +10044,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!( @@ -10051,24 +10054,22 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ counterparty_node_id, msg.channel_id, ), msg.channel_id)), hash_map::Entry::Occupied(mut chan_entry) => { - if let Some(chan) = chan_entry.get_mut().as_funded_mut() { - let splice_ack_msg = try_channel_entry!(self, peer_state, chan.splice_init(msg), chan_entry); + if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() { + let splice_ack_msg = try_channel_entry!(self, peer_state, + funded_channel.splice_init( + msg, our_funding_contribution, &self.signer_provider, &self.entropy_source, + &self.get_our_node_id(), &self.logger + ), chan_entry); peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceAck { node_id: *counterparty_node_id, msg: splice_ack_msg, }); + Ok(()) } else { - return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel is not funded, cannot be spliced".to_owned(), msg.channel_id)); + try_channel_entry!(self, peer_state, Err(ChannelError::close("Channel is not funded, cannot be spliced".into())), chan_entry) } }, - }; - - // 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). @@ -10086,26 +10087,26 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ // 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!( + hash_map::Entry::Vacant(_) => Err(MsgHandleErrInternal::send_err_msg_no_close(format!( "Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id ), msg.channel_id)), hash_map::Entry::Occupied(mut chan_entry) => { - if let Some(chan) = chan_entry.get_mut().as_funded_mut() { - try_channel_entry!(self, peer_state, chan.splice_ack(msg), chan_entry); + if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() { + let tx_msg_opt = try_channel_entry!(self, peer_state, + funded_channel.splice_ack( + msg, &self.signer_provider, &self.entropy_source, + &self.get_our_node_id(), &self.logger + ), chan_entry); + 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(()) } else { - return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel is not funded, cannot splice".to_owned(), msg.channel_id)); + try_channel_entry!(self, peer_state, Err(ChannelError::close("Channel is not funded, cannot be spliced".into())), chan_entry) } }, - }; - - // 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)) + } } #[cfg(splicing)] diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index ee991a0ae8c..f1b9cf4af62 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -1740,7 +1740,6 @@ impl InteractiveTxConstructor { /// `Ok(None)` /// - Inputs are not sufficent to cover contribution and fees: /// `Err(AbortReason::InsufficientFees)` -#[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: &Vec<(TxIn, TransactionU16LenLimited)>, funding_outputs: &Vec, diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index 33f5a500789..65f01a54092 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -48,10 +48,15 @@ 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 // Amount being added to the channel through the splice-in let splice_in_sats = 20_000; + let post_splice_channel_value = channel_value_sat + splice_in_sats; let funding_feerate_per_kw = 1024; // Create additional inputs @@ -121,17 +126,129 @@ fn test_v1_splice_in() { assert!(channel.is_usable); assert!(channel.is_channel_ready); assert_eq!(channel.channel_value_satoshis, channel_value_sat); - assert_eq!( - channel.outbound_capacity_msat, - 1000 * (channel_value_sat - channel_reserve_amnt_sat) - ); + assert_eq!(channel.outbound_capacity_msat, exp_balance1 - 1000 * channel_reserve_amnt_sat); assert!(channel.funding_txo.is_some()); assert!(channel.confirmations.unwrap() > 0); } - let _error_msg = get_err_msg(initiator_node, &acceptor_node.node.get_our_node_id()); + // exp_balance1 += 1000 * splice_in_sats; // increase in balance + + // Negotiate transaction inputs and outputs + + // First input + let tx_add_input_msg = get_event_msg!( + &initiator_node, + MessageSendEvent::SendTxAddInput, + acceptor_node.node.get_our_node_id() + ); + let value = tx_add_input_msg.prevtx.as_transaction().output + [tx_add_input_msg.prevtx_out as usize] + .value + .to_sat(); + // check which input is this + let inputs_seen_in_reverse = if value == extra_splice_funding_input_sats { + true + } else if value == channel_value_sat { + false + } else { + panic!("Unexpected input with value {}", 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 inputs_seen_in_reverse { channel_value_sat } else { extra_splice_funding_input_sats }; + let tx_add_input2_msg = get_event_msg!( + &initiator_node, + MessageSendEvent::SendTxAddInput, + acceptor_node.node.get_our_node_id() + ); + assert_eq!( + tx_add_input2_msg.prevtx.as_transaction().output[tx_add_input2_msg.prevtx_out as usize] + .value + .to_sat(), + exp_value + ); + + let _res = acceptor_node + .node + .handle_tx_add_input(initiator_node.node.get_our_node_id(), &tx_add_input2_msg); + let tx_complete_msg = get_event_msg!( + acceptor_node, + MessageSendEvent::SendTxComplete, + initiator_node.node.get_our_node_id() + ); + + let _res = initiator_node + .node + .handle_tx_complete(acceptor_node.node.get_our_node_id(), &tx_complete_msg); + + // TxAddOutput for the change output + let tx_add_output_msg = get_event_msg!( + &initiator_node, + MessageSendEvent::SendTxAddOutput, + acceptor_node.node.get_our_node_id() + ); + assert!(tx_add_output_msg.script.is_p2wsh()); + assert_eq!(tx_add_output_msg.sats, post_splice_channel_value); + + let _res = acceptor_node + .node + .handle_tx_add_output(initiator_node.node.get_our_node_id(), &tx_add_output_msg); + let tx_complete_msg = get_event_msg!( + &acceptor_node, + MessageSendEvent::SendTxComplete, + initiator_node.node.get_our_node_id() + ); + + let _res = initiator_node + .node + .handle_tx_complete(acceptor_node.node.get_our_node_id(), &tx_complete_msg); + // TxAddOutput for the splice funding + let tx_add_output2_msg = get_event_msg!( + &initiator_node, + MessageSendEvent::SendTxAddOutput, + acceptor_node.node.get_our_node_id() + ); + assert!(tx_add_output2_msg.script.is_p2wpkh()); + assert_eq!(tx_add_output2_msg.sats, 14094); // 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_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 fully implemented + let _res = initiator_node + .node + .handle_tx_complete(acceptor_node.node.get_our_node_id(), &tx_complete_msg); + let events = initiator_node.node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 2); + match events[0] { + MessageSendEvent::SendTxComplete { .. } => {}, + _ => panic!("Unexpected event {:?}", events[0]), + } + match events[1] { + MessageSendEvent::HandleError { .. } => {}, + _ => panic!("Unexpected event {:?}", events[1]), + } - // TODO(splicing): continue with splice transaction negotiation + // TODO(splicing): Continue with commitment flow, new tx confirmation // === Close channel, cooperatively initiator_node.node.close_channel(&channel_id, &acceptor_node.node.get_our_node_id()).unwrap(); From 3577418f198c8a56c1749ec035ac4f76dad331ff Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:07:25 +0200 Subject: [PATCH 5/5] Formatting, remove rustfmt skips --- lightning/src/ln/channel.rs | 58 +++++++++++++++++--------- lightning/src/ln/channelmanager.rs | 66 ++++++++++++++++++------------ 2 files changed, 79 insertions(+), 45 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 2892542d7bf..9160a5be032 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -10015,35 +10015,41 @@ where /// - `our_funding_inputs`: the inputs we contribute to the new funding transaction. /// Includes the witness weight for this input (e.g. P2WPKH_WITNESS_WEIGHT=109 for typical P2WPKH inputs). #[cfg(splicing)] - #[rustfmt::skip] - pub fn splice_channel(&mut self, our_funding_contribution_satoshis: i64, - our_funding_inputs: Vec<(TxIn, Transaction, Weight)>, - funding_feerate_per_kw: u32, locktime: u32, + pub fn splice_channel( + &mut self, our_funding_contribution_satoshis: i64, + our_funding_inputs: Vec<(TxIn, Transaction, Weight)>, funding_feerate_per_kw: u32, + locktime: u32, ) -> Result { // Check if a splice has been initiated already. // Note: only a single outstanding splice is supported (per spec) if let Some(pending_splice) = &self.pending_splice { - return Err(APIError::APIMisuseError { err: format!( + return Err(APIError::APIMisuseError { + err: format!( "Channel {} cannot be spliced, as it has already a splice pending (contribution {})", self.context.channel_id(), pending_splice.our_funding_contribution, - )}); + ), + }); } if !self.context.is_live() { - return Err(APIError::APIMisuseError { err: format!( - "Channel {} cannot be spliced, as channel is not live", - self.context.channel_id() - )}); + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} cannot be spliced, as channel is not live", + self.context.channel_id() + ), + }); } // TODO(splicing): check for quiescence if our_funding_contribution_satoshis < 0 { - return Err(APIError::APIMisuseError { err: format!( + return Err(APIError::APIMisuseError { + err: format!( "TODO(splicing): Splice-out not supported, only splice in; channel ID {}, contribution {}", self.context.channel_id(), our_funding_contribution_satoshis, - )}); + ), + }); } // TODO(splicing): Once splice-out is supported, check that channel balance does not go below 0 @@ -10053,20 +10059,30 @@ where // (Cannot test for miminum required post-splice channel value) // Check that inputs are sufficient to cover our contribution. - let _fee = check_v2_funding_inputs_sufficient(our_funding_contribution_satoshis, &our_funding_inputs, true, true, funding_feerate_per_kw) - .map_err(|err| APIError::APIMisuseError { err: format!( + let _fee = check_v2_funding_inputs_sufficient( + our_funding_contribution_satoshis, + &our_funding_inputs, + true, + true, + funding_feerate_per_kw, + ) + .map_err(|err| APIError::APIMisuseError { + err: format!( "Insufficient inputs for splicing; channel ID {}, err {}", - self.context.channel_id(), err, - )})?; + self.context.channel_id(), + err, + ), + })?; // Convert inputs let mut funding_inputs = Vec::new(); for (tx_in, tx, _w) in our_funding_inputs.into_iter() { - let tx16 = TransactionU16LenLimited::new(tx.clone()).map_err(|_e| APIError::APIMisuseError { err: format!("Too large transaction")})?; + let tx16 = TransactionU16LenLimited::new(tx.clone()) + .map_err(|_e| APIError::APIMisuseError { err: format!("Too large transaction") })?; funding_inputs.push((tx_in.clone(), tx16)); } let funding_negotiation_context = FundingNegotiationContext { - our_funding_satoshis: 0, // set at later phase + our_funding_satoshis: 0, // set at later phase their_funding_satoshis: None, // set at later phase funding_tx_locktime: LockTime::from_consensus(locktime), funding_feerate_sat_per_1000_weight: funding_feerate_per_kw, @@ -10082,7 +10098,11 @@ where received_funding_txid: None, }); - let msg = self.get_splice_init(our_funding_contribution_satoshis, funding_feerate_per_kw, locktime); + let msg = self.get_splice_init( + our_funding_contribution_satoshis, + funding_feerate_per_kw, + locktime, + ); Ok(msg) } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index bd31ac6a9a5..dd7cb53d05b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4537,16 +4537,22 @@ where /// See [`splice_channel`] #[cfg(splicing)] - #[rustfmt::skip] fn internal_splice_channel( - &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, our_funding_contribution_satoshis: i64, - our_funding_inputs: Vec<(TxIn, Transaction, Weight)>, - funding_feerate_per_kw: u32, locktime: Option, + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + our_funding_contribution_satoshis: i64, + our_funding_inputs: Vec<(TxIn, Transaction, Weight)>, funding_feerate_per_kw: u32, + locktime: Option, ) -> Result<(), APIError> { let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = match 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 peer_state_mutex = match 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 + ), + } + }) { Ok(p) => p, Err(e) => return Err(e), }; @@ -4559,7 +4565,12 @@ where hash_map::Entry::Occupied(mut chan_phase_entry) => { let locktime = locktime.unwrap_or_else(|| self.current_best_block().height); if let Some(chan) = chan_phase_entry.get_mut().as_funded_mut() { - let msg = chan.splice_channel(our_funding_contribution_satoshis, our_funding_inputs, funding_feerate_per_kw, locktime)?; + let msg = chan.splice_channel( + our_funding_contribution_satoshis, + our_funding_inputs, + funding_feerate_per_kw, + locktime, + )?; peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceInit { node_id: *counterparty_node_id, msg, @@ -4570,18 +4581,16 @@ where err: format!( "Channel with id {} is not funded, cannot splice it", channel_id - ) + ), }) } }, - hash_map::Entry::Vacant(_) => { - Err(APIError::ChannelUnavailable { - err: format!( - "Channel with id {} not found for the passed counterparty node_id {}", - channel_id, counterparty_node_id, - ) - }) - }, + hash_map::Entry::Vacant(_) => Err(APIError::ChannelUnavailable { + err: format!( + "Channel with id {} not found for the passed counterparty node_id {}", + channel_id, counterparty_node_id, + ), + }), } } @@ -8883,18 +8892,23 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } - #[rustfmt::skip] - fn internal_tx_msg) -> Result>( - &self, counterparty_node_id: &PublicKey, channel_id: ChannelId, tx_msg_handler: HandleTxMsgFn + fn internal_tx_msg< + HandleTxMsgFn: Fn(&mut Channel) -> Result, + >( + &self, counterparty_node_id: &PublicKey, channel_id: ChannelId, + tx_msg_handler: HandleTxMsgFn, ) -> 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), - channel_id) - })?; + 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 + ), + channel_id, + ) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(channel_id) {