Skip to content

Commit d7ea454

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 abacd48 commit d7ea454

File tree

2 files changed

+123
-13
lines changed

2 files changed

+123
-13
lines changed

src/ln/channelmonitor.rs

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2188,13 +2188,6 @@ impl ChannelMonitor {
21882188
}
21892189
}
21902190
}
2191-
for claim in pending_claims {
2192-
log_trace!(self, "Outpoint {}:{} is under claiming process, if it doesn't succeed, a bumped claiming txn is going to be broadcast at height {}", (claim.0).vout, (claim.0).txid, (claim.1).0);
2193-
match self.our_claim_txn_waiting_first_conf.entry(claim.0) {
2194-
hash_map::Entry::Occupied(_) => {},
2195-
hash_map::Entry::Vacant(entry) => { entry.insert(claim.1); }
2196-
}
2197-
}
21982191
if let Some(events) = self.onchain_events_waiting_threshold_conf.remove(&height) {
21992192
for ev in events {
22002193
match ev {
@@ -2208,7 +2201,29 @@ impl ChannelMonitor {
22082201
}
22092202
}
22102203
}
2211-
//TODO: iter on buffered TxMaterial in our_claim_txn_waiting_first_conf, if block timer is expired generate a bumped claim tx (RBF or CPFP accordingly)
2204+
let mut bumped_txn = Vec::new();
2205+
let mut pending_claims = Vec::new();
2206+
{
2207+
let mut bump_candidates = Vec::new();
2208+
for (claimed_outpoint, claim_tx_data) in self.our_claim_txn_waiting_first_conf.iter() {
2209+
if claim_tx_data.0 == height {
2210+
bump_candidates.push((claimed_outpoint, claim_tx_data));
2211+
}
2212+
}
2213+
for candidate in bump_candidates {
2214+
if let Some((new_timer, bumped_tx, feerate)) = self.bump_claim_tx(candidate.0, (candidate.1).0, &(candidate.1).1, (candidate.1).2, fee_estimator) {
2215+
pending_claims.push((*candidate.0, (new_timer, (candidate.1).1.clone() , feerate, (candidate.1).3, (candidate.1).4)));
2216+
bumped_txn.append(&mut vec![bumped_tx]);
2217+
}
2218+
}
2219+
}
2220+
for tx in pending_claims {
2221+
log_trace!(self, "Outpoint {}:{} is under claiming process, if it doesn't succeed, a bumped claiming txn is going to be broadcast at height {}", (tx.0).vout, (tx.0).txid, (tx.1).0);
2222+
self.our_claim_txn_waiting_first_conf.insert(tx.0, tx.1);
2223+
}
2224+
for tx in bumped_txn {
2225+
broadcaster.broadcast_transaction(&tx)
2226+
}
22122227
self.last_block_hash = block_hash.clone();
22132228
(watch_outputs, spendable_outputs, htlc_updated)
22142229
}
@@ -2422,6 +2437,100 @@ impl ChannelMonitor {
24222437
}
24232438
htlc_updated
24242439
}
2440+
2441+
/// 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
2442+
/// (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.
2443+
// TODO: we may use smarter heuristics to aggregate-at-bumping if we add timelock in cached materials
2444+
fn bump_claim_tx(&self, claimed_outpoint: &BitcoinOutPoint, old_timer: u32, tx_material: &TxMaterial, old_feerate: u64, fee_estimator: &FeeEstimator) -> Option<(u32, Transaction, u64)> {
2445+
let mut bumped_tx = Transaction {
2446+
version: 2,
2447+
lock_time: 0,
2448+
input: vec![TxIn {
2449+
previous_output: claimed_outpoint.clone(),
2450+
script_sig: Script::new(),
2451+
sequence: 0xfffffffd,
2452+
witness: Vec::new(),
2453+
}],
2454+
output: vec![TxOut {
2455+
script_pubkey: self.destination_script.clone(),
2456+
value: 0
2457+
}],
2458+
};
2459+
2460+
macro_rules! RBF_bump {
2461+
($amount: expr, $old_feerate: expr, $fee_estimator: expr, $predicted_weight: expr, $outpoint: expr, $output: expr) => {
2462+
{
2463+
let mut used_feerate;
2464+
// If old feerate inferior to actual one given back by Fee Estimator, use it to compute new fee...
2465+
let new_fee = if $old_feerate < $fee_estimator.get_est_sat_per_1000_weight(ConfirmationTarget::HighPriority) {
2466+
let mut value = $amount;
2467+
if subtract_high_prio_fee!(self, $fee_estimator, value, $predicted_weight, $outpoint.txid, used_feerate) {
2468+
$amount - value
2469+
} else {
2470+
log_trace!(self, "Can't new-estimation bump claiming on {} output {} from {}, amount {} is too small", $output, $outpoint.vout, $outpoint.txid, $amount);
2471+
return None;
2472+
}
2473+
// ...else just increase the previous feerate by 25% (because that's a nice number)
2474+
} else {
2475+
let fee = $old_feerate * $predicted_weight / 750;
2476+
if $amount <= fee {
2477+
log_trace!(self, "Can't 25% bump claiming on {} output {} from {}, amount {} is too small", $output, $outpoint.vout, $outpoint.txid, $amount);
2478+
return None;
2479+
}
2480+
fee
2481+
};
2482+
2483+
let previous_fee = $old_feerate * $predicted_weight / 1000;
2484+
let min_relay_fee = $fee_estimator.get_min_relay_sat_per_1000_weight() * $predicted_weight / 1000;
2485+
// BIP 125 Opt-in Full Replace-by-Fee Signaling
2486+
// * 3. The replacement transaction pays an absolute fee of at least the sum paid by the original transactions.
2487+
// * 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.
2488+
let new_fee = if new_fee < previous_fee + min_relay_fee {
2489+
new_fee + previous_fee + min_relay_fee - new_fee
2490+
} else {
2491+
new_fee
2492+
};
2493+
Some((new_fee, new_fee * 1000 / $predicted_weight))
2494+
}
2495+
}
2496+
}
2497+
2498+
// We set the new timer to 3 blocks, which is nearly HighConfirmation target
2499+
let new_timer = old_timer + 3; //TODO: we may even be more aggressive there by decreasing timer delay at each bumping
2500+
2501+
match tx_material {
2502+
&TxMaterial::Revoked { ref script, ref pubkey, ref key, ref is_htlc, ref amount } => {
2503+
let predicted_weight = bumped_tx.get_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 { &[] });
2504+
if let Some((new_fee, new_feerate)) = RBF_bump!(*amount, old_feerate, fee_estimator, predicted_weight, claimed_outpoint, "revoked") {
2505+
bumped_tx.output[0].value = amount - new_fee;
2506+
let sighash_parts = bip143::SighashComponents::new(&bumped_tx);
2507+
let sighash = hash_to_message!(&sighash_parts.sighash_all(&bumped_tx.input[0], &script, *amount)[..]);
2508+
let sig = self.secp_ctx.sign(&sighash, &key);
2509+
bumped_tx.input[0].witness.push(sig.serialize_der().to_vec());
2510+
bumped_tx.input[0].witness[0].push(SigHashType::All as u8);
2511+
if *is_htlc {
2512+
bumped_tx.input[0].witness.push(pubkey.unwrap().clone().serialize().to_vec());
2513+
} else {
2514+
bumped_tx.input[0].witness.push(vec!(1));
2515+
}
2516+
bumped_tx.input[0].witness.push(script.clone().into_bytes());
2517+
assert!(predicted_weight >= bumped_tx.get_weight());
2518+
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 { "" }, claimed_outpoint.vout, claimed_outpoint.txid, new_feerate);
2519+
return Some((new_timer, bumped_tx, new_feerate));
2520+
}
2521+
},
2522+
&TxMaterial::RemoteHTLC { .. } => {
2523+
return None;
2524+
},
2525+
&TxMaterial::LocalHTLC { .. } => {
2526+
//TODO : Given that Local Commitment Transaction and HTLC-Timeout/HTLC-Success are counter-signed by peer, we can't
2527+
// RBF them. Need a Lightning specs change and package relay modification :
2528+
// https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-November/016518.html
2529+
return None;
2530+
},
2531+
}
2532+
None
2533+
}
24252534
}
24262535

