From 2deb41aa5c416c92c7a0f9bf58f73cfce072b064 Mon Sep 17 00:00:00 2001 From: Duncan Dean Date: Tue, 15 Oct 2024 15:10:44 +0200 Subject: [PATCH] Add manual testing for accepting dual-funded channels --- lightning/src/ln/channel.rs | 16 +- lightning/src/ln/channelmanager.rs | 14 +- lightning/src/ln/dual_funding_tests.rs | 266 ++++++++++++++++++++++ lightning/src/ln/functional_test_utils.rs | 46 +++- lightning/src/ln/mod.rs | 8 +- 5 files changed, 338 insertions(+), 12 deletions(-) create mode 100644 lightning/src/ln/dual_funding_tests.rs diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index d2e31fddf..83616aa11 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -4071,6 +4071,20 @@ impl ChannelContext where SP::Target: SignerProvider { partial_signature_with_nonce: None, }) } + + #[cfg(test)] + pub fn get_initial_counterparty_commitment_signature_for_test( + &mut self, logger: &L, channel_transaction_parameters: ChannelTransactionParameters, + counterparty_cur_commitment_point_override: PublicKey, + ) -> Result + where + SP::Target: SignerProvider, + L::Target: Logger + { + self.counterparty_cur_commitment_point = Some(counterparty_cur_commitment_point_override); + self.channel_transaction_parameters = channel_transaction_parameters; + self.get_initial_counterparty_commitment_signature(logger) + } } // Internal utility functions for channels @@ -8942,7 +8956,7 @@ impl InboundV2Channel where SP::Target: SignerProvider { is_initiator: false, inputs_to_contribute: funding_inputs, outputs_to_contribute: Vec::new(), - expected_remote_shared_funding_output: Some((context.get_funding_redeemscript(), context.channel_value_satoshis)), + expected_remote_shared_funding_output: Some((context.get_funding_redeemscript().to_p2wsh(), context.channel_value_satoshis)), } ).map_err(|_| ChannelError::Close(( "V2 channel rejected due to sender error".into(), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9ef6ca7ff..86faeac9a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -2600,8 +2600,14 @@ where /// offer they resolve to to the given one. pub testing_dnssec_proof_offer_resolution_override: Mutex>, + #[cfg(test)] + pub(super) entropy_source: ES, + #[cfg(not(test))] entropy_source: ES, node_signer: NS, + #[cfg(test)] + pub(super) signer_provider: SP, + #[cfg(not(test))] signer_provider: SP, logger: L, @@ -3469,6 +3475,11 @@ where &self.default_configuration } + #[cfg(test)] + pub fn create_and_insert_outbound_scid_alias_for_test(&self) -> u64 { + self.create_and_insert_outbound_scid_alias() + } + fn create_and_insert_outbound_scid_alias(&self) -> u64 { let height = self.best_block.read().unwrap().height; let mut outbound_scid_alias = 0; @@ -15674,9 +15685,6 @@ 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/dual_funding_tests.rs b/lightning/src/ln/dual_funding_tests.rs new file mode 100644 index 000000000..7742931cd --- /dev/null +++ b/lightning/src/ln/dual_funding_tests.rs @@ -0,0 +1,266 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Tests that test the creation of dual-funded channels in ChannelManager. + +use bitcoin::Weight; + +use crate::chain::chaininterface::{ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator}; +use crate::events::{Event, MessageSendEvent, MessageSendEventsProvider}; +use crate::ln::chan_utils::{ + make_funding_redeemscript, ChannelPublicKeys, ChannelTransactionParameters, + CounterpartyChannelTransactionParameters, +}; +use crate::ln::channel::{ + calculate_our_funding_satoshis, OutboundV2Channel, MIN_CHAN_DUST_LIMIT_SATOSHIS, +}; +use crate::ln::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBasepoint}; +use crate::ln::functional_test_utils::*; +use crate::ln::msgs::ChannelMessageHandler; +use crate::ln::msgs::{CommitmentSigned, TxAddInput, TxAddOutput, TxComplete}; +use crate::ln::types::ChannelId; +use crate::prelude::*; +use crate::sign::{ChannelSigner as _, P2WPKH_WITNESS_WEIGHT}; +use crate::util::ser::TransactionU16LenLimited; +use crate::util::test_utils; + +// Dual-funding: V2 Channel Establishment Tests +struct V2ChannelEstablishmentTestSession { + initiator_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, test_async_persist: bool, +) { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let logger_a = test_utils::TestLogger::with_id("node a".to_owned()); + + // Create a 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], + ) + .into_iter() + .map(|(txin, tx)| (txin, TransactionU16LenLimited::new(tx).unwrap())) + .collect(); + + // Alice creates a dual-funded channel as initiator. + let funding_feerate = node_cfgs[0] + .fee_estimator + .get_est_sat_per_1000_weight(ConfirmationTarget::NonAnchorChannelFee); + let funding_satoshis = calculate_our_funding_satoshis( + true, + &initiator_funding_inputs[..], + Weight::from_wu(P2WPKH_WITNESS_WEIGHT), + funding_feerate, + MIN_CHAN_DUST_LIMIT_SATOSHIS, + ) + .unwrap(); + let mut channel = OutboundV2Channel::new( + &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(), + funding_satoshis, + initiator_funding_inputs.clone(), + 42, /* user_channel_id */ + nodes[0].node.get_current_default_configuration(), + 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 accept_channel_v2_msg = get_event_msg!( + nodes[1], + MessageSendEvent::SendAcceptChannelV2, + nodes[0].node.get_our_node_id() + ); + 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 tx_add_input_msg = TxAddInput { + channel_id, + serial_id: 2, // Even serial_id from initiator. + prevtx: initiator_funding_inputs[0].1.clone(), + prevtx_out: 0, + sequence: initiator_funding_inputs[0].0.sequence.0, + shared_input_txid: None, + }; + 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); + + 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: 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 _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(); + 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"), + }; + + 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_context = + peer_state.channel_by_id.get(&tx_complete_msg.channel_id).unwrap().context(); + (channel_context.get_funding_txo(), channel_context.get_channel_type().clone()) + }; + + let 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, + channel_type_features, + }; + + channel + .context + .get_mut_signer() + .as_mut_ecdsa() + .unwrap() + .provide_channel_parameters(&channel_transaction_parameters); + + let msg_commitment_signed_from_0 = CommitmentSigned { + channel_id, + signature: channel + .context + .get_initial_counterparty_commitment_signature_for_test( + &&logger_a, + channel_transaction_parameters, + accept_channel_v2_msg.common_fields.first_per_commitment_point, + ) + .unwrap(), + htlc_signatures: vec![], + batch: None, + #[cfg(taproot)] + partial_signature_with_nonce: None, + }; + + if test_async_persist { + 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); + + if test_async_persist { + let events = nodes[1].node.get_and_clear_pending_events(); + assert!(events.is_empty()); + + chanmon_cfgs[1] + .persister + .set_update_ret(crate::chain::ChannelMonitorUpdateStatus::Completed); + let (outpoint, latest_update, _) = *nodes[1] + .chain_monitor + .latest_monitor_update_id + .lock() + .unwrap() + .get(&channel_id) + .unwrap(); + nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(outpoint, latest_update); + } + + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match events[0] { + Event::ChannelPending { channel_id: chan_id, .. } => assert_eq!(chan_id, channel_id), + _ => panic!("Unexpected event"), + }; + + 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); +} + +#[test] +fn test_v2_channel_establishment() { + // Only initiator contributes, no persist pending + do_test_v2_channel_establishment( + V2ChannelEstablishmentTestSession { initiator_input_value_satoshis: 100_000 }, + false, + ); + // Only initiator contributes, persist pending + do_test_v2_channel_establishment( + V2ChannelEstablishmentTestSession { initiator_input_value_satoshis: 100_000 }, + true, + ); +} diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index a6e07a8bc..6c6b24bce 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -38,17 +38,20 @@ use crate::util::test_utils; use crate::util::test_utils::{TestChainMonitor, TestScorer, TestKeysInterface}; use crate::util::ser::{ReadableArgs, Writeable}; +use bitcoin::WPubkeyHash; use bitcoin::amount::Amount; -use bitcoin::block::{Block, Header, Version}; -use bitcoin::locktime::absolute::LockTime; -use bitcoin::transaction::{Transaction, TxIn, TxOut}; +use bitcoin::block::{Block, Header, Version as BlockVersion}; +use bitcoin::locktime::absolute::{LockTime, LOCK_TIME_THRESHOLD}; +use bitcoin::transaction::{Sequence, Transaction, TxIn, TxOut}; 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::script::ScriptBuf; use bitcoin::secp256k1::{PublicKey, SecretKey}; -use bitcoin::transaction; +use bitcoin::transaction::{self, Version as TxVersion}; +use bitcoin::witness::Witness; use alloc::rc::Rc; use core::cell::RefCell; @@ -90,7 +93,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, @@ -217,7 +220,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, @@ -1235,6 +1238,37 @@ fn internal_create_funding_transaction<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, } } +pub fn create_dual_funding_utxos_with_prev_txs( + node: &Node<'_, '_, '_>, 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.compute_txid(), + index: i as u16, + }.into_bitcoin_outpoint(), + script_sig: ScriptBuf::new(), + sequence: Sequence::ZERO, + witness: Witness::new(), + }, tx.clone())); + } + result +} + pub fn sign_funding_transaction<'a, 'b, 'c>(node_a: &Node<'a, 'b, 'c>, node_b: &Node<'a, 'b, 'c>, channel_value: u64, expected_temporary_channel_id: ChannelId) -> Transaction { let (temporary_channel_id, tx, funding_output) = create_funding_transaction(node_a, &node_b.node.get_our_node_id(), channel_value, 42); assert_eq!(temporary_channel_id, expected_temporary_channel_id); diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index 087ec4df6..e1631a289 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -44,6 +44,9 @@ pub(crate) mod onion_utils; mod outbound_payment; pub mod wire; +#[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled. +pub(crate) mod interactivetxs; + pub use onion_utils::create_payment_onion; // Older rustc (which we support) refuses to let us call the get_payment_preimage_hash!() macro // without the node parameter being mut. This is incorrect, and thus newer rustcs will complain @@ -88,7 +91,8 @@ mod async_signer_tests; #[cfg(test)] #[allow(unused_mut)] mod offers_tests; -#[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled. -pub(crate) mod interactivetxs; +#[cfg(test)] +#[allow(unused_mut)] +mod dual_funding_tests; pub use self::peer_channel_encryptor::LN_MAX_MSG_LEN; -- 2.39.5