mirror of
https://github.com/ZmnSCPxj/clboss.git
synced 2026-08-13 12:33:20 +02:00
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.
This commit is contained in:
parent
a18daeb65b
commit
04f6021551
6 changed files with 269 additions and 89 deletions
|
|
@ -293,30 +293,39 @@ private:
|
|||
return rpc.command("getroutes", std::move(pj));
|
||||
}).then([this](Jsmn::Object res) {
|
||||
try {
|
||||
/* 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.
|
||||
/* getroutes path[] hop fields node_id_out /
|
||||
* amount_out_msat / cltv_out shipped in CLN
|
||||
* v26.06 (short_channel_id_dir is older,
|
||||
* v24.11, and unconditional). The
|
||||
* deprecated pre-v26.06 names carry
|
||||
* one-hop-shifted in-side values that must
|
||||
* never feed a sendpay route, so there is
|
||||
* deliberately NO fallback: 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.
|
||||
*/
|
||||
auto path0 = res["routes"][0]["path"][0];
|
||||
if ( !path0.has("node_id_out")
|
||||
|| !path0.has("amount_out_msat")
|
||||
|| !path0.has("cltv_out")
|
||||
) {
|
||||
if (!to_try.empty())
|
||||
to_try.pop();
|
||||
return Boss::log( bus, Error
|
||||
, "ActiveProber: getroutes "
|
||||
"hop lacks node_id_out/"
|
||||
"amount_out_msat/cltv_out; "
|
||||
"CLBOSS requires CLN "
|
||||
"v26.06+ (or a backport of "
|
||||
"those getroutes fields)."
|
||||
).then([]() {
|
||||
return Ev::lift(false);
|
||||
});
|
||||
}
|
||||
id1 = Ln::NodeId(std::string(
|
||||
path0.has("node_id_out")
|
||||
? path0["node_id_out"]
|
||||
: path0["next_node_id"]
|
||||
path0["node_id_out"]
|
||||
));
|
||||
/* short_channel_id_dir is "SCID/dir"; split
|
||||
* into the SCID and the direction. If
|
||||
|
|
@ -337,14 +346,10 @@ private:
|
|||
sdir.substr(slash + 1)
|
||||
));
|
||||
amount1 = Ln::Amount::object(
|
||||
path0.has("amount_out_msat")
|
||||
? path0["amount_out_msat"]
|
||||
: path0["amount_msat"]
|
||||
path0["amount_out_msat"]
|
||||
);
|
||||
delay1 = std::uint32_t(double(
|
||||
path0.has("cltv_out")
|
||||
? path0["cltv_out"]
|
||||
: path0["delay"]
|
||||
path0["cltv_out"]
|
||||
));
|
||||
} catch (std::exception const& _) {
|
||||
/* Broaden catch to std::exception so we also
|
||||
|
|
|
|||
|
|
@ -128,20 +128,31 @@ private:
|
|||
).then([this](Jsmn::Object res) {
|
||||
auto patron = Ln::NodeId();
|
||||
try {
|
||||
/* getroutes' path[K].node_id_out was added
|
||||
* v26.06; the deprecated old name is
|
||||
* next_node_id, kept through v27.06 except
|
||||
* when developer mode suppresses
|
||||
* deprecated outputs. Prefer the new
|
||||
* name, fall back to the old. TODO: drop
|
||||
* the fallback once v26.04 is no longer
|
||||
* supported.
|
||||
/* 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.
|
||||
*/
|
||||
auto path0 = res["routes"][0]["path"][0];
|
||||
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()
|
||||
);
|
||||
});
|
||||
patron = Ln::NodeId(std::string(
|
||||
path0.has("node_id_out")
|
||||
? path0["node_id_out"]
|
||||
: path0["next_node_id"]
|
||||
path0["node_id_out"]
|
||||
));
|
||||
} catch (std::exception const&) {
|
||||
/* Jsmn::TypeError from the field access OR
|
||||
|
|
|
|||
|
|
@ -637,33 +637,47 @@ private:
|
|||
if (!path.is_array() || path.size() == 0)
|
||||
throw Jsmn::TypeError();
|
||||
|
||||
/* getroutes path[] hop fields were renamed
|
||||
* in CLN v26.06. Which set of names
|
||||
* actually appears in the response
|
||||
* depends on the CLN version AND whether
|
||||
* the node runs with developer mode
|
||||
* (which suppresses deprecated outputs):
|
||||
*
|
||||
* v26.04 -> old only
|
||||
* v26.06+ no developer -> both emitted
|
||||
* v26.06+ developer=true -> new 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
|
||||
* removed in v27.06.
|
||||
*
|
||||
* short_channel_id_dir is older (v24.11)
|
||||
* and is emitted unconditionally.
|
||||
/* getroutes path[] hop fields node_id_out /
|
||||
* amount_out_msat / cltv_out shipped in
|
||||
* CLN v26.06 (short_channel_id_dir is
|
||||
* older, v24.11, and unconditional). The
|
||||
* deprecated pre-v26.06 names carry
|
||||
* one-hop-shifted in-side values that
|
||||
* must never be spliced into a sendpay
|
||||
* route, so there is deliberately NO
|
||||
* fallback: the Initiator version gate
|
||||
* refuses stock older CLN at startup, and
|
||||
* this per-response check keeps a
|
||||
* bypassed gate (clboss-skip-cln-version-
|
||||
* check) loud instead of subtly wrong.
|
||||
*/
|
||||
route.clear();
|
||||
for (auto hop_j : path) {
|
||||
if ( !hop_j.has("node_id_out")
|
||||
|| !hop_j.has("amount_out_msat")
|
||||
|| !hop_j.has("cltv_out")
|
||||
) {
|
||||
return Boss::log( bus, Error
|
||||
, "FundsMover[%s]: "
|
||||
"getroutes hop lacks "
|
||||
"node_id_out/"
|
||||
"amount_out_msat/"
|
||||
"cltv_out; CLBOSS "
|
||||
"requires CLN v26.06+ "
|
||||
"(or a backport of "
|
||||
"those getroutes "
|
||||
"fields). Refusing to "
|
||||
"build a route from "
|
||||
"the deprecated "
|
||||
"shifted fields."
|
||||
, attempt_tag().c_str()
|
||||
).then([]() {
|
||||
return Ev::lift(false);
|
||||
});
|
||||
}
|
||||
Hop hop;
|
||||
hop.id = Ln::NodeId(std::string(
|
||||
hop_j.has("node_id_out")
|
||||
? hop_j["node_id_out"]
|
||||
: hop_j["next_node_id"]
|
||||
hop_j["node_id_out"]
|
||||
));
|
||||
auto sdir = std::string(
|
||||
hop_j["short_channel_id_dir"]
|
||||
|
|
@ -678,14 +692,10 @@ private:
|
|||
std::stoul(sdir.substr(slash + 1))
|
||||
);
|
||||
hop.amount_msat = Ln::Amount::object(
|
||||
hop_j.has("amount_out_msat")
|
||||
? hop_j["amount_out_msat"]
|
||||
: hop_j["amount_msat"]
|
||||
hop_j["amount_out_msat"]
|
||||
);
|
||||
hop.delay = std::uint32_t(double(
|
||||
hop_j.has("cltv_out")
|
||||
? hop_j["cltv_out"]
|
||||
: hop_j["delay"]
|
||||
hop_j["cltv_out"]
|
||||
));
|
||||
route.push_back(hop);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include"Boss/Msg/EndOfOptions.hpp"
|
||||
#include"Boss/Msg/Init.hpp"
|
||||
#include"Boss/Msg/ManifestOption.hpp"
|
||||
#include"Boss/Msg/Manifestation.hpp"
|
||||
#include"Boss/Msg/Option.hpp"
|
||||
#include"Boss/Msg/ProvideStatus.hpp"
|
||||
#include"Boss/Msg/SolicitStatus.hpp"
|
||||
|
|
@ -29,6 +30,7 @@
|
|||
#include"Util/make_unique.hpp"
|
||||
#include<algorithm>
|
||||
#include<assert.h>
|
||||
#include<cstdio>
|
||||
#include<set>
|
||||
#include<sstream>
|
||||
#include<stdlib.h>
|
||||
|
|
@ -68,6 +70,9 @@ private:
|
|||
|
||||
std::string proxy;
|
||||
bool always_use_proxy;
|
||||
/* Set from the clboss-skip-cln-version-check flag at init;
|
||||
* see check_cln_version() below. */
|
||||
bool skip_version_check;
|
||||
std::unique_ptr<Net::Connector> connector;
|
||||
|
||||
Secp256k1::Random random;
|
||||
|
|
@ -109,6 +114,85 @@ private:
|
|||
});
|
||||
}
|
||||
|
||||
/* The getroutes hop fields CLBOSS builds sendpay routes from
|
||||
* (node_id_out / amount_out_msat / cltv_out) shipped in CLN
|
||||
* v26.06. Older stock CLN emits only the deprecated names,
|
||||
* whose values are one-hop-shifted (in-side): routes built
|
||||
* from them misprice every middle hop, and the resulting
|
||||
* failures hard-exclude healthy channels in the persistent
|
||||
* failure-learning layer -- strictly worse than not running
|
||||
* at all. Refuse to start instead.
|
||||
*
|
||||
* clboss-skip-cln-version-check bypasses the gate for
|
||||
* operators whose older CLN carries a backport of the v26.06
|
||||
* getroutes fields (the version string alone cannot show
|
||||
* that); the getroutes parse sites still verify the fields on
|
||||
* every response, so a mistaken bypass fails loudly per
|
||||
* attempt instead of building shifted routes. An unparseable
|
||||
* version string is treated the same fail-open way -- custom
|
||||
* builds deserve a warning, not a lockout.
|
||||
*/
|
||||
Ev::Io<void> check_cln_version(Jsmn::Object info) {
|
||||
auto version = std::string();
|
||||
if (info.has("version") && info["version"].is_string())
|
||||
version = std::string(info["version"]);
|
||||
if (skip_version_check)
|
||||
return Boss::log( bus, Info
|
||||
, "Initiator: clboss-skip-cln-"
|
||||
"version-check set; not enforcing "
|
||||
"the CLN v26.06 minimum against "
|
||||
"\"%s\"."
|
||||
, version.c_str()
|
||||
);
|
||||
auto major = unsigned(0);
|
||||
auto minor = unsigned(0);
|
||||
if (std::sscanf(version.c_str(), "v%u.%u", &major, &minor) != 2)
|
||||
return Boss::log( bus, Warn
|
||||
, "Initiator: unrecognized CLN "
|
||||
"version \"%s\"; proceeding -- the "
|
||||
"getroutes parse verifies the "
|
||||
"required v26.06 fields on every "
|
||||
"response."
|
||||
, version.c_str()
|
||||
);
|
||||
if (major > 26 || (major == 26 && minor >= 6))
|
||||
return Ev::lift();
|
||||
return refuse_to_start( std::string("Initiator: CLN ")
|
||||
+ version
|
||||
+ " is older than v26.06: its getroutes "
|
||||
"lacks the node_id_out/"
|
||||
"amount_out_msat/cltv_out fields "
|
||||
"CLBOSS builds routes from, and the "
|
||||
"deprecated fields carry one-hop-"
|
||||
"shifted values that would misroute "
|
||||
"rebalances and poison the failure-"
|
||||
"learning layer. Refusing to start; "
|
||||
"no state was created or modified. "
|
||||
"Upgrade CLN to v26.06 or newer, or "
|
||||
"-- only if your CLN carries a "
|
||||
"backport of the v26.06 getroutes "
|
||||
"fields -- start with "
|
||||
"clboss-skip-cln-version-check."
|
||||
);
|
||||
}
|
||||
/* Same log-flush-then-abort dance as error() above: give the
|
||||
* output machinery a chance to push the Error line to
|
||||
* lightningd before the process exits. */
|
||||
Ev::Io<void> refuse_to_start(std::string reason) {
|
||||
return Boss::log( bus, Boss::Error
|
||||
, "%s"
|
||||
, reason.c_str()
|
||||
).then([]() {
|
||||
auto act = Ev::lift();
|
||||
for (auto i = 0; i < 32; ++i)
|
||||
act += Ev::yield();
|
||||
return act;
|
||||
}).then([]() {
|
||||
abort();
|
||||
return Ev::lift();
|
||||
});
|
||||
}
|
||||
|
||||
void setup_proxy(std::string proxy) {
|
||||
auto host = std::string();
|
||||
auto port = int();
|
||||
|
|
@ -144,6 +228,7 @@ public:
|
|||
, initted(false)
|
||||
, proxy("")
|
||||
, always_use_proxy(false)
|
||||
, skip_version_check(false)
|
||||
{
|
||||
assert(open_rpc_socket);
|
||||
|
||||
|
|
@ -256,28 +341,6 @@ public:
|
|||
return Boss::log( bus, Debug
|
||||
, "RPC socket opened."
|
||||
);
|
||||
}).then([this]() {
|
||||
db = Sqlite3::Db("data.clboss");
|
||||
return db.transact();
|
||||
}).then([this](Sqlite3::Tx tx) {
|
||||
tx.query_execute("PRAGMA application_id = 0x424F5353;");
|
||||
tx.query_execute("PRAGMA user_version = 0x2020434C;");
|
||||
tx.commit();
|
||||
return Boss::log( bus, Debug
|
||||
, "Database file opened."
|
||||
);
|
||||
}).then([this]() {
|
||||
return bus.raise(Msg::DbResource{db});
|
||||
}).then([this]() {
|
||||
return Boss::Signer( "keys.clboss"
|
||||
, random
|
||||
, db
|
||||
).construct();
|
||||
}).then([this](std::unique_ptr<Secp256k1::SignerIF> n_signer) {
|
||||
signer = std::move(n_signer);
|
||||
return Boss::log( bus, Debug
|
||||
, "Privkey file loaded."
|
||||
);
|
||||
}).then([this]() {
|
||||
return rpc->command( "getinfo"
|
||||
, Json::Out::empty_object()
|
||||
|
|
@ -310,7 +373,34 @@ public:
|
|||
Net::DirectConnector
|
||||
>();
|
||||
|
||||
return Ev::lift();
|
||||
/* CLN version gate. Deliberately ahead
|
||||
* of the database and signer steps below:
|
||||
* a refusal must leave no on-disk trace
|
||||
* (no data.clboss, no schema, no
|
||||
* keys.clboss). */
|
||||
return check_cln_version(info);
|
||||
}).then([this]() {
|
||||
db = Sqlite3::Db("data.clboss");
|
||||
return db.transact();
|
||||
}).then([this](Sqlite3::Tx tx) {
|
||||
tx.query_execute("PRAGMA application_id = 0x424F5353;");
|
||||
tx.query_execute("PRAGMA user_version = 0x2020434C;");
|
||||
tx.commit();
|
||||
return Boss::log( bus, Debug
|
||||
, "Database file opened."
|
||||
);
|
||||
}).then([this]() {
|
||||
return bus.raise(Msg::DbResource{db});
|
||||
}).then([this]() {
|
||||
return Boss::Signer( "keys.clboss"
|
||||
, random
|
||||
, db
|
||||
).construct();
|
||||
}).then([this](std::unique_ptr<Secp256k1::SignerIF> n_signer) {
|
||||
signer = std::move(n_signer);
|
||||
return Boss::log( bus, Debug
|
||||
, "Privkey file loaded."
|
||||
);
|
||||
}).then([this]() {
|
||||
return rpc->command( "listconfigs"
|
||||
, Json::Out::empty_object()
|
||||
|
|
@ -407,6 +497,24 @@ public:
|
|||
options.insert(o.name);
|
||||
return Ev::lift();
|
||||
});
|
||||
|
||||
bus.subscribe<Msg::Manifestation
|
||||
>([this](Msg::Manifestation const&) {
|
||||
return bus.raise(Msg::ManifestOption{
|
||||
"clboss-skip-cln-version-check",
|
||||
Msg::OptionType_Flag,
|
||||
Json::Out::direct(false),
|
||||
"Skip the CLN >= v26.06 startup check. ONLY "
|
||||
"for CLN builds older than v26.06 that carry "
|
||||
"a backport of the v26.06 getroutes fields "
|
||||
"(node_id_out/amount_out_msat/cltv_out). On "
|
||||
"a stock older CLN, CLBOSS would build "
|
||||
"mispriced routes and poison its failure-"
|
||||
"learning layer; every getroutes response is "
|
||||
"verified even with this set.",
|
||||
false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
|
|
@ -423,6 +531,17 @@ private:
|
|||
if (!options_j.has(o))
|
||||
continue;
|
||||
auto value = options_j[o];
|
||||
/* Stashed directly rather than via a Msg::Option
|
||||
* subscription: the version gate consults it
|
||||
* before most modules are even listening, and
|
||||
* Initiator itself owns the option. */
|
||||
if (o == "clboss-skip-cln-version-check") {
|
||||
if (value.is_boolean())
|
||||
skip_version_check = !!value;
|
||||
else if (value.is_string())
|
||||
skip_version_check =
|
||||
std::string(value) == "true";
|
||||
}
|
||||
rv += bus.raise(Msg::Option{o, std::move(value)});
|
||||
}
|
||||
return rv;
|
||||
|
|
|
|||
20
CHANGELOG.md
20
CHANGELOG.md
|
|
@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- **BREAKING**: CLBOSS now requires **Core Lightning v26.06 or later**.
|
||||
The rebalancer and probing subsystems build routes from the
|
||||
`getroutes` per-hop fields `node_id_out` / `amount_out_msat` /
|
||||
`cltv_out`, which shipped in v26.06; the deprecated pre-v26.06
|
||||
fields carry one-hop-shifted values that would misprice routes and
|
||||
poison CLBOSS's failure-learning askrene layer. CLBOSS checks the
|
||||
CLN version at startup and refuses to run on older nodes, before
|
||||
creating or modifying any on-disk state (note: with
|
||||
`important-plugin`, a refused start stops lightningd itself).
|
||||
Operators running an older CLN that carries a backport of the
|
||||
v26.06 `getroutes` fields can bypass the startup check with
|
||||
`--clboss-skip-cln-version-check`; every `getroutes` response is
|
||||
still verified even with the check skipped. Users on older CLN
|
||||
releases should stay on CLBOSS 0.16.x, which uses the legacy
|
||||
`getroute`/`pay` APIs that older CLN still provides.
|
||||
|
||||
## [0.16.0] - 2026-04-21: "Darkness on the Edge of the Mempool"
|
||||
|
||||
### Added
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -101,6 +101,21 @@ further.
|
|||
Installing
|
||||
----------
|
||||
|
||||
### Requirements
|
||||
|
||||
CLBOSS requires **Core Lightning v26.06 or later**. Its rebalancer
|
||||
and probing subsystems build routes from the `getroutes` per-hop
|
||||
fields `node_id_out` / `amount_out_msat` / `cltv_out`, which shipped
|
||||
in v26.06; the deprecated pre-v26.06 fields carry one-hop-shifted
|
||||
values that would misprice routes and poison CLBOSS's
|
||||
failure-learning askrene layer. CLBOSS checks the CLN version at
|
||||
startup and refuses to run on older nodes — before creating or
|
||||
modifying any on-disk state. If (and only if) your older CLN carries
|
||||
a backport of the v26.06 `getroutes` fields, you can bypass the
|
||||
startup check with `--clboss-skip-cln-version-check`; every
|
||||
`getroutes` response is still verified even with the check skipped.
|
||||
Users on older CLN releases should stay on CLBOSS 0.16.x.
|
||||
|
||||
From an [official source release](https://github.com/ZmnSCPxj/clboss/releases), just:
|
||||
|
||||
./configure && make
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue