diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 0796369d4c6..eb878eff4a9 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -8,12 +8,12 @@ // licenses. use bitcoin::absolute::LockTime; -use bitcoin::amount::Amount; +use bitcoin::amount::{Amount, SignedAmount}; use bitcoin::consensus::encode; use bitcoin::constants::ChainHash; use bitcoin::script::{Builder, Script, ScriptBuf, WScriptHash}; use bitcoin::sighash::EcdsaSighashType; -use bitcoin::transaction::{Transaction, TxIn, TxOut}; +use bitcoin::transaction::{Transaction, TxOut}; use bitcoin::Weight; use bitcoin::hash_types::{BlockHash, Txid}; @@ -26,7 +26,7 @@ use bitcoin::secp256k1::{ecdsa::Signature, Secp256k1}; use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::{secp256k1, sighash}; #[cfg(splicing)] -use bitcoin::{Sequence, Witness}; +use bitcoin::{FeeRate, Sequence, TxIn, Witness}; use crate::chain::chaininterface::{ fee_for_weight, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator, @@ -52,8 +52,10 @@ use crate::ln::channel_state::{ ChannelShutdownState, CounterpartyForwardingInfo, InboundHTLCDetails, InboundHTLCStateDetails, OutboundHTLCDetails, OutboundHTLCStateDetails, }; +#[cfg(splicing)] +use crate::ln::channelmanager::SpliceContribution; use crate::ln::channelmanager::{ - self, FundingConfirmedMessage, HTLCFailureMsg, HTLCSource, OpenChannelMessage, + self, FundingConfirmedMessage, FundingTxInput, HTLCFailureMsg, HTLCSource, OpenChannelMessage, PaymentClaimDetails, PendingHTLCInfo, PendingHTLCStatus, RAACommitmentOrder, SentHTLCId, BREAKDOWN_TIMEOUT, MAX_LOCAL_BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA, }; @@ -72,6 +74,8 @@ use crate::ln::onion_utils::{ }; use crate::ln::script::{self, ShutdownScript}; use crate::ln::types::ChannelId; +#[cfg(splicing)] +use crate::ln::LN_MAX_MSG_LEN; use crate::routing::gossip::NodeId; use crate::sign::ecdsa::EcdsaChannelSigner; use crate::sign::tx_builder::{SpecTxBuilder, TxBuilder}; @@ -85,9 +89,7 @@ use crate::util::config::{ use crate::util::errors::APIError; use crate::util::logger::{Logger, Record, WithContext}; use crate::util::scid_utils::{block_from_scid, scid_from_parts}; -use crate::util::ser::{ - Readable, ReadableArgs, RequiredWrapper, TransactionU16LenLimited, Writeable, Writer, -}; +use crate::util::ser::{Readable, ReadableArgs, RequiredWrapper, Writeable, Writer}; use alloc::collections::{btree_map, BTreeMap}; @@ -2254,20 +2256,22 @@ impl FundingScope { /// 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, + prev_funding: &Self, context: &ChannelContext, our_funding_contribution: SignedAmount, + their_funding_contribution: SignedAmount, counterparty_funding_pubkey: PublicKey, ) -> Result where SP::Target: SignerProvider, { + debug_assert!(our_funding_contribution <= SignedAmount::MAX_MONEY); + let post_channel_value = prev_funding.compute_post_splice_value( - our_funding_contribution_sats, - their_funding_contribution_sats, + our_funding_contribution.to_sat(), + their_funding_contribution.to_sat(), ); let post_value_to_self_msat = AddSigned::checked_add_signed( prev_funding.value_to_self_msat, - our_funding_contribution_sats * 1000, + our_funding_contribution.to_sat() * 1000, ); debug_assert!(post_value_to_self_msat.is_some()); let post_value_to_self_msat = post_value_to_self_msat.unwrap(); @@ -5875,20 +5879,53 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis)) } +#[cfg(splicing)] +fn check_splice_contribution_sufficient( + channel_balance: Amount, contribution: &SpliceContribution, is_initiator: bool, + funding_feerate: FeeRate, +) -> Result { + let contribution_amount = contribution.value(); + if contribution_amount < SignedAmount::ZERO { + let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee( + is_initiator, + 1, // spends the previous funding output + Weight::from_wu(FUNDING_TRANSACTION_WITNESS_WEIGHT), + contribution.outputs(), + funding_feerate.to_sat_per_kwu() as u32, + )); + + if channel_balance > contribution_amount.unsigned_abs() + estimated_fee { + Ok(estimated_fee) + } else { + Err(ChannelError::Warn(format!( + "Available channel balance {} is lower than needed for splicing out {}, considering fees of {}", + channel_balance, contribution_amount.unsigned_abs(), estimated_fee, + ))) + } + } else { + check_v2_funding_inputs_sufficient( + contribution_amount.to_sat(), + contribution.inputs(), + is_initiator, + true, + funding_feerate.to_sat_per_kwu() as u32, + ) + .map(Amount::from_sat) + } +} + /// Estimate our part of the fee of the new funding transaction. /// input_count: Number of contributed inputs. /// witness_weight: The witness weight for contributed inputs. #[allow(dead_code)] // TODO(dual_funding): TODO(splicing): Remove allow once used. #[rustfmt::skip] fn estimate_v2_funding_transaction_fee( - is_initiator: bool, input_count: usize, witness_weight: Weight, + is_initiator: bool, input_count: usize, witness_weight: Weight, outputs: &[TxOut], funding_feerate_sat_per_1000_weight: u32, ) -> u64 { - // Inputs let mut weight = (input_count as u64) * BASE_INPUT_WEIGHT; - - // Witnesses weight = weight.saturating_add(witness_weight.to_wu()); + weight = weight.saturating_add(outputs.iter().map(|txout| txout.weight().to_wu()).sum()); // If we are the initiator, we must pay for weight of all common fields in the funding transaction. if is_initiator { @@ -5914,26 +5951,27 @@ fn estimate_v2_funding_transaction_fee( #[cfg(splicing)] #[rustfmt::skip] fn check_v2_funding_inputs_sufficient( - contribution_amount: i64, funding_inputs: &[(TxIn, Transaction, Weight)], is_initiator: bool, + contribution_amount: i64, funding_inputs: &[FundingTxInput], is_initiator: bool, is_splice: bool, funding_feerate_sat_per_1000_weight: u32, ) -> Result { - let mut total_input_witness_weight = Weight::from_wu(funding_inputs.iter().map(|(_, _, w)| w.to_wu()).sum()); + let mut total_input_witness_weight = + funding_inputs.iter().map(|FundingTxInput { witness_weight, .. }| witness_weight).sum(); let mut funding_inputs_len = funding_inputs.len(); if is_initiator && is_splice { // consider the weight of the input and witness needed for spending the old funding transaction funding_inputs_len += 1; total_input_witness_weight += Weight::from_wu(FUNDING_TRANSACTION_WITNESS_WEIGHT); } - let estimated_fee = estimate_v2_funding_transaction_fee(is_initiator, funding_inputs_len, total_input_witness_weight, funding_feerate_sat_per_1000_weight); + let estimated_fee = estimate_v2_funding_transaction_fee(is_initiator, funding_inputs_len, total_input_witness_weight, &[], funding_feerate_sat_per_1000_weight); let mut total_input_sats = 0u64; - for (idx, input) in funding_inputs.iter().enumerate() { - if let Some(output) = input.1.output.get(input.0.previous_output.vout as usize) { + for (idx, FundingTxInput { txin, prevtx, .. }) in funding_inputs.iter().enumerate() { + if let Some(output) = prevtx.output.get(txin.previous_output.vout as usize) { total_input_sats = total_input_sats.saturating_add(output.value.to_sat()); } else { return Err(ChannelError::Warn(format!( "Transaction with txid {} does not have an output with vout of {} corresponding to TxIn at funding_inputs[{}]", - input.1.compute_txid(), input.0.previous_output.vout, idx + prevtx.compute_txid(), txin.previous_output.vout, idx ))); } } @@ -5965,10 +6003,7 @@ 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_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_contribution_satoshis: Option, + pub our_funding_contribution: SignedAmount, /// 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, @@ -5980,7 +6015,10 @@ pub(super) struct FundingNegotiationContext { pub shared_funding_input: Option, /// The funding inputs we will be contributing to the channel. #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled. - pub our_funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, + pub our_funding_inputs: Vec, + /// The funding outputs we will be contributing to the channel. + #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled. + pub our_funding_outputs: Vec, /// The change output script. This will be used if needed or -- if not set -- generated using /// `SignerProvider::get_destination_script`. #[allow(dead_code)] // TODO(splicing): Remove once splicing is enabled. @@ -6010,10 +6048,8 @@ impl FundingNegotiationContext { 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()), @@ -6021,36 +6057,46 @@ impl FundingNegotiationContext { }; // Optionally add change output - if self.our_funding_contribution_satoshis > 0 { - let change_value_opt = calculate_change_output_value( + let change_value_opt = if self.our_funding_contribution > SignedAmount::ZERO { + 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) = self.change_script { - script - } else { - signer_provider - .get_destination_script(context.channel_keys_id) - .map_err(|_err| AbortReason::InternalError("Error getting 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_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); - } - } - } + )? + } else { + None + }; + + let mut funding_outputs = self.our_funding_outputs; + + if let Some(change_value) = change_value_opt { + let change_script = if let Some(script) = self.change_script { + script + } else { + signer_provider + .get_destination_script(context.channel_keys_id) + .map_err(|_err| AbortReason::InternalError("Error getting 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_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 funding_inputs = self + .our_funding_inputs + .into_iter() + .map(|FundingTxInput { txin, prevtx, .. }| (txin, prevtx)) + .collect(); let constructor_args = InteractiveTxConstructorArgs { entropy_source, @@ -6060,7 +6106,7 @@ impl FundingNegotiationContext { 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, + inputs_to_contribute: funding_inputs, shared_funding_input: self.shared_funding_input, shared_funding_output: SharedOwnedOutput::new( shared_funding_output, @@ -10610,9 +10656,7 @@ where /// generated by `SignerProvider::get_destination_script`. #[cfg(splicing)] pub fn splice_channel( - &mut self, our_funding_contribution_satoshis: i64, - our_funding_inputs: Vec<(TxIn, Transaction, Weight)>, change_script: Option, - funding_feerate_per_kw: u32, locktime: u32, + &mut self, contribution: SpliceContribution, 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) @@ -10636,53 +10680,111 @@ where // TODO(splicing): check for quiescence - if our_funding_contribution_satoshis < 0 { + let our_funding_contribution = contribution.value(); + if our_funding_contribution > SignedAmount::MAX_MONEY { 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, - ), + "Channel {} cannot be spliced in; contribution exceeds total bitcoin supply: {}", + self.context.channel_id(), + our_funding_contribution, + ), }); } - // TODO(splicing): Once splice-out is supported, check that channel balance does not go below 0 - // (or below channel reserve) + if our_funding_contribution < -SignedAmount::MAX_MONEY { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} cannot be spliced out; contribution exceeds total bitcoin supply: {}", + self.context.channel_id(), + our_funding_contribution, + ), + }); + } + + let funding_inputs = contribution.inputs(); + let funding_outputs = contribution.outputs(); + if !funding_inputs.is_empty() && !funding_outputs.is_empty() { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} cannot be both spliced in and out; operation not supported", + self.context.channel_id(), + ), + }); + } // Note: post-splice channel value is not yet known at this point, counterparty contribution is not known // (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, + let channel_balance = Amount::from_sat(self.funding.get_value_to_self_msat() / 1000); + let fees = check_splice_contribution_sufficient( + channel_balance, + &contribution, + true, // is_initiator + FeeRate::from_sat_per_kwu(funding_feerate_per_kw as u64), ) - .map_err(|err| APIError::APIMisuseError { - err: format!( - "Insufficient inputs for splicing; channel ID {}, err {}", - self.context.channel_id(), - err, - ), + .map_err(|e| { + let splice_type = if our_funding_contribution < SignedAmount::ZERO { + "spliced out" + } else { + "spliced in" + }; + APIError::APIMisuseError { + err: format!( + "Channel {} cannot be {}; {}", + self.context.channel_id(), + splice_type, + e, + ), + } })?; - // 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)); + + // Fees for splice-out are paid from the channel balance whereas fees for splice-in are paid + // by the funding inputs. + let adjusted_funding_contribution = if our_funding_contribution < SignedAmount::ZERO { + let adjusted_funding_contribution = our_funding_contribution + - fees.to_signed().expect("fees should never exceed splice-out value"); + + // TODO(splicing): Check that channel balance does not go below the channel reserve + let _post_channel_balance = AddSigned::checked_add_signed( + channel_balance.to_sat(), + adjusted_funding_contribution.to_sat(), + ); + + adjusted_funding_contribution + } else { + our_funding_contribution + }; + + for FundingTxInput { txin, prevtx, .. } in contribution.inputs().iter() { + const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput { + channel_id: ChannelId([0; 32]), + serial_id: 0, + prevtx: None, + prevtx_out: 0, + sequence: 0, + shared_input_txid: None, + }; + let message_len = MESSAGE_TEMPLATE.serialized_length() + prevtx.serialized_length(); + if message_len > LN_MAX_MSG_LEN { + return Err(APIError::APIMisuseError { + err: format!( + "Funding input references a prevtx that is too large for tx_add_input: {}", + txin.previous_output, + ), + }); + } } let prev_funding_input = self.funding.to_splice_funding_input(); + let (our_funding_inputs, our_funding_outputs, change_script) = contribution.into_tx_parts(); let funding_negotiation_context = FundingNegotiationContext { is_initiator: true, - our_funding_contribution_satoshis, - their_funding_contribution_satoshis: None, + our_funding_contribution: adjusted_funding_contribution, 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, + our_funding_inputs, + our_funding_outputs, change_script, }; @@ -10698,7 +10800,7 @@ where Ok(msgs::SpliceInit { channel_id: self.context.channel_id, - funding_contribution_satoshis: our_funding_contribution_satoshis, + funding_contribution_satoshis: adjusted_funding_contribution.to_sat(), funding_feerate_per_kw, locktime, funding_pubkey, @@ -10709,10 +10811,8 @@ where /// Checks during handling splice_init #[cfg(splicing)] pub fn validate_splice_init( - &self, msg: &msgs::SpliceInit, our_funding_contribution_satoshis: i64, + &self, msg: &msgs::SpliceInit, our_funding_contribution: SignedAmount, ) -> Result { - let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; - // TODO(splicing): Add check that we are the quiescence acceptor // Check if a splice has been initiated already. @@ -10732,21 +10832,64 @@ where ))); } - if their_funding_contribution_satoshis.saturating_add(our_funding_contribution_satoshis) < 0 - { + debug_assert_eq!(our_funding_contribution, SignedAmount::ZERO); + + if our_funding_contribution > SignedAmount::MAX_MONEY { return Err(ChannelError::WarnAndDisconnect(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, + "Channel {} cannot be spliced in; our {} contribution exceeds the total bitcoin supply", + self.context.channel_id(), + our_funding_contribution, ))); } + if our_funding_contribution < -SignedAmount::MAX_MONEY { + return Err(ChannelError::WarnAndDisconnect(format!( + "Channel {} cannot be spliced out; our {} contribution exhausts the total bitcoin supply", + self.context.channel_id(), + our_funding_contribution, + ))); + } + + let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis); + if their_funding_contribution > SignedAmount::MAX_MONEY { + return Err(ChannelError::WarnAndDisconnect(format!( + "Channel {} cannot be spliced in; their {} contribution exceeds the total bitcoin supply", + self.context.channel_id(), + their_funding_contribution, + ))); + } + + if their_funding_contribution < -SignedAmount::MAX_MONEY { + return Err(ChannelError::WarnAndDisconnect(format!( + "Channel {} cannot be spliced out; their {} contribution exhausts the total bitcoin supply", + self.context.channel_id(), + their_funding_contribution, + ))); + } + + let their_channel_balance = Amount::from_sat(self.funding.get_value_satoshis()) + - Amount::from_sat(self.funding.get_value_to_self_msat() / 1000); + let post_channel_balance = AddSigned::checked_add_signed( + their_channel_balance.to_sat(), + their_funding_contribution.to_sat(), + ); + + if post_channel_balance.is_none() { + return Err(ChannelError::WarnAndDisconnect(format!( + "Channel {} cannot be spliced out; their {} contribution exhausts their channel balance: {}", + self.context.channel_id(), + their_funding_contribution, + their_channel_balance, + ))); + } + + // TODO(splicing): Check that channel balance does not go below the channel reserve + let splice_funding = FundingScope::for_splice( &self.funding, &self.context, - our_funding_contribution_satoshis, - their_funding_contribution_satoshis, + our_funding_contribution, + their_funding_contribution, msg.funding_pubkey, )?; @@ -10771,7 +10914,8 @@ where ES::Target: EntropySource, L::Target: Logger, { - let splice_funding = self.validate_splice_init(msg, our_funding_contribution_satoshis)?; + let our_funding_contribution = SignedAmount::from_sat(our_funding_contribution_satoshis); + let splice_funding = self.validate_splice_init(msg, our_funding_contribution)?; log_info!( logger, @@ -10781,16 +10925,15 @@ where 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), + our_funding_contribution, 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(), + our_funding_outputs: Vec::new(), change_script: None, }; @@ -10823,7 +10966,7 @@ where Ok(msgs::SpliceAck { channel_id: self.context.channel_id, - funding_contribution_satoshis: our_funding_contribution_satoshis, + funding_contribution_satoshis: our_funding_contribution.to_sat(), funding_pubkey, require_confirmed_inputs: None, }) @@ -10870,15 +11013,23 @@ where }, }; - let our_funding_contribution_satoshis = - funding_negotiation_context.our_funding_contribution_satoshis; - let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; + let our_funding_contribution = funding_negotiation_context.our_funding_contribution; + debug_assert!(our_funding_contribution <= SignedAmount::MAX_MONEY); + + let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis); + if their_funding_contribution > SignedAmount::MAX_MONEY { + return Err(ChannelError::Warn(format!( + "Channel {} cannot be spliced; their contribution exceeds total bitcoin supply: {}", + self.context.channel_id(), + their_funding_contribution, + ))); + } let splice_funding = FundingScope::for_splice( &self.funding, &self.context, - our_funding_contribution_satoshis, - their_funding_contribution_satoshis, + our_funding_contribution, + their_funding_contribution, msg.funding_pubkey, )?; @@ -12427,7 +12578,7 @@ where pub fn new_outbound( fee_estimator: &LowerBoundedFeeEstimator, entropy_source: &ES, signer_provider: &SP, counterparty_node_id: PublicKey, their_features: &InitFeatures, funding_satoshis: u64, - funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, user_id: u128, config: &UserConfig, + funding_inputs: Vec, user_id: u128, config: &UserConfig, current_chain_height: u32, outbound_scid_alias: u64, funding_confirmation_target: ConfirmationTarget, logger: L, ) -> Result @@ -12476,13 +12627,12 @@ where }; 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_contribution_satoshis: None, + our_funding_contribution: SignedAmount::from_sat(funding_satoshis as i64), funding_tx_locktime, funding_feerate_sat_per_1000_weight, shared_funding_input: None, our_funding_inputs: funding_inputs, + our_funding_outputs: Vec::new(), change_script: None, }; let chan = Self { @@ -12586,10 +12736,11 @@ where L::Target: Logger, { // TODO(dual_funding): Take these as input once supported - let our_funding_satoshis = 0u64; + let (our_funding_contribution, our_funding_contribution_sats) = (SignedAmount::ZERO, 0u64); let our_funding_inputs = Vec::new(); - let channel_value_satoshis = our_funding_satoshis.saturating_add(msg.common_fields.funding_satoshis); + let channel_value_satoshis = + our_funding_contribution_sats.saturating_add(msg.common_fields.funding_satoshis); let counterparty_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( channel_value_satoshis, msg.common_fields.dust_limit_satoshis); let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( @@ -12616,9 +12767,7 @@ where current_chain_height, logger, false, - - our_funding_satoshis, - + our_funding_contribution_sats, counterparty_pubkeys, channel_type, holder_selected_channel_reserve_satoshis, @@ -12633,18 +12782,22 @@ where 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), + our_funding_contribution, 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(), + our_funding_outputs: Vec::new(), change_script: None, }; let shared_funding_output = TxOut { value: Amount::from_sat(funding.get_value_satoshis()), script_pubkey: funding.get_funding_redeemscript().to_p2wsh(), }; + let inputs_to_contribute = our_funding_inputs + .into_iter() + .map(|FundingTxInput { txin, prevtx, .. }| (txin, prevtx)) + .collect(); let interactive_tx_constructor = Some(InteractiveTxConstructor::new( InteractiveTxConstructorArgs { @@ -12655,10 +12808,10 @@ where 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, + inputs_to_contribute, shared_funding_input: None, - shared_funding_output: SharedOwnedOutput::new(shared_funding_output, our_funding_satoshis), - outputs_to_contribute: Vec::new(), + shared_funding_output: SharedOwnedOutput::new(shared_funding_output, our_funding_contribution_sats), + outputs_to_contribute: funding_negotiation_context.our_funding_outputs.clone(), } ).map_err(|err| { let reason = ClosureReason::ProcessingError { err: err.to_string() }; @@ -12738,7 +12891,7 @@ where }), channel_type: Some(self.funding.get_channel_type().clone()), }, - funding_satoshis: self.funding_negotiation_context.our_funding_contribution_satoshis + funding_satoshis: self.funding_negotiation_context.our_funding_contribution.to_sat() as u64, second_per_commitment_point, require_confirmed_inputs: None, @@ -14080,6 +14233,8 @@ mod tests { TOTAL_BITCOIN_SUPPLY_SATOSHIS, }; use crate::ln::channel_keys::{RevocationBasepoint, RevocationKey}; + #[cfg(splicing)] + use crate::ln::channelmanager::FundingTxInput; use crate::ln::channelmanager::{self, HTLCSource, PaymentId}; use crate::ln::msgs; use crate::ln::msgs::{ChannelUpdate, UnsignedChannelUpdate, MAX_VALUE_MSAT}; @@ -15822,50 +15977,52 @@ mod tests { // 2 inputs with weight 300, initiator, 2000 sat/kw feerate assert_eq!( - estimate_v2_funding_transaction_fee(true, 2, Weight::from_wu(300), 2000), + estimate_v2_funding_transaction_fee(true, 2, Weight::from_wu(300), &[], 2000), 1668 ); // higher feerate assert_eq!( - estimate_v2_funding_transaction_fee(true, 2, Weight::from_wu(300), 3000), + estimate_v2_funding_transaction_fee(true, 2, Weight::from_wu(300), &[], 3000), 2502 ); // only 1 input assert_eq!( - estimate_v2_funding_transaction_fee(true, 1, Weight::from_wu(300), 2000), + estimate_v2_funding_transaction_fee(true, 1, Weight::from_wu(300), &[], 2000), 1348 ); // 0 input weight assert_eq!( - estimate_v2_funding_transaction_fee(true, 1, Weight::from_wu(0), 2000), + estimate_v2_funding_transaction_fee(true, 1, Weight::from_wu(0), &[], 2000), 748 ); // not initiator assert_eq!( - estimate_v2_funding_transaction_fee(false, 1, Weight::from_wu(0), 2000), + estimate_v2_funding_transaction_fee(false, 1, Weight::from_wu(0), &[], 2000), 320 ); } #[cfg(splicing)] #[rustfmt::skip] - fn funding_input_sats(input_value_sats: u64) -> (TxIn, Transaction, Weight) { + fn funding_input_sats(input_value_sats: u64) -> FundingTxInput { use crate::sign::P2WPKH_WITNESS_WEIGHT; - let input_1_prev_out = TxOut { value: Amount::from_sat(input_value_sats), script_pubkey: ScriptBuf::default() }; - let input_1_prev_tx = Transaction { - input: vec![], output: vec![input_1_prev_out], + let prevout = TxOut { value: Amount::from_sat(input_value_sats), script_pubkey: ScriptBuf::default() }; + let prevtx = Transaction { + input: vec![], output: vec![prevout], version: Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO, }; - let input_1_txin = TxIn { - previous_output: bitcoin::OutPoint { txid: input_1_prev_tx.compute_txid(), vout: 0 }, + let txin = TxIn { + previous_output: bitcoin::OutPoint { txid: prevtx.compute_txid(), vout: 0 }, ..Default::default() }; - (input_1_txin, input_1_prev_tx, Weight::from_wu(P2WPKH_WITNESS_WEIGHT)) + let witness_weight = Weight::from_wu(P2WPKH_WITNESS_WEIGHT); + + FundingTxInput { txin, prevtx, witness_weight } } #[cfg(splicing)] diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 6f411273aab..8a7408cea2c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -30,9 +30,9 @@ use bitcoin::hashes::{Hash, HashEngine, HmacEngine}; use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::{PublicKey, SecretKey}; -use bitcoin::{secp256k1, Sequence}; +use bitcoin::{secp256k1, Sequence, SignedAmount, TxIn, Weight}; #[cfg(splicing)] -use bitcoin::{ScriptBuf, TxIn, Weight}; +use bitcoin::{Amount, ScriptBuf, TxOut}; use crate::blinded_path::message::MessageForwardNode; use crate::blinded_path::message::{AsyncPaymentsContext, OffersContext}; @@ -200,6 +200,93 @@ pub use crate::ln::outbound_payment::{ }; use crate::ln::script::ShutdownScript; +/// The components of a splice's funding transaction that are contributed by one party. +#[cfg(splicing)] +pub enum SpliceContribution { + /// When funds are added to a channel. + SpliceIn { + /// The amount to contribute to the splice. + value: Amount, + + /// The inputs used to include in the splice's funding transaction used to meet the + /// contributed amount. Any excess amount will be sent to a change output. + inputs: Vec, + + /// An optional change output script. This will be used if needed or, when not set, + /// generated using `SignerProvider::get_destination_script`. + change_script: Option, + }, + /// When funds are removed from a channel. + SpliceOut { + /// The outputs to include in the splice's funding transaction. The total value of all + /// outputs will be the amount that is removed. + outputs: Vec, + }, +} + +#[cfg(splicing)] +impl SpliceContribution { + pub(super) fn value(&self) -> SignedAmount { + match self { + SpliceContribution::SpliceIn { value, .. } => { + value.to_signed().unwrap_or(SignedAmount::MAX) + }, + SpliceContribution::SpliceOut { outputs } => { + let value_removed = outputs + .iter() + .map(|txout| txout.value) + .sum::() + .to_signed() + .unwrap_or(SignedAmount::MAX); + -value_removed + }, + } + } + + pub(super) fn inputs(&self) -> &[FundingTxInput] { + match self { + SpliceContribution::SpliceIn { inputs, .. } => &inputs[..], + SpliceContribution::SpliceOut { .. } => &[], + } + } + + pub(super) fn outputs(&self) -> &[TxOut] { + match self { + SpliceContribution::SpliceIn { .. } => &[], + SpliceContribution::SpliceOut { outputs } => &outputs[..], + } + } + + pub(super) fn into_tx_parts(self) -> (Vec, Vec, Option) { + match self { + SpliceContribution::SpliceIn { inputs, change_script, .. } => { + (inputs, vec![], change_script) + }, + SpliceContribution::SpliceOut { outputs } => (vec![], outputs, None), + } + } +} + +/// An input to contribute to a channel's funding transaction either when using the v2 channel +/// establishment protocol or when splicing. +#[derive(Clone)] +pub struct FundingTxInput { + /// An input for the funding transaction used to cover the channel contributions. + pub txin: TxIn, + + /// The transaction containing the unspent [`TxOut`] referenced by [`txin`]. + /// + /// [`TxOut`]: bitcoin::TxOut + /// [`txin`]: Self::txin + pub prevtx: Transaction, + + /// The weight of the witness that is needed to spend the [`TxOut`] referenced by [`txin`]. + /// + /// [`TxOut`]: bitcoin::TxOut + /// [`txin`]: Self::txin + pub witness_weight: Weight, +} + // We hold various information about HTLC relay in the HTLC objects in Channel itself: // // Upon receipt of an HTLC from a peer, we'll give it a PendingHTLCStatus indicating if it should @@ -4436,14 +4523,13 @@ where #[cfg(splicing)] #[rustfmt::skip] pub fn splice_channel( - &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, our_funding_contribution_satoshis: i64, - our_funding_inputs: Vec<(TxIn, Transaction, Weight)>, change_script: Option, - funding_feerate_per_kw: u32, locktime: Option, + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: Option, ) -> Result<(), APIError> { 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, change_script, funding_feerate_per_kw, locktime + channel_id, counterparty_node_id, contribution, funding_feerate_per_kw, locktime ); res = result; match res { @@ -4458,9 +4544,7 @@ where #[cfg(splicing)] fn internal_splice_channel( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, - our_funding_contribution_satoshis: i64, - our_funding_inputs: Vec<(TxIn, Transaction, Weight)>, change_script: Option, - funding_feerate_per_kw: u32, locktime: Option, + contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: Option, ) -> Result<(), APIError> { let per_peer_state = self.per_peer_state.read().unwrap(); @@ -4481,13 +4565,8 @@ 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, - change_script, - funding_feerate_per_kw, - locktime, - )?; + let msg = + chan.splice_channel(contribution, funding_feerate_per_kw, locktime)?; peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceInit { node_id: *counterparty_node_id, msg, @@ -9198,7 +9277,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.funding_negotiation_context.our_funding_contribution_satoshis > 0 { + if unfunded_chan.funding_negotiation_context.our_funding_contribution > SignedAmount::ZERO { continue; } } diff --git a/lightning/src/ln/dual_funding_tests.rs b/lightning/src/ln/dual_funding_tests.rs index 39cf6200765..d40bdf1c2df 100644 --- a/lightning/src/ln/dual_funding_tests.rs +++ b/lightning/src/ln/dual_funding_tests.rs @@ -18,12 +18,12 @@ use { }, crate::ln::channel::PendingV2Channel, crate::ln::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBasepoint}, + crate::ln::channelmanager::FundingTxInput, crate::ln::functional_test_utils::*, crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}, crate::ln::msgs::{CommitmentSigned, TxAddInput, TxAddOutput, TxComplete, TxSignatures}, crate::ln::types::ChannelId, crate::prelude::*, - crate::util::ser::TransactionU16LenLimited, crate::util::test_utils, bitcoin::Witness, }; @@ -49,10 +49,7 @@ fn do_test_v2_channel_establishment(session: V2ChannelEstablishmentTestSession) let initiator_funding_inputs: Vec<_> = create_dual_funding_utxos_with_prev_txs( &nodes[0], &[session.initiator_input_value_satoshis], - ) - .into_iter() - .map(|(txin, tx, _)| (txin, TransactionU16LenLimited::new(tx).unwrap())) - .collect(); + ); // Alice creates a dual-funded channel as initiator. let funding_satoshis = session.funding_input_sats; @@ -86,15 +83,16 @@ fn do_test_v2_channel_establishment(session: V2ChannelEstablishmentTestSession) &RevocationBasepoint::from(open_channel_v2_msg.common_fields.revocation_basepoint), ); + let FundingTxInput { txin, prevtx, .. } = &initiator_funding_inputs[0]; let tx_add_input_msg = TxAddInput { channel_id, serial_id: 2, // Even serial_id from initiator. - prevtx: Some(initiator_funding_inputs[0].1.clone()), + prevtx: Some(prevtx.clone()), prevtx_out: 0, - sequence: initiator_funding_inputs[0].0.sequence.0, + sequence: txin.sequence.0, shared_input_txid: None, }; - let input_value = tx_add_input_msg.prevtx.as_ref().unwrap().as_transaction().output + let input_value = tx_add_input_msg.prevtx.as_ref().unwrap().output [tx_add_input_msg.prevtx_out as usize] .value; assert_eq!(input_value.to_sat(), session.initiator_input_value_satoshis); diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 00e883e8561..2fa21b4d00f 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -23,8 +23,8 @@ use crate::events::{ }; use crate::ln::chan_utils::{commitment_tx_base_weight, COMMITMENT_TX_WEIGHT_PER_HTLC}; use crate::ln::channelmanager::{ - AChannelManager, ChainParameters, ChannelManager, ChannelManagerReadArgs, PaymentId, - RAACommitmentOrder, RecipientOnionFields, MIN_CLTV_EXPIRY_DELTA, + AChannelManager, ChainParameters, ChannelManager, ChannelManagerReadArgs, FundingTxInput, + PaymentId, RAACommitmentOrder, RecipientOnionFields, MIN_CLTV_EXPIRY_DELTA, }; use crate::ln::msgs; use crate::ln::msgs::{ @@ -1440,7 +1440,7 @@ fn internal_create_funding_transaction<'a, 'b, 'c>( /// Return the inputs (with prev tx), and the total witness weight for these inputs pub fn create_dual_funding_utxos_with_prev_txs( node: &Node<'_, '_, '_>, utxo_values_in_satoshis: &[u64], -) -> Vec<(TxIn, Transaction, Weight)> { +) -> Vec { // Ensure we have unique transactions per node by using the locktime. let tx = Transaction { version: TxVersion::TWO, @@ -1462,17 +1462,17 @@ pub fn create_dual_funding_utxos_with_prev_txs( let mut inputs = vec![]; for i in 0..utxo_values_in_satoshis.len() { - inputs.push(( - TxIn { + inputs.push(FundingTxInput { + txin: TxIn { previous_output: OutPoint { txid: tx.compute_txid(), index: i as u16 } .into_bitcoin_outpoint(), script_sig: ScriptBuf::new(), sequence: Sequence::ZERO, witness: Witness::new(), }, - tx.clone(), - Weight::from_wu(P2WPKH_WITNESS_WEIGHT), - )); + prevtx: tx.clone(), + witness_weight: Weight::from_wu(P2WPKH_WITNESS_WEIGHT), + }); } inputs diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 3fcf3f4ee01..a531f1ab04e 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -11,7 +11,7 @@ use crate::io_extras::sink; use crate::prelude::*; use bitcoin::absolute::LockTime as AbsoluteLockTime; -use bitcoin::amount::Amount; +use bitcoin::amount::{Amount, SignedAmount}; use bitcoin::consensus::Encodable; use bitcoin::constants::WITNESS_SCALE_FACTOR; use bitcoin::policy::MAX_STANDARD_TX_WEIGHT; @@ -23,11 +23,11 @@ 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::{FundingNegotiationContext, TOTAL_BITCOIN_SUPPLY_SATOSHIS}; +use crate::ln::channelmanager::FundingTxInput; use crate::ln::msgs; use crate::ln::msgs::{MessageSendEvent, SerialId, TxSignatures}; use crate::ln::types::ChannelId; use crate::sign::{EntropySource, P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT}; -use crate::util::ser::TransactionU16LenLimited; use core::fmt::Display; use core::ops::Deref; @@ -676,10 +676,9 @@ impl NegotiationContext { return Err(AbortReason::UnexpectedFundingInput); } } else if let Some(prevtx) = &msg.prevtx { - let transaction = prevtx.as_transaction(); - let txid = transaction.compute_txid(); + let txid = prevtx.compute_txid(); - if let Some(tx_out) = transaction.output.get(msg.prevtx_out as usize) { + if let Some(tx_out) = prevtx.output.get(msg.prevtx_out as usize) { if !tx_out.script_pubkey.is_witness_program() { // The receiving node: // - MUST fail the negotiation if: @@ -860,14 +859,9 @@ impl NegotiationContext { return Err(AbortReason::UnexpectedFundingInput); } } else if let Some(prevtx) = &msg.prevtx { - let prev_txid = prevtx.as_transaction().compute_txid(); + let prev_txid = prevtx.compute_txid(); let prev_outpoint = OutPoint { txid: prev_txid, vout: msg.prevtx_out }; - let prev_output = prevtx - .as_transaction() - .output - .get(vout) - .ok_or(AbortReason::PrevTxOutInvalid)? - .clone(); + let prev_output = prevtx.output.get(vout).ok_or(AbortReason::PrevTxOutInvalid)?.clone(); let txin = TxIn { previous_output: prev_outpoint, sequence: Sequence(msg.sequence), @@ -1247,7 +1241,7 @@ impl_writeable_tlv_based_enum!(AddingRole, #[derive(Clone, Debug, Eq, PartialEq)] struct SingleOwnedInput { input: TxIn, - prev_tx: TransactionU16LenLimited, + prev_tx: Transaction, prev_output: TxOut, } @@ -1652,7 +1646,7 @@ where pub feerate_sat_per_kw: u32, pub is_initiator: bool, pub funding_tx_locktime: AbsoluteLockTime, - pub inputs_to_contribute: Vec<(TxIn, TransactionU16LenLimited)>, + pub inputs_to_contribute: Vec<(TxIn, Transaction)>, pub shared_funding_input: Option, pub shared_funding_output: SharedOwnedOutput, pub outputs_to_contribute: Vec, @@ -1694,7 +1688,7 @@ impl InteractiveTxConstructor { // Check for the existence of prevouts' for (txin, tx) in inputs_to_contribute.iter() { let vout = txin.previous_output.vout as usize; - if tx.as_transaction().output.get(vout).is_none() { + if tx.output.get(vout).is_none() { return Err(AbortReason::PrevTxOutInvalid); } } @@ -1703,7 +1697,7 @@ impl InteractiveTxConstructor { .map(|(txin, tx)| { let serial_id = generate_holder_serial_id(entropy_source, is_initiator); let vout = txin.previous_output.vout as usize; - let prev_output = tx.as_transaction().output.get(vout).unwrap().clone(); // checked above + let prev_output = tx.output.get(vout).unwrap().clone(); // checked above let input = InputOwned::Single(SingleOwnedInput { input: txin, prev_tx: tx, prev_output }); (serial_id, input) @@ -1884,28 +1878,29 @@ impl InteractiveTxConstructor { /// - `change_output_dust_limit` - The dust limit (in sats) to consider. pub(super) fn calculate_change_output_value( context: &FundingNegotiationContext, is_splice: bool, shared_output_funding_script: &ScriptBuf, - funding_outputs: &Vec, change_output_dust_limit: u64, + change_output_dust_limit: u64, ) -> Result, AbortReason> { - assert!(context.our_funding_contribution_satoshis > 0); - let our_funding_contribution_satoshis = context.our_funding_contribution_satoshis as u64; + assert!(context.our_funding_contribution > SignedAmount::ZERO); + let our_funding_contribution_satoshis = context.our_funding_contribution.to_sat() as u64; let mut total_input_satoshis = 0u64; let mut our_funding_inputs_weight = 0u64; - for (txin, tx) in context.our_funding_inputs.iter() { - let txid = tx.as_transaction().compute_txid(); + for FundingTxInput { txin, prevtx, witness_weight } in context.our_funding_inputs.iter() { + let txid = prevtx.compute_txid(); if txin.previous_output.txid != txid { return Err(AbortReason::PrevTxOutInvalid); } - let output = tx - .as_transaction() + let output = prevtx .output .get(txin.previous_output.vout as usize) .ok_or(AbortReason::PrevTxOutInvalid)?; total_input_satoshis = total_input_satoshis.saturating_add(output.value.to_sat()); - let weight = estimate_input_weight(output).to_wu(); + + let weight = BASE_INPUT_WEIGHT + EMPTY_SCRIPT_SIG_WEIGHT + witness_weight.to_wu(); our_funding_inputs_weight = our_funding_inputs_weight.saturating_add(weight); } + let funding_outputs = &context.our_funding_outputs; 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| { @@ -1945,6 +1940,7 @@ pub(super) fn calculate_change_output_value( mod tests { use crate::chain::chaininterface::{fee_for_weight, FEERATE_FLOOR_SATS_PER_KW}; use crate::ln::channel::{FundingNegotiationContext, TOTAL_BITCOIN_SUPPLY_SATOSHIS}; + use crate::ln::channelmanager::FundingTxInput; use crate::ln::interactivetxs::{ calculate_change_output_value, generate_holder_serial_id, AbortReason, HandleTxCompleteValue, InteractiveTxConstructor, InteractiveTxConstructorArgs, @@ -1954,7 +1950,6 @@ mod tests { use crate::ln::types::ChannelId; use crate::sign::EntropySource; use crate::util::atomic_counter::AtomicCounter; - use crate::util::ser::TransactionU16LenLimited; use bitcoin::absolute::LockTime as AbsoluteLockTime; use bitcoin::amount::Amount; use bitcoin::hashes::Hash; @@ -1964,13 +1959,14 @@ mod tests { use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, SecretKey}; use bitcoin::transaction::Version; use bitcoin::{ - OutPoint, PubkeyHash, ScriptBuf, Sequence, Transaction, TxIn, TxOut, WPubkeyHash, Witness, + OutPoint, PubkeyHash, ScriptBuf, Sequence, SignedAmount, Transaction, TxIn, TxOut, + WPubkeyHash, Weight, Witness, }; use core::ops::Deref; use super::{ get_output_weight, P2TR_INPUT_WEIGHT_LOWER_BOUND, P2WPKH_INPUT_WEIGHT_LOWER_BOUND, - P2WSH_INPUT_WEIGHT_LOWER_BOUND, TX_COMMON_FIELDS_WEIGHT, + P2WPKH_WITNESS_WEIGHT, P2WSH_INPUT_WEIGHT_LOWER_BOUND, TX_COMMON_FIELDS_WEIGHT, }; const TEST_FEERATE_SATS_PER_KW: u32 = FEERATE_FLOOR_SATS_PER_KW * 10; @@ -2017,12 +2013,12 @@ mod tests { struct TestSession { description: &'static str, - inputs_a: Vec<(TxIn, TransactionU16LenLimited)>, + inputs_a: Vec<(TxIn, Transaction)>, a_shared_input: Option<(OutPoint, TxOut, u64)>, /// The funding output, with the value contributed shared_output_a: (TxOut, u64), outputs_a: Vec, - inputs_b: Vec<(TxIn, TransactionU16LenLimited)>, + inputs_b: Vec<(TxIn, Transaction)>, b_shared_input: Option<(OutPoint, TxOut, u64)>, /// The funding output, with the value contributed shared_output_b: (TxOut, u64), @@ -2288,7 +2284,7 @@ mod tests { } } - fn generate_inputs(outputs: &[TestOutput]) -> Vec<(TxIn, TransactionU16LenLimited)> { + fn generate_inputs(outputs: &[TestOutput]) -> Vec<(TxIn, Transaction)> { let tx = generate_tx(outputs); let txid = tx.compute_txid(); tx.output @@ -2301,7 +2297,7 @@ mod tests { sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, witness: Default::default(), }; - (txin, TransactionU16LenLimited::new(tx.clone()).unwrap()) + (txin, tx.clone()) }) .collect() } @@ -2349,12 +2345,12 @@ mod tests { (generate_txout(&TestOutput::P2WSH(value)), local_value) } - fn generate_fixed_number_of_inputs(count: u16) -> Vec<(TxIn, TransactionU16LenLimited)> { + fn generate_fixed_number_of_inputs(count: u16) -> Vec<(TxIn, Transaction)> { // Generate transactions with a total `count` number of outputs such that no transaction has a // serialized length greater than u16::MAX. let max_outputs_per_prevtx = 1_500; let mut remaining = count; - let mut inputs: Vec<(TxIn, TransactionU16LenLimited)> = Vec::with_capacity(count as usize); + let mut inputs: Vec<(TxIn, Transaction)> = Vec::with_capacity(count as usize); while remaining > 0 { let tx_output_count = remaining.min(max_outputs_per_prevtx); @@ -2367,7 +2363,7 @@ mod tests { ); let txid = tx.compute_txid(); - let mut temp: Vec<(TxIn, TransactionU16LenLimited)> = tx + let mut temp: Vec<(TxIn, Transaction)> = tx .output .iter() .enumerate() @@ -2378,7 +2374,7 @@ mod tests { sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, witness: Default::default(), }; - (input, TransactionU16LenLimited::new(tx.clone()).unwrap()) + (input, tx.clone()) }) .collect(); @@ -2589,10 +2585,9 @@ mod tests { expect_error: Some((AbortReason::PrevTxOutInvalid, ErrorCulprit::NodeA)), }); - let tx = - TransactionU16LenLimited::new(generate_tx(&[TestOutput::P2WPKH(1_000_000)])).unwrap(); + let tx = generate_tx(&[TestOutput::P2WPKH(1_000_000)]); let invalid_sequence_input = TxIn { - previous_output: OutPoint { txid: tx.as_transaction().compute_txid(), vout: 0 }, + previous_output: OutPoint { txid: tx.compute_txid(), vout: 0 }, ..Default::default() }; do_test_interactive_tx_constructor(TestSession { @@ -2608,7 +2603,7 @@ mod tests { expect_error: Some((AbortReason::IncorrectInputSequenceValue, ErrorCulprit::NodeA)), }); let duplicate_input = TxIn { - previous_output: OutPoint { txid: tx.as_transaction().compute_txid(), vout: 0 }, + previous_output: OutPoint { txid: tx.compute_txid(), vout: 0 }, sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, ..Default::default() }; @@ -2626,7 +2621,7 @@ mod tests { }); // Non-initiator uses same prevout as initiator. let duplicate_input = TxIn { - previous_output: OutPoint { txid: tx.as_transaction().compute_txid(), vout: 0 }, + previous_output: OutPoint { txid: tx.compute_txid(), vout: 0 }, sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, ..Default::default() }; @@ -2643,7 +2638,7 @@ mod tests { expect_error: Some((AbortReason::PrevTxOutInvalid, ErrorCulprit::NodeA)), }); let duplicate_input = TxIn { - previous_output: OutPoint { txid: tx.as_transaction().compute_txid(), vout: 0 }, + previous_output: OutPoint { txid: tx.compute_txid(), vout: 0 }, sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, ..Default::default() }; @@ -2963,22 +2958,23 @@ mod tests { let inputs = input_prevouts .iter() .map(|txout| { - let tx = Transaction { + let prevtx = Transaction { input: Vec::new(), output: vec![(*txout).clone()], lock_time: AbsoluteLockTime::ZERO, version: Version::TWO, }; - let txid = tx.compute_txid(); + let txid = prevtx.compute_txid(); let txin = TxIn { previous_output: OutPoint { txid, vout: 0 }, script_sig: ScriptBuf::new(), sequence: Sequence::ZERO, witness: Witness::new(), }; - (txin, TransactionU16LenLimited::new(tx).unwrap()) + let witness_weight = Weight::from_wu(P2WPKH_WITNESS_WEIGHT); + FundingTxInput { txin, prevtx, witness_weight } }) - .collect::>(); + .collect(); let our_contributed = 110_000; let txout = TxOut { value: Amount::from_sat(10_000), script_pubkey: ScriptBuf::new() }; let outputs = vec![txout]; @@ -2993,68 +2989,68 @@ mod tests { // There is leftover for change let context = FundingNegotiationContext { is_initiator: true, - our_funding_contribution_satoshis: our_contributed as i64, - their_funding_contribution_satoshis: None, + our_funding_contribution: SignedAmount::from_sat(our_contributed as i64), funding_tx_locktime: AbsoluteLockTime::ZERO, funding_feerate_sat_per_1000_weight, shared_funding_input: None, our_funding_inputs: inputs, + our_funding_outputs: outputs, change_script: None, }; assert_eq!( - calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300), + calculate_change_output_value(&context, false, &ScriptBuf::new(), 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), + calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), Ok(Some(gross_change - fees)), ); // Insufficient inputs, no leftover let context = FundingNegotiationContext { is_initiator: false, - our_funding_contribution_satoshis: 130_000, + our_funding_contribution: SignedAmount::from_sat(130_000), ..context }; assert_eq!( - calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300), + calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), Err(AbortReason::InsufficientFees), ); // Very small leftover let context = FundingNegotiationContext { is_initiator: false, - our_funding_contribution_satoshis: 118_000, + our_funding_contribution: SignedAmount::from_sat(118_000), ..context }; assert_eq!( - calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300), + calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), Ok(None), ); // Small leftover, but not dust let context = FundingNegotiationContext { is_initiator: false, - our_funding_contribution_satoshis: 117_992, + our_funding_contribution: SignedAmount::from_sat(117_992), ..context }; assert_eq!( - calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 100), + calculate_change_output_value(&context, false, &ScriptBuf::new(), 100), Ok(Some(262)), ); // Larger fee, smaller change let context = FundingNegotiationContext { is_initiator: true, - our_funding_contribution_satoshis: our_contributed as i64, + our_funding_contribution: SignedAmount::from_sat(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), + calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), Ok(Some(4060)), ); } diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index e0219a5523f..71f73e04c2f 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -29,7 +29,7 @@ use bitcoin::hash_types::Txid; use bitcoin::script::ScriptBuf; use bitcoin::secp256k1::ecdsa::Signature; use bitcoin::secp256k1::PublicKey; -use bitcoin::{secp256k1, Witness}; +use bitcoin::{secp256k1, Transaction, Witness}; use crate::blinded_path::payment::{ BlindedPaymentTlvs, ForwardTlvs, ReceiveTlvs, UnauthenticatedReceiveTlvs, @@ -63,8 +63,7 @@ use crate::util::base32; use crate::util::logger; use crate::util::ser::{ BigSize, FixedLengthReader, HighZeroBytesDroppedBigSize, Hostname, LengthLimitedRead, - LengthReadable, LengthReadableArgs, Readable, ReadableArgs, TransactionU16LenLimited, - WithoutLength, Writeable, Writer, + LengthReadable, LengthReadableArgs, Readable, ReadableArgs, WithoutLength, Writeable, Writer, }; use crate::routing::gossip::{NodeAlias, NodeId}; @@ -524,7 +523,7 @@ pub struct TxAddInput { pub serial_id: SerialId, /// Serialized transaction that contains the output this input spends to verify that it is /// non-malleable. Omitted for shared input. - pub prevtx: Option, + pub prevtx: Option, /// The index of the output being spent pub prevtx_out: u32, /// The sequence number of this input @@ -2738,16 +2737,58 @@ impl_writeable_msg!(SpliceLocked, { splice_txid, }, {}); -impl_writeable_msg!(TxAddInput, { - channel_id, - serial_id, - prevtx, - prevtx_out, - sequence, -}, { - (0, shared_input_txid, option), // `funding_txid` -}); +impl Writeable for TxAddInput { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.channel_id.write(w)?; + self.serial_id.write(w)?; + + match &self.prevtx { + Some(tx) => { + (tx.serialized_length() as u16).write(w)?; + tx.write(w)?; + }, + None => 0u16.write(w)?, + } + + self.prevtx_out.write(w)?; + self.sequence.write(w)?; + + encode_tlv_stream!(w, { + (0, self.shared_input_txid, option), + }); + Ok(()) + } +} + +impl LengthReadable for TxAddInput { + fn read_from_fixed_length_buffer(r: &mut R) -> Result { + let channel_id: ChannelId = Readable::read(r)?; + let serial_id: SerialId = Readable::read(r)?; + + let prevtx_len: u16 = Readable::read(r)?; + let prevtx = if prevtx_len > 0 { + let mut tx_reader = FixedLengthReader::new(r, prevtx_len as u64); + let tx: Transaction = Readable::read(&mut tx_reader)?; + if tx_reader.bytes_remain() { + return Err(DecodeError::BadLengthDescriptor); + } + + Some(tx) + } else { + None + }; + + let prevtx_out: u32 = Readable::read(r)?; + let sequence: u32 = Readable::read(r)?; + let mut shared_input_txid: Option = None; + decode_tlv_stream!(r, { + (0, shared_input_txid, option), + }); + + Ok(TxAddInput { channel_id, serial_id, prevtx, prevtx_out, sequence, shared_input_txid }) + } +} impl_writeable_msg!(TxAddOutput, { channel_id, serial_id, @@ -4224,10 +4265,7 @@ mod tests { ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures, }; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; - use crate::util::ser::{ - BigSize, Hostname, LengthReadable, Readable, ReadableArgs, TransactionU16LenLimited, - Writeable, - }; + use crate::util::ser::{BigSize, Hostname, LengthReadable, Readable, ReadableArgs, Writeable}; use crate::util::test_utils; use bitcoin::hex::DisplayHex; use bitcoin::{Amount, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness}; @@ -5299,7 +5337,7 @@ mod tests { let tx_add_input = msgs::TxAddInput { channel_id: ChannelId::from_bytes([2; 32]), serial_id: 4886718345, - prevtx: Some(TransactionU16LenLimited::new(Transaction { + prevtx: Some(Transaction { version: Version::TWO, lock_time: LockTime::ZERO, input: vec![TxIn { @@ -5320,7 +5358,7 @@ mod tests { script_pubkey: Address::from_str("bc1qxmk834g5marzm227dgqvynd23y2nvt2ztwcw2z").unwrap().assume_checked().script_pubkey(), }, ], - }).unwrap()), + }), prevtx_out: 305419896, sequence: 305419896, shared_input_txid: None, diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index a88a5f76c7e..19b274de75f 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -7,10 +7,13 @@ // You may not use this file except in accordance with one or both of these // licenses. +use crate::ln::channelmanager::SpliceContribution; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; use crate::util::errors::APIError; +use bitcoin::Amount; + /// Splicing test, simple splice-in flow. Starts with opening a V1 channel first. /// Builds on test_channel_open_simple() #[test] @@ -66,15 +69,20 @@ fn test_v1_splice_in() { &initiator_node, &[extra_splice_funding_input_sats], ); + + let contribution = SpliceContribution::SpliceIn { + value: Amount::from_sat(splice_in_sats), + inputs: funding_inputs, + change_script: None, + }; + // Initiate splice-in let _res = initiator_node .node .splice_channel( &channel_id, &acceptor_node.node.get_our_node_id(), - splice_in_sats as i64, - funding_inputs, - None, // change_script + contribution, funding_feerate_per_kw, None, // locktime ) @@ -154,7 +162,7 @@ fn test_v1_splice_in() { ); } else { // Input is the extra input - let prevtx_value = tx_add_input_msg.prevtx.as_ref().unwrap().as_transaction().output + let prevtx_value = tx_add_input_msg.prevtx.as_ref().unwrap().output [tx_add_input_msg.prevtx_out as usize] .value .to_sat(); @@ -182,7 +190,7 @@ fn test_v1_splice_in() { ); if !inputs_seen_in_reverse { // Input is the extra input - let prevtx_value = tx_add_input2_msg.prevtx.as_ref().unwrap().as_transaction().output + let prevtx_value = tx_add_input2_msg.prevtx.as_ref().unwrap().output [tx_add_input2_msg.prevtx_out as usize] .value .to_sat(); @@ -317,19 +325,23 @@ fn test_v1_splice_in_negative_insufficient_inputs() { let funding_inputs = create_dual_funding_utxos_with_prev_txs(&nodes[0], &[extra_splice_funding_input_sats]); + let contribution = SpliceContribution::SpliceIn { + value: Amount::from_sat(splice_in_sats), + inputs: funding_inputs, + change_script: None, + }; + // Initiate splice-in, with insufficient input contribution let res = nodes[0].node.splice_channel( &channel_id, &nodes[1].node.get_our_node_id(), - splice_in_sats as i64, - funding_inputs, - None, // change_script + contribution, 1024, // funding_feerate_per_kw, None, // locktime ); match res { Err(APIError::APIMisuseError { err }) => { - assert!(err.contains("Insufficient inputs for splicing")) + assert!(err.contains("Need more inputs")) }, _ => panic!("Wrong error {:?}", res.err().unwrap()), } diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index ac2b529f0bd..ea49e59ab89 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1676,63 +1676,6 @@ impl Readable for Duration { } } -/// A wrapper for a `Transaction` which can only be constructed with [`TransactionU16LenLimited::new`] -/// if the `Transaction`'s consensus-serialized length is <= u16::MAX. -/// -/// Use [`TransactionU16LenLimited::into_transaction`] to convert into the contained `Transaction`. -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct TransactionU16LenLimited(Transaction); - -impl TransactionU16LenLimited { - /// Constructs a new `TransactionU16LenLimited` from a `Transaction` only if it's consensus- - /// serialized length is <= u16::MAX. - pub fn new(transaction: Transaction) -> Result { - if transaction.serialized_length() > (u16::MAX as usize) { - Err(()) - } else { - Ok(Self(transaction)) - } - } - - /// Consumes this `TransactionU16LenLimited` and returns its contained `Transaction`. - pub fn into_transaction(self) -> Transaction { - self.0 - } - - /// Returns a reference to the contained `Transaction` - pub fn as_transaction(&self) -> &Transaction { - &self.0 - } -} - -impl Writeable for Option { - fn write(&self, w: &mut W) -> Result<(), io::Error> { - match self { - Some(tx) => { - (tx.0.serialized_length() as u16).write(w)?; - tx.0.write(w) - }, - None => 0u16.write(w), - } - } -} - -impl Readable for Option { - fn read(r: &mut R) -> Result { - let len = ::read(r)?; - if len == 0 { - return Ok(None); - } - let mut tx_reader = FixedLengthReader::new(r, len as u64); - let tx: Transaction = Readable::read(&mut tx_reader)?; - if tx_reader.bytes_remain() { - Err(DecodeError::BadLengthDescriptor) - } else { - Ok(Some(TransactionU16LenLimited(tx))) - } - } -} - impl Writeable for ClaimId { fn write(&self, writer: &mut W) -> Result<(), io::Error> { self.0.write(writer)