Skip to content

Commit

Permalink
Add test for aggregated revoked HTLC claim on anchors channel
Browse files Browse the repository at this point in the history
  • Loading branch information
wpaulino committed Feb 15, 2023
1 parent 4240f5d commit 45dcd93
Showing 1 changed file with 232 additions and 1 deletion.
233 changes: 232 additions & 1 deletion lightning/src/ln/monitor_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,19 @@ use crate::chain::chaininterface::LowerBoundedFeeEstimator;
use crate::ln::channel;
#[cfg(anchors)]
use crate::ln::chan_utils;
#[cfg(anchors)]
use crate::ln::channelmanager::ChannelManager;
use crate::ln::channelmanager::{BREAKDOWN_TIMEOUT, PaymentId};
use crate::ln::msgs::ChannelMessageHandler;
#[cfg(anchors)]
use crate::util::config::UserConfig;
#[cfg(anchors)]
use crate::util::events::BumpTransactionEvent;
use crate::util::events::{Event, MessageSendEvent, MessageSendEventsProvider, ClosureReason, HTLCDestination};
#[cfg(anchors)]
use crate::util::ser::Writeable;
#[cfg(anchors)]
use crate::util::test_utils;

use bitcoin::blockdata::script::Builder;
use bitcoin::blockdata::opcodes;
Expand Down Expand Up @@ -1749,7 +1755,7 @@ fn test_yield_anchors_events() {

let mut holder_events = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events();
// Certain block `ConnectStyle`s cause an extra `ChannelClose` event to be emitted since the
// best block is being updated prior to the confirmed transactions.
// best block is updated before the confirmed transactions are notified.
match *nodes[0].connect_style.borrow() {
ConnectStyle::BestBlockFirst|ConnectStyle::BestBlockFirstReorgsOnlyTip|ConnectStyle::BestBlockFirstSkippingBlocks => {
assert_eq!(holder_events.len(), 3);
Expand Down Expand Up @@ -1816,3 +1822,228 @@ fn test_yield_anchors_events() {
// Clear the remaining events as they're not relevant to what we're testing.
nodes[0].node.get_and_clear_pending_events();
}

#[cfg(anchors)]
#[test]
fn test_anchors_aggregated_revoked_htlc_tx() {
// Test that `ChannelMonitor`s can properly detect and claim funds from a counterparty claiming
// multiple HTLCs in a single transaction via the success path from a revoked commitment.
let secp = Secp256k1::new();
let mut chanmon_cfgs = create_chanmon_cfgs(2);
// Required to sign a revoked commitment transaction
chanmon_cfgs[1].keys_manager.disable_revocation_policy_check = true;
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let mut anchors_config = UserConfig::default();
anchors_config.channel_handshake_config.announced_channel = true;
anchors_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(anchors_config), Some(anchors_config)]);

let bob_persister: test_utils::TestPersister;
let bob_chain_monitor: test_utils::TestChainMonitor;
let bob_deserialized: ChannelManager<
&test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface,
&test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator,
&test_utils::TestRouter, &test_utils::TestLogger,
>;

let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);

let chan_id = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000).2;

// Route two payments from Alice to Bob to lock in the HTLCs.
let (payment_preimage_a, payment_hash_a, _) = route_payment(&nodes[0], &[&nodes[1]], 100_000_000);
let (payment_preimage_b, payment_hash_b, _) = route_payment(&nodes[0], &[&nodes[1]], 100_000_000);

// Serialize Bob with the HTLCs locked in. We'll restart Bob later on with the state at this
// point such that he broadcasts a revoked commitment transaction.
let bob_serialized = nodes[1].node.encode();
let bob_serialized_monitor = get_monitor!(nodes[1], chan_id).encode();

// Bob claims both HTLCs...
claim_payment(&nodes[0], &[&nodes[1]], payment_preimage_a);
claim_payment(&nodes[0], &[&nodes[1]], payment_preimage_b);

// ...and sends one back such that he has a motive to broadcast his revoked state.
send_payment(&nodes[1], &[&nodes[0]], 30_000_000);

