Re-introduces the second rebalancing track (Track B, "xrebalance": the
circular askrene min-cost-flow rebalancer) into the shared selection and
layer infrastructure, so it can coexist with the classic track in one
binary and be chosen at runtime.
- Boss::RebalanceMode gains the xrebalance value (+ to_string /
from_string arms and the doc bullet), selectable via
clboss-rebalance-mode alongside classic and off.
- Boss::Mod::AskreneLayer gains xrebalance_layer_name
("clboss-xrebalance"), the persistent askrene layer the xrebalance
code paths accumulate probe knowledge into. Kept distinct from the
"clboss" layer so the two tracks' learning does not commingle while
both run side by side.
- clboss-rebalance-mode help text now lists xrebalance.
This is only the seam: nothing drives xrebalance yet. The XMoveFunds
primitive and the XRebalancer driver land in following commits. The
classic rebalancers already gate on mode == classic, so selecting
xrebalance simply quiesces them (like off) until the driver arrives.
The getroute -> getroutes migrations dropped the legacy
exclude=[self_id] argument in the two probing modules whose askrene
source is a remote node. Nothing replaced it: askrene's gossmap
includes our public channels like anyone else's, and an empty layers
array applies no exclusions. Consequences:
- Dowser capacity probes could route part of the candidate->patron
flow through our own node, counting our own liquidity toward a
candidate's capacity -- retaining weak candidates and over-sizing
the channels ChannelCreator opens.
- ActiveProber probes could pick path[0] = peer->us, degenerating
into a circular us->peer->us payment that measures our own shared
channel's peer->us balance instead of the peer's outward reach,
with SendpayResultMonitor crediting the peer destination_reached
for it.
Introduce AskreneLayer::self_layer_name ("clboss-self"): a tiny
persistent layer whose only content is our node in disabled_nodes,
maintained by AskreneLayer::ensure_self_layer() (idempotent create,
deduped disable). Both modules resolve it before probing and name it
in their getroutes layers array; when askrene is unavailable they
probe without it, as before. Kept separate from the clboss layer --
whose disabled_nodes also carries self -- because that layer's
learned constraints would bias what the probes measure.
askrene's get_constraints folds every constraint for a (layer, scid-dir) down
to a single tightest [min, max] at query time, so the per-HTLC stream of
inform-channel writes only bloats the layer. Hot rebalance corridors accreted
hundreds-to-thousands of dominated min_msat copies -- one survey found a single
channel-direction holding 5026 constraints -- every one of which askrene must
re-fold on each getroutes through that dir.
Coalesce at the inform_channel chokepoint: per (layer, scid-dir, inform-kind)
keep the tightest bound emitted in the current time bucket and write through
only on a new bucket (a keep-alive against the layer aging) or a tightening.
Dropping a dominated write is lossless -- it is exactly the entry
get_constraints discards when it folds. On the surveyed hot set this is a ~42x
depth reduction.
The bucket length is a fixed fraction (1/12) of the layer aging window
(clboss-classic-layer-age-secs), so it always stays well under the aging window
-- the once-per-bucket keep-alive refreshes a constraint before it can age out
-- and the aging window is then always exactly 12 buckets whatever its value,
making the prune and the depth floor scale-invariant. 30 minutes at the default
6h aging. FundsMover feeds the live value through set_aging_window_secs from its
clboss-classic-layer-age-secs handler, so a setconfig retunes the bucket
immediately.
A small amortized prune drops cache entries idle past the aging window so the
cache does not grow with the set of channel-directions seen over the process
lifetime. inform_coalesce_emit is factored out as a pure decision; the test
covers the dominance and oscillation cases plus a behavioural drop test.
Introduces Boss::Mod::AskreneLayer, a small shared module of free
functions that wrap the askrene layer-mutation RPCs against a named
layer. This is the common infrastructure that the classic rebalancer
(FundsMover) and ActiveProber write their routing feedback into; both
producers land in following commits.
The layer name follows the xpay convention -- the persistent
shared-knowledge layer is named after the owning plugin.
clboss_layer_name = "clboss"; every CLBOSS writer targets this single
layer so downstream getroutes calls that include it in their layers
array benefit from the accumulated knowledge.
Functions:
- inform_channel_constrained / inform_channel_unconstrained:
askrene-inform-channel with inform=constrained / =unconstrained,
recording a recent could-not-push upper bound on a directed
channel, or clearing it.
- disable_node: askrene-disable-node, steering routes away from a
node.
- is_node_disabled: reads askrene-listlayers to test whether a node
is already disabled in the layer (lets writers dedup).
- update_channel: askrene-update-channel, applying an xpay-style
channel_update refresh.
All writers silently swallow RpcError: if the layer is unavailable
(e.g. CLN without askrene layers) the call degrades to a no-op rather
than crashing the caller -- degraded learning beats a crashed plugin.
A unit test (tests/boss/test_askrene_layer) covers each helper's method
and JSON shape plus the silent-RpcError-swallow contract, using the
socketpair MockRpcServer pattern.