Skip to content

Commit a17d881

Browse files
Antoine RiardTheBlueMatt
Antoine Riard
authored andcommitted
Track and react to remote partial-claiming of pending claim request
A pending claim request may contain a set of multiple outpoints. If one or multiple of them get claimed by remote party, our in-flight claiming transactions aren't valid anymore so we need to react quickly and regenerate claiming transaction with accurate set. However, a claimed outpoint may be disconnected and we need to resurrect back outpoint among set of orignal pending claim request. To guarantee consistency of contentious claimed outpoint we cache it as OnchainEvent::ContentionsOutpoint and only delete it after ANTI_REORG_DELAY. Fix test broken by change, partial claiming on revoked txn force us to regenerate txn
1 parent 0843c9a commit a17d881

File tree

4 files changed

+150
-47
lines changed

4 files changed

+150
-47
lines changed

fuzz/fuzz_targets/full_stack_target.rs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

lightning/src/ln/channelmonitor.rs

Lines changed: 113 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ impl<'a, Key : Send + cmp::Eq + hash::Hash> ChainListener for SimpleManyChannelM
212212
let block_hash = header.bitcoin_hash();
213213
let mut monitors = self.monitors.lock().unwrap();
214214
for monitor in monitors.values_mut() {
215-
monitor.block_disconnected(disconnected_height, &block_hash);
215+
monitor.block_disconnected(disconnected_height, &block_hash, &*self.broadcaster, &*self.fee_estimator);
216216
}
217217
}
218218
}
@@ -502,6 +502,13 @@ enum OnchainEvent {
502502
HTLCUpdate {
503503
htlc_update: (HTLCSource, PaymentHash),
504504
},
505+
/// Claim tx aggregate multiple claimable outpoints. One of the outpoint may be claimed by a remote party tx.
506+
/// In this case, we need to drop the outpoint and regenerate a new claim tx. By safety, we keep tracking
507+
/// the outpoint to be sure to resurect it back to the claim tx if reorgs happen.
508+
ContentiousOutpoint {
509+
outpoint: BitcoinOutPoint,
510+
input_material: InputMaterial,
511+
}
505512
}
506513