24272536
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
@@ -1756,6 +1756,7 @@ fn test_justice_tx() {
17561756

17571757
check_spends!(node_txn[0], revoked_local_txn[0].clone());
17581758
node_txn.swap_remove(0);
1759+
node_txn.truncate(1);
17591760
}
17601761
test_txn_broadcast(&nodes[1], &chan_5, None, HTLCType::NONE);
17611762

@@ -1773,6 +1774,10 @@ fn test_justice_tx() {
17731774
// We test justice_tx build by A on B's revoked HTLC-Success tx
17741775
// Create some new channels:
17751776
let chan_6 = create_announced_chan_between_nodes(&nodes, 0, 1);
1777+
{
1778+
let mut node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
1779+
node_txn.clear();
1780+
}
17761781

17771782
// A pending HTLC which will be revoked:
17781783
let payment_preimage_4 = route_payment(&nodes[0], &vec!(&nodes[1])[..], 3000000).0;
@@ -1950,7 +1955,7 @@ fn claim_htlc_outputs_single_tx() {
19501955
}
19511956

19521957
let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap();
1953-
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)
1958+
assert_eq!(node_txn.len(), 26); // 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)
19541959

19551960
assert_eq!(node_txn[0], node_txn[7]);
19561961
assert_eq!(node_txn[1], node_txn[8]);
@@ -1960,10 +1965,6 @@ fn claim_htlc_outputs_single_tx() {
19601965
assert_eq!(node_txn[3], node_txn[5]); //local commitment tx + htlc timeout tx broadcasted by ChannelManger
19611966
assert_eq!(node_txn[4], node_txn[6]);
19621967

1963-
for i in 12..22 {
1964-
if i % 2 == 0 { assert_eq!(node_txn[3], node_txn[i]); } else { assert_eq!(node_txn[4], node_txn[i]); }
1965-
}
1966-
19671968
assert_eq!(node_txn[0].input.len(), 1);
19681969
assert_eq!(node_txn[1].input.len(), 1);
19691970
assert_eq!(node_txn[2].input.len(), 1);

0 commit comments

Comments
 (0)