ChannelCreateDestroyMonitor: tolerate missing old_state (CLN v26.06)

CLN's channel_state_changed notification used to emit the sentinel
value "unknown" for old_state when there was no previous state.
That value was deprecated in v25.05 and is last-supported in v26.04;
as of v26.06 the old_state field is simply omitted instead (see
doc/developers-guide/deprecated-features.md, entry
"channel_state_changed.old_state.unknown").

The notification handler unconditionally extracted old_state, which
throws Jsmn::TypeError when the field is absent.  The throw was
caught by the surrounding handler, logged as Error
("Unexpected channel_state_changed payload: ..."), and then the
handler returned without taking action.  Functional behavior was
unchanged compared to the legacy "unknown" path (both result in
no destruction event), but the new release variant produced noisy
Error log lines for every state-changed notification on nodes
running v26.06+.

Add an explicit has() check and leave old_state as the default
empty string when absent.  The empty string will not match either
"CHANNELD_NORMAL" or "CHANNELD_AWAITING_LOCKIN", so the handler
falls through to the no-op return -- the same outcome the legacy
catch-and-log path produced, but silently.

This is the third and final commit of the v26.06 compatibility
PR (preceded by the Dowser and Matchmaker/ActiveProber commits).
This commit is contained in:
Ken Sedgwick 2026-05-19 13:51:22 -07:00
parent ff55905794
commit d978de9d84
No known key found for this signature in database
GPG key ID: DBD2AF0849D711A9
3 changed files with 113 additions and 1 deletions

View file

@ -173,7 +173,17 @@ void ChannelCreateDestroyMonitor::start() {
try {
auto payload = params["channel_state_changed"];
n = Ln::NodeId(std::string(payload["peer_id"]));
old_state = std::string(payload["old_state"]);
/* `old_state` may be omitted: as of CLN v26.06 the
* previously-deprecated sentinel value "unknown"
* is no longer emitted; the field is simply
* absent instead. Leave old_state as the empty
* string in that case -- the CHANNELD_NORMAL /
* CHANNELD_AWAITING_LOCKIN check below will not
* match, so we skip silently, matching the
* original intent for the "unknown" value.
*/
if (payload.has("old_state"))
old_state = std::string(payload["old_state"]);
new_state = std::string(payload["new_state"]);
} catch (std::runtime_error const& err) {
return Boss::log( bus, Error

View file

@ -599,6 +599,7 @@ TESTS = \
tests/boss/channelcandidateinvestigator/test_gumshoe \
tests/boss/channelcandidateinvestigator/test_secretary \
tests/boss/test_availablerpccommandsannouncer \
tests/boss/test_channel_create_destroy_monitor_missing_old_state \
tests/boss/test_channelcreationdecider \
tests/boss/test_channelcreator_planner \
tests/boss/test_channelcreator_rearrangerbysize \

View file

@ -0,0 +1,101 @@
#undef NDEBUG
#include"Boss/Mod/ChannelCreateDestroyMonitor.hpp"
#include"Boss/Msg/ChannelDestruction.hpp"
#include"Boss/Msg/ListpeersAnalyzedResult.hpp"
#include"Boss/Msg/Notification.hpp"
#include"Boss/Shutdown.hpp"
#include"Ev/Io.hpp"
#include"Ev/start.hpp"
#include"Ev/yield.hpp"
#include"Jsmn/Object.hpp"
#include"Ln/NodeId.hpp"
#include"S/Bus.hpp"
#include<assert.h>
#include<memory>
#include<string>
/* Regression test for the CLN v26.06 compatibility branch in
* Boss::Mod::ChannelCreateDestroyMonitor that tolerates
* channel_state_changed notifications without an old_state field.
*
* Pre-v26.06 CLN emitted old_state="unknown" when a channel had
* no prior state. v25.05 deprecated that sentinel and v26.06+
* drops the old_state field entirely on the same case. The
* handler must:
* - not throw on the missing field,
* - not emit a ChannelDestruction event (matching the legacy
* "unknown" behaviour, where the CHANNELD_NORMAL /
* CHANNELD_AWAITING_LOCKIN check would never have matched
* "unknown" either).
*/
int main() {
auto bus = S::Bus();
auto monitor = Boss::Mod::ChannelCreateDestroyMonitor(bus);
auto destruction_count = std::make_shared<int>(0);
bus.subscribe<Boss::Msg::ChannelDestruction
>([destruction_count](Boss::Msg::ChannelDestruction const& _) {
*destruction_count = *destruction_count + 1;
return Ev::lift();
});
auto const peer_str = std::string(
"020000000000000000000000000000000000000000000000000000000000000000"
);
auto peer = Ln::NodeId(peer_str);
auto code = Ev::lift().then([&]() {
/* Seed the monitor's channeled set with `peer` and
* mark initted = true. Without this, the
* notification handler would block in
* wait_for_true(initted).
*/
auto r = Boss::Msg::ListpeersAnalyzedResult{};
r.connected_channeled.insert(peer);
r.initial = true;
return bus.raise(std::move(r));
}).then([&]() {
/* Build a channel_state_changed notification with
* NO old_state field -- the v26.06+ shape.
* new_state arbitrary; if the handler incorrectly
* parsed an empty old_state as matching one of the
* destruction-trigger states, we would see a
* ChannelDestruction event.
*/
auto json = std::string(
"{\"channel_state_changed\":{"
"\"peer_id\":\""
) + peer_str + "\","
"\"new_state\":\"ONCHAIN\""
"}}";
auto params = Jsmn::Object::parse_json(json.c_str());
return bus.raise(Boss::Msg::Notification{
std::string("channel_state_changed"),
params
});
}).then([&]() {
/* Pump a few event-loop ticks so the handler's
* wait_for_true polling and subsequent body run.
*/
return Ev::yield()
+ Ev::yield()
+ Ev::yield()
+ Ev::yield();
}).then([&]() {
return bus.raise(Boss::Shutdown{});
}).then([]() {
return Ev::lift(0);
});
auto ec = Ev::start(code);
assert(ec == 0);
/* The destruction handler must NOT have fired. empty
* old_state matches neither "CHANNELD_NORMAL" nor
* "CHANNELD_AWAITING_LOCKIN", so the handler returns
* Ev::lift() without doing anything.
*/
assert(*destruction_count == 0);
return 0;
}