Skip to content

Commit 3f8df98

Browse files
author
Antoine Riard
committed
Add test_bump_penalty_txn_on_revoked_txn
1 parent 4691298 commit 3f8df98

File tree

2 files changed

+254
-3
lines changed

2 files changed

+254
-3
lines changed

src/ln/functional_test_utils.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,14 @@ pub fn confirm_transaction(chain: &chaininterface::ChainWatchInterfaceUtil, tx:
4747
}
4848
}
4949

50-
pub fn connect_blocks(chain: &chaininterface::ChainWatchInterfaceUtil, depth: u32, height: u32, parent: bool, prev_blockhash: Sha256d) {
50+
pub fn connect_blocks(chain: &chaininterface::ChainWatchInterfaceUtil, depth: u32, height: u32, parent: bool, prev_blockhash: Sha256d) -> BlockHeader {
5151
let mut header = BlockHeader { version: 0x2000000, prev_blockhash: if parent { prev_blockhash } else { Default::default() }, merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 };
5252
chain.block_connected_checked(&header, height + 1, &Vec::new(), &Vec::new());
5353
for i in 2..depth + 1 {
5454
header = BlockHeader { version: 0x20000000, prev_blockhash: header.bitcoin_hash(), merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 };
5555
chain.block_connected_checked(&header, height + i, &Vec::new(), &Vec::new());
5656
}
57+
return header;
5758
}
5859

5960
pub fn disconnect_blocks(chain: &chaininterface::ChainWatchInterfaceUtil, depth: u32, height: u32, parent: bool, prev_blockhash: Sha256d) {

src/ln/functional_tests.rs

Lines changed: 252 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! claim outputs on-chain.
44
55
use chain::transaction::OutPoint;
6-
use chain::chaininterface::{ChainListener, ChainWatchInterface};
6+
use chain::chaininterface::{ChainListener, ChainWatchInterface, ChainWatchInterfaceUtil};
77
use chain::keysinterface::{KeysInterface, SpendableOutputDescriptor};
88
use chain::keysinterface;
99
use ln::channel::{COMMITMENT_TX_BASE_WEIGHT, COMMITMENT_TX_WEIGHT_PER_HTLC, BREAKDOWN_TIMEOUT};
@@ -19,6 +19,7 @@ use util::events::{Event, EventsProvider, MessageSendEvent, MessageSendEventsPro
1919
use util::errors::APIError;
2020
use util::ser::{Writeable, ReadableArgs};
2121
use util::config::UserConfig;
22+
use util::logger::Logger;
2223
use util::rng;
2324

2425
use bitcoin::util::hash::BitcoinHash;
@@ -41,7 +42,7 @@ use secp256k1::key::{PublicKey,SecretKey};
4142

4243
use std::collections::{BTreeSet, HashMap, HashSet};
4344
use std::default::Default;
44-
use std::sync::Arc;
45+
use std::sync::{Arc, Mutex};
4546
use std::sync::atomic::Ordering;
4647
use std::time::Instant;
4748
use std::mem;
@@ -5709,3 +5710,252 @@ fn test_sweep_outbound_htlc_failure_update() {
57095710
do_test_sweep_outbound_htlc_failure_update(false, false);
57105711
do_test_sweep_outbound_htlc_failure_update(true, false);
57115712
}
5713+
5714+
5715+
// aggregation
5716+
// multiple bumping rounds
5717+
// 2 different bumping heuristic
5718+
// min relay fee isn't enough
5719+
5720+
#[test]
5721+
fn test_bump_penalty_txn_on_revoked_commitment() {
5722+
// In case of penalty txn with too low feerates for getting into mempools, RBF-bump them to be sure
5723+
// we're able to claim outputs on revoked commitment transaction before timelocks expiration
5724+
5725+
let mut nodes = create_network(2);
5726+
5727+
let chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1000000, 59000000);
5728+
let payment_preimage = route_payment(&nodes[0], &vec!(&nodes[1])[..], 3000000).0;
5729+
route_payment(&nodes[1], &vec!(&nodes[0])[..], 3000000).0;
5730+
let revoked_local_txn = nodes[0].node.channel_state.lock().unwrap().by_id.get(&chan.2).unwrap().last_local_commitment_txn.clone();
5731+
assert_eq!(revoked_local_txn[0].output.len(), 4);
5732+
assert_eq!(revoked_local_txn[0].input.len(), 1);
5733+
assert_eq!(revoked_local_txn[0].input[0].previous_output.txid, chan.3.txid());
5734+
let revoked_txid = revoked_local_txn[0].txid();
5735+
5736+
let mut penalty_amount = 0;
5737+
for outp in revoked_local_txn[0].output.iter() {
5738+
if outp.script_pubkey.is_v0_p2wsh() {
5739+
penalty_amount += outp.value;
5740+
}
5741+
}
5742+
5743+
claim_payment(&nodes[0], &vec!(&nodes[1])[..], payment_preimage);
5744+
let header = BlockHeader { version: 0x20000000, prev_blockhash: Default::default(), merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 };
5745+
nodes[1].chain_monitor.block_connected_with_filtering(&Block { header, txdata: vec![revoked_local_txn[0].clone()] }, 1);
5746+
5747+
let previous_penalty_txid;
5748+
let feerate_1;
5749+
{
5750+
let mut node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
5751+
assert_eq!(node_txn.len(), 2);
5752+
assert_eq!(node_txn[0], node_txn[1]);
5753+
assert_eq!(node_txn[0].input.len(), 3); // Penalty txn claims to_local, offered_htlc and received_htlc outputs
5754+
assert_eq!(node_txn[0].output.len(), 1);
5755+
check_spends!(node_txn[0], revoked_local_txn[0].clone());
5756+
let fee_1 = penalty_amount - node_txn[0].output[0].value;
5757+
feerate_1 = fee_1 * 1000 / node_txn[0].get_weight();
5758+
previous_penalty_txid = node_txn[0].txid();
5759+
node_txn.clear();
5760+
};
5761+
5762+
let header = connect_blocks(&nodes[1].chain_monitor, 3, 1, true, header.bitcoin_hash());
5763+
let mut penalties_2 = HashMap::new();
5764+
let mut feerate_2 = 0;
5765+
{
5766+
let mut node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
5767+
assert_eq!(node_txn.len(), 3);
5768+
for tx in node_txn.drain(..) {
5769+
if tx.input[0].previous_output.txid == revoked_txid {
5770+
let txid = tx.txid();
5771+
// Verify new bumped tx is different from last claiming transaction, we don't want spurrious rebroadcast
5772+
assert_ne!(previous_penalty_txid, txid);
5773+
check_spends!(tx, revoked_local_txn[0].clone());
5774+
let fee_2 = revoked_local_txn[0].output[tx.input[0].previous_output.vout as usize].value - tx.output[0].value;
5775+
feerate_2 = fee_2 * 1000 / tx.get_weight();
5776+
// Verify 25% bump heuristic
5777+
assert!(feerate_2 * 100 >= feerate_1 * 125);
5778+
penalties_2.insert(tx.input[0].previous_output, (txid, fee_2));
5779+
}
5780+
}
5781+
}
5782+
assert_ne!(feerate_2, 0);
5783+
5784+
let header = connect_blocks(&nodes[1].chain_monitor, 3, 4, true, header.bitcoin_hash());
5785+
let mut penalties_3 = HashMap::new();
5786+
let mut feerate_3 = 0;
5787+
{
5788+
let mut node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
5789+
assert_eq!(node_txn.len(), 5);
5790+
for tx in node_txn.drain(..) {
5791+
if let Some(previous_data) = penalties_2.get(&tx.input[0].previous_output) {
5792+
let txid = tx.txid();
5793+
// Verify new bumped tx is different from any last claiming treansaction, we don't want spurrious rebroadcast
5794+
assert_ne!(previous_data.0, txid);
5795+
check_spends!(tx, revoked_local_txn[0].clone());
5796+
let fee_3 = revoked_local_txn[0].output[tx.input[0].previous_output.vout as usize].value - tx.output[0].value;
5797+
feerate_3 = fee_3 * 1000 / tx.get_weight();
5798+
// Verify 25% bump heuristic;
5799+
assert!(feerate_3 * 100 >= feerate_2 * 125);
5800+
penalties_3.insert(tx.input[0].previous_output, (txid, fee_3));
5801+
}
5802+
}
5803+
}
5804+
assert_ne!(feerate_3, 0);
5805+
5806+
macro_rules! update_fee_estimator {
5807+
($node: expr, $sat_per_kw: expr, $min_relay_sat_per_kw: expr) => {
5808+
let funding_outpoint = OutPoint { txid: chan.3.txid(), index: 0 };
5809+
let current_monitor = nodes[1].chan_monitor.simple_monitor.monitors.lock().unwrap().remove(&funding_outpoint).unwrap();
5810+
let logger: Arc<Logger> = Arc::new(test_utils::TestLogger::with_id(format!("node {}", 1)));
5811+
let chain_monitor = Arc::new(ChainWatchInterfaceUtil::new(Network::Testnet, Arc::clone(&logger)));
5812+
let new_fee_estimator = Arc::new(test_utils::TestFeeEstimator { sat_per_kw: $sat_per_kw, min_relay_sat_per_kw: $min_relay_sat_per_kw});
5813+
let tx_broadcaster = Arc::new(test_utils::TestBroadcaster{txn_broadcasted: Mutex::new(Vec::new())});
5814+
let chan_monitor = Arc::new(test_utils::TestChannelMonitor::new(chain_monitor.clone(), tx_broadcaster.clone(), logger.clone(), new_fee_estimator.clone()));
5815+
assert!(chan_monitor.add_update_monitor(funding_outpoint, current_monitor).is_ok());
5816+
$node.chain_monitor = chain_monitor;
5817+
$node.chan_monitor = chan_monitor;
5818+
$node.tx_broadcaster = tx_broadcaster;
5819+
}
5820+
}
5821+
5822+
update_fee_estimator!(nodes[1], 500, 0);
5823+
let header = connect_blocks(&nodes[1].chain_monitor, 3, 7, true, header.bitcoin_hash());
5824+
let mut penalties_4 = HashMap::new();
5825+
let mut feerate_4 = 0;
5826+
{
5827+
let mut node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
5828+
assert_eq!(node_txn.len(), 3);
5829+
for tx in node_txn.drain(..) {
5830+
if let Some(previous_data) = penalties_3.get(&tx.input[0].previous_output) {
5831+
let txid = tx.txid();
5832+
// Verify new bumped tx is different from any last claiming treansaction, we don't want spurrious rebroadcast
5833+
assert_ne!(previous_data.0, txid);
5834+
check_spends!(tx, revoked_local_txn[0].clone());
5835+
let fee_4 = revoked_local_txn[0].output[tx.input[0].previous_output.vout as usize].value - tx.output[0].value;
5836+
feerate_4 = fee_4 * 1000 / tx.get_weight();
5837+
// Verify HighPriority spike bump heuristic;
5838+
assert!(feerate_4 > feerate_3);
5839+
penalties_4.insert(tx.input[0].previous_output, (txid, fee_4));
5840+
}
5841+
}
5842+
}
5843+
assert_ne!(feerate_4, 0);
5844+
update_fee_estimator!(nodes[1], 500, 100);
5845+
check_added_monitors!(nodes[1], 1);
5846+
5847+
connect_blocks(&nodes[1].chain_monitor, 3, 10, true, header.bitcoin_hash());
5848+
let mut feerate_5 = 0;
5849+
{
5850+
let mut node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
5851+
assert_eq!(node_txn.len(), 3);
5852+
for tx in node_txn.drain(..) {
5853+
if let Some(previous_data) = penalties_4.get(&tx.input[0].previous_output) {
5854+
let txid = tx.txid();
5855+
// Verify new bumped tx is different from any last claiming treansaction, we don't want spurrious rebroadcast
5856+
assert_ne!(previous_data.0, txid);
5857+
check_spends!(tx, revoked_local_txn[0].clone());
5858+
let fee_5 = revoked_local_txn[0].output[tx.input[0].previous_output.vout as usize].value - tx.output[0].value;
5859+
feerate_5 = fee_5 * 1000 / tx.get_weight();
5860+
// Verify min_relay_fee threshold force bump
5861+
assert!(feerate_5 * 100 >= feerate_4 * 119);
5862+
}
5863+
}
5864+
}
5865+
assert_ne!(feerate_5, 0);
5866+
nodes[0].node.get_and_clear_pending_msg_events();
5867+
nodes[1].node.get_and_clear_pending_msg_events();
5868+
nodes[0].node.get_and_clear_pending_events();
5869+
nodes[1].node.get_and_clear_pending_events();
5870+
}
5871+
5872+
#[test]
5873+
fn test_bump_penalty_txn_on_revoked_htlc() {
5874+
// In case of penalty txn with too low feerates for getting into mempools, RBF-bump them to be sure
5875+
// we're able to claim outputs on revoked HTLC-Success/Timeout txn before timelock expiration
5876+
5877+
let nodes = create_network(2);
5878+
5879+
let chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1000000, 59000000);
5880+
let payment_preimage = route_payment(&nodes[0], &vec!(&nodes[1])[..], 3000000).0;
5881+
route_payment(&nodes[1], &vec!(&nodes[0])[..], 3000000).0;
5882+
let revoked_local_txn;
5883+
{
5884+
revoked_local_txn = nodes[1].node.channel_state.lock().unwrap().by_id.get(&chan.2).unwrap().last_local_commitment_txn.clone();
5885+
assert_eq!(revoked_local_txn[0].output.len(), 4);
5886+
assert_eq!(revoked_local_txn[0].input.len(), 1);
5887+
assert_eq!(revoked_local_txn[0].input[0].previous_output.txid, chan.3.txid());
5888+
}
5889+
5890+
claim_payment(&nodes[0], &vec!(&nodes[1])[..], payment_preimage);
5891+
let header = BlockHeader { version: 0x20000000, prev_blockhash: Default::default(), merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 };
5892+
nodes[1].chain_monitor.block_connected_with_filtering(&Block { header, txdata: vec![revoked_local_txn[0].clone()] }, 1);
5893+
let mut htlc_timeout = Transaction { version: 0, lock_time: 0, input: Vec::new(), output: Vec::new() };
5894+
let mut htlc_success = Transaction { version: 0, lock_time: 0, input: Vec::new(), output: Vec::new() };
5895+
{
5896+
let mut revoked_htlc_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
5897+
assert_eq!(revoked_htlc_txn.len(), 4);
5898+
if revoked_htlc_txn[0].input[0].witness.last().unwrap().len() == ACCEPTED_HTLC_SCRIPT_WEIGHT {
5899+
assert_eq!(revoked_htlc_txn[1].input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
5900+
htlc_success = revoked_htlc_txn[0].clone();
5901+
htlc_timeout = revoked_htlc_txn[1].clone();
5902+
} else if revoked_htlc_txn[0].input[0].witness.last().unwrap().len() == OFFERED_HTLC_SCRIPT_WEIGHT {
5903+
assert_eq!(revoked_htlc_txn[1].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
5904+
htlc_timeout = revoked_htlc_txn[0].clone();
5905+
htlc_success = revoked_htlc_txn[1].clone();
5906+
}
5907+
assert_eq!(revoked_htlc_txn[0], revoked_htlc_txn[2]);
5908+
assert_eq!(revoked_htlc_txn[1], revoked_htlc_txn[3]);
5909+
revoked_htlc_txn.clear();
5910+
}
5911+
nodes[0].chain_monitor.block_connected_with_filtering(&Block { header, txdata: vec![revoked_local_txn[0].clone()]}, 1);
5912+
{
5913+
let mut node_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap();
5914+
node_txn.clear();
5915+
}
5916+
let header = BlockHeader { version: 0x20000000, prev_blockhash: header.bitcoin_hash(), merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 };
5917+
nodes[0].chain_monitor.block_connected_with_filtering(&Block { header, txdata: vec![htlc_success.clone(), htlc_timeout.clone()] }, 2);
5918+
let mut penalties = HashMap::new();
5919+
{
5920+
let mut penalty_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap();
5921+
assert_eq!(penalty_txn.len(), 2);
5922+
if penalty_txn[0].input[0].previous_output.txid == htlc_timeout.txid() {
5923+
check_spends!(penalty_txn[0], htlc_timeout.clone());
5924+
penalties.insert(penalty_txn[0].input[0].previous_output, (penalty_txn[0].txid(), htlc_timeout.output[0].value - penalty_txn[0].output[0].value));
5925+
check_spends!(penalty_txn[1], htlc_success.clone());
5926+
penalties.insert(penalty_txn[1].input[0].previous_output, (penalty_txn[1].txid(), htlc_success.output[0].value - penalty_txn[1].output[0].value));
5927+
} else {
5928+
check_spends!(penalty_txn[1], htlc_timeout.clone());
5929+
penalties.insert(penalty_txn[1].input[0].previous_output, (penalty_txn[0].txid(), htlc_timeout.output[0].value - penalty_txn[1].output[0].value));
5930+
check_spends!(penalty_txn[0], htlc_success.clone());
5931+
penalties.insert(penalty_txn[0].input[0].previous_output, (penalty_txn[1].txid(), htlc_success.output[0].value - penalty_txn[0].output[0].value));
5932+
}
5933+
penalty_txn.clear();
5934+
}
5935+
let header = BlockHeader { version: 0x20000000, prev_blockhash: header.bitcoin_hash(), merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 };
5936+
let header = connect_blocks(&nodes[0].chain_monitor, 2, 2, true, header.bitcoin_hash());
5937+
{
5938+
let mut node_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap();
5939+
node_txn.clear();
5940+
}
5941+
connect_blocks(&nodes[0].chain_monitor, 1, 4, true, header.bitcoin_hash());
5942+
{
5943+
let penalty_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap();
5944+
for tx in penalty_txn.iter() {
5945+
if let Some(previous_data) = penalties.get(&tx.input[0].previous_output) {
5946+
let txid = tx.txid();
5947+
assert_ne!(previous_data.0, txid);
5948+
// Verify that fee has been increased
5949+
if tx.input[0].previous_output.txid == htlc_timeout.txid() {
5950+
check_spends!(tx, htlc_timeout.clone());
5951+
let fee = htlc_timeout.output[0].value - tx.output[0].value;
5952+
assert!(fee > previous_data.1);
5953+
} else {
5954+
check_spends!(tx, htlc_success.clone());
5955+
let fee = htlc_timeout.output[0].value - tx.output[0].value;
5956+
assert!(fee > previous_data.1);
5957+
}
5958+
}
5959+
}
5960+
}
5961+
}

0 commit comments

Comments
 (0)