Skip to content

routerrpc: refuse local dispatch on a switchrpc build - #11013

Open
calvinrzachman wants to merge 3 commits into
lightningnetwork:elle-base-branch-payment-servicefrom
calvinrzachman:routerrpc-local-payment-dispatch-guard
Open

routerrpc: refuse local dispatch on a switchrpc build#11013
calvinrzachman wants to merge 3 commits into
lightningnetwork:elle-base-branch-payment-servicefrom
calvinrzachman:routerrpc-local-payment-dispatch-guard

Conversation

@calvinrzachman

Copy link
Copy Markdown
Collaborator

Change Description

The v0.22 release notes say that only one entity, the local router or one external router, may dispatch HTLC attempts through the switch, and that running two "will lead to undefined behavior and potential loss of funds". Nothing enforced it. Both routers draw attempt IDs from independent sequencers into one shared attempt store, so a locally sent payment can collide with an in-flight external attempt and deliver one payment's result to the other caller.

This makes the rule enforced rather than a matter of operational discipline. routerrpc.SendPaymentV2 and SendToRouteV2 now return codes.FailedPrecondition on a switchrpc build; they are the only two RPCs that can originate an attempt. --enable-local-payment-dispatch opts back in for a node that wants the embedded router anyway, with a warning at startup.

Attempt store cleanup and the external-management DB marker stay tied to the build tag rather than to this flag. Both are destructive to an external router's state, so a wrong config value should not be able to reach them.

Alternatives considered

  • Guarding at ControlTower.InitPayment instead of the handlers. That works, since SendOnion never touches the control tower. I chose the handlers to keep the switchrpc concept in the RPC layer. Switch.SendHTLC is not an option, since nothing there says which router is calling.
  • No flag at all, with the guard always on in a switchrpc build. The itest binary is built with that tag, so every itest that sends a payment would fail. Making this work means splitting the itest suite so only some of it builds with the tag.
  • Owner-scoping the attempt store, so the two routers cannot collide at all. This is the real fix and makes the guard unnecessary, but it is a much larger change in the switch core.

NOTE: This would not be needed if lnd supports a new attempt ID format which ensures that multiple routers do not collide within the switch's attempt result store.

Testing

  • New unit tests in lnrpc/routerrpc and config_test.go cover the guard decision, both handlers, and the build-tag validation. There is also a new itest:
make itest icase='local payment dispatch guard'

A gateway that hands its payment lifecycle to an external router over
the switchrpc interface still exposes a working embedded router. Both
draw HTLC attempt IDs from independent sequencers into one shared,
unowned attempt store, so overlapping IDs can deliver one payment's
result to the wrong caller. Nothing prevents that today except operator
discipline, meaning no manual payment RPCs and no tooling that reaches
for routerrpc against a gateway.

Refuse SendPaymentV2 and SendToRouteV2 with codes.FailedPrecondition on
a switchrpc build, and add an enable-local-payment-dispatch flag for a
node that wants the embedded router anyway. The check lives in the two
routerrpc handlers, not an interceptor, since the blocked set is two
methods and a named check keeps rpcperms free of payment semantics.

The flag governs payment origination only. Skipping attempt store
cleanup and marking the HTLC attempt database follow the build tag,
because both are destructive to an external router's state and should
not be reachable from a config file. Startup warns when the two differ.

The itest binary carries the switchrpc tag, so the harness opts every
node in and the guard test opts one back out through a node option.
Assert the guard refuses SendPaymentV2 and SendToRouteV2 on a node that
keeps the switchrpc build default, while QueryRoutes, EstimateRouteFee
and the HTLC interceptor stream keep answering, since an external router
depends on all three.
Record the local payment dispatch guard, and note that it turns the
existing single-dispatcher warning into something the daemon enforces
rather than something an operator has to remember.
@calvinrzachman calvinrzachman changed the title routerrpc: refuse local payment dispatch on a switchrpc build routerrpc: refuse local dispatch on a switchrpc build Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant