XRebalancer: request the full matched volume
Some checks are pending
Code Base Sanity Check / tests (push) Waiting to run
Code Base Sanity Check / coverage (push) Waiting to run
Code Base Sanity Check / build-clang (push) Waiting to run

Remove clboss-xrebalance-size-factor and its per-cycle lo:hi random
sweep.  The matched-pool cycle now requests the full matched volume
that cleared the floor.  Both purposes in the option's help text no
longer apply: the per-channel band-edge caps in the request lists
bound what each channel absorbs, and per-request askrene layers
removed the accumulated route state the size sweep kept from staling.
The focused cycle was already sized flat (target deficit).  The
clboss-xrebalance-view emulation drops --size-factor to match.

Configs that set the option must drop it before starting this build
(an unknown plugin option fails lightningd startup).
This commit is contained in:
Ken Sedgwick 2026-08-04 12:39:51 -07:00
parent c6a45eae37
commit 166b4efbaf
No known key found for this signature in database
GPG key ID: DBD2AF0849D711A9
2 changed files with 9 additions and 140 deletions

View file

@ -36,7 +36,6 @@ namespace {
auto const opt_per_hour = std::string("clboss-xrebalance-per-hour");
auto const opt_floor = std::string("clboss-xrebalance-route-cost-floor");
auto const opt_size_factor = std::string("clboss-xrebalance-size-factor");
auto const opt_window_days = std::string("clboss-xrebalance-earnings-window-days");
auto const opt_fill_loc = std::string("clboss-xrebalance-fill-loc");
auto const opt_drain_loc = std::string("clboss-xrebalance-drain-loc");
@ -47,7 +46,6 @@ auto const opt_gain = std::string("clboss-xrebalance-gain");
auto constexpr default_per_hour = double(12.0);
auto constexpr default_floor = double(50.0);
auto constexpr default_size_factor = double(0.1);
auto constexpr default_window_days = double(90.0);
/* Tier bands (Loc%); match clboss-xrebalance-view defaults. */
auto constexpr default_fill_band = double(10.0);
@ -88,8 +86,6 @@ private:
double per_hour;
double floor_ppm;
double size_factor; /* lo bound, or the fixed value */
double size_factor_hi; /* hi bound when size_factor is a lo:hi range */
double window_days;
double fill_band;
double drain_band;
@ -98,7 +94,6 @@ private:
double grant_ppm; /* assumed prior rate (ppm of a capacity-turn) */
double gain; /* NetPpm multiplier */
bool floor_auto; /* floor option set to "auto" (sweep) */
bool size_factor_range; /* size_factor set as lo:hi (per-cycle random) */
/* Mode xrebalance2: execute through the external xrebalance
* plugin instead of clboss-xmovefunds. Captured at cycle start;
* cycles are serialized by the awaited loop, so one flag
@ -129,8 +124,6 @@ private:
void start() {
per_hour = default_per_hour;
floor_ppm = default_floor;
size_factor = default_size_factor;
size_factor_hi = default_size_factor;
window_days = default_window_days;
fill_band = default_fill_band;
drain_band = default_drain_band;
@ -139,7 +132,6 @@ private:
grant_ppm = default_grant;
gain = default_gain;
floor_auto = false;
size_factor_range = false;
use_plugin = false;
started = false;
@ -163,19 +155,6 @@ private:
"amount and the maxfee budget. Or \"auto\": "
"each cycle picks a random rung of the derived "
"floor ladder (sweep).")
+ manifest_option(opt_size_factor, default_size_factor,
"Multiplier (> 0) on the derived matched-pool "
"amount requested per cycle. <1 requests a "
"fraction (incremental fills); >1 over-fills "
"past the band targets -- recoverable, the "
"channel drifts back -- to make each of the "
"maxparts parts larger so it amortizes the base "
"fee and clears the per-part budget. Or a range "
"\"lo:hi\" (e.g. 0.5:3.0): each cycle draws a fresh "
"uniform-random multiplier in [lo,hi], sweeping the "
"request size so askrene's accumulated route state "
"does not stale into repeated 205/206 refusals the "
"way a single fixed value eventually does.")
+ manifest_option(opt_window_days, default_window_days,
"Trailing window (days) over which per-channel "
"NetPpm is measured for cycle selection.")
@ -203,7 +182,7 @@ private:
"and price maxfee at the target's NetPpm plus "
"the minimum NetPpm of the offered pool. The "
"remaining cycles run the matched-pool style "
"(floor ladder / size-factor).")
"(floor ladder).")
+ manifest_option(opt_grant, default_grant,
"Assumed prior earnings rate (ppm), credited "
"to every channeled peer on both sides as if "
@ -266,48 +245,6 @@ private:
}
if (o.name == opt_floor)
floor_auto = false;
/* size-factor also accepts a "lo:hi" range (e.g. 0.5:3.0):
* instead of one fixed multiplier, each cycle draws a fresh
* uniform-random value in [lo,hi] (see plan_and_log). Sweeping
* the size keeps the request varying so askrene's accumulated
* route state does not stale into repeated 205/206 refusals the
* way a fixed value eventually does. A plain number clears
* range mode and falls through to the numeric path below. */
if (o.name == opt_size_factor) {
auto s = std::string(o.value);
auto colon = s.find(':');
if (colon != std::string::npos) {
auto lo = double(0.0);
auto hi = double(0.0);
try {
lo = std::stod(s.substr(0, colon));
hi = std::stod(s.substr(colon + 1));
} catch (std::exception const&) {
o.reject(o.name + ": invalid range");
return Boss::log( bus, Error
, "XRebalancer: ignoring invalid "
"%s range \"%s\"."
, o.name.c_str(), s.c_str() );
}
if (!(lo > 0.0) || !(hi > 0.0)) {
o.reject( o.name + ": range bounds "
"must be > 0");
return Boss::log( bus, Error
, "XRebalancer: %s range bounds "
"must be > 0; ignoring \"%s\"."
, o.name.c_str(), s.c_str() );
}
if (hi < lo) { auto t = lo; lo = hi; hi = t; }
size_factor = lo;
size_factor_hi = hi;
size_factor_range = true;
return Boss::log( bus, Info
, "XRebalancer: %s set to range "
"%.3g:%.3g (per-cycle random)."
, o.name.c_str(), lo, hi );
}
size_factor_range = false;
}
/* maxparts is an integer count, not a continuous knob, so it
* gets dedicated handling: parse, round, floor at 1 (askrene
* requires >= 1), and store as an integer. */
@ -339,7 +276,6 @@ private:
double* target = nullptr;
if (o.name == opt_per_hour) target = &per_hour;
else if (o.name == opt_floor) target = &floor_ppm;
else if (o.name == opt_size_factor) target = &size_factor;
else if (o.name == opt_window_days)target = &window_days;
else if (o.name == opt_fill_loc) target = &fill_band;
else if (o.name == opt_drain_loc) target = &drain_band;
@ -365,7 +301,7 @@ private:
, o.name.c_str(), s.c_str()
);
}
if (o.name == opt_size_factor || o.name == opt_gain) {
if (o.name == opt_gain) {
if (!(v > 0.0)) {
o.reject(o.name + ": must be > 0");
return Boss::log( bus, Error
@ -929,38 +865,21 @@ private:
auto dest_caps = pick(fill); /* fill = where funds land */
auto source_caps = pick(drain); /* drain = where funds leave */
/* size_factor may be a "lo:hi" range; draw a fresh multiplier
* each cycle so the requested size sweeps (see handle_option). */
auto effective_size_factor = size_factor;
auto sf_note = std::string();
if (size_factor_range) {
auto dist = std::uniform_real_distribution<double>(
size_factor, size_factor_hi);
effective_size_factor = dist(Boss::random_engine);
auto os = std::ostringstream();
os << " (rand " << size_factor << ":" << size_factor_hi << ")";
sf_note = os.str();
}
auto requested = std::int64_t(
std::max<double>(1.0, std::llround(double(best_n)
* effective_size_factor)));
/* Request the full matched volume; the per-channel caps in
* the request lists bound what each channel absorbs. */
auto requested = best_n;
auto maxfee = std::uint32_t(std::llround(best_joint));
return Boss::log( bus, Info, "%s", levels_str.c_str() )
+ Boss::log( bus, Info
, "XRebalancer: cycle [matched] floor=%.1f%s window=%.0fd "
"-> derived N=%s sat, joint=%.1f ppm "
"(fill>=%.1f + drain>=%.1f); size_factor=%.3g%s "
"-> request=%s sat (maxfee %u ppm); "
"-> request=%s sat (matched volume), joint=%.1f ppm "
"(fill>=%.1f + drain>=%.1f), maxfee %u ppm; "
"sources=%zu dests=%zu; executing."
, effective_floor, picked_note.c_str(), window_days
, Util::Str::group_digits(
std::int64_t(best_n)).c_str(), best_joint
std::int64_t(requested)).c_str(), best_joint
, best_fill_ppm, best_drain_ppm
, effective_size_factor, sf_note.c_str()
, Util::Str::group_digits(
std::int64_t(requested)).c_str()
, (unsigned)maxfee
, source_caps.size(), dest_caps.size()
).then([this, source_caps, dest_caps]() {

View file

@ -132,7 +132,6 @@ def read_live_config(lightning_dir, network_option):
("drain_loc", "clboss-xrebalance-drain-loc"),
("days", "clboss-xrebalance-earnings-window-days"),
("route_cost_floor", "clboss-xrebalance-route-cost-floor"),
("size_factor", "clboss-xrebalance-size-factor"),
("maxparts", "clboss-xrebalance-maxparts"),
("grant", "clboss-xrebalance-grant"),
("gain", "clboss-xrebalance-gain"),
@ -412,19 +411,6 @@ def main():
"(a live value of \"auto\" means the driver sweeps a ladder; the "
"full joint(N) staircase below shows every rung regardless)."
)
parser.add_argument(
"--size-factor", type=float, default=None, metavar="MULT",
help="Multiplier (> 0) on the derived matched-pool amount in the "
"emitted command. The full economic picture (bold set, "
"thresholds, budget) is still computed on the derived amount; "
"only the emitted amount_msat is scaled. <1 requests a fraction "
"(incremental -- the matched pool may pile into one endpoint, and "
"the rebalancer converges over many cycles). >1 over-fills past "
"the band targets (recoverable -- the channel drifts back) to "
"make each part larger so it amortizes the base fee. Default: "
"1.0."
)
args = parser.parse_args()
# Reconcile network option (lightning-cli wants 'bitcoin' for mainnet)
@ -511,31 +497,6 @@ def main():
args.route_cost_floor = 50.0
settings.append(("route-cost-floor", "50", "default"))
# --size-factor scales only the emitted amount. A live "lo:hi" is a
# per-cycle uniform sweep; emit at the mean (its expected value) and note
# the range.
if args.size_factor is not None:
settings.append(("size-factor", f"{args.size_factor:g}", "arg"))
elif live.get("size_factor") is not None \
and ":" in str(live["size_factor"]):
lo, hi = (str(live["size_factor"]).split(":", 1) + [""])[:2]
lo, hi = _num(lo), _num(hi)
if lo is not None and hi is not None and lo > 0 and hi > 0:
args.size_factor = (lo + hi) / 2.0
settings.append(("size-factor",
f"{args.size_factor:g} (live sweeps "
f"{live['size_factor']}; emitted at the mean)",
"live"))
else:
args.size_factor = 1.0
settings.append(("size-factor", "1", "default"))
elif _num(live.get("size_factor")) is not None:
args.size_factor = _num(live["size_factor"])
settings.append(("size-factor", f"{args.size_factor:g}", "live"))
else:
args.size_factor = 1.0
settings.append(("size-factor", "1", "default"))
# grant / gain: strictness benders, same semantics as the driver's
# clboss-xrebalance-grant / -gain. Overriding them on the command
# line previews a combo before setconfig-ing it live.
@ -567,9 +528,6 @@ def main():
maxparts = int(_num(live["maxparts"]))
settings.append(("maxparts", str(maxparts), "live"))
if not (args.size_factor > 0.0):
sys.exit("--size-factor must be > 0")
# The matched-pool cycle analysis (and its bands) always runs now, so
# default to the tier sort that visualizes it: extreme bands ranked by
# the relevant NetPpm (the In/OutNetPpm headers bold, and the most
@ -1441,15 +1399,7 @@ def main():
if r["scid"] in bold_scids
for s in peer_scids[r["peer_id"]]]
if source_picks and dest_picks:
# Scale the requested amount: the bold set / budget above
# reflect the full matched-pool economics; the emitted amount
# is scaled by size_factor (<1 incremental, >1 over-fills).
requested_sat = max(1, round(args.transfer_size
* args.size_factor))
if args.size_factor != 1.0:
print(f"# size_factor {args.size_factor:g}: requesting "
f"{requested_sat:_} of {args.transfer_size:_} sat "
f"derived (budget unchanged at {int(joint)} ppm).")
requested_sat = max(1, round(args.transfer_size))
cmd = build_xmovefunds_command(
network_option, lightning_dir,
source_picks, dest_picks,