--- /dev/null
+// 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,
+ );
+}
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;
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,
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,
}
}
+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);