// Restart Bob with the revoked state and provide the HTLC preimages he claimed.
reload_node!(nodes[1], anchors_config, bob_serialized, &[&bob_serialized_monitor], bob_persister, bob_chain_monitor, bob_deserialized);
get_monitor!(nodes[1], chan_id).provide_payment_preimage(
&payment_hash_a, &payment_preimage_a, &node_cfgs[1].tx_broadcaster,
&LowerBoundedFeeEstimator::new(node_cfgs[1].fee_estimator), &nodes[1].logger
);
get_monitor!(nodes[1], chan_id).provide_payment_preimage(
&payment_hash_b, &payment_preimage_b, &node_cfgs[1].tx_broadcaster,
&LowerBoundedFeeEstimator::new(node_cfgs[1].fee_estimator), &nodes[1].logger
);

// Bob force closes by broadcasting his revoked state.
nodes[1].node.force_close_broadcasting_latest_txn(&chan_id, &nodes[0].node.get_our_node_id()).unwrap();
{
let mut txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
assert_eq!(txn.len(), 1);
let revoked_commitment = txn.pop().unwrap();
assert_eq!(revoked_commitment.output.len(), 6); // 2 HTLC outputs + 1 to_self output + 1 to_remote output + 2 anchor outputs
}

// Bob should now receive an event to bump his revoked commitment transaction's fee.
assert!(nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events().is_empty());
let events = nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
let (revoked_commitment, anchor_tx) = match &events[0] {
Event::BumpTransaction(BumpTransactionEvent::ChannelClose { commitment_tx, anchor_descriptor, .. }) => {
let mut anchor_tx = Transaction {
version: 2,
lock_time: PackedLockTime::ZERO,
input: vec![
TxIn { previous_output: anchor_descriptor.outpoint, ..Default::default() },
TxIn { ..Default::default() },
],
output: vec![TxOut {
value: Amount::ONE_BTC.to_sat(),
script_pubkey: Script::new_op_return(&[]),
}],
};
let signer = nodes[1].keys_manager.derive_channel_keys(
anchor_descriptor.channel_value_satoshis, &anchor_descriptor.channel_keys_id,
);
let funding_sig = signer.sign_holder_anchor_input(&mut anchor_tx, 0, &secp).unwrap();
anchor_tx.input[0].witness = chan_utils::build_anchor_input_witness(
&signer.pubkeys().funding_pubkey, &funding_sig
);
(commitment_tx, anchor_tx)
},
_ => panic!("Unexpected event"),
};

for node in &nodes {
mine_transactions(node, &[&revoked_commitment, &anchor_tx]);
check_added_monitors!(node, 1);
check_closed_broadcast!(node, true);
}
check_closed_event!(&nodes[0], 1, ClosureReason::CommitmentTxConfirmed);
check_closed_event!(&nodes[1], 1, ClosureReason::HolderForceClosed);

// Alice should detect the confirmed revoked commitment, and attempts to claim all of the
// revoked outputs.
{
let mut txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
assert_eq!(txn.len(), 1);
let tx = txn.pop().unwrap();
// TODO: to_self claim must be separate from HTLC claims
assert_eq!(tx.input.len(), 3); // Spends both HTLC outputs and to_self output
assert_eq!(tx.output.len(), 1);
check_spends!(tx, revoked_commitment);
}

