Skip to content

[v0.20.x-branch] Backport #10895: htlcswitch: separate onchain and offchain intercpeted HTLCs cleanly#10933

Merged
ziggie1984 merged 5 commits into
v0.20.x-branchfrom
backport-10895-to-v0.20.x-branch
Jun 27, 2026
Merged

[v0.20.x-branch] Backport #10895: htlcswitch: separate onchain and offchain intercpeted HTLCs cleanly#10933
ziggie1984 merged 5 commits into
v0.20.x-branchfrom
backport-10895-to-v0.20.x-branch

Conversation

@github-actions

Copy link
Copy Markdown

Backport of #10895


Change Description

Fixes #10892

Look at the first commit which shows via itests the failure cases of the old Interceptor Implementation

The second commit introduces a new interface and distingishes between onchain and offchain HTLC for the interceptor. It does not change any public interface.

@github-actions github-actions Bot added this to the v0.21.1 milestone Jun 26, 2026
@github-actions

Copy link
Copy Markdown
Author

Please cherry-pick the changes locally and resolve any conflicts.

git fetch origin backport-10895-to-v0.20.x-branch
git worktree add --checkout .worktree/backport-10895-to-v0.20.x-branch backport-10895-to-v0.20.x-branch
cd .worktree/backport-10895-to-v0.20.x-branch
git reset --hard HEAD^
git cherry-pick -x 9b31ba83ef52e2d2800f53564f44b9efd4a75ca3 eb1193f80bbe5f0ec2f6618657f1deada5f6561c 98da7b4a56a75ba2dbf47391d43229fd9696e98a 8909c2fbf5535b81ee18c4f4e7fe0160ca43e725 9c5f32a2ec8eab0760a46c3d0357d4127794425f
git push --force-with-lease

Add coverage for held forwards that move on chain after the
incoming channel force closes.

The restart case exercises the path where Bob loses the in-memory
held set and contractcourt re-offers the HTLC through the witness
beacon. The no-restart case keeps the original off-chain hold and
proves that settlement must still reach the on-chain resolver.

(cherry picked from commit 9b31ba8)
Store held forwards as off-chain or on-chain entries instead of a raw
InterceptedForward map. Off-chain entries keep the existing resume, fail,
settle and auto-fail behavior. On-chain entries are settle-only and
expire by pruning local interceptor state.

When contractcourt re-offers a circuit that is already held off-chain,
replace the stored entry with the on-chain forward so a later SETTLE
reaches the witness beacon instead of the old link mailbox path.

Also set the on-chain interceptor deadline to the HTLC refund timeout.
This keeps the public interceptor deadline populated while ensuring only
off-chain held entries use that value to fail back.

Only off-chain held HTLCs can be released when an optional interceptor
disconnects, because they can resume into the link forwarding flow.

On-chain held HTLCs have no link flow to resume. Keep them in the held
set so a reconnecting interceptor can replay and settle them while
contractcourt waits for the preimage or on-chain expiry.

Use distinct internal deadline types for off-chain auto-fail heights and
on-chain settlement deadlines instead of overloading the intercepted packet
field.

Project both variants back into the existing router RPC auto_fail_height
field to preserve wire compatibility. Reject mismatched held HTLC deadline
types in tests.

On-chain intercepted HTLCs can only be settled. Resume and fail actions
already return concrete errors through the on-chain intercepted forward, so
let those errors propagate to the interceptor client instead of converting
them to success.

Keep the held entry tracked on these errors so the client can reconnect and
settle the HTLC later.

(cherry picked from commit eb1193f)
Release the preimage beacon lock before invoking the on-chain
interceptor. The interceptor path can block on the htlcswitch event
loop, while resolution of another held on-chain HTLC can call back
into the beacon to add a preimage.

If interceptor delivery fails after the subscriber was registered,
cancel the subscription before returning the error.

On-chain held entries are replay handles for the interceptor while
contractcourt waits for a preimage or on-chain expiry. Once the resolver
tears down, keeping the handle until the refund timeout can replay a stale
HTLC to a reconnecting interceptor.

