diff --git a/Boss/Mod/AskreneLayer.cpp b/Boss/Mod/AskreneLayer.cpp new file mode 100644 index 0000000..de4835d --- /dev/null +++ b/Boss/Mod/AskreneLayer.cpp @@ -0,0 +1,106 @@ +#include"Boss/Mod/AskreneLayer.hpp" +#include"Boss/Mod/Rpc.hpp" +#include"Ev/Io.hpp" +#include"Jsmn/Object.hpp" +#include"Json/Out.hpp" +#include"Util/stringify.hpp" +#include + +namespace Boss { namespace Mod { namespace AskreneLayer { + +std::string const clboss_layer_name = "clboss"; + +namespace { + +/* Common machinery for the two inform_channel variants. askrene + * accepts inform=succeeded / constrained / unconstrained as the + * only difference between them; everything else (scid_dir, + * amount_msat, layer) is identical. + */ +Ev::Io +inform_channel( Boss::Mod::Rpc& rpc + , std::string const& layer + , Ln::Scid scid + , std::uint32_t direction + , Ln::Amount amount + , char const* inform + ) { + /* askrene only accepts direction 0 or 1 in + * short_channel_id_dir. All callers feed values from + * CLN's getroutes/sendpay responses, which are + * guaranteed to be 0/1, but guard explicitly: a bad + * direction would produce a syntactically valid but + * semantically wrong RPC param that askrene rejects, and + * the silent-swallow RpcError handler below would drop + * the learning update without a trace. + */ + assert(direction <= 1); + if (direction > 1) + return Ev::lift(); + auto sdir = std::string(scid) + "/" + Util::stringify(direction); + auto parms = Json::Out() + .start_object() + .field("layer", layer) + .field("short_channel_id_dir", sdir) + .field("amount_msat", amount.to_msat()) + .field("inform", std::string(inform)) + .end_object() + ; + return rpc.command( "askrene-inform-channel" + , std::move(parms) + ).then([](Jsmn::Object _) { + return Ev::lift(); + }).catching([](RpcError const&) { + /* Non-fatal -- if the layer is missing (e.g. layer- + * create failed at startup on CLN < v24.11), + * subsequent getroutes calls simply will not benefit + * from the constraint. Better to degrade learning + * than to crash the caller. + */ + return Ev::lift(); + }); +} + +} + +Ev::Io +inform_channel_constrained( Boss::Mod::Rpc& rpc + , std::string const& layer + , Ln::Scid scid + , std::uint32_t direction + , Ln::Amount amount + ) { + return inform_channel(rpc, layer, scid, direction, amount, "constrained"); +} + +Ev::Io +inform_channel_unconstrained( Boss::Mod::Rpc& rpc + , std::string const& layer + , Ln::Scid scid + , std::uint32_t direction + , Ln::Amount amount + ) { + return inform_channel(rpc, layer, scid, direction, amount, "unconstrained"); +} + +Ev::Io +disable_node( Boss::Mod::Rpc& rpc + , std::string const& layer + , Ln::NodeId node + ) { + auto parms = Json::Out() + .start_object() + .field("layer", layer) + .field("node", std::string(node)) + .end_object() + ; + return rpc.command( "askrene-disable-node" + , std::move(parms) + ).then([](Jsmn::Object _) { + return Ev::lift(); + }).catching([](RpcError const&) { + return Ev::lift(); + }); +} + +}}} diff --git a/Boss/Mod/AskreneLayer.hpp b/Boss/Mod/AskreneLayer.hpp new file mode 100644 index 0000000..63a7ccc --- /dev/null +++ b/Boss/Mod/AskreneLayer.hpp @@ -0,0 +1,74 @@ +#ifndef BOSS_MOD_ASKRENELAYER_HPP_ +#define BOSS_MOD_ASKRENELAYER_HPP_ + +#include"Ev/Io.hpp" +#include"Ln/Amount.hpp" +#include"Ln/NodeId.hpp" +#include"Ln/Scid.hpp" +#include +#include + +namespace Boss { namespace Mod { class Rpc; } } + +namespace Boss { namespace Mod { namespace AskreneLayer { + +/* Name of the persistent askrene layer that CLBOSS subsystems + * write failure-feedback and (optionally) success-observations + * into. Following the xpay convention -- the layer is named + * after the plugin that owns it. All CLBOSS writes go to this + * layer; downstream getroutes calls that include it in their + * layers array benefit from the accumulated knowledge. + */ +extern std::string const clboss_layer_name; + +/* Tell askrene that a directed channel could not push at least + * the given amount recently. Future getroutes calls that + * include the clboss layer will treat this as an upper-bound + * constraint and steer around it. Non-fatal on RpcError: if + * the layer is missing (e.g. CLN < v24.11 where askrene layers + * are unavailable) the call silently completes -- failure-mode + * is degraded learning, not crashed caller. + */ +Ev::Io inform_channel_constrained( Boss::Mod::Rpc& rpc + , std::string const& layer + , Ln::Scid scid + , std::uint32_t direction + , Ln::Amount amount + ); + +/* Tell askrene that a directed channel successfully pushed at + * least the given amount recently. Future getroutes calls + * that include the layer will treat this as a lower-bound on + * the channel's capacity (min=amount, max=NULL). Non-fatal + * on RpcError, same rationale as inform_channel_constrained. + * + * Naming note: this maps to askrene's `inform=unconstrained` + * mode (which means "no upper-bound; minimum is amount"). + * Askrene also has an `inform=succeeded` mode but as of + * v26.06 that branch is a no-op stub in askrene.c + * (`FIXME: We could do something useful here!`). xpay uses + * `inform=unconstrained` for the same after-success + * lower-bound-raise pattern (see plugins/xpay/xpay.c + * around `"We learned something about prior nodes"`). + */ +Ev::Io inform_channel_unconstrained( Boss::Mod::Rpc& rpc + , std::string const& layer + , Ln::Scid scid + , std::uint32_t direction + , Ln::Amount amount + ); + +/* Tell askrene to avoid the given node entirely for routes + * that include this layer. Used for NODE-level onion failures + * (failcode bit 0x2000) and for unparsable-onion fallback, + * where we cannot pin the failure to a specific channel. + * Non-fatal on RpcError. + */ +Ev::Io disable_node( Boss::Mod::Rpc& rpc + , std::string const& layer + , Ln::NodeId node + ); + +}}} + +#endif /* !defined(BOSS_MOD_ASKRENELAYER_HPP_) */ diff --git a/Boss/Mod/FundsMover/Attempter.cpp b/Boss/Mod/FundsMover/Attempter.cpp index ea51a58..030993c 100644 --- a/Boss/Mod/FundsMover/Attempter.cpp +++ b/Boss/Mod/FundsMover/Attempter.cpp @@ -1,3 +1,4 @@ +#include"Boss/Mod/AskreneLayer.hpp" #include"Boss/Mod/FundsMover/Attempter.hpp" #include"Boss/Mod/FundsMover/create_label.hpp" #include"Boss/Mod/Rpc.hpp" @@ -16,22 +17,6 @@ #include #include -namespace { - -/* Name of the persistent askrene layer that holds CLBOSS's - * failure-feedback knowledge -- channel capacity constraints - * (askrene-inform-channel) and node-level exclusions - * (askrene-disable-node) accumulated across attempts. Created - * idempotently at startup by Boss::Mod::FundsMover::Main. - * - * Naming follows the convention established by plugins/xpay - * (which uses "xpay"): the persistent shared-knowledge layer is - * named after the plugin that owns it. - */ -auto const clboss_layer = std::string("clboss"); - -} - namespace Boss { namespace Mod { namespace FundsMover { class Attempter::Impl : public std::enable_shared_from_this { @@ -175,7 +160,7 @@ private: .start_array("layers") .entry("auto.localchans") .entry("auto.sourcefree") - .entry(clboss_layer) + .entry(Boss::Mod::AskreneLayer::clboss_layer_name) .end_array() .field("maxfee_msat", route_maxfee.to_msat()) @@ -397,58 +382,6 @@ private: }); } - /* Tell askrene about a failed-channel constraint so future - * getroutes calls steer around it. Recorded into the - * persistent "clboss" layer; benefits all subsequent attempts - * and (when they migrate to consume the same layer) other - * CLBOSS subsystems too. - */ - Ev::Io inform_channel_constrained( Ln::Scid scid - , int dir - , Ln::Amount at - ) { - auto sdir = std::string(scid) + "/" + Util::stringify(dir); - auto parms = Json::Out() - .start_object() - .field("layer", clboss_layer) - .field("short_channel_id_dir", sdir) - .field("amount_msat", at.to_msat()) - .field("inform", "constrained") - .end_object() - ; - return rpc.command( "askrene-inform-channel" - , std::move(parms) - ).then([](Jsmn::Object _) { - return Ev::lift(); - }).catching([](RpcError const&) { - /* Non-fatal -- if the layer is missing - * (e.g. layer-create failed at startup on a - * CLN < v24.11), subsequent getroutes calls - * simply will not benefit from the constraint. - */ - return Ev::lift(); - }); - } - - /* Tell askrene to avoid a node entirely; same persistent - * layer. Used for NODE-level onion failures (failcode bit - * 0x2000) and for unparsable-onion fallback. - */ - Ev::Io disable_node(Ln::NodeId node) { - auto parms = Json::Out() - .start_object() - .field("layer", clboss_layer) - .field("node", std::string(node)) - .end_object() - ; - return rpc.command( "askrene-disable-node" - , std::move(parms) - ).then([](Jsmn::Object _) { - return Ev::lift(); - }).catching([](RpcError const&) { - return Ev::lift(); - }); - } Ev::Io sendpay() { auto payment_hash = std::make_shared(); @@ -612,7 +545,11 @@ private: ; /* 0x2000 == NODE level error. */ if ((fail & 0x2000)) - feedback = disable_node(enode); + feedback = Boss::Mod::AskreneLayer::disable_node( + rpc, + Boss::Mod::AskreneLayer::clboss_layer_name, + enode + ); else /* The amount to pass to askrene-inform-channel * is the HTLC amount attempted on the specific @@ -623,7 +560,9 @@ private: * [1, route.size()] here and route[eidx - 1] * describes the failing channel. */ - feedback = inform_channel_constrained( + feedback = Boss::Mod::AskreneLayer::inform_channel_constrained( + rpc, + Boss::Mod::AskreneLayer::clboss_layer_name, echan, edir, route[eidx - 1].amount_msat ); diff --git a/Boss/Mod/FundsMover/Main.cpp b/Boss/Mod/FundsMover/Main.cpp index fc05ef6..275cc7f 100644 --- a/Boss/Mod/FundsMover/Main.cpp +++ b/Boss/Mod/FundsMover/Main.cpp @@ -1,3 +1,4 @@ +#include"Boss/Mod/AskreneLayer.hpp" #include"Boss/Mod/FundsMover/Claimer.hpp" #include"Boss/Mod/FundsMover/Main.hpp" #include"Boss/Mod/FundsMover/Runner.hpp" @@ -133,7 +134,8 @@ private: return Ev::lift().then([this]() { auto parms = Json::Out() .start_object() - .field("layer", "clboss") + .field("layer", + Boss::Mod::AskreneLayer::clboss_layer_name) .field("persistent", true) .end_object() ; diff --git a/Makefile.am b/Makefile.am index f2cc462..03f1c98 100644 --- a/Makefile.am +++ b/Makefile.am @@ -69,6 +69,8 @@ libclboss_la_SOURCES = \ Boss/Mod/ActiveProber.hpp \ Boss/Mod/AmountSettingsHandler.cpp \ Boss/Mod/AmountSettingsHandler.hpp \ + Boss/Mod/AskreneLayer.cpp \ + Boss/Mod/AskreneLayer.hpp \ Boss/Mod/AutoDisconnector.cpp \ Boss/Mod/AutoDisconnector.hpp \ Boss/Mod/AvailableRpcCommandsAnnouncer.cpp \