// Since Bob was able to confirm his revoked commitment, he'll now try to claim the HTLCs
// through the success path.
assert!(nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events().is_empty());
let mut events = nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events();
// Certain block `ConnectStyle`s cause an extra `ChannelClose` event to be emitted since the
// best block is updated before the confirmed transactions are notified.
match *nodes[1].connect_style.borrow() {
ConnectStyle::BestBlockFirst|ConnectStyle::BestBlockFirstReorgsOnlyTip|ConnectStyle::BestBlockFirstSkippingBlocks => {
assert_eq!(events.len(), 2);
if let Event::BumpTransaction(BumpTransactionEvent::ChannelClose { .. }) = events.remove(0) {}
else { panic!("unexpected event"); }

},
_ => assert_eq!(events.len(), 1),
};
let mut htlc_tx = Transaction {
version: 2,
lock_time: PackedLockTime::ZERO,
input: vec![TxIn { ..Default::default() }], // Fee input
output: vec![TxOut { // Fee input change
value: Amount::ONE_BTC.to_sat(),
script_pubkey: Script::new_op_return(&[]),
}]
};
match &events[0] {
Event::BumpTransaction(BumpTransactionEvent::HTLCResolution { htlc_descriptors, .. }) => {
assert_eq!(htlc_descriptors.len(), 2);
for htlc_descriptor in htlc_descriptors {
assert!(!htlc_descriptor.htlc.offered);
let signer = nodes[1].keys_manager.derive_channel_keys(
htlc_descriptor.channel_value_satoshis, &htlc_descriptor.channel_keys_id
);
let per_commitment_point = signer.get_per_commitment_point(htlc_descriptor.per_commitment_number, &secp);
htlc_tx.input.push(htlc_descriptor.unsigned_tx_input());
htlc_tx.output.push(htlc_descriptor.tx_output(&per_commitment_point, &secp));
}
for (idx, htlc_descriptor) in htlc_descriptors.iter().enumerate() {
let htlc_input_idx = idx + 1;
let signer = nodes[1].keys_manager.derive_channel_keys(
htlc_descriptor.channel_value_satoshis, &htlc_descriptor.channel_keys_id
);
let our_sig = signer.sign_holder_htlc_transaction(&htlc_tx, htlc_input_idx, htlc_descriptor, &secp).unwrap();
let per_commitment_point = signer.get_per_commitment_point(htlc_descriptor.per_commitment_number, &secp);
let witness_script = htlc_descriptor.witness_script(&per_commitment_point, &secp);
htlc_tx.input[htlc_input_idx].witness = htlc_descriptor.tx_input_witness(&our_sig, &witness_script);
}
},
_ => panic!("Unexpected event"),
}

for node in &nodes {
mine_transaction(node, &htlc_tx);
}

// Alice should see that Bob is trying to claim to HTLCs, so she should now try to claim them at
// the second level instead.
let (revoked_to_self_claim, revoked_htlc_claim) = {
let mut txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
assert_eq!(txn.len(), 2);

let revoked_to_self_claim = txn.pop().unwrap();
assert_eq!(revoked_to_self_claim.input.len(), 1);
assert_eq!(revoked_to_self_claim.output.len(), 1);
check_spends!(revoked_to_self_claim, revoked_commitment);

let revoked_htlc_claim = txn.pop().unwrap();
assert_eq!(revoked_htlc_claim.input.len(), 2);
assert_eq!(revoked_htlc_claim.output.len(), 1);
check_spends!(revoked_htlc_claim, htlc_tx);

(revoked_to_self_claim, revoked_htlc_claim)
};
for node in &nodes {
mine_transactions(node, &[&revoked_to_self_claim, &revoked_htlc_claim]);
}


// Connect one block to make sure the HTLC events are not yielded while ANTI_REORG_DELAY has not
// been reached.
connect_blocks(&nodes[0], 1);
connect_blocks(&nodes[1], 1);

assert!(nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events().is_empty());
assert!(nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events().is_empty());

// Connect the remaining blocks to reach ANTI_REORG_DELAY.
connect_blocks(&nodes[0], ANTI_REORG_DELAY - 2);
connect_blocks(&nodes[1], ANTI_REORG_DELAY - 2);

assert!(nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events().is_empty());
let spendable_output_events = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events();
for (idx, event) in spendable_output_events.iter().enumerate() {
if let Event::SpendableOutputs { outputs } = event {
assert_eq!(outputs.len(), 1);
let spend_tx = nodes[0].keys_manager.backing.spend_spendable_outputs(
&[&outputs[0]], Vec::new(), Script::new_op_return(&[]), 253, &Secp256k1::new(),
).unwrap();
check_spends!(spend_tx, if idx == 0 { &revoked_to_self_claim } else { &revoked_htlc_claim });
} else {
panic!("unexpected event");
}
}

assert!(nodes[0].node.list_channels().is_empty());
assert!(nodes[1].node.list_channels().is_empty());
assert!(nodes[0].chain_monitor.chain_monitor.get_claimable_balances(&[]).is_empty());
// TODO: From Bob's PoV, he still thinks he can claim the outputs from his revoked commitment.
// This needs to be fixed before we enable pruning `ChannelMonitor`s once they don't have any
// balances to claim.
assert_eq!(nodes[1].chain_monitor.chain_monitor.get_claimable_balances(&[]).len(), 3);
}

0 comments on commit 45dcd93

Please sign in to comment.