diff --git a/Boss/Mod/FundsMover/Main.cpp b/Boss/Mod/FundsMover/Main.cpp index bb1c581..20817eb 100644 --- a/Boss/Mod/FundsMover/Main.cpp +++ b/Boss/Mod/FundsMover/Main.cpp @@ -12,6 +12,7 @@ #include"Boss/Msg/OptionType.hpp" #include"Boss/Msg/ProvideDeletablePaymentLabelFilter.hpp" #include"Boss/Msg/RequestMoveFunds.hpp" +#include"Boss/Msg/ResponseMoveFunds.hpp" #include"Boss/Msg/SolicitDeletablePaymentLabelFilter.hpp" #include"Boss/Msg/TimerRandomHourly.hpp" #include"Boss/concurrent.hpp" @@ -55,6 +56,16 @@ private: * age_clboss_layer() for the aging mechanics and rationale. */ std::uint64_t aging_window_secs = std::uint64_t(21600); + /* Minimum fee budget, as ppm of the moved amount, worth attempting + * a rebalance at. A requested move whose fee_budget/amount is below + * this is declined up front -- no Runner, no getroutes, no split- + * retry storm -- because classic rebalances essentially never clear + * below ~50 ppm: the high-traffic drains get budgeted near 35 ppm and + * deliver nothing while saturating askrene. Dynamic via + * clboss-min-rebalance-ppm; default 50. Set to 0 to disable the gate + * and attempt every requested move. */ + std::uint64_t min_rebalance_ppm = std::uint64_t(50); + void start() { bus.subscribe([this](Msg::Init const& init) { rpc = &init.rpc; @@ -81,6 +92,22 @@ private: "layer has the analogous " "clboss-xrebalance-age-secs.", /* dynamic = */ true + }) + + bus.raise(Msg::ManifestOption{ + "clboss-min-rebalance-ppm", + Msg::OptionType_Int, + Json::Out::direct(min_rebalance_ppm), + "Minimum fee budget, in ppm of the moved amount, " + "worth attempting a rebalance at. A move whose " + "requested fee_budget/amount is below this is " + "declined immediately -- no route solve, no " + "split-retry storm -- because classic rebalances " + "essentially never succeed below this rate. " + "Dynamic: settable at runtime via `lightning-cli " + "setconfig clboss-min-rebalance-ppm `. " + "Default 50; set to 0 to disable (attempt every " + "requested move).", + /* dynamic = */ true }); }); bus.subscribe([this](Msg::Option const& o) { + if (o.name != "clboss-min-rebalance-ppm") + return Ev::lift(); + /* Number at startup, string via setconfig -- the same + * dual encoding clboss-classic-layer-age-secs handles. + * Signed so a negative value is rejected below rather + * than wrapping to a huge unsigned. */ + long long ppm = 0; + try { + if (o.value.is_number()) { + ppm = static_cast(double(o.value)); + } else if (o.value.is_string()) { + ppm = std::stoll(std::string(o.value)); + } else { + return Boss::log( bus, Warn + , "FundsMover: clboss-min-" + "rebalance-ppm: unsupported " + "value type; keeping %" + PRIu64 "." + , min_rebalance_ppm + ); + } + } catch (std::exception const& e) { + return Boss::log( bus, Warn + , "FundsMover: clboss-min-rebalance-" + "ppm: parse error '%s'; keeping %" + PRIu64 "." + , e.what() + , min_rebalance_ppm + ); + } + if (ppm < 0) { + return Boss::log( bus, Warn + , "FundsMover: clboss-min-rebalance-" + "ppm: must be >= 0; keeping %" + PRIu64 "." + , min_rebalance_ppm + ); + } + min_rebalance_ppm = std::uint64_t(ppm); + return Boss::log( bus, Info + , "FundsMover: min rebalance budget = %" + PRIu64 " ppm" + , min_rebalance_ppm + ); + }); bus.subscribe([this](Msg::RequestMoveFunds const& m) { auto msg = std::make_shared(m); @@ -166,6 +240,46 @@ private: "Contact " PACKAGE_BUGREPORT ); } + /* Decline a rebalance whose fee budget is below + * clboss-min-rebalance-ppm: classic rebalances + * essentially never clear below this rate, so skip + * the whole Runner / getroutes / split-retry + * machinery and emit the zero ResponseMoveFunds + * that Runner::finish() would have produced after + * giving up. Cross-multiplied to avoid a divide: + * fee_budget/amount < min_ppm/1e6 iff + * fee_budget*1e6 < min_ppm*amount. min_ppm == 0 + * disables the gate (the test is never true). */ + if ( min_rebalance_ppm > 0 + && double(msg->fee_budget.to_msat()) * 1000000.0 + < double(min_rebalance_ppm) + * double(msg->amount.to_msat()) + ) { + auto src_pfx = + std::string(msg->source).substr(0, 8); + auto dst_pfx = + std::string(msg->destination).substr(0, 8); + return Boss::log( bus, Debug + , "FundsMover: not moving %s " + "from %s... to %s... -- fee " + "budget %s is below clboss-min-" + "rebalance-ppm=%" PRIu64 "; " + "never clears this cheap." + , std::string(msg->amount).c_str() + , src_pfx.c_str() + , dst_pfx.c_str() + , std::string(msg->fee_budget) + .c_str() + , min_rebalance_ppm + ) + + bus.raise(Msg::ResponseMoveFunds{ + msg->requester, + Ln::Amount::sat(0), + Ln::Amount::sat(0), + msg->source, + msg->destination + }); + } auto runner = Runner::create( bus , *rpc , self_id diff --git a/README.md b/README.md index 6db331d..5d2847a 100644 --- a/README.md +++ b/README.md @@ -599,6 +599,23 @@ startup default, or at runtime with lightning-cli setconfig clboss-classic-layer-age-secs +### `--clboss-min-rebalance-ppm=` + +Sets the minimum fee budget, expressed as parts-per-million of the amount +being moved, at which the classic rebalancer will bother attempting a +rebalance. When a requested move's budget (`fee_budget / amount`) is below +this, FundsMover declines it immediately — no route solve, no split-retry — +rather than spending effort on a move that almost never succeeds at that +rate. + +The default is `50`. Set it to `0` to disable the gate and attempt every +requested move. + +This is a *dynamic* option: set it in the `lightningd` config for the +startup default, or at runtime with + + lightning-cli setconfig clboss-min-rebalance-ppm + ### `--clboss-min-nodes-to-process=` Sets the minimum number of nodes that CLBOSS must know about before it