diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index b5b77972a6c..392377a2608 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, @@ -55,11 +57,13 @@ use crate::ln::channelmanager::{ PaymentClaimDetails, PendingHTLCInfo, PendingHTLCStatus, RAACommitmentOrder, SentHTLCId, BREAKDOWN_TIMEOUT, MAX_LOCAL_BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA, }; +#[cfg(splicing)] +use crate::ln::interactivetxs::{ + calculate_change_output_value, AbortReason, InteractiveTxMessageSend, +}; use crate::ln::interactivetxs::{ - calculate_change_output_value, get_output_weight, AbortReason, HandleTxCompleteResult, - InteractiveTxConstructor, InteractiveTxConstructorArgs, InteractiveTxMessageSend, - InteractiveTxMessageSendResult, InteractiveTxSigningSession, SharedOwnedOutput, - TX_COMMON_FIELDS_WEIGHT, + get_output_weight, InteractiveTxConstructor, InteractiveTxConstructorArgs, + InteractiveTxSigningSession, SharedOwnedInput, SharedOwnedOutput, TX_COMMON_FIELDS_WEIGHT, }; use crate::ln::msgs; use crate::ln::msgs::{ClosingSigned, ClosingSignedFeeRange, DecodeError, OnionErrorPacket}; @@ -1727,6 +1731,15 @@ where } } + pub fn interactive_tx_constructor_mut(&mut self) -> Option<&mut InteractiveTxConstructor> { + match &mut self.phase { + ChannelPhase::UnfundedV2(chan) => chan.interactive_tx_constructor.as_mut(), + #[cfg(splicing)] + ChannelPhase::Funded(chan) => chan.interactive_tx_constructor_mut(), + _ => None, + } + } + #[rustfmt::skip] pub fn funding_signed( &mut self, msg: &msgs::FundingSigned, best_block: BestBlock, signer_provider: &SP, logger: &L @@ -1760,16 +1773,71 @@ where } pub fn funding_tx_constructed( - &mut self, signing_session: InteractiveTxSigningSession, logger: &L, + &mut self, logger: &L, ) -> Result<(msgs::CommitmentSigned, Option), ChannelError> where L::Target: Logger, { - if let ChannelPhase::UnfundedV2(chan) = &mut self.phase { - let logger = WithChannelContext::from(logger, &chan.context, None); - chan.funding_tx_constructed(signing_session, &&logger) - } else { - Err(ChannelError::Warn("Got a tx_complete message with no interactive transaction construction expected or in-progress".to_owned())) + let logger = WithChannelContext::from(logger, self.context(), None); + match &mut self.phase { + ChannelPhase::UnfundedV2(chan) => { + let mut signing_session = chan + .interactive_tx_constructor + .take() + .expect("PendingV2Channel::interactive_tx_constructor should be set") + .into_signing_session(); + let (commitment_signed, event) = chan.context.funding_tx_constructed( + &mut chan.funding, + &mut signing_session, + false, + chan.unfunded_context.transaction_number(), + &&logger, + )?; + + chan.interactive_tx_signing_session = Some(signing_session); + + return Ok((commitment_signed, event)); + }, + #[cfg(splicing)] + ChannelPhase::Funded(chan) => { + if let Some(pending_splice) = chan.pending_splice.as_mut() { + if let Some(funding_negotiation) = pending_splice.funding_negotiation.take() { + if let FundingNegotiation::Pending( + mut funding, + interactive_tx_constructor, + ) = funding_negotiation + { + let mut signing_session = + interactive_tx_constructor.into_signing_session(); + let (commitment_signed, event) = chan.context.funding_tx_constructed( + &mut funding, + &mut signing_session, + true, + chan.holder_commitment_point.transaction_number(), + &&logger, + )?; + + chan.interactive_tx_signing_session = Some(signing_session); + pending_splice.funding_negotiation = + Some(FundingNegotiation::AwaitingSignatures(funding)); + + return Ok((commitment_signed, event)); + } else { + // Replace the taken state + pending_splice.funding_negotiation = Some(funding_negotiation); + } + } + } + + return Err(ChannelError::Warn( + "Got a transaction negotiation message in an invalid state".to_owned(), + )); + }, + _ => { + return Err(ChannelError::Warn( + "Got a transaction negotiation message in an invalid phase".to_owned(), + )) + }, } } @@ -1821,7 +1889,8 @@ where ChannelPhase::Funded(mut funded_channel) => { #[cfg(splicing)] let has_negotiated_pending_splice = funded_channel.pending_splice.as_ref() - .map(|pending_splice| pending_splice.funding.is_some()) + .and_then(|pending_splice| pending_splice.funding_negotiation.as_ref()) + .map(|funding_negotiation| funding_negotiation.as_funding().is_some()) .unwrap_or(false); #[cfg(splicing)] let session_received_commitment_signed = funded_channel @@ -2165,13 +2234,149 @@ impl FundingScope { pub fn get_short_channel_id(&self) -> Option { self.short_channel_id } + + /// Constructs a `FundingScope` for splicing a channel. + #[cfg(splicing)] + fn for_splice( + prev_funding: &Self, context: &ChannelContext, our_funding_contribution_sats: i64, + their_funding_contribution_sats: i64, counterparty_funding_pubkey: PublicKey, + ) -> Result + where + SP::Target: SignerProvider, + { + let post_channel_value = prev_funding.compute_post_splice_value( + our_funding_contribution_sats, + their_funding_contribution_sats, + ); + + let post_value_to_self_msat = AddSigned::checked_add_signed( + prev_funding.value_to_self_msat, + our_funding_contribution_sats * 1000, + ); + debug_assert!(post_value_to_self_msat.is_some()); + let post_value_to_self_msat = post_value_to_self_msat.unwrap(); + + // Rotate the pubkeys using the prev_funding_txid as a tweak + let prev_funding_txid = prev_funding.get_funding_txid(); + let holder_pubkeys = context.holder_pubkeys(prev_funding_txid); + + let channel_parameters = &prev_funding.channel_transaction_parameters; + let mut post_channel_transaction_parameters = ChannelTransactionParameters { + holder_pubkeys, + holder_selected_contest_delay: channel_parameters.holder_selected_contest_delay, + // The 'outbound' attribute doesn't change, even if the splice initiator is the other node + is_outbound_from_holder: channel_parameters.is_outbound_from_holder, + counterparty_parameters: channel_parameters.counterparty_parameters.clone(), + funding_outpoint: None, // filled later + splice_parent_funding_txid: prev_funding_txid, + channel_type_features: channel_parameters.channel_type_features.clone(), + channel_value_satoshis: post_channel_value, + }; + post_channel_transaction_parameters + .counterparty_parameters + .as_mut() + .expect("counterparty_parameters should be set") + .pubkeys + .funding_pubkey = counterparty_funding_pubkey; + + // New reserve values are based on the new channel value and are 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, + }) + } + + /// Compute the post-splice channel value from each counterparty's contributions. + #[cfg(splicing)] + pub(super) fn compute_post_splice_value( + &self, our_funding_contribution: i64, their_funding_contribution: i64, + ) -> u64 { + AddSigned::saturating_add_signed( + self.get_value_satoshis(), + our_funding_contribution.saturating_add(their_funding_contribution), + ) + } + + /// Returns a `SharedOwnedInput` for using this `FundingScope` as the input to a new splice. + #[cfg(splicing)] + fn to_splice_funding_input(&self) -> SharedOwnedInput { + let funding_txo = self.get_funding_txo().expect("funding_txo should be set"); + let input = TxIn { + previous_output: funding_txo.into_bitcoin_outpoint(), + script_sig: ScriptBuf::new(), + sequence: Sequence::ZERO, + witness: Witness::new(), + }; + + let prev_output = TxOut { + value: Amount::from_sat(self.get_value_satoshis()), + script_pubkey: self.get_funding_redeemscript().to_p2wsh(), + }; + + let local_owned = self.value_to_self_msat / 1000; + + SharedOwnedInput::new(input, prev_output, local_owned) + } +} + +// TODO: Remove once MSRV is at least 1.66 +#[cfg(splicing)] +trait AddSigned { + fn checked_add_signed(self, rhs: i64) -> Option; + fn saturating_add_signed(self, rhs: i64) -> u64; +} + +#[cfg(splicing)] +impl AddSigned for u64 { + fn checked_add_signed(self, rhs: i64) -> Option { + if rhs >= 0 { + self.checked_add(rhs as u64) + } else { + self.checked_sub(rhs.unsigned_abs()) + } + } + + fn saturating_add_signed(self, rhs: i64) -> u64 { + if rhs >= 0 { + self.saturating_add(rhs as u64) + } else { + self.saturating_sub(rhs.unsigned_abs()) + } + } } -/// Info about a pending splice, used in the pre-splice channel +/// Info about a pending splice #[cfg(splicing)] struct PendingSplice { - pub our_funding_contribution: i64, - funding: Option, + funding_negotiation: Option, /// The funding txid used in the `splice_locked` sent to the counterparty. sent_funding_txid: Option, @@ -2180,6 +2385,24 @@ struct PendingSplice { received_funding_txid: Option, } +#[cfg(splicing)] +enum FundingNegotiation { + AwaitingAck(FundingNegotiationContext), + Pending(FundingScope, InteractiveTxConstructor), + AwaitingSignatures(FundingScope), +} + +#[cfg(splicing)] +impl FundingNegotiation { + fn as_funding(&self) -> Option<&FundingScope> { + match self { + FundingNegotiation::AwaitingAck(_) => None, + FundingNegotiation::Pending(funding, _) => Some(funding), + FundingNegotiation::AwaitingSignatures(funding) => Some(funding), + } + } +} + #[cfg(splicing)] impl PendingSplice { fn check_get_splice_locked( @@ -2752,251 +2975,6 @@ where } } -impl PendingV2Channel -where - SP::Target: SignerProvider, -{ - /// Prepare and start interactive transaction negotiation. - /// `change_destination_opt` - Optional destination for optional change; if None, - /// 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, - ) -> Result, AbortReason> - where ES::Target: EntropySource - { - debug_assert!(matches!(self.context.channel_state, ChannelState::NegotiatingFunding(_))); - 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); - - // TODO(splicing): Add prev funding tx as input, must be provided as a parameter - - // Add output for funding tx - // Note: For the error case when the inputs are insufficient, it will be handled after - // the `calculate_change_output_value` call below - let mut funding_outputs = Vec::new(); - - let shared_funding_output = TxOut { - value: Amount::from_sat(self.funding.get_value_satoshis()), - script_pubkey: self.funding.get_funding_redeemscript().to_p2wsh(), - }; - - // Optionally add change output - let change_script = if let Some(script) = change_destination_opt { - script - } else { - 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.dual_funding_context.our_funding_satoshis, - &funding_inputs, None, - &shared_funding_output.script_pubkey, &funding_outputs, - self.dual_funding_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 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_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 { - change_output.value = Amount::from_sat(change_value_decreased_with_fee); - funding_outputs.push(change_output); - } - } - - let constructor_args = InteractiveTxConstructorArgs { - entropy_source, - holder_node_id, - counterparty_node_id: self.context.counterparty_node_id, - channel_id: self.context.channel_id(), - feerate_sat_per_kw: self.dual_funding_context.funding_feerate_sat_per_1000_weight, - is_initiator: self.funding.is_outbound(), - funding_tx_locktime: self.dual_funding_context.funding_tx_locktime, - inputs_to_contribute: funding_inputs, - shared_funding_input: None, - shared_funding_output: SharedOwnedOutput::new(shared_funding_output, self.dual_funding_context.our_funding_satoshis), - outputs_to_contribute: funding_outputs, - }; - let mut tx_constructor = InteractiveTxConstructor::new(constructor_args)?; - let msg = tx_constructor.take_initiator_first_message(); - - self.interactive_tx_constructor = Some(tx_constructor); - - Ok(msg) - } - - pub fn tx_add_input(&mut self, msg: &msgs::TxAddInput) -> InteractiveTxMessageSendResult { - InteractiveTxMessageSendResult(match &mut self.interactive_tx_constructor { - Some(ref mut tx_constructor) => tx_constructor - .handle_tx_add_input(msg) - .map_err(|reason| reason.into_tx_abort_msg(self.context.channel_id())), - None => Err(msgs::TxAbort { - channel_id: self.context.channel_id(), - data: b"No interactive transaction negotiation in progress".to_vec(), - }), - }) - } - - pub fn tx_add_output(&mut self, msg: &msgs::TxAddOutput) -> InteractiveTxMessageSendResult { - InteractiveTxMessageSendResult(match &mut 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())), - None => Err(msgs::TxAbort { - channel_id: self.context.channel_id(), - data: b"No interactive transaction negotiation in progress".to_vec(), - }), - }) - } - - pub fn tx_remove_input(&mut self, msg: &msgs::TxRemoveInput) -> InteractiveTxMessageSendResult { - InteractiveTxMessageSendResult(match &mut 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())), - None => Err(msgs::TxAbort { - channel_id: self.context.channel_id(), - data: b"No interactive transaction negotiation in progress".to_vec(), - }), - }) - } - - pub fn tx_remove_output( - &mut self, msg: &msgs::TxRemoveOutput, - ) -> InteractiveTxMessageSendResult { - InteractiveTxMessageSendResult(match &mut 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())), - None => Err(msgs::TxAbort { - channel_id: self.context.channel_id(), - data: b"No interactive transaction negotiation in progress".to_vec(), - }), - }) - } - - 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, - None => { - let tx_abort = msgs::TxAbort { - channel_id: msg.channel_id, - data: b"No interactive transaction negotiation in progress".to_vec(), - }; - return HandleTxCompleteResult(Err(tx_abort)); - }, - }; - - let tx_complete = match tx_constructor.handle_tx_complete(msg) { - Ok(tx_complete) => tx_complete, - Err(reason) => { - return HandleTxCompleteResult(Err(reason.into_tx_abort_msg(msg.channel_id))) - }, - }; - - HandleTxCompleteResult(Ok(tx_complete)) - } - - #[rustfmt::skip] - pub 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.dual_funding_context.our_funding_satoshis; - let transaction_number = self.unfunded_context.transaction_number(); - - let mut output_index = None; - let expected_spk = self.funding.get_funding_redeemscript().to_p2wsh(); - for (idx, outp) in signing_session.unsigned_tx().outputs().enumerate() { - if outp.script_pubkey() == &expected_spk && outp.value() == self.funding.get_value_satoshis() { - if output_index.is_some() { - let msg = "Multiple outputs matched the expected script and value"; - let reason = ClosureReason::ProcessingError { err: msg.to_owned() }; - return Err(ChannelError::Close((msg.to_owned(), reason))); - } - output_index = Some(idx as u16); - } - } - let outpoint = if let Some(output_index) = output_index { - OutPoint { txid: signing_session.unsigned_tx().compute_txid(), index: output_index } - } else { - let msg = "No output matched the funding script_pubkey"; - let reason = ClosureReason::ProcessingError { err: msg.to_owned() }; - return Err(ChannelError::Close((msg.to_owned(), reason))); - }; - self.funding.channel_transaction_parameters.funding_outpoint = Some(outpoint); - - self.context.assert_no_commitment_advancement(transaction_number, "initial commitment_signed"); - let commitment_signed = self.context.get_initial_commitment_signed(&self.funding, logger); - let commitment_signed = match commitment_signed { - Ok(commitment_signed) => commitment_signed, - Err(e) => { - self.funding.channel_transaction_parameters.funding_outpoint = None; - return Err(e) - }, - }; - - let funding_ready_for_sig_event = if signing_session.local_inputs_count() == 0 { - debug_assert_eq!(our_funding_satoshis, 0); - if signing_session.provide_holder_witnesses(self.context.channel_id, Vec::new()).is_err() { - debug_assert!( - false, - "Zero inputs were provided & zero witnesses were provided, but a count mismatch was somehow found", - ); - let msg = "V2 channel rejected due to sender error"; - let reason = ClosureReason::ProcessingError { err: msg.to_owned() }; - return Err(ChannelError::Close((msg.to_owned(), reason))); - } - None - } else { - // TODO(dual_funding): Send event for signing if we've contributed funds. - // Inform the user that SIGHASH_ALL must be used for all signatures when contributing - // inputs/signatures. - // Also warn the user that we don't do anything to prevent the counterparty from - // providing non-standard witnesses which will prevent the funding transaction from - // confirming. This warning must appear in doc comments wherever the user is contributing - // funds, whether they are initiator or acceptor. - // - // The following warning can be used when the APIs allowing contributing inputs become available: - //
- // WARNING: LDK makes no attempt to prevent the counterparty from using non-standard inputs which - // will prevent the funding transaction from being relayed on the bitcoin network and hence being - // confirmed. - //
- debug_assert!( - false, - "We don't support users providing inputs but somehow we had more than zero inputs", - ); - let msg = "V2 channel rejected due to sender error"; - let reason = ClosureReason::ProcessingError { err: msg.to_owned() }; - return Err(ChannelError::Close((msg.to_owned(), reason))); - }; - - let mut channel_state = ChannelState::FundingNegotiated(FundingNegotiatedFlags::new()); - channel_state.set_interactive_signing(); - self.context.channel_state = channel_state; - - // Clear the interactive transaction constructor - self.interactive_tx_constructor.take(); - self.interactive_tx_signing_session = Some(signing_session); - - Ok((commitment_signed, funding_ready_for_sig_event)) - } -} - impl ChannelContext where SP::Target: SignerProvider, @@ -3789,6 +3767,17 @@ where return &mut self.holder_signer; } + /// Returns holder pubkeys to use for the channel. + #[cfg(splicing)] + fn holder_pubkeys(&self, prev_funding_txid: Option) -> ChannelPublicKeys { + match &self.holder_signer { + ChannelSignerType::Ecdsa(ecdsa) => ecdsa.pubkeys(prev_funding_txid, &self.secp_ctx), + // TODO (taproot|arik) + #[cfg(taproot)] + _ => todo!(), + } + } + /// Only allowed immediately after deserialization if get_outbound_scid_alias returns 0, /// indicating we were written by LDK prior to 0.0.106 which did not set outbound SCID aliases /// or prior to any channel actions during `Channel` initialization. @@ -5480,6 +5469,97 @@ where Ok(()) } + #[rustfmt::skip] + fn funding_tx_constructed( + &mut self, funding: &mut FundingScope, signing_session: &mut InteractiveTxSigningSession, + is_splice: bool, holder_commitment_transaction_number: u64, logger: &L + ) -> Result<(msgs::CommitmentSigned, Option), ChannelError> + where + L::Target: Logger + { + let mut output_index = None; + let expected_spk = funding.get_funding_redeemscript().to_p2wsh(); + for (idx, outp) in signing_session.unsigned_tx().outputs().enumerate() { + if outp.script_pubkey() == &expected_spk && outp.value() == funding.get_value_satoshis() { + if output_index.is_some() { + let msg = "Multiple outputs matched the expected script and value"; + let reason = ClosureReason::ProcessingError { err: msg.to_owned() }; + return Err(ChannelError::Close((msg.to_owned(), reason))); + } + output_index = Some(idx as u16); + } + } + let outpoint = if let Some(output_index) = output_index { + OutPoint { txid: signing_session.unsigned_tx().compute_txid(), index: output_index } + } else { + let msg = "No output matched the funding script_pubkey"; + let reason = ClosureReason::ProcessingError { err: msg.to_owned() }; + return Err(ChannelError::Close((msg.to_owned(), reason))); + }; + funding + .channel_transaction_parameters.funding_outpoint = Some(outpoint); + + if is_splice { + let message = "TODO Forced error, incomplete implementation".to_owned(); + // TODO(splicing) Forced error, as the use case is not complete + return Err(ChannelError::Close(( + message.clone(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false), message } + ))); + } + + self.assert_no_commitment_advancement(holder_commitment_transaction_number, "initial commitment_signed"); + let commitment_signed = self.get_initial_commitment_signed(&funding, logger); + let commitment_signed = match commitment_signed { + Ok(commitment_signed) => commitment_signed, + Err(e) => { + funding.channel_transaction_parameters.funding_outpoint = None; + return Err(e) + }, + }; + + let funding_ready_for_sig_event = if signing_session.local_inputs_count() == 0 { + if signing_session.provide_holder_witnesses(self.channel_id, Vec::new()).is_err() { + debug_assert!( + false, + "Zero inputs were provided & zero witnesses were provided, but a count mismatch was somehow found", + ); + let msg = "V2 channel rejected due to sender error"; + let reason = ClosureReason::ProcessingError { err: msg.to_owned() }; + return Err(ChannelError::Close((msg.to_owned(), reason))); + } + None + } else { + // TODO(dual_funding): Send event for signing if we've contributed funds. + // Inform the user that SIGHASH_ALL must be used for all signatures when contributing + // inputs/signatures. + // Also warn the user that we don't do anything to prevent the counterparty from + // providing non-standard witnesses which will prevent the funding transaction from + // confirming. This warning must appear in doc comments wherever the user is contributing + // funds, whether they are initiator or acceptor. + // + // The following warning can be used when the APIs allowing contributing inputs become available: + //
+ // WARNING: LDK makes no attempt to prevent the counterparty from using non-standard inputs which + // will prevent the funding transaction from being relayed on the bitcoin network and hence being + // confirmed. + //
+ debug_assert!( + false, + "We don't support users providing inputs but somehow we had more than zero inputs", + ); + let msg = "V2 channel rejected due to sender error"; + let reason = ClosureReason::ProcessingError { err: msg.to_owned() }; + return Err(ChannelError::Close((msg.to_owned(), reason))); + }; + + let mut channel_state = ChannelState::FundingNegotiated(FundingNegotiatedFlags::new()); + channel_state.set_interactive_signing(); + self.channel_state = channel_state; + + Ok((commitment_signed, funding_ready_for_sig_event)) + } + /// Asserts that the commitment tx numbers have not advanced from their initial number. #[rustfmt::skip] fn assert_no_commitment_advancement(&self, holder_commitment_transaction_number: u64, msg_name: &str) { @@ -5853,30 +5933,116 @@ 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 { + /// Whether we initiated the funding negotiation. + pub is_initiator: bool, /// The amount in satoshis we will be contributing to the channel. - pub our_funding_satoshis: u64, + pub our_funding_contribution_satoshis: i64, /// 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, + pub their_funding_contribution_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, /// The feerate set by the initiator to be used for the funding transaction. #[allow(dead_code)] // TODO(dual_funding): Remove once V2 channels is enabled. pub funding_feerate_sat_per_1000_weight: u32, + /// The input spending the previous funding output, if this is a splice. + #[allow(dead_code)] // TODO(splicing): Remove once splicing is enabled. + pub shared_funding_input: Option, /// The funding inputs we will be contributing to the channel. - /// - /// Note that the `our_funding_satoshis` field is equal to the total value of `our_funding_inputs` - /// minus any fees paid for our contributed weight. This means that change will never be generated - /// and the maximum value possible will go towards funding the channel. - /// - /// Note that this field may be emptied once the interactive negotiation has been started. #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled. pub our_funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, } +impl FundingNegotiationContext { + /// Prepare and start interactive transaction negotiation. + /// `change_destination_opt` - Optional destination for optional change; if None, + /// default destination address is used. + /// If error occurs, it is caused by our side, not the counterparty. + #[cfg(splicing)] + fn into_interactive_tx_constructor( + self, context: &ChannelContext, funding: &FundingScope, signer_provider: &SP, + entropy_source: &ES, holder_node_id: PublicKey, change_destination_opt: Option, + ) -> Result + where + SP::Target: SignerProvider, + ES::Target: EntropySource, + { + debug_assert_eq!( + self.shared_funding_input.is_some(), + funding.channel_transaction_parameters.splice_parent_funding_txid.is_some(), + ); + + if self.shared_funding_input.is_some() { + debug_assert!(matches!(context.channel_state, ChannelState::ChannelReady(_))); + } else { + debug_assert!(matches!(context.channel_state, ChannelState::NegotiatingFunding(_))); + } + + // Add output for funding tx + // Note: For the error case when the inputs are insufficient, it will be handled after + // the `calculate_change_output_value` call below + let mut funding_outputs = Vec::new(); + + let shared_funding_output = TxOut { + value: Amount::from_sat(funding.get_value_satoshis()), + script_pubkey: funding.get_funding_redeemscript().to_p2wsh(), + }; + + // Optionally add change output + if self.our_funding_contribution_satoshis > 0 { + let change_value_opt = calculate_change_output_value( + &self, + self.shared_funding_input.is_some(), + &shared_funding_output.script_pubkey, + &funding_outputs, + context.holder_dust_limit_satoshis, + )?; + if let Some(change_value) = change_value_opt { + let change_script = if let Some(script) = change_destination_opt { + script + } else { + signer_provider.get_destination_script(context.channel_keys_id).map_err( + |_err| AbortReason::InternalError("Error getting destination 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_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 > context.holder_dust_limit_satoshis { + change_output.value = Amount::from_sat(change_value_decreased_with_fee); + funding_outputs.push(change_output); + } + } + } + + let constructor_args = InteractiveTxConstructorArgs { + entropy_source, + holder_node_id, + counterparty_node_id: context.counterparty_node_id, + channel_id: context.channel_id(), + feerate_sat_per_kw: self.funding_feerate_sat_per_1000_weight, + is_initiator: self.is_initiator, + funding_tx_locktime: self.funding_tx_locktime, + inputs_to_contribute: self.our_funding_inputs, + shared_funding_input: self.shared_funding_input, + shared_funding_output: SharedOwnedOutput::new( + shared_funding_output, + funding.value_to_self_msat / 1000, + ), + outputs_to_contribute: funding_outputs, + }; + InteractiveTxConstructor::new(constructor_args) + } +} + // Holder designates channel data owned for the benefit of the user client. // Counterparty designates channel data owned by the another channel participant entity. pub(super) struct FundedChannel @@ -6009,6 +6175,22 @@ where self.context.force_shutdown(&self.funding, closure_reason) } + #[cfg(splicing)] + fn interactive_tx_constructor_mut(&mut self) -> Option<&mut InteractiveTxConstructor> { + self.pending_splice + .as_mut() + .and_then(|pending_splice| pending_splice.funding_negotiation.as_mut()) + .and_then(|funding_negotiation| { + if let FundingNegotiation::Pending(_, interactive_tx_constructor) = + funding_negotiation + { + Some(interactive_tx_constructor) + } else { + None + } + }) + } + #[rustfmt::skip] fn check_remote_fee( channel_type: &ChannelTypeFeatures, fee_estimator: &LowerBoundedFeeEstimator, @@ -6808,7 +6990,8 @@ where let pending_splice_funding = self .pending_splice .as_ref() - .and_then(|pending_splice| pending_splice.funding.as_ref()) + .and_then(|pending_splice| pending_splice.funding_negotiation.as_ref()) + .and_then(|funding_negotiation| funding_negotiation.as_funding()) .expect("Funding must exist for negotiated pending splice"); let (holder_commitment_tx, _) = self.context.validate_commitment_signed( pending_splice_funding, @@ -10391,34 +10574,40 @@ 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(splice_info) = &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 - )}); + if self.pending_splice.is_some() { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} cannot be spliced, as it has already a splice pending", + self.context.channel_id(), + ), + }); } 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 @@ -10428,53 +10617,73 @@ 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) + .map_err(|_e| APIError::APIMisuseError { err: format!("Too large transaction") })?; + funding_inputs.push((tx_in, tx16)); + } + + let prev_funding_input = self.funding.to_splice_funding_input(); + let funding_negotiation_context = FundingNegotiationContext { + is_initiator: true, + our_funding_contribution_satoshis, + their_funding_contribution_satoshis: None, + funding_tx_locktime: LockTime::from_consensus(locktime), + funding_feerate_sat_per_1000_weight: funding_feerate_per_kw, + shared_funding_input: Some(prev_funding_input), + our_funding_inputs: funding_inputs, + }; self.pending_splice = Some(PendingSplice { - our_funding_contribution: our_funding_contribution_satoshis, - funding: None, + funding_negotiation: Some(FundingNegotiation::AwaitingAck(funding_negotiation_context)), sent_funding_txid: None, received_funding_txid: None, }); - let msg = self.get_splice_init(our_funding_contribution_satoshis, funding_feerate_per_kw, locktime); - Ok(msg) - } + // Rotate the pubkeys using the prev_funding_txid as a tweak + let prev_funding_txid = self.funding.get_funding_txid(); + let funding_pubkey = self.context.holder_pubkeys(prev_funding_txid).funding_pubkey; - /// Get the splice message that can be sent during splice initiation. - #[cfg(splicing)] - fn get_splice_init( - &self, our_funding_contribution_satoshis: i64, funding_feerate_per_kw: u32, locktime: u32, - ) -> msgs::SpliceInit { - // 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 funding_pubkey = self.funding.get_holder_pubkeys().funding_pubkey.clone(); - msgs::SpliceInit { + Ok(msgs::SpliceInit { channel_id: self.context.channel_id, funding_contribution_satoshis: our_funding_contribution_satoshis, funding_feerate_per_kw, locktime, funding_pubkey, require_confirmed_inputs: None, - } + }) } - /// 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, our_funding_contribution_satoshis: i64, + ) -> 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; + + // 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 self.pending_splice.is_some() { return Err(ChannelError::Warn(format!( - "Channel has already a splice pending, contribution {}", splice_info.our_funding_contribution, + "Channel {} already has a splice pending", + self.context.channel_id(), ))); } @@ -10482,50 +10691,191 @@ 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, ))); } + let splice_funding = FundingScope::for_splice( + &self.funding, + &self.context, + our_funding_contribution_satoshis, + their_funding_contribution_satoshis, + msg.funding_pubkey, + )?; + // TODO(splicing): Once splice acceptor can contribute, check that inputs are sufficient, // similarly to the check in `splice_channel`. // 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) + // TODO(splicing): Early check for reserve requirement + + Ok(splice_funding) + } + + /// See also [`validate_splice_init`] + #[cfg(splicing)] + pub(crate) fn splice_init( + &mut self, msg: &msgs::SpliceInit, our_funding_contribution_satoshis: i64, + signer_provider: &SP, entropy_source: &ES, holder_node_id: &PublicKey, logger: &L, + ) -> Result + where + ES::Target: EntropySource, + L::Target: Logger, + { + let splice_funding = self.validate_splice_init(msg, our_funding_contribution_satoshis)?; + + log_info!( + logger, + "Starting splice funding negotiation for channel {} after receiving splice_init; new channel value: {} sats (old: {} sats)", + self.context.channel_id, + splice_funding.get_value_satoshis(), + self.funding.get_value_satoshis(), + ); + + let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; + let prev_funding_input = self.funding.to_splice_funding_input(); + let funding_negotiation_context = FundingNegotiationContext { + is_initiator: false, + our_funding_contribution_satoshis, + their_funding_contribution_satoshis: Some(their_funding_contribution_satoshis), + funding_tx_locktime: LockTime::from_consensus(msg.locktime), + funding_feerate_sat_per_1000_weight: msg.funding_feerate_per_kw, + shared_funding_input: Some(prev_funding_input), + our_funding_inputs: Vec::new(), + }; + + let mut interactive_tx_constructor = funding_negotiation_context + .into_interactive_tx_constructor( + &self.context, + &splice_funding, + signer_provider, + entropy_source, + holder_node_id.clone(), + None, + ) + .map_err(|err| { + ChannelError::Warn(format!( + "Failed to start interactive transaction construction, {:?}", + err + )) + })?; + debug_assert!(interactive_tx_constructor.take_initiator_first_message().is_none()); + + let funding_pubkey = splice_funding.get_holder_pubkeys().funding_pubkey; + + self.pending_splice = Some(PendingSplice { + funding_negotiation: Some(FundingNegotiation::Pending( + splice_funding, + interactive_tx_constructor, + )), + received_funding_txid: None, + sent_funding_txid: None, + }); - // 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 { + Ok(msgs::SpliceAck { channel_id: self.context.channel_id, funding_contribution_satoshis: our_funding_contribution_satoshis, - funding_pubkey: self.funding.get_holder_pubkeys().funding_pubkey, + funding_pubkey, require_confirmed_inputs: None, - }; - // 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> { - // check if splice is pending - if self.pending_splice.is_none() { + 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, + { + 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 + + let funding_negotiation_context = match pending_splice.funding_negotiation.take() { + Some(FundingNegotiation::AwaitingAck(context)) => context, + Some(FundingNegotiation::Pending(funding, constructor)) => { + pending_splice.funding_negotiation = + Some(FundingNegotiation::Pending(funding, constructor)); + return Err(ChannelError::Warn(format!( + "Got unexpected splice_ack; splice negotiation already in progress" + ))); + }, + Some(FundingNegotiation::AwaitingSignatures(funding)) => { + pending_splice.funding_negotiation = + Some(FundingNegotiation::AwaitingSignatures(funding)); + return Err(ChannelError::Warn(format!( + "Got unexpected splice_ack; splice negotiation already in progress" + ))); + }, + None => { + return Err(ChannelError::Warn(format!( + "Got unexpected splice_ack; no splice negotiation in progress" + ))); + }, + }; + + let our_funding_contribution_satoshis = + funding_negotiation_context.our_funding_contribution_satoshis; + let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; + + let splice_funding = FundingScope::for_splice( + &self.funding, + &self.context, + our_funding_contribution_satoshis, + their_funding_contribution_satoshis, + msg.funding_pubkey, + )?; + // TODO(splicing): Pre-check for reserve requirement // (Note: It should also be checked later at tx_complete) - Ok(()) + + log_info!( + logger, + "Starting splice funding negotiation for channel {} after receiving splice_ack; new channel value: {} sats (old: {} sats)", + self.context.channel_id, + splice_funding.get_value_satoshis(), + self.funding.get_value_satoshis(), + ); + + let mut interactive_tx_constructor = funding_negotiation_context + .into_interactive_tx_constructor( + &self.context, + &splice_funding, + signer_provider, + entropy_source, + holder_node_id.clone(), + None, + ) + .map_err(|err| { + ChannelError::Warn(format!("V2 channel rejected due to sender error, {:?}", err)) + })?; + let tx_msg_opt = interactive_tx_constructor.take_initiator_first_message(); + + debug_assert!(self.interactive_tx_signing_session.is_none()); + pending_splice.funding_negotiation = + Some(FundingNegotiation::Pending(splice_funding, interactive_tx_constructor)); + + Ok(tx_msg_opt) } #[cfg(splicing)] @@ -12021,7 +12371,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 @@ -12084,19 +12434,21 @@ where unfunded_channel_age_ticks: 0, holder_commitment_point: HolderCommitmentPoint::new(&context.holder_signer, &context.secp_ctx), }; - let dual_funding_context = DualFundingChannelContext { - our_funding_satoshis: funding_satoshis, + let funding_negotiation_context = FundingNegotiationContext { + is_initiator: true, + our_funding_contribution_satoshis: funding_satoshis as i64, // TODO(dual_funding) TODO(splicing) Include counterparty contribution, once that's enabled - their_funding_satoshis: None, + their_funding_contribution_satoshis: None, funding_tx_locktime, funding_feerate_sat_per_1000_weight, + shared_funding_input: None, our_funding_inputs: funding_inputs, }; let chan = Self { funding, context, unfunded_context, - dual_funding_context, + funding_negotiation_context, interactive_tx_constructor: None, interactive_tx_signing_session: None, }; @@ -12172,7 +12524,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, } } @@ -12238,11 +12590,13 @@ where &funding.get_counterparty_pubkeys().revocation_basepoint); context.channel_id = channel_id; - let dual_funding_context = DualFundingChannelContext { - our_funding_satoshis: our_funding_satoshis, - their_funding_satoshis: Some(msg.common_fields.funding_satoshis), + let funding_negotiation_context = FundingNegotiationContext { + is_initiator: false, + our_funding_contribution_satoshis: our_funding_satoshis as i64, + their_funding_contribution_satoshis: Some(msg.common_fields.funding_satoshis as i64), funding_tx_locktime: LockTime::from_consensus(msg.locktime), funding_feerate_sat_per_1000_weight: msg.funding_feerate_sat_per_1000_weight, + shared_funding_input: None, our_funding_inputs: our_funding_inputs.clone(), }; let shared_funding_output = TxOut { @@ -12256,8 +12610,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, shared_funding_input: None, @@ -12276,7 +12630,7 @@ where Ok(Self { funding, context, - dual_funding_context, + funding_negotiation_context, interactive_tx_constructor, interactive_tx_signing_session: None, unfunded_context, @@ -12342,7 +12696,8 @@ 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_contribution_satoshis + as u64, second_per_commitment_point, require_confirmed_inputs: None, } @@ -13670,6 +14025,8 @@ mod tests { use crate::chain::chaininterface::LowerBoundedFeeEstimator; use crate::chain::transaction::OutPoint; use crate::chain::BestBlock; + #[cfg(splicing)] + use crate::ln::chan_utils::ChannelTransactionParameters; use crate::ln::chan_utils::{self, commit_tx_fee_sat}; use crate::ln::channel::{ AwaitingChannelReadyFlags, ChannelState, FundedChannel, HTLCCandidate, HTLCInitiator, @@ -13689,6 +14046,8 @@ mod tests { use crate::prelude::*; use crate::routing::router::{Path, RouteHop}; use crate::sign::{ChannelSigner, EntropySource, InMemorySigner, SignerProvider}; + #[cfg(splicing)] + use crate::sync::Mutex; #[cfg(ldk_test_vectors)] use crate::types::features::ChannelTypeFeatures; use crate::types::features::{ChannelFeatures, NodeFeatures}; @@ -15559,4 +15918,97 @@ 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::FundingScope; + + let funding = FundingScope { + value_to_self_msat: 0, + counterparty_selected_channel_reserve_satoshis: None, + holder_selected_channel_reserve_satoshis: 0, + + #[cfg(debug_assertions)] + holder_max_commitment_tx_output: Mutex::new((0, 0)), + #[cfg(debug_assertions)] + counterparty_max_commitment_tx_output: Mutex::new((0, 0)), + + #[cfg(any(test, fuzzing))] + next_local_commitment_tx_fee_info_cached: Mutex::new(None), + #[cfg(any(test, fuzzing))] + next_remote_commitment_tx_fee_info_cached: Mutex::new(None), + + channel_transaction_parameters: ChannelTransactionParameters::test_dummy( + pre_channel_value, + ), + funding_transaction: None, + funding_tx_confirmed_in: None, + funding_tx_confirmation_height: 0, + short_channel_id: None, + minimum_depth_override: None, + }; + let post_channel_value = + funding.compute_post_splice_value(our_funding_contribution, their_funding_contribution); + (pre_channel_value, post_channel_value) + } + + #[cfg(splicing)] + #[test] + fn test_compute_post_splice_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 2214676ceb7..8f3c49d57e9 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -66,6 +66,7 @@ use crate::ln::channel::{ }; use crate::ln::channel_state::ChannelDetails; use crate::ln::inbound_payment; +use crate::ln::interactivetxs::{HandleTxCompleteResult, InteractiveTxMessageSendResult}; use crate::ln::msgs; use crate::ln::msgs::{ BaseMessageHandler, ChannelMessageHandler, CommitmentUpdate, DecodeError, LightningError, @@ -4442,7 +4443,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, funding_feerate_per_kw, locktime ); res = result; match res { @@ -4455,16 +4456,19 @@ 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), }; @@ -4477,7 +4481,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, @@ -4488,18 +4497,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, + ), + }), } } @@ -9190,7 +9197,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_contribution_satoshis > 0 { continue; } } @@ -9590,28 +9597,29 @@ 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) -> Option>( + &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) { hash_map::Entry::Occupied(mut chan_entry) => { 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)), + Some(msg_send_event) => msg_send_event, + None => { + let err = ChannelError::Warn("Received unexpected interactive transaction negotiation message".to_owned()); + return Err(MsgHandleErrInternal::from_chan_no_close(err, channel_id)) + }, }; peer_state.pending_msg_events.push(msg_send_event); Ok(()) @@ -9629,12 +9637,15 @@ 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"), - } + Some( + InteractiveTxMessageSendResult( + channel + .interactive_tx_constructor_mut()? + .handle_tx_add_input(msg) + .map_err(|reason| reason.into_tx_abort_msg(msg.channel_id)), + ) + .into_msg_send_event(counterparty_node_id), + ) }) } @@ -9642,15 +9653,15 @@ 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"), - } + Some( + InteractiveTxMessageSendResult( + channel + .interactive_tx_constructor_mut()? + .handle_tx_add_output(msg) + .map_err(|reason| reason.into_tx_abort_msg(msg.channel_id)), + ) + .into_msg_send_event(counterparty_node_id), + ) }) } @@ -9658,15 +9669,15 @@ 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"), - } + Some( + InteractiveTxMessageSendResult( + channel + .interactive_tx_constructor_mut()? + .handle_tx_remove_input(msg) + .map_err(|reason| reason.into_tx_abort_msg(msg.channel_id)), + ) + .into_msg_send_event(counterparty_node_id), + ) }) } @@ -9674,15 +9685,15 @@ 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"), - } + Some( + InteractiveTxMessageSendResult( + channel + .interactive_tx_constructor_mut()? + .handle_tx_remove_output(msg) + .map_err(|reason| reason.into_tx_abort_msg(msg.channel_id)), + ) + .into_msg_send_event(counterparty_node_id), + ) }) } @@ -9700,23 +9711,27 @@ 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) - .into_msg_send_event_or_signing_session(counterparty_node_id), + let (msg_send_event_opt, negotiation_complete) = match chan_entry.get_mut().interactive_tx_constructor_mut() { + Some(interactive_tx_constructor) => { + HandleTxCompleteResult( + interactive_tx_constructor + .handle_tx_complete(msg) + .map_err(|reason| reason.into_tx_abort_msg(msg.channel_id)), + ) + .into_msg_send_event(counterparty_node_id) + }, None => { - let msg = "Got a tx_complete message with no interactive transaction construction expected or in-progress"; - let reason = ClosureReason::ProcessingError { err: msg.to_owned() }; - let err = ChannelError::Close((msg.to_owned(), reason)); - try_channel_entry!(self, peer_state, Err(err), chan_entry) + let err = ChannelError::Warn("Received unexpected tx_complete message".to_owned()); + return Err(MsgHandleErrInternal::from_chan_no_close(err, msg.channel_id)) }, }; if let Some(msg_send_event) = msg_send_event_opt { peer_state.pending_msg_events.push(msg_send_event); }; - if let Some(signing_session) = signing_session_opt { + if negotiation_complete { let (commitment_signed, funding_ready_for_sig_event_opt) = chan_entry .get_mut() - .funding_tx_constructed(signing_session, &self.logger) + .funding_tx_constructed(&self.logger) .map_err(|err| MsgHandleErrInternal::send_err_msg_no_close(format!("{}", err), msg.channel_id))?; if let Some(funding_ready_for_sig_event) = funding_ready_for_sig_event_opt { let mut pending_events = self.pending_events.lock().unwrap(); @@ -10848,6 +10863,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!( @@ -10855,24 +10873,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 init_res = funded_channel.splice_init( + msg, our_funding_contribution, &self.signer_provider, &self.entropy_source, + &self.get_our_node_id(), &self.logger + ); + let splice_ack_msg = try_channel_entry!(self, peer_state, init_res, 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). @@ -10890,26 +10906,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 splice_ack_res = funded_channel.splice_ack( + msg, &self.signer_provider, &self.entropy_source, + &self.get_our_node_id(), &self.logger + ); + let tx_msg_opt = try_channel_entry!(self, peer_state, splice_ack_res, 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 acf28cf8fd3..cb62a955c7b 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -22,7 +22,7 @@ use bitcoin::{OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Wei use crate::chain::chaininterface::fee_for_weight; use crate::events::bump_transaction::{BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT}; use crate::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT; -use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; +use crate::ln::channel::{FundingNegotiationContext, TOTAL_BITCOIN_SUPPLY_SATOSHIS}; use crate::ln::msgs; use crate::ln::msgs::{MessageSendEvent, SerialId, TxSignatures}; use crate::ln::types::ChannelId; @@ -1270,6 +1270,10 @@ impl SharedOwnedInput { Self { input, prev_output, local_owned } } + pub fn local_owned(&self) -> u64 { + self.local_owned + } + fn remote_owned(&self) -> u64 { self.prev_output.value.to_sat().saturating_sub(self.local_owned) } @@ -1600,24 +1604,22 @@ where pub(super) enum HandleTxCompleteValue { SendTxMessage(InteractiveTxMessageSend), - SendTxComplete(InteractiveTxMessageSend, InteractiveTxSigningSession), - NegotiationComplete(InteractiveTxSigningSession), + SendTxComplete(InteractiveTxMessageSend, bool), + NegotiationComplete, } impl HandleTxCompleteValue { - pub fn into_msg_send_event_or_signing_session( + pub fn into_msg_send_event( self, counterparty_node_id: PublicKey, - ) -> (Option, Option) { + ) -> (Option, bool) { match self { HandleTxCompleteValue::SendTxMessage(msg) => { - (Some(msg.into_msg_send_event(counterparty_node_id)), None) + (Some(msg.into_msg_send_event(counterparty_node_id)), false) }, - HandleTxCompleteValue::SendTxComplete(msg, signing_session) => { - (Some(msg.into_msg_send_event(counterparty_node_id)), Some(signing_session)) - }, - HandleTxCompleteValue::NegotiationComplete(signing_session) => { - (None, Some(signing_session)) + HandleTxCompleteValue::SendTxComplete(msg, negotiation_complete) => { + (Some(msg.into_msg_send_event(counterparty_node_id)), negotiation_complete) }, + HandleTxCompleteValue::NegotiationComplete => (None, true), } } } @@ -1625,19 +1627,19 @@ impl HandleTxCompleteValue { pub(super) struct HandleTxCompleteResult(pub Result); impl HandleTxCompleteResult { - pub fn into_msg_send_event_or_signing_session( + pub fn into_msg_send_event( self, counterparty_node_id: PublicKey, - ) -> (Option, Option) { + ) -> (Option, bool) { match self.0 { Ok(interactive_tx_msg_send) => { - interactive_tx_msg_send.into_msg_send_event_or_signing_session(counterparty_node_id) + interactive_tx_msg_send.into_msg_send_event(counterparty_node_id) }, Err(tx_abort_msg) => ( Some(MessageSendEvent::SendTxAbort { node_id: counterparty_node_id, msg: tx_abort_msg, }), - None, + false, ), } } @@ -1835,8 +1837,8 @@ impl InteractiveTxConstructor { StateMachine::ReceivedTxComplete(_) => { let msg_send = self.maybe_send_message()?; match &self.state_machine { - StateMachine::NegotiationComplete(s) => { - Ok(HandleTxCompleteValue::SendTxComplete(msg_send, s.0.clone())) + StateMachine::NegotiationComplete(_) => { + Ok(HandleTxCompleteValue::SendTxComplete(msg_send, true)) }, StateMachine::SentChangeMsg(_) => { Ok(HandleTxCompleteValue::SendTxMessage(msg_send)) @@ -1847,9 +1849,7 @@ impl InteractiveTxConstructor { }, } }, - StateMachine::NegotiationComplete(s) => { - Ok(HandleTxCompleteValue::NegotiationComplete(s.0.clone())) - }, + StateMachine::NegotiationComplete(_) => Ok(HandleTxCompleteValue::NegotiationComplete), _ => { debug_assert!( false, @@ -1859,6 +1859,13 @@ impl InteractiveTxConstructor { }, } } + + pub fn into_signing_session(self) -> InteractiveTxSigningSession { + match self.state_machine { + StateMachine::NegotiationComplete(s) => s.0, + _ => panic!("Signing session is not ready yet"), + } + } } /// Determine whether a change output should be added, and if yes, of what size, considering our @@ -1874,26 +1881,21 @@ impl InteractiveTxConstructor { /// `Err(AbortReason::InsufficientFees)` /// /// Parameters: -/// - `is_initiator` - Whether we are the negotiation initiator or not (acceptor). -/// - `our_contribution` - The sats amount we intend to contribute to the funding -/// transaction being negotiated. -/// - `funding_inputs` - List of our inputs. It does not include the shared input, if there is one. -/// - `shared_input` - The locally owned amount of the shared input (in sats), if there is one. +/// - `context` - Context of the funding negotiation, including non-shared inputs and feerate. +/// - `is_splice` - Whether we splicing an existing channel or dual-funding a new one. /// - `shared_output_funding_script` - The script of the shared output. /// - `funding_outputs` - Our funding outputs. -/// - `funding_feerate_sat_per_1000_weight` - Fee rate to be used. /// - `change_output_dust_limit` - The dust limit (in sats) to consider. pub(super) fn calculate_change_output_value( - is_initiator: bool, our_contribution: u64, - funding_inputs: &Vec<(TxIn, TransactionU16LenLimited)>, shared_input: Option, - shared_output_funding_script: &ScriptBuf, funding_outputs: &Vec, - funding_feerate_sat_per_1000_weight: u32, change_output_dust_limit: u64, + context: &FundingNegotiationContext, is_splice: bool, shared_output_funding_script: &ScriptBuf, + funding_outputs: &Vec, change_output_dust_limit: u64, ) -> Result, AbortReason> { - // Process inputs and their prev txs: - // calculate value sum and weight sum of inputs, also perform checks + assert!(context.our_funding_contribution_satoshis > 0); + let our_funding_contribution_satoshis = context.our_funding_contribution_satoshis as u64; + let mut total_input_satoshis = 0u64; let mut our_funding_inputs_weight = 0u64; - for (txin, tx) in funding_inputs.iter() { + for (txin, tx) in context.our_funding_inputs.iter() { let txid = tx.as_transaction().compute_txid(); if txin.previous_output.txid != txid { return Err(AbortReason::PrevTxOutInvalid); @@ -1908,13 +1910,8 @@ pub(super) fn calculate_change_output_value( our_funding_inputs_weight = our_funding_inputs_weight.saturating_add(weight); } - if let Some(shared_input) = shared_input { - total_input_satoshis = total_input_satoshis.saturating_add(shared_input); - } - let total_output_satoshis = funding_outputs.iter().fold(0u64, |total, out| total.saturating_add(out.value.to_sat())); - let our_funding_outputs_weight = funding_outputs.iter().fold(0u64, |weight, out| { weight.saturating_add(get_output_weight(&out.script_pubkey).to_wu()) }); @@ -1922,24 +1919,23 @@ pub(super) fn calculate_change_output_value( // If we are the initiator, we must pay for the weight of the funding output and // all common fields in the funding transaction. - if is_initiator { + if context.is_initiator { weight = weight.saturating_add(get_output_weight(shared_output_funding_script).to_wu()); weight = weight.saturating_add(TX_COMMON_FIELDS_WEIGHT); - - if shared_input.is_some() { + if is_splice { + // TODO(splicing): Needs to consider different weights based on channel type weight = weight.saturating_add(FUNDING_TRANSACTION_WITNESS_WEIGHT); } } - let fees_sats = fee_for_weight(funding_feerate_sat_per_1000_weight, weight); - + let fees_sats = fee_for_weight(context.funding_feerate_sat_per_1000_weight, weight); let net_total_less_fees = total_input_satoshis.saturating_sub(total_output_satoshis).saturating_sub(fees_sats); - if net_total_less_fees < our_contribution { + if net_total_less_fees < our_funding_contribution_satoshis { // Not enough to cover contribution plus fees return Err(AbortReason::InsufficientFees); } - let remaining_value = net_total_less_fees.saturating_sub(our_contribution); + let remaining_value = net_total_less_fees.saturating_sub(our_funding_contribution_satoshis); if remaining_value < change_output_dust_limit { // Enough to cover contribution plus fees, but leftover is below dust limit; no change Ok(None) @@ -1952,7 +1948,7 @@ pub(super) fn calculate_change_output_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::channel::{FundingNegotiationContext, TOTAL_BITCOIN_SUPPLY_SATOSHIS}; use crate::ln::interactivetxs::{ calculate_change_output_value, generate_holder_serial_id, AbortReason, HandleTxCompleteValue, InteractiveTxConstructor, InteractiveTxConstructorArgs, @@ -2093,7 +2089,7 @@ mod tests { ), outputs_to_contribute: session.outputs_a, }) { - Ok(r) => r, + Ok(r) => Some(r), Err(abort_reason) => { assert_eq!( Some((abort_reason, ErrorCulprit::NodeA)), @@ -2130,7 +2126,7 @@ mod tests { ), outputs_to_contribute: session.outputs_b, }) { - Ok(r) => r, + Ok(r) => Some(r), Err(abort_reason) => { assert_eq!( Some((abort_reason, ErrorCulprit::NodeB)), @@ -2147,35 +2143,44 @@ mod tests { match msg { InteractiveTxMessageSend::TxAddInput(msg) => for_constructor .handle_tx_add_input(&msg) - .map(|msg_send| (Some(msg_send), None)), + .map(|msg_send| (Some(msg_send), false)), InteractiveTxMessageSend::TxAddOutput(msg) => for_constructor .handle_tx_add_output(&msg) - .map(|msg_send| (Some(msg_send), None)), + .map(|msg_send| (Some(msg_send), false)), InteractiveTxMessageSend::TxComplete(msg) => { for_constructor.handle_tx_complete(&msg).map(|value| match value { HandleTxCompleteValue::SendTxMessage(msg_send) => { - (Some(msg_send), None) + (Some(msg_send), false) }, - HandleTxCompleteValue::SendTxComplete(msg_send, tx) => { - (Some(msg_send), Some(tx)) - }, - HandleTxCompleteValue::NegotiationComplete(tx) => (None, Some(tx)), + HandleTxCompleteValue::SendTxComplete( + msg_send, + negotiation_complete, + ) => (Some(msg_send), negotiation_complete), + HandleTxCompleteValue::NegotiationComplete => (None, true), }) }, } }; - let mut message_send_a = constructor_a.take_initiator_first_message(); + let mut message_send_a = constructor_a.as_mut().unwrap().take_initiator_first_message(); let mut message_send_b = None; let mut final_tx_a = None; let mut final_tx_b = None; - while final_tx_a.is_none() || final_tx_b.is_none() { + while constructor_a.is_some() || constructor_b.is_some() { if let Some(message_send_a) = message_send_a.take() { - match handle_message_send(message_send_a, &mut constructor_b) { - Ok((msg_send, interactive_signing_session)) => { + match handle_message_send(message_send_a, constructor_b.as_mut().unwrap()) { + Ok((msg_send, negotiation_complete)) => { message_send_b = msg_send; - final_tx_b = interactive_signing_session - .map(|session| session.unsigned_tx.compute_txid()); + if negotiation_complete { + final_tx_b = Some( + constructor_b + .take() + .unwrap() + .into_signing_session() + .unsigned_tx + .compute_txid(), + ); + } }, Err(abort_reason) => { let error_culprit = match abort_reason { @@ -2196,11 +2201,19 @@ mod tests { } } if let Some(message_send_b) = message_send_b.take() { - match handle_message_send(message_send_b, &mut constructor_a) { - Ok((msg_send, interactive_signing_session)) => { + match handle_message_send(message_send_b, constructor_a.as_mut().unwrap()) { + Ok((msg_send, negotiation_complete)) => { message_send_a = msg_send; - final_tx_a = interactive_signing_session - .map(|session| session.unsigned_tx.compute_txid()); + if negotiation_complete { + final_tx_a = Some( + constructor_a + .take() + .unwrap() + .into_signing_session() + .unsigned_tx + .compute_txid(), + ); + } }, Err(abort_reason) => { let error_culprit = match abort_reason { @@ -2980,89 +2993,72 @@ mod tests { let gross_change = total_inputs - total_outputs - our_contributed; let fees = 1746; let common_fees = 234; - { - // There is leftover for change - let res = calculate_change_output_value( - true, - our_contributed, - &inputs, - None, - &ScriptBuf::new(), - &outputs, - funding_feerate_sat_per_1000_weight, - 300, - ); - assert_eq!(res, Ok(Some(gross_change - fees - common_fees))); - } - { - // There is leftover for change, without common fees - let res = calculate_change_output_value( - false, - our_contributed, - &inputs, - None, - &ScriptBuf::new(), - &outputs, - funding_feerate_sat_per_1000_weight, - 300, - ); - assert_eq!(res, Ok(Some(gross_change - fees))); - } - { - // Larger fee, smaller change - let res = calculate_change_output_value( - true, - our_contributed, - &inputs, - None, - &ScriptBuf::new(), - &outputs, - funding_feerate_sat_per_1000_weight * 3, - 300, - ); - assert_eq!(res, Ok(Some(4060))); - } - { - // Insufficient inputs, no leftover - let res = calculate_change_output_value( - false, - 130_000, - &inputs, - None, - &ScriptBuf::new(), - &outputs, - funding_feerate_sat_per_1000_weight, - 300, - ); - assert_eq!(res, Err(AbortReason::InsufficientFees)); - } - { - // Very small leftover - let res = calculate_change_output_value( - false, - 118_000, - &inputs, - None, - &ScriptBuf::new(), - &outputs, - funding_feerate_sat_per_1000_weight, - 300, - ); - assert_eq!(res, Ok(None)); - } - { - // Small leftover, but not dust - let res = calculate_change_output_value( - false, - 117_992, - &inputs, - None, - &ScriptBuf::new(), - &outputs, - funding_feerate_sat_per_1000_weight, - 100, - ); - assert_eq!(res, Ok(Some(262))); - } + + // There is leftover for change + let context = FundingNegotiationContext { + is_initiator: true, + our_funding_contribution_satoshis: our_contributed as i64, + their_funding_contribution_satoshis: None, + funding_tx_locktime: AbsoluteLockTime::ZERO, + funding_feerate_sat_per_1000_weight, + shared_funding_input: None, + our_funding_inputs: inputs, + }; + assert_eq!( + calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300), + Ok(Some(gross_change - fees - common_fees)), + ); + + // There is leftover for change, without common fees + let context = FundingNegotiationContext { is_initiator: false, ..context }; + assert_eq!( + calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300), + Ok(Some(gross_change - fees)), + ); + + // Insufficient inputs, no leftover + let context = FundingNegotiationContext { + is_initiator: false, + our_funding_contribution_satoshis: 130_000, + ..context + }; + assert_eq!( + calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300), + Err(AbortReason::InsufficientFees), + ); + + // Very small leftover + let context = FundingNegotiationContext { + is_initiator: false, + our_funding_contribution_satoshis: 118_000, + ..context + }; + assert_eq!( + calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300), + Ok(None), + ); + + // Small leftover, but not dust + let context = FundingNegotiationContext { + is_initiator: false, + our_funding_contribution_satoshis: 117_992, + ..context + }; + assert_eq!( + calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 100), + Ok(Some(262)), + ); + + // Larger fee, smaller change + let context = FundingNegotiationContext { + is_initiator: true, + our_funding_contribution_satoshis: our_contributed as i64, + funding_feerate_sat_per_1000_weight: funding_feerate_sat_per_1000_weight * 3, + ..context + }; + assert_eq!( + calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300), + Ok(Some(4060)), + ); } } diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index 33f5a500789..af1fa1f3ab3 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -28,6 +28,7 @@ fn test_v1_splice_in() { let channel_value_sat = 100_000; let channel_reserve_amnt_sat = 1_000; + let expect_outputs_in_reverse = true; let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value( &nodes, @@ -42,16 +43,21 @@ fn test_v1_splice_in() { assert_eq!(channel_id.to_string(), expected_funded_channel_id); let expected_initiator_funding_key = - "03c21e841cbc0b48197d060c71e116c185fa0ac281b7d0aa5924f535154437ca3b"; + "020abf01c18d5a2543124a12150d698ebf3a8e17df9993521151a49e115678ceea"; let expected_acceptor_funding_key = - "039481c28b904cbe12681e79937373fc76245c1b29871028ae60ba3152162c319b"; + "036b47248c628fca98159f30f6b03a6cf0be0c4808cff17c75dc855fe94a244766"; // ==== 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 +127,153 @@ 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() + ); + // check which input is this (order is non-deterministic), based on the presense of prevtx + let inputs_seen_in_reverse = tx_add_input_msg.prevtx.is_some(); + if !inputs_seen_in_reverse { + // Input is the revious funding input + assert_eq!(tx_add_input_msg.prevtx, None); + assert_eq!( + tx_add_input_msg.shared_input_txid.unwrap().to_string(), + "4f128bedf1a15baf465ab1bfd6e97c8f82628f4156bf86eb1cbc132cda6733ae" + ); + } else { + // Input is the extra input + let prevtx_value = tx_add_input_msg.prevtx.as_ref().unwrap().as_transaction().output + [tx_add_input_msg.prevtx_out as usize] + .value + .to_sat(); + assert_eq!(prevtx_value, extra_splice_funding_input_sats); + assert_eq!(tx_add_input_msg.shared_input_txid, None); + } + + 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 tx_add_input2_msg = get_event_msg!( + &initiator_node, + MessageSendEvent::SendTxAddInput, + acceptor_node.node.get_our_node_id() + ); + if !inputs_seen_in_reverse { + // Input is the extra input + let prevtx_value = tx_add_input2_msg.prevtx.as_ref().unwrap().as_transaction().output + [tx_add_input2_msg.prevtx_out as usize] + .value + .to_sat(); + assert_eq!(prevtx_value, extra_splice_funding_input_sats); + assert_eq!(tx_add_input2_msg.shared_input_txid, None); + } else { + // Input is the revious funding input + assert_eq!(tx_add_input2_msg.prevtx, None); + assert_eq!( + tx_add_input2_msg.shared_input_txid.unwrap().to_string(), + "4f128bedf1a15baf465ab1bfd6e97c8f82628f4156bf86eb1cbc132cda6733ae" + ); + } + + 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() + ); + if !expect_outputs_in_reverse { + assert!(tx_add_output_msg.script.is_p2wsh()); + assert_eq!(tx_add_output_msg.sats, post_splice_channel_value); + } else { + assert!(tx_add_output_msg.script.is_p2wpkh()); + assert_eq!(tx_add_output_msg.sats, 14146); // extra_splice_funding_input_sats - splice_in_sats + } + + let _res = acceptor_node + .node + .handle_tx_add_output(initiator_node.node.get_our_node_id(), &tx_add_output_msg); + let tx_complete_msg = get_event_msg!( + &acceptor_node, + MessageSendEvent::SendTxComplete, + initiator_node.node.get_our_node_id() + ); + + let _res = initiator_node + .node + .handle_tx_complete(acceptor_node.node.get_our_node_id(), &tx_complete_msg); + // TxAddOutput for the splice funding + let tx_add_output2_msg = get_event_msg!( + &initiator_node, + MessageSendEvent::SendTxAddOutput, + acceptor_node.node.get_our_node_id() + ); + if !expect_outputs_in_reverse { + assert!(tx_add_output2_msg.script.is_p2wpkh()); + assert_eq!(tx_add_output2_msg.sats, 14146); // extra_splice_funding_input_sats - splice_in_sats + } else { + 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 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();