Thread a dedicated cleanup signal from the witness subscription cancel path
back through the interceptable switch event loop. The held set only removes
on-chain entries for that signal, leaving off-chain entries under the link
flow lifecycle.

(cherry picked from commit 98da7b4)
routerrpc: document on-chain interceptor responses
(cherry picked from commit 8909c2f)
(cherry picked from commit 9c5f32a)
@ziggie1984 ziggie1984 force-pushed the backport-10895-to-v0.20.x-branch branch from fa4aa7a to cf20de9 Compare June 26, 2026 17:32
@ziggie1984 ziggie1984 marked this pull request as ready for review June 26, 2026 17:33
@ziggie1984

Copy link
Copy Markdown
Collaborator
  • de8ca73 itest: cover on-chain interceptor settlement

    • Had a merge conflict in itest/list_on_test.go.
    • Cause: upstream inserted the new test entries before delete forwarding history, but that test is not present on v0.20.x.
    • Resolution: kept the two new forward-interceptor test entries and omitted the unrelated delete forwarding history entry.
  • b32e432 htlcswitch: track held HTLC source

    • No manual merge conflicts.
  • 7a42b56 witnessbeacon: avoid interceptor deadlock

    • No manual merge conflicts.
  • bc8463a routerrpc: add clarifying docs for the intercepted forward

    • No manual merge conflicts.
  • cf20de9 docs: add v0.20.2 release note

    • No manual merge conflicts.
    • Backport adjustment: registered the release note in docs/release-notes/release-notes-0.20.2.md instead of the upstream v0.21.1 release-note file.
    • Also updated the commit title from the upstream v0.21.1 wording to v0.20.2.

@github-actions github-actions Bot added the severity-critical Requires expert review - security/consensus critical label Jun 26, 2026
@github-actions

Copy link
Copy Markdown
Author

🔴 PR Severity: CRITICAL

htlcswitch/*, server.go | 9 files | 695 lines changed

🔴 Critical (4 files)
  • htlcswitch/held_htlc_set.go - HTLC forwarding state machine (held HTLC set logic)
  • htlcswitch/interceptable_switch.go - HTLC forwarding, payment routing state machine
  • htlcswitch/interfaces.go - Core htlcswitch interfaces
  • server.go - Core server coordination
🟠 High (1 file)
  • lnrpc/routerrpc/forward_interceptor.go - RPC/API layer for HTLC interceptor
🟡 Medium (3 files)
  • lnrpc/routerrpc/router.proto - API definition change (*.proto)
  • lnrpc/routerrpc/router.swagger.json - API schema change
  • witness_beacon.go - Uncategorized Go file (root package)
🟢 Low (1 file)
  • docs/release-notes/release-notes-0.20.2.md - Release notes documentation
Excluded from count (5 files)
  • htlcswitch/held_htlc_set_test.go - test file
  • htlcswitch/switch_test.go - test file
  • itest/list_on_test.go - integration test file
  • itest/lnd_forward_interceptor_test.go - integration test file
  • lnrpc/routerrpc/router.pb.go - auto-generated protobuf file

Analysis

This PR modifies core htlcswitch package files — specifically held_htlc_set.go and interceptable_switch.go, which implement the HTLC forwarding and interception state machine. These are among the most security-sensitive components in lnd, as bugs here can result in loss of funds (e.g., incorrect held HTLC state, improper interception lifecycle management). The change to server.go and witness_beacon.go adds additional surface.

The PR also exceeds 500 non-test lines changed (695 lines across 9 non-test, non-generated files), which triggers a severity bump — though the classification was already CRITICAL due to the htlcswitch changes.

Expert review is recommended, with particular attention to state transitions in the held HTLC set and the interceptable switch logic.


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

@yyforyongyu yyforyongyu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@ziggie1984

Copy link
Copy Markdown
Collaborator

Flake fix is not related to this change but still tracked here: #10938

@ziggie1984 ziggie1984 merged commit bf8df0a into v0.20.x-branch Jun 27, 2026
34 of 35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog severity-critical Requires expert review - security/consensus critical

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants