diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index e76cc0deb1d..02e1f490647 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -10,7 +10,7 @@ use bitcoin::amount::Amount; use bitcoin::constants::ChainHash; use bitcoin::script::{Script, ScriptBuf, Builder, WScriptHash}; -use bitcoin::transaction::{Transaction, TxIn}; +use bitcoin::transaction::{Transaction, TxIn, TxOut}; use bitcoin::sighash; use bitcoin::sighash::EcdsaSighashType; use bitcoin::consensus::encode; @@ -31,9 +31,9 @@ use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash}; use crate::types::features::{ChannelTypeFeatures, InitFeatures}; use crate::ln::interactivetxs::{ - get_output_weight, HandleTxCompleteValue, HandleTxCompleteResult, InteractiveTxConstructor, - InteractiveTxConstructorArgs, InteractiveTxSigningSession, InteractiveTxMessageSendResult, - TX_COMMON_FIELDS_WEIGHT, + get_output_weight, calculate_change_output_value, HandleTxCompleteValue, HandleTxCompleteResult, InteractiveTxConstructor, + InteractiveTxConstructorArgs, InteractiveTxMessageSend, InteractiveTxSigningSession, InteractiveTxMessageSendResult, + OutputOwned, SharedOwnedOutput, TX_COMMON_FIELDS_WEIGHT, }; use crate::ln::msgs; use crate::ln::msgs::{ClosingSigned, ClosingSignedFeeRange, DecodeError}; @@ -2032,6 +2032,96 @@ impl InitialRemoteCommitmentReceiver for FundedChannel where } impl PendingV2Channel where SP::Target: SignerProvider { + #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled + fn begin_interactive_funding_tx_construction( + &mut self, signer_provider: &SP, entropy_source: &ES, holder_node_id: PublicKey, + prev_funding_input: Option<(TxIn, TransactionU16LenLimited)>, + ) -> Result, APIError> + where ES::Target: EntropySource + { + let mut funding_inputs = Vec::new(); + mem::swap(&mut self.dual_funding_context.our_funding_inputs, &mut funding_inputs); + + if let Some(prev_funding_input) = prev_funding_input { + funding_inputs.push(prev_funding_input); + } + + let funding_inputs_prev_outputs = DualFundingChannelContext::txouts_from_input_prev_txs(&funding_inputs) + .map_err(|err| APIError::APIMisuseError { err: err.to_string() })?; + + let total_input_satoshis: u64 = funding_inputs_prev_outputs.iter().map(|txout| txout.value.to_sat()).sum(); + if total_input_satoshis < self.dual_funding_context.our_funding_satoshis { + return Err(APIError::APIMisuseError { + err: format!("Total value of funding inputs must be at least funding amount. It was {} sats", + total_input_satoshis) }); + } + + // Add output for funding tx + let mut funding_outputs = Vec::new(); + let funding_output_value_satoshis = self.context.get_value_satoshis(); + let funding_output_script_pubkey = self.context.get_funding_redeemscript().to_p2wsh(); + let expected_remote_shared_funding_output = if self.context.is_outbound() { + let tx_out = TxOut { + value: Amount::from_sat(funding_output_value_satoshis), + script_pubkey: funding_output_script_pubkey, + }; + funding_outputs.push( + if self.dual_funding_context.their_funding_satoshis.unwrap_or(0) == 0 { + OutputOwned::SharedControlFullyOwned(tx_out) + } else { + OutputOwned::Shared(SharedOwnedOutput::new( + tx_out, self.dual_funding_context.our_funding_satoshis + )) + } + ); + None + } else { + Some((funding_output_script_pubkey, funding_output_value_satoshis)) + }; + + // Optionally add change output + if let Some(change_value) = calculate_change_output_value( + self.context.is_outbound(), self.dual_funding_context.our_funding_satoshis, + &funding_inputs_prev_outputs, &funding_outputs, + self.dual_funding_context.funding_feerate_sat_per_1000_weight, + self.context.holder_dust_limit_satoshis, + ) { + let change_script = signer_provider.get_destination_script(self.context.channel_keys_id).map_err( + |err| APIError::APIMisuseError { + err: format!("Failed to get change script as new destination script, {:?}", err), + })?; + let mut change_output = TxOut { + value: Amount::from_sat(change_value), + script_pubkey: change_script, + }; + let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu(); + let change_output_fee = fee_for_weight(self.dual_funding_context.funding_feerate_sat_per_1000_weight, change_output_weight); + change_output.value = Amount::from_sat(change_value.saturating_sub(change_output_fee)); + // Note: dust check not done here, should be handled before + funding_outputs.push(OutputOwned::Single(change_output)); + } + + let constructor_args = InteractiveTxConstructorArgs { + entropy_source, + holder_node_id, + counterparty_node_id: self.context.counterparty_node_id, + channel_id: self.context.channel_id(), + feerate_sat_per_kw: self.dual_funding_context.funding_feerate_sat_per_1000_weight, + is_initiator: self.context.is_outbound(), + funding_tx_locktime: self.dual_funding_context.funding_tx_locktime, + inputs_to_contribute: funding_inputs, + outputs_to_contribute: funding_outputs, + expected_remote_shared_funding_output, + }; + let mut tx_constructor = InteractiveTxConstructor::new(constructor_args) + .map_err(|_| APIError::APIMisuseError { err: "Incorrect shared output provided".into() })?; + let msg = tx_constructor.take_initiator_first_message(); + + self.interactive_tx_constructor = Some(tx_constructor); + + Ok(msg) + } + pub fn tx_add_input(&mut self, msg: &msgs::TxAddInput) -> InteractiveTxMessageSendResult { InteractiveTxMessageSendResult(match &mut self.interactive_tx_constructor { Some(ref mut tx_constructor) => tx_constructor.handle_tx_add_input(msg).map_err( @@ -4470,7 +4560,6 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis)) } -#[allow(dead_code)] // TODO(dual_funding): Remove once V2 channels is enabled. pub(super) fn calculate_our_funding_satoshis( is_initiator: bool, funding_inputs: &[(TxIn, TransactionU16LenLimited)], total_witness_weight: Weight, funding_feerate_sat_per_1000_weight: u32, @@ -4516,6 +4605,9 @@ pub(super) fn calculate_our_funding_satoshis( pub(super) struct DualFundingChannelContext { /// The amount in satoshis we will be contributing to the channel. pub our_funding_satoshis: u64, + /// The amount in satoshis our counterparty will be contributing to the channel. + #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled. + pub their_funding_satoshis: Option, /// The funding transaction locktime suggested by the initiator. If set by us, it is always set /// to the current block height to align incentives against fee-sniping. pub funding_tx_locktime: LockTime, @@ -4527,10 +4619,39 @@ pub(super) struct DualFundingChannelContext { /// Note that the `our_funding_satoshis` field is equal to the total value of `our_funding_inputs` /// minus any fees paid for our contributed weight. This means that change will never be generated /// and the maximum value possible will go towards funding the channel. + /// + /// Note that this field may be emptied once the interactive negotiation has been started. #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled. pub our_funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, } +impl DualFundingChannelContext { + /// Obtain prev outputs for each supplied input and matching transaction. + /// Can error when there a prev tx does not have an output for the specified vout number. + /// Also checks for matching of transaction IDs. + fn txouts_from_input_prev_txs(inputs: &Vec<(TxIn, TransactionU16LenLimited)>) -> Result, ChannelError> { + let mut prev_outputs: Vec<&TxOut> = Vec::with_capacity(inputs.len()); + // Check that vouts exist for each TxIn in provided transactions. + for (idx, (txin, tx)) in inputs.iter().enumerate() { + let txid = tx.as_transaction().compute_txid(); + if txin.previous_output.txid != txid { + return Err(ChannelError::Warn( + format!("Transaction input txid mismatch, {} vs. {}, at index {}", txin.previous_output.txid, txid, idx) + )); + } + if let Some(output) = tx.as_transaction().output.get(txin.previous_output.vout as usize) { + prev_outputs.push(output); + } else { + return Err(ChannelError::Warn( + format!("Transaction with txid {} does not have an output with vout of {} corresponding to TxIn, at index {}", + txid, txin.previous_output.vout, idx) + )); + } + } + Ok(prev_outputs) + } +} + // Holder designates channel data owned for the benefit of the user client. // Counterparty designates channel data owned by the another channel participant entity. pub(super) struct FundedChannel where SP::Target: SignerProvider { @@ -9164,15 +9285,17 @@ impl PendingV2Channel where SP::Target: SignerProvider { unfunded_channel_age_ticks: 0, holder_commitment_point: HolderCommitmentPoint::new(&context.holder_signer, &context.secp_ctx), }; + let dual_funding_context = DualFundingChannelContext { + our_funding_satoshis: funding_satoshis, + their_funding_satoshis: None, + funding_tx_locktime, + funding_feerate_sat_per_1000_weight, + our_funding_inputs: funding_inputs, + }; let chan = Self { context, unfunded_context, - dual_funding_context: DualFundingChannelContext { - our_funding_satoshis: funding_satoshis, - funding_tx_locktime, - funding_feerate_sat_per_1000_weight, - our_funding_inputs: funding_inputs, - }, + dual_funding_context, interactive_tx_constructor: None, }; Ok(chan) @@ -9319,6 +9442,7 @@ impl PendingV2Channel where SP::Target: SignerProvider { let dual_funding_context = DualFundingChannelContext { our_funding_satoshis: funding_satoshis, + their_funding_satoshis: Some(msg.common_fields.funding_satoshis), funding_tx_locktime: LockTime::from_consensus(msg.locktime), funding_feerate_sat_per_1000_weight: msg.funding_feerate_sat_per_1000_weight, our_funding_inputs: funding_inputs.clone(), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index eb5bd89575a..05c357deb3e 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -49,7 +49,7 @@ use crate::ln::inbound_payment; use crate::ln::types::ChannelId; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::ln::channel::{self, Channel, ChannelError, ChannelUpdateStatus, FundedChannel, ShutdownResult, UpdateFulfillCommitFetch, OutboundV1Channel, ReconnectionMsg, InboundV1Channel, WithChannelContext}; -#[cfg(any(dual_funding, splicing))] +#[cfg(dual_funding)] use crate::ln::channel::PendingV2Channel; use crate::ln::channel_state::ChannelDetails; use crate::types::features::{Bolt12InvoiceFeatures, ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures}; diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 2b72133ec09..396afd407d7 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -1151,13 +1151,13 @@ pub(crate) enum InteractiveTxInput { } #[derive(Clone, Debug, Eq, PartialEq)] -pub struct SharedOwnedOutput { +pub(super) struct SharedOwnedOutput { tx_out: TxOut, local_owned: u64, } impl SharedOwnedOutput { - fn new(tx_out: TxOut, local_owned: u64) -> SharedOwnedOutput { + pub fn new(tx_out: TxOut, local_owned: u64) -> SharedOwnedOutput { debug_assert!( local_owned <= tx_out.value.to_sat(), "SharedOwnedOutput: Inconsistent local_owned value {}, larger than output value {}", @@ -1176,7 +1176,7 @@ impl SharedOwnedOutput { /// its control -- exclusive by the adder or shared --, and /// its ownership -- value fully owned by the adder or jointly #[derive(Clone, Debug, Eq, PartialEq)] -pub enum OutputOwned { +pub(super) enum OutputOwned { /// Belongs to a single party -- controlled exclusively and fully belonging to a single party Single(TxOut), /// Output with shared control, but fully belonging to local node @@ -1186,7 +1186,7 @@ pub enum OutputOwned { } impl OutputOwned { - fn tx_out(&self) -> &TxOut { + pub fn tx_out(&self) -> &TxOut { match self { OutputOwned::Single(tx_out) | OutputOwned::SharedControlFullyOwned(tx_out) => tx_out, OutputOwned::Shared(output) => &output.tx_out, @@ -1662,14 +1662,58 @@ impl InteractiveTxConstructor { } } +/// Determine whether a change output should be added or not, and if so, of what size, +/// considering our given inputs, outputs, and intended contribution. +/// Computes and takes into account fees. +/// Return value is the value computed for the change output (in satoshis), +/// or None if a change is not needed/possible. +#[allow(dead_code)] // TODO(dual_funding): Remove once begin_interactive_funding_tx_construction() is used +pub(super) fn calculate_change_output_value( + is_initiator: bool, our_contribution: u64, funding_inputs_prev_outputs: &Vec<&TxOut>, + funding_outputs: &Vec, funding_feerate_sat_per_1000_weight: u32, + holder_dust_limit_satoshis: u64, +) -> Option { + let our_funding_inputs_weight = + funding_inputs_prev_outputs.iter().fold(0u64, |weight, prev_output| { + weight.saturating_add(estimate_input_weight(prev_output).to_wu()) + }); + let our_funding_outputs_weight = funding_outputs.iter().fold(0u64, |weight, out| { + weight.saturating_add(get_output_weight(&out.tx_out().script_pubkey).to_wu()) + }); + let our_contributed_weight = + our_funding_outputs_weight.saturating_add(our_funding_inputs_weight); + let mut fees_sats = fee_for_weight(funding_feerate_sat_per_1000_weight, our_contributed_weight); + + // If we are the initiator, we must pay for weight of all common fields in the funding transaction. + if is_initiator { + let common_fees = + fee_for_weight(funding_feerate_sat_per_1000_weight, TX_COMMON_FIELDS_WEIGHT); + fees_sats = fees_sats.saturating_add(common_fees); + } + + let total_input_satoshis: u64 = + funding_inputs_prev_outputs.iter().map(|out| out.value.to_sat()).sum(); + + // Note: in case of additional outputs, they will have to be subtracted here + let remaining_value = + total_input_satoshis.saturating_sub(our_contribution).saturating_sub(fees_sats); + + if remaining_value <= holder_dust_limit_satoshis { + None + } else { + Some(remaining_value) + } +} + #[cfg(test)] mod tests { use crate::chain::chaininterface::{fee_for_weight, FEERATE_FLOOR_SATS_PER_KW}; use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; use crate::ln::interactivetxs::{ - generate_holder_serial_id, AbortReason, HandleTxCompleteValue, InteractiveTxConstructor, - InteractiveTxConstructorArgs, InteractiveTxMessageSend, MAX_INPUTS_OUTPUTS_COUNT, - MAX_RECEIVED_TX_ADD_INPUT_COUNT, MAX_RECEIVED_TX_ADD_OUTPUT_COUNT, + calculate_change_output_value, generate_holder_serial_id, AbortReason, + HandleTxCompleteValue, InteractiveTxConstructor, InteractiveTxConstructorArgs, + InteractiveTxMessageSend, MAX_INPUTS_OUTPUTS_COUNT, MAX_RECEIVED_TX_ADD_INPUT_COUNT, + MAX_RECEIVED_TX_ADD_OUTPUT_COUNT, }; use crate::ln::types::ChannelId; use crate::sign::EntropySource; @@ -2594,4 +2638,148 @@ mod tests { assert_eq!(generate_holder_serial_id(&&entropy_source, true) % 2, 0); assert_eq!(generate_holder_serial_id(&&entropy_source, false) % 2, 1) } + + #[test] + fn test_calculate_change_output_value_open() { + let input_prevouts_owned = vec![ + TxOut { value: Amount::from_sat(70_000), script_pubkey: ScriptBuf::new() }, + TxOut { value: Amount::from_sat(60_000), script_pubkey: ScriptBuf::new() }, + ]; + let input_prevouts: Vec<&TxOut> = input_prevouts_owned.iter().collect(); + let our_contributed = 110_000; + let txout = TxOut { value: Amount::from_sat(128_000), script_pubkey: ScriptBuf::new() }; + let outputs = vec![OutputOwned::SharedControlFullyOwned(txout)]; + let funding_feerate_sat_per_1000_weight = 3000; + + let total_inputs: u64 = input_prevouts.iter().map(|o| o.value.to_sat()).sum(); + let gross_change = total_inputs - our_contributed; + let fees = 1746; + let common_fees = 126; + { + // There is leftover for change + let res = calculate_change_output_value( + true, + our_contributed, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 300, + ); + assert_eq!(res.unwrap(), gross_change - fees - common_fees); + } + { + // There is leftover for change, without common fees + let res = calculate_change_output_value( + false, + our_contributed, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 300, + ); + assert_eq!(res.unwrap(), gross_change - fees); + } + { + // Larger fee, smaller change + let res = calculate_change_output_value( + true, + our_contributed, + &input_prevouts, + &outputs, + 9000, + 300, + ); + assert_eq!(res.unwrap(), 14384); + } + { + // Insufficient inputs, no leftover + let res = calculate_change_output_value( + false, + 130_000, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 300, + ); + assert!(res.is_none()); + } + { + // Very small leftover + let res = calculate_change_output_value( + false, + 128_100, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 300, + ); + assert!(res.is_none()); + } + { + // Small leftover, but not dust + let res = calculate_change_output_value( + false, + 128_100, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 100, + ); + assert_eq!(res.unwrap(), 154); + } + } + + #[test] + fn test_calculate_change_output_value_splice() { + let input_prevouts_owned = vec![ + TxOut { value: Amount::from_sat(70_000), script_pubkey: ScriptBuf::new() }, + TxOut { value: Amount::from_sat(60_000), script_pubkey: ScriptBuf::new() }, + ]; + let input_prevouts: Vec<&TxOut> = input_prevouts_owned.iter().collect(); + let our_contributed = 110_000; + let txout = TxOut { value: Amount::from_sat(148_000), script_pubkey: ScriptBuf::new() }; + let outputs = vec![OutputOwned::Shared(SharedOwnedOutput::new(txout, our_contributed))]; + let funding_feerate_sat_per_1000_weight = 3000; + + let total_inputs: u64 = input_prevouts.iter().map(|o| o.value.to_sat()).sum(); + let gross_change = total_inputs - our_contributed; + let fees = 1746; + let common_fees = 126; + { + // There is leftover for change + let res = calculate_change_output_value( + true, + our_contributed, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 300, + ); + assert_eq!(res.unwrap(), gross_change - fees - common_fees); + } + { + // Very small leftover + let res = calculate_change_output_value( + false, + 128_100, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 300, + ); + assert!(res.is_none()); + } + { + // Small leftover, but not dust + let res = calculate_change_output_value( + false, + 128_100, + &input_prevouts, + &outputs, + funding_feerate_sat_per_1000_weight, + 100, + ); + assert_eq!(res.unwrap(), 154); + } + } }