The pre-v26.06 getroutes compatibility fallback read the deprecated
per-hop fields (next_node_id / amount_msat / delay), whose values CLN
defines as the in-side of each hop -- one hop shifted from the
out-side values (node_id_out / amount_out_msat / cltv_out, shipped in
v26.06) that sendpay routes must be built from. On stock CLN
v24.11..v26.04 the fallback therefore built mispriced routes: small
overpays on the routes that survived, spurious FEE_INSUFFICIENT /
INCORRECT_CLTV_EXPIRY failures on the rest, and -- worst -- those
failures hard-excluded healthy channels in the persistent
failure-learning layer for hours, compounding across restarts.
Drop the fallback entirely and enforce the requirement twice:
- Initiator gains a CLN version gate, ordered before the database and
signer steps so a refusal leaves no on-disk trace: a getinfo
version older than v26.06 logs a detailed Error and aborts. The
new clboss-skip-cln-version-check flag bypasses the gate for
operators whose older CLN carries a backport of the v26.06
getroutes fields (the version string alone cannot show that); an
unparseable version string warns and proceeds rather than locking
out custom builds.
- The three getroutes parse sites (FundsMover/Attempter,
ActiveProber, ChannelCandidateMatchmaker) verify the new fields on
every response and fail the attempt with a clear Error if absent,
so a mistakenly bypassed gate degrades into loud per-attempt
failures rather than shifted routes.
CHANGELOG.md gains a prominent BREAKING entry; README documents the
requirement and the escape hatch. Users on older CLN releases stay
on CLBOSS 0.16.x, which uses the legacy getroute/pay APIs those
versions still provide.
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.
Continuation of the v26.06 migration started in the Dowser
commit. Two more getroute call sites:
ChannelCandidateMatchmaker.cpp and ActiveProber.cpp. Both use
maxparts=1 since each wants a single route, not a flow estimate.
ActiveProber probes the local node's own outbound liquidity, so
it uses layers=["auto.localchans","auto.sourcefree"] per the
standardized recipe. Matchmaker probes from a remote source
(a candidate patron) to a remote target, so it passes an empty
layers array -- auto.localchans would inject our private local
channels into a foreign source, and auto.sourcefree would zero
out the source's outgoing fees, either of which could make
askrene pick a patron the proposal cannot actually reach via
public topology. maxfee_msat is 1% of the probe amount in both
cases.
Matchmaker is a near-clean swap. The patron id (route[0].id in
the old getroute shape) is read from routes[0].path[0] in the
new getroutes shape; at the parse site we bridge the v26.06+
name (node_id_out) with its deprecated pre-v26.06 predecessor
(next_node_id) via has()/ternary, so the code works on either
CLN flavor.
ActiveProber is more involved. The previous code stashed
res["route"] as a Jsmn::Object and later appended hops from it
directly into the sendpay route parameter, relying on the fact
that the old getroute hop shape (id/channel/direction/
amount_msat/delay/style) was already sendpay-compatible. The
new getroutes path[] shape is NOT directly sendpay-compatible
(different field names, short_channel_id_dir encodes scid and
direction together), so we now extract path[0] into typed
values (id1, chan1, direction1, amount1, delay1) and rebuild
the sendpay hop1 explicitly from those. The Jsmn::Object route
member is dropped. The hop-field reads in ActiveProber bridge
between v26.06+ (node_id_out / amount_out_msat / cltv_out) and
the deprecated pre-v26.06 names (next_node_id / amount_msat /
delay) via the same has()/ternary pattern.
short_channel_id_dir splits on '/' with an explicit npos check;
a missing slash throws Jsmn::TypeError so the surrounding parse-
error log path catches it cleanly. Without the check,
sdir.substr(0, npos) and sdir.substr(npos + 1) would feed
malformed input into Ln::Scid (which throws
std::invalid_argument, not caught by the Jsmn::TypeError
handler) or into std::stoul (silently producing a wrong
direction).
ActiveProber also drops its vestigial exclude=[self_id]
parameter: askrene's source/destination model naturally excludes
self when source != self, which is always the case here (the
probe always flows from peer outward, never back through us).
Three of the four getroute call sites in CLBOSS are now on
getroutes; the fourth (FundsMover/Attempter) is deferred to PR2
because its exclude-vector pattern encodes failure feedback from
real payment attempts and warrants a redesign around
askrene-inform-channel rather than a mechanical port.