do_test_revoked_counterparty_commitment_balances(true, false);
}
-#[test]
-fn test_revoked_counterparty_htlc_tx_balances() {
+fn do_test_revoked_counterparty_htlc_tx_balances(anchors: bool) {
// Tests `get_claimable_balances` for revocation spends of HTLC transactions.
let mut chanmon_cfgs = create_chanmon_cfgs(2);
chanmon_cfgs[1].keys_manager.disable_revocation_policy_check = true;
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
- let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+ let mut user_config = test_default_channel_config();
+ if anchors {
+ user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
+ user_config.manually_accept_inbound_channels = true;
+ }
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config), Some(user_config)]);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+ let coinbase_tx = Transaction {
+ version: 2,
+ lock_time: PackedLockTime::ZERO,
+ input: vec![TxIn { ..Default::default() }],
+ output: vec![
+ TxOut {
+ value: Amount::ONE_BTC.to_sat(),
+ script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
+ },
+ TxOut {
+ value: Amount::ONE_BTC.to_sat(),
+ script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
+ },
+ ],
+ };
+ if anchors {
+ nodes[0].wallet_source.add_utxo(bitcoin::OutPoint { txid: coinbase_tx.txid(), vout: 0 }, coinbase_tx.output[0].value);
+ nodes[1].wallet_source.add_utxo(bitcoin::OutPoint { txid: coinbase_tx.txid(), vout: 1 }, coinbase_tx.output[1].value);
+ }
+
// Create some initial channels
let (_, _, chan_id, funding_tx) =
- create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 11_000_000);
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 12_000_000);
let funding_outpoint = OutPoint { txid: funding_tx.txid(), index: 0 };
assert_eq!(funding_outpoint.to_channel_id(), chan_id);
let revoked_local_txn = get_local_commitment_txn!(nodes[1], chan_id);
assert_eq!(revoked_local_txn[0].input.len(), 1);
assert_eq!(revoked_local_txn[0].input[0].previous_output.txid, funding_tx.txid());
+ if anchors {
+ assert_eq!(revoked_local_txn[0].output[4].value, 11000); // to_self output
+ } else {
+ assert_eq!(revoked_local_txn[0].output[2].value, 11000); // to_self output
+ }
- // The to-be-revoked commitment tx should have two HTLCs and an output for both sides
- assert_eq!(revoked_local_txn[0].output.len(), 4);
+ // The to-be-revoked commitment tx should have two HTLCs, an output for each side, and an
+ // anchor output for each side if enabled.
+ assert_eq!(revoked_local_txn[0].output.len(), if anchors { 6 } else { 4 });
claim_payment(&nodes[0], &[&nodes[1]], payment_preimage);
check_closed_broadcast!(nodes[1], true);
check_added_monitors!(nodes[1], 1);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed, [nodes[0].node.get_our_node_id()], 1000000);
+ if anchors {
+ handle_bump_htlc_event(&nodes[1], 1);
+ }
let revoked_htlc_success = {
let mut txn = nodes[1].tx_broadcaster.txn_broadcast();
assert_eq!(txn.len(), 1);
- assert_eq!(txn[0].input.len(), 1);
- assert_eq!(txn[0].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
- check_spends!(txn[0], revoked_local_txn[0]);
+ assert_eq!(txn[0].input.len(), if anchors { 2 } else { 1 });
+ assert_eq!(txn[0].input[0].previous_output.vout, if anchors { 3 } else { 1 });
+ assert_eq!(txn[0].input[0].witness.last().unwrap().len(),
+ if anchors { ACCEPTED_HTLC_SCRIPT_WEIGHT_ANCHORS } else { ACCEPTED_HTLC_SCRIPT_WEIGHT });
+ check_spends!(txn[0], revoked_local_txn[0], coinbase_tx);
txn.pop().unwrap()
};
+ let revoked_htlc_success_fee = chan_feerate * revoked_htlc_success.weight() as u64 / 1000;
connect_blocks(&nodes[1], TEST_FINAL_CLTV);
+ if anchors {
+ handle_bump_htlc_event(&nodes[1], 2);
+ }
let revoked_htlc_timeout = {
let mut txn = nodes[1].tx_broadcaster.unique_txn_broadcast();
assert_eq!(txn.len(), 2);
txn.remove(0)
}
};
- check_spends!(revoked_htlc_timeout, revoked_local_txn[0]);
+ check_spends!(revoked_htlc_timeout, revoked_local_txn[0], coinbase_tx);
assert_ne!(revoked_htlc_success.input[0].previous_output, revoked_htlc_timeout.input[0].previous_output);
assert_eq!(revoked_htlc_success.lock_time.0, 0);
assert_ne!(revoked_htlc_timeout.lock_time.0, 0);
check_closed_event!(nodes[0], 1, ClosureReason::CommitmentTxConfirmed, [nodes[1].node.get_our_node_id()], 1000000);
let to_remote_conf_height = nodes[0].best_block_info().1 + ANTI_REORG_DELAY - 1;
- let as_commitment_claim_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
- assert_eq!(as_commitment_claim_txn.len(), 1);
- check_spends!(as_commitment_claim_txn[0], revoked_local_txn[0]);
+ let revoked_to_self_claim = {
+ let mut as_commitment_claim_txn = nodes[0].tx_broadcaster.txn_broadcast();
+ assert_eq!(as_commitment_claim_txn.len(), if anchors { 2 } else { 1 });
+ if anchors {
+ assert_eq!(as_commitment_claim_txn[0].input.len(), 1);
+ assert_eq!(as_commitment_claim_txn[0].input[0].previous_output.vout, 4); // Separate to_remote claim
+ check_spends!(as_commitment_claim_txn[0], revoked_local_txn[0]);
+ assert_eq!(as_commitment_claim_txn[1].input.len(), 2);
+ assert_eq!(as_commitment_claim_txn[1].input[0].previous_output.vout, 2);
+ assert_eq!(as_commitment_claim_txn[1].input[1].previous_output.vout, 3);
+ check_spends!(as_commitment_claim_txn[1], revoked_local_txn[0]);
+ Some(as_commitment_claim_txn.remove(0))
+ } else {
+ assert_eq!(as_commitment_claim_txn[0].input.len(), 3);
+ assert_eq!(as_commitment_claim_txn[0].input[0].previous_output.vout, 2);
+ assert_eq!(as_commitment_claim_txn[0].input[1].previous_output.vout, 0);
+ assert_eq!(as_commitment_claim_txn[0].input[2].previous_output.vout, 1);
+ check_spends!(as_commitment_claim_txn[0], revoked_local_txn[0]);
+ None
+ }
+ };
// The next two checks have the same balance set for A - even though we confirm a revoked HTLC
// transaction our balance tracking doesn't use the on-chain value so the
// `CounterpartyRevokedOutputClaimable` entry doesn't change.
+ let commitment_tx_fee = chan_feerate *
+ (channel::commitment_tx_base_weight(&channel_type_features) + 2 * channel::COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000;
+ let anchor_outputs_value = if anchors { channel::ANCHOR_OUTPUT_VALUE_SATOSHI * 2 } else { 0 };
let as_balances = sorted_vec(vec![Balance::ClaimableAwaitingConfirmations {
// to_remote output in B's revoked commitment
- amount_satoshis: 1_000_000 - 11_000 - 3_000 - chan_feerate *
- (channel::commitment_tx_base_weight(&channel_type_features) + 2 * channel::COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000,
+ amount_satoshis: 1_000_000 - 12_000 - 3_000 - commitment_tx_fee - anchor_outputs_value,
confirmation_height: to_remote_conf_height,
}, Balance::CounterpartyRevokedOutputClaimable {
// to_self output in B's revoked commitment
- amount_satoshis: 10_000,
+ amount_satoshis: 11_000,
}, Balance::CounterpartyRevokedOutputClaimable { // HTLC 1
amount_satoshis: 3_000,
}, Balance::CounterpartyRevokedOutputClaimable { // HTLC 2
mine_transaction(&nodes[0], &revoked_htlc_success);
let as_htlc_claim_tx = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
assert_eq!(as_htlc_claim_tx.len(), 2);
+ assert_eq!(as_htlc_claim_tx[0].input.len(), 1);
check_spends!(as_htlc_claim_tx[0], revoked_htlc_success);
- check_spends!(as_htlc_claim_tx[1], revoked_local_txn[0]); // A has to generate a new claim for the remaining revoked
- // outputs (which no longer includes the spent HTLC output)
+ // A has to generate a new claim for the remaining revoked outputs (which no longer includes the
+ // spent HTLC output)
+ assert_eq!(as_htlc_claim_tx[1].input.len(), if anchors { 1 } else { 2 });
+ assert_eq!(as_htlc_claim_tx[1].input[0].previous_output.vout, 2);
+ if !anchors {
+ assert_eq!(as_htlc_claim_tx[1].input[1].previous_output.vout, 0);
+ }
+ check_spends!(as_htlc_claim_tx[1], revoked_local_txn[0]);
assert_eq!(as_balances,
sorted_vec(nodes[0].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances()));
assert_eq!(as_htlc_claim_tx[0].output.len(), 1);
- fuzzy_assert_eq(as_htlc_claim_tx[0].output[0].value,
- 3_000 - chan_feerate * (revoked_htlc_success.weight() + as_htlc_claim_tx[0].weight()) as u64 / 1000);
+ let as_revoked_htlc_success_claim_fee = chan_feerate * as_htlc_claim_tx[0].weight() as u64 / 1000;
+ if anchors {
+ // With anchors, B can pay for revoked_htlc_success's fee with additional inputs, rather
+ // than with the HTLC itself.
+ fuzzy_assert_eq(as_htlc_claim_tx[0].output[0].value,
+ 3_000 - as_revoked_htlc_success_claim_fee);
+ } else {
+ fuzzy_assert_eq(as_htlc_claim_tx[0].output[0].value,
+ 3_000 - revoked_htlc_success_fee - as_revoked_htlc_success_claim_fee);
+ }
mine_transaction(&nodes[0], &as_htlc_claim_tx[0]);
assert_eq!(sorted_vec(vec![Balance::ClaimableAwaitingConfirmations {
// to_remote output in B's revoked commitment
- amount_satoshis: 1_000_000 - 11_000 - 3_000 - chan_feerate *
- (channel::commitment_tx_base_weight(&channel_type_features) + 2 * channel::COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000,
+ amount_satoshis: 1_000_000 - 12_000 - 3_000 - commitment_tx_fee - anchor_outputs_value,
confirmation_height: to_remote_conf_height,
}, Balance::CounterpartyRevokedOutputClaimable {
// to_self output in B's revoked commitment
- amount_satoshis: 10_000,
+ amount_satoshis: 11_000,
}, Balance::CounterpartyRevokedOutputClaimable { // HTLC 2
amount_satoshis: 1_000,
}, Balance::ClaimableAwaitingConfirmations {
test_spendable_output(&nodes[0], &revoked_local_txn[0], false);
assert_eq!(sorted_vec(vec![Balance::CounterpartyRevokedOutputClaimable {
// to_self output to B
- amount_satoshis: 10_000,
+ amount_satoshis: 11_000,
}, Balance::CounterpartyRevokedOutputClaimable { // HTLC 2
amount_satoshis: 1_000,
}, Balance::ClaimableAwaitingConfirmations {
test_spendable_output(&nodes[0], &as_htlc_claim_tx[0], false);
assert_eq!(sorted_vec(vec![Balance::CounterpartyRevokedOutputClaimable {
// to_self output in B's revoked commitment
- amount_satoshis: 10_000,
+ amount_satoshis: 11_000,
}, Balance::CounterpartyRevokedOutputClaimable { // HTLC 2
amount_satoshis: 1_000,
}]),
}
mine_transaction(&nodes[0], &revoked_htlc_timeout);
- let as_second_htlc_claim_tx = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
- assert_eq!(as_second_htlc_claim_tx.len(), 2);
-
- check_spends!(as_second_htlc_claim_tx[0], revoked_htlc_timeout);
- check_spends!(as_second_htlc_claim_tx[1], revoked_local_txn[0]);
+ let (revoked_htlc_timeout_claim, revoked_to_self_claim) = {
+ let mut as_second_htlc_claim_tx = nodes[0].tx_broadcaster.txn_broadcast();
+ assert_eq!(as_second_htlc_claim_tx.len(), if anchors { 1 } else { 2 });
+ if anchors {
+ assert_eq!(as_second_htlc_claim_tx[0].input.len(), 1);
+ assert_eq!(as_second_htlc_claim_tx[0].input[0].previous_output.vout, 0);
+ check_spends!(as_second_htlc_claim_tx[0], revoked_htlc_timeout);
+ (as_second_htlc_claim_tx.remove(0), revoked_to_self_claim.unwrap())
+ } else {
+ assert_eq!(as_second_htlc_claim_tx[0].input.len(), 1);
+ assert_eq!(as_second_htlc_claim_tx[0].input[0].previous_output.vout, 0);
+ check_spends!(as_second_htlc_claim_tx[0], revoked_htlc_timeout);
+ assert_eq!(as_second_htlc_claim_tx[1].input.len(), 1);
+ assert_eq!(as_second_htlc_claim_tx[1].input[0].previous_output.vout, 2);
+ check_spends!(as_second_htlc_claim_tx[1], revoked_local_txn[0]);
+ (as_second_htlc_claim_tx.remove(0), as_second_htlc_claim_tx.remove(0))
+ }
+ };
// Connect blocks to finalize the HTLC resolution with the HTLC-Timeout transaction. In a
// previous iteration of the revoked balance handling this would result in us "forgetting" that
connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1);
assert_eq!(sorted_vec(vec![Balance::CounterpartyRevokedOutputClaimable {
// to_self output in B's revoked commitment
- amount_satoshis: 10_000,
+ amount_satoshis: 11_000,
}, Balance::CounterpartyRevokedOutputClaimable { // HTLC 2
amount_satoshis: 1_000,
}]),
sorted_vec(nodes[0].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances()));
- mine_transaction(&nodes[0], &as_second_htlc_claim_tx[0]);
+ mine_transaction(&nodes[0], &revoked_htlc_timeout_claim);
assert_eq!(sorted_vec(vec![Balance::CounterpartyRevokedOutputClaimable {
// to_self output in B's revoked commitment
- amount_satoshis: 10_000,
+ amount_satoshis: 11_000,
}, Balance::ClaimableAwaitingConfirmations {
- amount_satoshis: as_second_htlc_claim_tx[0].output[0].value,
+ amount_satoshis: revoked_htlc_timeout_claim.output[0].value,
confirmation_height: nodes[0].best_block_info().1 + ANTI_REORG_DELAY - 1,
}]),
sorted_vec(nodes[0].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances()));
- mine_transaction(&nodes[0], &as_second_htlc_claim_tx[1]);
+ mine_transaction(&nodes[0], &revoked_to_self_claim);
assert_eq!(sorted_vec(vec![Balance::ClaimableAwaitingConfirmations {
// to_self output in B's revoked commitment
- amount_satoshis: as_second_htlc_claim_tx[1].output[0].value,
+ amount_satoshis: revoked_to_self_claim.output[0].value,
confirmation_height: nodes[0].best_block_info().1 + ANTI_REORG_DELAY - 1,
}, Balance::ClaimableAwaitingConfirmations {
- amount_satoshis: as_second_htlc_claim_tx[0].output[0].value,
+ amount_satoshis: revoked_htlc_timeout_claim.output[0].value,
confirmation_height: nodes[0].best_block_info().1 + ANTI_REORG_DELAY - 2,
}]),
sorted_vec(nodes[0].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances()));
connect_blocks(&nodes[0], ANTI_REORG_DELAY - 2);
- test_spendable_output(&nodes[0], &as_second_htlc_claim_tx[0], false);
+ test_spendable_output(&nodes[0], &revoked_htlc_timeout_claim, false);
connect_blocks(&nodes[0], 1);
- test_spendable_output(&nodes[0], &as_second_htlc_claim_tx[1], false);
+ test_spendable_output(&nodes[0], &revoked_to_self_claim, false);
assert_eq!(nodes[0].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances(), Vec::new());
}
#[test]
-fn test_revoked_counterparty_aggregated_claims() {
+fn test_revoked_counterparty_htlc_tx_balances() {
+ do_test_revoked_counterparty_htlc_tx_balances(false);
+ do_test_revoked_counterparty_htlc_tx_balances(true);
+}
+
+fn do_test_revoked_counterparty_aggregated_claims(anchors: bool) {
// Tests `get_claimable_balances` for revoked counterparty commitment transactions when
// claiming with an aggregated claim transaction.
let mut chanmon_cfgs = create_chanmon_cfgs(2);
// secret to the counterparty. However, because we always immediately take the revocation
// secret from the keys_manager, we would panic at broadcast as we're trying to sign a
// transaction which, from the point of view of our keys_manager, is revoked.
- chanmon_cfgs[1].keys_manager.disable_revocation_policy_check = true;
+ chanmon_cfgs[0].keys_manager.disable_revocation_policy_check = true;
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
- let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+ let mut user_config = test_default_channel_config();
+ if anchors {
+ user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
+ user_config.manually_accept_inbound_channels = true;
+ }
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config), Some(user_config)]);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+ let coinbase_tx = Transaction {
+ version: 2,
+ lock_time: PackedLockTime::ZERO,
+ input: vec![TxIn { ..Default::default() }],
+ output: vec![TxOut {
+ value: Amount::ONE_BTC.to_sat(),
+ script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
+ }],
+ };
+ nodes[0].wallet_source.add_utxo(bitcoin::OutPoint { txid: coinbase_tx.txid(), vout: 0 }, coinbase_tx.output[0].value);
+
let (_, _, chan_id, funding_tx) =
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 100_000_000);
let funding_outpoint = OutPoint { txid: funding_tx.txid(), index: 0 };
// Now get the latest commitment transaction from A and then update the fee to revoke it
let as_revoked_txn = get_local_commitment_txn!(nodes[0], chan_id);
- assert_eq!(as_revoked_txn.len(), 2);
+ assert_eq!(as_revoked_txn.len(), if anchors { 1 } else { 2 });
check_spends!(as_revoked_txn[0], funding_tx);
- check_spends!(as_revoked_txn[1], as_revoked_txn[0]); // The HTLC-Claim transaction
+ if !anchors {
+ check_spends!(as_revoked_txn[1], as_revoked_txn[0]); // The HTLC-Claim transaction
+ }
let channel_type_features = get_channel_type_features!(nodes[0], nodes[1], chan_id);
let chan_feerate = get_feerate!(nodes[0], nodes[1], chan_id) as u64;
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed, [nodes[0].node.get_our_node_id()], 1000000);
check_added_monitors!(nodes[1], 1);
- let mut claim_txn: Vec<_> = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().drain(..).filter(|tx| tx.input.iter().any(|inp| inp.previous_output.txid == as_revoked_txn[0].txid())).collect();
- // Currently the revoked commitment outputs are all claimed in one aggregated transaction
- assert_eq!(claim_txn.len(), 1);
- assert_eq!(claim_txn[0].input.len(), 3);
- check_spends!(claim_txn[0], as_revoked_txn[0]);
+ let mut claim_txn = nodes[1].tx_broadcaster.txn_broadcast();
+ assert_eq!(claim_txn.len(), if anchors { 2 } else { 1 });
+ let revoked_to_self_claim = if anchors {
+ assert_eq!(claim_txn[0].input.len(), 1);
+ assert_eq!(claim_txn[0].input[0].previous_output.vout, 5); // Separate to_remote claim
+ check_spends!(claim_txn[0], as_revoked_txn[0]);
+ assert_eq!(claim_txn[1].input.len(), 2);
+ assert_eq!(claim_txn[1].input[0].previous_output.vout, 2);
+ assert_eq!(claim_txn[1].input[1].previous_output.vout, 3);
+ check_spends!(claim_txn[1], as_revoked_txn[0]);
+ Some(claim_txn.remove(0))
+ } else {
+ assert_eq!(claim_txn[0].input.len(), 3);
+ assert_eq!(claim_txn[0].input[0].previous_output.vout, 3);
+ assert_eq!(claim_txn[0].input[1].previous_output.vout, 0);
+ assert_eq!(claim_txn[0].input[2].previous_output.vout, 1);
+ check_spends!(claim_txn[0], as_revoked_txn[0]);
+ None
+ };
let to_remote_maturity = nodes[1].best_block_info().1 + ANTI_REORG_DELAY - 1;
+ let commitment_tx_fee = chan_feerate *
+ (channel::commitment_tx_base_weight(&channel_type_features) + 2 * channel::COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000;
+ let anchor_outputs_value = if anchors { channel::ANCHOR_OUTPUT_VALUE_SATOSHI * 2 } else { 0 };
assert_eq!(sorted_vec(vec![Balance::ClaimableAwaitingConfirmations {
// to_remote output in A's revoked commitment
amount_satoshis: 100_000 - 4_000 - 3_000,
confirmation_height: to_remote_maturity,
}, Balance::CounterpartyRevokedOutputClaimable {
// to_self output in A's revoked commitment
- amount_satoshis: 1_000_000 - 100_000 - chan_feerate *
- (channel::commitment_tx_base_weight(&channel_type_features) + 2 * channel::COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000,
+ amount_satoshis: 1_000_000 - 100_000 - commitment_tx_fee - anchor_outputs_value,
}, Balance::CounterpartyRevokedOutputClaimable { // HTLC 1
amount_satoshis: 4_000,
}, Balance::CounterpartyRevokedOutputClaimable { // HTLC 2
}]),
sorted_vec(nodes[1].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances()));
- // Confirm A's HTLC-Success tranasction which presumably raced B's claim, causing B to create a
+ // Confirm A's HTLC-Success transaction which presumably raced B's claim, causing B to create a
// new claim.
- mine_transaction(&nodes[1], &as_revoked_txn[1]);
+ if anchors {
+ mine_transaction(&nodes[0], &as_revoked_txn[0]);
+ check_closed_broadcast(&nodes[0], 1, true);
+ check_added_monitors(&nodes[0], 1);
+ check_closed_event!(&nodes[0], 1, ClosureReason::CommitmentTxConfirmed, false, [nodes[1].node.get_our_node_id()], 1_000_000);
+ handle_bump_htlc_event(&nodes[0], 1);
+ }
+ let htlc_success_claim = if anchors {
+ let mut txn = nodes[0].tx_broadcaster.txn_broadcast();
+ assert_eq!(txn.len(), 1);
+ check_spends!(txn[0], as_revoked_txn[0], coinbase_tx);
+ txn.pop().unwrap()
+ } else {
+ as_revoked_txn[1].clone()
+ };
+ mine_transaction(&nodes[1], &htlc_success_claim);
expect_payment_sent(&nodes[1], claimed_payment_preimage, None, true, false);
- let mut claim_txn_2: Vec<_> = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
- claim_txn_2.sort_unstable_by_key(|tx| if tx.input.iter().any(|inp| inp.previous_output.txid == as_revoked_txn[0].txid()) { 0 } else { 1 });
+
+ let mut claim_txn_2 = nodes[1].tx_broadcaster.txn_broadcast();
// Once B sees the HTLC-Success transaction it splits its claim transaction into two, though in
// theory it could re-aggregate the claims as well.
assert_eq!(claim_txn_2.len(), 2);
- assert_eq!(claim_txn_2[0].input.len(), 2);
- check_spends!(claim_txn_2[0], as_revoked_txn[0]);
- assert_eq!(claim_txn_2[1].input.len(), 1);
- check_spends!(claim_txn_2[1], as_revoked_txn[1]);
+ if anchors {
+ assert_eq!(claim_txn_2[0].input.len(), 1);
+ assert_eq!(claim_txn_2[0].input[0].previous_output.vout, 0);
+ check_spends!(claim_txn_2[0], &htlc_success_claim);
+ assert_eq!(claim_txn_2[1].input.len(), 1);
+ assert_eq!(claim_txn_2[1].input[0].previous_output.vout, 3);
+ check_spends!(claim_txn_2[1], as_revoked_txn[0]);
+ } else {
+ assert_eq!(claim_txn_2[0].input.len(), 1);
+ assert_eq!(claim_txn_2[0].input[0].previous_output.vout, 0);
+ check_spends!(claim_txn_2[0], as_revoked_txn[1]);
+ assert_eq!(claim_txn_2[1].input.len(), 2);
+ assert_eq!(claim_txn_2[1].input[0].previous_output.vout, 3);
+ assert_eq!(claim_txn_2[1].input[1].previous_output.vout, 1);
+ check_spends!(claim_txn_2[1], as_revoked_txn[0]);
+ }
assert_eq!(sorted_vec(vec![Balance::ClaimableAwaitingConfirmations {
// to_remote output in A's revoked commitment
confirmation_height: to_remote_maturity,
}, Balance::CounterpartyRevokedOutputClaimable {
// to_self output in A's revoked commitment
- amount_satoshis: 1_000_000 - 100_000 - chan_feerate *
- (channel::commitment_tx_base_weight(&channel_type_features) + 2 * channel::COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000,
+ amount_satoshis: 1_000_000 - 100_000 - commitment_tx_fee - anchor_outputs_value,
}, Balance::CounterpartyRevokedOutputClaimable { // HTLC 1
amount_satoshis: 4_000,
}, Balance::CounterpartyRevokedOutputClaimable { // HTLC 2
assert_eq!(sorted_vec(vec![Balance::CounterpartyRevokedOutputClaimable {
// to_self output in A's revoked commitment
- amount_satoshis: 1_000_000 - 100_000 - chan_feerate *
- (channel::commitment_tx_base_weight(&channel_type_features) + 2 * channel::COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000,
+ amount_satoshis: 1_000_000 - 100_000 - commitment_tx_fee - anchor_outputs_value,
}, Balance::CounterpartyRevokedOutputClaimable { // HTLC 1
amount_satoshis: 4_000,
}, Balance::CounterpartyRevokedOutputClaimable { // HTLC 2
}]),
sorted_vec(nodes[1].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances()));
- mine_transaction(&nodes[1], &claim_txn_2[1]);
+ mine_transaction(&nodes[1], &claim_txn_2[0]);
let htlc_2_claim_maturity = nodes[1].best_block_info().1 + ANTI_REORG_DELAY - 1;
assert_eq!(sorted_vec(vec![Balance::CounterpartyRevokedOutputClaimable {
// to_self output in A's revoked commitment
- amount_satoshis: 1_000_000 - 100_000 - chan_feerate *
- (channel::commitment_tx_base_weight(&channel_type_features) + 2 * channel::COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000,
+ amount_satoshis: 1_000_000 - 100_000 - commitment_tx_fee - anchor_outputs_value,
}, Balance::CounterpartyRevokedOutputClaimable { // HTLC 1
amount_satoshis: 4_000,
}, Balance::ClaimableAwaitingConfirmations { // HTLC 2
- amount_satoshis: claim_txn_2[1].output[0].value,
+ amount_satoshis: claim_txn_2[0].output[0].value,
confirmation_height: htlc_2_claim_maturity,
}]),
sorted_vec(nodes[1].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances()));
connect_blocks(&nodes[1], 5);
- test_spendable_output(&nodes[1], &claim_txn_2[1], false);
+ test_spendable_output(&nodes[1], &claim_txn_2[0], false);
assert_eq!(sorted_vec(vec![Balance::CounterpartyRevokedOutputClaimable {
// to_self output in A's revoked commitment
- amount_satoshis: 1_000_000 - 100_000 - chan_feerate *
- (channel::commitment_tx_base_weight(&channel_type_features) + 2 * channel::COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000,
+ amount_satoshis: 1_000_000 - 100_000 - commitment_tx_fee - anchor_outputs_value,
}, Balance::CounterpartyRevokedOutputClaimable { // HTLC 1
amount_satoshis: 4_000,
}]),
sorted_vec(nodes[1].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances()));
- mine_transaction(&nodes[1], &claim_txn_2[0]);
+ if anchors {
+ mine_transactions(&nodes[1], &[&claim_txn_2[1], revoked_to_self_claim.as_ref().unwrap()]);
+ } else {
+ mine_transaction(&nodes[1], &claim_txn_2[1]);
+ }
let rest_claim_maturity = nodes[1].best_block_info().1 + ANTI_REORG_DELAY - 1;
- assert_eq!(vec![Balance::ClaimableAwaitingConfirmations {
- amount_satoshis: claim_txn_2[0].output[0].value,
- confirmation_height: rest_claim_maturity,
- }],
- nodes[1].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances());
+ if anchors {
+ assert_eq!(vec![Balance::ClaimableAwaitingConfirmations {
+ amount_satoshis: claim_txn_2[1].output[0].value,
+ confirmation_height: rest_claim_maturity,
+ }, Balance::ClaimableAwaitingConfirmations {
+ amount_satoshis: revoked_to_self_claim.as_ref().unwrap().output[0].value,
+ confirmation_height: rest_claim_maturity,
+ }],
+ nodes[1].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances());
+ } else {
+ assert_eq!(vec![Balance::ClaimableAwaitingConfirmations {
+ amount_satoshis: claim_txn_2[1].output[0].value,
+ confirmation_height: rest_claim_maturity,
+ }],
+ nodes[1].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances());
+ }
assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); // We shouldn't fail the payment until we spend the output
connect_blocks(&nodes[1], 5);
expect_payment_failed!(nodes[1], revoked_payment_hash, false);
- test_spendable_output(&nodes[1], &claim_txn_2[0], false);
+ if anchors {
+ let events = nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events();
+ assert_eq!(events.len(), 2);
+ for (i, event) in events.into_iter().enumerate() {
+ if let Event::SpendableOutputs { outputs, .. } = event {
+ assert_eq!(outputs.len(), 1);
+ let spend_tx = nodes[1].keys_manager.backing.spend_spendable_outputs(
+ &[&outputs[0]], Vec::new(), Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(),
+ 253, None, &Secp256k1::new()
+ ).unwrap();
+ check_spends!(spend_tx, if i == 0 { &claim_txn_2[1] } else { revoked_to_self_claim.as_ref().unwrap() });
+ } else { panic!(); }
+ }
+ } else {
+ test_spendable_output(&nodes[1], &claim_txn_2[1], false);
+ }
assert!(nodes[1].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances().is_empty());
// Ensure that even if we connect more blocks, potentially replaying the entire chain if we're
assert!(nodes[1].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances().is_empty());
}
+#[test]
+fn test_revoked_counterparty_aggregated_claims() {
+ do_test_revoked_counterparty_aggregated_claims(false);
+ do_test_revoked_counterparty_aggregated_claims(true);
+}
+
fn do_test_restored_packages_retry(check_old_monitor_retries_after_upgrade: bool) {
// Tests that we'll retry packages that were previously timelocked after we've restored them.
let chanmon_cfgs = create_chanmon_cfgs(2);
&LowerBoundedFeeEstimator::new(node_cfgs[0].fee_estimator), &nodes[0].logger
);
get_monitor!(nodes[1], chan_id).provide_payment_preimage(
- &payment_hash_1, &payment_preimage_1, &node_cfgs[0].tx_broadcaster,
+ &payment_hash_1, &payment_preimage_1, &node_cfgs[1].tx_broadcaster,
&LowerBoundedFeeEstimator::new(node_cfgs[1].fee_estimator), &nodes[1].logger
);
do_test_anchors_monitor_fixes_counterparty_payment_script_on_reload(false);
do_test_anchors_monitor_fixes_counterparty_payment_script_on_reload(true);
}
+
+#[cfg(not(feature = "_test_vectors"))]
+fn do_test_monitor_claims_with_random_signatures(anchors: bool, confirm_counterparty_commitment: bool) {
+ // Tests that our monitor claims will always use fresh random signatures (ensuring a unique
+ // wtxid) to prevent certain classes of transaction replacement at the bitcoin P2P layer.
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let mut user_config = test_default_channel_config();
+ if anchors {
+ user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
+ user_config.manually_accept_inbound_channels = true;
+ }
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config), Some(user_config)]);
+ let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let coinbase_tx = Transaction {
+ version: 2,
+ lock_time: PackedLockTime::ZERO,
+ input: vec![TxIn { ..Default::default() }],
+ output: vec![
+ TxOut {
+ value: Amount::ONE_BTC.to_sat(),
+ script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
+ },
+ ],
+ };
+ if anchors {
+ nodes[0].wallet_source.add_utxo(bitcoin::OutPoint { txid: coinbase_tx.txid(), vout: 0 }, coinbase_tx.output[0].value);
+ }
+
+ // Open a channel and route a payment. We'll let it timeout to claim it.
+ let (_, _, chan_id, funding_tx) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
+ route_payment(&nodes[0], &[&nodes[1]], 1_000_000);
+
+ let (closing_node, other_node) = if confirm_counterparty_commitment {
+ (&nodes[1], &nodes[0])
+ } else {
+ (&nodes[0], &nodes[1])
+ };
+
+ closing_node.node.force_close_broadcasting_latest_txn(&chan_id, &other_node.node.get_our_node_id()).unwrap();
+
+ // The commitment transaction comes first.
+ let commitment_tx = {
+ let mut txn = closing_node.tx_broadcaster.unique_txn_broadcast();
+ assert_eq!(txn.len(), 1);
+ check_spends!(txn[0], funding_tx);
+ txn.pop().unwrap()
+ };
+
+ mine_transaction(closing_node, &commitment_tx);
+ check_added_monitors!(closing_node, 1);
+ check_closed_broadcast!(closing_node, true);
+ check_closed_event!(closing_node, 1, ClosureReason::HolderForceClosed, [other_node.node.get_our_node_id()], 1_000_000);
+
+ mine_transaction(other_node, &commitment_tx);
+ check_added_monitors!(other_node, 1);
+ check_closed_broadcast!(other_node, true);
+ check_closed_event!(other_node, 1, ClosureReason::CommitmentTxConfirmed, [closing_node.node.get_our_node_id()], 1_000_000);
+
+ // If we update the best block to the new height before providing the confirmed transactions,
+ // we'll see another broadcast of the commitment transaction.
+ if anchors && !confirm_counterparty_commitment && nodes[0].connect_style.borrow().updates_best_block_first() {
+ let _ = nodes[0].tx_broadcaster.txn_broadcast();
+ }
+
+ // Then comes the HTLC timeout transaction.
+ if confirm_counterparty_commitment {
+ connect_blocks(&nodes[0], 5);
+ test_spendable_output(&nodes[0], &commitment_tx, false);
+ connect_blocks(&nodes[0], TEST_FINAL_CLTV - 5);
+ } else {
+ connect_blocks(&nodes[0], TEST_FINAL_CLTV);
+ }
+ if anchors && !confirm_counterparty_commitment {
+ handle_bump_htlc_event(&nodes[0], 1);
+ }
+ let htlc_timeout_tx = {
+ let mut txn = nodes[0].tx_broadcaster.txn_broadcast();
+ assert_eq!(txn.len(), 1);
+ let tx = if txn[0].input[0].previous_output.txid == commitment_tx.txid() {
+ txn[0].clone()
+ } else {
+ txn[1].clone()
+ };
+ check_spends!(tx, commitment_tx, coinbase_tx);
+ tx
+ };
+
+ // Check we rebroadcast it with a different wtxid.
+ nodes[0].chain_monitor.chain_monitor.rebroadcast_pending_claims();
+ if anchors && !confirm_counterparty_commitment {
+ handle_bump_htlc_event(&nodes[0], 1);
+ }
+ {
+ let mut txn = nodes[0].tx_broadcaster.txn_broadcast();
+ assert_eq!(txn.len(), 1);
+ assert_eq!(txn[0].txid(), htlc_timeout_tx.txid());
+ assert_ne!(txn[0].wtxid(), htlc_timeout_tx.wtxid());
+ }
+}
+
+#[cfg(not(feature = "_test_vectors"))]
+#[test]
+fn test_monitor_claims_with_random_signatures() {
+ do_test_monitor_claims_with_random_signatures(false, false);
+ do_test_monitor_claims_with_random_signatures(false, true);
+ do_test_monitor_claims_with_random_signatures(true, false);
+ do_test_monitor_claims_with_random_signatures(true, true);
+}