From f3123573f09833e9a67c56f8a1dbb815c0b556b8 Mon Sep 17 00:00:00 2001 From: Duncan Dean Date: Fri, 25 Apr 2025 11:52:06 +0200 Subject: [PATCH 1/4] Allow passing funding inputs when manually accepting a dual-funded channel We introduce a `ChannelManager::accept_inbound_channel_with_contribution` method allowing contributing to the overall channel capacity of an inbound dual-funded channel by contributing inputs. --- lightning/src/ln/channel.rs | 77 +++++++++++++++++------------- lightning/src/ln/channelmanager.rs | 72 ++++++++++++++++++++++++---- lightning/src/ln/interactivetxs.rs | 11 +++++ 3 files changed, 118 insertions(+), 42 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index d9c3feab888..3d1c2ee51d4 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -13946,7 +13946,7 @@ where pub funding: FundingScope, pub context: ChannelContext, pub unfunded_context: UnfundedChannelContext, - pub funding_negotiation_context: FundingNegotiationContext, + pub funding_negotiation_context: Option, /// The current interactive transaction construction session under negotiation. pub interactive_tx_constructor: Option, } @@ -14021,7 +14021,7 @@ where funding, context, unfunded_context, - funding_negotiation_context, + funding_negotiation_context: Some(funding_negotiation_context), interactive_tx_constructor: None, }; Ok(chan) @@ -14096,29 +14096,28 @@ where }, funding_feerate_sat_per_1000_weight: self.context.feerate_per_kw, second_per_commitment_point, - locktime: self.funding_negotiation_context.funding_tx_locktime.to_consensus_u32(), + locktime: self.funding_tx_locktime().to_consensus_u32(), require_confirmed_inputs: None, } } /// Creates a new dual-funded channel from a remote side's request for one. /// Assumes chain_hash has already been checked and corresponds with what we expect! - /// TODO(dual_funding): Allow contributions, pass intended amount and inputs #[allow(dead_code)] // TODO(dual_funding): Remove once V2 channels is enabled. #[rustfmt::skip] pub fn new_inbound( fee_estimator: &LowerBoundedFeeEstimator, entropy_source: &ES, signer_provider: &SP, holder_node_id: PublicKey, counterparty_node_id: PublicKey, our_supported_features: &ChannelTypeFeatures, - their_features: &InitFeatures, msg: &msgs::OpenChannelV2, - user_id: u128, config: &UserConfig, current_chain_height: u32, logger: &L, + their_features: &InitFeatures, msg: &msgs::OpenChannelV2, user_id: u128, config: &UserConfig, + current_chain_height: u32, logger: &L, our_funding_contribution: Amount, + our_funding_inputs: Vec, ) -> Result where ES::Target: EntropySource, F::Target: FeeEstimator, L::Target: Logger, { - // TODO(dual_funding): Take these as input once supported - let (our_funding_contribution, our_funding_contribution_sats) = (SignedAmount::ZERO, 0u64); - let our_funding_inputs = Vec::new(); + debug_assert!(our_funding_contribution <= Amount::MAX_MONEY); + let our_funding_contribution_sats = our_funding_contribution.to_sat(); let channel_value_satoshis = our_funding_contribution_sats.saturating_add(msg.common_fields.funding_satoshis); @@ -14163,7 +14162,9 @@ where let funding_negotiation_context = FundingNegotiationContext { is_initiator: false, - our_funding_contribution, + our_funding_contribution: our_funding_contribution + .to_signed() + .expect("our_funding_contribution should not be greater than Amount::MAX_MONEY"), 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, @@ -14171,29 +14172,20 @@ where 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 interactive_tx_constructor = Some(InteractiveTxConstructor::new( - InteractiveTxConstructorArgs { + let mut interactive_tx_constructor = funding_negotiation_context + .into_interactive_tx_constructor( + &context, + &funding, + signer_provider, entropy_source, holder_node_id, - counterparty_node_id, - channel_id: context.channel_id, - feerate_sat_per_kw: funding_negotiation_context.funding_feerate_sat_per_1000_weight, - funding_tx_locktime: funding_negotiation_context.funding_tx_locktime, - is_initiator: false, - inputs_to_contribute: our_funding_inputs, - shared_funding_input: None, - 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.reason.to_string() }; - ChannelError::Close((err.reason.to_string(), reason)) - })?); + ) + .map_err(|err| { + let reason = ClosureReason::ProcessingError { err: err.reason.to_string() }; + ChannelError::Close((err.reason.to_string(), reason)) + })?; + debug_assert!(interactive_tx_constructor.take_initiator_first_message().is_none()); let unfunded_context = UnfundedChannelContext { unfunded_channel_age_ticks: 0, @@ -14202,8 +14194,8 @@ where Ok(Self { funding, context, - funding_negotiation_context, - interactive_tx_constructor, + funding_negotiation_context: None, + interactive_tx_constructor: Some(interactive_tx_constructor), unfunded_context, }) } @@ -14267,8 +14259,7 @@ where }), channel_type: Some(self.funding.get_channel_type().clone()), }, - funding_satoshis: self.funding_negotiation_context.our_funding_contribution.to_sat() - as u64, + funding_satoshis: self.our_funding_contribution().to_sat(), second_per_commitment_point, require_confirmed_inputs: None, } @@ -14283,6 +14274,24 @@ where pub fn get_accept_channel_v2_message(&self) -> msgs::AcceptChannelV2 { self.generate_accept_channel_v2_message() } + + pub fn our_funding_contribution(&self) -> Amount { + Amount::from_sat(self.funding.value_to_self_msat / 1000) + } + + pub fn funding_tx_locktime(&self) -> LockTime { + self.funding_negotiation_context + .as_ref() + .map(|context| context.funding_tx_locktime) + .or_else(|| { + self.interactive_tx_constructor + .as_ref() + .map(|constructor| constructor.funding_tx_locktime()) + }) + .expect( + "either funding_negotiation_context or interactive_tx_constructor should be set", + ) + } } // Unfunded channel utilities diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 8abb2378627..a2a6036600e 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -30,7 +30,7 @@ use bitcoin::hashes::{Hash, HashEngine, HmacEngine}; use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::{PublicKey, SecretKey}; -use bitcoin::{secp256k1, Sequence, SignedAmount}; +use bitcoin::{secp256k1, Amount, Sequence}; use crate::blinded_path::message::{ AsyncPaymentsContext, BlindedMessagePath, MessageForwardNode, OffersContext, @@ -65,7 +65,7 @@ use crate::ln::channel::{ WithChannelContext, }; use crate::ln::channel_state::ChannelDetails; -use crate::ln::funding::SpliceContribution; +use crate::ln::funding::{FundingTxInput, SpliceContribution}; use crate::ln::inbound_payment; use crate::ln::interactivetxs::InteractiveTxMessageSend; use crate::ln::msgs; @@ -9814,6 +9814,8 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ false, user_channel_id, config_overrides, + Amount::ZERO, + vec![], ) } @@ -9845,15 +9847,69 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ true, user_channel_id, config_overrides, + Amount::ZERO, + vec![], + ) + } + + /// Accepts a request to open a dual-funded channel with a contribution provided after an + /// [`Event::OpenChannelRequest`]. + /// + /// The [`Event::OpenChannelRequest::channel_negotiation_type`] field will indicate the open channel + /// request is for a dual-funded channel when the variant is [`InboundChannelFunds::DualFunded`]. + /// + /// The `temporary_channel_id` parameter indicates which inbound channel should be accepted, + /// and the `counterparty_node_id` parameter is the id of the peer that has requested to open + /// the channel. + /// + /// The `user_channel_id` parameter will be provided back in + /// [`Event::ChannelClosed::user_channel_id`] to allow tracking of which events correspond + /// with which `accept_inbound_channel_*` call. + /// + /// The `funding_inputs` parameter provides which UTXOs to use for `our_funding_contribution` + /// along with the corresponding satisfaction weight. They must be able to cover any fees needed + /// to pay for the contributed weight to the funding transaction. This includes the witnesses + /// provided through calling [`ChannelManager::funding_transaction_signed`] after receiving + /// [`Event::FundingTransactionReadyForSigning`]. + /// + /// Note that this method will return an error and reject the channel if it requires support for + /// zero confirmations. + // TODO(dual_funding): Discussion on complications with 0conf dual-funded channels where "locking" + // of UTXOs used for funding would be required and other issues. + // See https://diyhpl.us/~bryan/irc/bitcoin/bitcoin-dev/linuxfoundation-pipermail/lightning-dev/2023-May/003922.txt + /// + /// [`Event::OpenChannelRequest`]: events::Event::OpenChannelRequest + /// [`Event::OpenChannelRequest::channel_negotiation_type`]: events::Event::OpenChannelRequest::channel_negotiation_type + /// [`Event::ChannelClosed::user_channel_id`]: events::Event::ChannelClosed::user_channel_id + /// [`Event::FundingTransactionReadyForSigning`]: events::Event::FundingTransactionReadyForSigning + /// [`ChannelManager::funding_transaction_signed`]: ChannelManager::funding_transaction_signed + pub fn accept_inbound_channel_with_contribution( + &self, temporary_channel_id: &ChannelId, counterparty_node_id: &PublicKey, + user_channel_id: u128, config_overrides: Option, + our_funding_contribution: Amount, funding_inputs: Vec, + ) -> Result<(), APIError> { + self.do_accept_inbound_channel( + temporary_channel_id, + counterparty_node_id, + false, + user_channel_id, + config_overrides, + our_funding_contribution, + funding_inputs, ) } - /// TODO(dual_funding): Allow contributions, pass intended amount and inputs #[rustfmt::skip] fn do_accept_inbound_channel( - &self, temporary_channel_id: &ChannelId, counterparty_node_id: &PublicKey, accept_0conf: bool, - user_channel_id: u128, config_overrides: Option + &self, temporary_channel_id: &ChannelId, counterparty_node_id: &PublicKey, + accept_0conf: bool, user_channel_id: u128, config_overrides: Option, + our_funding_contribution: Amount, funding_inputs: Vec ) -> Result<(), APIError> { + if our_funding_contribution > Amount::MAX_MONEY { + return Err(APIError::APIMisuseError { + err: format!("the funding contribution must be smaller than the total bitcoin supply, it was {}", our_funding_contribution) + }); + } let mut config = self.config.read().unwrap().clone(); @@ -9911,7 +9967,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &self.channel_type_features(), &peer_state.latest_features, &open_channel_msg, user_channel_id, &config, best_block_height, - &self.logger, + &self.logger, our_funding_contribution, funding_inputs, ).map_err(|e| { let channel_id = open_channel_msg.common_fields.temporary_channel_id; MsgHandleErrInternal::from_chan_no_close(e, channel_id) @@ -10055,7 +10111,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 > SignedAmount::ZERO { + if unfunded_chan.our_funding_contribution() > Amount::ZERO { continue; } } @@ -10197,7 +10253,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &self.fee_estimator, &self.entropy_source, &self.signer_provider, self.get_our_node_id(), *counterparty_node_id, &self.channel_type_features(), &peer_state.latest_features, msg, user_channel_id, - &self.config.read().unwrap(), best_block_height, &self.logger, + &self.config.read().unwrap(), best_block_height, &self.logger, Amount::ZERO, vec![], ).map_err(|e| MsgHandleErrInternal::from_chan_no_close(e, msg.common_fields.temporary_channel_id))?; let message_send_event = MessageSendEvent::SendAcceptChannelV2 { node_id: *counterparty_node_id, diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 4340aad420a..b393c7642d8 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -1947,6 +1947,7 @@ pub(super) struct InteractiveTxConstructor { is_initiator: bool, initiator_first_message: Option, channel_id: ChannelId, + funding_tx_locktime: AbsoluteLockTime, inputs_to_contribute: Vec<(SerialId, InputOwned)>, outputs_to_contribute: Vec<(SerialId, OutputOwned)>, next_input_index: Option, @@ -2112,6 +2113,7 @@ impl InteractiveTxConstructor { is_initiator, initiator_first_message: None, channel_id, + funding_tx_locktime, inputs_to_contribute, outputs_to_contribute, next_input_index, @@ -2131,6 +2133,10 @@ impl InteractiveTxConstructor { Ok(constructor) } + pub(super) fn funding_tx_locktime(&self) -> AbsoluteLockTime { + self.funding_tx_locktime + } + fn into_negotiation_error(self, reason: AbortReason) -> NegotiationError { let (contributed_inputs, contributed_outputs) = self.into_contributed_inputs_and_outputs(); NegotiationError { reason, contributed_inputs, contributed_outputs } @@ -2320,6 +2326,11 @@ impl InteractiveTxConstructor { /// given inputs and outputs, and intended contribution. Takes into account the fees and the dust /// limit. /// +/// Note that since the type of change output cannot be determined at this point, this calculation +/// does not account for the weight contributed by the change output itself. The fees for the +/// weight of this change output should be subtracted from the result of this function call to get +/// the final amount for the change output (if above dust). +/// /// Three outcomes are possible: /// - Inputs are sufficient for intended contribution, fees, and a larger-than-dust change: /// `Ok(Some(change_amount))` From e2476524833fdc90aca1e5d06c3260ab4e0d48aa Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 20 Nov 2025 11:26:03 -0600 Subject: [PATCH 2/4] f - allow change script --- lightning/src/ln/channel.rs | 4 ++-- lightning/src/ln/channelmanager.rs | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 3d1c2ee51d4..40fc74e244b 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -14110,7 +14110,7 @@ where holder_node_id: PublicKey, counterparty_node_id: PublicKey, our_supported_features: &ChannelTypeFeatures, their_features: &InitFeatures, msg: &msgs::OpenChannelV2, user_id: u128, config: &UserConfig, current_chain_height: u32, logger: &L, our_funding_contribution: Amount, - our_funding_inputs: Vec, + our_funding_inputs: Vec, change_script: Option, ) -> Result where ES::Target: EntropySource, F::Target: FeeEstimator, @@ -14170,7 +14170,7 @@ where shared_funding_input: None, our_funding_inputs: our_funding_inputs.clone(), our_funding_outputs: Vec::new(), - change_script: None, + change_script, }; let mut interactive_tx_constructor = funding_negotiation_context diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index a2a6036600e..a2792873ddf 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -30,7 +30,7 @@ use bitcoin::hashes::{Hash, HashEngine, HmacEngine}; use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::{PublicKey, SecretKey}; -use bitcoin::{secp256k1, Amount, Sequence}; +use bitcoin::{secp256k1, Amount, ScriptBuf, Sequence}; use crate::blinded_path::message::{ AsyncPaymentsContext, BlindedMessagePath, MessageForwardNode, OffersContext, @@ -9816,6 +9816,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ config_overrides, Amount::ZERO, vec![], + None, ) } @@ -9849,6 +9850,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ config_overrides, Amount::ZERO, vec![], + None, ) } @@ -9887,6 +9889,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &self, temporary_channel_id: &ChannelId, counterparty_node_id: &PublicKey, user_channel_id: u128, config_overrides: Option, our_funding_contribution: Amount, funding_inputs: Vec, + change_script: Option, ) -> Result<(), APIError> { self.do_accept_inbound_channel( temporary_channel_id, @@ -9896,6 +9899,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ config_overrides, our_funding_contribution, funding_inputs, + change_script, ) } @@ -9903,7 +9907,8 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ fn do_accept_inbound_channel( &self, temporary_channel_id: &ChannelId, counterparty_node_id: &PublicKey, accept_0conf: bool, user_channel_id: u128, config_overrides: Option, - our_funding_contribution: Amount, funding_inputs: Vec + our_funding_contribution: Amount, funding_inputs: Vec, + change_script: Option, ) -> Result<(), APIError> { if our_funding_contribution > Amount::MAX_MONEY { return Err(APIError::APIMisuseError { @@ -9967,7 +9972,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &self.channel_type_features(), &peer_state.latest_features, &open_channel_msg, user_channel_id, &config, best_block_height, - &self.logger, our_funding_contribution, funding_inputs, + &self.logger, our_funding_contribution, funding_inputs, change_script, ).map_err(|e| { let channel_id = open_channel_msg.common_fields.temporary_channel_id; MsgHandleErrInternal::from_chan_no_close(e, channel_id) @@ -10253,7 +10258,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &self.fee_estimator, &self.entropy_source, &self.signer_provider, self.get_our_node_id(), *counterparty_node_id, &self.channel_type_features(), &peer_state.latest_features, msg, user_channel_id, - &self.config.read().unwrap(), best_block_height, &self.logger, Amount::ZERO, vec![], + &self.config.read().unwrap(), best_block_height, &self.logger, Amount::ZERO, vec![], None, ).map_err(|e| MsgHandleErrInternal::from_chan_no_close(e, msg.common_fields.temporary_channel_id))?; let message_send_event = MessageSendEvent::SendAcceptChannelV2 { node_id: *counterparty_node_id, From ac8803cf29973e476e25594d8b03ba74695a1680 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 20 Nov 2025 19:28:16 -0600 Subject: [PATCH 3/4] f - store funding_tx_locktime in PendingV2Channel --- lightning/src/ln/channel.rs | 25 ++++++++----------------- lightning/src/ln/interactivetxs.rs | 6 ------ 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 40fc74e244b..e73a4a3ff3f 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -13946,6 +13946,8 @@ where pub funding: FundingScope, pub context: ChannelContext, pub unfunded_context: UnfundedChannelContext, + funding_tx_locktime: LockTime, + #[allow(unused)] // TODO(dual_funding): Remove once initiating V2 channels is enabled. pub funding_negotiation_context: Option, /// The current interactive transaction construction session under negotiation. pub interactive_tx_constructor: Option, @@ -14021,6 +14023,7 @@ where funding, context, unfunded_context, + funding_tx_locktime, funding_negotiation_context: Some(funding_negotiation_context), interactive_tx_constructor: None, }; @@ -14096,7 +14099,7 @@ where }, funding_feerate_sat_per_1000_weight: self.context.feerate_per_kw, second_per_commitment_point, - locktime: self.funding_tx_locktime().to_consensus_u32(), + locktime: self.funding_tx_locktime.to_consensus_u32(), require_confirmed_inputs: None, } } @@ -14160,12 +14163,13 @@ where &funding.get_counterparty_pubkeys().revocation_basepoint); context.channel_id = channel_id; + let funding_tx_locktime = LockTime::from_consensus(msg.locktime); let funding_negotiation_context = FundingNegotiationContext { is_initiator: false, our_funding_contribution: our_funding_contribution .to_signed() .expect("our_funding_contribution should not be greater than Amount::MAX_MONEY"), - funding_tx_locktime: LockTime::from_consensus(msg.locktime), + funding_tx_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(), @@ -14194,9 +14198,10 @@ where Ok(Self { funding, context, + unfunded_context, + funding_tx_locktime, funding_negotiation_context: None, interactive_tx_constructor: Some(interactive_tx_constructor), - unfunded_context, }) } @@ -14278,20 +14283,6 @@ where pub fn our_funding_contribution(&self) -> Amount { Amount::from_sat(self.funding.value_to_self_msat / 1000) } - - pub fn funding_tx_locktime(&self) -> LockTime { - self.funding_negotiation_context - .as_ref() - .map(|context| context.funding_tx_locktime) - .or_else(|| { - self.interactive_tx_constructor - .as_ref() - .map(|constructor| constructor.funding_tx_locktime()) - }) - .expect( - "either funding_negotiation_context or interactive_tx_constructor should be set", - ) - } } // Unfunded channel utilities diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index b393c7642d8..457dc16b6ba 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -1947,7 +1947,6 @@ pub(super) struct InteractiveTxConstructor { is_initiator: bool, initiator_first_message: Option, channel_id: ChannelId, - funding_tx_locktime: AbsoluteLockTime, inputs_to_contribute: Vec<(SerialId, InputOwned)>, outputs_to_contribute: Vec<(SerialId, OutputOwned)>, next_input_index: Option, @@ -2113,7 +2112,6 @@ impl InteractiveTxConstructor { is_initiator, initiator_first_message: None, channel_id, - funding_tx_locktime, inputs_to_contribute, outputs_to_contribute, next_input_index, @@ -2133,10 +2131,6 @@ impl InteractiveTxConstructor { Ok(constructor) } - pub(super) fn funding_tx_locktime(&self) -> AbsoluteLockTime { - self.funding_tx_locktime - } - fn into_negotiation_error(self, reason: AbortReason) -> NegotiationError { let (contributed_inputs, contributed_outputs) = self.into_contributed_inputs_and_outputs(); NegotiationError { reason, contributed_inputs, contributed_outputs } From 9a04ed0dc5cc3fc675488aef3e2d3a5c86d321fd Mon Sep 17 00:00:00 2001 From: Duncan Dean Date: Fri, 9 May 2025 10:02:31 +0200 Subject: [PATCH 4/4] Test acceptor contributions in dual-funding functional tests We can now run through the case where the acceptor contributes to an inbound channel, with either more value in inputs, or less value, leading to a different `tx_signatures` exchange order. We also cannot use dummy P2WPKH funding inputs and witnesses anymore as `funding_transaction_signed` internally verifies signatures. Hence, we create external keypairs that we can create outputs for and sign with. --- lightning/src/ln/channel.rs | 13 + lightning/src/ln/dual_funding_tests.rs | 438 ++++++++++++++++++++++ lightning/src/ln/functional_test_utils.rs | 6 +- lightning/src/ln/splicing_tests.rs | 12 +- 4 files changed, 464 insertions(+), 5 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index e73a4a3ff3f..82a6315701c 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -6314,6 +6314,19 @@ where } } + #[cfg(all(test))] + pub fn get_initial_counterparty_commitment_signatures_for_test( + &mut self, funding: &mut FundingScope, logger: &L, + counterparty_next_commitment_point_override: PublicKey, + ) -> Option<(Signature, Vec)> + where + SP::Target: SignerProvider, + L::Target: Logger, + { + self.counterparty_next_commitment_point = Some(counterparty_next_commitment_point_override); + self.get_initial_counterparty_commitment_signatures(funding, logger) + } + fn check_funding_meets_minimum_depth(&self, funding: &FundingScope, height: u32) -> bool { let minimum_depth = self .minimum_depth(funding) diff --git a/lightning/src/ln/dual_funding_tests.rs b/lightning/src/ln/dual_funding_tests.rs index fc66ec315ca..b90a9a308bc 100644 --- a/lightning/src/ln/dual_funding_tests.rs +++ b/lightning/src/ln/dual_funding_tests.rs @@ -8,3 +8,441 @@ // licenses. //! Tests that test the creation of dual-funded channels in ChannelManager. + +use { + crate::{ + chain::chaininterface::{ConfirmationTarget, LowerBoundedFeeEstimator}, + events::{Event, InboundChannelFunds}, + ln::{ + chan_utils::{ + make_funding_redeemscript, ChannelPublicKeys, ChannelTransactionParameters, + CounterpartyChannelTransactionParameters, + }, + channel::PendingV2Channel, + channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBasepoint}, + functional_test_utils::*, + funding::FundingTxInput, + msgs::{ + BaseMessageHandler, ChannelMessageHandler, CommitmentSigned, MessageSendEvent, + TxAddInput, TxAddOutput, TxComplete, TxSignatures, + }, + types::ChannelId, + }, + prelude::*, + util::test_utils, + }, + bitcoin::{ + hashes::Hash, + key::{constants::SECRET_KEY_SIZE, Keypair, Secp256k1}, + secp256k1::Message, + sighash::SighashCache, + Amount, Witness, + }, +}; + +// Dual-funding: V2 Channel Establishment Tests +struct V2ChannelEstablishmentTestSession { + initiator_funding_satoshis: u64, + initiator_input_value_satoshis: u64, + acceptor_funding_satoshis: u64, + acceptor_input_value_satoshis: u64, +} + +// TODO(dual_funding): Use real node and API for creating V2 channels as initiator when available, +// instead of manually constructing messages. +fn do_test_v2_channel_establishment(session: V2ChannelEstablishmentTestSession) { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut node_1_user_config = test_default_channel_config(); + node_1_user_config.enable_dual_funded_channels = true; + node_1_user_config.manually_accept_inbound_channels = true; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(node_1_user_config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let logger_a = test_utils::TestLogger::with_id("node a".to_owned()); + + let secp_ctx = Secp256k1::new(); + let initiator_external_keypair = + Keypair::from_seckey_slice(&secp_ctx, &[2; SECRET_KEY_SIZE]).unwrap(); + let acceptor_external_keypair = + Keypair::from_seckey_slice(&secp_ctx, &[3; SECRET_KEY_SIZE]).unwrap(); + + // Create initiator funding input for the new channel along with its previous transaction. + let initiator_funding_inputs: Vec<_> = create_dual_funding_utxos_with_prev_txs( + &nodes[0], + &[session.initiator_input_value_satoshis], + &initiator_external_keypair.public_key(), + ); + + dbg!(&initiator_funding_inputs[0].utxo.output); + + // Create acceptor funding input for the new channel along with its previous transaction. + let acceptor_funding_inputs: Vec<_> = if session.acceptor_input_value_satoshis == 0 { + vec![] + } else { + create_dual_funding_utxos_with_prev_txs( + &nodes[1], + &[session.acceptor_input_value_satoshis], + &acceptor_external_keypair.public_key(), + ) + }; + if !acceptor_funding_inputs.is_empty() { + dbg!(&acceptor_funding_inputs[0].utxo.output); + } + let acceptor_funding_inputs_count = acceptor_funding_inputs.len(); + + // Alice creates a dual-funded channel as initiator. + let initiator_funding_satoshis = session.initiator_funding_satoshis; + let mut channel = PendingV2Channel::new_outbound( + &LowerBoundedFeeEstimator(node_cfgs[0].fee_estimator), + &nodes[0].node.entropy_source, + &nodes[0].node.signer_provider, + nodes[1].node.get_our_node_id(), + &nodes[1].node.init_features(), + initiator_funding_satoshis, + initiator_funding_inputs.clone(), + 42, /* user_channel_id */ + &nodes[0].node.get_current_config(), + nodes[0].best_block_info().1, + nodes[0].node.create_and_insert_outbound_scid_alias_for_test(), + ConfirmationTarget::NonAnchorChannelFee, + &logger_a, + ) + .unwrap(); + let open_channel_v2_msg = channel.get_open_channel_v2(nodes[0].chain_source.chain_hash); + + nodes[1].node.handle_open_channel_v2(nodes[0].node.get_our_node_id(), &open_channel_v2_msg); + + let events = nodes[1].node.get_and_clear_pending_events(); + let accept_channel_v2_msg = match &events[0] { + Event::OpenChannelRequest { + temporary_channel_id, + counterparty_node_id, + channel_negotiation_type, + .. + } => { + assert!(matches!(channel_negotiation_type, &InboundChannelFunds::DualFunded)); + nodes[1] + .node + .accept_inbound_channel_with_contribution( + temporary_channel_id, + counterparty_node_id, + u128::MAX - 2, + None, + Amount::from_sat(session.acceptor_funding_satoshis), + acceptor_funding_inputs.clone(), + None, + ) + .unwrap(); + get_event_msg!( + nodes[1], + MessageSendEvent::SendAcceptChannelV2, + nodes[0].node.get_our_node_id() + ) + }, + _ => panic!("Unexpected event"), + }; + + let channel_id = ChannelId::v2_from_revocation_basepoints( + &RevocationBasepoint::from(accept_channel_v2_msg.common_fields.revocation_basepoint), + &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(prevtx.clone()), + prevtx_out: 0, + sequence: sequence.0, + shared_input_txid: None, + }; + 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); + + nodes[1].node.handle_tx_add_input(nodes[0].node.get_our_node_id(), &tx_add_input_msg); + + if acceptor_funding_inputs_count > 0 { + let _tx_add_input_msg = get_event_msg!( + nodes[1], + MessageSendEvent::SendTxAddInput, + nodes[0].node.get_our_node_id() + ); + } else { + let _tx_complete_msg = get_event_msg!( + nodes[1], + MessageSendEvent::SendTxComplete, + nodes[0].node.get_our_node_id() + ); + } + + let tx_add_output_msg = TxAddOutput { + channel_id, + serial_id: 4, + sats: initiator_funding_satoshis.saturating_add(session.acceptor_funding_satoshis), + script: make_funding_redeemscript( + &open_channel_v2_msg.common_fields.funding_pubkey, + &accept_channel_v2_msg.common_fields.funding_pubkey, + ) + .to_p2wsh(), + }; + nodes[1].node.handle_tx_add_output(nodes[0].node.get_our_node_id(), &tx_add_output_msg); + + let acceptor_change_value_satoshis = + session.initiator_input_value_satoshis.saturating_sub(session.initiator_funding_satoshis); + if acceptor_funding_inputs_count > 0 + && acceptor_change_value_satoshis > accept_channel_v2_msg.common_fields.dust_limit_satoshis + { + println!("Change: {acceptor_change_value_satoshis} satoshis"); + let _tx_add_output_msg = get_event_msg!( + nodes[1], + MessageSendEvent::SendTxAddOutput, + nodes[0].node.get_our_node_id() + ); + } else { + let _tx_complete_msg = get_event_msg!( + nodes[1], + MessageSendEvent::SendTxComplete, + nodes[0].node.get_our_node_id() + ); + } + + let tx_complete_msg = TxComplete { channel_id }; + + nodes[1].node.handle_tx_complete(nodes[0].node.get_our_node_id(), &tx_complete_msg); + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + let update_htlcs_msg_event = if acceptor_funding_inputs_count > 0 { + assert_eq!(msg_events.len(), 2); + match msg_events[0] { + MessageSendEvent::SendTxComplete { ref node_id, .. } => { + assert_eq!(*node_id, nodes[0].node.get_our_node_id()); + }, + _ => panic!("Unexpected event"), + }; + &msg_events[1] + } else { + assert_eq!(msg_events.len(), 1); + &msg_events[0] + }; + let _msg_commitment_signed_from_1 = match update_htlcs_msg_event { + MessageSendEvent::UpdateHTLCs { node_id, channel_id: _, updates } => { + assert_eq!(*node_id, nodes[0].node.get_our_node_id()); + updates.commitment_signed.clone() + }, + _ => panic!("Unexpected event"), + }; + + let (funding_outpoint, channel_type_features) = { + let per_peer_state = nodes[1].node.per_peer_state.read().unwrap(); + let peer_state = + per_peer_state.get(&nodes[0].node.get_our_node_id()).unwrap().lock().unwrap(); + let channel_funding = + peer_state.channel_by_id.get(&tx_complete_msg.channel_id).unwrap().funding(); + (channel_funding.get_funding_txo(), channel_funding.get_channel_type().clone()) + }; + + channel.funding.channel_transaction_parameters = ChannelTransactionParameters { + counterparty_parameters: Some(CounterpartyChannelTransactionParameters { + pubkeys: ChannelPublicKeys { + funding_pubkey: accept_channel_v2_msg.common_fields.funding_pubkey, + revocation_basepoint: RevocationBasepoint( + accept_channel_v2_msg.common_fields.revocation_basepoint, + ), + payment_point: accept_channel_v2_msg.common_fields.payment_basepoint, + delayed_payment_basepoint: DelayedPaymentBasepoint( + accept_channel_v2_msg.common_fields.delayed_payment_basepoint, + ), + htlc_basepoint: HtlcBasepoint(accept_channel_v2_msg.common_fields.htlc_basepoint), + }, + selected_contest_delay: accept_channel_v2_msg.common_fields.to_self_delay, + }), + holder_pubkeys: ChannelPublicKeys { + funding_pubkey: open_channel_v2_msg.common_fields.funding_pubkey, + revocation_basepoint: RevocationBasepoint( + open_channel_v2_msg.common_fields.revocation_basepoint, + ), + payment_point: open_channel_v2_msg.common_fields.payment_basepoint, + delayed_payment_basepoint: DelayedPaymentBasepoint( + open_channel_v2_msg.common_fields.delayed_payment_basepoint, + ), + htlc_basepoint: HtlcBasepoint(open_channel_v2_msg.common_fields.htlc_basepoint), + }, + holder_selected_contest_delay: open_channel_v2_msg.common_fields.to_self_delay, + is_outbound_from_holder: true, + funding_outpoint, + splice_parent_funding_txid: None, + channel_type_features, + channel_value_satoshis: initiator_funding_satoshis + .saturating_add(session.acceptor_funding_satoshis), + }; + + let (signature, htlc_signatures) = channel + .context + .get_initial_counterparty_commitment_signatures_for_test( + &mut channel.funding, + &&logger_a, + accept_channel_v2_msg.common_fields.first_per_commitment_point, + ) + .unwrap(); + + let msg_commitment_signed_from_0 = CommitmentSigned { + channel_id, + signature, + htlc_signatures, + funding_txid: None, + #[cfg(taproot)] + partial_signature_with_nonce: None, + }; + + chanmon_cfgs[1].persister.set_update_ret(crate::chain::ChannelMonitorUpdateStatus::InProgress); + + // Handle the initial commitment_signed exchange. Order is not important here. + nodes[1] + .node + .handle_commitment_signed(nodes[0].node.get_our_node_id(), &msg_commitment_signed_from_0); + check_added_monitors(&nodes[1], 1); + + // The funding transaction should not have been broadcast before persisting initial monitor has + // been completed. + assert_eq!(nodes[1].tx_broadcaster.txn_broadcast().len(), 0); + + // Complete the persistence of the monitor. + let events = nodes[1].node.get_and_clear_pending_events(); + assert!(events.is_empty()); + nodes[1].chain_monitor.complete_sole_pending_chan_update(&channel_id); + + if acceptor_funding_inputs_count > 0 { + let events = nodes[1].node.get_and_clear_pending_events(); + match &events[0] { + Event::FundingTransactionReadyForSigning { + counterparty_node_id, + unsigned_transaction, + .. + } => { + assert_eq!(counterparty_node_id, &nodes[0].node.get_our_node_id()); + let mut transaction = unsigned_transaction.clone(); + let mut sighash_cache = SighashCache::new(unsigned_transaction); + for (idx, input) in transaction.input.iter_mut().enumerate() { + if input.previous_output.txid == acceptor_funding_inputs[0].utxo.outpoint.txid { + let sighash = sighash_cache + .p2wpkh_signature_hash( + idx, + &acceptor_funding_inputs[0].utxo.output.script_pubkey, + acceptor_funding_inputs[0].utxo.output.value, + bitcoin::EcdsaSighashType::All, + ) + .unwrap(); + let msg = Message::from_digest(sighash.as_raw_hash().to_byte_array()); + + let signature = + secp_ctx.sign_ecdsa(&msg, &acceptor_external_keypair.secret_key()); + let mut witness = Witness::p2wpkh( + &bitcoin::ecdsa::Signature::sighash_all(signature), + &acceptor_external_keypair.public_key(), + ); + input.witness = witness; + } + } + nodes[1] + .node + .funding_transaction_signed(&channel_id, counterparty_node_id, transaction) + .unwrap(); + }, + _ => panic!("Unexpected event"), + }; + } + + if session.acceptor_input_value_satoshis < session.initiator_input_value_satoshis { + let tx_signatures_msg = get_event_msg!( + nodes[1], + MessageSendEvent::SendTxSignatures, + nodes[0].node.get_our_node_id() + ); + + assert_eq!(tx_signatures_msg.channel_id, channel_id); + + let mut witness = Witness::new(); + witness.push([0x0]); + // Receive tx_signatures from channel initiator. + nodes[1].node.handle_tx_signatures( + nodes[0].node.get_our_node_id(), + &TxSignatures { + channel_id, + tx_hash: funding_outpoint.unwrap().txid, + witnesses: vec![witness], + shared_input_signature: None, + }, + ); + } else { + let mut witness = Witness::new(); + witness.push([0x0]); + // Receive tx_signatures from channel initiator. + nodes[1].node.handle_tx_signatures( + nodes[0].node.get_our_node_id(), + &TxSignatures { + channel_id, + tx_hash: funding_outpoint.unwrap().txid, + witnesses: vec![witness], + shared_input_signature: None, + }, + ); + + let tx_signatures_msg = get_event_msg!( + nodes[1], + MessageSendEvent::SendTxSignatures, + nodes[0].node.get_our_node_id() + ); + + assert_eq!(tx_signatures_msg.channel_id, channel_id); + } + + let events = nodes[1].node.get_and_clear_pending_events(); + if acceptor_funding_inputs_count == 0 { + assert_eq!(events.len(), 1); + match events[0] { + Event::ChannelPending { channel_id: chan_id, .. } => assert_eq!(chan_id, channel_id), + _ => panic!("Unexpected event"), + }; + } + + // For an inbound channel V2 channel the transaction should be broadcast once receiving a + // tx_signature and applying local tx_signatures: + let broadcasted_txs = nodes[1].tx_broadcaster.txn_broadcast(); + assert_eq!(broadcasted_txs.len(), 1); +} + +#[test] +fn test_v2_channel_establishment() { + // Initiator contributes inputs, acceptor does not. + do_test_v2_channel_establishment(V2ChannelEstablishmentTestSession { + initiator_funding_satoshis: 100_00, + initiator_input_value_satoshis: 150_000, + acceptor_funding_satoshis: 0, + acceptor_input_value_satoshis: 0, + }); + // Initiator contributes more input value than acceptor. + do_test_v2_channel_establishment(V2ChannelEstablishmentTestSession { + initiator_funding_satoshis: 100_00, + initiator_input_value_satoshis: 150_000, + acceptor_funding_satoshis: 50_00, + acceptor_input_value_satoshis: 100_000, + }); + // Initiator contributes less input value than acceptor. + do_test_v2_channel_establishment(V2ChannelEstablishmentTestSession { + initiator_funding_satoshis: 100_00, + initiator_input_value_satoshis: 150_000, + acceptor_funding_satoshis: 125_00, + acceptor_input_value_satoshis: 200_000, + }); + // Initiator contributes the same input value as acceptor. + // nodes[0] node_id: 88ce8f35acfc... + // nodes[1] node_id: 236cdaa42692... + // Since nodes[1] has a node_id in earlier lexicographical order, it should send tx_signatures first. + do_test_v2_channel_establishment(V2ChannelEstablishmentTestSession { + initiator_funding_satoshis: 100_00, + initiator_input_value_satoshis: 150_000, + acceptor_funding_satoshis: 125_00, + acceptor_input_value_satoshis: 150_000, + }); +} diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 5fac0fd9b4f..3aaa2253aaa 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -66,7 +66,7 @@ use bitcoin::script::ScriptBuf; use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::transaction::{self, Version as TxVersion}; use bitcoin::transaction::{Transaction, TxIn, TxOut}; -use bitcoin::WPubkeyHash; +use bitcoin::CompressedPublicKey; use crate::io; use crate::prelude::*; @@ -1475,7 +1475,7 @@ fn internal_create_funding_transaction<'a, 'b, 'c>( /// Create test inputs for a funding transaction. /// 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], + node: &Node<'_, '_, '_>, utxo_values_in_satoshis: &[u64], pubkey: &PublicKey, ) -> Vec { // Ensure we have unique transactions per node by using the locktime. let tx = Transaction { @@ -1491,7 +1491,7 @@ pub fn create_dual_funding_utxos_with_prev_txs( .iter() .map(|value_satoshis| TxOut { value: Amount::from_sat(*value_satoshis), - script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + script_pubkey: ScriptBuf::new_p2wpkh(&CompressedPublicKey(*pubkey).wpubkey_hash()), }) .collect(), }; diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index 29243ba8374..1ff0a15ea70 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -27,6 +27,7 @@ use crate::util::errors::APIError; use crate::util::ser::Writeable; use crate::util::test_channel_signer::SignerOp; +use bitcoin::key::{constants::SECRET_KEY_SIZE, Keypair, Secp256k1}; use bitcoin::secp256k1::PublicKey; use bitcoin::{Amount, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut}; @@ -43,10 +44,17 @@ fn test_v1_splice_in_negative_insufficient_inputs() { // Amount being added to the channel through the splice-in let splice_in_sats = 20_000; + let secp_ctx = Secp256k1::new(); + let initiator_external_keypair = + Keypair::from_seckey_slice(&secp_ctx, &[2; SECRET_KEY_SIZE]).unwrap(); + // Create additional inputs, but insufficient let extra_splice_funding_input_sats = splice_in_sats - 1; - let funding_inputs = - create_dual_funding_utxos_with_prev_txs(&nodes[0], &[extra_splice_funding_input_sats]); + let funding_inputs = create_dual_funding_utxos_with_prev_txs( + &nodes[0], + &[extra_splice_funding_input_sats], + &initiator_external_keypair.public_key(), + ); let contribution = SpliceContribution::SpliceIn { value: Amount::from_sat(splice_in_sats),