[v0.21.x-branch] Backport #10942: htlcswitch: forward blinded payments addressed by node_id - #11010
Merged
Merged
Conversation
During non-strict forwarding, handlePacketAdd evaluates every candidate channel to the next peer and calls CheckHtlcForward with the sender-requested outgoing SCID (originalOutgoingChanID) for each candidate. That SCID flowed through canSendHtlc into AuxTrafficShaper.ShouldHandleTraffic, so a channel-keyed shaper was asked about the requested channel rather than the candidate actually being evaluated. With parallel channels to a peer this inspects the wrong channel. Key the shaper on l.ShortChanID() (the channel under evaluation) instead. originalScid is retained solely for createFailureWithUpdate / FailAliasUpdate, so the alias-aware channel_update returned to the sender is unchanged and the real SCID handed to the shaper never leaks onto the wire. (cherry picked from commit b166780)
A blinded route may identify the next hop by node ID (next_node_id) rather than by channel, in which case there is no sender-specified outgoing channel to report to an HTLC interceptor. Add an outgoing_node_id field to ForwardHtlcInterceptRequest to carry the next hop's public key for these forwards, and document that outgoing_requested_chan_id then holds a reserved sentinel value so that clients switching on a zero channel ID to detect the exit hop do not misclassify the forward as a final receive. This commit only adds the schema and regenerated stubs; the fields are populated by later commits. (cherry picked from commit 14640a5)
Author
|
Please cherry-pick the changes locally and resolve any conflicts. git fetch origin backport-10942-to-v0.21.x-branch
git worktree add --checkout .worktree/backport-10942-to-v0.21.x-branch backport-10942-to-v0.21.x-branch
cd .worktree/backport-10942-to-v0.21.x-branch
git reset --hard HEAD^
git cherry-pick -x d28a71765bf639bd3917d9b67cdc0afc49209a25 4fd4289a08c34024c44747182b7c668e604e86fd dbc5704070a11c24694598b2101796e8a88348ec a4844ef52299bbe29259d9183164fe041871d18b 32373b76c7b74aba29655aefce25c24ff0c2771a 9c4b8bfec2e35d7fc2810cc3eb9b1162053f79bf da6a40c01d4963df8a462672b423e059d9a859a8 f42b4298992a64d49db0b0ddbf774f68ead089fd
git push --force-with-lease |
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. (cherry picked from commit d28a717)
Some implementations (e.g. Core Lightning) identify the next hop in a blinded route by the next node's ID (next_node_id) instead of a short channel ID. Decode such a hop into a node-ID next hop, the Right of ForwardingInfo.NextHop, holding the next node's public key. The switch resolves that key to one of our channels with the peer in a later commit. BOLT 4 requires a non-final blinded hop to carry exactly one of short_channel_id or next_node_id, so a hop that sets both is rejected. (cherry picked from commit 4fd4289)
Fixes #10937: forward a blinded-route payment when the recipient identifies the next hop by node ID rather than a short channel ID. The htlcPacket carries the decoded next hop 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 filters candidate channels before selection. (cherry picked from commit dbc5704)
Now that the switch forwards blinded hops identified by node ID, a new problem surfaces in the HTLC event stream. A node-ID next hop has no outgoing short channel ID until non-strict forwarding selects one, so a forward that fails before selection still carries outgoingChanID == hop.Exit. getEventType keys the exit hop off that sentinel, so it misclassifies such a failed node-ID forward as a receive, mislabeling the event streamed via SubscribeHtlcEvents (a forwarding failure reported as a receive failure). Two paths reach getEventType before an SCID is selected: the fail packet built by failAddPacket and the resolution packet built by resolve, both of which dropped the decoded next hop. Carry outgoingHop into both, and classify a Right (node-ID) outgoingHop as a forward before the hop.Exit check. A node-ID next hop is always a forward, never the exit hop. (cherry picked from commit a4844ef)
…ceptor When the switch forwards a blinded hop identified by node ID, it has not yet resolved a concrete outgoing channel at interception time. Expose the next hop to the interceptor: InterceptedForward.Packet() reports the packet's outgoing channel as-is (hop.Exit, since none is selected yet) and carries the requested pubkey in OutgoingNodeID. At the RPC boundary, forwardInterceptor.onIntercept maps a node-ID hop to the reserved NodeIDForwardSCID sentinel in outgoing_requested_chan_id and the pubkey in outgoing_requested_node_id, so a client switching on a zero channel ID to detect the exit hop does not misread the forward as a final receive. The sentinel is a wire-only concern, applied where the request is built rather than in the switch's internal InterceptedPacket, which stays truthful (OutgoingNodeID.IsSome() is the node-ID discriminator). (cherry picked from commit 32373b7)
ziggie1984
force-pushed
the
backport-10942-to-v0.21.x-branch
branch
from
July 30, 2026 14:35
063eecb to
88235fb
Compare
Collaborator
|
Resolved the automated backport conflict and force-pushed the completed backport. Reviewer note: only three original commits required manual v0.21 compatibility handling:
The Validation completed with: git diff --check origin/v0.21.x-branch...HEAD
go test . ./htlcswitch/hop ./contractcourt ./lnrpc/routerrpc |
Extend the on-chain interceptor path in the witness beacon to expose a node-ID next hop, mirroring the off-chain path. A node-ID next hop has no outgoing channel of its own, so the beacon reports hop.Exit as the outgoing channel (via ForwardingInfo.NextHopChannel().UnwrapOr) and the requested next node's public key. The RPC boundary maps that to the NodeIDForwardSCID sentinel so the forward is not misread as a final receive. This is the requested next hop, not the channel eventually selected by non-strict forwarding, so the beacon deliberately does not resolve it against the circuit map. (cherry picked from commit 9c4b8bf)
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.
(cherry picked from commit da6a40c)
(cherry picked from commit f42b429)
ziggie1984
force-pushed
the
backport-10942-to-v0.21.x-branch
branch
from
July 30, 2026 14:47
88235fb to
8436653
Compare
ziggie1984
marked this pull request as ready for review
July 30, 2026 14:50
Collaborator
|
depends on #11012 which fixes the static check |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backport of #10942
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 onlynext_node_idis present, and the switch resolves it through the same non-strictgetLinkspath 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.