clboss/Boss/Mod/AmountSettingsHandler.cpp

203 lines
6.1 KiB
C++
Raw Normal View History

#include"Boss/Mod/AmountSettingsHandler.hpp"
#include"Boss/Msg/AmountSettings.hpp"
#include"Boss/Msg/EndOfOptions.hpp"
#include"Boss/Msg/ManifestOption.hpp"
#include"Boss/Msg/Manifestation.hpp"
#include"Boss/Msg/Option.hpp"
#include"Boss/log.hpp"
#include"Jsmn/Object.hpp"
#include"S/Bus.hpp"
#include"Util/make_unique.hpp"
#include<assert.h>
#include<sstream>
#include<string>
namespace {
/* Default channel boundaries. */
auto const default_min_channel = Ln::Amount::sat( 500000);
auto const default_max_channel = Ln::Amount::sat(16777215);
/* Default amount to always leave onchain for future
* channel management actions. */
auto const default_reserve = Ln::Amount::sat( 30000);
/* The absolute lowest min_channel setting. */
auto const min_min_channel = Ln::Amount::sat( 500000);
/* How much larger the max_channel should be over the min_channel. */
auto const max_channel_factor = double(2.0);
/* How much larger the channel-creation trigger should be over
* the min_channel. */
auto const trigger_factor = double(2.0);
/* How much to add to the channel-creation trigger above, to get
* the amount to leave after creation. */
auto const additional_remaining = Ln::Amount::sat(20000);
Ln::Amount parse_sats(Jsmn::Object value) {
auto is = std::istringstream(std::string(value));
auto sats = std::uint64_t();
is >> sats;
return Ln::Amount::sat(sats);
}
}
namespace Boss { namespace Mod {
class AmountSettingsHandler::Impl {
private:
S::Bus& bus;
std::unique_ptr<Msg::AmountSettings> settings;
void start() {
settings->min_channel = default_min_channel;
settings->max_channel = default_max_channel;
settings->reserve = default_reserve;
bus.subscribe<Msg::Manifestation
>([this](Msg::Manifestation const& _) {
assert(settings);
return bus.raise(Msg::ManifestOption{
"clboss-min-onchain",
Msg::OptionType_String,
Json::Out::direct(default_reserve.to_sat()),
"Target to leave this number of satoshis "
"onchain, putting the rest into channels."
}) + bus.raise(Msg::ManifestOption{
"clboss-min-channel",
Msg::OptionType_String,
Json::Out::direct(default_min_channel.to_sat()),
"Minimum size of channels to make."
}) + bus.raise(Msg::ManifestOption{
"clboss-max-channel",
Msg::OptionType_String,
Json::Out::direct(default_max_channel.to_sat()),
"Maximum size of channels to make."
});
});
bus.subscribe<Msg::Option
>([this](Msg::Option o) {
XMoveFunds: age the clboss-xrebalance layer, runtime-tunable via setconfig Adds periodic askrene-age against the persistent clboss-xrebalance layer so capacity constraints written by inform_channel_constrained do not accumulate forever. Mirrors FundsMover's age_clboss_layer: fires on Msg::TimerRandomHourly, logs num_removed at Debug, and catches RpcError with -32601 stayed at Debug (graceful degradation on CLN without askrene-age) while other codes promote to Warn so sustained aging failure is visible. The aging cutoff is controlled by a new plugin option, clboss-xrebalance-age-secs, default 3600 (1h) to match FundsMover's production value. Operators on networks with slower flows (signet) are expected to widen this; the right value is empirical and will be tuned after observation. The option is registered dynamic=true so the window is mutable at runtime via: lightning-cli setconfig clboss-xrebalance-age-secs <secs> No clboss / lightningd restart required. To support that, three small infrastructure pieces: * Boss::Msg::ManifestOption gains a bool dynamic field (default false; preserves existing behavior). * 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 flag) from Msg::ManifestOption events, then handles incoming Msg::CommandRequest where command == "setconfig" by validating the named option is registered + dynamic and re-raising a fresh Msg::Option on the bus. Existing option handlers re-apply the new value transparently. Contract for any future opt-in to dynamic: at startup lightningd encodes Int / Bool / Flag option values as JSON primitives, but at setconfig time it encodes them as JSON strings. Handlers for dynamic options must accept both Jsmn shapes. The XMoveFunds option handler does this; the contract is documented in SetConfigHandler's header comment so future modules can opt in safely.
2026-06-01 09:31:50 -07:00
/* Msg::Option was originally a startup-only signal
* (delivered once per registered option between
* Manifestation and EndOfOptions), and EndOfOptions
* moves `settings` away on line below. Now that
* SetConfigHandler can re-raise Msg::Option at
* runtime to deliver setconfig updates, this handler
* may receive events for unrelated names long after
* `settings` has been moved -- e.g. a runtime
* `setconfig clboss-xrebalance-age-secs ...` triggers
* a Msg::Option that hits every subscriber.
*
* Drop those silently: none of the options this
* handler owns is registered dynamic, so by the time
* we see a post-EndOfOptions event it cannot
* legitimately apply. This also covers a latent
* assertion crash that pre-existed dynamic options
* (any post-EOO Msg::Option for any name would have
* tripped the old assert(settings) here). */
if (!settings)
return Ev::lift();
auto const& name = o.name;
if (name == "clboss-min-onchain") {
settings->reserve = parse_sats(o.value);
if (settings->reserve == default_reserve)
return Ev::lift();
return Boss::log( bus, Info
, "AmountSettingsHandler: "
"Onchain reserve set by "
"--clboss-min-onchain to "
"%s satoshis."
, std::string(o.value).c_str()
);
} else if (name == "clboss-min-channel") {
settings->min_channel = parse_sats(o.value);
if (settings->min_channel == default_min_channel)
return Ev::lift();
return Boss::log( bus, Info
, "AmountSettingsHandler: "
"Minimum channel size set by "
"--clboss-min-channel to "
"%s satoshis."
, std::string(o.value).c_str()
);
} else if (name == "clboss-max-channel") {
settings->max_channel = parse_sats(o.value);
if (settings->max_channel == default_max_channel)
return Ev::lift();
return Boss::log( bus, Info
, "AmountSettingsHandler: "
"Maximum channel size set by "
"--clboss-max-channel to "
"%s satoshis."
, std::string(o.value).c_str()
);
}
return Ev::lift();
});
bus.subscribe<Msg::EndOfOptions
>([this](Msg::EndOfOptions const& _) {
assert(settings);
auto act = Ev::lift();
/* Validate settings. */
if (settings->min_channel < min_min_channel) {
settings->min_channel = min_min_channel;
act += Boss::log( bus, Info
, "AmountSettingsHandler: "
"--clboss-min-channel too "
"low, forced to %u."
, (unsigned int)
min_min_channel.to_sat()
);
}
if (settings->max_channel < ( max_channel_factor
* settings->min_channel
)) {
settings->max_channel = ( max_channel_factor
* settings->min_channel
);
act += Boss::log( bus, Info
, "AmountSettingsHandler: "
"--clboss-max-channel too "
"low, forced to %u."
, (unsigned int)
settings->max_channel.to_sat()
);
}
/* Compute the rest. */
settings->min_amount = trigger_factor
* settings->min_channel
;
settings->min_remaining = settings->min_amount
+ additional_remaining
;
/* Grab the settings and send it. */
auto msg = std::move(settings);
return act + bus.raise(std::move(*msg));
});
}
public:
Impl() =delete;
Impl(Impl&&) =delete;
Impl(Impl const&) =delete;
explicit
Impl( S::Bus& bus_
) : bus(bus_)
, settings(Util::make_unique<Msg::AmountSettings>())
{ start(); }
};
AmountSettingsHandler::~AmountSettingsHandler() =default;
AmountSettingsHandler::AmountSettingsHandler(AmountSettingsHandler&&) =default;
AmountSettingsHandler::AmountSettingsHandler(S::Bus& bus) : pimpl(Util::make_unique<Impl>(bus)) {}
}}