Skip to content

contractcourt: fix HTLC success resolver phantom-input bug (#10840)#10869

Draft
yyforyongyu wants to merge 6 commits into
lightningnetwork:masterfrom
yyforyongyu:fix-10840-success-resolver
Draft

contractcourt: fix HTLC success resolver phantom-input bug (#10840)#10869
yyforyongyu wants to merge 6 commits into
lightningnetwork:masterfrom
yyforyongyu:fix-10840-success-resolver

Conversation

@yyforyongyu
Copy link
Copy Markdown
Member

Fixes #10840.

This fixes the HTLC success resolver path that could treat any spend of the original HTLC output as our own second-level HTLC success transaction. If a remote timeout spend is observed instead, the resolver now validates the spender output against the stored sweep descriptor and fails the final HTLC outcome instead of handing a phantom output to the sweeper.

The incoming contest resolver also avoids launching the success resolver after CLTV expiry and passes the current height into the immediate invoice-registry lookup.

Companion sweeper blast-radius fix: #10842.

Testing:

  • GOWORK=off go test ./contractcourt
  • git diff --check master..HEAD
  • make fmt in a clean temporary worktree
  • make lint in a clean temporary worktree
  • GOWORK=off go test ./contractcourt -run '^$' at each commit

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a bug in the HTLC success resolver where foreign spends of commitment HTLC outputs were incorrectly treated as valid success transactions. By implementing stricter validation of the spending transaction's output and ensuring that expired HTLCs do not trigger the success resolution path, the changes improve the robustness of the contract resolution process and prevent unnecessary sweeper activity.

Highlights

  • HTLC Success Resolver Validation: Added validation to the HTLC success resolver to ensure that the spending transaction of an HTLC output matches the expected second-level success output, preventing phantom inputs from being passed to the sweeper.
  • Expiry Handling: Updated the incoming contest resolver to skip launching the success path for HTLCs that have already reached their CLTV expiry.
  • Test Coverage: Introduced new unit tests to verify that the resolver correctly rejects foreign spends and handles expired HTLCs appropriately.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@yyforyongyu yyforyongyu marked this pull request as draft June 1, 2026 20:46
@github-actions github-actions Bot added the severity-critical Requires expert review - security/consensus critical label Jun 1, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 1, 2026

🔴 PR Severity: CRITICAL

Automated classification | 3 files | 139 lines changed

🔴 Critical (2 files)
  • contractcourt/htlc_incoming_contest_resolver.go - on-chain HTLC incoming contest resolution logic
  • contractcourt/htlc_success_resolver.go - on-chain HTLC success resolver logic
🟢 Low (1 file)
  • docs/release-notes/release-notes-0.22.0.md - release notes documentation

Analysis

This PR modifies two files in the contractcourt package — htlc_incoming_contest_resolver.go and htlc_success_resolver.go — which implement on-chain dispute resolution for HTLCs. These resolvers manage critical state machine transitions for fund recovery during channel force-closes, making them among the highest-risk code paths in the daemon. Changes here require expert review to ensure correct handling of on-chain settlement and to avoid any potential loss of funds.

Severity bump check: 3 non-test/non-generated files (threshold: >20), 139 lines changed (threshold: >500), single distinct critical package — no bump triggered.

Three test/mock files (*_test.go, mock_*.go) were excluded from file and line counts per classification rules.


To override, add a severity-override-{critical,high,medium,low} label.
<!-- pr-severity-bot -->

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request addresses an issue where an incoming HTLC resolver could incorrectly treat a foreign spend of a commitment HTLC output as its own success transaction. It introduces validation of the spending transaction's expected output before sweeping, and checkpoints the resolver as failed (timeout) if a foreign spend is detected. Additionally, the resolver now avoids launching the success path for incoming HTLCs that are already expired by checking the best block height at launch. Comprehensive unit tests have been added to verify these behaviors. No review comments were provided, so there is no additional feedback.

@saubyk saubyk added this to the v0.21.1 milestone Jun 1, 2026
@saubyk saubyk added this to v0.21 Jun 1, 2026
@saubyk saubyk moved this to In progress in v0.21 Jun 1, 2026
testHtlcSuccess(t, twoStageResolution, checkpoints)
}

func TestHtlcSuccessResolverRejectsForeignSpend(t *testing.T) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

all the methods in this commit are missing godoc, plus the superficial AAA comments - we should explain arrange what, act how, assert what

ctx.waitForResult(false)
}

func TestHtlcIncomingResolverLaunchSkipsPreimageAfterExpiry(t *testing.T) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

same here, missing godocs

the close transaction is actually broadcast, and
`WaitingCloseChannel.ClosingTx` is never empty.

* [Fixed an issue](https://github.com/lightningnetwork/lnd/issues/10840)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this should be in release 0.21.1

Make the existing two-stage success resolver test use an output that
matches the stored sweep descriptor. This keeps the fixture consistent
with the resolver invariant enforced by the following commit.
The success resolver previously treated any spend of the original HTLC
outpoint as confirmation of our own second-level success transaction.
That allowed a remote timeout reclaim to be misclassified as a phantom
second-level output offered to the sweeper.

Validate the spending transaction output against SweepSignDesc.Output
before proceeding. If the output does not match, checkpoint the resolver
as failed instead of registering the derived outpoint.
Add regression coverage for both success-resolver paths that can observe
a foreign spend: the initial resolve path and the restart path after
output incubation. The tests assert that no phantom second-level output
is handed to the sweeper and that the final HTLC outcome is failed.
The incoming contest resolver launch-time invoice-registry lookup
passed currentHeight=0, bypassing registry-side expiry checks for
immediate resolutions.

Fetch the current best height in Launch and pass it into the immediate
registry lookup. Known preimages can still launch the success resolver
after expiry so direct preimage sweeps are not suppressed on restart.
Add coverage for Launch still using an already-known preimage after HTLC
expiry, matching restart behavior where the preimage was learned before
shutdown.

Also assert launch-time registry lookups use the current chain height
instead of zero.
Document the user-visible fix for issue lightningnetwork#10840 in the v0.21.1 release
notes. The note covers both the foreign-spend validation and the
launch-time registry height fix.
@yyforyongyu yyforyongyu force-pushed the fix-10840-success-resolver branch from f82a83c to 0a2f9d3 Compare June 2, 2026 14:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

severity-critical Requires expert review - security/consensus critical

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

[bug]: sweeper batched 4 force-close outputs into invalid tx, then removed all inputs as State=Fatal

2 participants