Skip to content

Commit d7555b1

Browse files
author
Antoine Riard
committed
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.
1 parent 17e8e94 commit d7555b1

File tree

1 file changed

+174
-86
lines changed

1 file changed

+174
-86
lines changed

lightning/src/ln/channelmonitor.rs

Lines changed: 174 additions & 86 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
}
@@ -397,6 +397,96 @@ enum InputMaterial {
397397
}
398398
}
399399

400+
impl Writeable for InputMaterial {
401+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ::std::io::Error> {
402+
match self {
403+
&InputMaterial::Revoked { ref script, ref pubkey, ref key, ref is_htlc, ref amount} => {
404+
writer.write_all(&[0; 1])?;
405+
script.write(writer)?;
406+
pubkey.write(writer)?;
407+
writer.write_all(&key[..])?;
408+
if *is_htlc {
409+
writer.write_all(&[0; 1])?;
410+
} else {
411+
writer.write_all(&[1; 1])?;
412+
}
413+
writer.write_all(&byte_utils::be64_to_array(*amount))?;
414+
},
415+
&InputMaterial::RemoteHTLC { ref script, ref key, ref preimage, ref amount, ref locktime } => {
416+
writer.write_all(&[1; 1])?;
417+
script.write(writer)?;
418+
key.write(writer)?;
419+
preimage.write(writer)?;
420+
writer.write_all(&byte_utils::be64_to_array(*amount))?;
421+
writer.write_all(&byte_utils::be32_to_array(*locktime))?;
422+
},
423+
&InputMaterial::LocalHTLC { ref script, ref sigs, ref preimage, ref amount } => {
424+
writer.write_all(&[2; 1])?;
425+
script.write(writer)?;
426+
sigs.0.write(writer)?;
427+
sigs.1.write(writer)?;
428+
preimage.write(writer)?;
429+
writer.write_all(&byte_utils::be64_to_array(*amount))?;
430+
}
431+
}
432+
Ok(())
433+
}
434+
}
435+
436+
impl<R: std::io::Read> Readable<R> for InputMaterial {
437+
fn read(reader: &mut R) -> Result<Self, DecodeError> {
438+
let input_material = match <u8 as Readable<R>>::read(reader)? {
439+
0 => {
440+
let script = Readable::read(reader)?;
441+
let pubkey = Readable::read(reader)?;
442+
let key = Readable::read(reader)?;
443+
let is_htlc = match <u8 as Readable<R>>::read(reader)? {
444+
0 => true,
445+
1 => false,
446+
_ => return Err(DecodeError::InvalidValue),
447+
};
448+
let amount = Readable::read(reader)?;
449+
InputMaterial::Revoked {
450+
script,
451+
pubkey,
452+
key,
453+
is_htlc,
454+
amount
455+
}
456+
},
457+
1 => {
458+
let script = Readable::read(reader)?;
459+
let key = Readable::read(reader)?;
460+
let preimage = Readable::read(reader)?;
461+
let amount = Readable::read(reader)?;
462+
let locktime = Readable::read(reader)?;
463+
InputMaterial::RemoteHTLC {
464+
script,
465+
key,
466+
preimage,
467+
amount,
468+
locktime
469+
}
470+
},
471+
2 => {
472+
let script = Readable::read(reader)?;
473+
let their_sig = Readable::read(reader)?;
474+
let our_sig = Readable::read(reader)?;
475+
let preimage = Readable::read(reader)?;
476+
let amount = Readable::read(reader)?;
477+
InputMaterial::LocalHTLC {
478+
script,
479+
sigs: (their_sig, our_sig),
480+
preimage,
481+
amount
482+
}
483+
}
484+
_ => return Err(DecodeError::InvalidValue),
485+
};
486+
Ok(input_material)
487+
}
488+
}
489+
400490
/// Upon discovering of some classes of onchain tx by ChannelMonitor, we may have to take actions on it
401491
/// once they mature to enough confirmations (ANTI_REORG_DELAY)
402492
#[derive(Clone, PartialEq)]
@@ -412,6 +502,13 @@ enum OnchainEvent {
412502
HTLCUpdate {
413503
htlc_update: (HTLCSource, PaymentHash),
414504
},
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+
}
415512
}
416513

