]> git.bitcoin.ninja Git - rust-lightning/commitdiff
Add manual testing for accepting dual-funded channels
authorDuncan Dean <git@dunxen.dev>
Tue, 15 Oct 2024 13:10:44 +0000 (15:10 +0200)
committerDuncan Dean <git@dunxen.dev>
Wed, 20 Nov 2024 12:04:14 +0000 (14:04 +0200)
lightning/src/ln/channel.rs
lightning/src/ln/channelmanager.rs
lightning/src/ln/dual_funding_tests.rs [new file with mode: 0644]
lightning/src/ln/functional_test_utils.rs
lightning/src/ln/mod.rs

index d2e31fddfa02acbd2348205722d18b7a007716cd..83616aa11130200402352cd1df6a5341ef44e79d 100644 (file)
@@ -4071,6 +4071,20 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
                        partial_signature_with_nonce: None,
                })
        }
+
+       #[cfg(test)]
+       pub fn get_initial_counterparty_commitment_signature_for_test<L: Deref>(
+               &mut self, logger: &L, channel_transaction_parameters: ChannelTransactionParameters,
+               counterparty_cur_commitment_point_override: PublicKey,
+       ) -> Result<Signature, ChannelError>
+       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<SP: Deref> InboundV2Channel<SP> 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(),
index 9ef6ca7ffaeba98ac7b8bb33a56691195ed529d6..86faeac9a6820dad238ce21dfec2234f08c7493b 100644 (file)
@@ -2600,8 +2600,14 @@ where
        /// offer they resolve to to the given one.
        pub testing_dnssec_proof_offer_resolution_override: Mutex<HashMap<HumanReadableName, Offer>>,
 
+       #[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 (file)
index 0000000..7742931
--- /dev/null
@@ -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 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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,
+       );
+}
index a6e07a8bc446db615542c0eaf82376151bcd1be9..6c6b24bce7c3ccafaf10fe9c2de9d99b79fab6b8 100644 (file)
@@ -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);
index 087ec4df67e736d91fe4c3c48b3697db474e5263..e1631a2892c85bad3064487a6cd3919cbdc25787 100644 (file)
@@ -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;