Skip to content

Commit b60133c

Browse files
author
Antoine Riard
committed
Implement bumping engine in ChannelMonitor::block_connected
Add RBF-bumping of justice txn, given they are only signed by us we can RBF at wish. Aggregation of bump-candidates and more aggresive bumping heuristics are left open Fix tests broken by introduction of more txn broadcast. Some tests may have a relaxed check (claim_htlc_ouputs_single_tx) as broadcast bumped txn are now interwining in previous broadcast ones and breaking simple expectations
1 parent 3625dfa commit b60133c

File tree

2 files changed

+139
-5
lines changed

2 files changed

+139
-5
lines changed

src/ln/channelmonitor.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2285,6 +2285,22 @@ impl ChannelMonitor {
22852285
}
22862286
}
22872287
}
2288+
let mut bump_candidates = Vec::new();
2289+
for (claimed_txid, ref mut cached_claim_datas) in self.our_claim_txn_waiting_first_conf.iter_mut() {
2290+
if cached_claim_datas.height_timer == height {
2291+
bump_candidates.push((claimed_txid.clone(), cached_claim_datas.clone()));
2292+
}
2293+
}
2294+
for &mut (claimed_txid, ref mut cached_claim_datas) in bump_candidates.iter_mut() {
2295+
if let Some((new_timer, new_feerate, bump_tx)) = self.bump_claim_tx(height, &claimed_txid, &cached_claim_datas, fee_estimator) {
2296+
cached_claim_datas.height_timer = new_timer;
2297+
cached_claim_datas.feerate_previous = new_feerate;
2298+
broadcaster.broadcast_transaction(&bump_tx);
2299+
}
2300+
}
2301+
for (claimed_txid, cached_claim_datas) in bump_candidates.drain(..) {
2302+
self.our_claim_txn_waiting_first_conf.insert(claimed_txid, cached_claim_datas);
2303+
}
22882304
self.last_block_hash = block_hash.clone();
22892305
(watch_outputs, spendable_outputs, htlc_updated)
22902306
}
@@ -2498,6 +2514,123 @@ impl ChannelMonitor {
24982514
}
24992515
htlc_updated
25002516
}
2517+
2518+
/// Lightning security model (i.e being able to redeem/timeout HTLC or penalize coutnerparty onchain) lays on the assumption of claim transactions getting confirmed before timelock expiration
2519+
/// (CSV or CLTV following cases). In case of high-fee spikes, claim tx may stuck in the mempool, so you need to bump its feerate quickly using Replace-By-Fee or Child-Pay-For-Parent.
2520+
fn bump_claim_tx(&self, height: u32, txid: &Sha256dHash, cached_claim_datas: &ClaimTxBumpMaterial, fee_estimator: &FeeEstimator) -> Option<(u32, u64, Transaction)> {
2521+
let mut inputs = Vec::new();
2522+
for vout in cached_claim_datas.per_input_material.keys() {
2523+
inputs.push(TxIn {
2524+
previous_output: BitcoinOutPoint {
2525+
txid: txid.clone(),
2526+
vout: *vout,
2527+
},
2528+
script_sig: Script::new(),
2529+
sequence: 0xfffffffd,
2530+
witness: Vec::new(),
2531+
});
2532+
}
2533+
let mut bumped_tx = Transaction {
2534+
version: 2,
2535+
lock_time: 0,
2536+
input: inputs,
2537+
output: vec![TxOut {
2538+
script_pubkey: self.destination_script.clone(),
2539+
value: 0
2540+
}],
2541+
};
2542+
2543+
macro_rules! RBF_bump {
2544+
($amount: expr, $old_feerate: expr, $fee_estimator: expr, $predicted_weight: expr, $txid: expr) => {
2545+
{
2546+
let mut used_feerate;
2547+
// If old feerate inferior to actual one given back by Fee Estimator, use it to compute new fee...
2548+
let new_fee = if $old_feerate < $fee_estimator.get_est_sat_per_1000_weight(ConfirmationTarget::HighPriority) {
2549+
let mut value = $amount;
2550+
if subtract_high_prio_fee!(self, $fee_estimator, value, $predicted_weight, txid, used_feerate) {
2551+
$amount - value
2552+
} else {
2553+
log_trace!(self, "Can't new-estimation bump claiming on {}, amount {} is too small", $txid, $amount);
2554+
return None;
2555+
}
2556+
// ...else just increase the previous feerate by 25% (because that's a nice number)
2557+
} else {
2558+
let fee = $old_feerate * $predicted_weight / 750;
2559+
if $amount <= fee {
2560+
log_trace!(self, "Can't 25% bump claiming on {}, amount {} is too small", $txid, $amount);
2561+
return None;
2562+
}
2563+
fee
2564+
};
2565+
2566+
let previous_fee = $old_feerate * $predicted_weight / 1000;
2567+
let min_relay_fee = $fee_estimator.get_min_relay_sat_per_1000_weight() * $predicted_weight / 1000;
2568+
// BIP 125 Opt-in Full Replace-by-Fee Signaling
2569+
// * 3. The replacement transaction pays an absolute fee of at least the sum paid by the original transactions.
2570+
// * 4. The replacement transaction must also pay for its own bandwidth at or above the rate set by the node's minimum relay fee setting.
2571+
let new_fee = if new_fee < previous_fee + min_relay_fee {
2572+
new_fee + previous_fee + min_relay_fee - new_fee
2573+
} else {
2574+
new_fee
2575+
};
2576+
Some((new_fee, new_fee * 1000 / $predicted_weight))
2577+
}
2578+
}
2579+
}
2580+
2581+
let new_timer = Self::get_height_timer(height, cached_claim_datas.soonest_timelock);
2582+
let mut inputs_witnesses_weight = 0;
2583+
let mut amt = 0;
2584+
for per_outp_material in cached_claim_datas.per_input_material.values() {
2585+
match per_outp_material {
2586+
&InputMaterial::Revoked { ref script, ref is_htlc, ref amount, .. } => {
2587+
inputs_witnesses_weight += Self::get_witnesses_weight(if !is_htlc { &[InputDescriptors::RevokedOutput] } else if script.len() == OFFERED_HTLC_SCRIPT_WEIGHT { &[InputDescriptors::RevokedOfferedHTLC] } else if script.len() == ACCEPTED_HTLC_SCRIPT_WEIGHT { &[InputDescriptors::RevokedReceivedHTLC] } else { &[] });
2588+
amt += *amount;
2589+
},
2590+
&InputMaterial::RemoteHTLC { .. } => { return None; },
2591+
&InputMaterial::LocalHTLC { .. } => { return None; }
2592+
}
2593+
}
2594+
assert!(amt != 0);
2595+
2596+
let predicted_weight = bumped_tx.get_weight() + inputs_witnesses_weight;
2597+
let new_feerate;
2598+
if let Some((new_fee, feerate)) = RBF_bump!(amt, cached_claim_datas.feerate_previous, fee_estimator, predicted_weight as u64, txid) {
2599+
bumped_tx.output[0].value = amt - new_fee;
2600+
new_feerate = feerate;
2601+
} else {
2602+
return None;
2603+
}
2604+
assert!(new_feerate != 0);
2605+
2606+
for (i, (vout, per_outp_material)) in cached_claim_datas.per_input_material.iter().enumerate() {
2607+
match per_outp_material {
2608+
&InputMaterial::Revoked { ref script, ref pubkey, ref key, ref is_htlc, ref amount } => {
2609+
let sighash_parts = bip143::SighashComponents::new(&bumped_tx);
2610+
let sighash = hash_to_message!(&sighash_parts.sighash_all(&bumped_tx.input[i], &script, *amount)[..]);
2611+
let sig = self.secp_ctx.sign(&sighash, &key);
2612+
bumped_tx.input[i].witness.push(sig.serialize_der().to_vec());
2613+
bumped_tx.input[i].witness[0].push(SigHashType::All as u8);
2614+
if *is_htlc {
2615+
bumped_tx.input[i].witness.push(pubkey.unwrap().clone().serialize().to_vec());
2616+
} else {
2617+
bumped_tx.input[i].witness.push(vec!(1));
2618+
}
2619+
bumped_tx.input[i].witness.push(script.clone().into_bytes());
2620+
log_trace!(self, "Going to broadcast bumped Penalty Transaction {} claiming revoked {} output {} from {} with new feerate {}", bumped_tx.txid(), if !is_htlc { "to_local" } else if script.len() == OFFERED_HTLC_SCRIPT_WEIGHT { "offered" } else if script.len() == ACCEPTED_HTLC_SCRIPT_WEIGHT { "received" } else { "" }, vout, txid, new_feerate);
2621+
},
2622+
&InputMaterial::RemoteHTLC { .. } => {},
2623+
&InputMaterial::LocalHTLC { .. } => {
2624+
//TODO : Given that Local Commitment Transaction and HTLC-Timeout/HTLC-Success are counter-signed by peer, we can't
2625+
// RBF them. Need a Lightning specs change and package relay modification :
2626+
// https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-November/016518.html
2627+
return None;
2628+
}
2629+
}
2630+
}
2631+
assert!(predicted_weight >= bumped_tx.get_weight());
2632+
Some((new_timer, new_feerate, bumped_tx))
2633+
}
25012634
}
25022635