417514
/// Higher-level cache structure needed to re-generate bumped claim txn if needed
@@ -1170,36 +1267,7 @@ impl ChannelMonitor {
11701267
writer.write_all(&byte_utils::be64_to_array(claim_tx_data.per_input_material.len() as u64))?;
11711268
for (outp, tx_material) in claim_tx_data.per_input_material.iter() {
11721269
outp.write(writer)?;
1173-
match tx_material {
1174-
&InputMaterial::Revoked { ref script, ref pubkey, ref key, ref is_htlc, ref amount} => {
1175-
writer.write_all(&[0; 1])?;
1176-
script.write(writer)?;
1177-
pubkey.write(writer)?;
1178-
writer.write_all(&key[..])?;
1179-
if *is_htlc {
1180-
writer.write_all(&[0; 1])?;
1181-
} else {
1182-
writer.write_all(&[1; 1])?;
1183-
}
1184-
writer.write_all(&byte_utils::be64_to_array(*amount))?;
1185-
},
1186-
&InputMaterial::RemoteHTLC { ref script, ref key, ref preimage, ref amount, ref locktime } => {
1187-
writer.write_all(&[1; 1])?;
1188-
script.write(writer)?;
1189-
key.write(writer)?;
1190-
preimage.write(writer)?;
1191-
writer.write_all(&byte_utils::be64_to_array(*amount))?;
1192-
writer.write_all(&byte_utils::be32_to_array(*locktime))?;
1193-
},
1194-
&InputMaterial::LocalHTLC { ref script, ref sigs, ref preimage, ref amount } => {
1195-
writer.write_all(&[2; 1])?;
1196-
script.write(writer)?;
1197-
sigs.0.write(writer)?;
1198-
sigs.1.write(writer)?;
1199-
preimage.write(writer)?;
1200-
writer.write_all(&byte_utils::be64_to_array(*amount))?;
1201-
}
1202-
}
1270+
tx_material.write(writer)?;
12031271
}
12041272
}
12051273

@@ -1224,6 +1292,11 @@ impl ChannelMonitor {
12241292
writer.write_all(&[1; 1])?;
12251293
htlc_update.0.write(writer)?;
12261294
htlc_update.1.write(writer)?;
1295+
},
1296+
OnchainEvent::ContentiousOutpoint { ref outpoint, ref input_material } => {
1297+
writer.write_all(&[2; 1])?;
1298+
outpoint.write(writer)?;
1299+
input_material.write(writer)?;
12271300
}
12281301
}
12291302
}
@@ -2250,14 +2323,15 @@ impl ChannelMonitor {
22502323
}
22512324

