Skip to content

htlcswitch: forward blinded payments addressed by node_id#10942

Draft
bitromortac wants to merge 4 commits into
lightningnetwork:masterfrom
bitromortac:2606-fix-10937-a
Draft

htlcswitch: forward blinded payments addressed by node_id#10942
bitromortac wants to merge 4 commits into
lightningnetwork:masterfrom
bitromortac:2606-fix-10937-a

Conversation

@bitromortac

Copy link
Copy Markdown
Collaborator

Fixes #10937.

When lnd is the introduction node of a blinded path, the recipient may identify the next hop by next_node_id instead of a short_channel_id. lnd only handled the channel-ID case and failed such forwards with next SCID not set for non-final blinded hop.

The next hop now becomes an fn.Either[ShortChannelID, pubkey]: decode yields the peer's key when only next_node_id is present, and the switch resolves it through the same non-strict getLinks path that already load-balances across a peer's channels. outgoingChanID (the circuit key) stays a short channel ID, set once a channel is chosen, so private and alias channels need no special handling.

Alternative considered

Resolving the node ID to a channel at decode time is smaller but makes onion decoding depend on live link state and re-implements alias handling the switch already owns. Resolving in the switch keeps decode pure and is correct for alias channels by construction.

The forwarding next hop is currently always a short channel ID. To allow a
blinded route to identify the next hop by node ID instead, change
ForwardingInfo.NextHop to fn.Either[lnwire.ShortChannelID, [33]byte], where
the Left is the outgoing channel ID and the Right (wired up in a follow-up
commit) is the next node's public key.

This commit is a pure representational change with no behavioural effect:
every next hop is still a channel ID. The Either is encapsulated behind
ForwardingInfo methods so callers never destructure it directly: IsExit()
is the single source of truth for exit-hop detection (used by the link and
the contract court) and NextHopChannel() yields the outgoing SCID.
Fixes lightningnetwork#10937: forward a blinded-route payment when the
recipient identifies the next hop by node ID (next_node_id) instead of a
short channel ID, as some implementations (e.g. Core Lightning) do.

The blinded-route decode now emits the next node's public key (the Right of
ForwardingInfo.NextHop) when the recipient data carries next_node_id but no
short_channel_id. The htlcPacket carries this through to the switch, whose
handlePacketAdd resolves the pubkey to the peer's links via getLinks() and
lets the existing non-strict forwarding logic load-balance across the peer's
channels. outgoingChanID stays a ShortChannelID -- it is the persisted
CircuitKey -- and is set to the selected channel after non-strict selection;
the circular-route check is deferred to that point for the pubkey case.
Add integration tests for an lnd introduction node forwarding a blinded
payment whose non-final hops identify the next hop by node ID (next_node_id)
rather than a short channel ID, as produced by other implementations:

  - testBlindedRouteNextNodeID: the outgoing channel is public.
  - testBlindedRouteNextNodeIDPrivateChannel: the outgoing channel is
    private, so the node ID resolves to an SCID alias.
  - testBlindedRouteNextNodeIDRestart: the introduction node is restarted
    while the HTLC is in flight, exercising forwarding-package replay and
    re-decode of the node-ID blinded hop.
@github-actions github-actions Bot added the severity-critical Requires expert review - security/consensus critical label Jun 30, 2026
@github-actions

Copy link
Copy Markdown

PR Severity: CRITICAL

Highest-severity file determines level | 10 files (excl. tests) | ~259 lines changed (excl. tests)

CRITICAL files (8):

  • contractcourt/htlc_incoming_contest_resolver.go - on-chain HTLC dispute resolution
  • htlcswitch/hop/forwarding_info.go - HTLC forwarding, payment routing state machine
  • htlcswitch/hop/iterator.go - HTLC hop iterator
  • htlcswitch/hop/payload.go - HTLC payload handling
  • htlcswitch/link.go - HTLC link (channel forwarding)
  • htlcswitch/mock.go - htlcswitch package mock helpers
  • htlcswitch/packet.go - HTLC packet definitions
  • htlcswitch/switch.go - HTLC switch (core payment routing state machine)

MEDIUM files (1):

  • witness_beacon.go - root-level Go file not in a categorized critical/high package

LOW files (1):

  • docs/release-notes/release-notes-0.22.0.md - release notes documentation

Analysis:

This PR touches two distinct critical packages (contractcourt and htlcswitch), independently triggering CRITICAL severity. The bulk of changes are in htlcswitch/hop/ and htlcswitch/switch.go, at the heart of Lightning payment forwarding. The large itest addition (lnd_route_blinding_test.go, +417 lines) is excluded as a test file. Expert review of htlcswitch and contractcourt changes is required.

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

@saubyk saubyk added this to the v0.21.2 milestone Jun 30, 2026
@saubyk saubyk added this to v0.21 Jun 30, 2026
@saubyk saubyk moved this to In progress in v0.21 Jun 30, 2026
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.

LND 0.21-beta blinded path forwarding failure with CLN BOLT12 offer

2 participants