Delete XMoveFunds (the clboss-xmovefunds circular-payment primitive)
and the machinery that existed to serve it: XRebalanceHistory, the
Predict/Predictor persistence forecaster, the AskreneUpdates
learned-updates store, Ln/OnionError, and the contrib analysis
scripts (view/survey/survival). The external xrebalance plugin is
the executor going forward; it owns constraint knowledge, failure
feedback, and routing, so the in-clboss brain had no consumer left:
the predictor's only output was walls/floors informed into an
askrene layer that only XMoveFunds routed against, and every
AskreneUpdates message had XMoveFunds as its sole producer and
consumer.
XRebalancer loses the executor dispatch: execute_cycle now is the
plugin path, and the xmovefunds response parser goes with the arm.
The mode enum shrinks to xrebalance2/off. XRebalancePartMonitor is
untouched -- it was already attribution-only and remains how plugin
transfers reach EarningsTracker.
AskreneLayer shrinks to the self-exclusion core (self_layer_name,
ensure_self_layer, and the disable-node helpers); the inform-channel
coalescing machinery lost its last caller and its tests go with it.
Never-released options and commands go with their modules:
clboss-xrebalance-age-secs, clboss-xrebalance-part-wait-secs,
clboss-xrebalance-history-age-secs, clboss-xrebalance-predict-*,
clboss-node-disable-age-secs, clboss-channel-update-age-secs,
clboss-update-retain-secs; clboss-xmovefunds,
clboss-askrene-updates, clboss-xrebalance-history,
clboss-xrebalance-predictions. No public CHANGELOG entry: none of
this shipped in a release.
Delete FundsMover, its drivers (JitRebalancer, EarningsRebalancer,
InitialRebalancer), the manual clboss-movefunds command, and their
tests. The xrebalance engine is the rebalancer going forward;
carrying both doubles the support surface, and the classic code at
this point is not the proven getroute implementation anyway but a
fresh askrene port that would need its own soak.
JIT rebalancing is removed deliberately: holding an incoming HTLC
while a rebalance runs delays the whole payment path. DemandTracker
serves the same demand signal by reacting to observed forwards
without holding HTLCs.
clboss-rebalance-mode loses the "classic" value and now defaults to
xrebalance2, which idles with a log hint when the external plugin is
absent. clboss-max-rebalance-fee-ppm is removed; configs still
setting it must drop it or lightningd will refuse to start. The
never-released gate and aging options from the askrene port
(clboss-classic-layer-age-secs, clboss-min-rebalance-ppm,
clboss-min-rebalance-prob-ppm) go with it.
EarningsTracker's ResponseMoveFunds path goes with its only sender;
XRebalanceAttribution is the sole attribution route and the kept
earnings tests inject through it. Also removed as orphans: the
"clboss" askrene layer name and RebalanceModeProxy::is_classic().
AskreneLayer's set_aging_window_secs remains as a test seam for the
coalescing test.
New module DemandTracker registers an htlc_accepted deferrer that
never holds an HTLC: for each forward it raises DemandObserved
naming the outgoing channel, then immediately declines. The
message deliberately carries no amount: unforwardable probe HTLCs
cost an attacker nothing, so sizing from a demanded amount would
be a free lever over our spend.
XRebalancer consumes the message. A trigger arriving while any
cycle runs is discarded (traffic recurrence re-arms real demand);
otherwise it runs a demand cycle: the same fetch/join pipeline as
a matched cycle, targeting the peer whose channel the forward
exited through. Fill-pool membership is the entire criterion --
demand controls when we rebalance, never who qualifies or how
much. The request restores the peer to the fill edge, priced at
target NetPpm plus the minimum offered NetPpm, executed through
the existing executor with cycle tag [demand].
The new in_flight flag serializes matched and demand cycles; both
paths clear it behind a catch-all so an exception cannot wedge
it. The catch-all around the matched tick also keeps the Poisson
loop alive on RPC errors, which previously terminated it with
only a stderr notice.
XRebalancer: sweep the demand cycle's offered-pool rung
Pricing the demand cycle at the offered pool's minimum NetPpm let
one outlier cap every budget: a peer whose past refill
expenditures ate its inbound earnings sat at near-zero net and
priced all cycles, while the pool median was an order of
magnitude higher. Spending depresses a source's net rate, the
depressed rate becomes the minimum, and the minimum strangles the
next budget.
Instead, each demand cycle draws a rung p from {20, 50, 80},
offers only the top p% of the source pool by NetPpm, and prices
at the cheapest source actually offered. The invariant is
unchanged at every rung: every sat moved earns at least the
target's side plus at least its source's side. A narrow rung
offers premium sources at a rich budget; a wide rung offers most
of the pool at a lean one. Demand re-triggering redraws the
rung, so which rung delivers is learned from live traffic -- the
same sweep methodology as the auto floor ladder. The drawn rung
is logged in the [demand] cycle line.
New module that judges the earnings track record of nodes we had
channels with before, so channel-open candidate selection can prefer
proven earners. The metric is TRAL (annualized net return on
liquidity, in basis points), the same metric as
contrib/clboss-forwarding-stats: net earnings from the EarningsTracker
daily buckets, divided by the average balance and the observed
operational days from the FeeMonitor records, annualized. Both
sources persist after a channel closes, which is exactly the case this
serves: candidates whose previous channel with us is gone.
Operational days count only in-window records (roughly one per hour
while a channel exists), so a channel that overlapped the window
partially is annualized over its actual operating time. Averaging the
balance per-record rather than sample-and-hold over wall-clock time
keeps a mid-window close from diluting the average.
Verdicts: keeper (TRAL at or above threshold), no-record (no or
insufficient history), underperformer (history below threshold).
Three dynamic options (runtime-settable via setconfig):
clboss-candidate-record-window-days (180),
clboss-candidate-keeper-tral-bps (50),
clboss-candidate-min-record-days (7).
A clboss-track-record nodeid command shows the verdict and its inputs
for one node, plus the current option values, for tuning at runtime.
After the size rearranger and the IP-binning reprioritizer, partition
the proposal list by track-record verdict: keepers first, then
candidates with no record, then underperformers. Within a tier the
earlier stages' order is preserved. Because the Planner consumes
proposals in order until funds run out, placing a tier last implements
'only used if no better candidate can absorb the funds' without an
outright veto. The partition runs last so the earlier perturbation
stages cannot promote a candidate across a tier boundary.
Logs one Info line per creation request with the per-tier membership
and each judged candidate's TRAL and observed days.
When a part fails at hop k, hops 0..k-1 provably forwarded the
HTLC -- a lower bound on each hop's liquidity exactly as strong
as a settled part's evidence. Previously only the erring hop
produced any feedback and that forwarding proof was dropped.
Now every hop strictly before the failure point gets an
inform-unconstrained on the clboss-xrebalance layer plus a
history observation of a new kind, transit. Transit is kept
distinct from success because the two decay differently: a
settled part consumes the liquidity it proves, while a failed
part unwinds and puts it back. The predictor treats both as
floor-side lower bounds; the distinction exists so the planned
floor-factor calibration can measure proven-and-restored
separately from proven-and-consumed.
Details:
- own channels filtered out (auto.localchans owns their truth)
- amount_out_msat, matching the settled-part success informs
- for 0x100c the incoming hop route[k-1] may itself be blamed
(inbound-fee PolicyFail), so transit stops one hop short on
all 0x100c: never assert carried-fine and policy-excluded
about the same hop from the same part
- failcode/erring_node are NULL for transit, as for success
Tests: kind_is_bound maps transit as a non-fail bound; a plan()
case where the floor bound comes from a transit record (larger
than the settled success beside it) proves transit feeds the
floor side.
The phase-2 prediction algorithm as a pure function
(Boss/Mod/XRebalancePredict): no I/O, no bus, no clock -- bounds in,
verdict out -- so unit tests are exact and the same code backs the
live predictor module (next commits).
Model: observations are interval bounds (success at m: liquidity >= m;
failure at attempted A: liquidity < A). Walking records newest to
oldest and intersecting until contradiction yields the current regime
[B_lo, B_hi) -- the longest recent run consistent with one static
liquidity. horizon = min(horizon_max_secs, horizon_frac x regime
evidence span), where the span grows only with actual observations,
never with wall-clock passage. A side asserts while data_age <=
horizon and the regime holds min_samples records of its kind: the wall
is B_hi scaled by wall_margin (>= 1 biases errors high, which
self-corrects; too-low walls are sticky), the floor is B_lo scaled by
floor_factor (<= 1 conservative; 0 disables). Defaults: frac 2.0,
cap 24h, min_samples 2 per side, margin 1.0, factor 0.9. (With the
store recording ATTEMPTED amounts for failures, the wall re-assert
amount is B_hi directly -- inform constrained B_hi stores max =
B_hi - 1.)
Two read-only spot-check commands, so the constants are playable
against live production data with no setconfig, restart, or writes
(nothing is asserted into any layer):
- clboss-xrebalance-history <scid> appends a predictions block:
per direction, the regime stats (records, span, start, truncated),
data age, horizon, and each side's would_assert / amount /
decline_reason.
- clboss-xrebalance-predictions [kind] runs the forecaster over
EVERY channel direction in the store; kind selects walls / floors /
asserting (default) / all. Per-direction emission and override
parsing are shared helpers.
Both accept keyword overrides horizon_frac, horizon_max_secs,
min_samples, wall_margin, floor_factor per query.
Zero-span declines explain themselves: several MPP parts of one flow
failing on the same hop in the same second are many bounds but a
single temporal sample (span 0, horizon 0, declines) -- correct
(correlated samples must not fake durability; min_samples gates count,
span gates duration), but the bare "stale ... 0s" message was
confusing. Now: "zero evidence span (all N records simultaneous);
nothing to extrapolate until the channel is observed again later"
(and the singular "a single observation has no time span; ..." at
min_samples=1).
Unit tests cover the calibration examples (2 points/1h -> 2h,
3 points/2h -> 4h, 24h cap), contradiction truncation, per-side
min_samples, margin/factor scaling, policy zero-walls, the zero-span
shapes, and the command integration.
XRebalancePredictor: the live persistence forecaster (off by default)
Phase 2 of the history+prediction design: the module that closes the
loop from observation to synthetic re-assertion. After each hourly
askrene-age pass over the clboss-xrebalance layer, XMoveFunds now
raises Msg::XRebalanceLayerAged carrying the aging cutoff (on the
failure path too -- a skipped trim only leaves stale entries, which
is safe). The new Boss::Mod::XRebalancePredictor subscribes, reads
the XRebalanceHistory observation store, runs the pure regime-walk
algorithm per channel direction, and re-asserts the surviving
walls/floors into the routed layer via askrene-inform-channel --
but only for directions whose newest real observation predates the
cutoff: directions with live evidence need no synthesis. Synthetic
assertions are never recorded back into the observation store (no
self-confirmation).
OFF BY DEFAULT. The master switch is the dynamic option
clboss-xrebalance-predict-horizon-max-secs (0 = disabled, the
default; 86400 is the intended enabled value -- and since an
asserted wall is never contradicted by routing, this cap IS the
wall re-test schedule). The other constants are dynamic options
mirroring the read-only spot-check parameters: -horizon-frac (2.0),
-min-samples (2), -wall-margin (1.0), and -floor-factor, which
defaults to 0 = walls-only operation (floors are the riskier half:
a too-high floor attracts flow and costs a failed part to
self-correct). Note the live floor default deliberately differs
from the spot-check commands' 0.9. Also dormant unless
clboss-rebalance-mode is xrebalance.
The per-cycle decision is a pure static XRebalancePredictor::plan
(group directions, candidacy gate, predict, collect asserting
sides, skip amount-0 degenerates), unit-tested directly; the module
shell only reads the table, executes the plan, logs one Info
summary per asserting cycle, and reports an xrebalance_predictor
section (params + last-cycle counts) in clboss-status.
kind_is_bound (stored TEXT kind to bound side) is promoted into
XRebalancePredict and shared with XRebalanceHistory.
Phase 1 of the history + persistence-forecasting design: a long-lived,
lossless store of everything the xrebalance executor learns, kept in
clboss's own sqlite rather than an askrene layer because layer records
are askrene's lossy (scid, time, min/max) projection -- they cannot
distinguish a 0x100c inbound-fee policy exclusion from a liquidity
wall, and the planned chokepoint/node-bias reliability statistics
need the same event table.
XMoveFunds now raises a Msg::XRebalanceObservation adjacent to every
askrene feedback write on the clboss-xrebalance layer: one per middle
hop on part success (kind success), and one at each failure feedback
site (kinds liquidity_fail, policy_fail for the 0x100c exclusion,
node_fail), carrying the full event context: amount, failcode, erring
node. Observations mirror the informs exactly: local channels and the
self-node guard produce no observation, and update_channel policy
refreshes are not observations.
The new Boss::Mod::XRebalanceHistory module records these in the
XRebalanceHistory table (time, scid, dir, kind, amount_msat,
failcode, erring_node; NULL failcode/erring_node for success),
indexed on (scid, dir, time) and (time). Rows older than the new
dynamic option clboss-xrebalance-history-age-secs (default 604800,
one week) are trimmed once per TimerRandomHourly tick. A read-only
clboss-xrebalance-history command ([scid] [hours] filters, positional
or keyword) reports the series oldest-first per channel direction,
and clboss-status gains an xrebalance_history section (row count,
distinct channel directions, oldest/newest time).
No behavior change to routing or feedback writes; this only adds the
evidence base. The phase-2 persistence forecaster (regime detection
over interval bounds, horizon = min(cap, frac x evidence span)) will
consume this table and re-assert walls/floors for stable channels
into the short-term layer.
Unit test covers record/report/filtering/status/trim and the dynamic
retention option; suite passes 82/82 under valgrind.
XMoveFunds is the manual building block of the xrebalance track ("xpay,
for rebalancing"): a single clboss-xmovefunds RPC that moves liquidity
in a circle from our node back to our node over an operator-chosen set
of source and destination channels.
Requires patched CLN: the plan step calls getroutes with
source = destination = self, which the circular-askrene branch of
ksedgwic/lightning interprets as circular self-rebalance routing; stock
CLN aborts there. clboss-xmovefunds is a manual trigger only -- no
autonomous code path exercises circular routing until the XRebalancer
driver lands (next commit).
Per request:
- Parse params; require an explicit maxfee_msat and/or maxfee_ppm.
- Create a transient askrene layer holding per-direction masks (every
us->peer not in source_scid disabled, every peer->us not in dest_scid
disabled), call getroutes circularly with the persistent
clboss-xrebalance layer + the transient mask, sendpay the parts, then
remove the transient layer.
- Refuse parts whose fee/delivered exceeds the budget before sending
(per-part gate), and skip parts whose route exceeds 20 hops: a long
onion crashes CLN's sphinx serialization (the identical guard already
lives in Track A's FundsMover/Attempter). An over-long part is marked
skipped -- never sent, never waited on -- and counted in parts_skipped;
dropping one long part and sending the rest beats failing the whole
payment, since xmovefunds parts are independent self-pays. delpay
failed parts; reply with per-part summary stats and a per-hop fee
breakdown on 204s.
Layer learning + maintenance:
- Feed sendpay outcomes into the persistent clboss-xrebalance layer
(capacity constraints on failure).
- On policy/cltv/htlc-bound failures refresh the channel policy from the
embedded channel_update; detect bLIP-18 positive inbound fees and
exclude such channels (askrene cannot price them); on FEE_INSUFFICIENT
exclude the incoming channel; never disable our own node.
- Age the clboss-xrebalance layer hourly, tunable at runtime via the
clboss-xrebalance-age-secs dynamic option (default 3600s).
Earnings:
- Emit Msg::XRebalanceAttribution per delivered part so EarningsTracker
can attribute the fee and amount (consumed in the next commit).
Claimer handles the sendpay / wait execution path.
EarningsTracker: attribute clboss-xrebalance per-part fees and amounts
EarningsTracker now subscribes to Msg::XRebalanceAttribution (emitted by
XMoveFunds once per delivered part) and records the rebalance fee and
amount against the same earnings accounting the classic FundsMover path
feeds via response_move_funds. This is the XMoveFunds-side analog of
that attribution, so clboss-xrebalance deliveries show up in earnings
and clboss-status just like classic rebalances rather than being
invisible spend.
setconfig was acknowledged with unconditional success after
broadcasting Msg::Option, while owning modules reject bad values by
log-and-keep. lightningd persists a setconfig value (configvar_save)
only on a success response, so the blanket ack recorded values clboss
never applied: listconfigs and config.setconfig diverged from the
running configuration, and a non-numeric value persisted for an
int-typed option fails lightningd's own option parse on the next
start -- lightningd refuses to boot.
Add a rejection back-channel to Msg::Option: SetConfigHandler
allocates a shared reject_reason (null on init-time raises, so
aggregate initialization at existing sites is unaffected), the owning
subscriber reports rejection via the new Msg::Option::reject() helper
(a no-op at init time, where quietly keeping the default is right),
and SetConfigHandler -- whose bus.raise() returns only after all
subscribers ran -- fails the command with invalid-params when a
reason was set. All dynamic-option owners in this tree report their
rejections: RebalanceModeManager (unrecognized mode), FundsMover's
three numeric handlers, and AskreneUpdates' shared age/retain
handler.
Add AskreneUpdates, a standalone module both rebalancers will use to hold the
node disables and channel_update overrides they learn from routing failures.
askrene never ages these (they carry no timestamp, unlike the inform-channel
constraints), so instead of accumulating them in a shared askrene layer they
live in two append-only sqlite tables here and are projected, still-fresh, into
a private per-request layer for each getroutes.
Records arrive as AskreneNodeDisableUpdate / AskreneChannelUpdate. A ReqResp
(Request/ResponseAskreneUpdates) returns the distinct nodes disabled within
clboss-node-disable-age-secs and the latest override per channel direction
within clboss-channel-update-age-secs. Static open_layer/close_layer build and
tear down a uuid-named, non-persistent layer from a response (open_layer returns
an empty name if askrene is absent, so a caller never names a missing layer).
Rows are pruned only at clboss-update-retain-secs (default 30d), so the log
survives long enough to mine. Nothing calls the module yet.
Also add an AskreneLayer header comment pointing at this store for where
the learned node disables and channel_update overrides live.
Completed rebalances were correlated back to their (source,
destination) through an in-memory map keyed only by the
RequestMoveFunds requester pointer. When one rebalancer issues several
moves at once they share a single requester (EarningsRebalancer uses
its module pointer; JitRebalancer uses nullptr), so each request
overwrote the previous map entry. As the moves completed, the first
response consumed and erased the shared entry -- booking against
whatever pair happened to be written last -- and every later response,
including the actually-successful one, hit the not-in-our-table path
and was silently dropped.
Effect: rebalance spend and volume were under-counted and sometimes
mis-attributed to the wrong peer. That also starved the
EarningsRebalancer refusing-to-throw-good-money-after-bad guard of
accurate expenditure data. JIT-dominated periods looked fine only
because those moves complete one at a time and rarely overlap; an
EarningsRebalancer-dominated period booked almost nothing.
Fix: ResponseMoveFunds now carries source and destination
directly. FundsMover/Runner populates them and EarningsTracker books
the move straight from the response. The requester-keyed pendings map,
the RequestMoveFunds subscription, and request_move_funds are
removed. This mirrors the existing XRebalanceAttribution path, which
already passes source/destination in its message.
Tests: ResponseMoveFunds constructions updated across the rebalancer
and earnings tests; test_earningstracker now issues two moves under
the same requester and asserts both are booked and correctly
attributed -- a regression guard for the collision. Full suite 85/85.
Introduces a single source of truth for which rebalancing track is
active. Boss::Mod::RebalanceModeManager owns the mode in memory (no
sqlite, so a restart reverts to the configured default, giving a
known-good baseline on every boot) and registers clboss-rebalance-mode
as a dynamic option: the config file sets the startup default and
`setconfig clboss-rebalance-mode <mode>` switches it at runtime without
a restart. It answers RequestRebalanceMode queries and reports the mode
under clboss-status.
Modes are "classic" (run the rebalancer) and "off" (a real quiesce,
also the supported way to disable rebalancing entirely). This is the
seam that later lets a second rebalancing track coexist and be toggled
without a restart.
The classic-track rebalancers self-gate on the mode at their existing
decision points, modeled on RebalanceUnmanager: EarningsRebalancer gates
its trigger, InitialRebalancer gates its run, and JitRebalancer gates
the top of htlc_accepted so that in off mode it does not defer the HTLC
and adds no forwarding latency. A header-only Boss::ModG::
RebalanceModeProxy provides get_mode for the gate sites. off composes
with the existing per-peer unmanage balance tag: off wins globally,
otherwise the per-peer tag still excludes specific peers.
The three rebalancers' unit tests now install a RebalanceModeManager on
the test bus so the self-gate query is answered (default classic, so
they behave as before). Without a responder the RequestRebalanceMode
ReqResp is never satisfied and leaks, which the valgrind-checked tests
flag as a failure.
New files: Boss/RebalanceMode.hpp, Boss/Msg/RequestRebalanceMode.hpp,
Boss/Msg/ResponseRebalanceMode.hpp, Boss/ModG/RebalanceModeProxy.hpp,
Boss/Mod/RebalanceModeManager.{hpp,cpp}.
Introduces the mechanism for runtime-mutable plugin options: an option
marked dynamic can be changed via `lightning-cli setconfig <name> <val>`
without restarting clboss or lightningd. No option opts in yet -- this
is the foundation (the rebalancer mode selector is the first consumer).
- Boss::Msg::ManifestOption gains a bool dynamic field (default false,
preserving the existing startup-only contract).
- Boss::Mod::Manifester emits the per-option dynamic flag in the
getmanifest response, so lightningd knows to forward setconfig for
that option.
- New Boss::Mod::SetConfigHandler module records (name -> dynamic)
from Msg::ManifestOption events, then handles incoming setconfig
CommandRequests: it validates the named option is registered and
dynamic, and re-raises a fresh Msg::Option on the bus, so existing
option handlers re-apply the new value transparently.
Because Msg::Option is now re-emitted at runtime (not only during init),
subscribers must filter by name and tolerate post-init arrival.
AmountSettingsHandler gains an `if (!settings) return` guard: it moves
`settings` away at EndOfOptions, so a later Msg::Option for an unrelated
name must be dropped -- this also fixes a latent assert(settings) crash
that any post-EndOfOptions Msg::Option would have tripped.
Contract documented in SetConfigHandler.hpp: lightningd delivers
Int/Bool/Flag option values as JSON primitives at startup but as JSON
strings at setconfig time, so dynamic-option handlers must accept both
Jsmn shapes.
CLN v26.06 deprecates getroute in favor of askrene's getroutes.
Replace the Dowser's getroute+listchannels loop (up to ~40 RPC
round-trips) with a single getroutes call and let askrene's
min-cost-flow solver enumerate multi-path capacity directly. Response
parsing follows the getroutes schema; 205/206 (no route / too
expensive) maps to zero capacity.
Size the probe to the caller's threshold. The Dowser is a flow
estimator: callers compare the dowsed flow against a channel size (the
janitor and preinvestigator min-channel acceptance test, the channel
creator's sizing). A fixed probe amount caps the estimate, so any
threshold at or above that cap rejects every candidate regardless of
real capacity. RequestDowser now carries a min_amount; callers that
test against a channel size set it, and the probe runs at
min_amount / reserve_factor so a full-flow result clears the threshold
for any configured channel size. The channel creator dowses at
max-channel so new channels size toward the operator ceiling instead
of being pinned at the min-channel floor. The manual clboss-dowser
command keeps the fixed default probe.
Matchmaker, ActiveProber: migrate getroute -> getroutes for CLN v26.06 compat
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.
Addresses ([#229])
This allows effective feerates (PPM) to be computed for earnings and
expenses.
This PR updates the schema automatically. Downgrading to previous
will require manual DB migration (but is possible). Downgrade
commands are in a comment in EarningsTracker.c
Construct a "compatibility struct" to convert `listpeerchannels`
output into legacy `listpeers` format.
Tests written using the legacy listpeers format can use the
`convert_legacy_listpeers` utility to construct a compatibility
struct.
The test_peerjudge_datagatherer malformed test needed to be malformed
differently to achieve the desired effect.
Channel directions can be individually active or inactive, which affects
routing directions.
Thus, a dowsing request in on direction may have a different result in
the opposite direction.
Existing clients of the `Dowser` assume the first argument is the source
(proposal) and the second argument is the destination (patron), but the
`Dowser` used the first argument as the destination instead.
This also flips the `Dowser` direction.