22522325
// Scan all input to verify is one of the outpoint spent is of interest for us
2326+
let mut claimed_outpoints = Vec::new();
2327+
let mut claimed_input_material = Vec::new();
22532328
for inp in &tx.input {
22542329
if let Some(ancestor_claimable_txid) = self.claimable_outpoints.get(&inp.previous_output) {
22552330
// If outpoint has claim request pending on it...
22562331
if let Some(claim_material) = self.pending_claim_requests.get_mut(&ancestor_claimable_txid.0) {
22572332
//... we need to verify equality between transaction outpoints and claim request
22582333
// outpoints to know if transaction is the original claim or a bumped one issued
22592334
// by us.
2260-
let mut claimed_outpoints = Vec::new();
22612335
for (claim_inp, tx_inp) in claim_material.per_input_material.keys().zip(tx.input.iter()) {
22622336
if *claim_inp != tx_inp.previous_output {
22632337
claimed_outpoints.push(tx_inp.previous_output.clone());
@@ -2271,19 +2345,30 @@ impl ChannelMonitor {
22712345
}
22722346
}
22732347
} else { // If false, generate new claim request with update outpoint set
2274-
for already_claimed in claimed_outpoints {
2275-
claim_material.per_input_material.remove(&already_claimed);
2348+
for already_claimed in claimed_outpoints.iter() {
2349+
if let Some(input_material) = claim_material.per_input_material.remove(&already_claimed) {
2350+
claimed_input_material.push(input_material);
2351+
}
22762352
}
22772353
// Avoid bump engine using inaccurate feerate due to new transaction size
22782354
claim_material.feerate_previous = 0;
22792355
//TODO: recompute soonest_timelock to avoid wasting a bit on fees
22802356
bump_candidates.push((ancestor_claimable_txid.0.clone(), claim_material.clone()));
22812357
}
2358+
break; //No need to iterate further, either tx is our or their
22822359
} else {
22832360
panic!("Inconsistencies between pending_claim_requests map and claimable_outpoints map");
22842361
}
22852362
}
22862363
}
2364+
for (outpoint, input_material) in claimed_outpoints.iter().zip(claimed_input_material.drain(..)) {
2365+
match self.onchain_events_waiting_threshold_conf.entry(height + ANTI_REORG_DELAY - 1) {
2366+
hash_map::Entry::Occupied(_) => {},
2367+
hash_map::Entry::Vacant(entry) => {
2368+
entry.insert(vec![OnchainEvent::ContentiousOutpoint { outpoint: *outpoint, input_material: input_material }]);
2369+
}
2370+
}
2371+
}
22872372
}
22882373
if let Some(ref cur_local_tx) = self.current_local_signed_commitment_tx {
22892374
if self.would_broadcast_at_height(height) {
@@ -2326,6 +2411,9 @@ impl ChannelMonitor {
23262411
log_trace!(self, "HTLC {} failure update has got enough confirmations to be passed upstream", log_bytes!((htlc_update.1).0));
23272412
htlc_updated.push((htlc_update.0, None, htlc_update.1));
23282413
},
2414+
OnchainEvent::ContentiousOutpoint { outpoint, .. } => {
2415+
self.claimable_outpoints.remove(&outpoint);
2416+
}
23292417
}
23302418
}
23312419
}
@@ -2348,13 +2436,52 @@ impl ChannelMonitor {
23482436
(watch_outputs, spendable_outputs, htlc_updated)
23492437
}
23502438

