Skip to content

Commit 46f0d31

Browse files
committed
Only claim HTLCs with matching payment hash upon preimage monitor update
Previously, we'd attempt to claim all HTLCs that have expired or that we have the preimage for on each preimage monitor update. This happened due to reusing the code path (`get_counterparty_output_claim_info`) used when producing all claims for a newly confirmed counterparty commitment. Unfortunately, this can result in invalid claim transactions and ultimately in loss of funds (if the HTLC expires and the counterparty claims it via the timeout), as it didn't consider that some of those HTLCs may have already been claimed by a separate transaction. This commit changes the behavior when handling preimage monitor updates only. We will now only attempt to claim HTLCs for the specific preimage that we learned via the monitor update. This is safe to do, as even if a preimage HTLC claim transaction is reorged out, the `OnchainTxHandler` is responsible for continuous claiming attempts until we see a reorg of the corresponding commitment transaction.
1 parent 2fbaf96 commit 46f0d31

File tree

2 files changed

+183
-57
lines changed

2 files changed

+183
-57
lines changed

lightning/src/chain/channelmonitor.rs

Lines changed: 101 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3823,7 +3823,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
38233823
// First check if a counterparty commitment transaction has been broadcasted:
38243824
macro_rules! claim_htlcs {
38253825
($commitment_number: expr, $txid: expr, $htlcs: expr) => {
3826-
let (htlc_claim_reqs, _) = self.get_counterparty_output_claim_info(funding_spent, $commitment_number, $txid, None, $htlcs, confirmed_spend_height);
3826+
let htlc_claim_reqs = self.get_counterparty_output_claims_for_preimage(*payment_preimage, funding_spent, $commitment_number, $txid, $htlcs, confirmed_spend_height);
38273827
let conf_target = self.closure_conf_target();
38283828
self.onchain_tx_handler.update_claims_view_from_requests(
38293829
htlc_claim_reqs, self.best_block.height, self.best_block.height, broadcaster,
@@ -4722,7 +4722,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
47224722
(htlc, htlc_source.as_ref().map(|htlc_source| htlc_source.as_ref()))
47234723
), logger);
47244724
let (htlc_claim_reqs, counterparty_output_info) =
4725-
self.get_counterparty_output_claim_info(funding_spent, commitment_number, commitment_txid, Some(commitment_tx), per_commitment_option, Some(height));
4725+
self.get_counterparty_output_claim_info(funding_spent, commitment_number, commitment_txid, commitment_tx, per_commitment_claimable_data, Some(height));
47264726
to_counterparty_output_info = counterparty_output_info;
47274727
for req in htlc_claim_reqs {
47284728
claimable_outpoints.push(req);
@@ -4732,75 +4732,119 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
47324732
(claimable_outpoints, to_counterparty_output_info)
47334733
}
47344734

4735+
fn get_point_for_commitment_number(&self, commitment_number: u64) -> Option<PublicKey> {
4736+
let per_commitment_points = &self.their_cur_per_commitment_points?;
4737+
4738+
// If the counterparty commitment tx is the latest valid state, use their latest
4739+
// per-commitment point
4740+
if per_commitment_points.0 == commitment_number {
4741+
Some(per_commitment_points.1)
4742+
} else if let Some(point) = per_commitment_points.2.as_ref() {
4743+
// If counterparty commitment tx is the state previous to the latest valid state, use
4744+
// their previous per-commitment point (non-atomicity of revocation means it's valid for
4745+
// them to temporarily have two valid commitment txns from our viewpoint)
4746+
if per_commitment_points.0 == commitment_number + 1 {
4747+
Some(*point)
4748+
} else {
4749+
None
4750+
}
4751+
} else {
4752+
None
4753+
}
4754+
}
4755+
4756+
fn get_counterparty_output_claims_for_preimage(
4757+
&self, preimage: PaymentPreimage, funding_spent: &FundingScope, commitment_number: u64,
4758+
commitment_txid: Txid,
4759+
per_commitment_option: Option<&Vec<(HTLCOutputInCommitment, Option<Box<HTLCSource>>)>>,
4760+
confirmation_height: Option<u32>,
4761+
) -> Vec<PackageTemplate> {
4762+
let per_commitment_claimable_data = match per_commitment_option {
4763+
Some(outputs) => outputs,
4764+
None => return Vec::new(),
4765+
};
4766+
let per_commitment_point = match self.get_point_for_commitment_number(commitment_number) {
4767+
Some(point) => point,
4768+
None => return Vec::new(),
4769+
};
4770+
4771+
let matching_payment_hash = PaymentHash::from(preimage);
4772+
per_commitment_claimable_data
4773+
.iter()
4774+
.filter_map(|(htlc, _)| {
4775+
if let Some(transaction_output_index) = htlc.transaction_output_index {
4776+
if htlc.offered && htlc.payment_hash == matching_payment_hash {
4777+
let htlc_data = PackageSolvingData::CounterpartyOfferedHTLCOutput(
4778+
CounterpartyOfferedHTLCOutput::build(
4779+
per_commitment_point,
4780+
preimage,
4781+
htlc.clone(),
4782+
funding_spent.channel_parameters.clone(),
4783+
confirmation_height,
4784+
),
4785+
);
4786+
Some(PackageTemplate::build_package(
4787+
commitment_txid,
4788+
transaction_output_index,
4789+
htlc_data,
4790+
htlc.cltv_expiry,
4791+
))
4792+
} else {
4793+
None
4794+
}
4795+
} else {
4796+
None
4797+
}
4798+
})
4799+
.collect()
4800+
}
4801+
47354802
/// Returns the HTLC claim package templates and the counterparty output info
47364803
fn get_counterparty_output_claim_info(
47374804
&self, funding_spent: &FundingScope, commitment_number: u64, commitment_txid: Txid,
4738-
tx: Option<&Transaction>,
4739-
per_commitment_option: Option<&Vec<(HTLCOutputInCommitment, Option<Box<HTLCSource>>)>>,
4805+
tx: &Transaction,
4806+
per_commitment_claimable_data: &[(HTLCOutputInCommitment, Option<Box<HTLCSource>>)],
47404807
confirmation_height: Option<u32>,
47414808
) -> (Vec<PackageTemplate>, CommitmentTxCounterpartyOutputInfo) {
47424809
let mut claimable_outpoints = Vec::new();
47434810
let mut to_counterparty_output_info: CommitmentTxCounterpartyOutputInfo = None;
47444811

4745-
let per_commitment_claimable_data = match per_commitment_option {
4746-
Some(outputs) => outputs,
4747-
None => return (claimable_outpoints, to_counterparty_output_info),
4748-
};
4749-
let per_commitment_points = match self.their_cur_per_commitment_points {
4750-
Some(points) => points,
4812+
let per_commitment_point = match self.get_point_for_commitment_number(commitment_number) {
4813+
Some(point) => point,
47514814
None => return (claimable_outpoints, to_counterparty_output_info),
47524815
};
47534816

4754-
let per_commitment_point =
4755-
// If the counterparty commitment tx is the latest valid state, use their latest
4756-
// per-commitment point
4757-
if per_commitment_points.0 == commitment_number { &per_commitment_points.1 }
4758-
else if let Some(point) = per_commitment_points.2.as_ref() {
4759-
// If counterparty commitment tx is the state previous to the latest valid state, use
4760-
// their previous per-commitment point (non-atomicity of revocation means it's valid for
4761-
// them to temporarily have two valid commitment txns from our viewpoint)
4762-
if per_commitment_points.0 == commitment_number + 1 {
4763-
point
4764-
} else { return (claimable_outpoints, to_counterparty_output_info); }
4765-
} else { return (claimable_outpoints, to_counterparty_output_info); };
4766-
4767-
if let Some(transaction) = tx {
4768-
let revocation_pubkey = RevocationKey::from_basepoint(
4769-
&self.onchain_tx_handler.secp_ctx,
4770-
&self.holder_revocation_basepoint,
4771-
&per_commitment_point,
4772-
);
4773-
4774-
let delayed_key = DelayedPaymentKey::from_basepoint(
4775-
&self.onchain_tx_handler.secp_ctx,
4776-
&self.counterparty_commitment_params.counterparty_delayed_payment_base_key,
4777-
&per_commitment_point,
4778-
);
4779-
4780-
let revokeable_p2wsh = chan_utils::get_revokeable_redeemscript(
4781-
&revocation_pubkey,
4782-
self.counterparty_commitment_params.on_counterparty_tx_csv,
4783-
&delayed_key,
4784-
)
4785-
.to_p2wsh();
4786-
for (idx, outp) in transaction.output.iter().enumerate() {
4787-
if outp.script_pubkey == revokeable_p2wsh {
4788-
to_counterparty_output_info =
4789-
Some((idx.try_into().expect("Can't have > 2^32 outputs"), outp.value));
4790-
}
4817+
let revocation_pubkey = RevocationKey::from_basepoint(
4818+
&self.onchain_tx_handler.secp_ctx,
4819+
&self.holder_revocation_basepoint,
4820+
&per_commitment_point,
4821+
);
4822+
let delayed_key = DelayedPaymentKey::from_basepoint(
4823+
&self.onchain_tx_handler.secp_ctx,
4824+
&self.counterparty_commitment_params.counterparty_delayed_payment_base_key,
4825+
&per_commitment_point,
4826+
);
4827+
let revokeable_p2wsh = chan_utils::get_revokeable_redeemscript(
4828+
&revocation_pubkey,
4829+
self.counterparty_commitment_params.on_counterparty_tx_csv,
4830+
&delayed_key,
4831+
)
4832+
.to_p2wsh();
4833+
for (idx, outp) in tx.output.iter().enumerate() {
4834+
if outp.script_pubkey == revokeable_p2wsh {
4835+
to_counterparty_output_info =
4836+
Some((idx.try_into().expect("Can't have > 2^32 outputs"), outp.value));
47914837
}
47924838
}
47934839

47944840
for &(ref htlc, _) in per_commitment_claimable_data.iter() {
47954841
if let Some(transaction_output_index) = htlc.transaction_output_index {
4796-
if let Some(transaction) = tx {
4797-
if transaction_output_index as usize >= transaction.output.len()
4798-
|| transaction.output[transaction_output_index as usize].value
4799-
!= htlc.to_bitcoin_amount()
4800-
{
4801-
// per_commitment_data is corrupt or our commitment signing key leaked!
4802-
return (claimable_outpoints, to_counterparty_output_info);
4803-
}
4842+
if transaction_output_index as usize >= tx.output.len()
4843+
|| tx.output[transaction_output_index as usize].value
4844+
!= htlc.to_bitcoin_amount()
4845+
{
4846+
// per_commitment_data is corrupt or our commitment signing key leaked!
4847+
return (claimable_outpoints, to_counterparty_output_info);
48044848
}
48054849
let preimage = if htlc.offered {
48064850
if let Some((p, _)) = self.payment_preimages.get(&htlc.payment_hash) {
@@ -4815,7 +4859,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
48154859
let counterparty_htlc_outp = if htlc.offered {
48164860
PackageSolvingData::CounterpartyOfferedHTLCOutput(
48174861
CounterpartyOfferedHTLCOutput::build(
4818-
*per_commitment_point,
4862+
per_commitment_point,
48194863
preimage.unwrap(),
48204864
htlc.clone(),
48214865
funding_spent.channel_parameters.clone(),
@@ -4825,7 +4869,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
48254869
} else {
48264870
PackageSolvingData::CounterpartyReceivedHTLCOutput(
48274871
CounterpartyReceivedHTLCOutput::build(
4828-
*per_commitment_point,
4872+
per_commitment_point,
48294873
htlc.clone(),
48304874
funding_spent.channel_parameters.clone(),
48314875
confirmation_height,

lightning/src/ln/monitor_tests.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3836,3 +3836,85 @@ fn test_lost_timeout_monitor_events() {
38363836
do_test_lost_timeout_monitor_events(CommitmentType::LocalWithLastHTLC, false, true);
38373837
do_test_lost_timeout_monitor_events(CommitmentType::LocalWithLastHTLC, true, true);
38383838
}
3839+
3840+
#[test]
3841+
fn test_ladder_preimage_htlc_claims() {
3842+
// Tests that when we learn of a preimage via a monitor update we only claim HTLCs with the
3843+
// corresponding payment hash. This test is a reproduction of a scenario that happened in
3844+
// production where the second HTLC claim also included the first HTLC (even though it was
3845+
// already claimed) resulting in an invalid claim transaction.
3846+
let chanmon_cfgs = create_chanmon_cfgs(2);
3847+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
3848+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
3849+
let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);
3850+
3851+
let node_id_0 = nodes[0].node.get_our_node_id();
3852+
let node_id_1 = nodes[1].node.get_our_node_id();
3853+
3854+
let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
3855+
3856+
let (payment_preimage1, payment_hash1, _, _) = route_payment(&nodes[0], &[&nodes[1]], 1_000_000);
3857+
let (payment_preimage2, payment_hash2, _, _) = route_payment(&nodes[0], &[&nodes[1]], 1_000_000);
3858+
3859+
nodes[0].node.force_close_broadcasting_latest_txn(&channel_id, &node_id_1, "test".to_string()).unwrap();
3860+
check_added_monitors(&nodes[0], 1);
3861+
check_closed_broadcast(&nodes[0], 1, true);
3862+
let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message: "test".to_string() };
3863+
check_closed_event(&nodes[0], 1, reason, false, &[node_id_1], 1_000_000);
3864+
3865+
let commitment_tx = {
3866+
let mut txn = nodes[0].tx_broadcaster.txn_broadcast();
3867+
assert_eq!(txn.len(), 1);
3868+
txn.remove(0)
3869+
};
3870+
mine_transaction(&nodes[0], &commitment_tx);
3871+
mine_transaction(&nodes[1], &commitment_tx);
3872+
3873+
check_closed_broadcast(&nodes[1], 1, true);
3874+
check_added_monitors(&nodes[1], 1);
3875+
check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, false, &[node_id_0], 1_000_000);
3876+
3877+
nodes[1].node.claim_funds(payment_preimage1);
3878+
expect_payment_claimed!(&nodes[1], payment_hash1, 1_000_000);
3879+
check_added_monitors(&nodes[1], 1);
3880+
3881+
let (htlc1, htlc_claim_tx1) = {
3882+
let mut txn = nodes[1].tx_broadcaster.txn_broadcast();
3883+
assert_eq!(txn.len(), 1);
3884+
let htlc_claim_tx = txn.remove(0);
3885+
assert_eq!(htlc_claim_tx.input.len(), 1);
3886+
check_spends!(htlc_claim_tx, commitment_tx);
3887+
(htlc_claim_tx.input[0].previous_output, htlc_claim_tx)
3888+
};
3889+
mine_transaction(&nodes[0], &htlc_claim_tx1);
3890+
mine_transaction(&nodes[1], &htlc_claim_tx1);
3891+
3892+
connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1);
3893+
connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1);
3894+
3895+
expect_payment_sent(&nodes[0], payment_preimage1, None, true, false);
3896+
check_added_monitors(&nodes[0], 1);
3897+
3898+
nodes[1].node.claim_funds(payment_preimage2);
3899+
expect_payment_claimed!(&nodes[1], payment_hash2, 1_000_000);
3900+
check_added_monitors(&nodes[1], 1);
3901+
3902+
let (htlc2, htlc_claim_tx2) = {
3903+
let mut txn = nodes[1].tx_broadcaster.txn_broadcast();
3904+
assert_eq!(txn.len(), 1, "{:?}", txn.iter().map(|tx| tx.compute_txid()).collect::<Vec<_>>());
3905+
let htlc_claim_tx = txn.remove(0);
3906+
assert_eq!(htlc_claim_tx.input.len(), 1);
3907+
check_spends!(htlc_claim_tx, commitment_tx);
3908+
(htlc_claim_tx.input[0].previous_output, htlc_claim_tx)
3909+
};
3910+
assert_ne!(htlc1, htlc2);
3911+
3912+
mine_transaction(&nodes[0], &htlc_claim_tx2);
3913+
mine_transaction(&nodes[1], &htlc_claim_tx2);
3914+
3915+
connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1);
3916+
connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1);
3917+
3918+
expect_payment_sent(&nodes[0], payment_preimage2, None, true, false);
3919+
check_added_monitors(&nodes[0], 1);
3920+
}

0 commit comments

Comments
 (0)