Skip to content

[wip] Xpay limit path length - #9627

Draft
Lagrang3 wants to merge 8 commits into
ElementsProject:masterfrom
Lagrang3:xpay-limit-path-length
Draft

Lagrang3 wants to merge 8 commits into
ElementsProject:masterfrom
Lagrang3:xpay-limit-path-length

Conversation

@Lagrang3

@Lagrang3 Lagrang3 commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Addresses: #9156
Supersedes: #9539

This PR adds a new constraint into askrene: maxhops, to limit the length of the routes.
Hard constraints of path length is not possible under the current implementation, minimum cost flows
do not allow that. Our approach is to add a constant cost per hop (a similar tweak in renepay #7540 was already deployed)
and scale that cost until the computed routes fit the maxhops constraints.
Fewer hops are expected with a base cost per hops and a getroutes failure if no routing solutions
are found satisfying maxhops constraint.

On xpay we compute the onion budget for each payment as the maximum number of hops we can afford to put inside
an onion message given the hard limit of 1300 bytes for the onion package, payment amount, blockheight and tail
(ie. final payload in the case of bolt11 and blinded path plus final payload in the case of bolt12).
The onion budget is passed down to getroutes so that solutions will not exceed it.
No more "Could not create payment onion: path too long!" errors are expected afterwards, instead we might
get an error from getroutes if no solutions can be found with the maxhops limit.

TODO:

  • unit test onion_budget,
  • implement the cost based maxhops constraint into askrene,
  • integration test for maxhops in askrene,
  • integration test for xpay with heavy final payloads bolt11,
  • integration test for xpay with heavy final payloads bolt12,
  • promote maxhops from a developer option to a user option and add documentation for maxhops,

@Lagrang3 Lagrang3 added this to the v26.12 milestone Oct 14, 2026
@Lagrang3
Lagrang3 marked this pull request as draft October 14, 2026 08:40
@Lagrang3
Lagrang3 force-pushed the xpay-limit-path-length branch 3 times, most recently from 4eda748 to f79367b Compare October 16, 2026 20:50
Add onion budget and payload constructors to the internal xpay API.
Onion budget computation can be used to know a realistic number of hops
that constraints our payment before calling askrene.

Changelog-None

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
@Lagrang3
Lagrang3 force-pushed the xpay-limit-path-length branch 3 times, most recently from bb8a219 to 67679bd Compare October 20, 2026 11:56
@Lagrang3

Copy link
Copy Markdown
Collaborator Author

It was tricky to add a new feature cost (to have maxhops). Some changes were needed in combine_cost_function()
to make the approach extensible to multiple features. 93f6d34

@ksedgwic

ksedgwic commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Overall the per-hop penalty and maxhops do what a rebalancer wants: shorter routes and more requests settled in full.

I looked at what happens with the xrebalance plugin using this algorithm. I ran the askrene commits at 67679bd offline against a mainnet gossip snapshot (about 30,000 channels with real capacities), sending 120 getroutes requests of the kind xrebalance makes when rebalancing (multi-part, 50M to 2.4B msat, 300 and 600 ppm fee budgets), and the same 120 against master. Three results:

  • askrene's child process aborts on about a fifth of the requests (24 of 120): an assertion in combine_cost_function().
  • The fee weighting starts ten times higher than before. That changes fees and success probability on its own, and the changelog does not mention it.
  • maxhops works as described, and a limit of 20 costs nothing.

The abort. combine_cost_function() asserts that the four weights (fees, delay, hop count, probability) sum to exactly 1.0, fabs(total_dial - 1.0) < 1e-30, but renormalize_dials() cannot guarantee that in floating point. With the starting weights (0.1, 0.01, 0.01, 0.88) and the fee weight multiplied by the golden ratio on each over-budget retry, the sum is exact after the first three retries and off by 2.2e-16 after the fourth. Any request still over budget after four retries then kills the child with SIGABRT, and the caller gets code -1 "child died with signal 6" instead of a route or PAY_ROUTE_NOT_FOUND. On master the same requests retried up to ten times and ended with a route or 205. A tolerance of about 1e-9, or renormalizing inside combine_cost_function() instead of asserting, fixes it. With that one change all 120 requests complete.

The fee weight. On master the first solve uses mu = 1 of MU_MAX = 100, so fees weigh 1% against probability 99%. linear_routes() now starts mu at 0.1 with probability at 0.88. To tell this apart from the per-hop change I ran the 120 requests three ways with the assertion fixed: master, the series as is, and the series with mu = 0.01 (probability 0.97). The per-hop factor does what the changelog says at either starting point: routes average 5.5 hops instead of 7.1, the longest is 12 or 13 instead of 19, and 99 requests find routes for the full amount instead of 88. The starting mu is a separate effect. At 0.1 the routes are cheaper, 270 ppm of the delivered amount against 321 on master, but less likely to succeed, median probability_ppm 972,997 against 994,735, and the solver finishes in half the time because it needs fewer retries. At 0.01 the fee rate and probability come back near master's (333 ppm, 983,433) and the shorter routes stay. If the new starting point is intended, a changelog line would help callers that tune maxfee around the old behavior. If not, 0.01 reproduces the old weighting.

maxhops. With limits of 20, 12 and 8 the results were identical or nearly identical to no limit: no route was longer than 13 hops, 12 changed one request's routes and 8 changed seven, with no cost in delivered amount or fee rate. An unreachable limit fails with PAY_ROUTE_NOT_FOUND and "excessive hop count", which a caller that already treats 205 as "no route" handles without change.

@Lagrang3
Lagrang3 requested review from cdecker and ksedgwic October 21, 2026 06:36
Changelog-None

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
Added an homogeneous and constant cost to every hop in the MCF solver.

Changelog-Changed: askrene-getroutes: penalizes every extra hop in every route with an homogeneous and constant probability of failure of 1%, as a consequence askrene will slightly prefer smaller routes.

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
Treat all cost features as the same type of variable.

Changelog-None

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
Use cost dials normalized to 1 to combine the cost of different
features.  This is a generalization of the mu factor combination with
medians ratio applied to more than 2 features: fees and delays.

Changelog-None

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
Changelog-Added: askrene: getroutes: maxhops, a parameter to limit the length of the paths in the solutions.

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
Changelog-Fixed: xpay: Clamp the length of the payment routes using askrene's maxhops option aiming at producing routes that fit the data limits in the onion packet.

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
Changelog-None

Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
@Lagrang3
Lagrang3 force-pushed the xpay-limit-path-length branch from 67679bd to c7dcc9a Compare October 21, 2026 09:57

This branch has not been deployed

No deployments
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.

2 participants