2351-
fn block_disconnected(&mut self, height: u32, block_hash: &Sha256dHash) {
2352-
if let Some(_) = self.onchain_events_waiting_threshold_conf.remove(&(height + ANTI_REORG_DELAY - 1)) {
2439+
fn block_disconnected(&mut self, height: u32, block_hash: &Sha256dHash, broadcaster: &BroadcasterInterface, fee_estimator: &FeeEstimator) {
2440+
let mut bump_candidates = HashMap::new();
2441+
if let Some(events) = self.onchain_events_waiting_threshold_conf.remove(&(height + ANTI_REORG_DELAY - 1)) {
23532442
//We may discard:
23542443
//- htlc update there as failure-trigger tx (revoked commitment tx, non-revoked commitment tx, HTLC-timeout tx) has been disconnected
23552444
//- our claim tx on a commitment tx output
2445+
//- resurect outpoint back in its claimable set and regenerate tx
2446+
for ev in events {
2447+
match ev {
2448+
OnchainEvent::ContentiousOutpoint { outpoint, input_material } => {
2449+
if let Some(ancestor_claimable_txid) = self.claimable_outpoints.get(&outpoint) {
2450+
if let Some(claim_material) = self.pending_claim_requests.get_mut(&ancestor_claimable_txid.0) {
2451+
// Avoid bump engine using inaccurate feerate due to new transaction size
2452+
claim_material.feerate_previous = 0;
2453+
claim_material.per_input_material.insert(outpoint, input_material);
2454+
// Using a HashMap guarantee us than if we have multiple outpoints getting
2455+
// resurrected only one bump claim tx is going to be broadcast
2456+
bump_candidates.insert(ancestor_claimable_txid.clone(), claim_material.clone());
2457+
}
2458+
}
2459+
},
2460+
_ => {},
2461+
}
2462+
}
2463+
}
2464+
for (_, claim_material) in bump_candidates.iter_mut() {
2465+
if let Some((new_timer, new_feerate, bump_tx)) = self.bump_claim_tx(height, &claim_material, fee_estimator) {
2466+
claim_material.height_timer = new_timer;
2467+
claim_material.feerate_previous = new_feerate;
2468+
broadcaster.broadcast_transaction(&bump_tx);
2469+
}
2470+
}
2471+
for (ancestor_claim_txid, claim_material) in bump_candidates.drain() {
2472+
self.pending_claim_requests.insert(ancestor_claim_txid.0, claim_material);
2473+
}
2474+
//TODO: if we implement cross-block aggregated claim transaction we need to refresh set of outpoints and regenerate tx but
2475+
// right now if one of the outpoint get disconnected, just erase whole pending claim request.
2476+
let mut remove_request = Vec::new();
2477+
self.claimable_outpoints.retain(|_, ref v|
2478+
if v.1 == height {
2479+
remove_request.push(v.0.clone());
2480+
false
2481+
} else { true });
2482+
for req in remove_request {
2483+
self.pending_claim_requests.remove(&req);
23562484
}
2357-
self.claimable_outpoints.retain(|_, ref v| if v.1 == height { false } else { true });
23582485
self.last_block_hash = block_hash.clone();
23592486
}
23602487

@@ -2927,55 +3054,8 @@ impl<R: ::std::io::Read> ReadableArgs<R, Arc<Logger>> for (Sha256dHash, ChannelM
29273054
let mut per_input_material = HashMap::with_capacity(cmp::min(per_input_material_len as usize, MAX_ALLOC_SIZE / 128));
29283055
for _ in 0 ..per_input_material_len {
29293056
let outpoint = Readable::read(reader)?;
2930-
let tx_material = match <u8 as Readable<R>>::read(reader)? {
2931-
0 => {
2932-
let script = Readable::read(reader)?;
2933-
let pubkey = Readable::read(reader)?;
2934-
let key = Readable::read(reader)?;
2935-
let is_htlc = match <u8 as Readable<R>>::read(reader)? {
2936-
0 => true,
2937-
1 => false,
2938-
_ => return Err(DecodeError::InvalidValue),
2939-
};
2940-
let amount = Readable::read(reader)?;
2941-
InputMaterial::Revoked {
2942-
script,
2943-
pubkey,
2944-
key,
2945-
is_htlc,
2946-
amount
2947-
}
2948-
},
2949-
1 => {
2950-
let script = Readable::read(reader)?;
2951-
let key = Readable::read(reader)?;
2952-
let preimage = Readable::read(reader)?;
2953-
let amount = Readable::read(reader)?;
2954-
let locktime = Readable::read(reader)?;
2955-
InputMaterial::RemoteHTLC {
2956-
script,
2957-
key,
2958-
preimage,
2959-
amount,
2960-
locktime
2961-
}
2962-
},
2963-
2 => {
2964-
let script = Readable::read(reader)?;
2965-
let their_sig = Readable::read(reader)?;
2966-
let our_sig = Readable::read(reader)?;
2967-
let preimage = Readable::read(reader)?;
2968-
let amount = Readable::read(reader)?;
2969-
InputMaterial::LocalHTLC {
2970-
script,
2971-
sigs: (their_sig, our_sig),
2972-
preimage,
2973-
amount
2974-
}
2975-
}
2976-
_ => return Err(DecodeError::InvalidValue),
2977-
};
2978-
per_input_material.insert(outpoint, tx_material);
3057+
let input_material = Readable::read(reader)?;
3058+
per_input_material.insert(outpoint, input_material);
29793059
}
29803060
pending_claim_requests.insert(ancestor_claim_txid, ClaimTxBumpMaterial { height_timer, feerate_previous, soonest_timelock, per_input_material });
29813061
}
@@ -3010,6 +3090,14 @@ impl<R: ::std::io::Read> ReadableArgs<R, Arc<Logger>> for (Sha256dHash, ChannelM
30103090
htlc_update: (htlc_source, hash)
30113091
}
30123092
},
3093+
2 => {
3094+
let outpoint = Readable::read(reader)?;
3095+
let input_material = Readable::read(reader)?;
3096+
OnchainEvent::ContentiousOutpoint {
3097+
outpoint,
3098+
input_material
3099+
}
3100+
}
30133101
_ => return Err(DecodeError::InvalidValue),
30143102
};
30153103
events.push(ev);

0 commit comments

Comments
 (0)