Skip to content

askrene: opt-in circular (self-rebalance) routes in getroutes#9286

Open
ksedgwic wants to merge 2 commits into
ElementsProject:masterfrom
ksedgwic:circular-askrene4
Open

askrene: opt-in circular (self-rebalance) routes in getroutes#9286
ksedgwic wants to merge 2 commits into
ElementsProject:masterfrom
ksedgwic:circular-askrene4

Conversation

@ksedgwic

@ksedgwic ksedgwic commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

This adds an opt-in allow_circular parameter to askrene-getroutes. When set, source == destination requests a self-rebalance cycle -- a route that leaves the source, traverses the network, and returns -- for moving liquidity between one's own channels. Because the cycles come from the MCF solver, a single call can move liquidity out of several channels and into several others in one pass, rather than rebalancing one channel pair per attempt. Without the flag nothing changes: source == destination is still rejected with the same error as today, so existing callers are unaffected.

The change stays out of the solver. It uses the standard node-splitting construction from network flow: at the gossmap-localmods layer, the source keeps only its outgoing edges and a synthetic sink receives mirrored copies of its incoming edges, so the MCF sees an ordinary source-to-sink problem and the solver child runs unmodified. The commit messages have the details, including how the returned route represents the closing hop.

Motivation: rebalancing tools currently have to do their own routing for circular payments; this lets them use the same solver as everything else. We have been running this patch against live rebalance traffic on mainnet (and signet).

Tests cover the flag-off (unchanged) behavior, a deterministic synthetic cycle, mirror-scid collision handling, and a self-rebalance over a real gossmap snapshot.

Changelog-Added: JSON-RPC: askrene-getroutes has a new allow_circular parameter to request self-rebalance (source == destination) routes.

ksedgwic added 2 commits July 6, 2026 10:38
source == destination is rejected as invalid input: a node cannot pay
itself a normal route.  This adds an opt-in, allow_circular: when set,
source == destination instead requests a self-rebalance cycle -- a route
that leaves source, traverses the network, and returns to source -- for
moving liquidity between one's own channels.

Without allow_circular nothing changes: source == destination still fails
with "source and destination must be different".  Only callers that
explicitly opt in are affected, so this is purely additive for every
existing caller.

The cycle is produced by splitting the source node into two distinct nodes
at the gossmap-localmods layer, before the solver child forks:

  - source itself keeps only its outgoing edges (its real channels, modulo
    any masks the caller applied via layers).
  - A synthetic us_in node receives only incoming edges: one mirror per
    still-enabled (peer -> source) direction, with the real channel's
    capacity, fees, cltv delta and htlc bounds copied over.

The MCF then solves a regular source -> us_in flow.  No direct edge
connects them, so the algorithm must traverse the network, and every route
ends at us_in with a non-empty hop sequence.  The whole change lives in the
request-setup layer in askrene.c; the solver child (MCF / BFS / Dijkstra /
get_flow_paths / find_path_or_cycle / substract_flow / substract_cycle /
routing-cost machinery and route serialisation) runs unmodified, because
us_in is a real entry in the mod-augmented gossmap, indexed in the usual
range and picked up by every loop over gossmap_max_node_idx.

The trailing (peer -> us_in) hop is KEPT in each returned route: its
amount_out_msat is the delivered amount, and its short_channel_id_dir and
node_id_out are sentinel placeholders for the synthetic us_in node.  The
caller replaces them with the real closing channel and its own node id when
building the payment, closing the cycle.

Fake mirror scids count up from 0x0x0, skipping any scid already present in
the gossmap or the request's localmods: block 0 cannot hold a real mainnet
channel, but synthetic gossmaps and layer-created channels can occupy any
scid, and a clash would silently update the occupant instead of creating
the mirror.  Allocation failure (or any other mirror-construction failure)
fails the getroutes command cleanly rather than aborting, since cln-askrene
is an important plugin and an abort would take lightningd down with it.

Changelog-Added: JSON-RPC: `getroutes` `allow_circular` returns a self-rebalance cycle when `source` equals `destination`.
Add integration coverage for the allow_circular self-rebalance path:

- test_getroutes_circular: a focused test over a synthetic gossip store
  with one deterministic cycle (us -> 1 -> 2 -> us).  Asserts that
  source == destination without allow_circular is rejected, and that with
  allow_circular the returned route keeps its trailing synthetic closing
  hop (node_id_out == the fake us_in sentinel, scid == the first mirror
  0x0x0, amount_out_msat == delivered).  Also covers mirror-scid collision
  handling: a layer-created channel squatting on 0x0x0 plus the store's own
  real block-0 channel (gossmap-compress assigns block = index + node1)
  force the allocator to skip to 0x2x0.

- test_real_data_circular: the canonical clboss-xmovefunds shape over a
  real mainnet gossmap snapshot.  Mask the source node down to a single
  drain (source-out) channel and a single distinct fill (dest-in) channel,
  then self-rebalance.  The mask makes the route deterministic at the
  endpoints: every part must leave via the drain channel and return via the
  mirrored fill channel into us_in.

Real hops in the synthetic test are pinned by node id rather than
short_channel_id_dir, since the scids the test gossip store assigns are a
harness-internal detail.

Changelog-None
@ksedgwic ksedgwic requested a review from cdecker as a code owner July 6, 2026 19:42
@ksedgwic

ksedgwic commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

@Lagrang3

@Lagrang3 Lagrang3 self-requested a review July 6, 2026 20:26
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