add a test for downgrading askrene datastore
What changed, and why it matters
This commit adds a regression test for Core Lightning's database downgrade path. It ensures that when a user downgrades from an upcoming v26.09 release to an older version (v26.06), the new 'impressions' data added by the askrene routing subsystem is cleanly removed while other askrene data survives. The code change itself is purely a test and a marker comment in the migration list; it does not fix an active security vulnerability.
No security action required. Treat as normal QA/test infrastructure. If auditing downgrade tooling, ensure the actual downgrade SQL/script that removes impressions is present and correct separately from this test commit.
Security signals we found
Database downgrade path for new askrene schema (impressions table)
Regression test only; no production code change
No input validation, cryptography, network, or authorization changes
Evidence from the diff
The diff adds test_downgrade_impressions in tests/test_downgrade.py, which exercises the lightning-downgrade tool against an askrene layer containing impressions, constraints, created channels, channel updates, biases, disabled nodes, and node biases. It verifies that after downgrade to v26.06 the impressions list is removed and the remaining layer data is still readable by the older node. A comment /* ^v26.09 */ is added to wallet/migrations.c to mark the migration boundary. CI is updated to set CLN_PREV_VERSION=v26.06 for downgrade tests. There is no runtime code change that alters node behavior or fixes a memory-safety / cryptographic bug.
Changed components
tests/test_downgrade.pywallet/migrations.c (comment only).github/workflows/ci.yamlInspect captured patch +156 / −1
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index c93221c..eec91fb 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -465,6 +465,7 @@ jobs:
LIGHTNINGD_POSTGRES_NO_VACUUM: 1
VALGRIND: ${{ matrix.VALGRIND }}
PREV_LIGHTNINGD: old-cln/usr/bin/lightningd
+ CLN_PREV_VERSION: v26.06
PYTEST_OPTS: ${{ env.PYTEST_OPTS_BASE }}
run: |
env
diff --git a/tests/test_downgrade.py b/tests/test_downgrade.py
index f48686b..c791366 100644
--- a/tests/test_downgrade.py
+++ b/tests/test_downgrade.py
@@ -1,10 +1,14 @@
from fixtures import * # noqa: F401,F403
from utils import (
TIMEOUT, # noqa: F401
- first_scid, only_one,
+ first_scid,
+ only_one,
+ generate_gossip_store,
+ GenChannel,
)
import os
+import pytest
import subprocess
# From the binary:
@@ -13,6 +17,22 @@ ERROR_DBFAIL = 2
ERROR_USAGE = 3
# ERROR_INTERNAL = 99
+PREV_LIGHTNINGD = os.getenv('PREV_LIGHTNINGD')
+CLN_PREV_VERSION = os.getenv('CLN_PREV_VERSION')
+
+
+def direction(src, dst):
+ """BOLT 7 direction: 0 means from lesser encoded id"""
+ if src < dst:
+ return 0
+ return 1
+
+
+def scid_dir(nodemap, node1_idx, node2_idx, chan_idx):
+ """Get short_channel_id_dir for a channel in generate_gossip_store format"""
+ dir_val = direction(nodemap[node1_idx], nodemap[node2_idx])
+ return f"{node1_idx}x{node2_idx}x{chan_idx}/{dir_val}"
+
def downgrade_cmdline(node):
# lightning-downgrade understands a subset of the options
@@ -111,3 +131,136 @@ def test_downgrade_bias(node_factory, executor):
stderr=subprocess.PIPE)
_, err = p.communicate(timeout=TIMEOUT)
assert p.returncode == 0
+
+
+@pytest.mark.skipif(not CLN_PREV_VERSION, reason="Depends on CLN_PREV_VERSION")
+@pytest.mark.skipif(not PREV_LIGHTNINGD, reason="Depends on PREV_LIGHTNINGD")
+def test_downgrade_impressions(node_factory, executor):
+ """Impressions are added since v26.09, the downgrade tool should remove
+ them while the rest of the askrene data remains."""
+ cap_msat = 1000_000_000
+ gsfile, nodemap = generate_gossip_store(
+ [GenChannel(0, 1, capacity_sats=cap_msat // 1000)]
+ )
+ l1 = node_factory.get_node(
+ gossip_store_file=gsfile.name, options={"database-upgrade": True}
+ )
+
+ chan_dir = scid_dir(nodemap, 0, 1, 0)
+ l1.rpc.askrene_create_layer(layer="test_downgrade", persistent=True)
+ expect = {
+ "biases": [],
+ "channel_updates": [],
+ "constraints": [],
+ "created_channels": [],
+ "disabled_nodes": [],
+ "impressions": [],
+ "layer": "test_downgrade",
+ "node_biases": [],
+ "persistent": True,
+ }
+ ret = l1.rpc.askrene_inform_channel(
+ "test_downgrade", chan_dir, 1000000, "succeeded"
+ )
+
+ expect["impressions"] = [
+ {
+ "amount_msat": 1000000,
+ "short_channel_id_dir": chan_dir,
+ "timestamp": ret["impressions"][0]["timestamp"],
+ }
+ ]
+
+ ret = l1.rpc.askrene_inform_channel(
+ "test_downgrade", chan_dir, 500000, "constrained"
+ )
+ expect["constraints"] = [
+ {
+ "maximum_msat": 499999,
+ "short_channel_id_dir": chan_dir,
+ "timestamp": ret["constraints"][0]["timestamp"],
+ }
+ ]
+ ret = l1.rpc.askrene_inform_channel(
+ "test_downgrade", chan_dir, 20000, "unconstrained"
+ )
+ expect["constraints"].append(
+ {
+ "minimum_msat": 20000,
+ "short_channel_id_dir": chan_dir,
+ "timestamp": ret["constraints"][0]["timestamp"],
+ }
+ )
+
+ A_NODE = "020000000000000000000000000000000000000000000000000000000000000001"
+ ret = l1.rpc.askrene_create_channel(
+ "test_downgrade", A_NODE, l1.info["id"], "16000000x1x1", "100000000sat"
+ )
+ expect["created_channels"] = [
+ {
+ "source": A_NODE,
+ "destination": l1.info["id"],
+ "short_channel_id": "16000000x1x1",
+ "capacity_msat": 100000000000,
+ }
+ ]
+
+ l1.rpc.askrene_update_channel(
+ layer="test_downgrade",
+ short_channel_id_dir="16000000x1x1/0",
+ enabled=True,
+ htlc_minimum_msat=1000,
+ )
+ expect["channel_updates"] = [
+ {
+ "enabled": True,
+ "htlc_minimum_msat": 1000,
+ "short_channel_id_dir": "16000000x1x1/0",
+ }
+ ]
+ ret = l1.rpc.askrene_bias_channel(
+ layer="test_downgrade", short_channel_id_dir="16000000x1x1/0", bias=3
+ )
+ expect["biases"] = [
+ {
+ "timestamp": ret["biases"][0]["timestamp"],
+ "bias": 3,
+ "short_channel_id_dir": "16000000x1x1/0",
+ }
+ ]
+ B_NODE = "020000000000000000000000000000000000000000000000000000000000000002"
+
+ l1.rpc.askrene_disable_node(layer="test_downgrade", node=B_NODE)
+ expect["disabled_nodes"] = [B_NODE]
+
+ ret = l1.rpc.askrene_bias_node(
+ layer="test_downgrade", direction="in", node=A_NODE, bias=-2
+ )
+ expect["node_biases"] = [
+ {
+ "in_bias": -2,
+ "out_bias": 0,
+ "node": A_NODE,
+ "timestamp": ret["node_biases"][0]["timestamp"],
+ }
+ ]
+ assert l1.rpc.askrene_listlayers("test_downgrade") == {"layers": [expect]}
+
+ cmd_line = downgrade_cmdline(l1)
+
+ l1.stop()
+
+ p = subprocess.run(cmd_line, timeout=TIMEOUT, capture_output=True, text=True)
+ assert p.returncode == 0
+ if CLN_PREV_VERSION == "v26.06":
+ assert "Downgrade to v26.06 succeeded. Committing." in p.stdout
+
+ # we need to disable the schema checks, this node uses an old API
+ l1.rpc.jsonschemas = {}
+ # in the old node the impressions field doesn't even exist
+ if CLN_PREV_VERSION == "v26.06":
+ del expect["impressions"]
+ l1.daemon.executable = PREV_LIGHTNINGD
+
+ l1.start()
+ assert l1.rpc.askrene_listlayers("test_downgrade") == {"layers": [expect]}
diff --git a/wallet/migrations.c b/wallet/migrations.c
index 16b663a..2ee8c3b 100644
--- a/wallet/migrations.c
+++ b/wallet/migrations.c
@@ -1182,6 +1182,7 @@ static const struct db_migration dbmigrations[] = {
* writes stop in the release that removes chaintopology, freezing all
* the legacy tables at the same height. */
{NULL, migrate_backfill_bwatch_tables, NULL, NULL},
+ /* ^v26.09 */
};
const struct db_migration *get_db_migrations(size_t *num)
Why this scored 16/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.