Skip to content

Commit d618bf1

Browse files
committed
Update HTLC transaction detection from revoked counterparty commitments
Previously, this method assumed that all HTLC transactions have 1 input and 1 output, with the sole input having a witness of 5 elements. This will no longer be the case for HTLC transactions on channels with anchors outputs since additional inputs and outputs can be attached to them to allow fee bumping.
1 parent af02d10 commit d618bf1

File tree

1 file changed

+63
-34
lines changed

1 file changed

+63
-34
lines changed

lightning/src/chain/channelmonitor.rs

Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2645,31 +2645,49 @@ impl<Signer: Sign> ChannelMonitorImpl<Signer> {
26452645
}
26462646

26472647
/// Attempts to claim a counterparty HTLC-Success/HTLC-Timeout's outputs using the revocation key
2648-
fn check_spend_counterparty_htlc<L: Deref>(&mut self, tx: &Transaction, commitment_number: u64, height: u32, logger: &L) -> (Vec<PackageTemplate>, Option<TransactionOutputs>) where L::Target: Logger {
2649-
let htlc_txid = tx.txid();
2650-
if tx.input.len() != 1 || tx.output.len() != 1 || tx.input[0].witness.len() != 5 {
2651-
return (Vec::new(), None)
2652-
}
2653-
2654-
macro_rules! ignore_error {
2655-
( $thing : expr ) => {
2656-
match $thing {
2657-
Ok(a) => a,
2658-
Err(_) => return (Vec::new(), None)
2659-
}
2660-
};
2661-
}
2662-
2648+
fn check_spend_counterparty_htlc<L: Deref>(
2649+
&mut self, tx: &Transaction, commitment_number: u64, commitment_txid: &Txid, height: u32, logger: &L
2650+
) -> (Vec<PackageTemplate>, Option<TransactionOutputs>) where L::Target: Logger {
26632651
let secret = if let Some(secret) = self.get_secret(commitment_number) { secret } else { return (Vec::new(), None); };
2664-
let per_commitment_key = ignore_error!(SecretKey::from_slice(&secret));
2652+
let per_commitment_key = match SecretKey::from_slice(&secret) {
2653+
Ok(key) => key,
2654+
Err(_) => return (Vec::new(), None)
2655+
};
26652656
let per_commitment_point = PublicKey::from_secret_key(&self.secp_ctx, &per_commitment_key);
26662657

2667-
log_error!(logger, "Got broadcast of revoked counterparty HTLC transaction, spending {}:{}", htlc_txid, 0);
2668-
let revk_outp = RevokedOutput::build(per_commitment_point, self.counterparty_commitment_params.counterparty_delayed_payment_base_key, self.counterparty_commitment_params.counterparty_htlc_base_key, per_commitment_key, tx.output[0].value, self.counterparty_commitment_params.on_counterparty_tx_csv);
2669-
let justice_package = PackageTemplate::build_package(htlc_txid, 0, PackageSolvingData::RevokedOutput(revk_outp), height + self.counterparty_commitment_params.on_counterparty_tx_csv as u32, true, height);
2670-
let claimable_outpoints = vec!(justice_package);
2671-
let outputs = vec![(0, tx.output[0].clone())];
2672-
(claimable_outpoints, Some((htlc_txid, outputs)))
2658+
let htlc_txid = tx.txid();
2659+
let mut claimable_outpoints = vec![];
2660+
let mut outputs_to_watch = None;
2661+
// Previously, we would only claim HTLCs from revoked HTLC transactions if they had 1 input
2662+
// with a witness of 5 elements and 1 output. This wasn't enough for anchor outputs, as the
2663+
// counterparty can now aggregate multiple HTLCs into a single transaction thanks to
2664+
// `SIGHASH_SINGLE` remote signatures, leading us to not claim any HTLCs upon seeing a
2665+
// confirmed revoked HTLC transaction (for more details, see
2666+
// https://lists.linuxfoundation.org/pipermail/lightning-dev/2022-April/003561.html).
2667+
//
2668+
// We make sure we're not vulnerable to this case by checking all inputs of the transaction,
2669+
// and claim those which spend the commitment transaction, have a witness of 5 elements, and
2670+
// have a corresponding output at the same index within the transaction.
2671+
for (idx, input) in tx.input.iter().enumerate() {
2672+
if input.previous_output.txid == *commitment_txid && input.witness.len() == 5 && tx.output.get(idx).is_some() {
2673+
log_error!(logger, "Got broadcast of revoked counterparty HTLC transaction, spending {}:{}", htlc_txid, idx);
2674+
let revk_outp = RevokedOutput::build(
2675+
per_commitment_point, self.counterparty_commitment_params.counterparty_delayed_payment_base_key,
2676+
self.counterparty_commitment_params.counterparty_htlc_base_key, per_commitment_key,
2677+
tx.output[idx].value, self.counterparty_commitment_params.on_counterparty_tx_csv
2678+
);
2679+
let justice_package = PackageTemplate::build_package(
2680+
htlc_txid, idx as u32, PackageSolvingData::RevokedOutput(revk_outp),
2681+
height + self.counterparty_commitment_params.on_counterparty_tx_csv as u32, true, height
2682+
);
2683+
claimable_outpoints.push(justice_package);
2684+
if outputs_to_watch.is_none() {
2685+
outputs_to_watch = Some((htlc_txid, vec![]));
2686+
}
2687+
outputs_to_watch.as_mut().unwrap().1.push((idx as u32, tx.output[idx].clone()));
2688+
}
2689+
}
2690+
(claimable_outpoints, outputs_to_watch)
26732691
}
26742692

26752693
// Returns (1) `PackageTemplate`s that can be given to the OnchainTxHandler, so that the handler can
@@ -2927,9 +2945,9 @@ impl<Signer: Sign> ChannelMonitorImpl<Signer> {
29272945

29282946
if tx.input.len() == 1 {
29292947
// Assuming our keys were not leaked (in which case we're screwed no matter what),
2930-
// commitment transactions and HTLC transactions will all only ever have one input,
2931-
// which is an easy way to filter out any potential non-matching txn for lazy
2932-
// filters.
2948+
// commitment transactions and HTLC transactions will all only ever have one input
2949+
// (except for HTLC transactions for channels with anchor outputs), which is an easy
2950+
// way to filter out any potential non-matching txn for lazy filters.
29332951
let prevout = &tx.input[0].previous_output;
29342952
if prevout.txid == self.funding_info.0.txid && prevout.vout == self.funding_info.0.index as u32 {
29352953
let mut balance_spendable_csv = None;
@@ -2967,22 +2985,33 @@ impl<Signer: Sign> ChannelMonitorImpl<Signer> {
29672985
commitment_tx_to_counterparty_output,
29682986
},
29692987
});
2970-
} else {
2971-
if let Some(&commitment_number) = self.counterparty_commitment_txn_on_chain.get(&prevout.txid) {
2972-
let (mut new_outpoints, new_outputs_option) = self.check_spend_counterparty_htlc(&tx, commitment_number, height, &logger);
2988+
}
2989+
}
2990+
if tx.input.len() >= 1 {
2991+
// While all commitment transactions have one input, HTLC transactions may have more
2992+
// if the HTLC was present in an anchor channel. HTLCs can also be resolved in a few
2993+
// other ways which can have more than one output.
2994+
for tx_input in &tx.input {
2995+
let commitment_txid = tx_input.previous_output.txid;
2996+
if let Some(&commitment_number) = self.counterparty_commitment_txn_on_chain.get(&commitment_txid) {
2997+
let (mut new_outpoints, new_outputs_option) = self.check_spend_counterparty_htlc(
2998+
&tx, commitment_number, &commitment_txid, height, &logger
2999+
);
29733000
claimable_outpoints.append(&mut new_outpoints);
29743001
if let Some(new_outputs) = new_outputs_option {
29753002
watch_outputs.push(new_outputs);
29763003
}
3004+
// Since there may be multiple HTLCs (all from the same commitment) being
3005+
// claimed by the counterparty within the same transaction, and
3006+
// `check_spend_counterparty_htlc` already checks for all of them, we can
3007+
// safely break from our loop.
3008+
break;
29773009
}
29783010
}
2979-
}
2980-
// While all commitment/HTLC-Success/HTLC-Timeout transactions have one input, HTLCs
2981-
// can also be resolved in a few other ways which can have more than one output. Thus,
2982-
// we call is_resolving_htlc_output here outside of the tx.input.len() == 1 check.
2983-
self.is_resolving_htlc_output(&tx, height, &block_hash, &logger);
3011+
self.is_resolving_htlc_output(&tx, height, &block_hash, &logger);
29843012

2985-
self.is_paying_spendable_output(&tx, height, &block_hash, &logger);
3013+
self.is_paying_spendable_output(&tx, height, &block_hash, &logger);
3014+
}
29863015
}
29873016

29883017
if height > self.best_block.height() {

0 commit comments

Comments
 (0)