2020-10-07 15:06:38 +08:00
|
|
|
#include"Boss/Mod/ActiveProber.hpp"
|
ActiveProber: feed sendpay failures into the clboss askrene layer
When a 2-hop probe sendpay fails, parse the waitsendpay error and
record real route failures into the persistent clboss askrene
layer via Boss::Mod::AskreneLayer's shared helpers. Future
getroutes calls (FundsMover's rebalances, ActiveProber's own
subsequent probes, anything else that consumes the clboss layer)
automatically steer around the failed channel or node.
The PR2 commit introduced the layer + the inform-channel /
disable-node call shape, but FundsMover is the only writer
today, and its writes are gated behind real sendpay attempts
that rarely fire (fees on small rebalance amounts almost always
exceed the prorated budget, so FundsMover gives up
pre-sendpay). Net result: the layer is essentially empty in
production.
ActiveProber, by contrast, runs on a timer and sends real
sendpays without a budget filter. Wiring its failure path to
the layer turns it into the dominant feeder, making the layer
infrastructure observable and useful.
Probe-outcome discrimination -- the failure interpretation is
necessarily tighter than FundsMover's because every probe is
designed to fail at the final hop:
failcode 0x400F (WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS)
-> id1 received the HTLC and rejected it
because our bit-flipped random
payment_hash matches no invoice. This is
the EXPECTED outcome: the route was
viable all the way to id1. Probe
success; no layer write.
code 202 (unparsable onion)
-> cannot pin the failure to a specific
channel or node. On a 2-hop route, id1
is the most likely culprit; disable_node(id1).
echan == chan0 -> our outgoing channel had the issue. Skip
the layer write: CLBOSS already has
authoritative knowledge of local channel
state via listpeerchannels, and the
shared layer is for inter-subsystem
knowledge that isn't already authoritatively
available.
failcode & 0x2000 (NODE-level)
-> disable_node(erring_node).
channel-level failcode on chan1
-> inform_channel_constrained(chan1, dir1, amount1).
unparseable error data
-> swallow; don't guess.
No change to the existing probe-success accounting: the bool
returned by the catching handler still flows into the same
delpay-status / "Finished probing peer X" logic. The only new
behavior is the side-effect layer write inside record_failure.
Open question, deliberately not addressed here: should
ActiveProber ALSO feed inform=succeeded on probe success
(raising lower-bound estimates on chan1)? The DEVSTATE
ACTIVE-PROBER-LAYER-FEEDER-2026-05-20 recommends yes, ~5 lines
extra; defer until empirical signet observation shows whether
the failure-feedback alone is enough to drive observable
routing improvements.
2026-05-21 10:54:16 -07:00
|
|
|
#include"Boss/Mod/AskreneLayer.hpp"
|
2020-10-07 15:06:38 +08:00
|
|
|
#include"Boss/Mod/ChannelCandidateInvestigator/Main.hpp"
|
|
|
|
|
#include"Boss/Mod/Rpc.hpp"
|
|
|
|
|
#include"Boss/Msg/Init.hpp"
|
|
|
|
|
#include"Boss/Msg/ProbeActively.hpp"
|
2021-04-28 22:59:04 +08:00
|
|
|
#include"Boss/Msg/ProvideDeletablePaymentLabelFilter.hpp"
|
|
|
|
|
#include"Boss/Msg/SolicitDeletablePaymentLabelFilter.hpp"
|
2020-10-07 15:06:38 +08:00
|
|
|
#include"Boss/concurrent.hpp"
|
|
|
|
|
#include"Boss/log.hpp"
|
|
|
|
|
#include"Boss/random_engine.hpp"
|
|
|
|
|
#include"Ev/Io.hpp"
|
|
|
|
|
#include"Ev/yield.hpp"
|
|
|
|
|
#include"Jsmn/Object.hpp"
|
|
|
|
|
#include"Json/Out.hpp"
|
|
|
|
|
#include"Ln/Amount.hpp"
|
|
|
|
|
#include"Ln/NodeId.hpp"
|
|
|
|
|
#include"Ln/Preimage.hpp"
|
|
|
|
|
#include"Ln/Scid.hpp"
|
|
|
|
|
#include"S/Bus.hpp"
|
|
|
|
|
#include"Sha256/Hash.hpp"
|
2021-04-28 22:59:04 +08:00
|
|
|
#include"Util/Str.hpp"
|
2020-10-07 15:06:38 +08:00
|
|
|
#include"Util/stringify.hpp"
|
|
|
|
|
#include<algorithm>
|
|
|
|
|
#include<memory>
|
|
|
|
|
#include<queue>
|
|
|
|
|
#include<set>
|
|
|
|
|
#include<vector>
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
/* Try to probe the following amount. */
|
|
|
|
|
auto const reference_amount = Ln::Amount::sat(160000);
|
|
|
|
|
|
|
|
|
|
Ev::Io<void> wait_for_rpc(Boss::Mod::Rpc*& rpc) {
|
|
|
|
|
return Ev::yield().then([&rpc]() {
|
|
|
|
|
if (!rpc)
|
|
|
|
|
return wait_for_rpc(rpc);
|
|
|
|
|
return Ev::lift();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-28 22:59:04 +08:00
|
|
|
/* Prefix for all labels. */
|
|
|
|
|
auto const label_prefix = std::string( "CLBOSS ActiveProber "
|
|
|
|
|
"payment, this will fail "
|
|
|
|
|
"and should automatically "
|
|
|
|
|
"get deleted. Hash: "
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
bool is_our_label(std::string const& label) {
|
|
|
|
|
if (label.length() != label_prefix.length() + 64)
|
|
|
|
|
return false;
|
|
|
|
|
if ( std::string(label.begin(), label.begin() + label_prefix.length())
|
|
|
|
|
!= label_prefix
|
|
|
|
|
)
|
|
|
|
|
return false;
|
|
|
|
|
auto hash = std::string( label.begin() + label_prefix.length()
|
|
|
|
|
, label.end()
|
|
|
|
|
);
|
|
|
|
|
return Util::Str::ishex(hash);
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-07 15:06:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
namespace Boss { namespace Mod {
|
|
|
|
|
|
|
|
|
|
class ActiveProber::Run : public std::enable_shared_from_this<Run> {
|
|
|
|
|
private:
|
|
|
|
|
S::Bus& bus;
|
|
|
|
|
ChannelCandidateInvestigator::Main& investigator;
|
|
|
|
|
Rpc& rpc;
|
|
|
|
|
Ln::NodeId self_id;
|
|
|
|
|
|
|
|
|
|
Ln::NodeId peer;
|
|
|
|
|
|
|
|
|
|
Secp256k1::Random& random;
|
|
|
|
|
|
|
|
|
|
Run( ActiveProber& prober
|
|
|
|
|
, Ln::NodeId const& peer_
|
|
|
|
|
) : bus(prober.bus)
|
|
|
|
|
, investigator(prober.investigator)
|
|
|
|
|
, rpc(*prober.rpc)
|
|
|
|
|
, self_id(prober.self_id)
|
|
|
|
|
, peer(peer_)
|
|
|
|
|
, random(prober.random)
|
|
|
|
|
{ }
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
static
|
|
|
|
|
std::shared_ptr<Run>
|
|
|
|
|
create( ActiveProber& prober
|
|
|
|
|
, Ln::NodeId const& peer
|
|
|
|
|
) {
|
|
|
|
|
return std::shared_ptr<Run>(new Run(prober, peer));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ev::Io<void> run() {
|
|
|
|
|
auto self = shared_from_this();
|
|
|
|
|
return self->core_run().then([self]() {
|
|
|
|
|
return Ev::lift();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
private:
|
|
|
|
|
/* First channel. */
|
|
|
|
|
Ln::Scid chan0;
|
|
|
|
|
Ln::Amount cap0;
|
|
|
|
|
Ln::Amount amount;
|
|
|
|
|
|
|
|
|
|
Ev::Io<void> core_run() {
|
|
|
|
|
return Ev::lift().then([this]() {
|
|
|
|
|
return Boss::log( bus, Info
|
|
|
|
|
, "ActiveProber: Probe peer %s."
|
|
|
|
|
, std::string(peer).c_str()
|
|
|
|
|
);
|
|
|
|
|
}).then([this]() {
|
|
|
|
|
auto parms = Json::Out()
|
|
|
|
|
.start_object()
|
|
|
|
|
.field("id", std::string(peer))
|
|
|
|
|
.end_object()
|
|
|
|
|
;
|
2024-04-28 13:49:38 -05:00
|
|
|
return rpc.command("listpeerchannels", std::move(parms));
|
2020-10-07 15:06:38 +08:00
|
|
|
}).then([this](Jsmn::Object res) {
|
|
|
|
|
try {
|
2024-04-28 13:49:38 -05:00
|
|
|
auto cs = res["channels"];
|
|
|
|
|
for (auto c : cs) {
|
|
|
|
|
if (!c.has("short_channel_id"))
|
|
|
|
|
continue;
|
|
|
|
|
if (!c.has("spendable_msat"))
|
|
|
|
|
continue;
|
|
|
|
|
auto state = std::string(
|
|
|
|
|
c["state"]
|
|
|
|
|
);
|
|
|
|
|
if (state != "CHANNELD_NORMAL")
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
chan0 = Ln::Scid(std::string(
|
|
|
|
|
c["short_channel_id"]
|
|
|
|
|
));
|
|
|
|
|
cap0 = Ln::Amount::object(
|
|
|
|
|
c["spendable_msat"]
|
|
|
|
|
);
|
|
|
|
|
break;
|
2020-10-07 15:06:38 +08:00
|
|
|
}
|
|
|
|
|
} catch (Jsmn::TypeError const& _) {
|
|
|
|
|
return Boss::log( bus, Error
|
|
|
|
|
, "ActiveProber: unexpected "
|
2024-04-28 13:49:38 -05:00
|
|
|
"listpeerchannels result: %s"
|
2020-10-07 15:06:38 +08:00
|
|
|
, Util::stringify(res).c_str()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!chan0)
|
|
|
|
|
return Boss::log( bus, Info
|
|
|
|
|
, "ActiveProber: No "
|
|
|
|
|
"CHANNELD_NORMAL channel "
|
|
|
|
|
"with node %s, cannot probe."
|
|
|
|
|
, std::string(peer).c_str()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
amount = reference_amount;
|
|
|
|
|
if (amount > cap0 * 0.95)
|
|
|
|
|
amount = cap0 * 0.95;
|
|
|
|
|
|
|
|
|
|
return get_destinations();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* A candidate set of destinations. */
|
|
|
|
|
std::queue<Ln::NodeId> to_try;
|
|
|
|
|
|
|
|
|
|
Ev::Io<void> get_destinations() {
|
|
|
|
|
return Ev::lift().then([this]() {
|
|
|
|
|
return investigator.get_channel_candidates();
|
|
|
|
|
}).then([this](std::vector<std::pair< Ln::NodeId
|
|
|
|
|
, Ln::NodeId
|
|
|
|
|
>> cands) {
|
|
|
|
|
/* Put both proposal and patron into the
|
|
|
|
|
* set of candidate nodes. */
|
|
|
|
|
auto n_set = std::set<Ln::NodeId>();
|
|
|
|
|
for (auto& e : cands) {
|
|
|
|
|
n_set.insert(std::move(e.first));
|
|
|
|
|
n_set.insert(std::move(e.second));
|
|
|
|
|
}
|
|
|
|
|
/* If the peer itself is in the set, remove it. */
|
|
|
|
|
auto it = n_set.find(peer);
|
|
|
|
|
if (it != n_set.end())
|
|
|
|
|
n_set.erase(it);
|
|
|
|
|
/* Copy to a vector and shuffle. */
|
|
|
|
|
auto n_vec = std::vector<Ln::NodeId>(n_set.size());
|
|
|
|
|
std::copy( n_set.begin(), n_set.end()
|
|
|
|
|
, n_vec.begin()
|
|
|
|
|
);
|
|
|
|
|
std::shuffle( n_vec.begin(), n_vec.end()
|
|
|
|
|
, Boss::random_engine
|
|
|
|
|
);
|
|
|
|
|
/* Push to queue. */
|
|
|
|
|
for (auto& n : n_vec)
|
|
|
|
|
to_try.push(std::move(n));
|
|
|
|
|
|
|
|
|
|
if (to_try.empty())
|
|
|
|
|
return Boss::log( bus, Info
|
|
|
|
|
, "ActiveProber: No trial "
|
|
|
|
|
"destinations found "
|
|
|
|
|
"for peer %s."
|
|
|
|
|
, std::string(peer).c_str()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return Boss::log( bus, Debug
|
|
|
|
|
, "ActiveProber: Found %zu trial "
|
|
|
|
|
"destinations for peer %s."
|
|
|
|
|
, to_try.size()
|
|
|
|
|
, std::string(peer).c_str()
|
|
|
|
|
)
|
|
|
|
|
+ getroute()
|
|
|
|
|
;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
/* First hop after `peer`: the peer's neighbor that we probe
|
|
|
|
|
* towards. Extracted into typed values from getroutes' path[0]
|
|
|
|
|
* so we can rebuild the sendpay hop later without keeping the
|
|
|
|
|
* raw Jsmn::Object around.
|
|
|
|
|
*/
|
|
|
|
|
Ln::NodeId id1;
|
2020-10-07 15:06:38 +08:00
|
|
|
Ln::Scid chan1;
|
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
|
|
|
std::uint32_t direction1;
|
2020-10-07 15:06:38 +08:00
|
|
|
Ln::Amount amount1;
|
|
|
|
|
std::uint32_t delay1;
|
|
|
|
|
|
|
|
|
|
Ev::Io<void> getroute() {
|
|
|
|
|
if (to_try.empty())
|
|
|
|
|
return Boss::log( bus, Info
|
|
|
|
|
, "ActiveProber: No more trial "
|
|
|
|
|
"destinations for peer %s."
|
|
|
|
|
, std::string(peer).c_str()
|
|
|
|
|
);
|
|
|
|
|
return Ev::yield().then([this]() {
|
|
|
|
|
auto const& dest = to_try.front();
|
|
|
|
|
|
|
|
|
|
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(peer))
|
|
|
|
|
.field("destination", std::string(dest))
|
2024-04-02 12:23:47 -07:00
|
|
|
.field("amount_msat", amount.to_msat())
|
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
|
|
|
/* No layers: source is a remote node
|
|
|
|
|
* (peer), 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 path[0] that the
|
|
|
|
|
* peer cannot actually reach via
|
|
|
|
|
* public topology (and could even
|
|
|
|
|
* produce a route that loops back
|
|
|
|
|
* through us).
|
|
|
|
|
*/
|
|
|
|
|
.start_array("layers").end_array()
|
|
|
|
|
/* Generous max-fee tolerance for a
|
|
|
|
|
* probe; askrene optimizes for
|
|
|
|
|
* cheaper paths anyway via its
|
|
|
|
|
* probability scoring.
|
2020-10-07 15:06:38 +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("maxfee_msat",
|
|
|
|
|
(amount * 0.01).to_msat())
|
|
|
|
|
.field("final_cltv", 14)
|
|
|
|
|
.field("maxparts", 1)
|
2020-10-07 15:06:38 +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-07 15:06:38 +08:00
|
|
|
}).then([this](Jsmn::Object res) {
|
|
|
|
|
try {
|
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 path[] hop fields were renamed
|
|
|
|
|
* in CLN v26.06. Which names actually get
|
|
|
|
|
* emitted depends on the CLN version AND
|
|
|
|
|
* whether the node runs with developer
|
|
|
|
|
* mode (which suppresses deprecated
|
|
|
|
|
* outputs):
|
|
|
|
|
*
|
|
|
|
|
* v26.04 -> old names only
|
|
|
|
|
* v26.06+ no developer -> both names emitted
|
|
|
|
|
* v26.06+ developer=true -> new names only
|
|
|
|
|
*
|
|
|
|
|
* Bridge by preferring the new name, falling
|
|
|
|
|
* back to the old. TODO: drop the fallback
|
|
|
|
|
* once CLN v26.04 is no longer supported and
|
|
|
|
|
* the old names are gone for good in v27.06.
|
|
|
|
|
*
|
|
|
|
|
* short_channel_id_dir is older (v24.11)
|
|
|
|
|
* and is emitted unconditionally.
|
|
|
|
|
*/
|
|
|
|
|
auto path0 = res["routes"][0]["path"][0];
|
|
|
|
|
id1 = Ln::NodeId(std::string(
|
|
|
|
|
path0.has("node_id_out")
|
|
|
|
|
? path0["node_id_out"]
|
|
|
|
|
: path0["next_node_id"]
|
|
|
|
|
));
|
|
|
|
|
/* short_channel_id_dir is "SCID/dir"; split
|
|
|
|
|
* into the SCID and the direction. If
|
|
|
|
|
* the slash is missing the response is
|
|
|
|
|
* malformed; treat it as a parse error
|
|
|
|
|
* so the enclosing handler logs and
|
|
|
|
|
* skips this destination rather than
|
|
|
|
|
* feeding garbage to Ln::Scid / std::stoul.
|
|
|
|
|
*/
|
|
|
|
|
auto sdir = std::string(
|
|
|
|
|
path0["short_channel_id_dir"]
|
|
|
|
|
);
|
|
|
|
|
auto slash = sdir.find('/');
|
|
|
|
|
if (slash == std::string::npos)
|
|
|
|
|
throw Jsmn::TypeError();
|
|
|
|
|
chan1 = Ln::Scid(sdir.substr(0, slash));
|
|
|
|
|
direction1 = std::uint32_t(std::stoul(
|
|
|
|
|
sdir.substr(slash + 1)
|
2020-10-07 15:06:38 +08:00
|
|
|
));
|
2023-05-02 20:20:50 -07:00
|
|
|
amount1 = Ln::Amount::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
|
|
|
path0.has("amount_out_msat")
|
|
|
|
|
? path0["amount_out_msat"]
|
|
|
|
|
: path0["amount_msat"]
|
2023-05-02 20:20:50 -07:00
|
|
|
);
|
2020-10-07 15:06:38 +08:00
|
|
|
delay1 = std::uint32_t(double(
|
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
|
|
|
path0.has("cltv_out")
|
|
|
|
|
? path0["cltv_out"]
|
|
|
|
|
: path0["delay"]
|
2020-10-07 15:06:38 +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& _) {
|
|
|
|
|
/* Broaden catch to std::exception so we also
|
|
|
|
|
* handle std::invalid_argument and
|
|
|
|
|
* std::out_of_range from std::stoul on a
|
|
|
|
|
* malformed short_channel_id_dir tail. Pop
|
|
|
|
|
* the bad destination before logging so the
|
|
|
|
|
* recursive getroute() picks a different
|
|
|
|
|
* candidate instead of looping on the same
|
|
|
|
|
* one indefinitely.
|
|
|
|
|
*/
|
|
|
|
|
if (!to_try.empty())
|
|
|
|
|
to_try.pop();
|
2020-10-07 15:06:38 +08:00
|
|
|
return Boss::log( bus, Error
|
|
|
|
|
, "ActiveProber: Unexpected "
|
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
|
|
|
"result from getroutes: %s"
|
2020-10-07 15:06:38 +08:00
|
|
|
, Util::stringify(res).c_str()
|
2020-11-02 19:02:35 +08:00
|
|
|
).then([]() {
|
2020-10-07 15:06:38 +08:00
|
|
|
return Ev::lift(false);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Ev::lift(true);
|
|
|
|
|
}).catching<RpcError>([this](RpcError const& e) {
|
|
|
|
|
/* Go to next. */
|
|
|
|
|
to_try.pop();
|
|
|
|
|
return Ev::lift(false);
|
|
|
|
|
}).then([this](bool flag) {
|
|
|
|
|
if (flag)
|
|
|
|
|
return compute_hop0();
|
|
|
|
|
else
|
|
|
|
|
return getroute();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Details from the first hop in the found route, which have to be
|
|
|
|
|
* added in the 0th hop we will insert.
|
|
|
|
|
*/
|
|
|
|
|
Ln::Amount base_fee;
|
|
|
|
|
std::uint32_t proportional_fee;
|
|
|
|
|
std::uint32_t cltv_delta;
|
|
|
|
|
Ln::Amount amount0;
|
|
|
|
|
std::uint32_t delay0;
|
|
|
|
|
|
|
|
|
|
Ev::Io<void> compute_hop0() {
|
|
|
|
|
return Ev::lift().then([this]() {
|
|
|
|
|
/* Get information on chan1. */
|
|
|
|
|
auto parms = Json::Out()
|
|
|
|
|
.start_object()
|
|
|
|
|
.field( "short_channel_id"
|
|
|
|
|
, std::string(chan1)
|
|
|
|
|
)
|
|
|
|
|
.end_object()
|
|
|
|
|
;
|
|
|
|
|
return rpc.command("listchannels", std::move(parms));
|
|
|
|
|
}).then([this](Jsmn::Object res) {
|
|
|
|
|
auto found = false;
|
|
|
|
|
try {
|
|
|
|
|
auto cs = res["channels"];
|
2020-11-18 16:32:49 +08:00
|
|
|
for (auto c : cs) {
|
2020-10-07 15:06:38 +08:00
|
|
|
auto src = Ln::NodeId(std::string(
|
|
|
|
|
c["source"]
|
|
|
|
|
));
|
|
|
|
|
if (src != peer)
|
|
|
|
|
continue;
|
|
|
|
|
base_fee = Ln::Amount::msat(double(
|
|
|
|
|
c["base_fee_millisatoshi"]
|
|
|
|
|
));
|
|
|
|
|
proportional_fee
|
|
|
|
|
= std::uint32_t(double(
|
|
|
|
|
c["fee_per_millionth"]
|
|
|
|
|
));
|
|
|
|
|
cltv_delta = std::uint32_t(double(
|
|
|
|
|
c["delay"]
|
|
|
|
|
));
|
|
|
|
|
found = true;
|
|
|
|
|
}
|
|
|
|
|
} catch (Jsmn::TypeError const&) {
|
|
|
|
|
return Boss::log( bus, Error
|
|
|
|
|
, "ActiveProber: Unexpected "
|
|
|
|
|
"result from listchannels: "
|
|
|
|
|
"%s"
|
|
|
|
|
, Util::stringify(res).c_str()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Channel could have disappeared from under us. */
|
|
|
|
|
if (!found) {
|
|
|
|
|
to_try.pop();
|
|
|
|
|
return getroute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
amount0 = amount1 + base_fee
|
|
|
|
|
+ (amount1 * ( double(proportional_fee)
|
|
|
|
|
/ 1000000
|
|
|
|
|
))
|
|
|
|
|
/* Fudge roundoff erors. */
|
|
|
|
|
+ Ln::Amount::msat(1)
|
|
|
|
|
;
|
|
|
|
|
delay0 = delay1 + cltv_delta;
|
|
|
|
|
|
|
|
|
|
return select_hash();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Sha256::Hash hash;
|
|
|
|
|
|
|
|
|
|
Ev::Io<void> select_hash() {
|
|
|
|
|
/* Generate a random preimage and hash it. */
|
|
|
|
|
auto preimage = Ln::Preimage(random);
|
|
|
|
|
hash = preimage.sha256();
|
|
|
|
|
|
|
|
|
|
/* Flip the bits of the generated hash.
|
|
|
|
|
* This ensures that even if the entropy of our preimage
|
|
|
|
|
* is low, this is still not exploitable, as an attacker
|
|
|
|
|
* that knows every bit of our preimage still cannot
|
|
|
|
|
* reverse the inverse-hash of our preimage.
|
|
|
|
|
*/
|
|
|
|
|
std::uint8_t buf[32];
|
|
|
|
|
hash.to_buffer(buf);
|
|
|
|
|
for (auto i = std::size_t(0); i < 32; ++i)
|
|
|
|
|
buf[i] = ~buf[i];
|
|
|
|
|
hash.from_buffer(buf);
|
|
|
|
|
|
|
|
|
|
return sendpay();
|
|
|
|
|
}
|
|
|
|
|
|
ActiveProber: feed sendpay failures into the clboss askrene layer
When a 2-hop probe sendpay fails, parse the waitsendpay error and
record real route failures into the persistent clboss askrene
layer via Boss::Mod::AskreneLayer's shared helpers. Future
getroutes calls (FundsMover's rebalances, ActiveProber's own
subsequent probes, anything else that consumes the clboss layer)
automatically steer around the failed channel or node.
The PR2 commit introduced the layer + the inform-channel /
disable-node call shape, but FundsMover is the only writer
today, and its writes are gated behind real sendpay attempts
that rarely fire (fees on small rebalance amounts almost always
exceed the prorated budget, so FundsMover gives up
pre-sendpay). Net result: the layer is essentially empty in
production.
ActiveProber, by contrast, runs on a timer and sends real
sendpays without a budget filter. Wiring its failure path to
the layer turns it into the dominant feeder, making the layer
infrastructure observable and useful.
Probe-outcome discrimination -- the failure interpretation is
necessarily tighter than FundsMover's because every probe is
designed to fail at the final hop:
failcode 0x400F (WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS)
-> id1 received the HTLC and rejected it
because our bit-flipped random
payment_hash matches no invoice. This is
the EXPECTED outcome: the route was
viable all the way to id1. Probe
success; no layer write.
code 202 (unparsable onion)
-> cannot pin the failure to a specific
channel or node. On a 2-hop route, id1
is the most likely culprit; disable_node(id1).
echan == chan0 -> our outgoing channel had the issue. Skip
the layer write: CLBOSS already has
authoritative knowledge of local channel
state via listpeerchannels, and the
shared layer is for inter-subsystem
knowledge that isn't already authoritatively
available.
failcode & 0x2000 (NODE-level)
-> disable_node(erring_node).
channel-level failcode on chan1
-> inform_channel_constrained(chan1, dir1, amount1).
unparseable error data
-> swallow; don't guess.
No change to the existing probe-success accounting: the bool
returned by the catching handler still flows into the same
delpay-status / "Finished probing peer X" logic. The only new
behavior is the side-effect layer write inside record_failure.
Open question, deliberately not addressed here: should
ActiveProber ALSO feed inform=succeeded on probe success
(raising lower-bound estimates on chan1)? The DEVSTATE
ACTIVE-PROBER-LAYER-FEEDER-2026-05-20 recommends yes, ~5 lines
extra; defer until empirical signet observation shows whether
the failure-feedback alone is enough to drive observable
routing improvements.
2026-05-21 10:54:16 -07:00
|
|
|
/* Parse waitsendpay's error data and return whether the probe
|
|
|
|
|
* should be considered successful. Side effect: on real
|
|
|
|
|
* route failures, write to the persistent clboss askrene
|
|
|
|
|
* layer so future getroutes calls steer around the failing
|
|
|
|
|
* channel/node.
|
|
|
|
|
*
|
|
|
|
|
* Probe-success outcome: the final hop (id1) replied with
|
|
|
|
|
* WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS (failcode
|
|
|
|
|
* 0x400F), which is the expected failure given our
|
|
|
|
|
* bit-flipped random payment_hash and proves the route
|
|
|
|
|
* worked all the way through to id1.
|
|
|
|
|
*
|
|
|
|
|
* Failure outcomes that feed the layer:
|
|
|
|
|
* - Code 202 (unparsable onion): cannot pin a specific
|
|
|
|
|
* channel; disable id1 as the best-guess culprit on
|
|
|
|
|
* this 2-hop route.
|
|
|
|
|
* - NODE-level failcode (bit 0x2000 set): disable the
|
|
|
|
|
* erring_node.
|
|
|
|
|
* - CHANNEL-level failcode on a non-chan0 channel
|
|
|
|
|
* (= chan1): inform_channel_constrained at amount1.
|
|
|
|
|
*
|
|
|
|
|
* Failures we DO NOT feed:
|
|
|
|
|
* - echan == chan0: our local outgoing channel had the
|
|
|
|
|
* issue. CLBOSS already has authoritative knowledge of
|
|
|
|
|
* local channel state via listpeerchannels; no need to
|
|
|
|
|
* re-record into the shared layer.
|
|
|
|
|
* - Unparseable error data: don't guess.
|
|
|
|
|
*/
|
|
|
|
|
Ev::Io<bool> record_failure(RpcError const& err) {
|
|
|
|
|
auto code = int();
|
|
|
|
|
auto eidx = std::size_t();
|
|
|
|
|
auto echan = Ln::Scid();
|
|
|
|
|
auto edir = std::uint32_t();
|
|
|
|
|
auto enode = Ln::NodeId();
|
|
|
|
|
auto fail = std::uint16_t();
|
|
|
|
|
auto parsed = false;
|
|
|
|
|
try {
|
|
|
|
|
auto& error = err.error;
|
|
|
|
|
code = int(double(error["code"]));
|
|
|
|
|
/* Both codes carry the same data shape but
|
|
|
|
|
* different origin signal:
|
|
|
|
|
* 203 = destination permanently failed
|
|
|
|
|
* (e.g. final-hop responded with
|
|
|
|
|
* WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,
|
|
|
|
|
* the expected probe-success outcome).
|
|
|
|
|
* 204 = intermediate failure ("try another route").
|
|
|
|
|
*/
|
|
|
|
|
if (code == 203 || code == 204) {
|
|
|
|
|
auto data = error["data"];
|
|
|
|
|
eidx = std::size_t(double(
|
|
|
|
|
data["erring_index"]
|
|
|
|
|
));
|
|
|
|
|
echan = Ln::Scid(std::string(
|
|
|
|
|
data["erring_channel"]
|
|
|
|
|
));
|
|
|
|
|
edir = std::uint32_t(double(
|
|
|
|
|
data["erring_direction"]
|
|
|
|
|
));
|
|
|
|
|
enode = Ln::NodeId(std::string(
|
|
|
|
|
data["erring_node"]
|
|
|
|
|
));
|
|
|
|
|
fail = std::uint16_t(double(
|
|
|
|
|
data["failcode"]
|
|
|
|
|
));
|
|
|
|
|
parsed = true;
|
|
|
|
|
}
|
|
|
|
|
} catch (std::exception const&) {
|
|
|
|
|
/* Couldn't parse; treat as real failure with
|
|
|
|
|
* no layer feedback. Avoid acting on garbage.
|
|
|
|
|
*/
|
|
|
|
|
return Ev::lift(false);
|
|
|
|
|
}
|
|
|
|
|
(void) eidx; /* Currently unused; relying on echan and
|
|
|
|
|
* failcode to discriminate. Future:
|
|
|
|
|
* cross-check eidx against expected route
|
|
|
|
|
* positions if discrepancies emerge.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
auto const& layer_name =
|
|
|
|
|
Boss::Mod::AskreneLayer::clboss_layer_name;
|
|
|
|
|
|
|
|
|
|
if (code == 202) {
|
|
|
|
|
/* Unparsable onion: best guess on a 2-hop
|
|
|
|
|
* probe is that id1 is the culprit.
|
|
|
|
|
*/
|
|
|
|
|
return Boss::Mod::AskreneLayer::disable_node(
|
|
|
|
|
rpc, layer_name, id1
|
|
|
|
|
).then([]() {
|
|
|
|
|
return Ev::lift(false);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!parsed) {
|
|
|
|
|
/* Other error code, no actionable hop info. */
|
|
|
|
|
return Ev::lift(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS at the
|
|
|
|
|
* destination is the expected probe outcome: id1
|
|
|
|
|
* received the HTLC and rejected it because the
|
|
|
|
|
* random bit-flipped payment_hash matches no
|
|
|
|
|
* invoice. The route was viable all the way.
|
2026-05-21 11:44:01 -07:00
|
|
|
*
|
|
|
|
|
* Record a positive lower-bound observation on chan1:
|
|
|
|
|
* we just proved this directed channel can push at
|
|
|
|
|
* least amount1 right now. askrene's
|
|
|
|
|
* inform=unconstrained mode stores this as a
|
|
|
|
|
* constraint with min=amount, max=NULL -- exactly
|
|
|
|
|
* the lower-bound raise we want for future getroutes
|
|
|
|
|
* calls to trust this channel more.
|
|
|
|
|
*
|
|
|
|
|
* (Askrene also has an inform=succeeded mode but that
|
|
|
|
|
* branch is currently a no-op stub in askrene.c
|
|
|
|
|
* "FIXME: We could do something useful here!" -- so
|
|
|
|
|
* we follow xpay's convention of using
|
|
|
|
|
* inform=unconstrained for the post-success
|
|
|
|
|
* lower-bound-raise pattern.)
|
|
|
|
|
*
|
|
|
|
|
* We do NOT record a parallel observation on chan0
|
|
|
|
|
* (our outbound channel to peer). CLBOSS already
|
|
|
|
|
* has authoritative knowledge of local channel state
|
|
|
|
|
* via listpeerchannels; the shared askrene layer is
|
|
|
|
|
* for cross-subsystem knowledge that isn't already
|
|
|
|
|
* locally available.
|
ActiveProber: feed sendpay failures into the clboss askrene layer
When a 2-hop probe sendpay fails, parse the waitsendpay error and
record real route failures into the persistent clboss askrene
layer via Boss::Mod::AskreneLayer's shared helpers. Future
getroutes calls (FundsMover's rebalances, ActiveProber's own
subsequent probes, anything else that consumes the clboss layer)
automatically steer around the failed channel or node.
The PR2 commit introduced the layer + the inform-channel /
disable-node call shape, but FundsMover is the only writer
today, and its writes are gated behind real sendpay attempts
that rarely fire (fees on small rebalance amounts almost always
exceed the prorated budget, so FundsMover gives up
pre-sendpay). Net result: the layer is essentially empty in
production.
ActiveProber, by contrast, runs on a timer and sends real
sendpays without a budget filter. Wiring its failure path to
the layer turns it into the dominant feeder, making the layer
infrastructure observable and useful.
Probe-outcome discrimination -- the failure interpretation is
necessarily tighter than FundsMover's because every probe is
designed to fail at the final hop:
failcode 0x400F (WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS)
-> id1 received the HTLC and rejected it
because our bit-flipped random
payment_hash matches no invoice. This is
the EXPECTED outcome: the route was
viable all the way to id1. Probe
success; no layer write.
code 202 (unparsable onion)
-> cannot pin the failure to a specific
channel or node. On a 2-hop route, id1
is the most likely culprit; disable_node(id1).
echan == chan0 -> our outgoing channel had the issue. Skip
the layer write: CLBOSS already has
authoritative knowledge of local channel
state via listpeerchannels, and the
shared layer is for inter-subsystem
knowledge that isn't already authoritatively
available.
failcode & 0x2000 (NODE-level)
-> disable_node(erring_node).
channel-level failcode on chan1
-> inform_channel_constrained(chan1, dir1, amount1).
unparseable error data
-> swallow; don't guess.
No change to the existing probe-success accounting: the bool
returned by the catching handler still flows into the same
delpay-status / "Finished probing peer X" logic. The only new
behavior is the side-effect layer write inside record_failure.
Open question, deliberately not addressed here: should
ActiveProber ALSO feed inform=succeeded on probe success
(raising lower-bound estimates on chan1)? The DEVSTATE
ACTIVE-PROBER-LAYER-FEEDER-2026-05-20 recommends yes, ~5 lines
extra; defer until empirical signet observation shows whether
the failure-feedback alone is enough to drive observable
routing improvements.
2026-05-21 10:54:16 -07:00
|
|
|
*/
|
|
|
|
|
auto const WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS
|
|
|
|
|
= std::uint16_t(0x400F);
|
2026-05-21 11:44:01 -07:00
|
|
|
if (fail == WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS) {
|
|
|
|
|
return Boss::Mod::AskreneLayer::inform_channel_unconstrained(
|
|
|
|
|
rpc, layer_name, chan1, direction1, amount1
|
|
|
|
|
).then([]() {
|
|
|
|
|
return Ev::lift(true);
|
|
|
|
|
});
|
|
|
|
|
}
|
ActiveProber: feed sendpay failures into the clboss askrene layer
When a 2-hop probe sendpay fails, parse the waitsendpay error and
record real route failures into the persistent clboss askrene
layer via Boss::Mod::AskreneLayer's shared helpers. Future
getroutes calls (FundsMover's rebalances, ActiveProber's own
subsequent probes, anything else that consumes the clboss layer)
automatically steer around the failed channel or node.
The PR2 commit introduced the layer + the inform-channel /
disable-node call shape, but FundsMover is the only writer
today, and its writes are gated behind real sendpay attempts
that rarely fire (fees on small rebalance amounts almost always
exceed the prorated budget, so FundsMover gives up
pre-sendpay). Net result: the layer is essentially empty in
production.
ActiveProber, by contrast, runs on a timer and sends real
sendpays without a budget filter. Wiring its failure path to
the layer turns it into the dominant feeder, making the layer
infrastructure observable and useful.
Probe-outcome discrimination -- the failure interpretation is
necessarily tighter than FundsMover's because every probe is
designed to fail at the final hop:
failcode 0x400F (WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS)
-> id1 received the HTLC and rejected it
because our bit-flipped random
payment_hash matches no invoice. This is
the EXPECTED outcome: the route was
viable all the way to id1. Probe
success; no layer write.
code 202 (unparsable onion)
-> cannot pin the failure to a specific
channel or node. On a 2-hop route, id1
is the most likely culprit; disable_node(id1).
echan == chan0 -> our outgoing channel had the issue. Skip
the layer write: CLBOSS already has
authoritative knowledge of local channel
state via listpeerchannels, and the
shared layer is for inter-subsystem
knowledge that isn't already authoritatively
available.
failcode & 0x2000 (NODE-level)
-> disable_node(erring_node).
channel-level failcode on chan1
-> inform_channel_constrained(chan1, dir1, amount1).
unparseable error data
-> swallow; don't guess.
No change to the existing probe-success accounting: the bool
returned by the catching handler still flows into the same
delpay-status / "Finished probing peer X" logic. The only new
behavior is the side-effect layer write inside record_failure.
Open question, deliberately not addressed here: should
ActiveProber ALSO feed inform=succeeded on probe success
(raising lower-bound estimates on chan1)? The DEVSTATE
ACTIVE-PROBER-LAYER-FEEDER-2026-05-20 recommends yes, ~5 lines
extra; defer until empirical signet observation shows whether
the failure-feedback alone is enough to drive observable
routing improvements.
2026-05-21 10:54:16 -07:00
|
|
|
|
|
|
|
|
/* Don't record local-channel failures. */
|
|
|
|
|
if (echan == chan0)
|
|
|
|
|
return Ev::lift(false);
|
|
|
|
|
|
|
|
|
|
/* NODE-level: 0x2000 bit set. */
|
|
|
|
|
if (fail & 0x2000) {
|
|
|
|
|
return Boss::Mod::AskreneLayer::disable_node(
|
|
|
|
|
rpc, layer_name, enode
|
|
|
|
|
).then([]() {
|
|
|
|
|
return Ev::lift(false);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* CHANNEL-level failure on a non-chan0 channel
|
|
|
|
|
* (= chan1). Record an upper-bound constraint at
|
|
|
|
|
* the amount that failed to push through.
|
|
|
|
|
*/
|
|
|
|
|
return Boss::Mod::AskreneLayer::inform_channel_constrained(
|
|
|
|
|
rpc, layer_name, echan, edir, amount1
|
|
|
|
|
).then([]() {
|
|
|
|
|
return Ev::lift(false);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-07 15:06:38 +08:00
|
|
|
Ev::Io<void> sendpay() {
|
|
|
|
|
return Ev::lift().then([this]() {
|
|
|
|
|
auto os = std::ostringstream();
|
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
|
|
|
os << chan0 << " " << std::string(chan1);
|
2020-10-07 15:06:38 +08:00
|
|
|
return Boss::log( bus, Debug
|
|
|
|
|
, "ActiveProber: Probe %s by route %s."
|
|
|
|
|
, std::string(peer).c_str()
|
|
|
|
|
, os.str().c_str()
|
|
|
|
|
);
|
|
|
|
|
}).then([this]() {
|
|
|
|
|
auto routeparm = Json::Out();
|
|
|
|
|
auto routearr = routeparm.start_array();
|
|
|
|
|
/* Load the 0th hop. */
|
|
|
|
|
routearr.start_object()
|
|
|
|
|
.field("id", std::string(peer))
|
|
|
|
|
.field("channel", std::string(chan0))
|
|
|
|
|
.field( "direction"
|
|
|
|
|
, self_id > peer ? 1 : 0
|
|
|
|
|
)
|
|
|
|
|
.field( "amount_msat"
|
2024-04-02 12:23:47 -07:00
|
|
|
, amount0.to_msat()
|
2020-10-07 15:06:38 +08:00
|
|
|
)
|
|
|
|
|
.field("delay", delay0)
|
|
|
|
|
.field("style", "tlv")
|
|
|
|
|
.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
|
|
|
/* Load the first hop after the peer, rebuilt from
|
|
|
|
|
* the getroutes path[0] we extracted earlier.
|
|
|
|
|
*
|
|
|
|
|
* We always probe with a short two-hop route (hop
|
|
|
|
|
* 0 above, and this hop 1). This gives the peer
|
|
|
|
|
* the "benefit of the doubt": we only probe the
|
|
|
|
|
* peer and *its* direct peer for uptime and
|
|
|
|
|
* capacity. Still "realistic" since the
|
|
|
|
|
* destinations were chosen as popular nodes (or
|
|
|
|
|
* at least to nodes that CLBOSS thinks are good
|
|
|
|
|
* to have capacity towards).
|
|
|
|
|
*/
|
|
|
|
|
routearr.start_object()
|
|
|
|
|
.field("id", std::string(id1))
|
|
|
|
|
.field("channel", std::string(chan1))
|
|
|
|
|
.field("direction", direction1)
|
|
|
|
|
.field("amount_msat", amount1.to_msat())
|
|
|
|
|
.field("delay", delay1)
|
|
|
|
|
.field("style", "tlv")
|
|
|
|
|
.end_object();
|
2020-10-07 15:06:38 +08:00
|
|
|
routearr.end_array();
|
|
|
|
|
|
2021-04-28 22:59:04 +08:00
|
|
|
auto label = label_prefix + std::string(hash);
|
2020-10-07 15:06:38 +08:00
|
|
|
|
|
|
|
|
auto parms = Json::Out()
|
|
|
|
|
.start_object()
|
|
|
|
|
.field("route", std::move(routeparm))
|
|
|
|
|
.field( "payment_hash"
|
|
|
|
|
, std::string(hash)
|
|
|
|
|
)
|
|
|
|
|
.field("label", label)
|
|
|
|
|
.end_object()
|
|
|
|
|
;
|
|
|
|
|
return rpc.command("sendpay", std::move(parms));
|
|
|
|
|
}).then([this](Jsmn::Object _) {
|
|
|
|
|
|
|
|
|
|
auto parms = Json::Out()
|
|
|
|
|
.start_object()
|
|
|
|
|
.field( "payment_hash"
|
|
|
|
|
, std::string(hash)
|
|
|
|
|
)
|
|
|
|
|
.end_object()
|
|
|
|
|
;
|
|
|
|
|
return rpc.command("waitsendpay", std::move(parms));
|
2020-11-02 19:02:35 +08:00
|
|
|
}).then([](Jsmn::Object _) {
|
2020-10-07 15:06:38 +08:00
|
|
|
|
|
|
|
|
/* Oh look, we succeeded.
|
|
|
|
|
* Should not happen though.
|
|
|
|
|
*/
|
|
|
|
|
return Ev::lift(true);
|
ActiveProber: feed sendpay failures into the clboss askrene layer
When a 2-hop probe sendpay fails, parse the waitsendpay error and
record real route failures into the persistent clboss askrene
layer via Boss::Mod::AskreneLayer's shared helpers. Future
getroutes calls (FundsMover's rebalances, ActiveProber's own
subsequent probes, anything else that consumes the clboss layer)
automatically steer around the failed channel or node.
The PR2 commit introduced the layer + the inform-channel /
disable-node call shape, but FundsMover is the only writer
today, and its writes are gated behind real sendpay attempts
that rarely fire (fees on small rebalance amounts almost always
exceed the prorated budget, so FundsMover gives up
pre-sendpay). Net result: the layer is essentially empty in
production.
ActiveProber, by contrast, runs on a timer and sends real
sendpays without a budget filter. Wiring its failure path to
the layer turns it into the dominant feeder, making the layer
infrastructure observable and useful.
Probe-outcome discrimination -- the failure interpretation is
necessarily tighter than FundsMover's because every probe is
designed to fail at the final hop:
failcode 0x400F (WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS)
-> id1 received the HTLC and rejected it
because our bit-flipped random
payment_hash matches no invoice. This is
the EXPECTED outcome: the route was
viable all the way to id1. Probe
success; no layer write.
code 202 (unparsable onion)
-> cannot pin the failure to a specific
channel or node. On a 2-hop route, id1
is the most likely culprit; disable_node(id1).
echan == chan0 -> our outgoing channel had the issue. Skip
the layer write: CLBOSS already has
authoritative knowledge of local channel
state via listpeerchannels, and the
shared layer is for inter-subsystem
knowledge that isn't already authoritatively
available.
failcode & 0x2000 (NODE-level)
-> disable_node(erring_node).
channel-level failcode on chan1
-> inform_channel_constrained(chan1, dir1, amount1).
unparseable error data
-> swallow; don't guess.
No change to the existing probe-success accounting: the bool
returned by the catching handler still flows into the same
delpay-status / "Finished probing peer X" logic. The only new
behavior is the side-effect layer write inside record_failure.
Open question, deliberately not addressed here: should
ActiveProber ALSO feed inform=succeeded on probe success
(raising lower-bound estimates on chan1)? The DEVSTATE
ACTIVE-PROBER-LAYER-FEEDER-2026-05-20 recommends yes, ~5 lines
extra; defer until empirical signet observation shows whether
the failure-feedback alone is enough to drive observable
routing improvements.
2026-05-21 10:54:16 -07:00
|
|
|
}).catching<RpcError>([this](RpcError const& err) {
|
|
|
|
|
/* Inspect the error to determine: was this a
|
|
|
|
|
* successful probe (id1 received the HTLC and
|
|
|
|
|
* rejected it with
|
|
|
|
|
* WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS as
|
|
|
|
|
* expected given our bit-flipped random
|
|
|
|
|
* payment_hash) or a real route failure? Real
|
|
|
|
|
* failures are recorded into the persistent
|
|
|
|
|
* clboss askrene layer so future getroutes
|
|
|
|
|
* calls steer around the failing channel/node.
|
|
|
|
|
*
|
|
|
|
|
* record_failure returns whether the probe was
|
|
|
|
|
* useful (reached destination), but the bool
|
|
|
|
|
* we hand downstream tracks sendpay-completion,
|
|
|
|
|
* not probe-usefulness. We are in the
|
|
|
|
|
* RpcError handler, so the sendpay failed in
|
|
|
|
|
* CLN's view regardless of how useful the
|
|
|
|
|
* probe was -- delpay below must pass
|
|
|
|
|
* status="failed" to match what CLN recorded.
|
|
|
|
|
* Discard the probe-useful bit here.
|
|
|
|
|
*/
|
|
|
|
|
return record_failure(err).then([](bool) {
|
|
|
|
|
return Ev::lift(false);
|
|
|
|
|
});
|
2020-10-07 15:06:38 +08:00
|
|
|
}).then([this](bool success) {
|
|
|
|
|
|
|
|
|
|
auto status = std::string(
|
|
|
|
|
success ? "complete" : "failed"
|
|
|
|
|
);
|
|
|
|
|
/* Now delete the payment, so that the operator
|
|
|
|
|
* does not get confused with random failing
|
|
|
|
|
* payments they did not make. */
|
|
|
|
|
auto parms = Json::Out()
|
|
|
|
|
.start_object()
|
|
|
|
|
.field( "payment_hash"
|
|
|
|
|
, std::string(hash)
|
|
|
|
|
)
|
|
|
|
|
.field( "status"
|
|
|
|
|
, status
|
|
|
|
|
)
|
|
|
|
|
.end_object()
|
|
|
|
|
;
|
|
|
|
|
return rpc.command("delpay", std::move(parms));
|
|
|
|
|
}).then([](Jsmn::Object _) {
|
|
|
|
|
/* We do not actually care if the `delpay` succeeds
|
|
|
|
|
* or not.
|
|
|
|
|
*/
|
|
|
|
|
return Ev::lift();
|
|
|
|
|
}).catching<RpcError>([](RpcError const& _) {
|
|
|
|
|
return Ev::lift();
|
|
|
|
|
}).then([this]() {
|
|
|
|
|
return Boss::log( bus, Info
|
|
|
|
|
, "ActiveProber: Finished probing "
|
|
|
|
|
"peer %s."
|
|
|
|
|
, std::string(peer).c_str()
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void ActiveProber::start() {
|
|
|
|
|
bus.subscribe<Msg::Init
|
|
|
|
|
>([this](Msg::Init const& init) {
|
|
|
|
|
rpc = &init.rpc;
|
|
|
|
|
self_id = init.self_id;
|
|
|
|
|
return Ev::lift();
|
|
|
|
|
});
|
|
|
|
|
bus.subscribe<Msg::ProbeActively
|
|
|
|
|
>([this](Msg::ProbeActively const& m) {
|
|
|
|
|
auto run = Run::create(*this, m.peer);
|
|
|
|
|
return Boss::concurrent( wait_for_rpc(rpc)
|
|
|
|
|
+ run->run()
|
|
|
|
|
);
|
|
|
|
|
});
|
2021-04-28 22:59:04 +08:00
|
|
|
|
|
|
|
|
using Msg::ProvideDeletablePaymentLabelFilter;
|
|
|
|
|
using Msg::SolicitDeletablePaymentLabelFilter;
|
|
|
|
|
bus.subscribe<SolicitDeletablePaymentLabelFilter
|
|
|
|
|
>([this](SolicitDeletablePaymentLabelFilter const& _) {
|
|
|
|
|
return bus.raise(ProvideDeletablePaymentLabelFilter{
|
|
|
|
|
&is_our_label
|
|
|
|
|
});
|
|
|
|
|
});
|
2020-10-07 15:06:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}}
|