From 6451e2b44da4fca79198e64c15e3bdf0c6fa5f25 Mon Sep 17 00:00:00 2001 From: Duncan Dean Date: Fri, 6 Oct 2023 13:05:17 +0200 Subject: [PATCH 1/6] Add interactive tx constructor to `ChannelContext` --- lightning/src/ln/channel.rs | 25 +++++++++++++++++++++---- lightning/src/ln/channelmanager.rs | 3 +-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index f5c47260f6e..16749de47de 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -27,6 +27,8 @@ use bitcoin::secp256k1; use crate::ln::types::{ChannelId, PaymentPreimage, PaymentHash}; use crate::ln::features::{ChannelTypeFeatures, InitFeatures}; +#[cfg(any(dual_funding, splicing))] +use crate::ln::interactivetxs::InteractiveTxConstructor; use crate::ln::msgs; use crate::ln::msgs::DecodeError; use crate::ln::script::{self, ShutdownScript}; @@ -3633,6 +3635,9 @@ pub(super) struct Channel where SP::Target: SignerProvider { pub context: ChannelContext, #[cfg(any(dual_funding, splicing))] pub dual_funding_channel_context: Option, + /// The current interactive transaction construction session under negotiation. + #[cfg(any(dual_funding, splicing))] + interactive_tx_constructor: Option, } #[cfg(any(test, fuzzing))] @@ -7734,6 +7739,8 @@ impl OutboundV1Channel where SP::Target: SignerProvider { context: self.context, #[cfg(any(dual_funding, splicing))] dual_funding_channel_context: None, + #[cfg(any(dual_funding, splicing))] + interactive_tx_constructor: None, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some(); @@ -7842,7 +7849,7 @@ impl InboundV1Channel where SP::Target: SignerProvider { msg.push_msat, msg.common_fields.clone(), )?, - unfunded_context: UnfundedChannelContext { unfunded_channel_age_ticks: 0 } + unfunded_context: UnfundedChannelContext { unfunded_channel_age_ticks: 0 }, }; Ok(chan) } @@ -8025,6 +8032,8 @@ impl InboundV1Channel where SP::Target: SignerProvider { context: self.context, #[cfg(any(dual_funding, splicing))] dual_funding_channel_context: None, + #[cfg(any(dual_funding, splicing))] + interactive_tx_constructor: None, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some(); channel.monitor_updating_paused(false, false, need_channel_ready, Vec::new(), Vec::new(), Vec::new()); @@ -8038,8 +8047,9 @@ impl InboundV1Channel where SP::Target: SignerProvider { pub(super) struct OutboundV2Channel where SP::Target: SignerProvider { pub context: ChannelContext, pub unfunded_context: UnfundedChannelContext, - #[cfg(any(dual_funding, splicing))] pub dual_funding_context: DualFundingChannelContext, + /// The current interactive transaction construction session under negotiation. + interactive_tx_constructor: Option, } #[cfg(any(dual_funding, splicing))] @@ -8090,7 +8100,9 @@ impl OutboundV2Channel where SP::Target: SignerProvider { their_funding_satoshis: 0, funding_tx_locktime, funding_feerate_sat_per_1000_weight, - } + }, + #[cfg(any(dual_funding, splicing))] + interactive_tx_constructor: None, }; Ok(chan) } @@ -8163,6 +8175,8 @@ pub(super) struct InboundV2Channel where SP::Target: SignerProvider { pub context: ChannelContext, pub unfunded_context: UnfundedChannelContext, pub dual_funding_context: DualFundingChannelContext, + /// The current interactive transaction construction session under negotiation. + interactive_tx_constructor: Option, } #[cfg(any(dual_funding, splicing))] @@ -8235,7 +8249,8 @@ impl InboundV2Channel where SP::Target: SignerProvider { their_funding_satoshis: msg.common_fields.funding_satoshis, funding_tx_locktime: msg.locktime, funding_feerate_sat_per_1000_weight: msg.funding_feerate_sat_per_1000_weight, - } + }, + interactive_tx_constructor: None, }; Ok(chan) @@ -9361,6 +9376,8 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, u32, &'c Ch }, #[cfg(any(dual_funding, splicing))] dual_funding_channel_context: None, + #[cfg(any(dual_funding, splicing))] + interactive_tx_constructor: None, }) } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 508d13a1582..096f0308bf1 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -7287,7 +7287,6 @@ where // TODO(dual_funding): Combine this match arm with above. #[cfg(any(dual_funding, splicing))] ChannelPhase::UnfundedInboundV2(_) | ChannelPhase::UnfundedOutboundV2(_) => { - let context = phase.context_mut(); log_error!(self.logger, "Immediately closing unfunded channel {} as peer asked to cooperatively shut it down (which is unnecessary)", &msg.channel_id); let mut chan = remove_channel_phase!(self, chan_phase_entry); finish_shutdown = Some(chan.context_mut().force_shutdown(false, ClosureReason::CounterpartyCoopClosedUnfundedChannel)); @@ -9997,7 +9996,7 @@ where // TODO(dual_funding): Combine this match arm with above once #[cfg(any(dual_funding, splicing))] is removed. #[cfg(any(dual_funding, splicing))] - ChannelPhase::UnfundedInboundV2(channel) => { + ChannelPhase::UnfundedInboundV2(_) => { // Since unfunded inbound channel maps are cleared upon disconnecting a peer, // they are not persisted and won't be recovered after a crash. // Therefore, they shouldn't exist at this point. From 9fafba904feda37242ec6d6c0cf7e81fc4d7722e Mon Sep 17 00:00:00 2001 From: Duncan Dean Date: Fri, 15 Sep 2023 13:19:24 +0200 Subject: [PATCH 2/6] Implement support for accepting V2 channels --- lightning/src/events/mod.rs | 34 +- lightning/src/ln/channel.rs | 247 +++++++++- lightning/src/ln/channelmanager.rs | 729 ++++++++++++++++++++++++----- lightning/src/ln/interactivetxs.rs | 96 +++- lightning/src/util/config.rs | 9 + 5 files changed, 991 insertions(+), 124 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 887851864c9..a5a42eceec3 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -538,6 +538,18 @@ impl_writeable_tlv_based_enum!(PaymentFailureReason, (10, UnexpectedError) => {}, ; ); +/// Used to indicate the kind of funding for this channel by the channel acceptor (us). +/// +/// Allows the differentiation between a request for a dual-funded and non-dual-funded channel. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum InboundChannelFunds { + /// For a non-dual-funded channel, the `push_msat` value from the channel initiator to us. + PushMsat(u64), + /// Indicates the open request is for a dual funded channel and we may choose to contribute + /// funds to the channel. + DualFunded, +} + /// An Event which you should probably take some action in response to. /// /// Note that while Writeable and Readable are implemented for Event, you probably shouldn't use @@ -1143,14 +1155,28 @@ pub enum Event { }, /// Indicates a request to open a new channel by a peer. /// - /// To accept the request, call [`ChannelManager::accept_inbound_channel`]. To reject the request, - /// call [`ChannelManager::force_close_without_broadcasting_txn`]. Note that a ['ChannelClosed`] - /// event will _not_ be triggered if the channel is rejected. + /// If `acceptor_funds` is `InboundChannelFunds::DualFunded`, this indicates that the peer wishes to + /// open a dual-funded channel. In this case you can choose whether to contribute funds or not. + /// Otherwise, `acceptor_funds` will be `InboundChannelFunds::PushMsats`, indicating the `push_msats` + /// value for a non-dual-funded channel. + /// + /// To accept the request (and in the case of a dual-funded channel, not contribute funds), + /// call [`ChannelManager::accept_inbound_channel`]. + // TODO(dual_funding): Make this a doc comment when dual-funding fully released. + // To accept the request and contribute funds for a dual-funded channel, + // call [`ChannelManager::accept_inbound_channel_with_contribution`]. + /// To reject the request, call [`ChannelManager::force_close_without_broadcasting_txn`]. + /// Note that a ['ChannelClosed`] event will _not_ be triggered if the channel is rejected. /// /// The event is only triggered when a new open channel request is received and the /// [`UserConfig::manually_accept_inbound_channels`] config flag is set to true. + /// Note that if you wish to be able to contribute funds to dual-funded open channel requests, + /// [`UserConfig::manually_accept_inbound_channels`] MUST be set to true so that you may + /// provide funding inputs when choosing to contribute to the channel capacity. /// /// [`ChannelManager::accept_inbound_channel`]: crate::ln::channelmanager::ChannelManager::accept_inbound_channel + // TODO(dual_funding): Make this a doc comment when dual-funding fully released. + // [`ChannelManager::accept_inbound_channel_with_contribution`]: crate::ln::channelmanager::ChannelManager::accept_inbound_channel_with_contribution /// [`ChannelManager::force_close_without_broadcasting_txn`]: crate::ln::channelmanager::ChannelManager::force_close_without_broadcasting_txn /// [`UserConfig::manually_accept_inbound_channels`]: crate::util::config::UserConfig::manually_accept_inbound_channels OpenChannelRequest { @@ -1176,7 +1202,7 @@ pub enum Event { /// The channel value of the requested channel. funding_satoshis: u64, /// Our starting balance in the channel if the request is accepted, in milli-satoshi. - push_msat: u64, + acceptor_funds: InboundChannelFunds, /// The features that this channel will operate with. If you reject the channel, a /// well-behaved counterparty may automatically re-attempt the channel with a new set of /// feature flags. diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 16749de47de..f071b0e0fcd 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -11,8 +11,7 @@ use bitcoin::amount::Amount; use bitcoin::blockdata::constants::ChainHash; use bitcoin::blockdata::script::{Script, ScriptBuf, Builder}; use bitcoin::blockdata::transaction::Transaction; -use bitcoin::sighash; -use bitcoin::sighash::EcdsaSighashType; +use bitcoin::sighash::{self, EcdsaSighashType}; use bitcoin::consensus::encode; use bitcoin::hashes::Hash; @@ -24,11 +23,15 @@ use bitcoin::secp256k1::constants::PUBLIC_KEY_SIZE; use bitcoin::secp256k1::{PublicKey,SecretKey}; use bitcoin::secp256k1::{Secp256k1,ecdsa::Signature}; use bitcoin::secp256k1; +#[cfg(any(dual_funding, splicing))] +use bitcoin::{TxIn, TxOut}; +#[cfg(any(dual_funding, splicing))] +use bitcoin::locktime::absolute::LockTime; use crate::ln::types::{ChannelId, PaymentPreimage, PaymentHash}; use crate::ln::features::{ChannelTypeFeatures, InitFeatures}; #[cfg(any(dual_funding, splicing))] -use crate::ln::interactivetxs::InteractiveTxConstructor; +use crate::ln::interactivetxs::{estimate_input_weight, get_output_weight, HandleTxCompleteResult, InteractiveTxConstructor, InteractiveTxMessageSend, InteractiveTxMessageSendResult, TX_COMMON_FIELDS_WEIGHT}; use crate::ln::msgs; use crate::ln::msgs::DecodeError; use crate::ln::script::{self, ShutdownScript}; @@ -39,6 +42,8 @@ use crate::ln::chan_utils; use crate::ln::onion_utils::HTLCFailReason; use crate::chain::BestBlock; use crate::chain::chaininterface::{FeeEstimator, ConfirmationTarget, LowerBoundedFeeEstimator}; +#[cfg(any(dual_funding, splicing))] +use crate::chain::chaininterface::fee_for_weight; use crate::chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateStep, LATENCY_GRACE_PERIOD_BLOCKS, CLOSED_CHANNEL_UPDATE_ID}; use crate::chain::transaction::{OutPoint, TransactionData}; use crate::sign::ecdsa::EcdsaChannelSigner; @@ -46,6 +51,8 @@ use crate::sign::{EntropySource, ChannelSigner, SignerProvider, NodeSigner, Reci use crate::events::ClosureReason; use crate::routing::gossip::NodeId; use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer}; +#[cfg(any(dual_funding, splicing))] +use crate::util::ser::TransactionU16LenLimited; use crate::util::logger::{Logger, Record, WithContext}; use crate::util::errors::APIError; use crate::util::config::{UserConfig, ChannelConfig, LegacyChannelConfig, ChannelHandshakeConfig, ChannelHandshakeLimits, MaxDustHTLCExposure}; @@ -1439,6 +1446,144 @@ pub(super) struct ChannelContext where SP::Target: SignerProvider { blocked_monitor_updates: Vec, } +#[cfg(any(dual_funding, splicing))] +pub(super) trait InteractivelyFunded where SP::Target: SignerProvider { + fn context(&self) -> &ChannelContext; + + fn context_mut(&mut self) -> &mut ChannelContext; + + fn interactive_tx_constructor_mut(&mut self) -> &mut Option; + + fn dual_funding_context_mut(&mut self) -> &mut DualFundingChannelContext; + + fn set_interactive_tx_constructor(&mut self, interactive_tx_constructor: InteractiveTxConstructor); + + fn is_initiator(&self) -> bool; + + fn begin_interactive_funding_tx_construction( + &mut self, entropy_source: &ES, + ) -> Result, APIError> + where ES::Target: EntropySource + { + let mut funding_outputs = Vec::new(); + if self.is_initiator() { + funding_outputs.push(TxOut { + value: Amount::from_sat(self.context().get_value_satoshis()), + script_pubkey: self.context().get_funding_redeemscript().to_p2wsh(), + }); + } + + let (tx_constructor, msg) = InteractiveTxConstructor::new( + entropy_source, self.context().channel_id(), + self.dual_funding_context_mut().funding_feerate_sat_per_1000_weight, self.is_initiator(), + self.dual_funding_context_mut().funding_tx_locktime, + self.dual_funding_context_mut().our_funding_inputs.clone(), funding_outputs + ); + self.set_interactive_tx_constructor(tx_constructor); + + Ok(msg) + } + + fn tx_add_input(&mut self, msg: &msgs::TxAddInput) -> InteractiveTxMessageSendResult { + InteractiveTxMessageSendResult(match self.interactive_tx_constructor_mut() { + Some(ref mut tx_constructor) => tx_constructor.handle_tx_add_input(msg).map_err( + |reason| reason.into_tx_abort_msg(self.context_mut().channel_id())), + None => Err(msgs::TxAbort { + channel_id: self.context_mut().channel_id(), + data: "We do not have an interactive transaction negotiation in progress".to_string().into_bytes() + }), + }) + } + + fn tx_add_output(&mut self, msg: &msgs::TxAddOutput)-> InteractiveTxMessageSendResult { + InteractiveTxMessageSendResult(match self.interactive_tx_constructor_mut() { + Some(ref mut tx_constructor) => tx_constructor.handle_tx_add_output(msg).map_err( + |reason| reason.into_tx_abort_msg(self.context_mut().channel_id())), + None => Err(msgs::TxAbort { + channel_id: self.context_mut().channel_id(), + data: "We do not have an interactive transaction negotiation in progress".to_string().into_bytes() + }), + }) + } + + fn tx_remove_input(&mut self, msg: &msgs::TxRemoveInput)-> InteractiveTxMessageSendResult { + InteractiveTxMessageSendResult(match self.interactive_tx_constructor_mut() { + Some(ref mut tx_constructor) => tx_constructor.handle_tx_remove_input(msg).map_err( + |reason| reason.into_tx_abort_msg(self.context_mut().channel_id())), + None => Err(msgs::TxAbort { + channel_id: self.context_mut().channel_id(), + data: "We do not have an interactive transaction negotiation in progress".to_string().into_bytes() + }), + }) + } + + fn tx_remove_output(&mut self, msg: &msgs::TxRemoveOutput)-> InteractiveTxMessageSendResult { + InteractiveTxMessageSendResult(match self.interactive_tx_constructor_mut() { + Some(ref mut tx_constructor) => tx_constructor.handle_tx_remove_output(msg).map_err( + |reason| reason.into_tx_abort_msg(self.context_mut().channel_id())), + None => Err(msgs::TxAbort { + channel_id: self.context_mut().channel_id(), + data: "We do not have an interactive transaction negotiation in progress".to_string().into_bytes() + }), + }) + } + + fn tx_complete(&mut self, msg: &msgs::TxComplete) -> HandleTxCompleteResult { + HandleTxCompleteResult(match self.interactive_tx_constructor_mut() { + Some(ref mut tx_constructor) => tx_constructor.handle_tx_complete(msg).map_err( + |reason| reason.into_tx_abort_msg(self.context_mut().channel_id())), + None => Err(msgs::TxAbort { + channel_id: self.context_mut().channel_id(), + data: "We do not have an interactive transaction negotiation in progress".to_string().into_bytes() + }), + }) + } +} + +#[cfg(any(dual_funding, splicing))] +impl InteractivelyFunded for OutboundV2Channel where SP::Target: SignerProvider { + fn context(&self) -> &ChannelContext { + &self.context + } + fn context_mut(&mut self) -> &mut ChannelContext { + &mut self.context + } + fn dual_funding_context_mut(&mut self) -> &mut DualFundingChannelContext { + &mut self.dual_funding_context + } + fn interactive_tx_constructor_mut(&mut self) -> &mut Option { + &mut self.interactive_tx_constructor + } + fn set_interactive_tx_constructor(&mut self, interactive_tx_constructor: InteractiveTxConstructor) { + self.interactive_tx_constructor = Some(interactive_tx_constructor); + } + fn is_initiator(&self) -> bool { + true + } +} + +#[cfg(any(dual_funding, splicing))] +impl InteractivelyFunded for InboundV2Channel where SP::Target: SignerProvider { + fn context(&self) -> &ChannelContext { + &self.context + } + fn context_mut(&mut self) -> &mut ChannelContext { + &mut self.context + } + fn dual_funding_context_mut(&mut self) -> &mut DualFundingChannelContext { + &mut self.dual_funding_context + } + fn interactive_tx_constructor_mut(&mut self) -> &mut Option { + &mut self.interactive_tx_constructor + } + fn set_interactive_tx_constructor(&mut self, interactive_tx_constructor: InteractiveTxConstructor) { + self.interactive_tx_constructor = Some(interactive_tx_constructor); + } + fn is_initiator(&self) -> bool { + false + } +} + impl ChannelContext where SP::Target: SignerProvider { fn new_for_inbound_channel<'a, ES: Deref, F: Deref, L: Deref>( fee_estimator: &'a LowerBoundedFeeEstimator, @@ -3531,6 +3676,28 @@ impl ChannelContext where SP::Target: SignerProvider { self.channel_transaction_parameters.channel_type_features = self.channel_type.clone(); Ok(()) } + + // Interactive transaction construction + + #[cfg(any(dual_funding, splicing))] + pub fn tx_signatures(&self, msg: &msgs::TxSignatures)-> Result { + todo!(); + } + + #[cfg(any(dual_funding, splicing))] + pub fn tx_init_rbf(&self, msg: &msgs::TxInitRbf)-> Result { + todo!(); + } + + #[cfg(any(dual_funding, splicing))] + pub fn tx_ack_rbf(&self, msg: &msgs::TxAckRbf)-> Result { + todo!(); + } + + #[cfg(any(dual_funding, splicing))] + pub fn tx_abort(&self, msg: &msgs::TxAbort)-> Result { + todo!(); + } } // Internal utility functions for channels @@ -3615,6 +3782,43 @@ pub(crate) fn per_outbound_htlc_counterparty_commit_tx_fee_msat(feerate_per_kw: } } +#[cfg(any(dual_funding, splicing))] +pub(super) fn calculate_our_funding_satoshis( + is_initiator: bool, funding_inputs: &[(TxIn, TransactionU16LenLimited)], + funding_outputs: &[TxOut], funding_feerate_sat_per_1000_weight: u32, + holder_dust_limit_satoshis: u64, +) -> Result { + let mut total_input_satoshis = 0u64; + let mut our_contributed_weight = 0u64; + + for (idx, input) in funding_inputs.iter().enumerate() { + if let Some(output) = input.1.as_transaction().output.get(input.0.previous_output.vout as usize) { + total_input_satoshis = total_input_satoshis.saturating_add(output.value.to_sat()); + our_contributed_weight = our_contributed_weight.saturating_add(estimate_input_weight(output).to_wu()); + } else { + return Err(APIError::APIMisuseError { + err: format!("Transaction with txid {} does not have an output with vout of {} corresponding to TxIn at funding_inputs[{}]", + input.1.as_transaction().txid(), input.0.previous_output.vout, idx) }); + } + } + our_contributed_weight = our_contributed_weight.saturating_add(funding_outputs.iter().fold(0u64, |weight, txout| { + weight.saturating_add(get_output_weight(&txout.script_pubkey).to_wu()) + })); + + // If we are the initiator, we must pay for weight of all common fields in the funding transaction. + if is_initiator { + our_contributed_weight = our_contributed_weight.saturating_add(TX_COMMON_FIELDS_WEIGHT); + } + + let funding_satoshis = total_input_satoshis + .saturating_sub(fee_for_weight(funding_feerate_sat_per_1000_weight, our_contributed_weight)); + if funding_satoshis < holder_dust_limit_satoshis { + Ok(0) + } else { + Ok(funding_satoshis) + } +} + /// Context for dual-funded channels. #[cfg(any(dual_funding, splicing))] pub(super) struct DualFundingChannelContext { @@ -3624,9 +3828,15 @@ pub(super) struct DualFundingChannelContext { pub their_funding_satoshis: u64, /// 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: u32, + pub funding_tx_locktime: LockTime, /// The feerate set by the initiator to be used for the funding transaction. pub funding_feerate_sat_per_1000_weight: u32, + /// The funding inputs we will be contributing to the channel. + /// + /// Note that the `our_funding_satoshis` field is equal to the total value of `our_funding_inputs` + /// minus any fees paid for our contributed weight. This means that change will never be generated + /// and the maximum value possible will go towards funding the channel. + pub our_funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, } // Holder designates channel data owned for the benefit of the user client. @@ -8057,8 +8267,8 @@ impl OutboundV2Channel where SP::Target: SignerProvider { pub fn new( fee_estimator: &LowerBoundedFeeEstimator, entropy_source: &ES, signer_provider: &SP, counterparty_node_id: PublicKey, their_features: &InitFeatures, funding_satoshis: u64, - user_id: u128, config: &UserConfig, current_chain_height: u32, outbound_scid_alias: u64, - funding_confirmation_target: ConfirmationTarget, + funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, user_id: u128, config: &UserConfig, + current_chain_height: u32, outbound_scid_alias: u64, funding_confirmation_target: ConfirmationTarget, ) -> Result, APIError> where ES::Target: EntropySource, F::Target: FeeEstimator, @@ -8073,7 +8283,11 @@ impl OutboundV2Channel where SP::Target: SignerProvider { funding_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS); let funding_feerate_sat_per_1000_weight = fee_estimator.bounded_sat_per_1000_weight(funding_confirmation_target); - let funding_tx_locktime = current_chain_height; + let funding_tx_locktime = LockTime::from_height(current_chain_height) + .map_err(|_| APIError::APIMisuseError { + err: format!( + "Provided current chain height of {} doesn't make sense for a height-based timelock for the funding transaction", + current_chain_height) })?; let chan = Self { context: ChannelContext::new_for_outbound_channel( @@ -8100,6 +8314,7 @@ impl OutboundV2Channel where SP::Target: SignerProvider { their_funding_satoshis: 0, funding_tx_locktime, funding_feerate_sat_per_1000_weight, + our_funding_inputs: funding_inputs, }, #[cfg(any(dual_funding, splicing))] interactive_tx_constructor: None, @@ -8163,7 +8378,7 @@ impl OutboundV2Channel where SP::Target: SignerProvider { }, funding_feerate_sat_per_1000_weight: self.context.feerate_per_kw, second_per_commitment_point, - locktime: self.dual_funding_context.funding_tx_locktime, + locktime: self.dual_funding_context.funding_tx_locktime.to_consensus_u32(), require_confirmed_inputs: None, } } @@ -8186,13 +8401,22 @@ impl InboundV2Channel where SP::Target: SignerProvider { pub fn new( fee_estimator: &LowerBoundedFeeEstimator, entropy_source: &ES, signer_provider: &SP, counterparty_node_id: PublicKey, our_supported_features: &ChannelTypeFeatures, - their_features: &InitFeatures, msg: &msgs::OpenChannelV2, funding_satoshis: u64, user_id: u128, - config: &UserConfig, current_chain_height: u32, logger: &L, + their_features: &InitFeatures, msg: &msgs::OpenChannelV2, + funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, user_id: u128, config: &UserConfig, + current_chain_height: u32, logger: &L, ) -> Result, ChannelError> where ES::Target: EntropySource, F::Target: FeeEstimator, L::Target: Logger, { + let funding_satoshis = calculate_our_funding_satoshis( + false, &funding_inputs, &[], msg.funding_feerate_sat_per_1000_weight, + msg.common_fields.dust_limit_satoshis + ).map_err(|_| ChannelError::Close( + ( + "Failed to accept channel".to_string(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + )))?; let channel_value_satoshis = funding_satoshis.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); @@ -8247,8 +8471,9 @@ impl InboundV2Channel where SP::Target: SignerProvider { dual_funding_context: DualFundingChannelContext { our_funding_satoshis: funding_satoshis, their_funding_satoshis: msg.common_fields.funding_satoshis, - funding_tx_locktime: msg.locktime, + 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, }, interactive_tx_constructor: None, }; diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 096f0308bf1..ec17fce2fb7 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -18,7 +18,7 @@ //! imply it needs to fail HTLCs/payments/channels it manages). use bitcoin::blockdata::block::Header; -use bitcoin::blockdata::transaction::Transaction; +use bitcoin::blockdata::transaction::{Transaction, TxIn}; use bitcoin::blockdata::constants::ChainHash; use bitcoin::key::constants::SECRET_KEY_SIZE; use bitcoin::network::Network; @@ -39,17 +39,21 @@ use crate::chain::{Confirm, ChannelMonitorUpdateStatus, Watch, BestBlock}; use crate::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator}; use crate::chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate, WithChannelMonitor, ChannelMonitorUpdateStep, HTLC_FAIL_BACK_BUFFER, CLTV_CLAIM_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS, ANTI_REORG_DELAY, MonitorEvent, CLOSED_CHANNEL_UPDATE_ID}; use crate::chain::transaction::{OutPoint, TransactionData}; -use crate::events; +use crate::events::{self, InboundChannelFunds}; use crate::events::{Event, EventHandler, EventsProvider, MessageSendEvent, MessageSendEventsProvider, ClosureReason, HTLCDestination, PaymentFailureReason}; // Since this struct is returned in `list_channels` methods, expose it here in case users want to // construct one themselves. use crate::ln::inbound_payment; use crate::ln::types::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret}; -use crate::ln::channel::{self, Channel, ChannelPhase, ChannelContext, ChannelError, ChannelUpdateStatus, ShutdownResult, UnfundedChannelContext, UpdateFulfillCommitFetch, OutboundV1Channel, InboundV1Channel, WithChannelContext}; +use crate::ln::channel::{Channel, ChannelPhase, ChannelContext, ChannelError, ChannelUpdateStatus, ShutdownResult, UnfundedChannelContext, UpdateFulfillCommitFetch, OutboundV1Channel, InboundV1Channel, WithChannelContext}; use crate::ln::channel_state::ChannelDetails; +#[cfg(any(dual_funding, splicing))] +use crate::ln::channel::{InboundV2Channel, InteractivelyFunded as _}; use crate::ln::features::{Bolt12InvoiceFeatures, ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures}; #[cfg(any(feature = "_test_utils", test))] use crate::ln::features::Bolt11InvoiceFeatures; +#[cfg(any(dual_funding, splicing))] +use crate::ln::interactivetxs::InteractiveTxMessageSend; use crate::routing::router::{BlindedTail, InFlightHtlcs, Path, Payee, PaymentParameters, Route, RouteParameters, Router}; use crate::ln::onion_payment::{check_incoming_htlc_cltv, create_recv_pending_htlc_info, create_fwd_pending_htlc_info, decode_incoming_update_add_htlc_onion, InboundHTLCErr, NextPacketDetails}; use crate::ln::msgs; @@ -75,6 +79,7 @@ use crate::util::wakers::{Future, Notifier}; use crate::util::scid_utils::fake_scid; use crate::util::string::UntrustedString; use crate::util::ser::{BigSize, FixedLengthReader, Readable, ReadableArgs, MaybeReadable, Writeable, Writer, VecWriter}; +use crate::util::ser::TransactionU16LenLimited; use crate::util::logger::{Level, Logger, WithContext}; use crate::util::errors::APIError; @@ -949,11 +954,18 @@ impl PeerState where SP::Target: SignerProvider { } } +#[derive(Clone)] +pub(super) enum OpenChannelMessage { + V1(msgs::OpenChannel), + #[cfg(any(dual_funding, splicing))] + V2(msgs::OpenChannelV2), +} + /// A not-yet-accepted inbound (from counterparty) channel. Once /// accepted, the parameters will be used to construct a channel. pub(super) struct InboundChannelRequest { /// The original OpenChannel message. - pub open_channel_msg: msgs::OpenChannel, + pub open_channel_msg: OpenChannelMessage, /// The number of ticks remaining before the request expires. pub ticks_remaining: i32, } @@ -6673,7 +6685,7 @@ where /// [`Event::OpenChannelRequest`]: events::Event::OpenChannelRequest /// [`Event::ChannelClosed::user_channel_id`]: events::Event::ChannelClosed::user_channel_id pub fn accept_inbound_channel(&self, temporary_channel_id: &ChannelId, counterparty_node_id: &PublicKey, user_channel_id: u128) -> Result<(), APIError> { - self.do_accept_inbound_channel(temporary_channel_id, counterparty_node_id, false, user_channel_id) + self.do_accept_inbound_channel(temporary_channel_id, counterparty_node_id, false, user_channel_id, vec![]) } /// Accepts a request to open a channel after a [`events::Event::OpenChannelRequest`], treating @@ -6695,11 +6707,59 @@ where /// [`Event::OpenChannelRequest`]: events::Event::OpenChannelRequest /// [`Event::ChannelClosed::user_channel_id`]: events::Event::ChannelClosed::user_channel_id pub fn accept_inbound_channel_from_trusted_peer_0conf(&self, temporary_channel_id: &ChannelId, counterparty_node_id: &PublicKey, user_channel_id: u128) -> Result<(), APIError> { - self.do_accept_inbound_channel(temporary_channel_id, counterparty_node_id, true, user_channel_id) + self.do_accept_inbound_channel(temporary_channel_id, counterparty_node_id, true, user_channel_id, vec![]) } - fn do_accept_inbound_channel(&self, temporary_channel_id: &ChannelId, counterparty_node_id: &PublicKey, accept_0conf: bool, user_channel_id: u128) -> Result<(), APIError> { + /// Accepts a request to open a dual-funded channel with a contribution provided by us after an + /// [`Event::OpenChannelRequest`]. + /// + /// The `temporary_channel_id` parameter indicates which inbound channel should be accepted, + /// and the `counterparty_node_id` parameter is the id of the peer which 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 the `txin`s along with their previous transactions that + /// will be used to contribute towards our portion of the channel value. Our contribution will be + /// calculated as the total value of these inputs minus the fees we need to cover for the + /// interactive funding transaction. + /// + /// 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://lists.linuxfoundation.org/pipermail/lightning-dev/2023-May/003920.html + /// + /// + /// [`Event::OpenChannelRequest`]: events::Event::OpenChannelRequest + /// [`Event::ChannelClosed::user_channel_id`]: events::Event::ChannelClosed::user_channel_id + #[cfg(any(dual_funding, splicing))] + pub fn accept_inbound_channel_with_contribution(&self, temporary_channel_id: &ChannelId, + counterparty_node_id: &PublicKey, user_channel_id: u128, funding_inputs: Vec<(TxIn, Transaction)>, + ) -> Result<(), APIError> { + let funding_inputs = Self::length_limit_holder_input_prev_txs(funding_inputs)?; + self.do_accept_inbound_channel(temporary_channel_id, counterparty_node_id, false, user_channel_id, + funding_inputs) + } + #[cfg(any(dual_funding, splicing))] + fn length_limit_holder_input_prev_txs(funding_inputs: Vec<(TxIn, Transaction)>) -> Result, APIError> { + funding_inputs.into_iter().map(|(txin, tx)| { + match TransactionU16LenLimited::new(tx) { + Ok(tx) => Ok((txin, tx)), + Err(err) => Err(err) + } + }).collect::, ()>>() + .map_err(|_| APIError::APIMisuseError { err: "One or more transactions had a serialized length exceeding 65535 bytes".into() }) + } + + // TODO(dual_funding): Remove param _-prefix once #[cfg(dual_funding)] is dropped. + fn do_accept_inbound_channel( + &self, temporary_channel_id: &ChannelId, counterparty_node_id: &PublicKey, accept_0conf: bool, + user_channel_id: u128, _funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, + ) -> Result<(), APIError> { let logger = WithContext::from(&self.logger, Some(*counterparty_node_id), Some(*temporary_channel_id), None); let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); @@ -6724,12 +6784,59 @@ where let res = match peer_state.inbound_channel_request_by_id.remove(temporary_channel_id) { Some(unaccepted_channel) => { let best_block_height = self.best_block.read().unwrap().height; - InboundV1Channel::new(&self.fee_estimator, &self.entropy_source, &self.signer_provider, - counterparty_node_id.clone(), &self.channel_type_features(), &peer_state.latest_features, - &unaccepted_channel.open_channel_msg, user_channel_id, &self.default_configuration, best_block_height, - &self.logger, accept_0conf).map_err(|err| MsgHandleErrInternal::from_chan_no_close(err, *temporary_channel_id)) + match unaccepted_channel.open_channel_msg { + OpenChannelMessage::V1(open_channel_msg) => { + InboundV1Channel::new( + &self.fee_estimator, &self.entropy_source, &self.signer_provider, *counterparty_node_id, + &self.channel_type_features(), &peer_state.latest_features, &open_channel_msg, + user_channel_id, &self.default_configuration, best_block_height, &self.logger, accept_0conf + ) + .map(|channel| ChannelPhase::UnfundedInboundV1(channel)) + .map_err(|err| MsgHandleErrInternal::from_chan_no_close(err, *temporary_channel_id)) + }, + #[cfg(any(dual_funding, splicing))] + OpenChannelMessage::V2(open_channel_msg) => { + let channel_res = InboundV2Channel::new(&self.fee_estimator, &self.entropy_source, &self.signer_provider, + counterparty_node_id.clone(), &self.channel_type_features(), &peer_state.latest_features, + &open_channel_msg, _funding_inputs, user_channel_id, &self.default_configuration, best_block_height, + &self.logger); + match channel_res { + Ok(mut channel) => { + let tx_msg_opt_res = channel.begin_interactive_funding_tx_construction(&self.entropy_source); + match tx_msg_opt_res { + Ok(tx_msg_opt) => { + if let Some(tx_msg) = tx_msg_opt { + let msg_send_event = match tx_msg { + InteractiveTxMessageSend::TxAddInput(msg) => events::MessageSendEvent::SendTxAddInput { + node_id: *counterparty_node_id, msg }, + InteractiveTxMessageSend::TxAddOutput(msg) => events::MessageSendEvent::SendTxAddOutput { + node_id: *counterparty_node_id, msg }, + InteractiveTxMessageSend::TxComplete(msg) => events::MessageSendEvent::SendTxComplete { + node_id: *counterparty_node_id, msg }, + }; + peer_state.pending_msg_events.push(msg_send_event); + } + Ok(ChannelPhase::UnfundedInboundV2(channel)) + }, + Err(_) => { + Err(MsgHandleErrInternal::from_chan_no_close(ChannelError::Close( + ( + "V2 channel rejected due to sender error".into(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + )), *temporary_channel_id)) + } + } + }, + Err(_) => Err(MsgHandleErrInternal::from_chan_no_close(ChannelError::Close( + ( + "V2 channel rejected due to sender error".into(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + )), *temporary_channel_id)), + } + }, + } }, - _ => { + None => { let err_str = "No such channel awaiting to be accepted.".to_owned(); log_error!(logger, "{}", err_str); @@ -6744,19 +6851,19 @@ where match handle_error!(self, Result::<(), MsgHandleErrInternal>::Err(err), *counterparty_node_id) { Ok(_) => unreachable!("`handle_error` only returns Err as we've passed in an Err"), Err(e) => { - return Err(APIError::ChannelUnavailable { err: e.err }); + Err(APIError::ChannelUnavailable { err: e.err }) }, } } - Ok(mut channel) => { + Ok(mut channel_phase) => { if accept_0conf { - // This should have been correctly configured by the call to InboundV1Channel::new. - debug_assert!(channel.context.minimum_depth().unwrap() == 0); - } else if channel.context.get_channel_type().requires_zero_conf() { + // This should have been correctly configured by the call to Inbound(V1/V2)Channel::new. + debug_assert!(channel_phase.context().minimum_depth().unwrap() == 0); + } else if channel_phase.context().get_channel_type().requires_zero_conf() { let send_msg_err_event = events::MessageSendEvent::HandleError { - node_id: channel.context.get_counterparty_node_id(), + node_id: channel_phase.context().get_counterparty_node_id(), action: msgs::ErrorAction::SendErrorMessage{ - msg: msgs::ErrorMessage { channel_id: temporary_channel_id.clone(), data: "No zero confirmation channels accepted".to_owned(), } + msg: msgs::ErrorMessage { channel_id: *temporary_channel_id, data: "No zero confirmation channels accepted".to_owned(), } } }; peer_state.pending_msg_events.push(send_msg_err_event); @@ -6770,9 +6877,9 @@ where // channels per-peer we can accept channels from a peer with existing ones. if is_only_peer_channel && peers_without_funded_channels >= MAX_UNFUNDED_CHANNEL_PEERS { let send_msg_err_event = events::MessageSendEvent::HandleError { - node_id: channel.context.get_counterparty_node_id(), + node_id: channel_phase.context().get_counterparty_node_id(), action: msgs::ErrorAction::SendErrorMessage{ - msg: msgs::ErrorMessage { channel_id: temporary_channel_id.clone(), data: "Have too many peers with unfunded channels, not accepting new ones".to_owned(), } + msg: msgs::ErrorMessage { channel_id: *temporary_channel_id, data: "Have too many peers with unfunded channels, not accepting new ones".to_owned(), } } }; peer_state.pending_msg_events.push(send_msg_err_event); @@ -6785,20 +6892,69 @@ where // Now that we know we have a channel, assign an outbound SCID alias. let outbound_scid_alias = self.create_and_insert_outbound_scid_alias(); - channel.context.set_outbound_scid_alias(outbound_scid_alias); - - peer_state.pending_msg_events.push(events::MessageSendEvent::SendAcceptChannel { - node_id: channel.context.get_counterparty_node_id(), - msg: channel.accept_inbound_channel(), - }); + channel_phase.context_mut().set_outbound_scid_alias(outbound_scid_alias); - peer_state.channel_by_id.insert(temporary_channel_id.clone(), ChannelPhase::UnfundedInboundV1(channel)); + match channel_phase { + ChannelPhase::UnfundedInboundV1(mut channel) => { + peer_state.pending_msg_events.push(events::MessageSendEvent::SendAcceptChannel { + node_id: channel.context.get_counterparty_node_id(), + msg: channel.accept_inbound_channel() }); + peer_state.channel_by_id.insert(*temporary_channel_id, + ChannelPhase::UnfundedInboundV1(channel)); + }, + #[cfg(any(dual_funding, splicing))] + ChannelPhase::UnfundedInboundV2(mut channel) => { + peer_state.pending_msg_events.push(events::MessageSendEvent::SendAcceptChannelV2 { + node_id: channel.context.get_counterparty_node_id(), + msg: channel.accept_inbound_dual_funded_channel() }); + peer_state.channel_by_id.insert(channel.context.channel_id(), + ChannelPhase::UnfundedInboundV2(channel)); + }, + _ => { + debug_assert!(false); + // This should be unreachable, but if it is then we would have dropped the inbound channel request + // and there'd be nothing to clean up as we haven't added anything to the channel_by_id map yet. + return Err(APIError::APIMisuseError { + err: "Channel somehow changed to a non-inbound channel before accepting".to_owned() }) + } + } Ok(()) }, } } + /// Checks related to inputs and their amounts related to establishing dual-funded channels. + #[cfg(any(dual_funding, splicing))] + fn dual_funding_amount_checks(funding_satoshis: u64, funding_inputs: &Vec<(TxIn, Transaction)>) + -> Result<(), APIError> { + if funding_satoshis < 1000 { + return Err(APIError::APIMisuseError { + err: format!("Funding amount must be at least 1000 satoshis. It was {} sats", funding_satoshis), + }); + } + + // Check that vouts exist for each TxIn in provided transactions. + for (idx, input) in funding_inputs.iter().enumerate() { + if input.1.output.get(input.0.previous_output.vout as usize).is_none() { + return Err(APIError::APIMisuseError { + err: format!("Transaction with txid {} does not have an output with vout of {} corresponding to TxIn at funding_inputs[{}]", + input.1.txid(), input.0.previous_output.vout, idx), + }); + } + } + + let total_input_satoshis: u64 = funding_inputs.iter().map( + |input| input.1.output[input.0.previous_output.vout as usize].value.to_sat()).sum(); + if total_input_satoshis < funding_satoshis { + Err(APIError::APIMisuseError { + err: format!("Total value of funding inputs must be at least funding amount. It was {} sats", + total_input_satoshis) }) + } else { + Ok(()) + } + } + /// Gets the number of peers which match the given filter and do not have any funded, outbound, /// or 0-conf channels. /// @@ -6867,17 +7023,25 @@ where num_unfunded_channels + peer.inbound_channel_request_by_id.len() } - fn internal_open_channel(&self, counterparty_node_id: &PublicKey, msg: &msgs::OpenChannel) -> Result<(), MsgHandleErrInternal> { + fn internal_open_channel(&self, counterparty_node_id: &PublicKey, msg: OpenChannelMessage) -> Result<(), MsgHandleErrInternal> { + let (chain_hash, temporary_channel_id) = match msg { + OpenChannelMessage::V1(ref msg) => (msg.common_fields.chain_hash, msg.common_fields.temporary_channel_id), + #[cfg(any(dual_funding, splicing))] + OpenChannelMessage::V2(ref msg) => (msg.common_fields.chain_hash, msg.common_fields.temporary_channel_id), + }; + + // Do common open_channel(2) checks + // Note that the ChannelManager is NOT re-persisted on disk after this, so any changes are // likely to be lost on restart! - if msg.common_fields.chain_hash != self.chain_hash { + if chain_hash != self.chain_hash { return Err(MsgHandleErrInternal::send_err_msg_no_close("Unknown genesis block hash".to_owned(), - msg.common_fields.temporary_channel_id.clone())); + temporary_channel_id)); } if !self.default_configuration.accept_inbound_channels { return Err(MsgHandleErrInternal::send_err_msg_no_close("No inbound channels accepted".to_owned(), - msg.common_fields.temporary_channel_id.clone())); + temporary_channel_id)); } // Get the number of peers with channels, but without funded ones. We don't care too much @@ -6892,7 +7056,7 @@ where debug_assert!(false); MsgHandleErrInternal::send_err_msg_no_close( format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id), - msg.common_fields.temporary_channel_id.clone()) + temporary_channel_id.clone()) })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; @@ -6906,80 +7070,133 @@ where { return Err(MsgHandleErrInternal::send_err_msg_no_close( "Have too many peers with unfunded channels, not accepting new ones".to_owned(), - msg.common_fields.temporary_channel_id.clone())); + temporary_channel_id.clone())); } let best_block_height = self.best_block.read().unwrap().height; if Self::unfunded_channel_count(peer_state, best_block_height) >= MAX_UNFUNDED_CHANS_PER_PEER { return Err(MsgHandleErrInternal::send_err_msg_no_close( format!("Refusing more than {} unfunded channels.", MAX_UNFUNDED_CHANS_PER_PEER), - msg.common_fields.temporary_channel_id.clone())); + temporary_channel_id.clone())); } - let channel_id = msg.common_fields.temporary_channel_id; + let channel_id = temporary_channel_id; let channel_exists = peer_state.has_channel(&channel_id); if channel_exists { return Err(MsgHandleErrInternal::send_err_msg_no_close( "temporary_channel_id collision for the same peer!".to_owned(), - msg.common_fields.temporary_channel_id.clone())); - } + temporary_channel_id.clone())); + } + + // Version-specific checks and logic + match msg { + OpenChannelMessage::V1(ref msg) => { + // If we're doing manual acceptance checks on the channel, then defer creation until we're sure we want to accept. + if self.default_configuration.manually_accept_inbound_channels { + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back((events::Event::OpenChannelRequest { + temporary_channel_id: msg.common_fields.temporary_channel_id.clone(), + counterparty_node_id: counterparty_node_id.clone(), + funding_satoshis: msg.common_fields.funding_satoshis, + acceptor_funds: InboundChannelFunds::PushMsat(msg.push_msat), + channel_type: msg.common_fields.channel_type.clone().unwrap(), + }, None)); + peer_state.inbound_channel_request_by_id.insert(temporary_channel_id, InboundChannelRequest { + open_channel_msg: OpenChannelMessage::V1(msg.clone()), + ticks_remaining: UNACCEPTED_INBOUND_CHANNEL_AGE_LIMIT_TICKS, + }); + return Ok(()); + } - // If we're doing manual acceptance checks on the channel, then defer creation until we're sure we want to accept. - if self.default_configuration.manually_accept_inbound_channels { - let channel_type = channel::channel_type_from_open_channel( - &msg.common_fields, &peer_state.latest_features, &self.channel_type_features() - ).map_err(|e| - MsgHandleErrInternal::from_chan_no_close(e, msg.common_fields.temporary_channel_id) - )?; - let mut pending_events = self.pending_events.lock().unwrap(); - pending_events.push_back((events::Event::OpenChannelRequest { - temporary_channel_id: msg.common_fields.temporary_channel_id.clone(), - counterparty_node_id: counterparty_node_id.clone(), - funding_satoshis: msg.common_fields.funding_satoshis, - push_msat: msg.push_msat, - channel_type, - }, None)); - peer_state.inbound_channel_request_by_id.insert(channel_id, InboundChannelRequest { - open_channel_msg: msg.clone(), - ticks_remaining: UNACCEPTED_INBOUND_CHANNEL_AGE_LIMIT_TICKS, - }); - return Ok(()); - } + // Otherwise create the channel right now. + let mut random_bytes = [0u8; 16]; + random_bytes.copy_from_slice(&self.entropy_source.get_secure_random_bytes()[..16]); + let user_channel_id = u128::from_be_bytes(random_bytes); + let mut channel = match InboundV1Channel::new( + &self.fee_estimator, &self.entropy_source, &self.signer_provider, counterparty_node_id.clone(), + &self.channel_type_features(), &peer_state.latest_features, &msg, user_channel_id, + &self.default_configuration, best_block_height, &self.logger, /*is_0conf=*/false + ) { + Err(e) => { + return Err(MsgHandleErrInternal::from_chan_no_close(e, msg.common_fields.temporary_channel_id)); + }, + Ok(res) => res + }; - // Otherwise create the channel right now. - let mut random_bytes = [0u8; 16]; - random_bytes.copy_from_slice(&self.entropy_source.get_secure_random_bytes()[..16]); - let user_channel_id = u128::from_be_bytes(random_bytes); - let mut channel = match InboundV1Channel::new(&self.fee_estimator, &self.entropy_source, &self.signer_provider, - counterparty_node_id.clone(), &self.channel_type_features(), &peer_state.latest_features, msg, user_channel_id, - &self.default_configuration, best_block_height, &self.logger, /*is_0conf=*/false) - { - Err(e) => { - return Err(MsgHandleErrInternal::from_chan_no_close(e, msg.common_fields.temporary_channel_id)); + let channel_type = channel.context.get_channel_type(); + if channel_type.requires_zero_conf() { + return Err(MsgHandleErrInternal::send_err_msg_no_close("No zero confirmation channels accepted".to_owned(), msg.common_fields.temporary_channel_id.clone())); + } + if channel_type.requires_anchors_zero_fee_htlc_tx() { + return Err(MsgHandleErrInternal::send_err_msg_no_close("No channels with anchor outputs accepted".to_owned(), msg.common_fields.temporary_channel_id.clone())); + } + + let outbound_scid_alias = self.create_and_insert_outbound_scid_alias(); + channel.context.set_outbound_scid_alias(outbound_scid_alias); + + peer_state.pending_msg_events.push(events::MessageSendEvent::SendAcceptChannel { + node_id: counterparty_node_id.clone(), + msg: channel.accept_inbound_channel(), + }); + peer_state.channel_by_id.insert(temporary_channel_id, ChannelPhase::UnfundedInboundV1(channel)); }, - Ok(res) => res - }; + #[cfg(any(dual_funding, splicing))] + OpenChannelMessage::V2(ref msg) => { + // If we're doing manual acceptance checks on the channel, then defer creation until we're sure + // we want to accept and, optionally, contribute to the channel value. + if self.default_configuration.manually_accept_inbound_channels { + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back((events::Event::OpenChannelRequest { + temporary_channel_id: msg.common_fields.temporary_channel_id.clone(), + counterparty_node_id: counterparty_node_id.clone(), + funding_satoshis: msg.common_fields.funding_satoshis, + acceptor_funds: InboundChannelFunds::DualFunded, + channel_type: msg.common_fields.channel_type.clone().unwrap(), + }, None)); + peer_state.inbound_channel_request_by_id.insert(temporary_channel_id, InboundChannelRequest { + open_channel_msg: OpenChannelMessage::V2(msg.clone()), + ticks_remaining: UNACCEPTED_INBOUND_CHANNEL_AGE_LIMIT_TICKS, + }); + return Ok(()); + } - let channel_type = channel.context.get_channel_type(); - if channel_type.requires_zero_conf() { - return Err(MsgHandleErrInternal::send_err_msg_no_close( - "No zero confirmation channels accepted".to_owned(), - msg.common_fields.temporary_channel_id.clone())); - } - if channel_type.requires_anchors_zero_fee_htlc_tx() { - return Err(MsgHandleErrInternal::send_err_msg_no_close( - "No channels with anchor outputs accepted".to_owned(), - msg.common_fields.temporary_channel_id.clone())); - } + // Otherwise create the channel right now. + let mut random_bytes = [0u8; 16]; + random_bytes.copy_from_slice(&self.entropy_source.get_secure_random_bytes()[..16]); + let user_channel_id = u128::from_be_bytes(random_bytes); + let mut channel = match InboundV2Channel::new(&self.fee_estimator, &self.entropy_source, + &self.signer_provider, counterparty_node_id.clone(), &self.channel_type_features(), + &peer_state.latest_features, &msg, vec![], user_channel_id, &self.default_configuration, + best_block_height, &self.logger) + { + Err(e) => { + return Err(MsgHandleErrInternal::from_chan_no_close(e, msg.common_fields.temporary_channel_id)); + }, + Ok(res) => res + }; - let outbound_scid_alias = self.create_and_insert_outbound_scid_alias(); - channel.context.set_outbound_scid_alias(outbound_scid_alias); + let channel_type = channel.context.get_channel_type(); + if channel_type.requires_zero_conf() { + return Err(MsgHandleErrInternal::send_err_msg_no_close("No zero confirmation channels accepted".to_owned(), msg.common_fields.temporary_channel_id.clone())); + } + if channel_type.requires_anchors_zero_fee_htlc_tx() { + return Err(MsgHandleErrInternal::send_err_msg_no_close("No channels with anchor outputs accepted".to_owned(), msg.common_fields.temporary_channel_id.clone())); + } - peer_state.pending_msg_events.push(events::MessageSendEvent::SendAcceptChannel { - node_id: counterparty_node_id.clone(), - msg: channel.accept_inbound_channel(), - }); - peer_state.channel_by_id.insert(channel_id, ChannelPhase::UnfundedInboundV1(channel)); + let outbound_scid_alias = self.create_and_insert_outbound_scid_alias(); + channel.context.set_outbound_scid_alias(outbound_scid_alias); + + channel.begin_interactive_funding_tx_construction(&self.entropy_source) + .map_err(|_| MsgHandleErrInternal::send_err_msg_no_close( + "Failed to start interactive transaction construction".to_owned(), msg.common_fields.temporary_channel_id))?; + + peer_state.pending_msg_events.push(events::MessageSendEvent::SendAcceptChannelV2 { + node_id: counterparty_node_id.clone(), + msg: channel.accept_inbound_dual_funded_channel(), + }); + peer_state.channel_by_id.insert(channel.context.channel_id(), ChannelPhase::UnfundedInboundV2(channel)); + }, + } Ok(()) } @@ -7179,6 +7396,216 @@ where } } + #[cfg(any(dual_funding, splicing))] + fn internal_tx_add_input(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxAddInput) -> Result<(), MsgHandleErrInternal> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id) + .ok_or_else(|| { + debug_assert!(false); + MsgHandleErrInternal::send_err_msg_no_close( + format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id), + msg.channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Occupied(mut chan_phase_entry) => { + let channel_phase = chan_phase_entry.get_mut(); + let msg_send_event = match channel_phase { + ChannelPhase::UnfundedInboundV2(ref mut channel) => { + channel.tx_add_input(msg).into_msg_send_event(counterparty_node_id) + }, + ChannelPhase::UnfundedOutboundV2(ref mut channel) => { + channel.tx_add_input(msg).into_msg_send_event(counterparty_node_id) + }, + _ => try_chan_phase_entry!(self, Err(ChannelError::Warn( + "Got a tx_add_input message with no interactive transaction construction expected or in-progress" + .into())), chan_phase_entry) + }; + peer_state.pending_msg_events.push(msg_send_event); + Ok(()) + }, + hash_map::Entry::Vacant(_) => { + Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + } + } + } + + #[cfg(any(dual_funding, splicing))] + fn internal_tx_add_output(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxAddOutput) -> Result<(), MsgHandleErrInternal> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id) + .ok_or_else(|| { + debug_assert!(false); + MsgHandleErrInternal::send_err_msg_no_close( + format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id), + msg.channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Occupied(mut chan_phase_entry) => { + let channel_phase = chan_phase_entry.get_mut(); + let msg_send_event = match channel_phase { + ChannelPhase::UnfundedInboundV2(ref mut channel) => { + channel.tx_add_output(msg).into_msg_send_event(counterparty_node_id) + }, + ChannelPhase::UnfundedOutboundV2(ref mut channel) => { + channel.tx_add_output(msg).into_msg_send_event(counterparty_node_id) + }, + _ => try_chan_phase_entry!(self, Err(ChannelError::Warn( + "Got a tx_add_output message with no interactive transaction construction expected or in-progress" + .into())), chan_phase_entry) + }; + peer_state.pending_msg_events.push(msg_send_event); + Ok(()) + }, + hash_map::Entry::Vacant(_) => { + Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + } + } + } + + #[cfg(any(dual_funding, splicing))] + fn internal_tx_remove_input(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxRemoveInput) -> Result<(), MsgHandleErrInternal> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id) + .ok_or_else(|| { + debug_assert!(false); + MsgHandleErrInternal::send_err_msg_no_close( + format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id), + msg.channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Occupied(mut chan_phase_entry) => { + let channel_phase = chan_phase_entry.get_mut(); + let msg_send_event = match channel_phase { + ChannelPhase::UnfundedInboundV2(ref mut channel) => { + channel.tx_remove_input(msg).into_msg_send_event(counterparty_node_id) + }, + ChannelPhase::UnfundedOutboundV2(ref mut channel) => { + channel.tx_remove_input(msg).into_msg_send_event(counterparty_node_id) + }, + _ => try_chan_phase_entry!(self, Err(ChannelError::Warn( + "Got a tx_remove_input message with no interactive transaction construction expected or in-progress" + .into())), chan_phase_entry) + }; + peer_state.pending_msg_events.push(msg_send_event); + Ok(()) + }, + hash_map::Entry::Vacant(_) => { + Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + } + } + } + + #[cfg(any(dual_funding, splicing))] + fn internal_tx_remove_output(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxRemoveOutput) -> Result<(), MsgHandleErrInternal> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id) + .ok_or_else(|| { + debug_assert!(false); + MsgHandleErrInternal::send_err_msg_no_close( + format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id), + msg.channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Occupied(mut chan_phase_entry) => { + let channel_phase = chan_phase_entry.get_mut(); + let msg_send_event = match channel_phase { + ChannelPhase::UnfundedInboundV2(ref mut channel) => { + channel.tx_remove_output(msg).into_msg_send_event(counterparty_node_id) + }, + ChannelPhase::UnfundedOutboundV2(ref mut channel) => { + channel.tx_remove_output(msg).into_msg_send_event(counterparty_node_id) + }, + _ => try_chan_phase_entry!(self, Err(ChannelError::Warn( + "Got a tx_remove_output message with no interactive transaction construction expected or in-progress" + .into())), chan_phase_entry) + }; + peer_state.pending_msg_events.push(msg_send_event); + Ok(()) + }, + hash_map::Entry::Vacant(_) => { + Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + } + } + } + + #[cfg(any(dual_funding, splicing))] + fn internal_tx_complete(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxComplete) -> Result<(), MsgHandleErrInternal> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id) + .ok_or_else(|| { + debug_assert!(false); + MsgHandleErrInternal::send_err_msg_no_close( + format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id), + msg.channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Occupied(mut chan_phase_entry) => { + let channel_phase = chan_phase_entry.get_mut(); + let (msg_send_event_opt, tx_opt) = match channel_phase { + ChannelPhase::UnfundedInboundV2(channel) => { + channel.tx_complete(msg).into_msg_send_event(counterparty_node_id) + }, + ChannelPhase::UnfundedOutboundV2(channel) => { + channel.tx_complete(msg).into_msg_send_event(counterparty_node_id) + }, + _ => try_chan_phase_entry!(self, Err(ChannelError::Close( + ( + "Got a tx_complete message with no interactive transaction construction expected or in-progress".into(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + ))), chan_phase_entry) + }; + if let Some(msg_send_event) = msg_send_event_opt { + peer_state.pending_msg_events.push(msg_send_event); + } + if let Some(tx) = tx_opt { + // TODO(dual_funding): Handle this unsigned transaction. + } + Ok(()) + }, + hash_map::Entry::Vacant(_) => { + Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + } + } + } + + #[cfg(any(dual_funding, splicing))] + fn internal_tx_signatures(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxSignatures) { + let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( + "Dual-funded channels not supported".to_owned(), + msg.channel_id.clone())), *counterparty_node_id); + } + + #[cfg(any(dual_funding, splicing))] + fn internal_tx_init_rbf(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxInitRbf) { + let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( + "Dual-funded channels not supported".to_owned(), + msg.channel_id.clone())), *counterparty_node_id); + } + + #[cfg(any(dual_funding, splicing))] + fn internal_tx_ack_rbf(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxAckRbf) { + let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( + "Dual-funded channels not supported".to_owned(), + msg.channel_id.clone())), *counterparty_node_id); + } + + #[cfg(any(dual_funding, splicing))] + fn internal_tx_abort(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxAbort) { + let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( + "Dual-funded channels not supported".to_owned(), + msg.channel_id.clone())), *counterparty_node_id); + } + fn internal_channel_ready(&self, counterparty_node_id: &PublicKey, msg: &msgs::ChannelReady) -> Result<(), MsgHandleErrInternal> { // Note that the ChannelManager is NOT re-persisted on disk after this (unless we error // closing a channel), so any changes are likely to be lost on restart! @@ -9571,7 +9998,7 @@ where // open_channel message - pre-funded channels are never written so there should be no // change to the contents. let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { - let res = self.internal_open_channel(counterparty_node_id, msg); + let res = self.internal_open_channel(counterparty_node_id, OpenChannelMessage::V1(msg.clone())); let persist = match &res { Err(e) if e.closes_channel() => { debug_assert!(false, "We shouldn't close a new channel"); @@ -9585,9 +10012,30 @@ where } fn handle_open_channel_v2(&self, counterparty_node_id: &PublicKey, msg: &msgs::OpenChannelV2) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Dual-funded channels not supported".to_owned(), - msg.common_fields.temporary_channel_id.clone())), *counterparty_node_id); + #[cfg(any(dual_funding, splicing))] + { + // Note that we never need to persist the updated ChannelManager for an inbound + // open_channel message - pre-funded channels are never written so there should be no + // change to the contents. + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let res = self.internal_open_channel(counterparty_node_id, OpenChannelMessage::V2(msg.clone())); + let persist = match &res { + Err(e) if e.closes_channel() => { + debug_assert!(false, "We shouldn't close a new channel"); + NotifyOption::DoPersist + }, + _ => NotifyOption::SkipPersistHandleEvents, + }; + let _ = handle_error!(self, res, *counterparty_node_id); + persist + }); + }; + #[cfg(not(any(dual_funding, splicing)))] + { + let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( + "Dual-funded channels not supported".to_owned(), + msg.common_fields.temporary_channel_id.clone())), *counterparty_node_id); + }; } fn handle_accept_channel(&self, counterparty_node_id: &PublicKey, msg: &msgs::AcceptChannel) { @@ -10132,33 +10580,98 @@ where } fn handle_tx_add_input(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxAddInput) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Dual-funded channels not supported".to_owned(), - msg.channel_id.clone())), *counterparty_node_id); + #[cfg(any(dual_funding, splicing))] + { + // Note that we never need to persist the updated ChannelManager for an inbound + // tx_add_input message - interactive transaction construction does not need to + // be persisted before any signatures are exchanged. + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let _ = handle_error!(self, self.internal_tx_add_input(counterparty_node_id, msg), *counterparty_node_id); + NotifyOption::SkipPersistHandleEvents + }); + }; + #[cfg(not(any(dual_funding, splicing)))] + { + let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( + "Dual-funded channels not supported".to_owned(), + msg.channel_id.clone())), *counterparty_node_id); + }; } fn handle_tx_add_output(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxAddOutput) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Dual-funded channels not supported".to_owned(), - msg.channel_id.clone())), *counterparty_node_id); + #[cfg(any(dual_funding, splicing))] + { + // Note that we never need to persist the updated ChannelManager for an inbound + // tx_add_input message - interactive transaction construction does not need to + // be persisted before any signatures are exchanged. + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let _ = handle_error!(self, self.internal_tx_add_output(counterparty_node_id, msg), *counterparty_node_id); + NotifyOption::SkipPersistHandleEvents + }); + }; + #[cfg(not(any(dual_funding, splicing)))] + { + let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( + "Dual-funded channels not supported".to_owned(), + msg.channel_id.clone())), *counterparty_node_id); + }; } fn handle_tx_remove_input(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxRemoveInput) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Dual-funded channels not supported".to_owned(), - msg.channel_id.clone())), *counterparty_node_id); + #[cfg(any(dual_funding, splicing))] + { + // Note that we never need to persist the updated ChannelManager for an inbound + // tx_add_input message - interactive transaction construction does not need to + // be persisted before any signatures are exchanged. + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let _ = handle_error!(self, self.internal_tx_remove_input(counterparty_node_id, msg), *counterparty_node_id); + NotifyOption::SkipPersistHandleEvents + }); + }; + #[cfg(not(any(dual_funding, splicing)))] + { + let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( + "Dual-funded channels not supported".to_owned(), + msg.channel_id.clone())), *counterparty_node_id); + }; } fn handle_tx_remove_output(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxRemoveOutput) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Dual-funded channels not supported".to_owned(), - msg.channel_id.clone())), *counterparty_node_id); + #[cfg(any(dual_funding, splicing))] + { + // Note that we never need to persist the updated ChannelManager for an inbound + // tx_add_input message - interactive transaction construction does not need to + // be persisted before any signatures are exchanged. + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let _ = handle_error!(self, self.internal_tx_remove_output(counterparty_node_id, msg), *counterparty_node_id); + NotifyOption::SkipPersistHandleEvents + }); + }; + #[cfg(not(any(dual_funding, splicing)))] + { + let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( + "Dual-funded channels not supported".to_owned(), + msg.channel_id.clone())), *counterparty_node_id); + }; } fn handle_tx_complete(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxComplete) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Dual-funded channels not supported".to_owned(), - msg.channel_id.clone())), *counterparty_node_id); + #[cfg(any(dual_funding, splicing))] + { + // Note that we never need to persist the updated ChannelManager for an inbound + // tx_add_input message - interactive transaction construction does not need to + // be persisted before any signatures are exchanged. + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let _ = handle_error!(self, self.internal_tx_complete(counterparty_node_id, msg), *counterparty_node_id); + NotifyOption::SkipPersistHandleEvents + }); + }; + #[cfg(not(any(dual_funding, splicing)))] + { + let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( + "Dual-funded channels not supported".to_owned(), + msg.channel_id.clone())), *counterparty_node_id); + }; } fn handle_tx_signatures(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxSignatures) { diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 17c69905086..ce153420c87 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -15,6 +15,7 @@ use bitcoin::amount::Amount; use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; use bitcoin::consensus::Encodable; use bitcoin::policy::MAX_STANDARD_TX_WEIGHT; +use bitcoin::secp256k1::PublicKey; use bitcoin::transaction::Version; use bitcoin::{ absolute::LockTime as AbsoluteLockTime, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, @@ -23,6 +24,7 @@ use bitcoin::{ use crate::chain::chaininterface::fee_for_weight; use crate::events::bump_transaction::{BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT}; +use crate::events::MessageSendEvent; use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; use crate::ln::msgs; use crate::ln::msgs::SerialId; @@ -44,7 +46,7 @@ const MAX_INPUTS_OUTPUTS_COUNT: usize = 252; /// The total weight of the common fields whose fee is paid by the initiator of the interactive /// transaction construction protocol. -const TX_COMMON_FIELDS_WEIGHT: u64 = (4 /* version */ + 4 /* locktime */ + 1 /* input count */ + +pub(crate) const TX_COMMON_FIELDS_WEIGHT: u64 = (4 /* version */ + 4 /* locktime */ + 1 /* input count */ + 1 /* output count */) * WITNESS_SCALE_FACTOR as u64 + 2 /* segwit marker + flag */; // BOLT 3 - Lower bounds for input weights @@ -101,6 +103,39 @@ pub(crate) enum AbortReason { InvalidTx, } +impl AbortReason { + pub fn into_tx_abort_msg(self, channel_id: ChannelId) -> msgs::TxAbort { + let msg = match self { + AbortReason::InvalidStateTransition => "State transition was invalid", + AbortReason::UnexpectedCounterpartyMessage => "Unexpected message", + AbortReason::ReceivedTooManyTxAddInputs => "Too many `tx_add_input`s received", + AbortReason::ReceivedTooManyTxAddOutputs => "Too many `tx_add_output`s received", + AbortReason::IncorrectInputSequenceValue => { + "Input has a sequence value greater than 0xFFFFFFFD" + }, + AbortReason::IncorrectSerialIdParity => "Parity for `serial_id` was incorrect", + AbortReason::SerialIdUnknown => "The `serial_id` is unknown", + AbortReason::DuplicateSerialId => "The `serial_id` already exists", + AbortReason::PrevTxOutInvalid => "Invalid previous transaction output", + AbortReason::ExceededMaximumSatsAllowed => { + "Output amount exceeded total bitcoin supply" + }, + AbortReason::ExceededNumberOfInputsOrOutputs => "Too many inputs or outputs", + AbortReason::TransactionTooLarge => "Transaction weight is too large", + AbortReason::BelowDustLimit => "Output amount is below the dust limit", + AbortReason::InvalidOutputScript => "The output script is non-standard", + AbortReason::InsufficientFees => "Insufficient fees paid", + AbortReason::OutputsValueExceedsInputsValue => { + "Total value of outputs exceeds total value of inputs" + }, + AbortReason::InvalidTx => "The transaction is invalid", + } + .to_string(); + + msgs::TxAbort { channel_id, data: msg.into_bytes() } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct InteractiveTxInput { serial_id: SerialId, @@ -844,6 +879,39 @@ pub(crate) enum InteractiveTxMessageSend { TxComplete(msgs::TxComplete), } +impl InteractiveTxMessageSend { + pub fn into_msg_send_event(self, counterparty_node_id: &PublicKey) -> MessageSendEvent { + match self { + InteractiveTxMessageSend::TxAddInput(msg) => { + MessageSendEvent::SendTxAddInput { node_id: *counterparty_node_id, msg } + }, + InteractiveTxMessageSend::TxAddOutput(msg) => { + MessageSendEvent::SendTxAddOutput { node_id: *counterparty_node_id, msg } + }, + InteractiveTxMessageSend::TxComplete(msg) => { + MessageSendEvent::SendTxComplete { node_id: *counterparty_node_id, msg } + }, + } + } +} + +pub(super) struct InteractiveTxMessageSendResult( + pub Result, +); + +impl InteractiveTxMessageSendResult { + pub fn into_msg_send_event(self, counterparty_node_id: &PublicKey) -> MessageSendEvent { + match self.0 { + Ok(interactive_tx_msg_send) => { + interactive_tx_msg_send.into_msg_send_event(counterparty_node_id) + }, + Err(tx_abort_msg) => { + MessageSendEvent::SendTxAbort { node_id: *counterparty_node_id, msg: tx_abort_msg } + }, + } + } +} + // This macro executes a state machine transition based on a provided action. macro_rules! do_state_transition { ($self: ident, $transition: ident, $msg: expr) => {{ @@ -876,6 +944,32 @@ pub(crate) enum HandleTxCompleteValue { NegotiationComplete(ConstructedTransaction), } +pub(super) struct HandleTxCompleteResult(pub Result); + +impl HandleTxCompleteResult { + pub fn into_msg_send_event( + self, counterparty_node_id: &PublicKey, + ) -> (Option, Option) { + match self.0 { + Ok(tx_complete_res) => { + let (tx_msg_opt, tx_opt) = match tx_complete_res { + HandleTxCompleteValue::SendTxMessage(msg) => (Some(msg), None), + HandleTxCompleteValue::SendTxComplete(msg, tx) => (Some(msg), Some(tx)), + HandleTxCompleteValue::NegotiationComplete(tx) => (None, Some(tx)), + }; + (tx_msg_opt.map(|tx_msg| tx_msg.into_msg_send_event(counterparty_node_id)), tx_opt) + }, + Err(tx_abort_msg) => ( + Some(MessageSendEvent::SendTxAbort { + node_id: *counterparty_node_id, + msg: tx_abort_msg, + }), + None, + ), + } + } +} + impl InteractiveTxConstructor { /// Instantiates a new `InteractiveTxConstructor`. /// diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs index 4e124c27fd9..aa10c46c9ca 100644 --- a/lightning/src/util/config.rs +++ b/lightning/src/util/config.rs @@ -828,11 +828,20 @@ pub struct UserConfig { /// [`msgs::AcceptChannel`] message will not be sent back to the counterparty node unless the /// user explicitly chooses to accept the request. /// + // TODO(dual_funding): Make these part of doc comments when #[cfg(dual_funding)] is dropped. + // To be able to contribute to inbound dual-funded channels, this field must be set to true. + // A corresponding [`msgs::AcceptChannelV2`] message will not be sent back to the counterparty + // node until the user explicitly chooses to accept the request, optionally contributing funds + // to it. + /// /// Default value: `false` /// /// [`Event::OpenChannelRequest`]: crate::events::Event::OpenChannelRequest /// [`msgs::OpenChannel`]: crate::ln::msgs::OpenChannel /// [`msgs::AcceptChannel`]: crate::ln::msgs::AcceptChannel + // TODO(dual_funding): Make these part of doc comments when #[cfg(dual_funding)] is dropped. + // [`msgs::OpenChannelV2`]: crate::ln::msgs::OpenChannelV2 + // [`msgs::AcceptChannelV2`]: crate::ln::msgs::AcceptChannelV2 pub manually_accept_inbound_channels: bool, /// If this is set to `true`, LDK will intercept HTLCs that are attempting to be forwarded over /// fake short channel ids generated via [`ChannelManager::get_intercept_scid`]. Upon HTLC From dac491f38133812b6ac31abf9279efe44d600019 Mon Sep 17 00:00:00 2001 From: Duncan Dean Date: Thu, 19 Oct 2023 15:47:33 +0200 Subject: [PATCH 3/6] Handle initial commitment_signed for V2 channels --- lightning/src/ln/channel.rs | 284 +++++++++++++++++++++++- lightning/src/ln/channelmanager.rs | 332 +++++++++++++++++++++++++---- lightning/src/ln/interactivetxs.rs | 20 +- 3 files changed, 589 insertions(+), 47 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index f071b0e0fcd..4bc19e4f985 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -31,7 +31,7 @@ use bitcoin::locktime::absolute::LockTime; use crate::ln::types::{ChannelId, PaymentPreimage, PaymentHash}; use crate::ln::features::{ChannelTypeFeatures, InitFeatures}; #[cfg(any(dual_funding, splicing))] -use crate::ln::interactivetxs::{estimate_input_weight, get_output_weight, HandleTxCompleteResult, InteractiveTxConstructor, InteractiveTxMessageSend, InteractiveTxMessageSendResult, TX_COMMON_FIELDS_WEIGHT}; +use crate::ln::interactivetxs::{ConstructedTransaction, estimate_input_weight, get_output_weight, HandleTxCompleteResult, InteractiveTxConstructor, InteractiveTxMessageSend, InteractiveTxMessageSendResult, TX_COMMON_FIELDS_WEIGHT}; use crate::ln::msgs; use crate::ln::msgs::DecodeError; use crate::ln::script::{self, ShutdownScript}; @@ -3825,7 +3825,7 @@ 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. - pub their_funding_satoshis: u64, + 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, @@ -4618,6 +4618,106 @@ impl Channel where Ok(()) } + #[cfg(any(dual_funding, splicing))] + pub fn commitment_signed_initial_v2( + &mut self, msg: &msgs::CommitmentSigned, best_block: BestBlock, signer_provider: &SP, logger: &L + ) -> Result::EcdsaSigner>, ChannelError> + where L::Target: Logger + { + if !matches!(self.context.channel_state, ChannelState::FundingNegotiated) { + return Err(ChannelError::Close( + ( + "Received initial commitment_signed before funding transaction constructed!".to_owned(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + ))); + } + if self.context.commitment_secrets.get_min_seen_secret() != (1 << 48) || + self.context.cur_counterparty_commitment_transaction_number != INITIAL_COMMITMENT_NUMBER || + self.context.holder_commitment_point.transaction_number() != INITIAL_COMMITMENT_NUMBER { + panic!("Should not have advanced channel commitment tx numbers prior to funding_created"); + } + let dual_funding_channel_context = self.dual_funding_channel_context.as_mut().ok_or( + ChannelError::Close(("Have no context for dual-funded channel".to_owned(), ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) })) + )?; + + let funding_script = self.context.get_funding_redeemscript(); + + let counterparty_keys = self.context.build_remote_transaction_keys(); + let counterparty_initial_commitment_tx = self.context.build_commitment_transaction(self.context.cur_counterparty_commitment_transaction_number, &counterparty_keys, false, false, logger).tx; + let counterparty_trusted_tx = counterparty_initial_commitment_tx.trust(); + let counterparty_initial_bitcoin_tx = counterparty_trusted_tx.built_transaction(); + + log_trace!(logger, "Initial counterparty tx for channel {} is: txid {} tx {}", + &self.context.channel_id(), counterparty_initial_bitcoin_tx.txid, encode::serialize_hex(&counterparty_initial_bitcoin_tx.transaction)); + + let holder_signer = self.context.build_holder_transaction_keys(); + let initial_commitment_tx = self.context.build_commitment_transaction( + self.context.holder_commitment_point.transaction_number(), &holder_signer, true, false, logger + ).tx; + { + let trusted_tx = initial_commitment_tx.trust(); + let initial_commitment_bitcoin_tx = trusted_tx.built_transaction(); + let sighash = initial_commitment_bitcoin_tx.get_sighash_all(&funding_script, self.context.channel_value_satoshis); + // They sign our commitment transaction, allowing us to broadcast the tx if we wish. + if let Err(_) = self.context.secp_ctx.verify_ecdsa(&sighash, &msg.signature, &self.context.get_counterparty_pubkeys().funding_pubkey) { + return Err(ChannelError::Close( + ( + "Invalid funding_signed signature from peer".to_owned(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + ))); + } + } + + let holder_commitment_tx = HolderCommitmentTransaction::new( + initial_commitment_tx, + msg.signature, + Vec::new(), + &self.context.get_holder_pubkeys().funding_pubkey, + self.context.counterparty_funding_pubkey() + ); + + self.context.holder_signer.as_ref().validate_holder_commitment(&holder_commitment_tx, Vec::new()) + .map_err(|_| ChannelError::Close( + ( + "Failed to validate our commitment".to_owned(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + )))?; + + let funding_redeemscript = self.context.get_funding_redeemscript(); + let funding_txo = self.context.get_funding_txo().unwrap(); + let funding_txo_script = funding_redeemscript.to_p2wsh(); + let obscure_factor = get_commitment_transaction_number_obscure_factor(&self.context.get_holder_pubkeys().payment_point, &self.context.get_counterparty_pubkeys().payment_point, self.context.is_outbound()); + let shutdown_script = self.context.shutdown_scriptpubkey.clone().map(|script| script.into_inner()); + let mut monitor_signer = signer_provider.derive_channel_signer(self.context.channel_value_satoshis, self.context.channel_keys_id); + monitor_signer.provide_channel_parameters(&self.context.channel_transaction_parameters); + let channel_monitor = ChannelMonitor::new(self.context.secp_ctx.clone(), monitor_signer, + shutdown_script, self.context.get_holder_selected_contest_delay(), + &self.context.destination_script, (funding_txo, funding_txo_script), + &self.context.channel_transaction_parameters, + funding_redeemscript.clone(), self.context.channel_value_satoshis, + obscure_factor, + holder_commitment_tx, best_block, self.context.counterparty_node_id, self.context.channel_id()); + + channel_monitor.provide_initial_counterparty_commitment_tx( + counterparty_initial_bitcoin_tx.txid, Vec::new(), + self.context.cur_counterparty_commitment_transaction_number, + self.context.counterparty_cur_commitment_point.unwrap(), + counterparty_initial_commitment_tx.feerate_per_kw(), + counterparty_initial_commitment_tx.to_broadcaster_value_sat(), + counterparty_initial_commitment_tx.to_countersignatory_value_sat(), logger); + + assert!(!self.context.channel_state.is_monitor_update_in_progress()); // We have no had any monitor(s) yet to fail update! + self.context.holder_commitment_point.advance(&self.context.holder_signer, &self.context.secp_ctx, logger); + self.context.cur_counterparty_commitment_transaction_number -= 1; + + log_info!(logger, "Received initial commitment_signed from peer for channel {}", &self.context.channel_id()); + + let need_channel_ready = self.check_get_channel_ready(0, logger).is_some(); + self.monitor_updating_paused(false, false, need_channel_ready, Vec::new(), Vec::new(), Vec::new()); + + Ok(channel_monitor) + } + pub fn commitment_signed(&mut self, msg: &msgs::CommitmentSigned, logger: &L) -> Result, ChannelError> where L::Target: Logger { @@ -7853,7 +7953,8 @@ impl OutboundV1Channel where SP::Target: SignerProvider { &mut self, msg: &msgs::AcceptChannel, default_limits: &ChannelHandshakeLimits, their_features: &InitFeatures ) -> Result<(), ChannelError> { - self.context.do_accept_channel_checks(default_limits, their_features, &msg.common_fields, msg.channel_reserve_satoshis) + self.context.do_accept_channel_checks( + default_limits, their_features, &msg.common_fields, msg.channel_reserve_satoshis) } /// Handles a funding_signed message from the remote end. @@ -8311,7 +8412,7 @@ impl OutboundV2Channel where SP::Target: SignerProvider { unfunded_context: UnfundedChannelContext { unfunded_channel_age_ticks: 0 }, dual_funding_context: DualFundingChannelContext { our_funding_satoshis: funding_satoshis, - their_funding_satoshis: 0, + their_funding_satoshis: None, funding_tx_locktime, funding_feerate_sat_per_1000_weight, our_funding_inputs: funding_inputs, @@ -8382,6 +8483,46 @@ impl OutboundV2Channel where SP::Target: SignerProvider { require_confirmed_inputs: None, } } + + pub fn funding_tx_constructed( + mut self, transaction: ConstructedTransaction, est_block: BestBlock, signer_provider: &SP, logger: &L + ) -> Result<(Channel, msgs::CommitmentSigned), (Self, ChannelError)> + where + L::Target: Logger + { + let res = get_initial_commitment_signed(&mut self.context, transaction, est_block, + signer_provider, logger); + let commitment_signed = match res { + Ok(commitment_signed) => commitment_signed, + Err(err) => return Err((self, err)), + }; + + let channel = Channel { + context: self.context, + dual_funding_channel_context: Some(self.dual_funding_context), + interactive_tx_constructor: self.interactive_tx_constructor, + }; + + Ok((channel, commitment_signed)) + } + + pub fn accept_channel_v2( + &mut self, msg: &msgs::AcceptChannelV2, default_limits: &ChannelHandshakeLimits, + their_features: &InitFeatures, + ) -> Result<(), ChannelError> { + // According to the spec we MUST fail the negotiation if `require_confirmed_inputs` is set in + // `accept_channel2` but we cannot provide confirmed inputs. We're not going to check if the user + // upheld this requirement, so we just defer the failure to the counterparty's checks during + // interactive transaction construction and remain blissfully unaware here. + + // Now we can generate the `channel_id` since we have our counterparty's `revocation_basepoint`. + self.context.channel_id = ChannelId::v2_from_revocation_basepoints( + &self.context.get_holder_pubkeys().revocation_basepoint, &RevocationBasepoint::from(msg.common_fields.revocation_basepoint)); + self.dual_funding_context.their_funding_satoshis = Some(msg.funding_satoshis); + self.context.do_accept_channel_checks( + default_limits, their_features, &msg.common_fields, get_v2_channel_reserve_satoshis( + msg.common_fields.dust_limit_satoshis, self.context.channel_value_satoshis)) + } } // A not-yet-funded inbound (from counterparty) channel using V2 channel establishment. @@ -8470,7 +8611,7 @@ impl InboundV2Channel where SP::Target: SignerProvider { unfunded_context: UnfundedChannelContext { unfunded_channel_age_ticks: 0 }, dual_funding_context: DualFundingChannelContext { our_funding_satoshis: funding_satoshis, - their_funding_satoshis: msg.common_fields.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, @@ -8549,6 +8690,28 @@ impl InboundV2Channel where SP::Target: SignerProvider { pub fn get_accept_channel_v2_message(&self) -> msgs::AcceptChannelV2 { self.generate_accept_channel_v2_message() } + + pub fn funding_tx_constructed( + mut self, transaction: ConstructedTransaction, est_block: BestBlock, signer_provider: &SP, logger: &L + ) -> Result<(Channel, msgs::CommitmentSigned), (Self, ChannelError)> + where + L::Target: Logger + { + let res = get_initial_commitment_signed(&mut self.context, transaction, est_block, + signer_provider, logger); + let commitment_signed = match res { + Ok(commitment_signed) => commitment_signed, + Err(err) => return Err((self, err)), + }; + + let channel = Channel { + context: self.context, + dual_funding_channel_context: Some(self.dual_funding_context), + interactive_tx_constructor: self.interactive_tx_constructor, + }; + + Ok((channel, commitment_signed)) + } } // Unfunded channel utilities @@ -8576,6 +8739,117 @@ fn get_initial_channel_type(config: &UserConfig, their_features: &InitFeatures) ret } +/// If an Err is returned, it is a ChannelError::Close +#[cfg(any(dual_funding, splicing))] +fn get_initial_remote_commitment_tx_signature( + context: &mut ChannelContext, logger: &L +) -> Result<(CommitmentTransaction, Signature), ChannelError> +where + SP::Target: SignerProvider, + L::Target: Logger +{ + let counterparty_keys = context.build_remote_transaction_keys(); + let counterparty_initial_commitment_tx = context.build_commitment_transaction( + context.cur_counterparty_commitment_transaction_number, &counterparty_keys, false, false, logger).tx; + + let holder_keys = context.build_holder_transaction_keys(); + let initial_commitment_tx = context.build_commitment_transaction( + context.holder_commitment_point.transaction_number(), &holder_keys, true, true, logger).tx; + + match &context.holder_signer { + ChannelSignerType::Ecdsa(ecdsa) => { + let signature = ecdsa.sign_counterparty_commitment(&counterparty_initial_commitment_tx, Vec::new(), Vec::new(), &context.secp_ctx) + .map_err(|_| ChannelError::Close( + ( + "Failed to get signatures for new commitment_signed".to_owned(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + ) + ))?.0; + Ok((counterparty_initial_commitment_tx, signature)) + } + } +} + +#[cfg(any(dual_funding, splicing))] +fn get_initial_counterparty_commitment_signature( + context: &mut ChannelContext, logger: &L +) -> Result +where + SP::Target: SignerProvider, + L::Target: Logger +{ + let counterparty_keys = context.build_remote_transaction_keys(); + let counterparty_initial_commitment_tx = context.build_commitment_transaction( + context.cur_counterparty_commitment_transaction_number, &counterparty_keys, false, false, logger).tx; + match context.holder_signer { + // TODO (taproot|arik): move match into calling method for Taproot + ChannelSignerType::Ecdsa(ref ecdsa) => { + Ok(ecdsa.sign_counterparty_commitment(&counterparty_initial_commitment_tx, Vec::new(), Vec::new(), &context.secp_ctx) + .map_err(|_| ChannelError::Close( + ( + "Failed to get signatures for new commitment_signed".to_owned(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + )))?.0) + }, + // TODO (taproot|arik) + #[cfg(taproot)] + _ => todo!(), + } +} + +#[cfg(any(dual_funding, splicing))] +fn get_initial_commitment_signed( + context: &mut ChannelContext, transaction: ConstructedTransaction, est_block: BestBlock, signer_provider: &SP, logger: &L +) -> Result +where + SP::Target: SignerProvider, + L::Target: Logger +{ + if !matches!( + context.channel_state, ChannelState::NegotiatingFunding(flags) + if flags == (NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT)) { + panic!("Tried to get a funding_created messsage at a time other than immediately after initial handshake completion (or tried to get funding_created twice)"); + } + if context.commitment_secrets.get_min_seen_secret() != (1 << 48) || + context.cur_counterparty_commitment_transaction_number != INITIAL_COMMITMENT_NUMBER || + context.holder_commitment_point.transaction_number() != INITIAL_COMMITMENT_NUMBER { + panic!("Should not have advanced channel commitment tx numbers prior to initial commitment_signed"); + } + + let funding_redeemscript = context.get_funding_redeemscript().to_p2wsh(); + let funding_outpoint_index = transaction.outputs().enumerate().find_map( + |(idx, output)| { + if output.tx_out().script_pubkey == funding_redeemscript { Some(idx as u16) } else { None } + }).expect("funding transaction contains funding output"); + let funding_txo = OutPoint { txid: transaction.txid(), index: funding_outpoint_index }; + context.channel_transaction_parameters.funding_outpoint = Some(funding_txo); + context.holder_signer.as_mut().provide_channel_parameters(&context.channel_transaction_parameters); + + let signature = match get_initial_counterparty_commitment_signature(context, logger) { + Ok(res) => res, + Err(e) => { + log_error!(logger, "Got bad signatures: {:?}!", e); + context.channel_transaction_parameters.funding_outpoint = None; + return Err(e); + } + }; + + if context.signer_pending_funding { + log_trace!(logger, "Counterparty commitment signature ready for funding_created message: clearing signer_pending_funding"); + context.signer_pending_funding = false; + } + + log_info!(logger, "Generated commitment_signed for peer for channel {}", &context.channel_id()); + + Ok(msgs::CommitmentSigned { + channel_id: context.channel_id.clone(), + htlc_signatures: vec![], + signature, + #[cfg(taproot)] + partial_signature_with_nonce: None, + }) +} + const SERIALIZATION_VERSION: u8 = 4; const MIN_SERIALIZATION_VERSION: u8 = 3; diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index ec17fce2fb7..73a321d7639 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -48,7 +48,7 @@ use crate::ln::types::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret}; use crate::ln::channel::{Channel, ChannelPhase, ChannelContext, ChannelError, ChannelUpdateStatus, ShutdownResult, UnfundedChannelContext, UpdateFulfillCommitFetch, OutboundV1Channel, InboundV1Channel, WithChannelContext}; use crate::ln::channel_state::ChannelDetails; #[cfg(any(dual_funding, splicing))] -use crate::ln::channel::{InboundV2Channel, InteractivelyFunded as _}; +use crate::ln::channel::{InboundV2Channel, OutboundV2Channel, InteractivelyFunded as _}; use crate::ln::features::{Bolt12InvoiceFeatures, ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures}; #[cfg(any(feature = "_test_utils", test))] use crate::ln::features::Bolt11InvoiceFeatures; @@ -60,6 +60,8 @@ use crate::ln::msgs; use crate::ln::onion_utils; use crate::ln::onion_utils::{HTLCFailReason, INVALID_ONION_BLINDING}; use crate::ln::msgs::{ChannelMessageHandler, DecodeError, LightningError}; +#[cfg(any(dual_funding, splicing))] +use crate::ln::msgs::CommitmentUpdate; #[cfg(test)] use crate::ln::outbound_payment; use crate::ln::outbound_payment::{OutboundPayments, PaymentAttempts, PendingOutboundPayment, SendAlongPathArgs, StaleExpiration}; @@ -2974,8 +2976,64 @@ where /// [`Event::FundingGenerationReady::temporary_channel_id`]: events::Event::FundingGenerationReady::temporary_channel_id /// [`Event::ChannelClosed::channel_id`]: events::Event::ChannelClosed::channel_id pub fn create_channel(&self, their_network_key: PublicKey, channel_value_satoshis: u64, push_msat: u64, user_channel_id: u128, temporary_channel_id: Option, override_config: Option) -> Result { - if channel_value_satoshis < 1000 { - return Err(APIError::APIMisuseError { err: format!("Channel value must be at least 1000 satoshis. It was {}", channel_value_satoshis) }); + self.create_channel_internal(false, their_network_key, channel_value_satoshis, vec![], None, + push_msat, user_channel_id, temporary_channel_id, override_config) + } + + /// Creates a new outbound dual-funded channel to the given remote node and with the given value + /// contributed by us. + /// + /// `user_channel_id` will be provided back as in + /// [`Event::FundingGenerationReady::user_channel_id`] to allow tracking of which events + /// correspond with which `create_channel` call. Note that the `user_channel_id` defaults to a + /// randomized value for inbound channels. `user_channel_id` has no meaning inside of LDK, it + /// is simply copied to events and otherwise ignored. + /// + /// `funding_satoshis` is the amount we are contributing to the channel. + /// Raises [`APIError::APIMisuseError`] when `funding_satoshis` > 2**24. + /// + /// The `funding_conf_target` parameter sets the priority of the funding transaction for appropriate + /// fee estimation. If `None`, then [`ConfirmationTarget::Normal`] is used. + /// + /// Raises [`APIError::ChannelUnavailable`] if the channel cannot be opened due to failing to + /// generate a shutdown scriptpubkey or destination script set by + /// [`SignerProvider::get_shutdown_scriptpubkey`] or [`SignerProvider::get_destination_script`]. + /// + /// Note that we do not check if you are currently connected to the given peer. If no + /// connection is available, the outbound `open_channel` message may fail to send, resulting in + /// the channel eventually being silently forgotten (dropped on reload). + /// + /// Returns the new Channel's temporary `channel_id`. This ID will appear as + /// [`Event::FundingGenerationReady::temporary_channel_id`] and in + /// [`ChannelDetails::channel_id`] until after + /// [`ChannelManager::funding_transaction_generated`] is called, swapping the Channel's ID for + /// one derived from the funding transaction's TXID. If the counterparty rejects the channel + /// immediately, this temporary ID will appear in [`Event::ChannelClosed::channel_id`]. + /// + /// [`Event::FundingGenerationReady::user_channel_id`]: events::Event::FundingGenerationReady::user_channel_id + /// [`Event::FundingGenerationReady::temporary_channel_id`]: events::Event::FundingGenerationReady::temporary_channel_id + /// [`Event::ChannelClosed::channel_id`]: events::Event::ChannelClosed::channel_id + /// [`ConfirmationTarget::Normal`]: chain::chaininterface::ConfirmationTarget + #[cfg(any(dual_funding, splicing))] + pub fn create_dual_funded_channel( + &self, their_network_key: PublicKey, funding_satoshis: u64, + funding_inputs: Vec<(TxIn, Transaction)>, funding_conf_target: Option, + user_channel_id: u128, override_config: Option + ) -> Result { + let funding_inputs = Self::length_limit_holder_input_prev_txs(funding_inputs)?; + self.create_channel_internal(true, their_network_key, funding_satoshis, funding_inputs, + funding_conf_target, 0, user_channel_id, None, override_config) + } + + // TODO(dual_funding): Remove param _-prefix once #[cfg(dual_funding)] is dropped. + fn create_channel_internal( + &self, _is_v2: bool, their_network_key: PublicKey, funding_satoshis: u64, + _funding_inputs: Vec<(TxIn,TransactionU16LenLimited)>, + _funding_conf_target: Option, push_msat: u64, user_channel_id: u128, + temporary_channel_id: Option, override_config: Option, + ) -> Result { + if funding_satoshis < 1000 { + return Err(APIError::APIMisuseError { err: format!("Channel value must be at least 1000 satoshis. It was {}", funding_satoshis) }); } let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); @@ -2995,24 +3053,76 @@ where } } - let channel = { - let outbound_scid_alias = self.create_and_insert_outbound_scid_alias(); - let their_features = &peer_state.latest_features; - let config = if override_config.is_some() { override_config.as_ref().unwrap() } else { &self.default_configuration }; - match OutboundV1Channel::new(&self.fee_estimator, &self.entropy_source, &self.signer_provider, their_network_key, - their_features, channel_value_satoshis, push_msat, user_channel_id, config, - self.best_block.read().unwrap().height, outbound_scid_alias, temporary_channel_id) - { - Ok(res) => res, - Err(e) => { - self.outbound_scid_aliases.lock().unwrap().remove(&outbound_scid_alias); - return Err(e); - }, - } + let outbound_scid_alias = self.create_and_insert_outbound_scid_alias(); + let their_features = &peer_state.latest_features; + let config = if override_config.is_some() { override_config.as_ref().unwrap() } else { &self.default_configuration }; + + // TODO(dual_funding): Merge this with below when cfg is removed. + #[cfg(not(any(dual_funding, splicing)))] + let (channel_phase, msg_send_event) = { + let channel = { + match OutboundV1Channel::new(&self.fee_estimator, &self.entropy_source, &self.signer_provider, their_network_key, + their_features, funding_satoshis, push_msat, user_channel_id, config, + self.best_block.read().unwrap().height, outbound_scid_alias, temporary_channel_id) + { + Ok(res) => res, + Err(e) => { + self.outbound_scid_aliases.lock().unwrap().remove(&outbound_scid_alias); + return Err(e); + }, + } + }; + let res = channel.get_open_channel(self.chain_hash); + let event = events::MessageSendEvent::SendOpenChannel { + node_id: their_network_key, + msg: res, + }; + (ChannelPhase::UnfundedOutboundV1(channel), event) }; - let res = channel.get_open_channel(self.chain_hash); - let temporary_channel_id = channel.context.channel_id(); + #[cfg(any(dual_funding, splicing))] + let (channel_phase, msg_send_event) = if _is_v2 { + let channel = { + match OutboundV2Channel::new(&self.fee_estimator, &self.entropy_source, &self.signer_provider, their_network_key, + their_features, funding_satoshis, _funding_inputs, user_channel_id, config, + self.best_block.read().unwrap().height, outbound_scid_alias, + _funding_conf_target.unwrap_or(ConfirmationTarget::NonAnchorChannelFee)) + { + Ok(res) => res, + Err(e) => { + self.outbound_scid_aliases.lock().unwrap().remove(&outbound_scid_alias); + return Err(e); + }, + } + }; + let res = channel.get_open_channel_v2(self.chain_hash); + let event = events::MessageSendEvent::SendOpenChannelV2 { + node_id: their_network_key, + msg: res, + }; + (ChannelPhase::UnfundedOutboundV2(channel), event) + } else { + let channel = { + match OutboundV1Channel::new(&self.fee_estimator, &self.entropy_source, &self.signer_provider, their_network_key, + their_features, funding_satoshis, push_msat, user_channel_id, config, + self.best_block.read().unwrap().height, outbound_scid_alias, temporary_channel_id) + { + Ok(res) => res, + Err(e) => { + self.outbound_scid_aliases.lock().unwrap().remove(&outbound_scid_alias); + return Err(e); + }, + } + }; + let res = channel.get_open_channel(self.chain_hash); + let event = events::MessageSendEvent::SendOpenChannel { + node_id: their_network_key, + msg: res, + }; + (ChannelPhase::UnfundedOutboundV1(channel), event) + }; + + let temporary_channel_id = channel_phase.context().channel_id(); match peer_state.channel_by_id.entry(temporary_channel_id) { hash_map::Entry::Occupied(_) => { if cfg!(fuzzing) { @@ -3021,13 +3131,10 @@ where panic!("RNG is bad???"); } }, - hash_map::Entry::Vacant(entry) => { entry.insert(ChannelPhase::UnfundedOutboundV1(channel)); } + hash_map::Entry::Vacant(entry) => { entry.insert(channel_phase); } } - peer_state.pending_msg_events.push(events::MessageSendEvent::SendOpenChannel { - node_id: their_network_key, - msg: res, - }); + peer_state.pending_msg_events.push(msg_send_event); Ok(temporary_channel_id) } @@ -7238,6 +7345,62 @@ where Ok(()) } + #[cfg(any(dual_funding, splicing))] + fn internal_accept_channel_v2(&self, counterparty_node_id: &PublicKey, msg: &msgs::AcceptChannelV2) -> Result<(), MsgHandleErrInternal> { + // Note that the ChannelManager is NOT re-persisted on disk after this, so any changes are + // likely to be lost on restart! + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id) + .ok_or_else(|| { + debug_assert!(false); + MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id), msg.common_fields.temporary_channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + + let (mut chan, channel_id) = { + match peer_state.channel_by_id.remove(&msg.common_fields.temporary_channel_id) { + Some(phase) => { + match phase { + ChannelPhase::UnfundedOutboundV2(mut chan) => { + if let Err(err) = chan.accept_channel_v2(&msg, &self.default_configuration.channel_handshake_limits, &peer_state.latest_features) { + let (_, res) = convert_chan_phase_err!(self, err, chan, &msg.common_fields.temporary_channel_id, UNFUNDED_CHANNEL); + let _: Result<(), _> = handle_error!(self, Err(res), *counterparty_node_id); + } + let channel_id = chan.context.channel_id(); + (chan, channel_id) + }, + _ => { + peer_state.channel_by_id.insert(msg.common_fields.temporary_channel_id, phase); + return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got an unexpected accept_channel2 message from peer with counterparty_node_id {}", counterparty_node_id), msg.common_fields.temporary_channel_id)); + } + } + }, + None => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.common_fields.temporary_channel_id)) + } + }; + let tx_msg_opt = chan.begin_interactive_funding_tx_construction(&self.entropy_source) + .map_err(|_| MsgHandleErrInternal::from_chan_no_close( + ChannelError::Close( + ( + "V2 channel rejected due to sender error".into(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + )), channel_id))?; + if let Some(tx_msg) = tx_msg_opt { + let msg_send_event = match tx_msg { + InteractiveTxMessageSend::TxAddInput(msg) => events::MessageSendEvent::SendTxAddInput { + node_id: *counterparty_node_id, msg }, + InteractiveTxMessageSend::TxAddOutput(msg) => events::MessageSendEvent::SendTxAddOutput { + node_id: *counterparty_node_id, msg }, + InteractiveTxMessageSend::TxComplete(msg) => events::MessageSendEvent::SendTxComplete { + node_id: *counterparty_node_id, msg }, + }; + peer_state.pending_msg_events.push(msg_send_event); + } + peer_state.channel_by_id.insert(chan.context.channel_id(), ChannelPhase::UnfundedOutboundV2(chan)); + Ok(()) + } + fn internal_funding_created(&self, counterparty_node_id: &PublicKey, msg: &msgs::FundingCreated) -> Result<(), MsgHandleErrInternal> { let best_block = *self.best_block.read().unwrap(); @@ -7419,7 +7582,7 @@ where channel.tx_add_input(msg).into_msg_send_event(counterparty_node_id) }, _ => try_chan_phase_entry!(self, Err(ChannelError::Warn( - "Got a tx_add_input message with no interactive transaction construction expected or in-progress" + "Got a tx_add_input message with no interactive transaction construction expected" .into())), chan_phase_entry) }; peer_state.pending_msg_events.push(msg_send_event); @@ -7538,6 +7701,7 @@ where #[cfg(any(dual_funding, splicing))] fn internal_tx_complete(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxComplete) -> Result<(), MsgHandleErrInternal> { + let best_block = *self.best_block.read().unwrap(); let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(counterparty_node_id) .ok_or_else(|| { @@ -7568,7 +7732,46 @@ where peer_state.pending_msg_events.push(msg_send_event); } if let Some(tx) = tx_opt { - // TODO(dual_funding): Handle this unsigned transaction. + let (channel_id, channel_phase) = chan_phase_entry.remove_entry(); + let res = match channel_phase { + ChannelPhase::UnfundedOutboundV2(chan) => { + chan.funding_tx_constructed(tx, best_block, &self.signer_provider, &self.logger).map_err( + |(chan, err)| { + (ChannelPhase::UnfundedOutboundV2(chan), err) + } + ) + }, + ChannelPhase::UnfundedInboundV2(chan) => { + chan.funding_tx_constructed(tx, best_block, &self.signer_provider, &self.logger).map_err( + |(chan, err)| { + (ChannelPhase::UnfundedInboundV2(chan), err) + } + ) + }, + _ => Err((channel_phase, ChannelError::Warn( + "Got a tx_complete message with no interactive transaction construction expected or in-progress" + .into()))), + }; + match res { + Ok((channel, commitment_signed)) => { + peer_state.pending_msg_events.push(events::MessageSendEvent::UpdateHTLCs { + node_id: counterparty_node_id.clone(), + updates: CommitmentUpdate { + commitment_signed, + update_add_htlcs: vec![], + update_fulfill_htlcs: vec![], + update_fail_htlcs: vec![], + update_fail_malformed_htlcs: vec![], + update_fee: None, + }, + }); + peer_state.channel_by_id.insert(channel_id.clone(), ChannelPhase::Funded(channel)); + }, + Err((channel_phase, _)) => { + peer_state.channel_by_id.insert(channel_id, channel_phase); + return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("{}", counterparty_node_id), msg.channel_id)) + }, + } } Ok(()) }, @@ -7979,6 +8182,8 @@ where } fn internal_commitment_signed(&self, counterparty_node_id: &PublicKey, msg: &msgs::CommitmentSigned) -> Result<(), MsgHandleErrInternal> { + #[cfg(any(dual_funding, splicing))] + let best_block = *self.best_block.read().unwrap(); let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(counterparty_node_id) .ok_or_else(|| { @@ -7989,18 +8194,49 @@ where let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() { - let logger = WithChannelContext::from(&self.logger, &chan.context, None); - let funding_txo = chan.context.get_funding_txo(); - let monitor_update_opt = try_chan_phase_entry!(self, chan.commitment_signed(&msg, &&logger), chan_phase_entry); - if let Some(monitor_update) = monitor_update_opt { - handle_new_monitor_update!(self, funding_txo.unwrap(), monitor_update, peer_state_lock, - peer_state, per_peer_state, chan); - } - Ok(()) - } else { - return try_chan_phase_entry!(self, Err(ChannelError::close( - "Got a commitment_signed message for an unfunded channel!".into())), chan_phase_entry); + match chan_phase_entry.get_mut() { + ChannelPhase::Funded(chan) => { + let logger = WithChannelContext::from(&self.logger, &chan.context, None); + let funding_txo = chan.context.get_funding_txo(); + + // TODO(dual_funding): Remove this allow directive after #[cfg(dual_funding)] is dropped. + #[allow(unused_mut, unused_assignments)] + let mut has_dual_funding_channel_context = false; + #[cfg(any(dual_funding, splicing))] + { + has_dual_funding_channel_context = chan.dual_funding_channel_context.is_some(); + } + + if has_dual_funding_channel_context { + #[cfg(any(dual_funding, splicing))] + { + let monitor = try_chan_phase_entry!(self, + chan.commitment_signed_initial_v2(&msg, best_block, &self.signer_provider, &&logger), chan_phase_entry); + if let Ok(persist_status) = self.chain_monitor.watch_channel(chan.context.get_funding_txo().unwrap(), monitor) { + handle_new_monitor_update!(self, persist_status, peer_state_lock, peer_state, per_peer_state, chan, INITIAL_MONITOR); + } else { + try_chan_phase_entry!(self, Err(ChannelError::Close( + ( + "Channel funding outpoint was a duplicate".to_owned(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + ) + )), chan_phase_entry) + } + } + } else { + let monitor_update_opt = try_chan_phase_entry!(self, chan.commitment_signed(&msg, &&logger), chan_phase_entry); + if let Some(monitor_update) = monitor_update_opt { + handle_new_monitor_update!(self, funding_txo.unwrap(), monitor_update, peer_state_lock, + peer_state, per_peer_state, chan); + } + } + Ok(()) + }, + _ => return try_chan_phase_entry!(self, Err(ChannelError::Close( + ( + "Got a commitment_signed message for an unfunded channel!".into(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + ))), chan_phase_entry), } }, hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) @@ -10049,9 +10285,20 @@ where } fn handle_accept_channel_v2(&self, counterparty_node_id: &PublicKey, msg: &msgs::AcceptChannelV2) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Dual-funded channels not supported".to_owned(), - msg.common_fields.temporary_channel_id.clone())), *counterparty_node_id); + #[cfg(any(dual_funding, splicing))] + { + // Note that we never need to persist the updated ChannelManager for an inbound + // accept_channel2 message - pre-funded channels are never written so there should be no + // change to the contents. + let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + let _ = handle_error!(self, self.internal_accept_channel_v2(counterparty_node_id, msg), *counterparty_node_id); + }; + #[cfg(not(any(dual_funding, splicing)))] + { + let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( + "Dual-funded channels not supported".to_owned(), + msg.common_fields.temporary_channel_id.clone())), *counterparty_node_id); + }; } fn handle_funding_created(&self, counterparty_node_id: &PublicKey, msg: &msgs::FundingCreated) { @@ -14014,6 +14261,9 @@ mod tests { expect_pending_htlcs_forwardable!(nodes[0]); } + + // Dual-funding: V2 Channel Establishment Tests + // TODO(dual_funding): Complete these. } #[cfg(ldk_bench)] diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index ce153420c87..6a74e360b39 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -19,7 +19,7 @@ use bitcoin::secp256k1::PublicKey; use bitcoin::transaction::Version; use bitcoin::{ absolute::LockTime as AbsoluteLockTime, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, - TxOut, Weight, + TxOut, Txid, Weight, }; use crate::chain::chaininterface::fee_for_weight; @@ -149,6 +149,12 @@ pub(crate) struct InteractiveTxOutput { tx_out: TxOut, } +impl InteractiveTxOutput { + pub fn tx_out(&self) -> &TxOut { + &self.tx_out + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct ConstructedTransaction { holder_is_initiator: bool, @@ -232,6 +238,18 @@ impl ConstructedTransaction { Transaction { version: Version::TWO, lock_time: self.lock_time, input, output } } + + pub fn outputs(&self) -> impl Iterator { + self.outputs.iter() + } + + pub fn inputs(&self) -> impl Iterator { + self.inputs.iter() + } + + pub fn txid(&self) -> Txid { + self.clone().into_unsigned_tx().txid() + } } #[derive(Debug)] From 7e8c3dda485ce299a99a6a72c4cfdd0b1c5602e3 Mon Sep 17 00:00:00 2001 From: Duncan Dean Date: Thu, 19 Oct 2023 14:08:14 +0200 Subject: [PATCH 4/6] Handle interactive signing sessions --- lightning/src/events/mod.rs | 59 +- lightning/src/ln/channel.rs | 300 ++++-- lightning/src/ln/channelmanager.rs | 483 ++++++++- lightning/src/ln/functional_test_utils.rs | 46 +- lightning/src/ln/interactivetxs.rs | 1175 +++++++++++++++++---- 5 files changed, 1749 insertions(+), 314 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index a5a42eceec3..798b4de2cbd 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1270,7 +1270,56 @@ pub enum Event { /// The node id of the peer we just connected to, who advertises support for /// onion messages. peer_node_id: PublicKey, - } + }, + #[cfg(any(dual_funding, splicing))] + /// Indicates that a transaction constructed via interactive transaction construction for a + /// dual-funded (V2) channel is ready to be signed by the client. This event will only be triggered + /// if at least one input was contributed by the holder. + /// + /// The transaction contains all inputs provided by both parties when the channel was + /// created/accepted along with the channel's funding output and a change output if applicable. + /// + /// No part of the transaction should be changed before signing as the content of the transaction + /// has already been negotiated with the counterparty. + /// + /// Each signature MUST use the SIGHASH_ALL flag to avoid invalidation of initial commitment and + /// hence possible loss of funds. + /// + /// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially) signed + /// funding transaction. + /// + /// Generated in [`ChannelManager`] message handling. + /// + /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager + /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed + FundingTransactionReadyForSigning { + /// The channel_id of the V2 channel which you'll need to pass back into + /// [`ChannelManager::funding_transaction_signed`]. + /// + /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed + channel_id: ChannelId, + /// The counterparty's node_id, which you'll need to pass back into + /// [`ChannelManager::funding_transaction_signed`]. + /// + /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed + counterparty_node_id: PublicKey, + /// The `user_channel_id` value passed in to [`ChannelManager::create_dual_funded_channel`] for outbound + /// channels, or to [`ChannelManager::accept_inbound_channel`] or [`ChannelManager::accept_inbound_channel_with_contribution`] + /// for inbound channels if [`UserConfig::manually_accept_inbound_channels`] config flag is set to true. + /// Otherwise `user_channel_id` will be randomized for an inbound channel. + /// This may be zero for objects serialized with LDK versions prior to 0.0.113. + /// + /// [`ChannelManager::create_dual_funded_channel`]: crate::ln::channelmanager::ChannelManager::create_dual_funded_channel + /// [`ChannelManager::accept_inbound_channel`]: crate::ln::channelmanager::ChannelManager::accept_inbound_channel + /// [`ChannelManager::accept_inbound_channel_with_contribution`]: crate::ln::channelmanager::ChannelManager::accept_inbound_channel_with_contribution + /// [`UserConfig::manually_accept_inbound_channels`]: crate::util::config::UserConfig::manually_accept_inbound_channels + user_channel_id: u128, + /// The unsigned transaction to be signed and passed back to + /// [`ChannelManager::funding_transaction_signed`]. + /// + /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed + unsigned_transaction: Transaction, + }, } impl Writeable for Event { @@ -1556,6 +1605,14 @@ impl Writeable for Event { (4, responder, option), }) }, + #[cfg(any(splicing, dual_funding))] + &Event::FundingTransactionReadyForSigning { .. } => { + 43u8.write(writer)?; + // We never write out FundingTransactionReadyForSigning events as, upon disconnection, peers + // drop any V2-established channels which have not yet exchanged the initial `commitment_signed`. + // We only exhange the initial `commitment_signed` after the client calls + // `ChannelManager::funding_transaction_signed` and ALWAYS before we send a `tx_signatures` + }, // Note that, going forward, all new events must only write data inside of // `write_tlv_fields`. Versions 0.0.101+ will ignore odd-numbered events that write // data via `write_tlv_fields`. diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 4bc19e4f985..1f675f32e10 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -24,14 +24,18 @@ use bitcoin::secp256k1::{PublicKey,SecretKey}; use bitcoin::secp256k1::{Secp256k1,ecdsa::Signature}; use bitcoin::secp256k1; #[cfg(any(dual_funding, splicing))] -use bitcoin::{TxIn, TxOut}; +use bitcoin::{TxIn, TxOut, Witness}; #[cfg(any(dual_funding, splicing))] use bitcoin::locktime::absolute::LockTime; use crate::ln::types::{ChannelId, PaymentPreimage, PaymentHash}; use crate::ln::features::{ChannelTypeFeatures, InitFeatures}; #[cfg(any(dual_funding, splicing))] -use crate::ln::interactivetxs::{ConstructedTransaction, estimate_input_weight, get_output_weight, HandleTxCompleteResult, InteractiveTxConstructor, InteractiveTxMessageSend, InteractiveTxMessageSendResult, TX_COMMON_FIELDS_WEIGHT}; +use crate::ln::interactivetxs::{ + ConstructedTransaction, estimate_input_weight, get_output_weight, HandleTxCompleteResult, + InteractiveTxConstructor, InteractiveTxSigningSession, InteractiveTxMessageSend, + InteractiveTxMessageSendResult, OutputOwned, SharedOwnedOutput, TX_COMMON_FIELDS_WEIGHT +}; use crate::ln::msgs; use crate::ln::msgs::DecodeError; use crate::ln::script::{self, ShutdownScript}; @@ -49,6 +53,8 @@ use crate::chain::transaction::{OutPoint, TransactionData}; use crate::sign::ecdsa::EcdsaChannelSigner; use crate::sign::{EntropySource, ChannelSigner, SignerProvider, NodeSigner, Recipient}; use crate::events::ClosureReason; +#[cfg(any(dual_funding, splicing))] +use crate::events::Event; use crate::routing::gossip::NodeId; use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer}; #[cfg(any(dual_funding, splicing))] @@ -1454,32 +1460,50 @@ pub(super) trait InteractivelyFunded where SP::Target: SignerProvider fn interactive_tx_constructor_mut(&mut self) -> &mut Option; - fn dual_funding_context_mut(&mut self) -> &mut DualFundingChannelContext; + fn dual_funding_context(&self) -> &DualFundingChannelContext; - fn set_interactive_tx_constructor(&mut self, interactive_tx_constructor: InteractiveTxConstructor); + fn dual_funding_context_mut(&mut self) -> &mut DualFundingChannelContext; fn is_initiator(&self) -> bool; fn begin_interactive_funding_tx_construction( - &mut self, entropy_source: &ES, + &mut self, entropy_source: &ES, holder_node_id: PublicKey, ) -> Result, APIError> where ES::Target: EntropySource { let mut funding_outputs = Vec::new(); - if self.is_initiator() { - funding_outputs.push(TxOut { - value: Amount::from_sat(self.context().get_value_satoshis()), - script_pubkey: self.context().get_funding_redeemscript().to_p2wsh(), - }); - } + 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.is_initiator() { + 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)) + }; let (tx_constructor, msg) = InteractiveTxConstructor::new( entropy_source, self.context().channel_id(), - self.dual_funding_context_mut().funding_feerate_sat_per_1000_weight, self.is_initiator(), + self.dual_funding_context_mut().funding_feerate_sat_per_1000_weight, + holder_node_id, self.context().counterparty_node_id, self.is_initiator(), self.dual_funding_context_mut().funding_tx_locktime, - self.dual_funding_context_mut().our_funding_inputs.clone(), funding_outputs - ); - self.set_interactive_tx_constructor(tx_constructor); + self.dual_funding_context_mut().our_funding_inputs.take().unwrap_or_else(|| vec![]), + funding_outputs, + expected_remote_shared_funding_output, + ).map_err(|_| APIError::APIMisuseError { err: "Incorrect shared output provided".into() })?; + + self.interactive_tx_constructor_mut().replace(tx_constructor); Ok(msg) } @@ -1538,6 +1562,66 @@ pub(super) trait InteractivelyFunded where SP::Target: SignerProvider }), }) } + + fn internal_funding_tx_constructed( + &mut self, counterparty_node_id: &PublicKey, signing_session: &mut InteractiveTxSigningSession, + signer_provider: &SP, logger: &L + ) -> Result<(msgs::CommitmentSigned, Option), ChannelError> + where + L::Target: Logger + { + let our_funding_satoshis = self.dual_funding_context().our_funding_satoshis; + + let mut output_index = None; + let expected_spk = self.context().get_funding_redeemscript().to_p2wsh(); + for (idx, outp) in signing_session.unsigned_tx.outputs().enumerate() { + if outp.tx_out().script_pubkey == expected_spk && outp.tx_out().value.to_sat() == self.context().get_value_satoshis() { + if output_index.is_some() { + return Err(ChannelError::Close( + ( + "Multiple outputs matched the expected script and value".to_owned(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + ))); + } + output_index = Some(idx as u16); + } + } + if output_index.is_none() { + return Err(ChannelError::Close( + ( + "No output matched the funding script_pubkey".to_owned(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + ))); + } + let outpoint = OutPoint { txid: signing_session.unsigned_tx.txid(), index: output_index.unwrap() }; + self.context_mut().channel_transaction_parameters.funding_outpoint = Some(outpoint); + let channel_transaction_parameters = self.context().channel_transaction_parameters.clone(); + self.context_mut().holder_signer.as_mut().provide_channel_parameters(&channel_transaction_parameters); + + let commitment_signed = get_initial_commitment_signed(self.context_mut(), signing_session.unsigned_tx.clone(), signer_provider, logger); + let commitment_signed = match commitment_signed { + Ok(commitment_signed) => commitment_signed, + Err(err) => return Err(ChannelError::Close((err.to_string(), ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }))), + }; + + let mut funding_ready_for_sig_event = None; + if our_funding_satoshis == 0 { + signing_session.provide_holder_witnesses(self.context().channel_id, Vec::new()); + } else { + funding_ready_for_sig_event = Some(Event::FundingTransactionReadyForSigning { + channel_id: self.context().channel_id, + counterparty_node_id: *counterparty_node_id, + user_channel_id: self.context().user_id, + unsigned_transaction: signing_session.unsigned_tx.clone().into_unsigned_tx(), + }); + } + + // Clear the interactive transaction constructor + self.interactive_tx_constructor_mut().take(); + self.context_mut().channel_state = ChannelState::FundingNegotiated; + + Ok((commitment_signed, funding_ready_for_sig_event)) + } } #[cfg(any(dual_funding, splicing))] @@ -1548,15 +1632,15 @@ impl InteractivelyFunded for OutboundV2Channel where SP::Targ fn context_mut(&mut self) -> &mut ChannelContext { &mut self.context } + fn dual_funding_context(&self) -> &DualFundingChannelContext { + &self.dual_funding_context + } fn dual_funding_context_mut(&mut self) -> &mut DualFundingChannelContext { &mut self.dual_funding_context } fn interactive_tx_constructor_mut(&mut self) -> &mut Option { &mut self.interactive_tx_constructor } - fn set_interactive_tx_constructor(&mut self, interactive_tx_constructor: InteractiveTxConstructor) { - self.interactive_tx_constructor = Some(interactive_tx_constructor); - } fn is_initiator(&self) -> bool { true } @@ -1570,15 +1654,15 @@ impl InteractivelyFunded for InboundV2Channel where SP::Targe fn context_mut(&mut self) -> &mut ChannelContext { &mut self.context } + fn dual_funding_context(&self) -> &DualFundingChannelContext { + &self.dual_funding_context + } fn dual_funding_context_mut(&mut self) -> &mut DualFundingChannelContext { &mut self.dual_funding_context } fn interactive_tx_constructor_mut(&mut self) -> &mut Option { &mut self.interactive_tx_constructor } - fn set_interactive_tx_constructor(&mut self, interactive_tx_constructor: InteractiveTxConstructor) { - self.interactive_tx_constructor = Some(interactive_tx_constructor); - } fn is_initiator(&self) -> bool { false } @@ -2885,7 +2969,7 @@ impl ChannelContext where SP::Target: SignerProvider { } /// Gets the redeemscript for the funding transaction output (ie the funding transaction output - /// pays to get_funding_redeemscript().to_v0_p2wsh()). + /// pays to get_funding_redeemscript().to_p2wsh()). /// Panics if called before accept_channel/InboundV1Channel::new pub fn get_funding_redeemscript(&self) -> ScriptBuf { make_funding_redeemscript(&self.get_holder_pubkeys().funding_pubkey, self.counterparty_funding_pubkey()) @@ -3676,28 +3760,6 @@ impl ChannelContext where SP::Target: SignerProvider { self.channel_transaction_parameters.channel_type_features = self.channel_type.clone(); Ok(()) } - - // Interactive transaction construction - - #[cfg(any(dual_funding, splicing))] - pub fn tx_signatures(&self, msg: &msgs::TxSignatures)-> Result { - todo!(); - } - - #[cfg(any(dual_funding, splicing))] - pub fn tx_init_rbf(&self, msg: &msgs::TxInitRbf)-> Result { - todo!(); - } - - #[cfg(any(dual_funding, splicing))] - pub fn tx_ack_rbf(&self, msg: &msgs::TxAckRbf)-> Result { - todo!(); - } - - #[cfg(any(dual_funding, splicing))] - pub fn tx_abort(&self, msg: &msgs::TxAbort)-> Result { - todo!(); - } } // Internal utility functions for channels @@ -3785,9 +3847,10 @@ pub(crate) fn per_outbound_htlc_counterparty_commit_tx_fee_msat(feerate_per_kw: #[cfg(any(dual_funding, splicing))] pub(super) fn calculate_our_funding_satoshis( is_initiator: bool, funding_inputs: &[(TxIn, TransactionU16LenLimited)], - funding_outputs: &[TxOut], funding_feerate_sat_per_1000_weight: u32, - holder_dust_limit_satoshis: u64, + funding_feerate_sat_per_1000_weight: u32, holder_dust_limit_satoshis: u64, ) -> Result { + use bitcoin::WScriptHash; + let mut total_input_satoshis = 0u64; let mut our_contributed_weight = 0u64; @@ -3801,13 +3864,15 @@ pub(super) fn calculate_our_funding_satoshis( input.1.as_transaction().txid(), input.0.previous_output.vout, idx) }); } } - our_contributed_weight = our_contributed_weight.saturating_add(funding_outputs.iter().fold(0u64, |weight, txout| { - weight.saturating_add(get_output_weight(&txout.script_pubkey).to_wu()) - })); // If we are the initiator, we must pay for weight of all common fields in the funding transaction. if is_initiator { - our_contributed_weight = our_contributed_weight.saturating_add(TX_COMMON_FIELDS_WEIGHT); + our_contributed_weight = our_contributed_weight + .saturating_add(TX_COMMON_FIELDS_WEIGHT) + // The weight of a P2WSH output to be added later. + .saturating_add(get_output_weight(&ScriptBuf::new_p2wsh( + &WScriptHash::from_slice(&[2; 32]).expect("valid scrpt hash") + )).to_wu()) } let funding_satoshis = total_input_satoshis @@ -3836,7 +3901,7 @@ 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. - pub our_funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>, + pub our_funding_inputs: Option>, } // Holder designates channel data owned for the benefit of the user client. @@ -3847,7 +3912,9 @@ pub(super) struct Channel where SP::Target: SignerProvider { pub dual_funding_channel_context: Option, /// The current interactive transaction construction session under negotiation. #[cfg(any(dual_funding, splicing))] - interactive_tx_constructor: Option, + pub interactive_tx_constructor: Option, + #[cfg(any(dual_funding, splicing))] + pub interactive_tx_signing_session: Option, } #[cfg(any(test, fuzzing))] @@ -4636,9 +4703,6 @@ impl Channel where self.context.holder_commitment_point.transaction_number() != INITIAL_COMMITMENT_NUMBER { panic!("Should not have advanced channel commitment tx numbers prior to funding_created"); } - let dual_funding_channel_context = self.dual_funding_channel_context.as_mut().ok_or( - ChannelError::Close(("Have no context for dual-funded channel".to_owned(), ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) })) - )?; let funding_script = self.context.get_funding_redeemscript(); @@ -4713,6 +4777,7 @@ impl Channel where log_info!(logger, "Received initial commitment_signed from peer for channel {}", &self.context.channel_id()); let need_channel_ready = self.check_get_channel_ready(0, logger).is_some(); + self.context.channel_state = ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::new()); self.monitor_updating_paused(false, false, need_channel_ready, Vec::new(), Vec::new(), Vec::new()); Ok(channel_monitor) @@ -5372,6 +5437,77 @@ impl Channel where } } + #[cfg(any(dual_funding, splicing))] + pub fn verify_interactive_tx_signatures(&mut self, _witnesses: &Vec) { + if let Some(ref mut _signing_session) = self.interactive_tx_signing_session { + // Check that sighash_all was used: + // TODO(dual_funding): Check sig for sighash + } + } + + #[cfg(any(dual_funding, splicing))] + pub fn tx_signatures(&mut self, msg: &msgs::TxSignatures) -> Result<(Option, Option), ChannelError> { + if let Some(ref mut signing_session) = self.interactive_tx_signing_session { + if msg.witnesses.len() != signing_session.remote_inputs_count() { + return Err(ChannelError::Close( + ( + "Witness count did not match contributed input count".to_string(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + ))); + } + + for witness in &msg.witnesses { + if witness.is_empty() { + return Err(ChannelError::Close( + ( + "Unexpected empty witness in tx_signatures received".to_string(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + ))); + } + + // TODO(dual_funding): Check all sigs are SIGHASH_ALL. + + // TODO(dual_funding): I don't see how we're going to be able to ensure witness-standardness + // for spending. Doesn't seem to be anything in rust-bitcoin. + } + + if msg.tx_hash != signing_session.unsigned_tx.txid() { + return Err(ChannelError::Close( + ( + "The txid for the transaction does not match".to_string(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + ))); + } + + let (tx_signatures_opt, funding_tx_opt) = signing_session.received_tx_signatures(msg.clone()); + if funding_tx_opt.is_some() { + self.context.channel_state = ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::new()); + } + self.context.funding_transaction = funding_tx_opt.clone(); + + // Clear out the signing session + self.interactive_tx_signing_session = None; + + Ok((tx_signatures_opt, funding_tx_opt)) + } else { + return Err(ChannelError::Close( + ( + "Unexpected tx_signatures. No funding transaction awaiting signatures".to_string(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + ))); + } + } + + #[cfg(any(dual_funding, splicing))] + pub fn tx_init_rbf(&self, _msg: &msgs::TxInitRbf)-> Result { + todo!(); + } + + #[cfg(any(dual_funding, splicing))] + pub fn tx_ack_rbf(&self, _msg: &msgs::TxAckRbf)-> Result { + todo!(); + } + /// Queues up an outbound update fee by placing it in the holding cell. You should call /// [`Self::maybe_free_holding_cell_htlcs`] in order to actually generate and send the /// commitment update. @@ -8052,6 +8188,8 @@ impl OutboundV1Channel where SP::Target: SignerProvider { dual_funding_channel_context: None, #[cfg(any(dual_funding, splicing))] interactive_tx_constructor: None, + #[cfg(any(dual_funding, splicing))] + interactive_tx_signing_session: None, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some(); @@ -8345,6 +8483,8 @@ impl InboundV1Channel where SP::Target: SignerProvider { dual_funding_channel_context: None, #[cfg(any(dual_funding, splicing))] interactive_tx_constructor: None, + #[cfg(any(dual_funding, splicing))] + interactive_tx_signing_session: None, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some(); channel.monitor_updating_paused(false, false, need_channel_ready, Vec::new(), Vec::new(), Vec::new()); @@ -8415,7 +8555,7 @@ impl OutboundV2Channel where SP::Target: SignerProvider { their_funding_satoshis: None, funding_tx_locktime, funding_feerate_sat_per_1000_weight, - our_funding_inputs: funding_inputs, + our_funding_inputs: Some(funding_inputs), }, #[cfg(any(dual_funding, splicing))] interactive_tx_constructor: None, @@ -8485,15 +8625,15 @@ impl OutboundV2Channel where SP::Target: SignerProvider { } pub fn funding_tx_constructed( - mut self, transaction: ConstructedTransaction, est_block: BestBlock, signer_provider: &SP, logger: &L - ) -> Result<(Channel, msgs::CommitmentSigned), (Self, ChannelError)> + mut self, counterparty_node_id: &PublicKey, mut signing_session: InteractiveTxSigningSession, + signer_provider: &SP, logger: &L + ) -> Result<(Channel, msgs::CommitmentSigned, Option), (Self, ChannelError)> where L::Target: Logger { - let res = get_initial_commitment_signed(&mut self.context, transaction, est_block, - signer_provider, logger); - let commitment_signed = match res { - Ok(commitment_signed) => commitment_signed, + let (commitment_signed, funding_ready_for_sig_event) = match self.internal_funding_tx_constructed( + counterparty_node_id, &mut signing_session, signer_provider, logger) { + Ok(res) => res, Err(err) => return Err((self, err)), }; @@ -8501,14 +8641,15 @@ impl OutboundV2Channel where SP::Target: SignerProvider { context: self.context, dual_funding_channel_context: Some(self.dual_funding_context), interactive_tx_constructor: self.interactive_tx_constructor, + interactive_tx_signing_session: Some(signing_session), }; - Ok((channel, commitment_signed)) + Ok((channel, commitment_signed, funding_ready_for_sig_event)) } pub fn accept_channel_v2( &mut self, msg: &msgs::AcceptChannelV2, default_limits: &ChannelHandshakeLimits, - their_features: &InitFeatures, + their_features: &InitFeatures, signer_provider: &SP, ) -> Result<(), ChannelError> { // According to the spec we MUST fail the negotiation if `require_confirmed_inputs` is set in // `accept_channel2` but we cannot provide confirmed inputs. We're not going to check if the user @@ -8519,6 +8660,14 @@ impl OutboundV2Channel where SP::Target: SignerProvider { self.context.channel_id = ChannelId::v2_from_revocation_basepoints( &self.context.get_holder_pubkeys().revocation_basepoint, &RevocationBasepoint::from(msg.common_fields.revocation_basepoint)); self.dual_funding_context.their_funding_satoshis = Some(msg.funding_satoshis); + self.context.channel_value_satoshis = self.context.channel_value_satoshis.saturating_add(msg.funding_satoshis); + // We need to derive a new a holder signer if the channel value has been updated due to the acceptor contributing funds. + // Pubkeys are not affected + if msg.funding_satoshis > 0 { + self.context.holder_signer = ChannelSignerType::Ecdsa(signer_provider.derive_channel_signer( + self.context.channel_value_satoshis, self.context.channel_keys_id + )); + } self.context.do_accept_channel_checks( default_limits, their_features, &msg.common_fields, get_v2_channel_reserve_satoshis( msg.common_fields.dust_limit_satoshis, self.context.channel_value_satoshis)) @@ -8551,7 +8700,7 @@ impl InboundV2Channel where SP::Target: SignerProvider { L::Target: Logger, { let funding_satoshis = calculate_our_funding_satoshis( - false, &funding_inputs, &[], msg.funding_feerate_sat_per_1000_weight, + false, &funding_inputs, msg.funding_feerate_sat_per_1000_weight, msg.common_fields.dust_limit_satoshis ).map_err(|_| ChannelError::Close( ( @@ -8614,7 +8763,7 @@ impl InboundV2Channel where SP::Target: SignerProvider { 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, + our_funding_inputs: Some(funding_inputs), }, interactive_tx_constructor: None, }; @@ -8692,15 +8841,15 @@ impl InboundV2Channel where SP::Target: SignerProvider { } pub fn funding_tx_constructed( - mut self, transaction: ConstructedTransaction, est_block: BestBlock, signer_provider: &SP, logger: &L - ) -> Result<(Channel, msgs::CommitmentSigned), (Self, ChannelError)> + mut self, counterparty_node_id: &PublicKey, mut signing_session: InteractiveTxSigningSession, + signer_provider: &SP, logger: &L + ) -> Result<(Channel, msgs::CommitmentSigned, Option), (Self, ChannelError)> where L::Target: Logger { - let res = get_initial_commitment_signed(&mut self.context, transaction, est_block, - signer_provider, logger); - let commitment_signed = match res { - Ok(commitment_signed) => commitment_signed, + let (commitment_signed, funding_ready_for_sig_event) = match self.internal_funding_tx_constructed( + counterparty_node_id, &mut signing_session, signer_provider, logger) { + Ok(res) => res, Err(err) => return Err((self, err)), }; @@ -8708,9 +8857,10 @@ impl InboundV2Channel where SP::Target: SignerProvider { context: self.context, dual_funding_channel_context: Some(self.dual_funding_context), interactive_tx_constructor: self.interactive_tx_constructor, + interactive_tx_signing_session: Some(signing_session), }; - Ok((channel, commitment_signed)) + Ok((channel, commitment_signed, funding_ready_for_sig_event)) } } @@ -8799,7 +8949,7 @@ where #[cfg(any(dual_funding, splicing))] fn get_initial_commitment_signed( - context: &mut ChannelContext, transaction: ConstructedTransaction, est_block: BestBlock, signer_provider: &SP, logger: &L + context: &mut ChannelContext, transaction: ConstructedTransaction, signer_provider: &SP, logger: &L ) -> Result where SP::Target: SignerProvider, @@ -9877,6 +10027,8 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, u32, &'c Ch dual_funding_channel_context: None, #[cfg(any(dual_funding, splicing))] interactive_tx_constructor: None, + #[cfg(any(dual_funding, splicing))] + interactive_tx_signing_session: None, }) } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 73a321d7639..4a91837e642 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -48,12 +48,12 @@ use crate::ln::types::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret}; use crate::ln::channel::{Channel, ChannelPhase, ChannelContext, ChannelError, ChannelUpdateStatus, ShutdownResult, UnfundedChannelContext, UpdateFulfillCommitFetch, OutboundV1Channel, InboundV1Channel, WithChannelContext}; use crate::ln::channel_state::ChannelDetails; #[cfg(any(dual_funding, splicing))] -use crate::ln::channel::{InboundV2Channel, OutboundV2Channel, InteractivelyFunded as _}; +use crate::ln::channel::{calculate_our_funding_satoshis, InboundV2Channel, MIN_CHAN_DUST_LIMIT_SATOSHIS, OutboundV2Channel, InteractivelyFunded as _}; use crate::ln::features::{Bolt12InvoiceFeatures, ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures}; #[cfg(any(feature = "_test_utils", test))] use crate::ln::features::Bolt11InvoiceFeatures; #[cfg(any(dual_funding, splicing))] -use crate::ln::interactivetxs::InteractiveTxMessageSend; +use crate::ln::interactivetxs::{InteractiveTxConstructor, InteractiveTxMessageSend}; use crate::routing::router::{BlindedTail, InFlightHtlcs, Path, Payee, PaymentParameters, Route, RouteParameters, Router}; use crate::ln::onion_payment::{check_incoming_htlc_cltv, create_recv_pending_htlc_info, create_fwd_pending_htlc_info, decode_incoming_update_add_htlc_onion, InboundHTLCErr, NextPacketDetails}; use crate::ln::msgs; @@ -3016,11 +3016,18 @@ where /// [`ConfirmationTarget::Normal`]: chain::chaininterface::ConfirmationTarget #[cfg(any(dual_funding, splicing))] pub fn create_dual_funded_channel( - &self, their_network_key: PublicKey, funding_satoshis: u64, - funding_inputs: Vec<(TxIn, Transaction)>, funding_conf_target: Option, - user_channel_id: u128, override_config: Option + &self, their_network_key: PublicKey, funding_inputs: Vec<(TxIn, Transaction)>, + funding_conf_target: Option, user_channel_id: u128, + override_config: Option ) -> Result { + + let funding_feerate_sat_per_1000_weight = self.fee_estimator.bounded_sat_per_1000_weight( + funding_conf_target.unwrap_or(ConfirmationTarget::NonAnchorChannelFee) + ); let funding_inputs = Self::length_limit_holder_input_prev_txs(funding_inputs)?; + let funding_satoshis = calculate_our_funding_satoshis( + true, &funding_inputs, funding_feerate_sat_per_1000_weight, MIN_CHAN_DUST_LIMIT_SATOSHIS + )?; self.create_channel_internal(true, their_network_key, funding_satoshis, funding_inputs, funding_conf_target, 0, user_channel_id, None, override_config) } @@ -4625,6 +4632,54 @@ where result } + /// Handles a signed funding transaction generated by interactive transaction construction and + /// provided by the client. + /// + /// Do NOT broadcast the funding transaction yourself. When we have safely received our + /// counterparty's signature(s) the funding transaction will automatically be broadcast via the + /// [`BroadcasterInterface`] provided when this `ChannelManager` was constructed. + #[cfg(any(dual_funding, splicing))] + pub fn funding_transaction_signed( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, transaction: Transaction + ) -> Result<(), APIError> { + let witnesses: Vec<_> = transaction.input.into_iter().filter_map(|input| { + if input.witness.is_empty() { None } else { Some(input.witness) } + }).collect(); + + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id) + .ok_or_else(|| APIError::ChannelUnavailable { + err: format!("Can't find a peer matching the passed counterparty node_id {}", + counterparty_node_id) })?; + + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + + match peer_state.channel_by_id.get_mut(channel_id) { + Some(ChannelPhase::Funded(chan)) => { + chan.verify_interactive_tx_signatures(&witnesses); + if let Some(ref mut signing_session) = chan.interactive_tx_signing_session { + if let Some(tx_signatures) = signing_session.provide_holder_witnesses(*channel_id, witnesses) { + peer_state.pending_msg_events.push(events::MessageSendEvent::SendTxSignatures { + node_id: *counterparty_node_id, + msg: tx_signatures, + }); + } + } else { + return Err(APIError::APIMisuseError { + err: format!("Channel with id {} not expecting funding signatures", channel_id)}); + } + }, + Some(_) => return Err(APIError::APIMisuseError { + err: format!("Channel with id {} not expecting funding signatures", channel_id)}), + None => return Err(APIError::ChannelUnavailable{ + err: format!("Channel with id {} not found for the passed counterparty node_id {}", channel_id, + counterparty_node_id) }), + } + + Ok(()) + } + /// Atomically applies partial updates to the [`ChannelConfig`] of the given channels. /// /// Once the updates are applied, each eligible channel (advertised with a known short channel @@ -6592,7 +6647,7 @@ where /// Gets the node_id held by this ChannelManager pub fn get_our_node_id(&self) -> PublicKey { - self.our_network_pubkey.clone() + self.our_network_pubkey } fn handle_monitor_update_completion_actions>(&self, actions: I) { @@ -6909,7 +6964,9 @@ where &self.logger); match channel_res { Ok(mut channel) => { - let tx_msg_opt_res = channel.begin_interactive_funding_tx_construction(&self.entropy_source); + let tx_msg_opt_res = channel.begin_interactive_funding_tx_construction( + &self.entropy_source, self.get_our_node_id() + ); match tx_msg_opt_res { Ok(tx_msg_opt) => { if let Some(tx_msg) = tx_msg_opt { @@ -7293,7 +7350,7 @@ where let outbound_scid_alias = self.create_and_insert_outbound_scid_alias(); channel.context.set_outbound_scid_alias(outbound_scid_alias); - channel.begin_interactive_funding_tx_construction(&self.entropy_source) + channel.begin_interactive_funding_tx_construction(&self.entropy_source, self.get_our_node_id()) .map_err(|_| MsgHandleErrInternal::send_err_msg_no_close( "Failed to start interactive transaction construction".to_owned(), msg.common_fields.temporary_channel_id))?; @@ -7363,7 +7420,10 @@ where Some(phase) => { match phase { ChannelPhase::UnfundedOutboundV2(mut chan) => { - if let Err(err) = chan.accept_channel_v2(&msg, &self.default_configuration.channel_handshake_limits, &peer_state.latest_features) { + if let Err(err) = chan.accept_channel_v2( + &msg, &self.default_configuration.channel_handshake_limits, &peer_state.latest_features, + &self.signer_provider + ) { let (_, res) = convert_chan_phase_err!(self, err, chan, &msg.common_fields.temporary_channel_id, UNFUNDED_CHANNEL); let _: Result<(), _> = handle_error!(self, Err(res), *counterparty_node_id); } @@ -7379,7 +7439,7 @@ where None => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.common_fields.temporary_channel_id)) } }; - let tx_msg_opt = chan.begin_interactive_funding_tx_construction(&self.entropy_source) + let tx_msg_opt = chan.begin_interactive_funding_tx_construction(&self.entropy_source, self.get_our_node_id()) .map_err(|_| MsgHandleErrInternal::from_chan_no_close( ChannelError::Close( ( @@ -7701,7 +7761,6 @@ where #[cfg(any(dual_funding, splicing))] fn internal_tx_complete(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxComplete) -> Result<(), MsgHandleErrInternal> { - let best_block = *self.best_block.read().unwrap(); let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(counterparty_node_id) .ok_or_else(|| { @@ -7715,7 +7774,7 @@ where match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { let channel_phase = chan_phase_entry.get_mut(); - let (msg_send_event_opt, tx_opt) = match channel_phase { + let (msg_send_event_opt, signing_session_opt) = match channel_phase { ChannelPhase::UnfundedInboundV2(channel) => { channel.tx_complete(msg).into_msg_send_event(counterparty_node_id) }, @@ -7731,18 +7790,18 @@ where if let Some(msg_send_event) = msg_send_event_opt { peer_state.pending_msg_events.push(msg_send_event); } - if let Some(tx) = tx_opt { + if let Some(signing_session) = signing_session_opt { let (channel_id, channel_phase) = chan_phase_entry.remove_entry(); let res = match channel_phase { ChannelPhase::UnfundedOutboundV2(chan) => { - chan.funding_tx_constructed(tx, best_block, &self.signer_provider, &self.logger).map_err( + chan.funding_tx_constructed(counterparty_node_id, signing_session, &self.signer_provider, &self.logger).map_err( |(chan, err)| { (ChannelPhase::UnfundedOutboundV2(chan), err) } ) }, ChannelPhase::UnfundedInboundV2(chan) => { - chan.funding_tx_constructed(tx, best_block, &self.signer_provider, &self.logger).map_err( + chan.funding_tx_constructed(counterparty_node_id, signing_session, &self.signer_provider, &self.logger).map_err( |(chan, err)| { (ChannelPhase::UnfundedInboundV2(chan), err) } @@ -7753,7 +7812,11 @@ where .into()))), }; match res { - Ok((channel, commitment_signed)) => { + Ok((channel, commitment_signed, funding_ready_for_sig_event_opt)) => { + if let Some(funding_ready_for_sig_event) = funding_ready_for_sig_event_opt { + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back((funding_ready_for_sig_event, None)); + } peer_state.pending_msg_events.push(events::MessageSendEvent::UpdateHTLCs { node_id: counterparty_node_id.clone(), updates: CommitmentUpdate { @@ -7767,9 +7830,9 @@ where }); peer_state.channel_by_id.insert(channel_id.clone(), ChannelPhase::Funded(channel)); }, - Err((channel_phase, _)) => { + Err((channel_phase, err)) => { peer_state.channel_by_id.insert(channel_id, channel_phase); - return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("{}", counterparty_node_id), msg.channel_id)) + return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("{}", err), msg.channel_id)) }, } } @@ -7782,10 +7845,50 @@ where } #[cfg(any(dual_funding, splicing))] - fn internal_tx_signatures(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxSignatures) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Dual-funded channels not supported".to_owned(), - msg.channel_id.clone())), *counterparty_node_id); + fn internal_tx_signatures(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxSignatures) + -> Result<(), MsgHandleErrInternal> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id) + .ok_or_else(|| { + debug_assert!(false); + MsgHandleErrInternal::send_err_msg_no_close( + format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id), + msg.channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Occupied(mut chan_phase_entry) => { + let channel_phase = chan_phase_entry.get_mut(); + match channel_phase { + ChannelPhase::Funded(chan) => { + let (tx_signatures_opt, funding_tx_opt) = try_chan_phase_entry!(self, chan.tx_signatures(&msg), chan_phase_entry); + if let Some(tx_signatures) = tx_signatures_opt { + peer_state.pending_msg_events.push(events::MessageSendEvent::SendTxSignatures { + node_id: *counterparty_node_id, + msg: tx_signatures, + }); + } + if let Some(ref funding_tx) = funding_tx_opt { + self.tx_broadcaster.broadcast_transactions(&[funding_tx]); + { + let mut pending_events = self.pending_events.lock().unwrap(); + emit_channel_pending_event!(pending_events, chan); + } + } + }, + _ => try_chan_phase_entry!(self, Err(ChannelError::Close( + ( + "Got an unexpected tx_signatures message".into(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + ))), chan_phase_entry) + } + Ok(()) + }, + hash_map::Entry::Vacant(_) => { + Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + } + } } #[cfg(any(dual_funding, splicing))] @@ -7803,10 +7906,58 @@ where } #[cfg(any(dual_funding, splicing))] - fn internal_tx_abort(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxAbort) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Dual-funded channels not supported".to_owned(), - msg.channel_id.clone())), *counterparty_node_id); + fn maybe_reset_interactive_tx_state( + tx_constructor: &mut Option, + channel_id: ChannelId, + ) -> Option { + // If we have not reset the negtotiation state yet, we must echo back a `tx_abort`. + tx_constructor.take().map(|_| msgs::TxAbort { + channel_id, + data: "Acknowledged tx_abort".to_string().into_bytes(), + }) + } + + #[cfg(any(dual_funding, splicing))] + fn internal_tx_abort(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxAbort) + -> Result<(), MsgHandleErrInternal> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id) + .ok_or_else(|| { + debug_assert!(false); + MsgHandleErrInternal::send_err_msg_no_close( + format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id), + msg.channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Occupied(mut chan_phase_entry) => { + let channel_phase = chan_phase_entry.get_mut(); + let tx_abort_opt = match channel_phase { + ChannelPhase::UnfundedInboundV2(chan) => Self::maybe_reset_interactive_tx_state( + chan.interactive_tx_constructor_mut(), msg.channel_id), + ChannelPhase::UnfundedOutboundV2(chan) => Self::maybe_reset_interactive_tx_state( + chan.interactive_tx_constructor_mut(), msg.channel_id), + ChannelPhase::Funded(chan) => Self::maybe_reset_interactive_tx_state( + &mut chan.interactive_tx_constructor, msg.channel_id), + _ => try_chan_phase_entry!(self, Err(ChannelError::Close( + ( + "Got an unexpected tx_signatures message".into(), + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false) }, + ))), chan_phase_entry) + }; + if let Some(tx_abort) = tx_abort_opt { + peer_state.pending_msg_events.push(events::MessageSendEvent::SendTxAbort { + node_id: *counterparty_node_id, + msg: tx_abort, + }); + } + Ok(()) + }, + hash_map::Entry::Vacant(_) => { + Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + } + } } fn internal_channel_ready(&self, counterparty_node_id: &PublicKey, msg: &msgs::ChannelReady) -> Result<(), MsgHandleErrInternal> { @@ -8201,18 +8352,26 @@ where // TODO(dual_funding): Remove this allow directive after #[cfg(dual_funding)] is dropped. #[allow(unused_mut, unused_assignments)] - let mut has_dual_funding_channel_context = false; + let mut interactive_tx_signing_in_progress = false; #[cfg(any(dual_funding, splicing))] { - has_dual_funding_channel_context = chan.dual_funding_channel_context.is_some(); + interactive_tx_signing_in_progress = chan.interactive_tx_signing_session.is_some(); } - if has_dual_funding_channel_context { + if interactive_tx_signing_in_progress { #[cfg(any(dual_funding, splicing))] { let monitor = try_chan_phase_entry!(self, chan.commitment_signed_initial_v2(&msg, best_block, &self.signer_provider, &&logger), chan_phase_entry); if let Ok(persist_status) = self.chain_monitor.watch_channel(chan.context.get_funding_txo().unwrap(), monitor) { + if let Some(tx_signatures) = chan.interactive_tx_signing_session.as_mut().map(|session| session.received_commitment_signed(msg.clone())).flatten() { + // At this point we have received a commitment signed and we are watching the channel so + // we're good to send our tx_signatures if we're up first. + peer_state.pending_msg_events.push(events::MessageSendEvent::SendTxSignatures { + node_id: *counterparty_node_id, + msg: tx_signatures, + }); + } handle_new_monitor_update!(self, persist_status, peer_state_lock, peer_state, per_peer_state, chan, INITIAL_MONITOR); } else { try_chan_phase_entry!(self, Err(ChannelError::Close( @@ -10922,9 +11081,17 @@ where } fn handle_tx_signatures(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxSignatures) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Dual-funded channels not supported".to_owned(), - msg.channel_id.clone())), *counterparty_node_id); + #[cfg(any(dual_funding, splicing))] + { + let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + let _ = handle_error!(self, self.internal_tx_signatures(counterparty_node_id, msg), *counterparty_node_id); + }; + #[cfg(not(any(dual_funding, splicing)))] + { + let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( + "Dual-funded channels not supported".to_owned(), + msg.channel_id.clone())), *counterparty_node_id); + }; } fn handle_tx_init_rbf(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxInitRbf) { @@ -10940,9 +11107,22 @@ where } fn handle_tx_abort(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxAbort) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Dual-funded channels not supported".to_owned(), - msg.channel_id.clone())), *counterparty_node_id); + #[cfg(any(dual_funding, splicing))] + { + // Note that we never need to persist the updated ChannelManager for an inbound + // tx_add_input message - interactive transaction construction does not need to + // be persisted before any signatures are exchanged. + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let _ = handle_error!(self, self.internal_tx_abort(counterparty_node_id, msg), *counterparty_node_id); + NotifyOption::SkipPersistHandleEvents + }); + }; + #[cfg(not(any(dual_funding, splicing)))] + { + let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( + "Dual-funded channels not supported".to_owned(), + msg.channel_id.clone())), *counterparty_node_id); + }; } } @@ -12856,11 +13036,17 @@ where #[cfg(test)] mod tests { + #[cfg(any(dual_funding, splicing))] + use bitcoin::Witness; use bitcoin::hashes::Hash; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use core::sync::atomic::Ordering; + #[cfg(any(dual_funding, splicing))] + use crate::chain::chaininterface::ConfirmationTarget; use crate::events::{Event, HTLCDestination, MessageSendEvent, MessageSendEventsProvider, ClosureReason}; + #[cfg(any(dual_funding, splicing))] + use crate::events::InboundChannelFunds; use crate::ln::types::{ChannelId, PaymentPreimage, PaymentHash, PaymentSecret}; use crate::ln::channelmanager::{create_recv_pending_htlc_info, HTLCForwardInfo, inbound_payment, PaymentId, PaymentSendFailure, RecipientOnionFields, InterceptId}; use crate::ln::functional_test_utils::*; @@ -14263,7 +14449,232 @@ mod tests { } // Dual-funding: V2 Channel Establishment Tests - // TODO(dual_funding): Complete these. + #[cfg(any(dual_funding, splicing))] + struct V2ChannelEstablishmentTestSession { + initiator_input_value_satoshis: u64, + acceptor_input_value_satoshis: u64, + } + + #[cfg(any(dual_funding, splicing))] + fn do_test_v2_channel_establishment(session: V2ChannelEstablishmentTestSession) { + let acceptor_will_fund = session.acceptor_input_value_satoshis > 0; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let acceptor_config = if acceptor_will_fund { + let mut acceptor_config = test_default_channel_config(); + acceptor_config.manually_accept_inbound_channels = true; + Some(acceptor_config) + } else { + None + }; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, acceptor_config]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + // Create a funding input for the new channel along with its previous transaction. + let initiator_funding_inputs = create_dual_funding_utxos_with_prev_txs(&nodes[0], &[session.initiator_input_value_satoshis]); + + // Alice creates a dual-funded channel as initiator. + nodes[0].node.create_dual_funded_channel( + nodes[1].node.get_our_node_id(), initiator_funding_inputs, + Some(ConfirmationTarget::NonAnchorChannelFee), 42, None, + ).unwrap(); + let open_channel_v2_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannelV2, nodes[1].node.get_our_node_id()); + + assert_eq!(nodes[0].node.list_channels().len(), 1); + + nodes[1].node.handle_open_channel_v2(&nodes[0].node.get_our_node_id(), &open_channel_v2_msg); + + if acceptor_will_fund { + // Since `manually_accept_inbound_channels` is set to true for Bob's node, he can contribute to + // the dual-funded channel. + let temporary_channel_id = if let Event::OpenChannelRequest { + temporary_channel_id, + counterparty_node_id, + acceptor_funds, + .. + } = get_event!(nodes[1], Event::OpenChannelRequest) { + assert_eq!(counterparty_node_id, nodes[0].node.get_our_node_id()); + assert!(matches!(acceptor_funds, InboundChannelFunds::DualFunded)); + temporary_channel_id + } else { panic!(); }; + + // Bob contributes to the channel providing an input. + let acceptor_funding_inputs = create_dual_funding_utxos_with_prev_txs(&nodes[1], &[session.acceptor_input_value_satoshis]); + + nodes[1].node.accept_inbound_channel_with_contribution( + &temporary_channel_id, &nodes[0].node.get_our_node_id(), 1337, acceptor_funding_inputs + ).unwrap(); + } + + let accept_channel_v2_msg = get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannelV2, nodes[0].node.get_our_node_id()); + + nodes[0].node.handle_accept_channel_v2(&nodes[1].node.get_our_node_id(), &accept_channel_v2_msg); + + let tx_add_input_msg = get_event_msg!(&nodes[0], MessageSendEvent::SendTxAddInput, nodes[1].node.get_our_node_id()); + let input_value = tx_add_input_msg.prevtx.as_transaction().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_will_fund { + let tx_add_input_msg = get_event_msg!(nodes[1], MessageSendEvent::SendTxAddInput, nodes[0].node.get_our_node_id()); + let input_value = tx_add_input_msg.prevtx.as_transaction().output[tx_add_input_msg.prevtx_out as usize].value; + assert_eq!(input_value.to_sat(), session.acceptor_input_value_satoshis); + nodes[0].node.handle_tx_add_input(&nodes[1].node.get_our_node_id(), &tx_add_input_msg); + } else { + let tx_complete_msg = get_event_msg!(nodes[1], MessageSendEvent::SendTxComplete, nodes[0].node.get_our_node_id()); + nodes[0].node.handle_tx_complete(&nodes[1].node.get_our_node_id(), &tx_complete_msg); + }; + + let tx_add_output_msg = get_event_msg!(&nodes[0], MessageSendEvent::SendTxAddOutput, nodes[1].node.get_our_node_id()); + nodes[1].node.handle_tx_add_output(&nodes[0].node.get_our_node_id(), &tx_add_output_msg); + + let tx_complete_msg = get_event_msg!(nodes[1], MessageSendEvent::SendTxComplete, nodes[0].node.get_our_node_id()); + nodes[0].node.handle_tx_complete(&nodes[1].node.get_our_node_id(), &tx_complete_msg); + + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2); + let tx_complete_msg = match msg_events[0] { + MessageSendEvent::SendTxComplete { ref node_id, ref msg } => { + assert_eq!(*node_id, nodes[1].node.get_our_node_id()); + (*msg).clone() + }, + _ => panic!("Unexpected event"), + }; + let msg_commitment_signed_from_0 = match msg_events[1] { + MessageSendEvent::UpdateHTLCs { ref node_id, ref updates } => { + assert_eq!(*node_id, nodes[1].node.get_our_node_id()); + updates.commitment_signed.clone() + }, + _ => panic!("Unexpected event"), + }; + if let Event::FundingTransactionReadyForSigning { + channel_id, + counterparty_node_id, + mut unsigned_transaction, + .. + } = get_event!(nodes[0], Event::FundingTransactionReadyForSigning) { + assert_eq!(counterparty_node_id, nodes[1].node.get_our_node_id()); + + let mut witness = Witness::new(); + witness.push(vec![0]); + unsigned_transaction.input[0].witness = witness; + + nodes[0].node.funding_transaction_signed(&channel_id, &counterparty_node_id, unsigned_transaction).unwrap(); + } else { panic!(); } + + 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(); + assert_eq!(msg_events.len(), 1); + let msg_commitment_signed_from_1 = match msg_events[0] { + MessageSendEvent::UpdateHTLCs { ref node_id, ref updates } => { + assert_eq!(*node_id, nodes[0].node.get_our_node_id()); + updates.commitment_signed.clone() + }, + _ => panic!("Unexpected event"), + }; + + if acceptor_will_fund { + if let Event::FundingTransactionReadyForSigning { + channel_id, + counterparty_node_id, + mut unsigned_transaction, + .. + } = get_event!(nodes[1], Event::FundingTransactionReadyForSigning) { + assert_eq!(counterparty_node_id, nodes[0].node.get_our_node_id()); + + let mut witness = Witness::new(); + witness.push(vec![0]); + unsigned_transaction.input[0].witness = witness; + + nodes[1].node.funding_transaction_signed(&channel_id, &counterparty_node_id, unsigned_transaction).unwrap(); + } else { panic!(); } + } + + // 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); + nodes[0].node.handle_commitment_signed(&nodes[1].node.get_our_node_id(), &msg_commitment_signed_from_1); + check_added_monitors(&nodes[0], 1); + check_added_monitors(&nodes[1], 1); + + let tx_signatures_exchange = |first: usize, second: usize| { + let msg_events = nodes[second].node.get_and_clear_pending_msg_events(); + assert!(msg_events.is_empty()); + let tx_signatures_from_first = get_event_msg!(nodes[first], MessageSendEvent::SendTxSignatures, nodes[second].node.get_our_node_id()); + + nodes[second].node.handle_tx_signatures(&nodes[first].node.get_our_node_id(), &tx_signatures_from_first); + let events_0 = nodes[second].node.get_and_clear_pending_events(); + assert_eq!(events_0.len(), 1); + match events_0[0] { + Event::ChannelPending{ ref counterparty_node_id, .. } => { + assert_eq!(*counterparty_node_id, nodes[first].node.get_our_node_id()); + }, + _ => panic!("Unexpected event"), + } + let tx_signatures_from_second = get_event_msg!(nodes[second], MessageSendEvent::SendTxSignatures, nodes[first].node.get_our_node_id()); + nodes[first].node.handle_tx_signatures(&nodes[second].node.get_our_node_id(), &tx_signatures_from_second); + let events_1 = nodes[first].node.get_and_clear_pending_events(); + assert_eq!(events_1.len(), 1); + match events_1[0] { + Event::ChannelPending{ ref counterparty_node_id, .. } => { + assert_eq!(*counterparty_node_id, nodes[second].node.get_our_node_id()); + }, + _ => panic!("Unexpected event"), + } + }; + + if session.initiator_input_value_satoshis < session.acceptor_input_value_satoshis + || session.initiator_input_value_satoshis == session.acceptor_input_value_satoshis + && nodes[0].node.our_network_pubkey.serialize() < nodes[1].node.our_network_pubkey.serialize() { + // Alice contributed less input value than Bob so he should send tx_signatures only after + // receiving tx_signatures from Alice. + tx_signatures_exchange(0, 1); + } else { + // Alice contributed more input value than Bob so she should send tx_signatures only after + // receiving tx_signatures from Bob. + tx_signatures_exchange(1, 0); + } + + let tx = { + let tx_0 = &nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap()[0]; + let tx_1 = &nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap()[0]; + assert_eq!(tx_0, tx_1); + tx_0.clone() + }; + + let (channel_ready, _) = create_chan_between_nodes_with_value_confirm(&nodes[0], &nodes[1], &tx); + let (announcement, nodes_0_update, nodes_1_update) = create_chan_between_nodes_with_value_b(&nodes[0], &nodes[1], &channel_ready); + update_nodes_with_chan_announce(&nodes, 0, 1, &announcement, &nodes_0_update, &nodes_1_update); + } + + #[test] + #[cfg(any(dual_funding, splicing))] + fn test_v2_channel_establishment() { + // Only initiator contributes + do_test_v2_channel_establishment(V2ChannelEstablishmentTestSession { + initiator_input_value_satoshis: 100_000, + acceptor_input_value_satoshis: 0, + }); + + // Both peers contribute but initiator contributes more input value + do_test_v2_channel_establishment(V2ChannelEstablishmentTestSession { + initiator_input_value_satoshis: 100_000, + acceptor_input_value_satoshis: 80_000, + }); + + // Both peers contribute but acceptor contributes more input value + do_test_v2_channel_establishment(V2ChannelEstablishmentTestSession { + initiator_input_value_satoshis: 80_000, + acceptor_input_value_satoshis: 100_000, + }); + + // Both peers contribute the same input value + do_test_v2_channel_establishment(V2ChannelEstablishmentTestSession { + initiator_input_value_satoshis: 80_000, + acceptor_input_value_satoshis: 80_000, + }); + } } #[cfg(ldk_bench)] diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 8c08d37f5f6..dd300c34928 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -37,17 +37,20 @@ use crate::util::test_utils; use crate::util::test_utils::{panicking, TestChainMonitor, TestScorer, TestKeysInterface}; use crate::util::ser::{ReadableArgs, Writeable}; +use bitcoin::WPubkeyHash; use bitcoin::amount::Amount; -use bitcoin::blockdata::block::{Block, Header, Version}; -use bitcoin::blockdata::locktime::absolute::LockTime; -use bitcoin::blockdata::transaction::{Transaction, TxIn, TxOut}; +use bitcoin::blockdata::block::{Block, Header, Version as BlockVersion}; +use bitcoin::blockdata::locktime::absolute::{LOCK_TIME_THRESHOLD, LockTime}; +use bitcoin::blockdata::script::ScriptBuf; +use bitcoin::blockdata::transaction::{Sequence, Transaction, TxIn, TxOut}; +use bitcoin::blockdata::witness::Witness; use bitcoin::hash_types::{BlockHash, TxMerkleNode}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash as _; use bitcoin::network::Network; use bitcoin::pow::CompactTarget; use bitcoin::secp256k1::{PublicKey, SecretKey}; -use bitcoin::transaction; +use bitcoin::transaction::{self, Version as TxVersion}; use alloc::rc::Rc; use core::cell::RefCell; @@ -89,7 +92,7 @@ pub fn mine_transaction_without_consistency_checks<'a, 'b, 'c, 'd>(node: &'a Nod let height = node.best_block_info().1 + 1; let mut block = Block { header: Header { - version: Version::NO_SOFT_FORK_SIGNALLING, + version: BlockVersion::NO_SOFT_FORK_SIGNALLING, prev_blockhash: node.best_block_hash(), merkle_root: TxMerkleNode::all_zeros(), time: height, @@ -221,7 +224,7 @@ impl ConnectStyle { pub fn create_dummy_header(prev_blockhash: BlockHash, time: u32) -> Header { Header { - version: Version::NO_SOFT_FORK_SIGNALLING, + version: BlockVersion::NO_SOFT_FORK_SIGNALLING, prev_blockhash, merkle_root: TxMerkleNode::all_zeros(), time, @@ -1165,6 +1168,37 @@ pub fn create_coinbase_funding_transaction<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, internal_create_funding_transaction(node, expected_counterparty_node_id, expected_chan_value, expected_user_chan_id, true) } +pub fn create_dual_funding_utxos_with_prev_txs<'a, 'b, 'c>( + node: &Node<'a, 'b, 'c>, utxo_values_in_satoshis: &[u64], +) -> Vec<(TxIn, Transaction)> { + // Ensure we have unique transactions per node by using the locktime. + let tx = Transaction { + version: TxVersion::TWO, + lock_time: LockTime::from_height( + u32::from_be_bytes(node.keys_manager.get_secure_random_bytes()[0..4].try_into().unwrap()) % LOCK_TIME_THRESHOLD + ).unwrap(), + input: vec![], + output: utxo_values_in_satoshis.iter().map(|value_satoshis| TxOut { + value: Amount::from_sat(*value_satoshis), script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + }).collect() + }; + + let mut result = vec![]; + for i in 0..utxo_values_in_satoshis.len() { + result.push( + (TxIn { + previous_output: OutPoint { + txid: tx.txid(), + index: i as u16, + }.into_bitcoin_outpoint(), + script_sig: ScriptBuf::new(), + sequence: Sequence::ZERO, + witness: Witness::new(), + }, tx.clone())); + } + result +} + fn internal_create_funding_transaction<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, expected_counterparty_node_id: &PublicKey, expected_chan_value: u64, expected_user_chan_id: u128, coinbase: bool) -> (ChannelId, Transaction, OutPoint) { diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 6a74e360b39..e1ac2d2dedd 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -19,7 +19,7 @@ use bitcoin::secp256k1::PublicKey; use bitcoin::transaction::Version; use bitcoin::{ absolute::LockTime as AbsoluteLockTime, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, - TxOut, Txid, Weight, + TxOut, Txid, Weight, Witness, }; use crate::chain::chaininterface::fee_for_weight; @@ -27,7 +27,7 @@ use crate::events::bump_transaction::{BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT use crate::events::MessageSendEvent; use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; use crate::ln::msgs; -use crate::ln::msgs::SerialId; +use crate::ln::msgs::{CommitmentSigned, SerialId, TxSignatures}; use crate::ln::types::ChannelId; use crate::sign::{EntropySource, P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT}; use crate::util::ser::TransactionU16LenLimited; @@ -101,6 +101,12 @@ pub(crate) enum AbortReason { InsufficientFees, OutputsValueExceedsInputsValue, InvalidTx, + /// More than one funding (shared) output found. + DuplicateFundingOutput, + /// The intended local part of the funding output is higher than the actual shared funding output, + /// if funding output is provided by the peer this is an interop error, + /// if provided by the same node than internal input consistency error. + InvalidLowFundingOutputValue, } impl AbortReason { @@ -129,6 +135,10 @@ impl AbortReason { "Total value of outputs exceeds total value of inputs" }, AbortReason::InvalidTx => "The transaction is invalid", + AbortReason::DuplicateFundingOutput => "More than one funding (shared) output found", + AbortReason::InvalidLowFundingOutputValue => { + "The intended local part of the funding output is higher than the actual shared funding output" + }, } .to_string(); @@ -136,25 +146,6 @@ impl AbortReason { } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct InteractiveTxInput { - serial_id: SerialId, - input: TxIn, - prev_output: TxOut, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct InteractiveTxOutput { - serial_id: SerialId, - tx_out: TxOut, -} - -impl InteractiveTxOutput { - pub fn tx_out(&self) -> &TxOut { - &self.tx_out - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct ConstructedTransaction { holder_is_initiator: bool, @@ -169,6 +160,7 @@ pub(crate) struct ConstructedTransaction { remote_outputs_value_satoshis: u64, lock_time: AbsoluteLockTime, + holder_sends_tx_signatures_first: bool, } impl ConstructedTransaction { @@ -176,18 +168,31 @@ impl ConstructedTransaction { let local_inputs_value_satoshis = context .inputs .iter() - .filter(|(serial_id, _)| { - !is_serial_id_valid_for_counterparty(context.holder_is_initiator, serial_id) - }) - .fold(0u64, |value, (_, input)| value.saturating_add(input.prev_output.value.to_sat())); + .fold(0u64, |value, (_, input)| value.saturating_add(input.local_value())); let local_outputs_value_satoshis = context .outputs .iter() - .filter(|(serial_id, _)| { - !is_serial_id_valid_for_counterparty(context.holder_is_initiator, serial_id) - }) - .fold(0u64, |value, (_, output)| value.saturating_add(output.tx_out.value.to_sat())); + .fold(0u64, |value, (_, output)| value.saturating_add(output.local_value())); + + let remote_inputs_value_satoshis = context.remote_inputs_value(); + let remote_outputs_value_satoshis = context.remote_outputs_value(); + let mut inputs: Vec = context.inputs.into_values().collect(); + let mut outputs: Vec = context.outputs.into_values().collect(); + // Inputs and outputs must be sorted by serial_id + inputs.sort_unstable_by_key(|input| input.serial_id()); + outputs.sort_unstable_by_key(|output| output.serial_id); + + // There is a strict ordering for `tx_signatures` exchange to prevent deadlocks. + let holder_sends_tx_signatures_first = + if local_inputs_value_satoshis == remote_inputs_value_satoshis { + // If the amounts are the same then the peer with the lowest pubkey lexicographically sends its + // tx_signatures first + context.holder_node_id.serialize() < context.counterparty_node_id.serialize() + } else { + // Otherwise the peer with the lowest contributed input value sends its tx_signatures first. + local_inputs_value_satoshis < remote_inputs_value_satoshis + }; Self { holder_is_initiator: context.holder_is_initiator, @@ -195,29 +200,24 @@ impl ConstructedTransaction { local_inputs_value_satoshis, local_outputs_value_satoshis, - remote_inputs_value_satoshis: context.remote_inputs_value(), - remote_outputs_value_satoshis: context.remote_outputs_value(), + remote_inputs_value_satoshis, + remote_outputs_value_satoshis, - inputs: context.inputs.into_values().collect(), - outputs: context.outputs.into_values().collect(), + inputs, + outputs, lock_time: context.tx_locktime, + holder_sends_tx_signatures_first, } } pub fn weight(&self) -> Weight { - let inputs_weight = self.inputs.iter().fold( - Weight::from_wu(0), - |weight, InteractiveTxInput { prev_output, .. }| { - weight.checked_add(estimate_input_weight(prev_output)).unwrap_or(Weight::MAX) - }, - ); - let outputs_weight = self.outputs.iter().fold( - Weight::from_wu(0), - |weight, InteractiveTxOutput { tx_out, .. }| { - weight.checked_add(get_output_weight(&tx_out.script_pubkey)).unwrap_or(Weight::MAX) - }, - ); + let inputs_weight = self.inputs.iter().fold(Weight::from_wu(0), |weight, input| { + weight.checked_add(estimate_input_weight(input.prev_output())).unwrap_or(Weight::MAX) + }); + let outputs_weight = self.outputs.iter().fold(Weight::from_wu(0), |weight, output| { + weight.checked_add(get_output_weight(&output.script_pubkey())).unwrap_or(Weight::MAX) + }); Weight::from_wu(TX_COMMON_FIELDS_WEIGHT) .checked_add(inputs_weight) .and_then(|weight| weight.checked_add(outputs_weight)) @@ -225,16 +225,11 @@ impl ConstructedTransaction { } pub fn into_unsigned_tx(self) -> Transaction { - // Inputs and outputs must be sorted by serial_id - let ConstructedTransaction { mut inputs, mut outputs, .. } = self; - - inputs.sort_unstable_by_key(|InteractiveTxInput { serial_id, .. }| *serial_id); - outputs.sort_unstable_by_key(|InteractiveTxOutput { serial_id, .. }| *serial_id); + let ConstructedTransaction { inputs, outputs, .. } = self; - let input: Vec = - inputs.into_iter().map(|InteractiveTxInput { input, .. }| input).collect(); + let input: Vec = inputs.into_iter().map(|input| input.txin().clone()).collect(); let output: Vec = - outputs.into_iter().map(|InteractiveTxOutput { tx_out, .. }| tx_out).collect(); + outputs.into_iter().map(|output| output.tx_out().clone()).collect(); Transaction { version: Version::TWO, lock_time: self.lock_time, input, output } } @@ -250,17 +245,161 @@ impl ConstructedTransaction { pub fn txid(&self) -> Txid { self.clone().into_unsigned_tx().txid() } + + pub fn add_local_witnesses(&mut self, witnesses: Vec) { + self.inputs + .iter_mut() + .filter(|input| { + !is_serial_id_valid_for_counterparty(self.holder_is_initiator, input.serial_id()) + }) + .map(|input| input.txin_mut()) + .zip(witnesses.into_iter()) + .for_each(|(input, witness)| input.witness = witness); + } + + pub fn add_remote_witnesses(&mut self, witnesses: Vec) { + self.inputs + .iter_mut() + .filter(|input| { + is_serial_id_valid_for_counterparty(self.holder_is_initiator, input.serial_id()) + }) + .map(|input| input.txin_mut()) + .zip(witnesses.into_iter()) + .for_each(|(input, witness)| input.witness = witness); + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct InteractiveTxSigningSession { + pub unsigned_tx: ConstructedTransaction, + holder_sends_tx_signatures_first: bool, + sent_commitment_signed: Option, + received_commitment_signed: Option, + holder_tx_signatures: Option, + counterparty_tx_signatures: Option, +} + +impl InteractiveTxSigningSession { + pub fn received_commitment_signed( + &mut self, commitment_signed: CommitmentSigned, + ) -> Option { + self.received_commitment_signed = Some(commitment_signed); + if self.holder_sends_tx_signatures_first { + self.holder_tx_signatures.clone() + } else { + None + } + } + + pub fn received_tx_signatures( + &mut self, tx_signatures: TxSignatures, + ) -> (Option, Option) { + if self.counterparty_tx_signatures.is_some() { + return (None, None); + }; + self.counterparty_tx_signatures = Some(tx_signatures.clone()); + self.unsigned_tx.add_remote_witnesses(tx_signatures.witnesses.clone()); + + let holder_tx_signatures = if !self.holder_sends_tx_signatures_first { + self.holder_tx_signatures.clone() + } else { + None + }; + + let funding_tx = if self.holder_tx_signatures.is_some() { + Some(self.finalize_funding_tx()) + } else { + None + }; + + (holder_tx_signatures, funding_tx) + } + + pub fn provide_holder_witnesses( + &mut self, channel_id: ChannelId, witnesses: Vec, + ) -> Option { + self.unsigned_tx.add_local_witnesses(witnesses.clone()); + self.holder_tx_signatures = Some(TxSignatures { + channel_id, + tx_hash: self.unsigned_tx.txid(), + witnesses: witnesses.into_iter().map(|witness| witness).collect(), + funding_outpoint_sig: None, + }); + if self.received_commitment_signed.is_some() + && (self.holder_sends_tx_signatures_first || self.counterparty_tx_signatures.is_some()) + { + self.holder_tx_signatures.clone() + } else { + None + } + } + + pub fn remote_inputs_count(&self) -> usize { + self.unsigned_tx + .inputs + .iter() + .filter(|input| { + is_serial_id_valid_for_counterparty( + self.unsigned_tx.holder_is_initiator, + input.serial_id(), + ) + }) + .count() + } + + pub fn local_inputs_count(&self) -> usize { + self.unsigned_tx + .inputs + .iter() + .filter(|input| { + !is_serial_id_valid_for_counterparty( + self.unsigned_tx.holder_is_initiator, + input.serial_id(), + ) + }) + .count() + } + + fn finalize_funding_tx(&mut self) -> Transaction { + let lock_time = self.unsigned_tx.lock_time; + let ConstructedTransaction { inputs, outputs, .. } = &mut self.unsigned_tx; + + Transaction { + version: Version::TWO, + lock_time, + input: inputs.iter().cloned().map(|input| input.into_txin()).collect(), + output: outputs.iter().cloned().map(|output| output.into_tx_out()).collect(), + } + } } #[derive(Debug)] struct NegotiationContext { + holder_node_id: PublicKey, + counterparty_node_id: PublicKey, holder_is_initiator: bool, received_tx_add_input_count: u16, received_tx_add_output_count: u16, inputs: HashMap, + /// The output script intended to be the new funding output script. + /// The script pubkey is used to determine which output is the funding output. + /// When an output with the same script pubkey is added by any of the nodes, it will be + /// treated as the shared output. + /// The value is the holder's intended contribution to the shared funding output. + /// The rest is the counterparty's contribution. + /// When the funding output is added (recognized by its output script pubkey), it will be marked + /// as shared, and split between the peers according to the local value. + /// If the local value is found to be larger than the actual funding output, an error is generated. + expected_shared_funding_output: Option<(ScriptBuf, u64)>, + /// The actual new funding output, set only after the output has actually been added. + /// NOTE: this output is also included in `outputs`. + actual_new_funding_output: Option, prevtx_outpoints: HashSet, + /// The outputs added so far. outputs: HashMap, + /// The locktime of the funding transaction. tx_locktime: AbsoluteLockTime, + /// The fee rate used for the transaction feerate_sat_per_kw: u32, } @@ -283,41 +422,71 @@ pub(crate) fn get_output_weight(script_pubkey: &ScriptBuf) -> Weight { ) } -fn is_serial_id_valid_for_counterparty(holder_is_initiator: bool, serial_id: &SerialId) -> bool { +fn is_serial_id_valid_for_counterparty(holder_is_initiator: bool, serial_id: SerialId) -> bool { // A received `SerialId`'s parity must match the role of the counterparty. holder_is_initiator == serial_id.is_for_non_initiator() } impl NegotiationContext { - fn is_serial_id_valid_for_counterparty(&self, serial_id: &SerialId) -> bool { + fn new( + holder_node_id: PublicKey, counterparty_node_id: PublicKey, holder_is_initiator: bool, + expected_shared_funding_output: Option<(ScriptBuf, u64)>, tx_locktime: AbsoluteLockTime, + feerate_sat_per_kw: u32, + ) -> Self { + NegotiationContext { + holder_node_id, + counterparty_node_id, + holder_is_initiator, + received_tx_add_input_count: 0, + received_tx_add_output_count: 0, + inputs: new_hash_map(), + expected_shared_funding_output, + actual_new_funding_output: None, + prevtx_outpoints: new_hash_set(), + outputs: new_hash_map(), + tx_locktime, + feerate_sat_per_kw, + } + } + + fn set_actual_new_funding_output( + &mut self, tx_out: TxOut, + ) -> Result { + if self.actual_new_funding_output.is_some() { + return Err(AbortReason::DuplicateFundingOutput); + } + + assert!(self.expected_shared_funding_output.is_some()); + let value = tx_out.value; + let local_owned = self.expected_shared_funding_output.as_ref().unwrap().1; + // Sanity check + if local_owned > value.to_sat() { + return Err(AbortReason::InvalidLowFundingOutputValue); + } + let shared_output = SharedOwnedOutput::new(tx_out, local_owned); + self.actual_new_funding_output = Some(shared_output.clone()); + Ok(shared_output) + } + + fn is_serial_id_valid_for_counterparty(&self, serial_id: SerialId) -> bool { is_serial_id_valid_for_counterparty(self.holder_is_initiator, serial_id) } fn remote_inputs_value(&self) -> u64 { - self.inputs - .iter() - .filter(|(serial_id, _)| self.is_serial_id_valid_for_counterparty(serial_id)) - .fold(0u64, |acc, (_, InteractiveTxInput { prev_output, .. })| { - acc.saturating_add(prev_output.value.to_sat()) - }) + self.inputs.iter().fold(0u64, |acc, (_, input)| acc.saturating_add(input.remote_value())) } fn remote_outputs_value(&self) -> u64 { - self.outputs - .iter() - .filter(|(serial_id, _)| self.is_serial_id_valid_for_counterparty(serial_id)) - .fold(0u64, |acc, (_, InteractiveTxOutput { tx_out, .. })| { - acc.saturating_add(tx_out.value.to_sat()) - }) + self.outputs.iter().fold(0u64, |acc, (_, output)| acc.saturating_add(output.remote_value())) } fn remote_inputs_weight(&self) -> Weight { Weight::from_wu( self.inputs .iter() - .filter(|(serial_id, _)| self.is_serial_id_valid_for_counterparty(serial_id)) - .fold(0u64, |weight, (_, InteractiveTxInput { prev_output, .. })| { - weight.saturating_add(estimate_input_weight(prev_output).to_wu()) + .filter(|(serial_id, _)| self.is_serial_id_valid_for_counterparty(**serial_id)) + .fold(0u64, |weight, (_, input)| { + weight.saturating_add(estimate_input_weight(input.prev_output()).to_wu()) }), ) } @@ -326,20 +495,26 @@ impl NegotiationContext { Weight::from_wu( self.outputs .iter() - .filter(|(serial_id, _)| self.is_serial_id_valid_for_counterparty(serial_id)) - .fold(0u64, |weight, (_, InteractiveTxOutput { tx_out, .. })| { - weight.saturating_add(get_output_weight(&tx_out.script_pubkey).to_wu()) + .filter(|(serial_id, _)| self.is_serial_id_valid_for_counterparty(**serial_id)) + .fold(0u64, |weight, (_, output)| { + weight.saturating_add(get_output_weight(&output.script_pubkey()).to_wu()) }), ) } + fn local_inputs_value(&self) -> u64 { + self.inputs + .iter() + .fold(0u64, |acc, (_, input)| acc.saturating_add(input.prev_output().value.to_sat())) + } + fn received_tx_add_input(&mut self, msg: &msgs::TxAddInput) -> Result<(), AbortReason> { // The interactive-txs spec calls for us to fail negotiation if the `prevtx` we receive is // invalid. However, we would not need to account for this explicit negotiation failure // mode here since `PeerManager` would already disconnect the peer if the `prevtx` is // invalid; implicitly ending the negotiation. - if !self.is_serial_id_valid_for_counterparty(&msg.serial_id) { + if !self.is_serial_id_valid_for_counterparty(msg.serial_id) { // The receiving node: // - MUST fail the negotiation if: // - the `serial_id` has the wrong parity @@ -400,7 +575,7 @@ impl NegotiationContext { }, hash_map::Entry::Vacant(entry) => { let prev_outpoint = OutPoint { txid, vout: msg.prevtx_out }; - entry.insert(InteractiveTxInput { + entry.insert(InteractiveTxInput::Remote(LocalOrRemoteInput { serial_id: msg.serial_id, input: TxIn { previous_output: prev_outpoint, @@ -408,7 +583,7 @@ impl NegotiationContext { ..Default::default() }, prev_output: prev_out, - }); + })); self.prevtx_outpoints.insert(prev_outpoint); Ok(()) }, @@ -416,7 +591,7 @@ impl NegotiationContext { } fn received_tx_remove_input(&mut self, msg: &msgs::TxRemoveInput) -> Result<(), AbortReason> { - if !self.is_serial_id_valid_for_counterparty(&msg.serial_id) { + if !self.is_serial_id_valid_for_counterparty(msg.serial_id) { return Err(AbortReason::IncorrectSerialIdParity); } @@ -434,7 +609,7 @@ impl NegotiationContext { // The receiving node: // - MUST fail the negotiation if: // - the serial_id has the wrong parity - if !self.is_serial_id_valid_for_counterparty(&msg.serial_id) { + if !self.is_serial_id_valid_for_counterparty(msg.serial_id) { return Err(AbortReason::IncorrectSerialIdParity); } @@ -457,7 +632,7 @@ impl NegotiationContext { // bitcoin supply. let mut outputs_value: u64 = 0; for output in self.outputs.iter() { - outputs_value = outputs_value.saturating_add(output.1.tx_out.value.to_sat()); + outputs_value = outputs_value.saturating_add(output.1.value()); } if outputs_value.saturating_add(msg.sats) > TOTAL_BITCOIN_SUPPLY_SATOSHIS { // The receiving node: @@ -486,6 +661,29 @@ impl NegotiationContext { return Err(AbortReason::InvalidOutputScript); } + let txout = TxOut { value: Amount::from_sat(msg.sats), script_pubkey: msg.script.clone() }; + let is_shared = if let Some((shared_funding_script, _local_value)) = + &self.expected_shared_funding_output + { + msg.script == *shared_funding_script + } else { + false + }; + let output = if is_shared { + // this is a shared funding output + let shared_output = self.set_actual_new_funding_output(txout)?; + InteractiveTxOutput { + serial_id: msg.serial_id, + added_by: AddingRole::Remote, + output: OutputOwned::Shared(shared_output), + } + } else { + InteractiveTxOutput { + serial_id: msg.serial_id, + added_by: AddingRole::Remote, + output: OutputOwned::Single(txout), + } + }; match self.outputs.entry(msg.serial_id) { hash_map::Entry::Occupied(_) => { // The receiving node: @@ -494,20 +692,14 @@ impl NegotiationContext { Err(AbortReason::DuplicateSerialId) }, hash_map::Entry::Vacant(entry) => { - entry.insert(InteractiveTxOutput { - serial_id: msg.serial_id, - tx_out: TxOut { - value: Amount::from_sat(msg.sats), - script_pubkey: msg.script.clone(), - }, - }); + entry.insert(output); Ok(()) }, } } fn received_tx_remove_output(&mut self, msg: &msgs::TxRemoveOutput) -> Result<(), AbortReason> { - if !self.is_serial_id_valid_for_counterparty(&msg.serial_id) { + if !self.is_serial_id_valid_for_counterparty(msg.serial_id) { return Err(AbortReason::IncorrectSerialIdParity); } if self.outputs.remove(&msg.serial_id).is_some() { @@ -523,35 +715,50 @@ impl NegotiationContext { fn sent_tx_add_input(&mut self, msg: &msgs::TxAddInput) -> Result<(), AbortReason> { let tx = msg.prevtx.as_transaction(); - let input = TxIn { + let txin = TxIn { previous_output: OutPoint { txid: tx.txid(), vout: msg.prevtx_out }, sequence: Sequence(msg.sequence), ..Default::default() }; - let prev_output = - tx.output.get(msg.prevtx_out as usize).ok_or(AbortReason::PrevTxOutInvalid)?.clone(); - if !self.prevtx_outpoints.insert(input.previous_output) { + if !self.prevtx_outpoints.insert(txin.previous_output.clone()) { // We have added an input that already exists return Err(AbortReason::PrevTxOutInvalid); } - self.inputs.insert( - msg.serial_id, - InteractiveTxInput { serial_id: msg.serial_id, input, prev_output }, - ); + let vout = txin.previous_output.vout as usize; + let input = InteractiveTxInput::Local(LocalOrRemoteInput { + serial_id: msg.serial_id, + input: txin, + prev_output: tx.output.get(vout).ok_or(AbortReason::PrevTxOutInvalid)?.clone(), + }); + self.inputs.insert(msg.serial_id, input); Ok(()) } fn sent_tx_add_output(&mut self, msg: &msgs::TxAddOutput) -> Result<(), AbortReason> { - self.outputs.insert( - msg.serial_id, + let txout = TxOut { value: Amount::from_sat(msg.sats), script_pubkey: msg.script.clone() }; + let is_shared = if let Some((shared_funding_script, _local_value)) = + &self.expected_shared_funding_output + { + msg.script == *shared_funding_script + } else { + false + }; + let output = if is_shared { + // this is a shared funding output + let shared_output = self.set_actual_new_funding_output(txout)?; InteractiveTxOutput { serial_id: msg.serial_id, - tx_out: TxOut { - value: Amount::from_sat(msg.sats), - script_pubkey: msg.script.clone(), - }, - }, - ); + added_by: AddingRole::Local, + output: OutputOwned::Shared(shared_output), + } + } else { + InteractiveTxOutput { + serial_id: msg.serial_id, + added_by: AddingRole::Local, + output: OutputOwned::Single(txout), + } + }; + self.outputs.insert(msg.serial_id, output); Ok(()) } @@ -704,7 +911,7 @@ define_state!( ReceivedTxComplete, "We have received a `tx_complete` message and the counterparty is awaiting ours." ); -define_state!(NegotiationComplete, ConstructedTransaction, "We have exchanged consecutive `tx_complete` messages with the counterparty and the transaction negotiation is complete."); +define_state!(NegotiationComplete, InteractiveTxSigningSession, "We have exchanged consecutive `tx_complete` messages with the counterparty and the transaction negotiation is complete."); define_state!( NegotiationAborted, AbortReason, @@ -747,7 +954,15 @@ macro_rules! define_state_transitions { fn transition(self, _data: &msgs::TxComplete) -> StateTransitionResult { let context = self.into_negotiation_context(); let tx = context.validate_tx()?; - Ok(NegotiationComplete(tx)) + let signing_session = InteractiveTxSigningSession { + holder_sends_tx_signatures_first: tx.holder_sends_tx_signatures_first, + unsigned_tx: tx, + sent_commitment_signed: None, + received_commitment_signed: None, + holder_tx_signatures: None, + counterparty_tx_signatures: None, + }; + Ok(NegotiationComplete(signing_session)) } } @@ -815,17 +1030,19 @@ macro_rules! define_state_machine_transitions { } impl StateMachine { - fn new(feerate_sat_per_kw: u32, is_initiator: bool, tx_locktime: AbsoluteLockTime) -> Self { - let context = NegotiationContext { + fn new( + holder_node_id: PublicKey, counterparty_node_id: PublicKey, feerate_sat_per_kw: u32, + is_initiator: bool, tx_locktime: AbsoluteLockTime, + expected_shared_funding_output: Option<(ScriptBuf, u64)>, + ) -> Self { + let context = NegotiationContext::new( + holder_node_id, + counterparty_node_id, + is_initiator, + expected_shared_funding_output, tx_locktime, - holder_is_initiator: is_initiator, - received_tx_add_input_count: 0, - received_tx_add_output_count: 0, - inputs: new_hash_map(), - prevtx_outpoints: new_hash_set(), - outputs: new_hash_map(), feerate_sat_per_kw, - }; + ); if is_initiator { Self::ReceivedChangeMsg(ReceivedChangeMsg(context)) } else { @@ -884,11 +1101,207 @@ impl StateMachine { ]); } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum AddingRole { + Local, + Remote, +} + +/// Represents an input -- local or remote (both have the same fields) +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LocalOrRemoteInput { + serial_id: SerialId, + input: TxIn, + prev_output: TxOut, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum InteractiveTxInput { + Local(LocalOrRemoteInput), + Remote(LocalOrRemoteInput), + // TODO(splicing) SharedInput should be added +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SharedOwnedOutput { + tx_out: TxOut, + local_owned: u64, +} + +impl 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 {}", + local_owned, + tx_out.value + ); + SharedOwnedOutput { tx_out, local_owned } + } + + fn remote_owned(&self) -> u64 { + self.tx_out.value.to_sat().saturating_sub(self.local_owned) + } +} + +/// Represents an output, with information about +/// its control -- exclusive by the adder or shared --, and +/// its ownership -- fully owned by the adder or jointly +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum OutputOwned { + /// Belongs to local node -- controlled exclusively and fully belonging to local node + Single(TxOut), + /// Output with shared control, but fully belonging to local node + SharedControlFullyOwned(TxOut), + /// Output with shared control and joint ownership + Shared(SharedOwnedOutput), +} + +impl OutputOwned { + pub fn tx_out(&self) -> &TxOut { + match self { + OutputOwned::Single(tx_out) | OutputOwned::SharedControlFullyOwned(tx_out) => tx_out, + OutputOwned::Shared(output) => &output.tx_out, + } + } + + fn into_tx_out(self) -> TxOut { + match self { + OutputOwned::Single(tx_out) | OutputOwned::SharedControlFullyOwned(tx_out) => tx_out, + OutputOwned::Shared(output) => output.tx_out, + } + } + + fn value(&self) -> u64 { + self.tx_out().value.to_sat() + } + + fn is_shared(&self) -> bool { + match self { + OutputOwned::Single(_) => false, + OutputOwned::SharedControlFullyOwned(_) => true, + OutputOwned::Shared(_) => true, + } + } + + fn local_value(&self, local_role: AddingRole) -> u64 { + match self { + OutputOwned::Single(tx_out) | OutputOwned::SharedControlFullyOwned(tx_out) => { + match local_role { + AddingRole::Local => tx_out.value.to_sat(), + AddingRole::Remote => 0, + } + }, + OutputOwned::Shared(output) => output.local_owned, + } + } + + fn remote_value(&self, local_role: AddingRole) -> u64 { + match self { + OutputOwned::Single(tx_out) | OutputOwned::SharedControlFullyOwned(tx_out) => { + match local_role { + AddingRole::Local => 0, + AddingRole::Remote => tx_out.value.to_sat(), + } + }, + OutputOwned::Shared(output) => output.remote_owned(), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct InteractiveTxOutput { + serial_id: SerialId, + added_by: AddingRole, + output: OutputOwned, +} + +impl InteractiveTxOutput { + pub fn tx_out(&self) -> &TxOut { + self.output.tx_out() + } + + fn into_tx_out(self) -> TxOut { + self.output.into_tx_out() + } + + fn value(&self) -> u64 { + self.tx_out().value.to_sat() + } + + fn local_value(&self) -> u64 { + self.output.local_value(self.added_by) + } + + fn remote_value(&self) -> u64 { + self.output.remote_value(self.added_by) + } + + fn script_pubkey(&self) -> &ScriptBuf { + &self.output.tx_out().script_pubkey + } +} + +impl InteractiveTxInput { + pub fn serial_id(&self) -> SerialId { + match self { + InteractiveTxInput::Local(input) => input.serial_id, + InteractiveTxInput::Remote(input) => input.serial_id, + } + } + + pub fn txin(&self) -> &TxIn { + match self { + InteractiveTxInput::Local(input) => &input.input, + InteractiveTxInput::Remote(input) => &input.input, + } + } + + pub fn txin_mut(&mut self) -> &mut TxIn { + match self { + InteractiveTxInput::Local(input) => &mut input.input, + InteractiveTxInput::Remote(input) => &mut input.input, + } + } + + pub fn into_txin(self) -> TxIn { + match self { + InteractiveTxInput::Local(input) => input.input, + InteractiveTxInput::Remote(input) => input.input, + } + } + + pub fn prev_output(&self) -> &TxOut { + match self { + InteractiveTxInput::Local(input) => &input.prev_output, + InteractiveTxInput::Remote(input) => &input.prev_output, + } + } + + pub fn value(&self) -> u64 { + self.prev_output().value.to_sat() + } + + pub fn local_value(&self) -> u64 { + match self { + InteractiveTxInput::Local(input) => input.prev_output.value.to_sat(), + InteractiveTxInput::Remote(_input) => 0, + } + } + + pub fn remote_value(&self) -> u64 { + match self { + InteractiveTxInput::Local(_input) => 0, + InteractiveTxInput::Remote(input) => input.prev_output.value.to_sat(), + } + } +} + pub(crate) struct InteractiveTxConstructor { state_machine: StateMachine, channel_id: ChannelId, inputs_to_contribute: Vec<(SerialId, TxIn, TransactionU16LenLimited)>, - outputs_to_contribute: Vec<(SerialId, TxOut)>, + outputs_to_contribute: Vec<(SerialId, OutputOwned)>, } pub(crate) enum InteractiveTxMessageSend { @@ -958,8 +1371,8 @@ where pub(crate) enum HandleTxCompleteValue { SendTxMessage(InteractiveTxMessageSend), - SendTxComplete(InteractiveTxMessageSend, ConstructedTransaction), - NegotiationComplete(ConstructedTransaction), + SendTxComplete(InteractiveTxMessageSend, InteractiveTxSigningSession), + NegotiationComplete(InteractiveTxSigningSession), } pub(super) struct HandleTxCompleteResult(pub Result); @@ -967,15 +1380,22 @@ pub(super) struct HandleTxCompleteResult(pub Result (Option, Option) { + ) -> (Option, Option) { match self.0 { Ok(tx_complete_res) => { - let (tx_msg_opt, tx_opt) = match tx_complete_res { + let (tx_msg_opt, signing_session_opt) = match tx_complete_res { HandleTxCompleteValue::SendTxMessage(msg) => (Some(msg), None), - HandleTxCompleteValue::SendTxComplete(msg, tx) => (Some(msg), Some(tx)), - HandleTxCompleteValue::NegotiationComplete(tx) => (None, Some(tx)), + HandleTxCompleteValue::SendTxComplete(msg, signing_session) => { + (Some(msg), Some(signing_session)) + }, + HandleTxCompleteValue::NegotiationComplete(signing_session) => { + (None, Some(signing_session)) + }, }; - (tx_msg_opt.map(|tx_msg| tx_msg.into_msg_send_event(counterparty_node_id)), tx_opt) + ( + tx_msg_opt.map(|tx_msg| tx_msg.into_msg_send_event(counterparty_node_id)), + signing_session_opt, + ) }, Err(tx_abort_msg) => ( Some(MessageSendEvent::SendTxAbort { @@ -991,19 +1411,65 @@ impl HandleTxCompleteResult { impl InteractiveTxConstructor { /// Instantiates a new `InteractiveTxConstructor`. /// + /// `expected_remote_shared_funding_output`: In the rare case when the local node doesn't + /// add a shared output, but it expects a shared output to be added by the remote node, + /// it can specify the script pubkey, used to determine the shared output, + /// and its (local) contribution from the shared output. + /// The local value cannot be larger that the actual shared output. + /// /// A tuple is returned containing the newly instantiate `InteractiveTxConstructor` and optionally /// an initial wrapped `Tx_` message which the holder needs to send to the counterparty. pub fn new( - entropy_source: &ES, channel_id: ChannelId, feerate_sat_per_kw: u32, is_initiator: bool, + entropy_source: &ES, channel_id: ChannelId, feerate_sat_per_kw: u32, + holder_node_id: PublicKey, counterparty_node_id: PublicKey, is_initiator: bool, funding_tx_locktime: AbsoluteLockTime, inputs_to_contribute: Vec<(TxIn, TransactionU16LenLimited)>, - outputs_to_contribute: Vec, - ) -> (Self, Option) + outputs_to_contribute: Vec, + expected_remote_shared_funding_output: Option<(ScriptBuf, u64)>, + ) -> Result<(Self, Option), AbortReason> where ES::Target: EntropySource, { - let state_machine = - StateMachine::new(feerate_sat_per_kw, is_initiator, funding_tx_locktime); + // Sanity check: There can be at most one shared output, local-added or remote added + let mut shared_funding_output: Option<(ScriptBuf, u64)> = None; + for output in &outputs_to_contribute { + let new_output = match output { + OutputOwned::Single(_tx_out) => None, + OutputOwned::SharedControlFullyOwned(tx_out) => { + Some((tx_out.script_pubkey.clone(), tx_out.value.to_sat())) + }, + OutputOwned::Shared(output) => { + // Sanity check + if output.local_owned > output.tx_out.value.to_sat() { + return Err(AbortReason::InvalidLowFundingOutputValue); + } + Some((output.tx_out.script_pubkey.clone(), output.local_owned)) + }, + }; + if new_output.is_some() { + if shared_funding_output.is_some() { + // more than one local-added shared output + return Err(AbortReason::DuplicateFundingOutput); + } + shared_funding_output = new_output; + } + } + if expected_remote_shared_funding_output.is_some() { + if shared_funding_output.is_some() { + // one local-added and one remote-expected shared output + return Err(AbortReason::DuplicateFundingOutput); + } + shared_funding_output = Some(expected_remote_shared_funding_output.unwrap()); + } + + let state_machine = StateMachine::new( + holder_node_id, + counterparty_node_id, + feerate_sat_per_kw, + is_initiator, + funding_tx_locktime, + shared_funding_output, + ); let mut inputs_to_contribute: Vec<(SerialId, TxIn, TransactionU16LenLimited)> = inputs_to_contribute .into_iter() @@ -1016,7 +1482,7 @@ impl InteractiveTxConstructor { // as the user passed them to us to avoid leaking any potential categorization of transactions // before we pass any of the inputs to the counterparty. inputs_to_contribute.sort_unstable_by_key(|(serial_id, _, _)| *serial_id); - let mut outputs_to_contribute: Vec<(SerialId, TxOut)> = outputs_to_contribute + let mut outputs_to_contribute: Vec<_> = outputs_to_contribute .into_iter() .map(|output| { let serial_id = generate_holder_serial_id(entropy_source, is_initiator); @@ -1041,7 +1507,7 @@ impl InteractiveTxConstructor { } else { None }; - (constructor, message_send) + Ok((constructor, message_send)) } fn maybe_send_message(&mut self) -> Result { @@ -1061,8 +1527,8 @@ impl InteractiveTxConstructor { let msg = msgs::TxAddOutput { channel_id: self.channel_id, serial_id, - sats: output.value.to_sat(), - script: output.script_pubkey, + sats: output.tx_out().value.to_sat(), + script: output.tx_out().script_pubkey.clone(), }; do_state_transition!(self, sent_tx_add_output, &msg)?; Ok(InteractiveTxMessageSend::TxAddOutput(msg)) @@ -1148,21 +1614,22 @@ mod tests { 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::blockdata::opcodes; use bitcoin::blockdata::script::Builder; use bitcoin::hashes::Hash; use bitcoin::key::UntweakedPublicKey; - use bitcoin::secp256k1::{Keypair, Secp256k1}; + use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, SecretKey}; use bitcoin::transaction::Version; use bitcoin::{ - absolute::LockTime as AbsoluteLockTime, OutPoint, Sequence, Transaction, TxIn, TxOut, + OutPoint, PubkeyHash, ScriptBuf, Sequence, Transaction, TxIn, TxOut, WPubkeyHash, }; - use bitcoin::{PubkeyHash, ScriptBuf, WPubkeyHash, WScriptHash}; use core::ops::Deref; use super::{ - get_output_weight, P2TR_INPUT_WEIGHT_LOWER_BOUND, P2WPKH_INPUT_WEIGHT_LOWER_BOUND, + get_output_weight, AddingRole, OutputOwned, SharedOwnedOutput, + P2TR_INPUT_WEIGHT_LOWER_BOUND, P2WPKH_INPUT_WEIGHT_LOWER_BOUND, P2WSH_INPUT_WEIGHT_LOWER_BOUND, TX_COMMON_FIELDS_WEIGHT, }; @@ -1211,10 +1678,14 @@ mod tests { struct TestSession { description: &'static str, inputs_a: Vec<(TxIn, TransactionU16LenLimited)>, - outputs_a: Vec, + outputs_a: Vec, inputs_b: Vec<(TxIn, TransactionU16LenLimited)>, - outputs_b: Vec, + outputs_b: Vec, expect_error: Option<(AbortReason, ErrorCulprit)>, + /// A node adds no shared output, but expects the peer to add one, with the specific script pubkey, and local contribution + a_expected_remote_shared_output: Option<(ScriptBuf, u64)>, + /// B node adds no shared output, but expects the peer to add one, with the specific script pubkey, and local contribution + b_expected_remote_shared_output: Option<(ScriptBuf, u64)>, } fn do_test_interactive_tx_constructor(session: TestSession) { @@ -1237,25 +1708,116 @@ mod tests { { let channel_id = ChannelId(entropy_source.get_secure_random_bytes()); let tx_locktime = AbsoluteLockTime::from_height(1337).unwrap(); + let holder_node_id = PublicKey::from_secret_key( + &Secp256k1::signing_only(), + &SecretKey::from_slice(&[42; 32]).unwrap(), + ); + let counterparty_node_id = PublicKey::from_secret_key( + &Secp256k1::signing_only(), + &SecretKey::from_slice(&[43; 32]).unwrap(), + ); + + // funding output sanity check + let shared_outputs_by_a: Vec<_> = + session.outputs_a.iter().filter(|o| o.is_shared()).collect(); + if shared_outputs_by_a.len() > 1 { + println!("Test warning: Expected at most one shared output. NodeA"); + } + let shared_output_by_a = if shared_outputs_by_a.len() >= 1 { + Some(shared_outputs_by_a[0].value()) + } else { + None + }; + let shared_outputs_by_b: Vec<_> = + session.outputs_b.iter().filter(|o| o.is_shared()).collect(); + if shared_outputs_by_b.len() > 1 { + println!("Test warning: Expected at most one shared output. NodeB"); + } + let shared_output_by_b = if shared_outputs_by_b.len() >= 1 { + Some(shared_outputs_by_b[0].value()) + } else { + None + }; + if session.a_expected_remote_shared_output.is_some() + || session.b_expected_remote_shared_output.is_some() + { + let expected_by_a = if let Some(a_expected_remote_shared_output) = + &session.a_expected_remote_shared_output + { + a_expected_remote_shared_output.1 + } else { + if shared_outputs_by_a.len() >= 1 { + shared_outputs_by_a[0].local_value(AddingRole::Local) + } else { + 0 + } + }; + let expected_by_b = if let Some(b_expected_remote_shared_output) = + &session.b_expected_remote_shared_output + { + b_expected_remote_shared_output.1 + } else { + if shared_outputs_by_b.len() >= 1 { + shared_outputs_by_b[0].local_value(AddingRole::Local) + } else { + 0 + } + }; + + let expected_sum = expected_by_a + expected_by_b; + let actual_shared_output = + shared_output_by_a.unwrap_or(shared_output_by_b.unwrap_or(0)); + if expected_sum != actual_shared_output { + println!("Test warning: Sum of expected shared output values does not match actual shared output value, {} {} {} {} {} {}", expected_sum, actual_shared_output, expected_by_a, expected_by_b, shared_output_by_a.unwrap_or(0), shared_output_by_b.unwrap_or(0)); + } + } - let (mut constructor_a, first_message_a) = InteractiveTxConstructor::new( + let (mut constructor_a, first_message_a) = match InteractiveTxConstructor::new( entropy_source, channel_id, TEST_FEERATE_SATS_PER_KW, - true, + holder_node_id, + counterparty_node_id, + true, // is_initiator tx_locktime, session.inputs_a, - session.outputs_a, - ); - let (mut constructor_b, first_message_b) = InteractiveTxConstructor::new( + session.outputs_a.iter().map(|o| o.clone()).collect(), + session.a_expected_remote_shared_output, + ) { + Ok(r) => r, + Err(abort_reason) => { + assert_eq!( + Some((abort_reason, ErrorCulprit::NodeA)), + session.expect_error, + "Test: {}", + session.description + ); + return; + }, + }; + let (mut constructor_b, first_message_b) = match InteractiveTxConstructor::new( entropy_source, channel_id, TEST_FEERATE_SATS_PER_KW, - false, + holder_node_id, + counterparty_node_id, + false, // is_initiator tx_locktime, session.inputs_b, - session.outputs_b, - ); + session.outputs_b.iter().map(|o| o.clone()).collect(), + session.b_expected_remote_shared_output, + ) { + Ok(r) => r, + Err(abort_reason) => { + assert_eq!( + Some((abort_reason, ErrorCulprit::NodeB)), + session.expect_error, + "Test: {}", + session.description + ); + return; + }, + }; let handle_message_send = |msg: InteractiveTxMessageSend, for_constructor: &mut InteractiveTxConstructor| { @@ -1288,9 +1850,10 @@ mod tests { while final_tx_a.is_none() || final_tx_b.is_none() { if let Some(message_send_a) = message_send_a.take() { match handle_message_send(message_send_a, &mut constructor_b) { - Ok((msg_send, final_tx)) => { + Ok((msg_send, interactive_signing_session)) => { message_send_b = msg_send; - final_tx_b = final_tx; + final_tx_b = + interactive_signing_session.map(|session| session.unsigned_tx.txid()); }, Err(abort_reason) => { let error_culprit = match abort_reason { @@ -1305,16 +1868,17 @@ mod tests { "Test: {}", session.description ); - assert!(message_send_b.is_none()); + assert!(message_send_b.is_none(), "Test: {}", session.description); return; }, } } if let Some(message_send_b) = message_send_b.take() { match handle_message_send(message_send_b, &mut constructor_a) { - Ok((msg_send, final_tx)) => { + Ok((msg_send, interactive_signing_session)) => { message_send_a = msg_send; - final_tx_a = final_tx; + final_tx_a = + interactive_signing_session.map(|session| session.unsigned_tx.txid()); }, Err(abort_reason) => { let error_culprit = match abort_reason { @@ -1329,7 +1893,7 @@ mod tests { "Test: {}", session.description ); - assert!(message_send_a.is_none()); + assert!(message_send_a.is_none(), "Test: {}", session.description); return; }, } @@ -1337,13 +1901,19 @@ mod tests { } assert!(message_send_a.is_none()); assert!(message_send_b.is_none()); - assert_eq!(final_tx_a.unwrap().into_unsigned_tx(), final_tx_b.unwrap().into_unsigned_tx()); - assert!(session.expect_error.is_none(), "Test: {}", session.description); + assert_eq!(final_tx_a.unwrap(), final_tx_b.unwrap()); + assert!( + session.expect_error.is_none(), + "Missing expected error {:?}, Test: {}", + session.expect_error, + session.description, + ); } #[derive(Debug, Clone, Copy)] enum TestOutput { P2WPKH(u64), + /// P2WSH, but with the specific script used for the funding output P2WSH(u64), P2TR(u64), // Non-witness type to test rejection. @@ -1360,9 +1930,7 @@ mod tests { TestOutput::P2WPKH(value) => { (*value, ScriptBuf::new_p2wpkh(&WPubkeyHash::from_slice(&[1; 20]).unwrap())) }, - TestOutput::P2WSH(value) => { - (*value, ScriptBuf::new_p2wsh(&WScriptHash::from_slice(&[2; 32]).unwrap())) - }, + TestOutput::P2WSH(value) => (*value, generate_funding_script_pubkey()), TestOutput::P2TR(value) => ( *value, ScriptBuf::new_p2tr( @@ -1417,8 +1985,39 @@ mod tests { ScriptBuf::new_p2wpkh(&WPubkeyHash::from_slice(&[1; 20]).unwrap()) } - fn generate_outputs(outputs: &[TestOutput]) -> Vec { - outputs.iter().map(generate_txout).collect() + fn generate_funding_script_pubkey() -> ScriptBuf { + Builder::new().push_int(33).into_script().to_p2wsh() + } + + fn generate_output_nonfunding_one(output: &TestOutput) -> OutputOwned { + OutputOwned::Single(generate_txout(output)) + } + + fn generate_outputs(outputs: &[TestOutput]) -> Vec { + outputs.iter().map(|o| generate_output_nonfunding_one(o)).collect() + } + + /// Generate a single output that is the funding output + fn generate_output(output: &TestOutput) -> Vec { + vec![OutputOwned::SharedControlFullyOwned(generate_txout(output))] + } + + /// Generate a single P2WSH output that is the funding output + fn generate_funding_output(value: u64) -> Vec { + generate_output(&TestOutput::P2WSH(value)) + } + + /// Generate a single P2WSH output with shared contribution that is the funding output + fn generate_shared_funding_output_one(value: u64, local_value: u64) -> OutputOwned { + OutputOwned::Shared(SharedOwnedOutput { + tx_out: generate_txout(&TestOutput::P2WSH(value)), + local_owned: local_value, + }) + } + + /// Generate a single P2WSH output with shared contribution that is the funding output + fn generate_shared_funding_output(value: u64, local_value: u64) -> Vec { + vec![generate_shared_funding_output_one(value, local_value)] } fn generate_fixed_number_of_inputs(count: u16) -> Vec<(TxIn, TransactionU16LenLimited)> { @@ -1460,7 +2059,7 @@ mod tests { inputs } - fn generate_fixed_number_of_outputs(count: u16) -> Vec { + fn generate_fixed_number_of_outputs(count: u16) -> Vec { // Set a constant value for each TxOut generate_outputs(&vec![TestOutput::P2WPKH(1_000_000); count as usize]) } @@ -1469,8 +2068,11 @@ mod tests { Builder::new().push_opcode(opcodes::OP_TRUE).into_script().to_p2sh() } - fn generate_non_witness_output(value: u64) -> TxOut { - TxOut { value: Amount::from_sat(value), script_pubkey: generate_p2sh_script_pubkey() } + fn generate_non_witness_output(value_satoshis: u64) -> OutputOwned { + OutputOwned::Single(TxOut { + value: Amount::from_sat(value_satoshis), + script_pubkey: generate_p2sh_script_pubkey(), + }) } #[test] @@ -1482,14 +2084,18 @@ mod tests { inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::InsufficientFees, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor(TestSession { description: "Single contribution, no initiator inputs", inputs_a: vec![], - outputs_a: generate_outputs(&[TestOutput::P2WPKH(1_000_000)]), + outputs_a: generate_output(&TestOutput::P2WPKH(1_000_000)), inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::OutputsValueExceedsInputsValue, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor(TestSession { description: "Single contribution, no initiator outputs", @@ -1498,14 +2104,18 @@ mod tests { inputs_b: vec![], outputs_b: vec![], expect_error: None, + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor(TestSession { description: "Single contribution, no fees", inputs_a: generate_inputs(&[TestOutput::P2WPKH(1_000_000)]), - outputs_a: generate_outputs(&[TestOutput::P2WPKH(1_000_000)]), + outputs_a: generate_output(&TestOutput::P2WPKH(1_000_000)), inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::InsufficientFees, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); let p2wpkh_fee = fee_for_weight(TEST_FEERATE_SATS_PER_KW, P2WPKH_INPUT_WEIGHT_LOWER_BOUND); let outputs_fee = fee_for_weight( @@ -1514,92 +2124,106 @@ mod tests { ); let tx_common_fields_fee = fee_for_weight(TEST_FEERATE_SATS_PER_KW, TX_COMMON_FIELDS_WEIGHT); + + let amount_adjusted_with_p2wpkh_fee = + 1_000_000 - p2wpkh_fee - outputs_fee - tx_common_fields_fee; do_test_interactive_tx_constructor(TestSession { description: "Single contribution, with P2WPKH input, insufficient fees", inputs_a: generate_inputs(&[TestOutput::P2WPKH(1_000_000)]), - outputs_a: generate_outputs(&[TestOutput::P2WPKH( - 1_000_000 - p2wpkh_fee - outputs_fee - tx_common_fields_fee + 1, /* makes fees insuffcient for initiator */ - )]), + outputs_a: generate_output(&TestOutput::P2WPKH( + amount_adjusted_with_p2wpkh_fee + 1, /* makes fees insuffcient for initiator */ + )), inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::InsufficientFees, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor(TestSession { description: "Single contribution with P2WPKH input, sufficient fees", inputs_a: generate_inputs(&[TestOutput::P2WPKH(1_000_000)]), - outputs_a: generate_outputs(&[TestOutput::P2WPKH( - 1_000_000 - p2wpkh_fee - outputs_fee - tx_common_fields_fee, - )]), + outputs_a: generate_output(&TestOutput::P2WPKH(amount_adjusted_with_p2wpkh_fee)), inputs_b: vec![], outputs_b: vec![], expect_error: None, + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); let p2wsh_fee = fee_for_weight(TEST_FEERATE_SATS_PER_KW, P2WSH_INPUT_WEIGHT_LOWER_BOUND); + let amount_adjusted_with_p2wsh_fee = + 1_000_000 - p2wsh_fee - outputs_fee - tx_common_fields_fee; do_test_interactive_tx_constructor(TestSession { description: "Single contribution, with P2WSH input, insufficient fees", inputs_a: generate_inputs(&[TestOutput::P2WSH(1_000_000)]), - outputs_a: generate_outputs(&[TestOutput::P2WPKH( - 1_000_000 - p2wsh_fee - outputs_fee - tx_common_fields_fee + 1, /* makes fees insuffcient for initiator */ - )]), + outputs_a: generate_output(&TestOutput::P2WPKH( + amount_adjusted_with_p2wsh_fee + 1, /* makes fees insuffcient for initiator */ + )), inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::InsufficientFees, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor(TestSession { description: "Single contribution with P2WSH input, sufficient fees", inputs_a: generate_inputs(&[TestOutput::P2WSH(1_000_000)]), - outputs_a: generate_outputs(&[TestOutput::P2WPKH( - 1_000_000 - p2wsh_fee - outputs_fee - tx_common_fields_fee, - )]), + outputs_a: generate_output(&TestOutput::P2WPKH(amount_adjusted_with_p2wsh_fee)), inputs_b: vec![], outputs_b: vec![], expect_error: None, + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); let p2tr_fee = fee_for_weight(TEST_FEERATE_SATS_PER_KW, P2TR_INPUT_WEIGHT_LOWER_BOUND); + let amount_adjusted_with_p2tr_fee = + 1_000_000 - p2tr_fee - outputs_fee - tx_common_fields_fee; do_test_interactive_tx_constructor(TestSession { description: "Single contribution, with P2TR input, insufficient fees", inputs_a: generate_inputs(&[TestOutput::P2TR(1_000_000)]), - outputs_a: generate_outputs(&[TestOutput::P2WPKH( - 1_000_000 - p2tr_fee - outputs_fee - tx_common_fields_fee + 1, /* makes fees insuffcient for initiator */ - )]), + outputs_a: generate_output(&TestOutput::P2WPKH( + amount_adjusted_with_p2tr_fee + 1, /* makes fees insuffcient for initiator */ + )), inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::InsufficientFees, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor(TestSession { description: "Single contribution with P2TR input, sufficient fees", inputs_a: generate_inputs(&[TestOutput::P2TR(1_000_000)]), - outputs_a: generate_outputs(&[TestOutput::P2WPKH( - 1_000_000 - p2tr_fee - outputs_fee - tx_common_fields_fee, - )]), + outputs_a: generate_output(&TestOutput::P2WPKH(amount_adjusted_with_p2tr_fee)), inputs_b: vec![], outputs_b: vec![], expect_error: None, + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor(TestSession { description: "Initiator contributes sufficient fees, but non-initiator does not", inputs_a: generate_inputs(&[TestOutput::P2WPKH(1_000_000)]), outputs_a: vec![], inputs_b: generate_inputs(&[TestOutput::P2WPKH(100_000)]), - outputs_b: generate_outputs(&[TestOutput::P2WPKH(100_000)]), + outputs_b: generate_output(&TestOutput::P2WPKH(100_000)), expect_error: Some((AbortReason::InsufficientFees, ErrorCulprit::NodeB)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor(TestSession { description: "Multi-input-output contributions from both sides", inputs_a: generate_inputs(&[TestOutput::P2WPKH(1_000_000); 2]), - outputs_a: generate_outputs(&[ - TestOutput::P2WPKH(1_000_000), - TestOutput::P2WPKH(200_000), - ]), + outputs_a: vec![ + generate_shared_funding_output_one(1_000_000, 200_000), + generate_output_nonfunding_one(&TestOutput::P2WPKH(200_000)), + ], inputs_b: generate_inputs(&[ TestOutput::P2WPKH(1_000_000), TestOutput::P2WPKH(500_000), ]), - outputs_b: generate_outputs(&[ - TestOutput::P2WPKH(1_000_000), - TestOutput::P2WPKH(400_000), - ]), + outputs_b: vec![generate_output_nonfunding_one(&TestOutput::P2WPKH(400_000))], expect_error: None, + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 800_000)), }); do_test_interactive_tx_constructor(TestSession { @@ -1609,6 +2233,8 @@ mod tests { inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::PrevTxOutInvalid, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); let tx = @@ -1620,10 +2246,12 @@ mod tests { do_test_interactive_tx_constructor(TestSession { description: "Invalid input sequence from initiator", inputs_a: vec![(invalid_sequence_input, tx.clone())], - outputs_a: generate_outputs(&[TestOutput::P2WPKH(1_000_000)]), + outputs_a: generate_output(&TestOutput::P2WPKH(1_000_000)), inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::IncorrectInputSequenceValue, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); let duplicate_input = TxIn { previous_output: OutPoint { txid: tx.as_transaction().txid(), vout: 0 }, @@ -1633,10 +2261,28 @@ mod tests { do_test_interactive_tx_constructor(TestSession { description: "Duplicate prevout from initiator", inputs_a: vec![(duplicate_input.clone(), tx.clone()), (duplicate_input, tx.clone())], - outputs_a: generate_outputs(&[TestOutput::P2WPKH(1_000_000)]), + outputs_a: generate_output(&TestOutput::P2WPKH(1_000_000)), inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::PrevTxOutInvalid, ErrorCulprit::NodeB)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, + }); + // Non-initiator uses same prevout as initiator. + let duplicate_input = TxIn { + previous_output: OutPoint { txid: tx.as_transaction().txid(), vout: 0 }, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..Default::default() + }; + do_test_interactive_tx_constructor(TestSession { + description: "Non-initiator uses same prevout as initiator".into(), + inputs_a: vec![(duplicate_input.clone(), tx.clone())], + outputs_a: generate_shared_funding_output(1_000_000, 905_000), + inputs_b: vec![(duplicate_input.clone(), tx.clone())], + outputs_b: vec![], + expect_error: Some((AbortReason::PrevTxOutInvalid, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 95_000)), }); let duplicate_input = TxIn { previous_output: OutPoint { txid: tx.as_transaction().txid(), vout: 0 }, @@ -1646,10 +2292,12 @@ mod tests { do_test_interactive_tx_constructor(TestSession { description: "Non-initiator uses same prevout as initiator", inputs_a: vec![(duplicate_input.clone(), tx.clone())], - outputs_a: generate_outputs(&[TestOutput::P2WPKH(1_000_000)]), + outputs_a: generate_output(&TestOutput::P2WPKH(1_000_000)), inputs_b: vec![(duplicate_input.clone(), tx.clone())], outputs_b: vec![], expect_error: Some((AbortReason::PrevTxOutInvalid, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor(TestSession { description: "Initiator sends too many TxAddInputs", @@ -1658,6 +2306,8 @@ mod tests { inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::ReceivedTooManyTxAddInputs, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor_with_entropy_source( TestSession { @@ -1668,6 +2318,8 @@ mod tests { inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::DuplicateSerialId, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }, &DuplicateEntropySource, ); @@ -1678,24 +2330,30 @@ mod tests { inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::ReceivedTooManyTxAddOutputs, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor(TestSession { description: "Initiator sends an output below dust value", inputs_a: vec![], - outputs_a: generate_outputs(&[TestOutput::P2WSH( + outputs_a: generate_funding_output( generate_p2wsh_script_pubkey().dust_value().to_sat() - 1, - )]), + ), inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::BelowDustLimit, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor(TestSession { description: "Initiator sends an output above maximum sats allowed", inputs_a: vec![], - outputs_a: generate_outputs(&[TestOutput::P2WPKH(TOTAL_BITCOIN_SUPPLY_SATOSHIS + 1)]), + outputs_a: generate_output(&TestOutput::P2WPKH(TOTAL_BITCOIN_SUPPLY_SATOSHIS + 1)), inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::ExceededMaximumSatsAllowed, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor(TestSession { description: "Initiator sends an output without a witness program", @@ -1704,6 +2362,8 @@ mod tests { inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::InvalidOutputScript, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor_with_entropy_source( TestSession { @@ -1714,6 +2374,8 @@ mod tests { inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::DuplicateSerialId, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }, &DuplicateEntropySource, ); @@ -1721,10 +2383,12 @@ mod tests { do_test_interactive_tx_constructor(TestSession { description: "Peer contributed more output value than inputs", inputs_a: generate_inputs(&[TestOutput::P2WPKH(100_000)]), - outputs_a: generate_outputs(&[TestOutput::P2WPKH(1_000_000)]), + outputs_a: generate_output(&TestOutput::P2WPKH(1_000_000)), inputs_b: vec![], outputs_b: vec![], expect_error: Some((AbortReason::OutputsValueExceedsInputsValue, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor(TestSession { @@ -1737,6 +2401,8 @@ mod tests { AbortReason::ExceededNumberOfInputsOrOutputs, ErrorCulprit::Indeterminate, )), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, }); do_test_interactive_tx_constructor(TestSession { description: "Peer contributed more than allowed number of outputs", @@ -1748,6 +2414,121 @@ mod tests { AbortReason::ExceededNumberOfInputsOrOutputs, ErrorCulprit::Indeterminate, )), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, + }); + + // Adding multiple outputs to the funding output pubkey is an error + do_test_interactive_tx_constructor(TestSession { + description: "Adding two outputs to the funding output pubkey", + inputs_a: generate_inputs(&[TestOutput::P2WPKH(1_000_000)]), + outputs_a: generate_funding_output(100_000), + inputs_b: generate_inputs(&[TestOutput::P2WPKH(1_001_000)]), + outputs_b: generate_funding_output(100_000), + expect_error: Some((AbortReason::DuplicateFundingOutput, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, + }); + + // We add the funding output, but we contribute a little + do_test_interactive_tx_constructor(TestSession { + description: "Funding output by us, small contribution", + inputs_a: generate_inputs(&[TestOutput::P2WPKH(12_000)]), + outputs_a: generate_shared_funding_output(1_000_000, 10_000), + inputs_b: generate_inputs(&[TestOutput::P2WPKH(992_000)]), + outputs_b: vec![], + expect_error: None, + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 990_000)), + }); + + // They add the funding output, and we contribute a little + do_test_interactive_tx_constructor(TestSession { + description: "Funding output by them, small contribution", + inputs_a: generate_inputs(&[TestOutput::P2WPKH(12_000)]), + outputs_a: vec![], + inputs_b: generate_inputs(&[TestOutput::P2WPKH(992_000)]), + outputs_b: generate_shared_funding_output(1_000_000, 990_000), + expect_error: None, + a_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 10_000)), + b_expected_remote_shared_output: None, + }); + + // We add the funding output, and we contribute most + do_test_interactive_tx_constructor(TestSession { + description: "Funding output by us, large contribution", + inputs_a: generate_inputs(&[TestOutput::P2WPKH(992_000)]), + outputs_a: generate_shared_funding_output(1_000_000, 990_000), + inputs_b: generate_inputs(&[TestOutput::P2WPKH(12_000)]), + outputs_b: vec![], + expect_error: None, + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 10_000)), + }); + + // They add the funding output, but we contribute most + do_test_interactive_tx_constructor(TestSession { + description: "Funding output by them, large contribution", + inputs_a: generate_inputs(&[TestOutput::P2WPKH(992_000)]), + outputs_a: vec![], + inputs_b: generate_inputs(&[TestOutput::P2WPKH(12_000)]), + outputs_b: generate_shared_funding_output(1_000_000, 10_000), + expect_error: None, + a_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 990_000)), + b_expected_remote_shared_output: None, + }); + + // During a splice-out, with peer providing more output value than input value + // but still pays enough fees due to their to_remote_value_satoshis portion in + // the shared input. + do_test_interactive_tx_constructor(TestSession { + description: "Splice out with sufficient initiator balance", + inputs_a: generate_inputs(&[TestOutput::P2WPKH(100_000), TestOutput::P2WPKH(50_000)]), + outputs_a: generate_funding_output(120_000), + inputs_b: generate_inputs(&[TestOutput::P2WPKH(50_000)]), + outputs_b: vec![], + expect_error: None, + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, + }); + + // During a splice-out, with peer providing more output value than input value + // and the to_remote_value_satoshis portion in + // the shared input cannot cover fees + do_test_interactive_tx_constructor(TestSession { + description: "Splice out with insufficient initiator balance", + inputs_a: generate_inputs(&[TestOutput::P2WPKH(100_000), TestOutput::P2WPKH(15_000)]), + outputs_a: generate_funding_output(120_000), + inputs_b: generate_inputs(&[TestOutput::P2WPKH(85_000)]), + outputs_b: vec![], + expect_error: Some((AbortReason::OutputsValueExceedsInputsValue, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: None, + }); + + // The actual funding output value is lower than the intended local contribution by the same node + do_test_interactive_tx_constructor(TestSession { + description: "Splice in, invalid intended local contribution", + inputs_a: generate_inputs(&[TestOutput::P2WPKH(100_000), TestOutput::P2WPKH(15_000)]), + outputs_a: generate_shared_funding_output(100_000, 120_000), // local value is higher than the output value + inputs_b: generate_inputs(&[TestOutput::P2WPKH(85_000)]), + outputs_b: vec![], + expect_error: Some((AbortReason::InvalidLowFundingOutputValue, ErrorCulprit::NodeA)), + a_expected_remote_shared_output: None, + b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 20_000)), + }); + + // The actual funding output value is lower than the intended local contribution of the other node + do_test_interactive_tx_constructor(TestSession { + description: "Splice in, invalid intended local contribution", + inputs_a: generate_inputs(&[TestOutput::P2WPKH(100_000), TestOutput::P2WPKH(15_000)]), + outputs_a: vec![], + inputs_b: generate_inputs(&[TestOutput::P2WPKH(85_000)]), + outputs_b: generate_funding_output(100_000), + // The error is caused by NodeA, it occurs when nodeA prepares the message to be sent to NodeB, that's why here it shows up as NodeB + expect_error: Some((AbortReason::InvalidLowFundingOutputValue, ErrorCulprit::NodeB)), + a_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 120_000)), // this is higher than the actual output value + b_expected_remote_shared_output: None, }); } From 8e4879f8acb131750af2c66a344d323ceb9836fc Mon Sep 17 00:00:00 2001 From: Duncan Dean Date: Thu, 2 May 2024 10:05:43 +0200 Subject: [PATCH 5/6] Handle re-establishment next_funding_txid --- lightning/src/ln/channel.rs | 26 ++++++++++++++++++++++---- lightning/src/ln/channelmanager.rs | 5 ++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 1f675f32e10..f34dd63c936 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1450,6 +1450,10 @@ pub(super) struct ChannelContext where SP::Target: SignerProvider { /// If we can't release a [`ChannelMonitorUpdate`] until some external action completes, we /// store it here and only release it to the `ChannelManager` once it asks for it. blocked_monitor_updates: Vec, + // If we've sent `commtiment_signed` for an interactive transaction construction, + // but have not received `tx_signatures` we MUST set `next_funding_txid` to the + // txid of that interactive transaction, else we MUST NOT set it. + next_funding_txid: Option, } #[cfg(any(dual_funding, splicing))] @@ -2000,6 +2004,7 @@ impl ChannelContext where SP::Target: SignerProvider { local_initiated_shutdown: None, blocked_monitor_updates: Vec::new(), + next_funding_txid: None, }; Ok(channel_context) @@ -2223,6 +2228,7 @@ impl ChannelContext where SP::Target: SignerProvider { blocked_monitor_updates: Vec::new(), local_initiated_shutdown: None, + next_funding_txid: None, }) } @@ -4400,6 +4406,16 @@ impl Channel where self.context.channel_state.clear_waiting_for_batch(); } + #[cfg(any(dual_funding, splicing))] + pub fn set_next_funding_txid(&mut self, txid: &Txid) { + self.context.next_funding_txid = Some(*txid); + } + + #[cfg(any(dual_funding, splicing))] + pub fn clear_next_funding_txid(&mut self) { + self.context.next_funding_txid = None; + } + /// Unsets the existing funding information. /// /// This must only be used if the channel has not yet completed funding and has not been used. @@ -7440,10 +7456,7 @@ impl Channel where next_remote_commitment_number: INITIAL_COMMITMENT_NUMBER - self.context.cur_counterparty_commitment_transaction_number - 1, your_last_per_commitment_secret: remote_last_secret, my_current_per_commitment_point: dummy_pubkey, - // TODO(dual_funding): If we've sent `commtiment_signed` for an interactive transaction - // construction but have not received `tx_signatures` we MUST set `next_funding_txid` to the - // txid of that interactive transaction, else we MUST NOT set it. - next_funding_txid: None, + next_funding_txid: self.context.next_funding_txid, } } @@ -9425,6 +9438,7 @@ impl Writeable for Channel where SP::Target: SignerProvider { (45, cur_holder_commitment_point, option), (47, next_holder_commitment_point, option), (49, self.context.local_initiated_shutdown, option), // Added in 0.0.122 + (51, self.context.next_funding_txid, option), // Added in 0.0.124 }); Ok(()) @@ -10022,6 +10036,10 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, u32, &'c Ch local_initiated_shutdown, blocked_monitor_updates: blocked_monitor_updates.unwrap(), + // If we've sent `commtiment_signed` for an interactive transaction construction, + // but have not received `tx_signatures` we MUST set `next_funding_txid` to the + // txid of that interactive transaction, else we MUST NOT set it. + next_funding_txid: None, }, #[cfg(any(dual_funding, splicing))] dual_funding_channel_context: None, diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 4a91837e642..017ec99d5b3 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -7791,6 +7791,7 @@ where peer_state.pending_msg_events.push(msg_send_event); } if let Some(signing_session) = signing_session_opt { + let funding_txid = signing_session.unsigned_tx.txid(); let (channel_id, channel_phase) = chan_phase_entry.remove_entry(); let res = match channel_phase { ChannelPhase::UnfundedOutboundV2(chan) => { @@ -7812,7 +7813,7 @@ where .into()))), }; match res { - Ok((channel, commitment_signed, funding_ready_for_sig_event_opt)) => { + Ok((mut channel, commitment_signed, funding_ready_for_sig_event_opt)) => { if let Some(funding_ready_for_sig_event) = funding_ready_for_sig_event_opt { let mut pending_events = self.pending_events.lock().unwrap(); pending_events.push_back((funding_ready_for_sig_event, None)); @@ -7828,6 +7829,7 @@ where update_fee: None, }, }); + channel.set_next_funding_txid(&funding_txid); peer_state.channel_by_id.insert(channel_id.clone(), ChannelPhase::Funded(channel)); }, Err((channel_phase, err)) => { @@ -7863,6 +7865,7 @@ where match channel_phase { ChannelPhase::Funded(chan) => { let (tx_signatures_opt, funding_tx_opt) = try_chan_phase_entry!(self, chan.tx_signatures(&msg), chan_phase_entry); + chan.clear_next_funding_txid(); if let Some(tx_signatures) = tx_signatures_opt { peer_state.pending_msg_events.push(events::MessageSendEvent::SendTxSignatures { node_id: *counterparty_node_id, From 90888948ed6b554e9afe5819f928d5c1ec2d386f Mon Sep 17 00:00:00 2001 From: Duncan Dean Date: Wed, 13 Sep 2023 23:42:05 +0200 Subject: [PATCH 6/6] Add `option_dual_fund` feature --- lightning/src/ln/channelmanager.rs | 2 ++ lightning/src/ln/features.rs | 10 ++++++++-- lightning/src/ln/peer_handler.rs | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 017ec99d5b3..68f1daf608e 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -11354,6 +11354,8 @@ pub fn provided_init_features(config: &UserConfig) -> InitFeatures { if config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx { features.set_anchors_zero_fee_htlc_tx_optional(); } + #[cfg(any(dual_funding, splicing))] + features.set_dual_fund_optional(); features } diff --git a/lightning/src/ln/features.rs b/lightning/src/ln/features.rs index 51c608c1a6b..59b4e186521 100644 --- a/lightning/src/ln/features.rs +++ b/lightning/src/ln/features.rs @@ -49,6 +49,9 @@ //! (see [BOLT-4](https://github.com/lightning/bolts/blob/master/04-onion-routing.md#route-blinding) for more information). //! - `ShutdownAnySegwit` - requires/supports that future segwit versions are allowed in `shutdown` //! (see [BOLT-2](https://github.com/lightning/bolts/blob/master/02-peer-protocol.md) for more information). +//! - `DualFund` - requires/supports V2 channel establishment +//! (see [BOLT-2](https://github.com/lightning/bolts/pull/851/files) for more information). +// TODO: update link //! - `OnionMessages` - requires/supports forwarding onion messages //! (see [BOLT-7](https://github.com/lightning/bolts/pull/759/files) for more information). // TODO: update link @@ -149,7 +152,7 @@ mod sealed { // Byte 2 BasicMPP | Wumbo | AnchorsNonzeroFeeHtlcTx | AnchorsZeroFeeHtlcTx, // Byte 3 - RouteBlinding | ShutdownAnySegwit | Taproot, + RouteBlinding | ShutdownAnySegwit | DualFund | Taproot, // Byte 4 OnionMessages, // Byte 5 @@ -167,7 +170,7 @@ mod sealed { // Byte 2 BasicMPP | Wumbo | AnchorsNonzeroFeeHtlcTx | AnchorsZeroFeeHtlcTx, // Byte 3 - RouteBlinding | ShutdownAnySegwit | Taproot, + RouteBlinding | ShutdownAnySegwit | DualFund | Taproot, // Byte 4 OnionMessages, // Byte 5 @@ -409,6 +412,9 @@ mod sealed { define_feature!(27, ShutdownAnySegwit, [InitContext, NodeContext], "Feature flags for `opt_shutdown_anysegwit`.", set_shutdown_any_segwit_optional, set_shutdown_any_segwit_required, supports_shutdown_anysegwit, requires_shutdown_anysegwit); + define_feature!(29, DualFund, [InitContext, NodeContext], + "Feature flags for `option_dual_fund`.", set_dual_fund_optional, set_dual_fund_required, + supports_dual_fund, requires_dual_fund); define_feature!(31, Taproot, [InitContext, NodeContext, ChannelTypeContext], "Feature flags for `option_taproot`.", set_taproot_optional, set_taproot_required, supports_taproot, requires_taproot); diff --git a/lightning/src/ln/peer_handler.rs b/lightning/src/ln/peer_handler.rs index 448dd213dad..fc14a68d6c2 100644 --- a/lightning/src/ln/peer_handler.rs +++ b/lightning/src/ln/peer_handler.rs @@ -322,6 +322,7 @@ impl ChannelMessageHandler for ErroringMessageHandler { features.set_basic_mpp_optional(); features.set_wumbo_optional(); features.set_shutdown_any_segwit_optional(); + features.set_dual_fund_optional(); features.set_channel_type_optional(); features.set_scid_privacy_optional(); features.set_zero_conf_optional();