Skip to content

Avoid duplicate on-chain HTLC claims after replay#4583

Open
joostjager wants to merge 7 commits intolightningdevkit:mainfrom
joostjager:onchain-claim-replay-fixes
Open

Avoid duplicate on-chain HTLC claims after replay#4583
joostjager wants to merge 7 commits intolightningdevkit:mainfrom
joostjager:onchain-claim-replay-fixes

Conversation

@joostjager
Copy link
Copy Markdown
Contributor

@joostjager joostjager commented Apr 30, 2026

Fixes #4572

@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented Apr 30, 2026

👋 I see @valentinewallace was un-assigned.
If you'd like another reviewer assignment, please click here.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 30, 2026

Codecov Report

❌ Patch coverage is 99.10180% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.20%. Comparing base (42e198c) to head (9e0f886).

Files with missing lines Patch % Lines
lightning/src/chain/channelmonitor.rs 94.59% 2 Missing ⚠️
lightning/src/chain/onchaintx.rs 99.55% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4583      +/-   ##
==========================================
+ Coverage   87.15%   87.20%   +0.04%     
==========================================
  Files         161      161              
  Lines      109251   109518     +267     
  Branches   109251   109518     +267     
==========================================
+ Hits        95215    95501     +286     
+ Misses      11560    11541      -19     
  Partials     2476     2476              
Flag Coverage Δ
fuzzing-fake-hashes 31.17% <51.64%> (+0.01%) ⬆️
fuzzing-real-hashes 22.90% <0.00%> (-0.02%) ⬇️
tests 86.26% <99.10%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@joostjager joostjager self-assigned this Apr 30, 2026
@joostjager joostjager force-pushed the onchain-claim-replay-fixes branch from dca8cfb to 531576d Compare May 1, 2026 10:49
joostjager added 7 commits May 1, 2026 12:54
Apply rustfmt to the chanmon consistency fuzz target.
Have ChannelMonitor hand singular ClaimRequests to OnchainTxHandler.

Convert them to PackageTemplates only after duplicate filtering.

This makes the single-outpoint invariant explicit at that boundary.
Move repeated OnchainTxHandler setup into shared test helpers so the
claim-replay coverage can focus on the behavior under test.
A replayed holder HTLC claim may arrive as a single-outpoint
request after earlier requests were merged into a delayed package.
Check whether an existing delayed package already covers the new
request instead of requiring exact outpoint-set equality.

Add focused OnchainTxHandler coverage and a ChannelMonitor regression
through claim_funds for both current anchor variants.
When a transaction spends one outpoint from a delayed package, the
split outpoint is tracked as a ContentiousOutpoint while it awaits
anti-reorg confirmation. Reject replayed claim requests for those
pending-spent outpoints so they are not added back before the spend
either matures or reorgs out.

Add an OnchainTxHandler regression that replays a holder claim during
that pending-spent window and verifies reorg resurrection still works.
Filter regenerated HTLC claim requests when the commitment output
already has a confirmed spend tracked by the ChannelMonitor.

This keeps replayed preimage updates from recreating claims after
OnchainTxHandler has cleaned up its anti-reorg tracking, without
adding a new persisted outpoint tombstone set.
Hash HTLC claim outpoints in canonical order so the same logical HTLC
set produces the same ClaimId regardless of descriptor order.

Add a unit test covering reversed descriptor order.
@joostjager joostjager force-pushed the onchain-claim-replay-fixes branch from 531576d to 9e0f886 Compare May 1, 2026 11:06
@joostjager joostjager marked this pull request as ready for review May 1, 2026 11:22
@joostjager joostjager changed the title Onchain claim replay fixes Avoid duplicate on-chain HTLC claims after replay May 1, 2026
@joostjager joostjager removed the request for review from valentinewallace May 1, 2026 11:24
@ldk-claude-review-bot
Copy link
Copy Markdown
Collaborator

OK, that's fine. The test uses the correct API.

I've completed my thorough review. The PR is well-structured and the changes are logically sound. I found no significant bugs or security issues. The changes are:

  1. ClaimRequest type - Correctly enforces single-outpoint semantics at the type level
  2. ClaimId::from_htlcs sorting - Correctly makes claim IDs order-independent
  3. is_htlc_output_spent_on_chain - Correctly filters already-resolved HTLCs at the monitor level
  4. is_outpoint_spend_waiting_threshold_conf - Correctly prevents re-adding outpoints during the anti-reorg window
  5. contains_outpoint check in locktimed packages - Correctly handles the case where a single-outpoint replay request matches a multi-outpoint aggregated package

The overall approach of defense-in-depth (monitor-level filtering + handler-level filtering) is sound.

No issues found.

requests.retain(|req| {
let outpoint = req.outpoint();
if self.claimable_outpoints.get(outpoint).is_some() {
if self.is_outpoint_spend_waiting_threshold_conf(outpoint) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this break for reorgs where a counterparty's transaction gets unconfirmed? ie if we have a event awaiting confirmations for the counterparty claiming an outpoint, but then that gets reorg'd out and not replayed, do we still manage to broadcast our own claim (and RBF it)?

I assume that this case is only reachable if we receive a preimage after a counterparty's timeout claim is confirming?


fn is_htlc_output_spent_on_chain(&self, htlc: &HTLCOutputInCommitment) -> bool {
if let Some(transaction_output_index) = htlc.transaction_output_index {
// This is a monitor-level HTLC generation filter. OnchainTxHandler
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So why exactly do we need the duplicate?

// still guards package state for outpoints split out by confirmed
// spends; here we avoid recreating HTLC claim requests once the
// monitor has observed resolution.
self.onchain_events_awaiting_threshold_conf.iter().any(|entry| match entry.event {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as the previous commit - does this break broadcasting our conflicting claim on reorg?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Duplicate Delayed Holder HTLC Claim Replay After Force-Close

4 participants