Commit graph

2 commits

Author SHA1 Message Date
Ken Sedgwick
19a9b2e848
xrebalance options: report setconfig rejections; harden the parses
Adopt the Msg::Option::reject() contract in the Track B dynamic-option
handlers, which predate it.  Without this, a rejected setconfig for an
xrebalance-family option was still acknowledged as success, so
lightningd persisted the never-applied value to config.setconfig --
including the junk-string case that then fails lightningd's own option
parse on the NEXT start.  Every reject branch in XRebalancer,
XRebalancePredictor (via its bad_option choke point, which now names
the specific constraint instead of 'could not parse value'),
XRebalanceHistory and XMoveFunds now reports a quote-free reason.

While in the handlers, close the same parse holes Track A's review
fixed plus a NaN family specific to these stod-based knobs:

- XRebalanceHistory and XRebalancePredictor::parse_u64 parsed via
  std::stoull, which accepts a leading minus and wraps modulo 2^64:
  setconfig -1 became ~1.8e19.  Signed parse + negative reject,
  matching the FundsMover/AskreneUpdates handlers.

- std::stod accepts 'nan' and 'inf'.  A NaN slips through every
  range/clamping comparison (all false) straight into the stored
  setting -- a NaN per_hour breaks the cycle timing, a NaN or inf
  part-wait-secs breaks the part wait, and casting non-finite to
  uint32 (maxparts) is undefined.  parse_double and all direct stod
  sites now require finite values, and maxparts clamps to
  [1, 1000000] before its uint32 cast.
2026-08-04 11:02:09 -07:00
Ken Sedgwick
c981d67cb6
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.
2026-08-04 11:02:03 -07:00