2020-10-01 14:54:42 +08:00
|
|
|
#include"Boss/Mod/ChannelCandidateMatchmaker.hpp"
|
|
|
|
|
#include"Boss/Mod/Rpc.hpp"
|
2021-12-07 16:57:10 +08:00
|
|
|
#include"Boss/Msg/AmountSettings.hpp"
|
2020-10-01 14:54:42 +08:00
|
|
|
#include"Boss/Msg/Init.hpp"
|
|
|
|
|
#include"Boss/Msg/PatronizeChannelCandidate.hpp"
|
2020-10-01 15:13:26 +08:00
|
|
|
#include"Boss/Msg/PreinvestigateChannelCandidates.hpp"
|
2020-10-01 14:54:42 +08:00
|
|
|
#include"Boss/Msg/ProposeChannelCandidates.hpp"
|
|
|
|
|
#include"Boss/concurrent.hpp"
|
|
|
|
|
#include"Boss/log.hpp"
|
|
|
|
|
#include"Ev/Io.hpp"
|
|
|
|
|
#include"Ev/yield.hpp"
|
|
|
|
|
#include"Jsmn/Object.hpp"
|
|
|
|
|
#include"Json/Out.hpp"
|
2021-12-07 16:57:10 +08:00
|
|
|
#include"Ln/Amount.hpp"
|
2020-10-01 14:54:42 +08:00
|
|
|
#include"Ln/NodeId.hpp"
|
|
|
|
|
#include"S/Bus.hpp"
|
|
|
|
|
#include<memory>
|
|
|
|
|
#include<sstream>
|
|
|
|
|
|
|
|
|
|
namespace Boss { namespace Mod {
|
|
|
|
|
|
|
|
|
|
/* This is effectively a single run of the matchmaker. */
|
|
|
|
|
class ChannelCandidateMatchmaker::Run
|
|
|
|
|
: public std::enable_shared_from_this<Run> {
|
|
|
|
|
private:
|
|
|
|
|
S::Bus& bus;
|
|
|
|
|
Boss::Mod::Rpc& rpc;
|
|
|
|
|
|
|
|
|
|
Ln::NodeId proposal;
|
|
|
|
|
std::queue<Ln::NodeId> guide;
|
|
|
|
|
|
2021-12-07 16:57:10 +08:00
|
|
|
Ln::Amount min_channel;
|
|
|
|
|
|
2020-10-01 14:54:42 +08:00
|
|
|
explicit
|
|
|
|
|
Run( S::Bus& bus_
|
|
|
|
|
, Boss::Mod::Rpc& rpc_
|
|
|
|
|
, Ln::NodeId proposal_
|
|
|
|
|
, std::queue<Ln::NodeId> guide_
|
2021-12-07 16:57:10 +08:00
|
|
|
, Ln::Amount min_channel_
|
2020-10-01 14:54:42 +08:00
|
|
|
) : bus(bus_)
|
|
|
|
|
, rpc(rpc_)
|
|
|
|
|
, proposal(std::move(proposal_))
|
|
|
|
|
, guide(std::move(guide_))
|
2021-12-07 16:57:10 +08:00
|
|
|
, min_channel(min_channel_)
|
2020-10-01 14:54:42 +08:00
|
|
|
{ }
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
Run() =delete;
|
|
|
|
|
Run(Run&&) =delete;
|
|
|
|
|
Run(Run const&) =delete;
|
|
|
|
|
|
|
|
|
|
static
|
|
|
|
|
std::shared_ptr<Run>
|
|
|
|
|
create( S::Bus& bus
|
|
|
|
|
, Boss::Mod::Rpc& rpc
|
|
|
|
|
, Ln::NodeId proposal
|
|
|
|
|
, std::queue<Ln::NodeId> guide
|
2021-12-07 16:57:10 +08:00
|
|
|
, Ln::Amount min_channel
|
2020-10-01 14:54:42 +08:00
|
|
|
) {
|
|
|
|
|
return std::shared_ptr<Run>(
|
|
|
|
|
new Run( bus
|
|
|
|
|
, rpc
|
|
|
|
|
, std::move(proposal)
|
|
|
|
|
, std::move(guide)
|
2021-12-07 16:57:10 +08:00
|
|
|
, min_channel
|
2020-10-01 14:54:42 +08:00
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ev::Io<void> run() {
|
|
|
|
|
auto self = shared_from_this();
|
|
|
|
|
return self->core_run().then([self]() {
|
|
|
|
|
return Ev::lift();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
private:
|
|
|
|
|
Ev::Io<void> core_run() {
|
|
|
|
|
return Ev::yield().then([this]() {
|
|
|
|
|
if (guide.empty())
|
|
|
|
|
/* Failed. */
|
|
|
|
|
return Boss::log( bus, Debug
|
|
|
|
|
, "ChannelCandidateMatchmaker:"
|
|
|
|
|
" Could not find patron for "
|
|
|
|
|
"%s."
|
|
|
|
|
, std::string(proposal
|
|
|
|
|
).c_str()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return step();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
Ev::Io<void> step() {
|
|
|
|
|
auto target = std::move(guide.front());
|
|
|
|
|
guide.pop();
|
|
|
|
|
|
Matchmaker, ActiveProber: migrate getroute -> getroutes for CLN v26.06 compat
Continuation of the v26.06 migration started in the Dowser
commit. Two more getroute call sites:
ChannelCandidateMatchmaker.cpp and ActiveProber.cpp. Both use
maxparts=1 since each wants a single route, not a flow estimate.
ActiveProber probes the local node's own outbound liquidity, so
it uses layers=["auto.localchans","auto.sourcefree"] per the
standardized recipe. Matchmaker probes from a remote source
(a candidate patron) to a remote target, so it passes an empty
layers array -- auto.localchans would inject our private local
channels into a foreign source, and auto.sourcefree would zero
out the source's outgoing fees, either of which could make
askrene pick a patron the proposal cannot actually reach via
public topology. maxfee_msat is 1% of the probe amount in both
cases.
Matchmaker is a near-clean swap. The patron id (route[0].id in
the old getroute shape) is read from routes[0].path[0] in the
new getroutes shape; at the parse site we bridge the v26.06+
name (node_id_out) with its deprecated pre-v26.06 predecessor
(next_node_id) via has()/ternary, so the code works on either
CLN flavor.
ActiveProber is more involved. The previous code stashed
res["route"] as a Jsmn::Object and later appended hops from it
directly into the sendpay route parameter, relying on the fact
that the old getroute hop shape (id/channel/direction/
amount_msat/delay/style) was already sendpay-compatible. The
new getroutes path[] shape is NOT directly sendpay-compatible
(different field names, short_channel_id_dir encodes scid and
direction together), so we now extract path[0] into typed
values (id1, chan1, direction1, amount1, delay1) and rebuild
the sendpay hop1 explicitly from those. The Jsmn::Object route
member is dropped. The hop-field reads in ActiveProber bridge
between v26.06+ (node_id_out / amount_out_msat / cltv_out) and
the deprecated pre-v26.06 names (next_node_id / amount_msat /
delay) via the same has()/ternary pattern.
short_channel_id_dir splits on '/' with an explicit npos check;
a missing slash throws Jsmn::TypeError so the surrounding parse-
error log path catches it cleanly. Without the check,
sdir.substr(0, npos) and sdir.substr(npos + 1) would feed
malformed input into Ln::Scid (which throws
std::invalid_argument, not caught by the Jsmn::TypeError
handler) or into std::stoul (silently producing a wrong
direction).
ActiveProber also drops its vestigial exclude=[self_id]
parameter: askrene's source/destination model naturally excludes
self when source != self, which is always the case here (the
probe always flows from peer outward, never back through us).
Three of the four getroute call sites in CLBOSS are now on
getroutes; the fourth (FundsMover/Attempter) is deferred to PR2
because its exclude-vector pattern encodes failure feedback from
real payment attempts and warrants a redesign around
askrene-inform-channel rather than a mechanical port.
2026-05-19 13:40:52 -07:00
|
|
|
auto probe_amount = 2.0 * min_channel;
|
2020-10-01 14:54:42 +08:00
|
|
|
auto parms = Json::Out()
|
|
|
|
|
.start_object()
|
Matchmaker, ActiveProber: migrate getroute -> getroutes for CLN v26.06 compat
Continuation of the v26.06 migration started in the Dowser
commit. Two more getroute call sites:
ChannelCandidateMatchmaker.cpp and ActiveProber.cpp. Both use
maxparts=1 since each wants a single route, not a flow estimate.
ActiveProber probes the local node's own outbound liquidity, so
it uses layers=["auto.localchans","auto.sourcefree"] per the
standardized recipe. Matchmaker probes from a remote source
(a candidate patron) to a remote target, so it passes an empty
layers array -- auto.localchans would inject our private local
channels into a foreign source, and auto.sourcefree would zero
out the source's outgoing fees, either of which could make
askrene pick a patron the proposal cannot actually reach via
public topology. maxfee_msat is 1% of the probe amount in both
cases.
Matchmaker is a near-clean swap. The patron id (route[0].id in
the old getroute shape) is read from routes[0].path[0] in the
new getroutes shape; at the parse site we bridge the v26.06+
name (node_id_out) with its deprecated pre-v26.06 predecessor
(next_node_id) via has()/ternary, so the code works on either
CLN flavor.
ActiveProber is more involved. The previous code stashed
res["route"] as a Jsmn::Object and later appended hops from it
directly into the sendpay route parameter, relying on the fact
that the old getroute hop shape (id/channel/direction/
amount_msat/delay/style) was already sendpay-compatible. The
new getroutes path[] shape is NOT directly sendpay-compatible
(different field names, short_channel_id_dir encodes scid and
direction together), so we now extract path[0] into typed
values (id1, chan1, direction1, amount1, delay1) and rebuild
the sendpay hop1 explicitly from those. The Jsmn::Object route
member is dropped. The hop-field reads in ActiveProber bridge
between v26.06+ (node_id_out / amount_out_msat / cltv_out) and
the deprecated pre-v26.06 names (next_node_id / amount_msat /
delay) via the same has()/ternary pattern.
short_channel_id_dir splits on '/' with an explicit npos check;
a missing slash throws Jsmn::TypeError so the surrounding parse-
error log path catches it cleanly. Without the check,
sdir.substr(0, npos) and sdir.substr(npos + 1) would feed
malformed input into Ln::Scid (which throws
std::invalid_argument, not caught by the Jsmn::TypeError
handler) or into std::stoul (silently producing a wrong
direction).
ActiveProber also drops its vestigial exclude=[self_id]
parameter: askrene's source/destination model naturally excludes
self when source != self, which is always the case here (the
probe always flows from peer outward, never back through us).
Three of the four getroute call sites in CLBOSS are now on
getroutes; the fourth (FundsMover/Attempter) is deferred to PR2
because its exclude-vector pattern encodes failure feedback from
real payment attempts and warrants a redesign around
askrene-inform-channel rather than a mechanical port.
2026-05-19 13:40:52 -07:00
|
|
|
.field("source", std::string(proposal))
|
|
|
|
|
.field("destination", std::string(target))
|
|
|
|
|
/* 2x min_channel because the dowser will
|
|
|
|
|
* halve the channel capacity of the first
|
|
|
|
|
* hop.
|
2021-12-07 16:57:10 +08:00
|
|
|
*/
|
Matchmaker, ActiveProber: migrate getroute -> getroutes for CLN v26.06 compat
Continuation of the v26.06 migration started in the Dowser
commit. Two more getroute call sites:
ChannelCandidateMatchmaker.cpp and ActiveProber.cpp. Both use
maxparts=1 since each wants a single route, not a flow estimate.
ActiveProber probes the local node's own outbound liquidity, so
it uses layers=["auto.localchans","auto.sourcefree"] per the
standardized recipe. Matchmaker probes from a remote source
(a candidate patron) to a remote target, so it passes an empty
layers array -- auto.localchans would inject our private local
channels into a foreign source, and auto.sourcefree would zero
out the source's outgoing fees, either of which could make
askrene pick a patron the proposal cannot actually reach via
public topology. maxfee_msat is 1% of the probe amount in both
cases.
Matchmaker is a near-clean swap. The patron id (route[0].id in
the old getroute shape) is read from routes[0].path[0] in the
new getroutes shape; at the parse site we bridge the v26.06+
name (node_id_out) with its deprecated pre-v26.06 predecessor
(next_node_id) via has()/ternary, so the code works on either
CLN flavor.
ActiveProber is more involved. The previous code stashed
res["route"] as a Jsmn::Object and later appended hops from it
directly into the sendpay route parameter, relying on the fact
that the old getroute hop shape (id/channel/direction/
amount_msat/delay/style) was already sendpay-compatible. The
new getroutes path[] shape is NOT directly sendpay-compatible
(different field names, short_channel_id_dir encodes scid and
direction together), so we now extract path[0] into typed
values (id1, chan1, direction1, amount1, delay1) and rebuild
the sendpay hop1 explicitly from those. The Jsmn::Object route
member is dropped. The hop-field reads in ActiveProber bridge
between v26.06+ (node_id_out / amount_out_msat / cltv_out) and
the deprecated pre-v26.06 names (next_node_id / amount_msat /
delay) via the same has()/ternary pattern.
short_channel_id_dir splits on '/' with an explicit npos check;
a missing slash throws Jsmn::TypeError so the surrounding parse-
error log path catches it cleanly. Without the check,
sdir.substr(0, npos) and sdir.substr(npos + 1) would feed
malformed input into Ln::Scid (which throws
std::invalid_argument, not caught by the Jsmn::TypeError
handler) or into std::stoul (silently producing a wrong
direction).
ActiveProber also drops its vestigial exclude=[self_id]
parameter: askrene's source/destination model naturally excludes
self when source != self, which is always the case here (the
probe always flows from peer outward, never back through us).
Three of the four getroute call sites in CLBOSS are now on
getroutes; the fourth (FundsMover/Attempter) is deferred to PR2
because its exclude-vector pattern encodes failure feedback from
real payment attempts and warrants a redesign around
askrene-inform-channel rather than a mechanical port.
2026-05-19 13:40:52 -07:00
|
|
|
.field("amount_msat", probe_amount.to_msat())
|
|
|
|
|
/* No layers: source is a remote node
|
|
|
|
|
* (proposal), not us, so the
|
|
|
|
|
* auto.localchans / auto.sourcefree
|
|
|
|
|
* helpers do not apply -- they would
|
|
|
|
|
* inject our private local channels and
|
|
|
|
|
* zero out the source's outgoing fees,
|
|
|
|
|
* either of which could make askrene
|
|
|
|
|
* pick a patron that the proposal cannot
|
|
|
|
|
* actually reach via public topology.
|
2020-10-01 14:54:42 +08:00
|
|
|
*/
|
Matchmaker, ActiveProber: migrate getroute -> getroutes for CLN v26.06 compat
Continuation of the v26.06 migration started in the Dowser
commit. Two more getroute call sites:
ChannelCandidateMatchmaker.cpp and ActiveProber.cpp. Both use
maxparts=1 since each wants a single route, not a flow estimate.
ActiveProber probes the local node's own outbound liquidity, so
it uses layers=["auto.localchans","auto.sourcefree"] per the
standardized recipe. Matchmaker probes from a remote source
(a candidate patron) to a remote target, so it passes an empty
layers array -- auto.localchans would inject our private local
channels into a foreign source, and auto.sourcefree would zero
out the source's outgoing fees, either of which could make
askrene pick a patron the proposal cannot actually reach via
public topology. maxfee_msat is 1% of the probe amount in both
cases.
Matchmaker is a near-clean swap. The patron id (route[0].id in
the old getroute shape) is read from routes[0].path[0] in the
new getroutes shape; at the parse site we bridge the v26.06+
name (node_id_out) with its deprecated pre-v26.06 predecessor
(next_node_id) via has()/ternary, so the code works on either
CLN flavor.
ActiveProber is more involved. The previous code stashed
res["route"] as a Jsmn::Object and later appended hops from it
directly into the sendpay route parameter, relying on the fact
that the old getroute hop shape (id/channel/direction/
amount_msat/delay/style) was already sendpay-compatible. The
new getroutes path[] shape is NOT directly sendpay-compatible
(different field names, short_channel_id_dir encodes scid and
direction together), so we now extract path[0] into typed
values (id1, chan1, direction1, amount1, delay1) and rebuild
the sendpay hop1 explicitly from those. The Jsmn::Object route
member is dropped. The hop-field reads in ActiveProber bridge
between v26.06+ (node_id_out / amount_out_msat / cltv_out) and
the deprecated pre-v26.06 names (next_node_id / amount_msat /
delay) via the same has()/ternary pattern.
short_channel_id_dir splits on '/' with an explicit npos check;
a missing slash throws Jsmn::TypeError so the surrounding parse-
error log path catches it cleanly. Without the check,
sdir.substr(0, npos) and sdir.substr(npos + 1) would feed
malformed input into Ln::Scid (which throws
std::invalid_argument, not caught by the Jsmn::TypeError
handler) or into std::stoul (silently producing a wrong
direction).
ActiveProber also drops its vestigial exclude=[self_id]
parameter: askrene's source/destination model naturally excludes
self when source != self, which is always the case here (the
probe always flows from peer outward, never back through us).
Three of the four getroute call sites in CLBOSS are now on
getroutes; the fourth (FundsMover/Attempter) is deferred to PR2
because its exclude-vector pattern encodes failure feedback from
real payment attempts and warrants a redesign around
askrene-inform-channel rather than a mechanical port.
2026-05-19 13:40:52 -07:00
|
|
|
.start_array("layers").end_array()
|
|
|
|
|
/* Generous max-fee tolerance for what is a
|
|
|
|
|
* route-discovery probe, not an actual
|
|
|
|
|
* payment.
|
|
|
|
|
*/
|
|
|
|
|
.field("maxfee_msat",
|
|
|
|
|
(probe_amount * 0.01).to_msat())
|
|
|
|
|
.field("final_cltv", 14)
|
|
|
|
|
.field("maxparts", 1)
|
2020-10-01 14:54:42 +08:00
|
|
|
.end_object()
|
|
|
|
|
;
|
Matchmaker, ActiveProber: migrate getroute -> getroutes for CLN v26.06 compat
Continuation of the v26.06 migration started in the Dowser
commit. Two more getroute call sites:
ChannelCandidateMatchmaker.cpp and ActiveProber.cpp. Both use
maxparts=1 since each wants a single route, not a flow estimate.
ActiveProber probes the local node's own outbound liquidity, so
it uses layers=["auto.localchans","auto.sourcefree"] per the
standardized recipe. Matchmaker probes from a remote source
(a candidate patron) to a remote target, so it passes an empty
layers array -- auto.localchans would inject our private local
channels into a foreign source, and auto.sourcefree would zero
out the source's outgoing fees, either of which could make
askrene pick a patron the proposal cannot actually reach via
public topology. maxfee_msat is 1% of the probe amount in both
cases.
Matchmaker is a near-clean swap. The patron id (route[0].id in
the old getroute shape) is read from routes[0].path[0] in the
new getroutes shape; at the parse site we bridge the v26.06+
name (node_id_out) with its deprecated pre-v26.06 predecessor
(next_node_id) via has()/ternary, so the code works on either
CLN flavor.
ActiveProber is more involved. The previous code stashed
res["route"] as a Jsmn::Object and later appended hops from it
directly into the sendpay route parameter, relying on the fact
that the old getroute hop shape (id/channel/direction/
amount_msat/delay/style) was already sendpay-compatible. The
new getroutes path[] shape is NOT directly sendpay-compatible
(different field names, short_channel_id_dir encodes scid and
direction together), so we now extract path[0] into typed
values (id1, chan1, direction1, amount1, delay1) and rebuild
the sendpay hop1 explicitly from those. The Jsmn::Object route
member is dropped. The hop-field reads in ActiveProber bridge
between v26.06+ (node_id_out / amount_out_msat / cltv_out) and
the deprecated pre-v26.06 names (next_node_id / amount_msat /
delay) via the same has()/ternary pattern.
short_channel_id_dir splits on '/' with an explicit npos check;
a missing slash throws Jsmn::TypeError so the surrounding parse-
error log path catches it cleanly. Without the check,
sdir.substr(0, npos) and sdir.substr(npos + 1) would feed
malformed input into Ln::Scid (which throws
std::invalid_argument, not caught by the Jsmn::TypeError
handler) or into std::stoul (silently producing a wrong
direction).
ActiveProber also drops its vestigial exclude=[self_id]
parameter: askrene's source/destination model naturally excludes
self when source != self, which is always the case here (the
probe always flows from peer outward, never back through us).
Three of the four getroute call sites in CLBOSS are now on
getroutes; the fourth (FundsMover/Attempter) is deferred to PR2
because its exclude-vector pattern encodes failure feedback from
real payment attempts and warrants a redesign around
askrene-inform-channel rather than a mechanical port.
2026-05-19 13:40:52 -07:00
|
|
|
return rpc.command("getroutes", std::move(parms)
|
2020-10-01 14:54:42 +08:00
|
|
|
).then([this](Jsmn::Object res) {
|
|
|
|
|
auto patron = Ln::NodeId();
|
|
|
|
|
try {
|
Require the CLN v26.06 getroutes fields; refuse to start on older CLN
The pre-v26.06 getroutes compatibility fallback read the deprecated
per-hop fields (next_node_id / amount_msat / delay), whose values CLN
defines as the in-side of each hop -- one hop shifted from the
out-side values (node_id_out / amount_out_msat / cltv_out, shipped in
v26.06) that sendpay routes must be built from. On stock CLN
v24.11..v26.04 the fallback therefore built mispriced routes: small
overpays on the routes that survived, spurious FEE_INSUFFICIENT /
INCORRECT_CLTV_EXPIRY failures on the rest, and -- worst -- those
failures hard-excluded healthy channels in the persistent
failure-learning layer for hours, compounding across restarts.
Drop the fallback entirely and enforce the requirement twice:
- Initiator gains a CLN version gate, ordered before the database and
signer steps so a refusal leaves no on-disk trace: a getinfo
version older than v26.06 logs a detailed Error and aborts. The
new clboss-skip-cln-version-check flag bypasses the gate for
operators whose older CLN carries a backport of the v26.06
getroutes fields (the version string alone cannot show that); an
unparseable version string warns and proceeds rather than locking
out custom builds.
- The three getroutes parse sites (FundsMover/Attempter,
ActiveProber, ChannelCandidateMatchmaker) verify the new fields on
every response and fail the attempt with a clear Error if absent,
so a mistakenly bypassed gate degrades into loud per-attempt
failures rather than shifted routes.
CHANGELOG.md gains a prominent BREAKING entry; README documents the
requirement and the escape hatch. Users on older CLN releases stay
on CLBOSS 0.16.x, which uses the legacy getroute/pay APIs those
versions still provide.
2026-07-02 14:08:15 -07:00
|
|
|
/* getroutes' path[K].node_id_out shipped in
|
|
|
|
|
* CLN v26.06. No fallback to the
|
|
|
|
|
* deprecated next_node_id: the Initiator
|
|
|
|
|
* version gate refuses stock older CLN at
|
|
|
|
|
* startup, and this check keeps a bypassed
|
|
|
|
|
* gate (clboss-skip-cln-version-check)
|
|
|
|
|
* loud instead of subtly wrong.
|
Matchmaker, ActiveProber: migrate getroute -> getroutes for CLN v26.06 compat
Continuation of the v26.06 migration started in the Dowser
commit. Two more getroute call sites:
ChannelCandidateMatchmaker.cpp and ActiveProber.cpp. Both use
maxparts=1 since each wants a single route, not a flow estimate.
ActiveProber probes the local node's own outbound liquidity, so
it uses layers=["auto.localchans","auto.sourcefree"] per the
standardized recipe. Matchmaker probes from a remote source
(a candidate patron) to a remote target, so it passes an empty
layers array -- auto.localchans would inject our private local
channels into a foreign source, and auto.sourcefree would zero
out the source's outgoing fees, either of which could make
askrene pick a patron the proposal cannot actually reach via
public topology. maxfee_msat is 1% of the probe amount in both
cases.
Matchmaker is a near-clean swap. The patron id (route[0].id in
the old getroute shape) is read from routes[0].path[0] in the
new getroutes shape; at the parse site we bridge the v26.06+
name (node_id_out) with its deprecated pre-v26.06 predecessor
(next_node_id) via has()/ternary, so the code works on either
CLN flavor.
ActiveProber is more involved. The previous code stashed
res["route"] as a Jsmn::Object and later appended hops from it
directly into the sendpay route parameter, relying on the fact
that the old getroute hop shape (id/channel/direction/
amount_msat/delay/style) was already sendpay-compatible. The
new getroutes path[] shape is NOT directly sendpay-compatible
(different field names, short_channel_id_dir encodes scid and
direction together), so we now extract path[0] into typed
values (id1, chan1, direction1, amount1, delay1) and rebuild
the sendpay hop1 explicitly from those. The Jsmn::Object route
member is dropped. The hop-field reads in ActiveProber bridge
between v26.06+ (node_id_out / amount_out_msat / cltv_out) and
the deprecated pre-v26.06 names (next_node_id / amount_msat /
delay) via the same has()/ternary pattern.
short_channel_id_dir splits on '/' with an explicit npos check;
a missing slash throws Jsmn::TypeError so the surrounding parse-
error log path catches it cleanly. Without the check,
sdir.substr(0, npos) and sdir.substr(npos + 1) would feed
malformed input into Ln::Scid (which throws
std::invalid_argument, not caught by the Jsmn::TypeError
handler) or into std::stoul (silently producing a wrong
direction).
ActiveProber also drops its vestigial exclude=[self_id]
parameter: askrene's source/destination model naturally excludes
self when source != self, which is always the case here (the
probe always flows from peer outward, never back through us).
Three of the four getroute call sites in CLBOSS are now on
getroutes; the fourth (FundsMover/Attempter) is deferred to PR2
because its exclude-vector pattern encodes failure feedback from
real payment attempts and warrants a redesign around
askrene-inform-channel rather than a mechanical port.
2026-05-19 13:40:52 -07:00
|
|
|
*/
|
|
|
|
|
auto path0 = res["routes"][0]["path"][0];
|
Require the CLN v26.06 getroutes fields; refuse to start on older CLN
The pre-v26.06 getroutes compatibility fallback read the deprecated
per-hop fields (next_node_id / amount_msat / delay), whose values CLN
defines as the in-side of each hop -- one hop shifted from the
out-side values (node_id_out / amount_out_msat / cltv_out, shipped in
v26.06) that sendpay routes must be built from. On stock CLN
v24.11..v26.04 the fallback therefore built mispriced routes: small
overpays on the routes that survived, spurious FEE_INSUFFICIENT /
INCORRECT_CLTV_EXPIRY failures on the rest, and -- worst -- those
failures hard-excluded healthy channels in the persistent
failure-learning layer for hours, compounding across restarts.
Drop the fallback entirely and enforce the requirement twice:
- Initiator gains a CLN version gate, ordered before the database and
signer steps so a refusal leaves no on-disk trace: a getinfo
version older than v26.06 logs a detailed Error and aborts. The
new clboss-skip-cln-version-check flag bypasses the gate for
operators whose older CLN carries a backport of the v26.06
getroutes fields (the version string alone cannot show that); an
unparseable version string warns and proceeds rather than locking
out custom builds.
- The three getroutes parse sites (FundsMover/Attempter,
ActiveProber, ChannelCandidateMatchmaker) verify the new fields on
every response and fail the attempt with a clear Error if absent,
so a mistakenly bypassed gate degrades into loud per-attempt
failures rather than shifted routes.
CHANGELOG.md gains a prominent BREAKING entry; README documents the
requirement and the escape hatch. Users on older CLN releases stay
on CLBOSS 0.16.x, which uses the legacy getroute/pay APIs those
versions still provide.
2026-07-02 14:08:15 -07:00
|
|
|
if (!path0.has("node_id_out"))
|
|
|
|
|
return Boss::log( bus, Error
|
|
|
|
|
, "ChannelCandidateMatchmaker: "
|
|
|
|
|
"getroutes hop lacks "
|
|
|
|
|
"node_id_out; CLBOSS "
|
|
|
|
|
"requires CLN v26.06+ (or "
|
|
|
|
|
"a backport of the "
|
|
|
|
|
"getroutes fields)."
|
|
|
|
|
).then([]()
|
|
|
|
|
-> Ev::Io<void>{
|
|
|
|
|
throw RpcError( "getroutes"
|
|
|
|
|
, Jsmn::Object()
|
|
|
|
|
);
|
|
|
|
|
});
|
2020-10-01 14:54:42 +08:00
|
|
|
patron = Ln::NodeId(std::string(
|
Require the CLN v26.06 getroutes fields; refuse to start on older CLN
The pre-v26.06 getroutes compatibility fallback read the deprecated
per-hop fields (next_node_id / amount_msat / delay), whose values CLN
defines as the in-side of each hop -- one hop shifted from the
out-side values (node_id_out / amount_out_msat / cltv_out, shipped in
v26.06) that sendpay routes must be built from. On stock CLN
v24.11..v26.04 the fallback therefore built mispriced routes: small
overpays on the routes that survived, spurious FEE_INSUFFICIENT /
INCORRECT_CLTV_EXPIRY failures on the rest, and -- worst -- those
failures hard-excluded healthy channels in the persistent
failure-learning layer for hours, compounding across restarts.
Drop the fallback entirely and enforce the requirement twice:
- Initiator gains a CLN version gate, ordered before the database and
signer steps so a refusal leaves no on-disk trace: a getinfo
version older than v26.06 logs a detailed Error and aborts. The
new clboss-skip-cln-version-check flag bypasses the gate for
operators whose older CLN carries a backport of the v26.06
getroutes fields (the version string alone cannot show that); an
unparseable version string warns and proceeds rather than locking
out custom builds.
- The three getroutes parse sites (FundsMover/Attempter,
ActiveProber, ChannelCandidateMatchmaker) verify the new fields on
every response and fail the attempt with a clear Error if absent,
so a mistakenly bypassed gate degrades into loud per-attempt
failures rather than shifted routes.
CHANGELOG.md gains a prominent BREAKING entry; README documents the
requirement and the escape hatch. Users on older CLN releases stay
on CLBOSS 0.16.x, which uses the legacy getroute/pay APIs those
versions still provide.
2026-07-02 14:08:15 -07:00
|
|
|
path0["node_id_out"]
|
2020-10-01 14:54:42 +08:00
|
|
|
));
|
Matchmaker, ActiveProber: migrate getroute -> getroutes for CLN v26.06 compat
Continuation of the v26.06 migration started in the Dowser
commit. Two more getroute call sites:
ChannelCandidateMatchmaker.cpp and ActiveProber.cpp. Both use
maxparts=1 since each wants a single route, not a flow estimate.
ActiveProber probes the local node's own outbound liquidity, so
it uses layers=["auto.localchans","auto.sourcefree"] per the
standardized recipe. Matchmaker probes from a remote source
(a candidate patron) to a remote target, so it passes an empty
layers array -- auto.localchans would inject our private local
channels into a foreign source, and auto.sourcefree would zero
out the source's outgoing fees, either of which could make
askrene pick a patron the proposal cannot actually reach via
public topology. maxfee_msat is 1% of the probe amount in both
cases.
Matchmaker is a near-clean swap. The patron id (route[0].id in
the old getroute shape) is read from routes[0].path[0] in the
new getroutes shape; at the parse site we bridge the v26.06+
name (node_id_out) with its deprecated pre-v26.06 predecessor
(next_node_id) via has()/ternary, so the code works on either
CLN flavor.
ActiveProber is more involved. The previous code stashed
res["route"] as a Jsmn::Object and later appended hops from it
directly into the sendpay route parameter, relying on the fact
that the old getroute hop shape (id/channel/direction/
amount_msat/delay/style) was already sendpay-compatible. The
new getroutes path[] shape is NOT directly sendpay-compatible
(different field names, short_channel_id_dir encodes scid and
direction together), so we now extract path[0] into typed
values (id1, chan1, direction1, amount1, delay1) and rebuild
the sendpay hop1 explicitly from those. The Jsmn::Object route
member is dropped. The hop-field reads in ActiveProber bridge
between v26.06+ (node_id_out / amount_out_msat / cltv_out) and
the deprecated pre-v26.06 names (next_node_id / amount_msat /
delay) via the same has()/ternary pattern.
short_channel_id_dir splits on '/' with an explicit npos check;
a missing slash throws Jsmn::TypeError so the surrounding parse-
error log path catches it cleanly. Without the check,
sdir.substr(0, npos) and sdir.substr(npos + 1) would feed
malformed input into Ln::Scid (which throws
std::invalid_argument, not caught by the Jsmn::TypeError
handler) or into std::stoul (silently producing a wrong
direction).
ActiveProber also drops its vestigial exclude=[self_id]
parameter: askrene's source/destination model naturally excludes
self when source != self, which is always the case here (the
probe always flows from peer outward, never back through us).
Three of the four getroute call sites in CLBOSS are now on
getroutes; the fourth (FundsMover/Attempter) is deferred to PR2
because its exclude-vector pattern encodes failure feedback from
real payment attempts and warrants a redesign around
askrene-inform-channel rather than a mechanical port.
2026-05-19 13:40:52 -07:00
|
|
|
} catch (std::exception const&) {
|
|
|
|
|
/* Jsmn::TypeError from the field access OR
|
|
|
|
|
* std::range_error (via BacktraceException)
|
|
|
|
|
* from Ln::NodeId on a malformed id -- both
|
|
|
|
|
* mean an unexpected getroutes result; log and
|
|
|
|
|
* route to the retry path rather than aborting
|
|
|
|
|
* the run. */
|
2020-10-01 14:54:42 +08:00
|
|
|
auto os = std::ostringstream();
|
|
|
|
|
os << res;
|
|
|
|
|
return Boss::log( bus, Error
|
|
|
|
|
, "ChannelCandidateMatchmaker:"
|
|
|
|
|
" Unexpected result from "
|
Matchmaker, ActiveProber: migrate getroute -> getroutes for CLN v26.06 compat
Continuation of the v26.06 migration started in the Dowser
commit. Two more getroute call sites:
ChannelCandidateMatchmaker.cpp and ActiveProber.cpp. Both use
maxparts=1 since each wants a single route, not a flow estimate.
ActiveProber probes the local node's own outbound liquidity, so
it uses layers=["auto.localchans","auto.sourcefree"] per the
standardized recipe. Matchmaker probes from a remote source
(a candidate patron) to a remote target, so it passes an empty
layers array -- auto.localchans would inject our private local
channels into a foreign source, and auto.sourcefree would zero
out the source's outgoing fees, either of which could make
askrene pick a patron the proposal cannot actually reach via
public topology. maxfee_msat is 1% of the probe amount in both
cases.
Matchmaker is a near-clean swap. The patron id (route[0].id in
the old getroute shape) is read from routes[0].path[0] in the
new getroutes shape; at the parse site we bridge the v26.06+
name (node_id_out) with its deprecated pre-v26.06 predecessor
(next_node_id) via has()/ternary, so the code works on either
CLN flavor.
ActiveProber is more involved. The previous code stashed
res["route"] as a Jsmn::Object and later appended hops from it
directly into the sendpay route parameter, relying on the fact
that the old getroute hop shape (id/channel/direction/
amount_msat/delay/style) was already sendpay-compatible. The
new getroutes path[] shape is NOT directly sendpay-compatible
(different field names, short_channel_id_dir encodes scid and
direction together), so we now extract path[0] into typed
values (id1, chan1, direction1, amount1, delay1) and rebuild
the sendpay hop1 explicitly from those. The Jsmn::Object route
member is dropped. The hop-field reads in ActiveProber bridge
between v26.06+ (node_id_out / amount_out_msat / cltv_out) and
the deprecated pre-v26.06 names (next_node_id / amount_msat /
delay) via the same has()/ternary pattern.
short_channel_id_dir splits on '/' with an explicit npos check;
a missing slash throws Jsmn::TypeError so the surrounding parse-
error log path catches it cleanly. Without the check,
sdir.substr(0, npos) and sdir.substr(npos + 1) would feed
malformed input into Ln::Scid (which throws
std::invalid_argument, not caught by the Jsmn::TypeError
handler) or into std::stoul (silently producing a wrong
direction).
ActiveProber also drops its vestigial exclude=[self_id]
parameter: askrene's source/destination model naturally excludes
self when source != self, which is always the case here (the
probe always flows from peer outward, never back through us).
Three of the four getroute call sites in CLBOSS are now on
getroutes; the fourth (FundsMover/Attempter) is deferred to PR2
because its exclude-vector pattern encodes failure feedback from
real payment attempts and warrants a redesign around
askrene-inform-channel rather than a mechanical port.
2026-05-19 13:40:52 -07:00
|
|
|
"getroutes: %s"
|
2020-10-01 14:54:42 +08:00
|
|
|
, os.str().c_str()
|
2020-11-02 19:02:35 +08:00
|
|
|
).then([]()
|
2020-10-01 14:54:42 +08:00
|
|
|
-> Ev::Io<void>{
|
Matchmaker, ActiveProber: migrate getroute -> getroutes for CLN v26.06 compat
Continuation of the v26.06 migration started in the Dowser
commit. Two more getroute call sites:
ChannelCandidateMatchmaker.cpp and ActiveProber.cpp. Both use
maxparts=1 since each wants a single route, not a flow estimate.
ActiveProber probes the local node's own outbound liquidity, so
it uses layers=["auto.localchans","auto.sourcefree"] per the
standardized recipe. Matchmaker probes from a remote source
(a candidate patron) to a remote target, so it passes an empty
layers array -- auto.localchans would inject our private local
channels into a foreign source, and auto.sourcefree would zero
out the source's outgoing fees, either of which could make
askrene pick a patron the proposal cannot actually reach via
public topology. maxfee_msat is 1% of the probe amount in both
cases.
Matchmaker is a near-clean swap. The patron id (route[0].id in
the old getroute shape) is read from routes[0].path[0] in the
new getroutes shape; at the parse site we bridge the v26.06+
name (node_id_out) with its deprecated pre-v26.06 predecessor
(next_node_id) via has()/ternary, so the code works on either
CLN flavor.
ActiveProber is more involved. The previous code stashed
res["route"] as a Jsmn::Object and later appended hops from it
directly into the sendpay route parameter, relying on the fact
that the old getroute hop shape (id/channel/direction/
amount_msat/delay/style) was already sendpay-compatible. The
new getroutes path[] shape is NOT directly sendpay-compatible
(different field names, short_channel_id_dir encodes scid and
direction together), so we now extract path[0] into typed
values (id1, chan1, direction1, amount1, delay1) and rebuild
the sendpay hop1 explicitly from those. The Jsmn::Object route
member is dropped. The hop-field reads in ActiveProber bridge
between v26.06+ (node_id_out / amount_out_msat / cltv_out) and
the deprecated pre-v26.06 names (next_node_id / amount_msat /
delay) via the same has()/ternary pattern.
short_channel_id_dir splits on '/' with an explicit npos check;
a missing slash throws Jsmn::TypeError so the surrounding parse-
error log path catches it cleanly. Without the check,
sdir.substr(0, npos) and sdir.substr(npos + 1) would feed
malformed input into Ln::Scid (which throws
std::invalid_argument, not caught by the Jsmn::TypeError
handler) or into std::stoul (silently producing a wrong
direction).
ActiveProber also drops its vestigial exclude=[self_id]
parameter: askrene's source/destination model naturally excludes
self when source != self, which is always the case here (the
probe always flows from peer outward, never back through us).
Three of the four getroute call sites in CLBOSS are now on
getroutes; the fourth (FundsMover/Attempter) is deferred to PR2
because its exclude-vector pattern encodes failure feedback from
real payment attempts and warrants a redesign around
askrene-inform-channel rather than a mechanical port.
2026-05-19 13:40:52 -07:00
|
|
|
throw RpcError( "getroutes"
|
2020-10-01 14:54:42 +08:00
|
|
|
, Jsmn::Object()
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
auto act = Ev::lift();
|
|
|
|
|
act += Boss::log( bus, Debug
|
|
|
|
|
, "ChannelCandidateMatchmaker: "
|
|
|
|
|
"Matched proposal %s to patron %s."
|
|
|
|
|
, std::string(proposal).c_str()
|
|
|
|
|
, std::string(patron).c_str()
|
|
|
|
|
);
|
2020-10-01 15:13:26 +08:00
|
|
|
auto propose = Msg::ProposeChannelCandidates{
|
2020-10-01 14:54:42 +08:00
|
|
|
std::move(proposal), std::move(patron)
|
2020-10-01 15:13:26 +08:00
|
|
|
};
|
|
|
|
|
auto preinv = Msg::PreinvestigateChannelCandidates{
|
|
|
|
|
{std::move(propose)},
|
|
|
|
|
1
|
|
|
|
|
};
|
|
|
|
|
act += bus.raise(std::move(preinv));
|
2020-10-01 14:54:42 +08:00
|
|
|
return act;
|
|
|
|
|
}).catching<RpcError>([this](RpcError const&) {
|
|
|
|
|
/* Try next. */
|
|
|
|
|
return core_run();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void ChannelCandidateMatchmaker::start() {
|
2021-12-07 16:57:10 +08:00
|
|
|
bus.subscribe<Msg::AmountSettings
|
|
|
|
|
>([this](Msg::AmountSettings const& r) {
|
|
|
|
|
min_channel = r.min_channel;
|
|
|
|
|
return Ev::lift();
|
|
|
|
|
});
|
2020-10-01 14:54:42 +08:00
|
|
|
bus.subscribe<Msg::Init
|
|
|
|
|
>([this](Msg::Init const& init) {
|
|
|
|
|
rpc = &init.rpc;
|
|
|
|
|
return Ev::lift();
|
|
|
|
|
});
|
|
|
|
|
bus.subscribe<Msg::PatronizeChannelCandidate
|
|
|
|
|
>([this](Msg::PatronizeChannelCandidate const& m) {
|
|
|
|
|
if (!rpc)
|
|
|
|
|
return Ev::lift();
|
|
|
|
|
|
|
|
|
|
/* Construct queue. */
|
|
|
|
|
auto q = std::queue<Ln::NodeId>();
|
|
|
|
|
for (auto const& n : m.guide)
|
|
|
|
|
q.push(n);
|
|
|
|
|
/* Create run object. */
|
2021-12-07 16:57:10 +08:00
|
|
|
auto run = Run::create( bus, *rpc, m.proposal, std::move(q)
|
|
|
|
|
, min_channel
|
|
|
|
|
);
|
2020-10-01 14:54:42 +08:00
|
|
|
return Boss::concurrent(run->run());
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}}
|