507514
/// Higher-level cache structure needed to re-generate bumped claim txn if needed
@@ -1321,6 +1328,11 @@ impl ChannelMonitor {
13211328
writer.write_all(&[1; 1])?;
13221329
htlc_update.0.write(writer)?;
13231330
htlc_update.1.write(writer)?;
1331+
},
1332+
OnchainEvent::ContentiousOutpoint { ref outpoint, ref input_material } => {
1333+
writer.write_all(&[2; 1])?;
1334+
outpoint.write(writer)?;
1335+
input_material.write(writer)?;
13241336
}
13251337
}
13261338
}
@@ -1545,6 +1557,10 @@ impl ChannelMonitor {
15451557
log_trace!(self, "Outpoint {}:{} is being being claimed, if it doesn't succeed, a bumped claiming txn is going to be broadcast at height {}", single_htlc_tx.input[0].previous_output.txid, single_htlc_tx.input[0].previous_output.vout, height_timer);
15461558
let mut per_input_material = HashMap::with_capacity(1);
15471559
per_input_material.insert(single_htlc_tx.input[0].previous_output, InputMaterial::Revoked { script: redeemscript, pubkey: Some(revocation_pubkey), key: revocation_key, is_htlc: true, amount: htlc.amount_msat / 1000 });
1560+
match self.claimable_outpoints.entry(single_htlc_tx.input[0].previous_output) {
1561+
hash_map::Entry::Occupied(_) => {},
1562+
hash_map::Entry::Vacant(entry) => { entry.insert((single_htlc_tx.txid(), height)); }
1563+
}
15481564
match self.pending_claim_requests.entry(single_htlc_tx.txid()) {
15491565
hash_map::Entry::Occupied(_) => {},
15501566
hash_map::Entry::Vacant(entry) => { entry.insert(ClaimTxBumpMaterial { height_timer, feerate_previous: used_feerate, soonest_timelock: htlc.cltv_expiry, per_input_material }); }
@@ -1630,15 +1646,17 @@ impl ChannelMonitor {
16301646
}
16311647
}
16321648
let height_timer = Self::get_height_timer(height, soonest_timelock);
1649+
let spend_txid = spend_tx.txid();
16331650
for (input, info) in spend_tx.input.iter_mut().zip(inputs_info.iter()) {
16341651
let (redeemscript, revocation_key) = sign_input!(sighash_parts, input, info.0, info.1);
16351652
log_trace!(self, "Outpoint {}:{} is being being claimed, if it doesn't succeed, a bumped claiming txn is going to be broadcast at height {}", input.previous_output.txid, input.previous_output.vout, height_timer);
16361653
per_input_material.insert(input.previous_output, InputMaterial::Revoked { script: redeemscript, pubkey: if info.0.is_some() { Some(revocation_pubkey) } else { None }, key: revocation_key, is_htlc: if info.0.is_some() { true } else { false }, amount: info.1 });
1637-
if info.2 < soonest_timelock {
1638-
soonest_timelock = info.2;
1654+
match self.claimable_outpoints.entry(input.previous_output) {
1655+
hash_map::Entry::Occupied(_) => {},
1656+
hash_map::Entry::Vacant(entry) => { entry.insert((spend_txid, height)); }
16391657
}
16401658
}
1641-
match self.pending_claim_requests.entry(spend_tx.txid()) {
1659+
match self.pending_claim_requests.entry(spend_txid) {
16421660
hash_map::Entry::Occupied(_) => {},
16431661
hash_map::Entry::Vacant(entry) => { entry.insert(ClaimTxBumpMaterial { height_timer, feerate_previous: used_feerate, soonest_timelock, per_input_material }); }
16441662
}
@@ -1831,6 +1849,10 @@ impl ChannelMonitor {
18311849
log_trace!(self, "Outpoint {}:{} is being being claimed, if it doesn't succeed, a bumped claiming txn is going to be broadcast at height {}", single_htlc_tx.input[0].previous_output.txid, single_htlc_tx.input[0].previous_output.vout, height_timer);
18321850
let mut per_input_material = HashMap::with_capacity(1);
18331851
per_input_material.insert(single_htlc_tx.input[0].previous_output, InputMaterial::RemoteHTLC { script: redeemscript, key: htlc_key, preimage: Some(*payment_preimage), amount: htlc.amount_msat / 1000, locktime: 0 });
1852+
match self.claimable_outpoints.entry(single_htlc_tx.input[0].previous_output) {
1853+
hash_map::Entry::Occupied(_) => {},
1854+
hash_map::Entry::Vacant(entry) => { entry.insert((single_htlc_tx.txid(), height)); }
1855+
}
18341856
match self.pending_claim_requests.entry(single_htlc_tx.txid()) {
18351857
hash_map::Entry::Occupied(_) => {},
18361858
hash_map::Entry::Vacant(entry) => { entry.insert(ClaimTxBumpMaterial { height_timer, feerate_previous: used_feerate, soonest_timelock: htlc.cltv_expiry, per_input_material}); }
@@ -1872,6 +1894,10 @@ impl ChannelMonitor {
18721894
log_trace!(self, "Outpoint {}:{} is being being claimed, if it doesn't succeed, a bumped claiming txn is going to be broadcast at height {}", timeout_tx.input[0].previous_output.txid, timeout_tx.input[0].previous_output.vout, height_timer);
18731895
let mut per_input_material = HashMap::with_capacity(1);
18741896
per_input_material.insert(timeout_tx.input[0].previous_output, InputMaterial::RemoteHTLC { script : redeemscript, key: htlc_key, preimage: None, amount: htlc.amount_msat / 1000, locktime: htlc.cltv_expiry });
1897+
match self.claimable_outpoints.entry(timeout_tx.input[0].previous_output) {
1898+
hash_map::Entry::Occupied(_) => {},
1899+
hash_map::Entry::Vacant(entry) => { entry.insert((timeout_tx.txid(), height)); }
1900+
}
18751901
match self.pending_claim_requests.entry(timeout_tx.txid()) {
18761902
hash_map::Entry::Occupied(_) => {},
18771903
hash_map::Entry::Vacant(entry) => { entry.insert(ClaimTxBumpMaterial { height_timer, feerate_previous: used_feerate, soonest_timelock: htlc.cltv_expiry, per_input_material }); }
@@ -1912,12 +1938,17 @@ impl ChannelMonitor {
19121938
}
19131939
}
19141940
let height_timer = Self::get_height_timer(height, soonest_timelock);
1941+
let spend_txid = spend_tx.txid();
19151942
for (input, info) in spend_tx.input.iter_mut().zip(inputs_info.iter()) {
19161943
let (redeemscript, htlc_key) = sign_input!(sighash_parts, input, info.1, (info.0).0.to_vec());
19171944
log_trace!(self, "Outpoint {}:{} is being being claimed, if it doesn't succeed, a bumped claiming txn is going to be broadcast at height {}", input.previous_output.txid, input.previous_output.vout, height_timer);
19181945
per_input_material.insert(input.previous_output, InputMaterial::RemoteHTLC { script: redeemscript, key: htlc_key, preimage: Some(*(info.0)), amount: info.1, locktime: 0});
1946+
match self.claimable_outpoints.entry(input.previous_output) {
1947+
hash_map::Entry::Occupied(_) => {},
1948+
hash_map::Entry::Vacant(entry) => { entry.insert((spend_txid, height)); }
1949+
}
19191950
}
1920-
match self.pending_claim_requests.entry(spend_tx.txid()) {
1951+
match self.pending_claim_requests.entry(spend_txid) {
19211952
hash_map::Entry::Occupied(_) => {},
19221953
hash_map::Entry::Vacant(entry) => { entry.insert(ClaimTxBumpMaterial { height_timer, feerate_previous: used_feerate, soonest_timelock, per_input_material }); }
19231954
}
@@ -2036,6 +2067,10 @@ impl ChannelMonitor {
20362067
log_trace!(self, "Outpoint {}:{} is being being claimed, if it doesn't succeed, a bumped claiming txn is going to be broadcast at height {}", spend_tx.input[0].previous_output.txid, spend_tx.input[0].previous_output.vout, height_timer);
20372068
let mut per_input_material = HashMap::with_capacity(1);
20382069
per_input_material.insert(spend_tx.input[0].previous_output, InputMaterial::Revoked { script: redeemscript, pubkey: None, key: revocation_key, is_htlc: false, amount: tx.output[0].value });
2070+
match self.claimable_outpoints.entry(spend_tx.input[0].previous_output) {
2071+
hash_map::Entry::Occupied(_) => {},
2072+
hash_map::Entry::Vacant(entry) => { entry.insert((spend_tx.txid(), height)); }
2073+
}
20392074
match self.pending_claim_requests.entry(spend_tx.txid()) {
20402075
hash_map::Entry::Occupied(_) => {},
20412076
hash_map::Entry::Vacant(entry) => { entry.insert(ClaimTxBumpMaterial { height_timer, feerate_previous: used_feerate, soonest_timelock: height + self.our_to_self_delay as u32, per_input_material }); }
@@ -2099,6 +2134,7 @@ impl ChannelMonitor {
20992134
let height_timer = Self::get_height_timer(height, htlc.cltv_expiry);
21002135
let mut per_input_material = HashMap::with_capacity(1);
21012136
per_input_material.insert(htlc_timeout_tx.input[0].previous_output, InputMaterial::LocalHTLC { script: htlc_script, sigs: (*their_sig, *our_sig), preimage: None, amount: htlc.amount_msat / 1000});
2137+
//TODO: with option_simplified_commitment track outpoint too
21022138
log_trace!(self, "Outpoint {}:{} is being being claimed, if it doesn't succeed, a bumped claiming txn is going to be broadcast at height {}", htlc_timeout_tx.input[0].previous_output.vout, htlc_timeout_tx.input[0].previous_output.txid, height_timer);
21032139
pending_claims.push((htlc_timeout_tx.txid(), ClaimTxBumpMaterial { height_timer, feerate_previous: 0, soonest_timelock: htlc.cltv_expiry, per_input_material }));
21042140
res.push(htlc_timeout_tx);
@@ -2122,6 +2158,7 @@ impl ChannelMonitor {
21222158
let height_timer = Self::get_height_timer(height, htlc.cltv_expiry);
21232159
let mut per_input_material = HashMap::with_capacity(1);
21242160
per_input_material.insert(htlc_success_tx.input[0].previous_output, InputMaterial::LocalHTLC { script: htlc_script, sigs: (*their_sig, *our_sig), preimage: Some(*payment_preimage), amount: htlc.amount_msat / 1000});
2161+
//TODO: with option_simplified_commitment track outpoint too
21252162
log_trace!(self, "Outpoint {}:{} is being being claimed, if it doesn't succeed, a bumped claiming txn is going to be broadcast at height {}", htlc_success_tx.input[0].previous_output.vout, htlc_success_tx.input[0].previous_output.txid, height_timer);
21262163
pending_claims.push((htlc_success_tx.txid(), ClaimTxBumpMaterial { height_timer, feerate_previous: 0, soonest_timelock: htlc.cltv_expiry, per_input_material }));
21272164
res.push(htlc_success_tx);
@@ -2356,17 +2393,18 @@ impl ChannelMonitor {
23562393
}
23572394

23582395
// Scan all input to verify is one of the outpoint spent is of interest for us
2396+
let mut claimed_outpoints = Vec::new();
2397+
let mut claimed_input_material = Vec::new();
23592398
for inp in &tx.input {
23602399
if let Some(ancestor_claimable_txid) = self.claimable_outpoints.get(&inp.previous_output) {
23612400
// If outpoint has claim request pending on it...
23622401
if let Some(claim_material) = self.pending_claim_requests.get_mut(&ancestor_claimable_txid.0) {
23632402
//... we need to verify equality between transaction outpoints and claim request
23642403
// outpoints to know if transaction is the original claim or a bumped one issued
23652404
// by us.
2366-
let mut claimed_outpoints = Vec::new();
2367-
for (claim_inp, tx_inp) in claim_material.per_input_material.keys().zip(tx.input.iter()) {
2368-
if *claim_inp != tx_inp.previous_output {
2369-
claimed_outpoints.push(tx_inp.previous_output.clone());
2405+
for claim_inp in claim_material.per_input_material.keys() {
2406+
if *claim_inp == inp.previous_output {
2407+
claimed_outpoints.push(inp.previous_output.clone());
23702408
}
23712409
}
23722410
if claimed_outpoints.len() == 0 && claim_material.per_input_material.len() == tx.input.len() { // If true, register claim request to be removed after reaching a block security height
@@ -2377,19 +2415,30 @@ impl ChannelMonitor {
23772415
}
23782416
}
23792417
} else { // If false, generate new claim request with update outpoint set
2380-
for already_claimed in claimed_outpoints {
2381-
claim_material.per_input_material.remove(&already_claimed);
2418+
for already_claimed in claimed_outpoints.iter() {
2419+
if let Some(input_material) = claim_material.per_input_material.remove(&already_claimed) {
2420+
claimed_input_material.push(input_material);
2421+
}
23822422
}
23832423
// Avoid bump engine using inaccurate feerate due to new transaction size
23842424
claim_material.feerate_previous = 0;
23852425
//TODO: recompute soonest_timelock to avoid wasting a bit on fees
23862426
bump_candidates.push((ancestor_claimable_txid.0.clone(), claim_material.clone()));
23872427
}
2428+
break; //No need to iterate further, either tx is our or their
23882429
} else {
23892430
panic!("Inconsistencies between pending_claim_requests map and claimable_outpoints map");
23902431
}
23912432
}
23922433
}
2434+
for (outpoint, input_material) in claimed_outpoints.iter().zip(claimed_input_material.drain(..)) {
2435+
match self.onchain_events_waiting_threshold_conf.entry(height + ANTI_REORG_DELAY - 1) {
2436+
hash_map::Entry::Occupied(_) => {},
2437+
hash_map::Entry::Vacant(entry) => {
2438+
entry.insert(vec![OnchainEvent::ContentiousOutpoint { outpoint: *outpoint, input_material: input_material }]);
2439+
}
2440+
}
2441+
}
23932442
}
23942443
if let Some(ref cur_local_tx) = self.current_local_signed_commitment_tx {
23952444
if self.would_broadcast_at_height(height) {
@@ -2432,6 +2481,9 @@ impl ChannelMonitor {
24322481
log_trace!(self, "HTLC {} failure update has got enough confirmations to be passed upstream", log_bytes!((htlc_update.1).0));
24332482
htlc_updated.push((htlc_update.0, None, htlc_update.1));
24342483
},
2484+
OnchainEvent::ContentiousOutpoint { outpoint, .. } => {
2485+
self.claimable_outpoints.remove(&outpoint);
2486+
}
24352487
}
24362488
}
24372489
}
@@ -2454,13 +2506,52 @@ impl ChannelMonitor {
24542506
(watch_outputs, spendable_outputs, htlc_updated)
24552507
}
24562508

2457-
fn block_disconnected(&mut self, height: u32, block_hash: &Sha256dHash) {
2458-
if let Some(_) = self.onchain_events_waiting_threshold_conf.remove(&(height + ANTI_REORG_DELAY - 1)) {
2509+
fn block_disconnected(&mut self, height: u32, block_hash: &Sha256dHash, broadcaster: &BroadcasterInterface, fee_estimator: &FeeEstimator) {
2510+
let mut bump_candidates = HashMap::new();
2511+
if let Some(events) = self.onchain_events_waiting_threshold_conf.remove(&(height + ANTI_REORG_DELAY - 1)) {
24592512
//We may discard:
24602513
//- htlc update there as failure-trigger tx (revoked commitment tx, non-revoked commitment tx, HTLC-timeout tx) has been disconnected
24612514
//- our claim tx on a commitment tx output
2515+
//- resurect outpoint back in its claimable set and regenerate tx
2516+
for ev in events {
2517+
match ev {
2518+
OnchainEvent::ContentiousOutpoint { outpoint, input_material } => {
2519+
if let Some(ancestor_claimable_txid) = self.claimable_outpoints.get(&outpoint) {
2520+
if let Some(claim_material) = self.pending_claim_requests.get_mut(&ancestor_claimable_txid.0) {
2521+
// Avoid bump engine using inaccurate feerate due to new transaction size
2522+
claim_material.feerate_previous = 0;
2523+
claim_material.per_input_material.insert(outpoint, input_material);
2524+
// Using a HashMap guarantee us than if we have multiple outpoints getting
2525+
// resurrected only one bump claim tx is going to be broadcast
2526+
bump_candidates.insert(ancestor_claimable_txid.clone(), claim_material.clone());
2527+
}
2528+
}
2529+
},
2530+
_ => {},
2531+
}
2532+
}
2533+
}
2534+
for (_, claim_material) in bump_candidates.iter_mut() {
2535+
if let Some((new_timer, new_feerate, bump_tx)) = self.bump_claim_tx(height, &claim_material, fee_estimator) {
2536+
claim_material.height_timer = new_timer;
2537+
claim_material.feerate_previous = new_feerate;
2538+
broadcaster.broadcast_transaction(&bump_tx);
2539+
}
2540+
}
2541+
for (ancestor_claim_txid, claim_material) in bump_candidates.drain() {
2542+
self.pending_claim_requests.insert(ancestor_claim_txid.0, claim_material);
2543+
}
2544+
//TODO: if we implement cross-block aggregated claim transaction we need to refresh set of outpoints and regenerate tx but
2545+
// right now if one of the outpoint get disconnected, just erase whole pending claim request.
2546+
let mut remove_request = Vec::new();
2547+
self.claimable_outpoints.retain(|_, ref v|
2548+
if v.1 == height {
2549+
remove_request.push(v.0.clone());
2550+
false
2551+
} else { true });
2552+
for req in remove_request {
2553+
self.pending_claim_requests.remove(&req);
24622554
}
2463-
self.claimable_outpoints.retain(|_, ref v| if v.1 == height { false } else { true });
24642555
self.last_block_hash = block_hash.clone();
24652556
}
24662557

@@ -3060,6 +3151,14 @@ impl<R: ::std::io::Read> ReadableArgs<R, Arc<Logger>> for (Sha256dHash, ChannelM
30603151
htlc_update: (htlc_source, hash)
30613152
}
30623153
},
3154+
2 => {
3155+
let outpoint = Readable::read(reader)?;
3156+
let input_material = Readable::read(reader)?;
3157+
OnchainEvent::ContentiousOutpoint {
3158+
outpoint,
3159+
input_material
3160+
}
3161+
}
30633162
_ => return Err(DecodeError::InvalidValue),
30643163
};
30653164
events.push(ev);

lightning/src/ln/functional_test_utils.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -920,15 +920,21 @@ pub fn test_txn_broadcast(node: &Node, chan: &(msgs::ChannelUpdate, msgs::Channe
920920

921921
/// Tests that the given node has broadcast a claim transaction against the provided revoked
922922
/// HTLC transaction.
923-
pub fn test_revoked_htlc_claim_txn_broadcast(node: &Node, revoked_tx: Transaction) {
923+
pub fn test_revoked_htlc_claim_txn_broadcast(node: &Node, revoked_tx: Transaction, commitment_revoked_tx: Transaction) {
924924
let mut node_txn = node.tx_broadcaster.txn_broadcasted.lock().unwrap();
925-
assert_eq!(node_txn.len(), 1);
925+
// We should issue a 2nd transaction if one htlc is dropped from initial claiming tx
926+
// but sometimes not as feerate is too-low
927+
if node_txn.len() != 1 && node_txn.len() != 2 { assert!(false); }
926928
node_txn.retain(|tx| {
927929
if tx.input.len() == 1 && tx.input[0].previous_output.txid == revoked_tx.txid() {
928-
check_spends!(tx, revoked_tx.clone());
930+
check_spends!(tx, revoked_tx);
929931
false
930932
} else { true }
931933
});
934+
node_txn.retain(|tx| {
935+
check_spends!(tx, commitment_revoked_tx);
936+
false
937+
});
932938
assert!(node_txn.is_empty());
933939
}
934940

0 commit comments

Comments
 (0)