diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index d9c3feab888..83c43dadbef 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -6314,6 +6314,19 @@ where } } + #[cfg(all(test))] + pub fn get_initial_counterparty_commitment_signatures_for_test( + &mut self, funding: &mut FundingScope, logger: &L, + counterparty_next_commitment_point_override: PublicKey, + ) -> Option<(Signature, Vec)> + where + SP::Target: SignerProvider, + L::Target: Logger, + { + self.counterparty_next_commitment_point = Some(counterparty_next_commitment_point_override); + self.get_initial_counterparty_commitment_signatures(funding, logger) + } + fn check_funding_meets_minimum_depth(&self, funding: &FundingScope, height: u32) -> bool { let minimum_depth = self .minimum_depth(funding) @@ -10955,7 +10968,7 @@ where #[rustfmt::skip] pub fn is_awaiting_initial_mon_persist(&self) -> bool { if !self.is_awaiting_monitor_update() { return false; } - if matches!( + if self.context.interactive_tx_signing_session.is_some() || matches!( self.context.channel_state, ChannelState::AwaitingChannelReady(flags) if flags.clone().clear(AwaitingChannelReadyFlags::THEIR_CHANNEL_READY | FundedStateFlags::PEER_DISCONNECTED | FundedStateFlags::MONITOR_UPDATE_IN_PROGRESS | AwaitingChannelReadyFlags::WAITING_FOR_BATCH).is_empty() ) { @@ -14103,22 +14116,22 @@ where /// Creates a new dual-funded channel from a remote side's request for one. /// Assumes chain_hash has already been checked and corresponds with what we expect! - /// TODO(dual_funding): Allow contributions, pass intended amount and inputs #[allow(dead_code)] // TODO(dual_funding): Remove once V2 channels is enabled. #[rustfmt::skip] pub fn new_inbound( fee_estimator: &LowerBoundedFeeEstimator, entropy_source: &ES, signer_provider: &SP, holder_node_id: PublicKey, counterparty_node_id: PublicKey, our_supported_features: &ChannelTypeFeatures, - their_features: &InitFeatures, msg: &msgs::OpenChannelV2, - user_id: u128, config: &UserConfig, current_chain_height: u32, logger: &L, + their_features: &InitFeatures, msg: &msgs::OpenChannelV2, user_id: u128, config: &UserConfig, + current_chain_height: u32, logger: &L, our_funding_contribution_sats: u64, + our_funding_inputs: Vec, ) -> Result where ES::Target: EntropySource, F::Target: FeeEstimator, L::Target: Logger, { - // TODO(dual_funding): Take these as input once supported - let (our_funding_contribution, our_funding_contribution_sats) = (SignedAmount::ZERO, 0u64); - let our_funding_inputs = Vec::new(); + // This u64 -> i64 cast is safe as `our_funding_contribution_sats` is bounded above by the total + // bitcoin supply, which is far less than i64::MAX. + let our_funding_contribution = SignedAmount::from_sat(our_funding_contribution_sats as i64); let channel_value_satoshis = our_funding_contribution_sats.saturating_add(msg.common_fields.funding_satoshis); @@ -14176,6 +14189,30 @@ where script_pubkey: funding.get_funding_redeemscript().to_p2wsh(), }; + // Optionally add change output + let change_script = signer_provider.get_destination_script(context.channel_keys_id) + .map_err(|_| ChannelError::close("Error getting change destination script".to_string()))?; + let change_value_opt = if our_funding_contribution > SignedAmount::ZERO { + calculate_change_output_value( + &funding_negotiation_context, false, &shared_funding_output.script_pubkey, context.holder_dust_limit_satoshis).map_err(|_| ChannelError::close("Error calculating change output value".to_string()))? } else { + None + }; + let mut our_funding_outputs = vec![]; + if let Some(change_value) = change_value_opt { + let mut change_output = TxOut { + value: Amount::from_sat(change_value), + script_pubkey: change_script, + }; + let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu(); + let change_output_fee = fee_for_weight(funding_negotiation_context.funding_feerate_sat_per_1000_weight, change_output_weight); + let change_value_decreased_with_fee = change_value.saturating_sub(change_output_fee); + // Check dust limit again + if change_value_decreased_with_fee > context.holder_dust_limit_satoshis { + change_output.value = Amount::from_sat(change_value_decreased_with_fee); + our_funding_outputs.push(change_output); + } + } + let interactive_tx_constructor = Some(InteractiveTxConstructor::new( InteractiveTxConstructorArgs { entropy_source, @@ -14188,7 +14225,7 @@ where inputs_to_contribute: our_funding_inputs, shared_funding_input: None, shared_funding_output: SharedOwnedOutput::new(shared_funding_output, our_funding_contribution_sats), - outputs_to_contribute: funding_negotiation_context.our_funding_outputs.clone(), + outputs_to_contribute: our_funding_outputs, } ).map_err(|err| { let reason = ClosureReason::ProcessingError { err: err.reason.to_string() }; diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 8abb2378627..db886e61e1c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -62,10 +62,10 @@ use crate::ln::channel::{ self, hold_time_since, Channel, ChannelError, ChannelUpdateStatus, DisconnectResult, FundedChannel, FundingTxSigned, InboundV1Channel, OutboundV1Channel, PendingV2Channel, ReconnectionMsg, ShutdownResult, SpliceFundingFailed, StfuResponse, UpdateFulfillCommitFetch, - WithChannelContext, + WithChannelContext, TOTAL_BITCOIN_SUPPLY_SATOSHIS, }; use crate::ln::channel_state::ChannelDetails; -use crate::ln::funding::SpliceContribution; +use crate::ln::funding::{FundingTxInput, SpliceContribution}; use crate::ln::inbound_payment; use crate::ln::interactivetxs::InteractiveTxMessageSend; use crate::ln::msgs; @@ -9814,6 +9814,8 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ false, user_channel_id, config_overrides, + 0, + vec![], ) } @@ -9845,6 +9847,56 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ true, user_channel_id, config_overrides, + 0, + vec![], + ) + } + + /// Accepts a request to open a dual-funded channel with a contribution provided by us after an + /// [`Event::OpenChannelRequest`]. + /// + /// The [`Event::OpenChannelRequest::channel_negotiation_type`] field will indicate the open channel + /// request is for a dual-funded channel when the variant is `InboundChannelFunds::DualFunded`. + /// + /// The `temporary_channel_id` parameter indicates which inbound channel should be accepted, + /// and the `counterparty_node_id` parameter is the id of the peer 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, and + /// a corresponding witness weight for each input 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. The witness + /// weights must correspond to the witnesses you will provide through [`ChannelManager::funding_transaction_signed`] + /// after receiving [`Event::FundingTransactionReadyForSigning`]. + /// + /// Note that this method will return an error and reject the channel if it requires support for + /// zero confirmations. + // TODO(dual_funding): Discussion on complications with 0conf dual-funded channels where "locking" + // of UTXOs used for funding would be required and other issues. + // See https://diyhpl.us/~bryan/irc/bitcoin/bitcoin-dev/linuxfoundation-pipermail/lightning-dev/2023-May/003922.txt + /// + /// [`Event::OpenChannelRequest`]: events::Event::OpenChannelRequest + /// [`Event::OpenChannelRequest::channel_negotiation_type`]: events::Event::OpenChannelRequest::channel_negotiation_type + /// [`Event::ChannelClosed::user_channel_id`]: events::Event::ChannelClosed::user_channel_id + /// [`Event::FundingTransactionReadyForSigning`]: events::Event::FundingTransactionReadyForSigning + /// [`ChannelManager::funding_transaction_signed`]: ChannelManager::funding_transaction_signed + pub fn accept_inbound_channel_with_contribution( + &self, temporary_channel_id: &ChannelId, counterparty_node_id: &PublicKey, + user_channel_id: u128, config_overrides: Option, + our_funding_satoshis: u64, funding_inputs: Vec, + ) -> Result<(), APIError> { + self.do_accept_inbound_channel( + temporary_channel_id, + counterparty_node_id, + false, + user_channel_id, + config_overrides, + our_funding_satoshis, + funding_inputs, ) } @@ -9852,8 +9904,14 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ #[rustfmt::skip] fn do_accept_inbound_channel( &self, temporary_channel_id: &ChannelId, counterparty_node_id: &PublicKey, accept_0conf: bool, - user_channel_id: u128, config_overrides: Option + user_channel_id: u128, config_overrides: Option, our_funding_satoshis: u64, + funding_inputs: Vec ) -> Result<(), APIError> { + // We do this check early as we will cast this to i64 for the NegotiationContext, and this is the + // actual upper bound which is less than i64::MAX. + if our_funding_satoshis >= TOTAL_BITCOIN_SUPPLY_SATOSHIS { + return Err(APIError::APIMisuseError{err: format!("the funding contribution must be smaller than the total bitcoin supply, it was {} satoshis", our_funding_satoshis)}); + } let mut config = self.config.read().unwrap().clone(); @@ -9911,7 +9969,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &self.channel_type_features(), &peer_state.latest_features, &open_channel_msg, user_channel_id, &config, best_block_height, - &self.logger, + &self.logger, our_funding_satoshis, funding_inputs, ).map_err(|e| { let channel_id = open_channel_msg.common_fields.temporary_channel_id; MsgHandleErrInternal::from_chan_no_close(e, channel_id) @@ -10197,7 +10255,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &self.fee_estimator, &self.entropy_source, &self.signer_provider, self.get_our_node_id(), *counterparty_node_id, &self.channel_type_features(), &peer_state.latest_features, msg, user_channel_id, - &self.config.read().unwrap(), best_block_height, &self.logger, + &self.config.read().unwrap(), best_block_height, &self.logger, 0, vec![], ).map_err(|e| MsgHandleErrInternal::from_chan_no_close(e, msg.common_fields.temporary_channel_id))?; let message_send_event = MessageSendEvent::SendAcceptChannelV2 { node_id: *counterparty_node_id, diff --git a/lightning/src/ln/dual_funding_tests.rs b/lightning/src/ln/dual_funding_tests.rs index fc66ec315ca..387222466e1 100644 --- a/lightning/src/ln/dual_funding_tests.rs +++ b/lightning/src/ln/dual_funding_tests.rs @@ -8,3 +8,440 @@ // licenses. //! Tests that test the creation of dual-funded channels in ChannelManager. + +use { + crate::{ + chain::chaininterface::{ConfirmationTarget, LowerBoundedFeeEstimator}, + events::{Event, InboundChannelFunds}, + ln::{ + chan_utils::{ + make_funding_redeemscript, ChannelPublicKeys, ChannelTransactionParameters, + CounterpartyChannelTransactionParameters, + }, + channel::PendingV2Channel, + channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBasepoint}, + functional_test_utils::*, + funding::FundingTxInput, + msgs::{ + BaseMessageHandler, ChannelMessageHandler, CommitmentSigned, MessageSendEvent, + TxAddInput, TxAddOutput, TxComplete, TxSignatures, + }, + types::ChannelId, + }, + prelude::*, + util::test_utils, + }, + bitcoin::{ + hashes::Hash, + key::{constants::SECRET_KEY_SIZE, Keypair, Secp256k1}, + secp256k1::Message, + sighash::SighashCache, + Witness, + }, +}; + +// Dual-funding: V2 Channel Establishment Tests +struct V2ChannelEstablishmentTestSession { + initiator_funding_satoshis: u64, + initiator_input_value_satoshis: u64, + acceptor_funding_satoshis: u64, + acceptor_input_value_satoshis: u64, +} + +// TODO(dual_funding): Use real node and API for creating V2 channels as initiator when available, +// instead of manually constructing messages. +fn do_test_v2_channel_establishment(session: V2ChannelEstablishmentTestSession) { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut node_1_user_config = test_default_channel_config(); + node_1_user_config.enable_dual_funded_channels = true; + node_1_user_config.manually_accept_inbound_channels = true; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(node_1_user_config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let logger_a = test_utils::TestLogger::with_id("node a".to_owned()); + + let secp_ctx = Secp256k1::new(); + let initiator_external_keypair = + Keypair::from_seckey_slice(&secp_ctx, &[2; SECRET_KEY_SIZE]).unwrap(); + let acceptor_external_keypair = + Keypair::from_seckey_slice(&secp_ctx, &[3; SECRET_KEY_SIZE]).unwrap(); + + // Create initiator funding input for the new channel along with its previous transaction. + let initiator_funding_inputs: Vec<_> = create_dual_funding_utxos_with_prev_txs( + &nodes[0], + &[session.initiator_input_value_satoshis], + &initiator_external_keypair.public_key(), + ); + + dbg!(&initiator_funding_inputs[0].utxo.output); + + // Create acceptor funding input for the new channel along with its previous transaction. + let acceptor_funding_inputs: Vec<_> = if session.acceptor_input_value_satoshis == 0 { + vec![] + } else { + create_dual_funding_utxos_with_prev_txs( + &nodes[1], + &[session.acceptor_input_value_satoshis], + &acceptor_external_keypair.public_key(), + ) + }; + if !acceptor_funding_inputs.is_empty() { + dbg!(&acceptor_funding_inputs[0].utxo.output); + } + let acceptor_funding_inputs_count = acceptor_funding_inputs.len(); + + // Alice creates a dual-funded channel as initiator. + let initiator_funding_satoshis = session.initiator_funding_satoshis; + let mut channel = PendingV2Channel::new_outbound( + &LowerBoundedFeeEstimator(node_cfgs[0].fee_estimator), + &nodes[0].node.entropy_source, + &nodes[0].node.signer_provider, + nodes[1].node.get_our_node_id(), + &nodes[1].node.init_features(), + initiator_funding_satoshis, + initiator_funding_inputs.clone(), + 42, /* user_channel_id */ + &nodes[0].node.get_current_config(), + nodes[0].best_block_info().1, + nodes[0].node.create_and_insert_outbound_scid_alias_for_test(), + ConfirmationTarget::NonAnchorChannelFee, + &logger_a, + ) + .unwrap(); + let open_channel_v2_msg = channel.get_open_channel_v2(nodes[0].chain_source.chain_hash); + + nodes[1].node.handle_open_channel_v2(nodes[0].node.get_our_node_id(), &open_channel_v2_msg); + + let events = nodes[1].node.get_and_clear_pending_events(); + let accept_channel_v2_msg = match &events[0] { + Event::OpenChannelRequest { + temporary_channel_id, + counterparty_node_id, + channel_negotiation_type, + .. + } => { + assert!(matches!(channel_negotiation_type, &InboundChannelFunds::DualFunded)); + nodes[1] + .node + .accept_inbound_channel_with_contribution( + temporary_channel_id, + counterparty_node_id, + u128::MAX - 2, + None, + session.acceptor_funding_satoshis, + acceptor_funding_inputs.clone(), + ) + .unwrap(); + get_event_msg!( + nodes[1], + MessageSendEvent::SendAcceptChannelV2, + nodes[0].node.get_our_node_id() + ) + }, + _ => panic!("Unexpected event"), + }; + + let channel_id = ChannelId::v2_from_revocation_basepoints( + &RevocationBasepoint::from(accept_channel_v2_msg.common_fields.revocation_basepoint), + &RevocationBasepoint::from(open_channel_v2_msg.common_fields.revocation_basepoint), + ); + + let FundingTxInput { sequence, prevtx, .. } = &initiator_funding_inputs[0]; + let tx_add_input_msg = TxAddInput { + channel_id, + serial_id: 2, // Even serial_id from initiator. + prevtx: Some(prevtx.clone()), + prevtx_out: 0, + sequence: sequence.0, + shared_input_txid: None, + }; + let input_value = tx_add_input_msg.prevtx.as_ref().unwrap().output + [tx_add_input_msg.prevtx_out as usize] + .value; + assert_eq!(input_value.to_sat(), session.initiator_input_value_satoshis); + + nodes[1].node.handle_tx_add_input(nodes[0].node.get_our_node_id(), &tx_add_input_msg); + + if acceptor_funding_inputs_count > 0 { + let _tx_add_input_msg = get_event_msg!( + nodes[1], + MessageSendEvent::SendTxAddInput, + nodes[0].node.get_our_node_id() + ); + } else { + let _tx_complete_msg = get_event_msg!( + nodes[1], + MessageSendEvent::SendTxComplete, + nodes[0].node.get_our_node_id() + ); + } + + let tx_add_output_msg = TxAddOutput { + channel_id, + serial_id: 4, + sats: initiator_funding_satoshis.saturating_add(session.acceptor_funding_satoshis), + script: make_funding_redeemscript( + &open_channel_v2_msg.common_fields.funding_pubkey, + &accept_channel_v2_msg.common_fields.funding_pubkey, + ) + .to_p2wsh(), + }; + nodes[1].node.handle_tx_add_output(nodes[0].node.get_our_node_id(), &tx_add_output_msg); + + let acceptor_change_value_satoshis = + session.initiator_input_value_satoshis.saturating_sub(session.initiator_funding_satoshis); + if acceptor_funding_inputs_count > 0 + && acceptor_change_value_satoshis > accept_channel_v2_msg.common_fields.dust_limit_satoshis + { + println!("Change: {acceptor_change_value_satoshis} satoshis"); + let _tx_add_output_msg = get_event_msg!( + nodes[1], + MessageSendEvent::SendTxAddOutput, + nodes[0].node.get_our_node_id() + ); + } else { + let _tx_complete_msg = get_event_msg!( + nodes[1], + MessageSendEvent::SendTxComplete, + nodes[0].node.get_our_node_id() + ); + } + + let tx_complete_msg = TxComplete { channel_id }; + + nodes[1].node.handle_tx_complete(nodes[0].node.get_our_node_id(), &tx_complete_msg); + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + let update_htlcs_msg_event = if acceptor_funding_inputs_count > 0 { + assert_eq!(msg_events.len(), 2); + match msg_events[0] { + MessageSendEvent::SendTxComplete { ref node_id, .. } => { + assert_eq!(*node_id, nodes[0].node.get_our_node_id()); + }, + _ => panic!("Unexpected event"), + }; + &msg_events[1] + } else { + assert_eq!(msg_events.len(), 1); + &msg_events[0] + }; + let _msg_commitment_signed_from_1 = match update_htlcs_msg_event { + MessageSendEvent::UpdateHTLCs { node_id, channel_id: _, updates } => { + assert_eq!(*node_id, nodes[0].node.get_our_node_id()); + updates.commitment_signed.clone() + }, + _ => panic!("Unexpected event"), + }; + + let (funding_outpoint, channel_type_features) = { + let per_peer_state = nodes[1].node.per_peer_state.read().unwrap(); + let peer_state = + per_peer_state.get(&nodes[0].node.get_our_node_id()).unwrap().lock().unwrap(); + let channel_funding = + peer_state.channel_by_id.get(&tx_complete_msg.channel_id).unwrap().funding(); + (channel_funding.get_funding_txo(), channel_funding.get_channel_type().clone()) + }; + + channel.funding.channel_transaction_parameters = ChannelTransactionParameters { + counterparty_parameters: Some(CounterpartyChannelTransactionParameters { + pubkeys: ChannelPublicKeys { + funding_pubkey: accept_channel_v2_msg.common_fields.funding_pubkey, + revocation_basepoint: RevocationBasepoint( + accept_channel_v2_msg.common_fields.revocation_basepoint, + ), + payment_point: accept_channel_v2_msg.common_fields.payment_basepoint, + delayed_payment_basepoint: DelayedPaymentBasepoint( + accept_channel_v2_msg.common_fields.delayed_payment_basepoint, + ), + htlc_basepoint: HtlcBasepoint(accept_channel_v2_msg.common_fields.htlc_basepoint), + }, + selected_contest_delay: accept_channel_v2_msg.common_fields.to_self_delay, + }), + holder_pubkeys: ChannelPublicKeys { + funding_pubkey: open_channel_v2_msg.common_fields.funding_pubkey, + revocation_basepoint: RevocationBasepoint( + open_channel_v2_msg.common_fields.revocation_basepoint, + ), + payment_point: open_channel_v2_msg.common_fields.payment_basepoint, + delayed_payment_basepoint: DelayedPaymentBasepoint( + open_channel_v2_msg.common_fields.delayed_payment_basepoint, + ), + htlc_basepoint: HtlcBasepoint(open_channel_v2_msg.common_fields.htlc_basepoint), + }, + holder_selected_contest_delay: open_channel_v2_msg.common_fields.to_self_delay, + is_outbound_from_holder: true, + funding_outpoint, + splice_parent_funding_txid: None, + channel_type_features, + channel_value_satoshis: initiator_funding_satoshis + .saturating_add(session.acceptor_funding_satoshis), + }; + + let (signature, htlc_signatures) = channel + .context + .get_initial_counterparty_commitment_signatures_for_test( + &mut channel.funding, + &&logger_a, + accept_channel_v2_msg.common_fields.first_per_commitment_point, + ) + .unwrap(); + + let msg_commitment_signed_from_0 = CommitmentSigned { + channel_id, + signature, + htlc_signatures, + funding_txid: None, + #[cfg(taproot)] + partial_signature_with_nonce: None, + }; + + chanmon_cfgs[1].persister.set_update_ret(crate::chain::ChannelMonitorUpdateStatus::InProgress); + + // Handle the initial commitment_signed exchange. Order is not important here. + nodes[1] + .node + .handle_commitment_signed(nodes[0].node.get_our_node_id(), &msg_commitment_signed_from_0); + check_added_monitors(&nodes[1], 1); + + // The funding transaction should not have been broadcast before persisting initial monitor has + // been completed. + assert_eq!(nodes[1].tx_broadcaster.txn_broadcast().len(), 0); + + // Complete the persistence of the monitor. + let events = nodes[1].node.get_and_clear_pending_events(); + assert!(events.is_empty()); + nodes[1].chain_monitor.complete_sole_pending_chan_update(&channel_id); + + if acceptor_funding_inputs_count > 0 { + let events = nodes[1].node.get_and_clear_pending_events(); + match &events[0] { + Event::FundingTransactionReadyForSigning { + counterparty_node_id, + unsigned_transaction, + .. + } => { + assert_eq!(counterparty_node_id, &nodes[0].node.get_our_node_id()); + let mut transaction = unsigned_transaction.clone(); + let mut sighash_cache = SighashCache::new(unsigned_transaction); + for (idx, input) in transaction.input.iter_mut().enumerate() { + if input.previous_output.txid == acceptor_funding_inputs[0].utxo.outpoint.txid { + let sighash = sighash_cache + .p2wpkh_signature_hash( + idx, + &acceptor_funding_inputs[0].utxo.output.script_pubkey, + acceptor_funding_inputs[0].utxo.output.value, + bitcoin::EcdsaSighashType::All, + ) + .unwrap(); + let msg = Message::from_digest(sighash.as_raw_hash().to_byte_array()); + + let signature = + secp_ctx.sign_ecdsa(&msg, &acceptor_external_keypair.secret_key()); + let mut witness = Witness::p2wpkh( + &bitcoin::ecdsa::Signature::sighash_all(signature), + &acceptor_external_keypair.public_key(), + ); + input.witness = witness; + } + } + nodes[1] + .node + .funding_transaction_signed(&channel_id, counterparty_node_id, transaction) + .unwrap(); + }, + _ => panic!("Unexpected event"), + }; + } + + if session.acceptor_input_value_satoshis < session.initiator_input_value_satoshis { + let tx_signatures_msg = get_event_msg!( + nodes[1], + MessageSendEvent::SendTxSignatures, + nodes[0].node.get_our_node_id() + ); + + assert_eq!(tx_signatures_msg.channel_id, channel_id); + + let mut witness = Witness::new(); + witness.push([0x0]); + // Receive tx_signatures from channel initiator. + nodes[1].node.handle_tx_signatures( + nodes[0].node.get_our_node_id(), + &TxSignatures { + channel_id, + tx_hash: funding_outpoint.unwrap().txid, + witnesses: vec![witness], + shared_input_signature: None, + }, + ); + } else { + let mut witness = Witness::new(); + witness.push([0x0]); + // Receive tx_signatures from channel initiator. + nodes[1].node.handle_tx_signatures( + nodes[0].node.get_our_node_id(), + &TxSignatures { + channel_id, + tx_hash: funding_outpoint.unwrap().txid, + witnesses: vec![witness], + shared_input_signature: None, + }, + ); + + let tx_signatures_msg = get_event_msg!( + nodes[1], + MessageSendEvent::SendTxSignatures, + nodes[0].node.get_our_node_id() + ); + + assert_eq!(tx_signatures_msg.channel_id, channel_id); + } + + let events = nodes[1].node.get_and_clear_pending_events(); + if acceptor_funding_inputs_count == 0 { + assert_eq!(events.len(), 1); + match events[0] { + Event::ChannelPending { channel_id: chan_id, .. } => assert_eq!(chan_id, channel_id), + _ => panic!("Unexpected event"), + }; + } + + // For an inbound channel V2 channel the transaction should be broadcast once receiving a + // tx_signature and applying local tx_signatures: + let broadcasted_txs = nodes[1].tx_broadcaster.txn_broadcast(); + assert_eq!(broadcasted_txs.len(), 1); +} + +#[test] +fn test_v2_channel_establishment() { + // Initiator contributes inputs, acceptor does not. + do_test_v2_channel_establishment(V2ChannelEstablishmentTestSession { + initiator_funding_satoshis: 100_00, + initiator_input_value_satoshis: 150_000, + acceptor_funding_satoshis: 0, + acceptor_input_value_satoshis: 0, + }); + // Initiator contributes more input value than acceptor. + do_test_v2_channel_establishment(V2ChannelEstablishmentTestSession { + initiator_funding_satoshis: 100_00, + initiator_input_value_satoshis: 150_000, + acceptor_funding_satoshis: 50_00, + acceptor_input_value_satoshis: 100_000, + }); + // Initiator contributes less input value than acceptor. + do_test_v2_channel_establishment(V2ChannelEstablishmentTestSession { + initiator_funding_satoshis: 100_00, + initiator_input_value_satoshis: 150_000, + acceptor_funding_satoshis: 125_00, + acceptor_input_value_satoshis: 200_000, + }); + // Initiator contributes the same input value as acceptor. + // nodes[0] node_id: 88ce8f35acfc... + // nodes[1] node_id: 236cdaa42692... + // Since nodes[1] has a node_id in earlier lexicographical order, it should send tx_signatures first. + do_test_v2_channel_establishment(V2ChannelEstablishmentTestSession { + initiator_funding_satoshis: 100_00, + initiator_input_value_satoshis: 150_000, + acceptor_funding_satoshis: 125_00, + acceptor_input_value_satoshis: 150_000, + }); +} diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 5fac0fd9b4f..3aaa2253aaa 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -66,7 +66,7 @@ use bitcoin::script::ScriptBuf; use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::transaction::{self, Version as TxVersion}; use bitcoin::transaction::{Transaction, TxIn, TxOut}; -use bitcoin::WPubkeyHash; +use bitcoin::CompressedPublicKey; use crate::io; use crate::prelude::*; @@ -1475,7 +1475,7 @@ fn internal_create_funding_transaction<'a, 'b, 'c>( /// Create test inputs for a funding transaction. /// Return the inputs (with prev tx), and the total witness weight for these inputs pub fn create_dual_funding_utxos_with_prev_txs( - node: &Node<'_, '_, '_>, utxo_values_in_satoshis: &[u64], + node: &Node<'_, '_, '_>, utxo_values_in_satoshis: &[u64], pubkey: &PublicKey, ) -> Vec { // Ensure we have unique transactions per node by using the locktime. let tx = Transaction { @@ -1491,7 +1491,7 @@ pub fn create_dual_funding_utxos_with_prev_txs( .iter() .map(|value_satoshis| TxOut { value: Amount::from_sat(*value_satoshis), - script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + script_pubkey: ScriptBuf::new_p2wpkh(&CompressedPublicKey(*pubkey).wpubkey_hash()), }) .collect(), }; diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 4340aad420a..457dc16b6ba 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -2320,6 +2320,11 @@ impl InteractiveTxConstructor { /// given inputs and outputs, and intended contribution. Takes into account the fees and the dust /// limit. /// +/// Note that since the type of change output cannot be determined at this point, this calculation +/// does not account for the weight contributed by the change output itself. The fees for the +/// weight of this change output should be subtracted from the result of this function call to get +/// the final amount for the change output (if above dust). +/// /// Three outcomes are possible: /// - Inputs are sufficient for intended contribution, fees, and a larger-than-dust change: /// `Ok(Some(change_amount))` diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index 29243ba8374..1ff0a15ea70 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -27,6 +27,7 @@ use crate::util::errors::APIError; use crate::util::ser::Writeable; use crate::util::test_channel_signer::SignerOp; +use bitcoin::key::{constants::SECRET_KEY_SIZE, Keypair, Secp256k1}; use bitcoin::secp256k1::PublicKey; use bitcoin::{Amount, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut}; @@ -43,10 +44,17 @@ fn test_v1_splice_in_negative_insufficient_inputs() { // Amount being added to the channel through the splice-in let splice_in_sats = 20_000; + let secp_ctx = Secp256k1::new(); + let initiator_external_keypair = + Keypair::from_seckey_slice(&secp_ctx, &[2; SECRET_KEY_SIZE]).unwrap(); + // Create additional inputs, but insufficient let extra_splice_funding_input_sats = splice_in_sats - 1; - let funding_inputs = - create_dual_funding_utxos_with_prev_txs(&nodes[0], &[extra_splice_funding_input_sats]); + let funding_inputs = create_dual_funding_utxos_with_prev_txs( + &nodes[0], + &[extra_splice_funding_input_sats], + &initiator_external_keypair.public_key(), + ); let contribution = SpliceContribution::SpliceIn { value: Amount::from_sat(splice_in_sats),