diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 56d38d5545c..c812fe64090 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, Witness}; use bitcoin::hash_types::{BlockHash, Txid}; @@ -24,9 +24,9 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::constants::PUBLIC_KEY_SIZE; use bitcoin::secp256k1::{ecdsa::Signature, Secp256k1}; use bitcoin::secp256k1::{PublicKey, SecretKey}; +use bitcoin::{secp256k1, sighash, TxIn}; #[cfg(splicing)] -use bitcoin::Sequence; -use bitcoin::{secp256k1, sighash}; +use bitcoin::{FeeRate, Sequence}; use crate::chain::chaininterface::{ fee_for_weight, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator, @@ -57,6 +57,9 @@ use crate::ln::channelmanager::{ PaymentClaimDetails, PendingHTLCInfo, PendingHTLCStatus, RAACommitmentOrder, SentHTLCId, BREAKDOWN_TIMEOUT, MAX_LOCAL_BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA, }; +use crate::ln::funding::FundingTxInput; +#[cfg(splicing)] +use crate::ln::funding::SpliceContribution; #[cfg(splicing)] use crate::ln::interactivetxs::{ calculate_change_output_value, AbortReason, InteractiveTxMessageSend, @@ -72,6 +75,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 +90,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}; @@ -2244,20 +2247,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(); @@ -5874,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 { @@ -5913,28 +5951,23 @@ 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_satisfaction_weight = Weight::from_wu( + funding_inputs.iter().map(|input| input.utxo.satisfaction_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); + total_input_satisfaction_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_satisfaction_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) { - 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 - ))); - } + for FundingTxInput { utxo, .. } in funding_inputs.iter() { + total_input_sats = total_input_sats.saturating_add(utxo.output.value.to_sat()); } // If the inputs are enough to cover intended contribution amount, with fees even when @@ -5964,10 +5997,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, @@ -5979,7 +6009,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. @@ -6009,10 +6042,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()), @@ -6020,36 +6051,47 @@ 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 { utxo, sequence, prevtx }| { + (TxIn { previous_output: utxo.outpoint, sequence, ..Default::default() }, prevtx) + }) + .collect(); let constructor_args = InteractiveTxConstructorArgs { entropy_source, @@ -6059,7 +6101,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, @@ -10602,9 +10644,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) @@ -10628,53 +10668,100 @@ 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 exhausts total bitcoin supply: {}", + self.context.channel_id(), + our_funding_contribution, + ), + }); + } // 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 { utxo, 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: {}", + utxo.outpoint, + ), + }); + } } 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, }; @@ -10690,7 +10777,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, @@ -10701,10 +10788,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. @@ -10724,21 +10809,66 @@ where ))); } - if their_funding_contribution_satoshis.saturating_add(our_funding_contribution_satoshis) < 0 - { + debug_assert_eq!(our_funding_contribution, SignedAmount::ZERO); + + // TODO(splicing): Move this check once user-provided contributions are supported for + // counterparty-initiated splices. + if our_funding_contribution > SignedAmount::MAX_MONEY { + return Err(ChannelError::WarnAndDisconnect(format!( + "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!( - "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 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, )?; @@ -10763,7 +10893,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, @@ -10773,16 +10904,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, }; @@ -10815,7 +10945,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, }) @@ -10862,15 +10992,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, )?; @@ -12419,7 +12557,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 @@ -12468,13 +12606,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 { @@ -12578,10 +12715,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( @@ -12608,9 +12746,7 @@ where current_chain_height, logger, false, - - our_funding_satoshis, - + our_funding_contribution_sats, counterparty_pubkeys, channel_type, holder_selected_channel_reserve_satoshis, @@ -12625,18 +12761,24 @@ 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 { utxo, sequence, prevtx }| { + (TxIn { previous_output: utxo.outpoint, sequence, ..Default::default() }, prevtx) + }) + .collect(); let interactive_tx_constructor = Some(InteractiveTxConstructor::new( InteractiveTxConstructorArgs { @@ -12647,10 +12789,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() }; @@ -12730,7 +12872,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, @@ -14073,6 +14215,8 @@ mod tests { }; use crate::ln::channel_keys::{RevocationBasepoint, RevocationKey}; use crate::ln::channelmanager::{self, HTLCSource, PaymentId}; + #[cfg(splicing)] + use crate::ln::funding::FundingTxInput; use crate::ln::msgs; use crate::ln::msgs::{ChannelUpdate, UnsignedChannelUpdate, MAX_VALUE_MSAT}; use crate::ln::onion_utils::{AttributionData, LocalHTLCFailureReason}; @@ -14104,11 +14248,9 @@ mod tests { use bitcoin::secp256k1::ffi::Signature as FFISignature; use bitcoin::secp256k1::{ecdsa::Signature, Secp256k1}; use bitcoin::secp256k1::{PublicKey, SecretKey}; - #[cfg(splicing)] - use bitcoin::transaction::TxIn; use bitcoin::transaction::{Transaction, TxOut, Version}; #[cfg(splicing)] - use bitcoin::Weight; + use bitcoin::{ScriptBuf, Sequence, WPubkeyHash}; use bitcoin::{WitnessProgram, WitnessVersion}; use std::cmp; @@ -15819,50 +15961,48 @@ 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) { - use crate::sign::P2WPKH_WITNESS_WEIGHT; - - let input_1_prev_out = TxOut { value: Amount::from_sat(input_value_sats), script_pubkey: bitcoin::ScriptBuf::default() }; - let input_1_prev_tx = Transaction { - input: vec![], output: vec![input_1_prev_out], - version: Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO, + fn funding_input_sats(input_value_sats: u64) -> FundingTxInput { + let prevout = TxOut { + value: Amount::from_sat(input_value_sats), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), }; - let input_1_txin = TxIn { - previous_output: bitcoin::OutPoint { txid: input_1_prev_tx.compute_txid(), vout: 0 }, - ..Default::default() + let prevtx = Transaction { + input: vec![], output: vec![prevout], + version: Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO, }; - (input_1_txin, input_1_prev_tx, Weight::from_wu(P2WPKH_WITNESS_WEIGHT)) + + FundingTxInput::new_p2wpkh(prevtx, 0, Sequence::ZERO).unwrap() } #[cfg(splicing)] @@ -15883,7 +16023,7 @@ mod tests { true, 2000, ).unwrap(), - 2268, + 2284, ); // negative case, inputs clearly insufficient @@ -15899,13 +16039,13 @@ mod tests { ); assert_eq!( format!("{:?}", res.err().unwrap()), - "Warn: Total input amount 100000 is lower than needed for contribution 220000, considering fees of 1730. Need more inputs.", + "Warn: Total input amount 100000 is lower than needed for contribution 220000, considering fees of 1738. Need more inputs.", ); } // barely covers { - let expected_fee: u64 = 2268; + let expected_fee: u64 = 2284; assert_eq!( check_v2_funding_inputs_sufficient( (300_000 - expected_fee - 20) as i64, @@ -15935,13 +16075,13 @@ mod tests { ); assert_eq!( format!("{:?}", res.err().unwrap()), - "Warn: Total input amount 300000 is lower than needed for contribution 298032, considering fees of 2495. Need more inputs.", + "Warn: Total input amount 300000 is lower than needed for contribution 298032, considering fees of 2513. Need more inputs.", ); } // barely covers, less fees (no extra weight, no init) { - let expected_fee: u64 = 1076; + let expected_fee: u64 = 1092; assert_eq!( check_v2_funding_inputs_sufficient( (300_000 - expected_fee - 20) as i64, diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9d1c6292826..3d14937532b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -30,9 +30,7 @@ use bitcoin::hashes::{Hash, HashEngine, HmacEngine}; use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::{PublicKey, SecretKey}; -use bitcoin::{secp256k1, Sequence}; -#[cfg(splicing)] -use bitcoin::{ScriptBuf, TxIn, Weight}; +use bitcoin::{secp256k1, Sequence, SignedAmount}; use crate::blinded_path::message::MessageForwardNode; use crate::blinded_path::message::{AsyncPaymentsContext, OffersContext}; @@ -65,6 +63,8 @@ use crate::ln::channel::{ UpdateFulfillCommitFetch, WithChannelContext, }; use crate::ln::channel_state::ChannelDetails; +#[cfg(splicing)] +use crate::ln::funding::SpliceContribution; use crate::ln::inbound_payment; use crate::ln::interactivetxs::{HandleTxCompleteResult, InteractiveTxMessageSendResult}; use crate::ln::msgs; @@ -4458,14 +4458,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 { @@ -4480,9 +4479,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(); @@ -4503,13 +4500,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, @@ -9401,7 +9393,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..a91e04bbd82 100644 --- a/lightning/src/ln/dual_funding_tests.rs +++ b/lightning/src/ln/dual_funding_tests.rs @@ -19,11 +19,11 @@ use { crate::ln::channel::PendingV2Channel, crate::ln::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBasepoint}, crate::ln::functional_test_utils::*, + crate::ln::funding::FundingTxInput, 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 { sequence, 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: 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 b14b2287f9b..b72395d96c3 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -26,6 +26,7 @@ use crate::ln::channelmanager::{ AChannelManager, ChainParameters, ChannelManager, ChannelManagerReadArgs, PaymentId, RAACommitmentOrder, RecipientOnionFields, MIN_CLTV_EXPIRY_DELTA, }; +use crate::ln::funding::FundingTxInput; use crate::ln::msgs; use crate::ln::msgs::{ BaseMessageHandler, ChannelMessageHandler, MessageSendEvent, RoutingMessageHandler, @@ -62,12 +63,10 @@ use bitcoin::script::ScriptBuf; use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::transaction::{self, Version as TxVersion}; use bitcoin::transaction::{Sequence, Transaction, TxIn, TxOut}; -use bitcoin::witness::Witness; -use bitcoin::{WPubkeyHash, Weight}; +use bitcoin::WPubkeyHash; use crate::io; use crate::prelude::*; -use crate::sign::P2WPKH_WITNESS_WEIGHT; use crate::sync::{Arc, LockTestExt, Mutex, RwLock}; use alloc::rc::Rc; use core::cell::RefCell; @@ -1440,7 +1439,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, @@ -1460,22 +1459,12 @@ pub fn create_dual_funding_utxos_with_prev_txs( .collect(), }; - let mut inputs = vec![]; - for i in 0..utxo_values_in_satoshis.len() { - inputs.push(( - 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), - )); - } - - inputs + tx.output + .iter() + .enumerate() + .map(|(index, _)| index as u32) + .map(|vout| FundingTxInput::new_p2wpkh(tx.clone(), vout, Sequence::ZERO).unwrap()) + .collect() } pub fn sign_funding_transaction<'a, 'b, 'c>( diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs new file mode 100644 index 00000000000..fb05ab89bc3 --- /dev/null +++ b/lightning/src/ln/funding.rs @@ -0,0 +1,179 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Types pertaining to funding channels. + +#[cfg(splicing)] +use bitcoin::{Amount, ScriptBuf, SignedAmount, TxOut}; +use bitcoin::{Script, Sequence, Transaction, Weight}; + +use crate::events::bump_transaction::{Utxo, EMPTY_SCRIPT_SIG_WEIGHT}; +use crate::sign::{P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT}; + +/// 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 included in the splice's funding transaction 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 { + /// The unspent [`TxOut`] that the input spends. + /// + /// [`TxOut`]: bitcoin::TxOut + pub(super) utxo: Utxo, + + /// The sequence number to use in the [`TxIn`]. + /// + /// [`TxIn`]: bitcoin::TxIn + pub(super) sequence: Sequence, + + /// The transaction containing the unspent [`TxOut`] referenced by [`utxo`]. + /// + /// [`TxOut`]: bitcoin::TxOut + /// [`utxo`]: Self::utxo + pub(super) prevtx: Transaction, +} + +impl FundingTxInput { + fn new bool>( + prevtx: Transaction, vout: u32, sequence: Sequence, witness_weight: Weight, + script_filter: F, + ) -> Result { + Ok(FundingTxInput { + utxo: Utxo { + outpoint: bitcoin::OutPoint { txid: prevtx.compute_txid(), vout }, + output: prevtx + .output + .get(vout as usize) + .filter(|output| script_filter(&output.script_pubkey)) + .ok_or(())? + .clone(), + satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + witness_weight.to_wu(), + }, + sequence, + prevtx, + }) + } + + /// Creates an input spending a P2WPKH output from the given `prevtx` at index `vout` using the + /// provided `sequence` number. + /// + /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + pub fn new_p2wpkh(prevtx: Transaction, vout: u32, sequence: Sequence) -> Result { + let witness_weight = Weight::from_wu(P2WPKH_WITNESS_WEIGHT); + FundingTxInput::new(prevtx, vout, sequence, witness_weight, Script::is_p2wpkh) + } + + /// Creates an input spending a P2WSH output from the given `prevtx` at index `vout` using the + /// provided `sequence` number. + /// + /// Requires passing the weight of witness needed to satisfy the output's script. + /// + /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + pub fn new_p2wsh( + prevtx: Transaction, vout: u32, sequence: Sequence, witness_weight: Weight, + ) -> Result { + FundingTxInput::new(prevtx, vout, sequence, witness_weight, Script::is_p2wsh) + } + + /// Creates an input spending a P2TR output from the given `prevtx` at index `vout` using the + /// provided `sequence` number. + /// + /// This is meant for inputs spending a taproot output using the key path. See + /// [`new_p2tr_script_spend`] for when spending using a script path. + /// + /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + /// + /// [`new_p2tr_script_spend`]: Self::new_p2tr_script_spend + pub fn new_p2tr_key_spend( + prevtx: Transaction, vout: u32, sequence: Sequence, + ) -> Result { + let witness_weight = Weight::from_wu(P2TR_KEY_PATH_WITNESS_WEIGHT); + FundingTxInput::new(prevtx, vout, sequence, witness_weight, Script::is_p2tr) + } + + /// Creates an input spending a P2TR output from the given `prevtx` at index `vout` using the + /// provided `sequence` number. + /// + /// Requires passing the weight of witness needed to satisfy a script path of the taproot + /// output. See [`new_p2tr_key_spend`] for when spending using the key path. + /// + /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + /// + /// [`new_p2tr_key_spend`]: Self::new_p2tr_key_spend + pub fn new_p2tr_script_spend( + prevtx: Transaction, vout: u32, sequence: Sequence, witness_weight: Weight, + ) -> Result { + FundingTxInput::new(prevtx, vout, sequence, witness_weight, Script::is_p2tr) + } +} diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index b3b3d6df112..8d2406ef33f 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::key::Secp256k1; @@ -28,11 +28,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::funding::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; @@ -869,10 +869,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: @@ -1053,14 +1052,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), @@ -1441,7 +1435,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, } @@ -1843,7 +1837,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, @@ -1885,7 +1879,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); } } @@ -1894,7 +1888,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) @@ -2075,28 +2069,21 @@ 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(); - if txin.previous_output.txid != txid { - return Err(AbortReason::PrevTxOutInvalid); - } - let output = tx - .as_transaction() - .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(); + for FundingTxInput { utxo, .. } in context.our_funding_inputs.iter() { + total_input_satoshis = total_input_satoshis.saturating_add(utxo.output.value.to_sat()); + + let weight = BASE_INPUT_WEIGHT + utxo.satisfaction_weight; 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| { @@ -2136,6 +2123,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::funding::FundingTxInput; use crate::ln::interactivetxs::{ calculate_change_output_value, generate_holder_serial_id, AbortReason, HandleTxCompleteValue, InteractiveTxConstructor, InteractiveTxConstructorArgs, @@ -2145,7 +2133,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; @@ -2156,7 +2143,8 @@ mod tests { use bitcoin::transaction::Version; use bitcoin::{opcodes, WScriptHash, Weight, XOnlyPublicKey}; use bitcoin::{ - OutPoint, PubkeyHash, ScriptBuf, Sequence, Transaction, TxIn, TxOut, WPubkeyHash, Witness, + OutPoint, PubkeyHash, ScriptBuf, Sequence, SignedAmount, Transaction, TxIn, TxOut, + WPubkeyHash, }; use core::ops::Deref; @@ -2210,12 +2198,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), @@ -2481,7 +2469,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 @@ -2494,7 +2482,7 @@ mod tests { sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, witness: Default::default(), }; - (txin, TransactionU16LenLimited::new(tx.clone()).unwrap()) + (txin, tx.clone()) }) .collect() } @@ -2542,12 +2530,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); @@ -2560,7 +2548,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() @@ -2571,7 +2559,7 @@ mod tests { sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, witness: Default::default(), }; - (input, TransactionU16LenLimited::new(tx.clone()).unwrap()) + (input, tx.clone()) }) .collect(); @@ -2782,10 +2770,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 { @@ -2801,7 +2788,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() }; @@ -2819,7 +2806,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() }; @@ -2836,7 +2823,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() }; @@ -3150,28 +3137,28 @@ mod tests { #[test] fn test_calculate_change_output_value_open() { let input_prevouts = [ - TxOut { value: Amount::from_sat(70_000), script_pubkey: ScriptBuf::new() }, - TxOut { value: Amount::from_sat(60_000), script_pubkey: ScriptBuf::new() }, + TxOut { + value: Amount::from_sat(70_000), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + }, + TxOut { + value: Amount::from_sat(60_000), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + }, ]; 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 txin = TxIn { - previous_output: OutPoint { txid, vout: 0 }, - script_sig: ScriptBuf::new(), - sequence: Sequence::ZERO, - witness: Witness::new(), - }; - (txin, TransactionU16LenLimited::new(tx).unwrap()) + + FundingTxInput::new_p2wpkh(prevtx, 0, Sequence::ZERO).unwrap() }) - .collect::>(); + .collect(); let our_contributed = 110_000; let txout = TxOut { value: Amount::from_sat(10_000), script_pubkey: ScriptBuf::new() }; let outputs = vec![txout]; @@ -3186,68 +3173,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/mod.rs b/lightning/src/ln/mod.rs index a513582cb64..1169f6efe89 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -18,6 +18,7 @@ pub mod channel_keys; pub mod channel_state; pub mod channelmanager; mod features; +pub mod funding; pub mod inbound_payment; pub mod msgs; pub mod onion_payment; 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 51f9f2e387e..2445a2d5fc2 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -8,9 +8,12 @@ // licenses. use crate::ln::functional_test_utils::*; +use crate::ln::funding::SpliceContribution; 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(); @@ -295,19 +303,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)