Skip to content

Commit

Permalink
Add manual testing for accepting dual-funded channels
Browse files Browse the repository at this point in the history
  • Loading branch information
dunxen committed Nov 12, 2024
1 parent 7618ebf commit cebc759
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 12 deletions.
16 changes: 15 additions & 1 deletion lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4073,6 +4073,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
Expand Down Expand Up @@ -8936,7 +8950,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(),
Expand Down
14 changes: 11 additions & 3 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2577,8 +2577,14 @@ where
/// [`ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee`] estimate.
last_days_feerates: Mutex<VecDeque<(u32, u32)>>,

#[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,
Expand Down Expand Up @@ -3406,6 +3412,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;
Expand Down Expand Up @@ -15189,9 +15200,6 @@ mod tests {

expect_pending_htlcs_forwardable!(nodes[0]);
}

// Dual-funding: V2 Channel Establishment Tests
// TODO(dual_funding): Complete these.
}

#[cfg(ldk_bench)]
Expand Down
224 changes: 224 additions & 0 deletions lightning/src/ln/dual_funding_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// 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 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 _;
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) {
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[..],
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,
};

// 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);

let events = nodes[1].node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
match events[0] {
Event::ChannelPending { channel_id, .. } => channel_id == channel.context.channel_id(),
_ => panic!("Unexpected event"),
};
}

#[test]
fn test_v2_channel_establishment() {
// Only initiator contributes
do_test_v2_channel_establishment(V2ChannelEstablishmentTestSession {
initiator_input_value_satoshis: 100_000,
});
}
46 changes: 40 additions & 6 deletions lightning/src/ln/functional_test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1221,6 +1224,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);
Expand Down
8 changes: 6 additions & 2 deletions lightning/src/ln/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

0 comments on commit cebc759

Please sign in to comment.