25032636
const MAX_ALLOC_SIZE: usize = 64*1024;

src/ln/functional_tests.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1818,6 +1818,7 @@ fn test_justice_tx() {
18181818

18191819
check_spends!(node_txn[0], revoked_local_txn[0].clone());
18201820
node_txn.swap_remove(0);
1821+
node_txn.truncate(1);
18211822
}
18221823
test_txn_broadcast(&nodes[1], &chan_5, None, HTLCType::NONE);
18231824

@@ -1835,6 +1836,10 @@ fn test_justice_tx() {
18351836
// We test justice_tx build by A on B's revoked HTLC-Success tx
18361837
// Create some new channels:
18371838
let chan_6 = create_announced_chan_between_nodes(&nodes, 0, 1, LocalFeatures::new(), LocalFeatures::new());
1839+
{
1840+
let mut node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
1841+
node_txn.clear();
1842+
}
18381843

18391844
// A pending HTLC which will be revoked:
18401845
let payment_preimage_4 = route_payment(&nodes[0], &vec!(&nodes[1])[..], 3000000).0;
@@ -2012,7 +2017,7 @@ fn claim_htlc_outputs_single_tx() {
20122017
}
20132018

20142019
let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
2015-
assert_eq!(node_txn.len(), 22); // ChannelManager : 2, ChannelMontitor: 8 (1 standard revoked output, 2 revocation htlc tx, 1 local commitment tx + 1 htlc timeout tx) * 2 (block-rescan) + 5 * (1 local commitment tx + 1 htlc timeout tx)
2020+
assert_eq!(node_txn.len(), 27); // ChannelManager : 2, ChannelMontitor: 8 (1 standard revoked output, 2 revocation htlc tx, 1 local commitment tx + 1 htlc timeout tx) * 2 (block-rescan) + 5 * (1 local commitment tx + 1 htlc timeout tx)
20162021

20172022
assert_eq!(node_txn[0], node_txn[7]);
20182023
assert_eq!(node_txn[1], node_txn[8]);
@@ -2022,10 +2027,6 @@ fn claim_htlc_outputs_single_tx() {
20222027
assert_eq!(node_txn[3], node_txn[5]); //local commitment tx + htlc timeout tx broadcasted by ChannelManger
20232028
assert_eq!(node_txn[4], node_txn[6]);
20242029

2025-
for i in 12..22 {
2026-
if i % 2 == 0 { assert_eq!(node_txn[3], node_txn[i]); } else { assert_eq!(node_txn[4], node_txn[i]); }
2027-
}
2028-
20292030
assert_eq!(node_txn[0].input.len(), 1);
20302031
assert_eq!(node_txn[1].input.len(), 1);
20312032
assert_eq!(node_txn[2].input.len(), 1);

0 commit comments

Comments
 (0)