From 9981e238a3b532459d7b8f013a95942a5658eb5f Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 28 Nov 2025 06:42:32 +1030 Subject: [PATCH] tools/lightning-downgrade: tool to downgrade (offline) v25.12 to v25.09. Signed-off-by: Rusty Russell Changelog-Added: tools: `lightningd-downgrade` can downgrade your database from v25.12 to v25.09 if something goes wrong. --- Makefile | 3 + tests/test_downgrade.py | 65 +++++++++ tools/Makefile | 18 ++- tools/lightning-downgrade.c | 262 ++++++++++++++++++++++++++++++++++++ wallet/Makefile | 2 + 5 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 tests/test_downgrade.py create mode 100644 tools/lightning-downgrade.c diff --git a/Makefile b/Makefile index 58d0ae5df..d48b75b44 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,9 @@ $(info Building version $(VERSION)) # Next release. CLN_NEXT_VERSION := v25.12 +# Previous release (for downgrade testing) +CLN_PREV_VERSION := v25.09 + # --quiet / -s means quiet, dammit! ifeq ($(findstring s,$(word 1, $(MAKEFLAGS))),s) ECHO := : diff --git a/tests/test_downgrade.py b/tests/test_downgrade.py new file mode 100644 index 000000000..9ac82acb7 --- /dev/null +++ b/tests/test_downgrade.py @@ -0,0 +1,65 @@ +from fixtures import * # noqa: F401,F403 +from utils import ( + TIMEOUT # noqa: F401 +) + +import os +import subprocess + + +def test_downgrade(node_factory, executor): + l1, l2 = node_factory.line_graph(2, opts={'may_reconnect': True}) + + # From the binary: + # ERROR_DBVERSION = 1 + # ERROR_DBFAIL = 2 + ERROR_USAGE = 3 + # ERROR_INTERNAL = 99 + + # lightning-downgrade understands a subset of the options + # to lightningd. + downgrade_opts = [] + for o in l1.daemon.opts: + if o in ('network', 'lightning-dir', 'conf', 'rpc-file', 'wallet'): + if l1.daemon.opts[o] is None: + downgrade_opts.append(f"--{o}") + else: + downgrade_opts.append(f"--{o}={l1.daemon.opts[o]}") + + cmd_line = ["tools/lightning-downgrade"] + downgrade_opts + if os.getenv("VALGRIND") == "1": + cmd_line = ['valgrind', '-q', '--error-exitcode=7'] + cmd_line + + # No downgrade on live nodes! + retcode = subprocess.call(cmd_line, timeout=TIMEOUT) + assert retcode == ERROR_USAGE + + l1.stop() + subprocess.check_call(cmd_line) + + # Test with old lightningd if it's available. + old_cln = os.getenv('PREV_LIGHTNINGD') + if old_cln: + current_executable = l1.daemon.executable + l1.daemon.executable = old_cln + + l1.start() + + # It should connect to l2 no problems, make payment. + l1.connect(l2) + inv = l2.rpc.invoice(1000, 'test_downgrade', 'test_downgrade') + l1.rpc.xpay(inv['bolt11']) + l1.stop() + l1.daemon.executable = current_executable + + # Another downgrade is a noop. + assert subprocess.check_output(cmd_line).decode("utf8").startswith("Already compatible with ") + + # Should be able to upgrade without any trouble + l1.daemon.opts['database-upgrade'] = True + l1.start() + assert l1.daemon.is_in_log("Updating database from version") + + l1.connect(l2) + inv2 = l2.rpc.invoice(1000, 'test_downgrade2', 'test_downgrade2') + l1.rpc.xpay(inv2['bolt11']) diff --git a/tools/Makefile b/tools/Makefile index df03ada44..dbb5c0e93 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -1,5 +1,5 @@ #! /usr/bin/make -TOOLS := tools/lightning-hsmtool +TOOLS := tools/lightning-hsmtool tools/lightning-downgrade TOOLS_SRC := $(TOOLS:=.c) TOOLS_OBJ := $(TOOLS_SRC:.c=.o) @@ -13,8 +13,22 @@ ALL_PROGRAMS += $(TOOLS) tools/headerversions: $(FORCE) tools/headerversions.o libccan.a @trap "rm -f $@.tmp.$$$$" EXIT; $(LINK.o) tools/headerversions.o libccan.a $(LOADLIBES) $(LDLIBS) -o $@.tmp.$$$$ && mv -f $@.tmp.$$$$ $@ +$(TOOLS): libcommon.a + tools/headerversions.o: ccan/config.h -tools/lightning-hsmtool: tools/lightning-hsmtool.o libcommon.a +tools/lightning-hsmtool: tools/lightning-hsmtool.o +tools/lightning-downgrade.o: CFLAGS:=$(CFLAGS) -DCLN_PREV_VERSION=$(CLN_PREV_VERSION) + +tools/lightning-downgrade: \ + db/exec.o \ + db/bindings.o \ + db/utils.o \ + wallet/migrations.o \ + $(DB_OBJS) \ + $(WALLET_DB_QUERIES:.c=.o) \ + tools/lightning-downgrade.o + +update-mocks: $(tools/lightning-downgrade.c:%=update-mocks/%.c) clean: tools-clean diff --git a/tools/lightning-downgrade.c b/tools/lightning-downgrade.c new file mode 100644 index 000000000..5dce80f19 --- /dev/null +++ b/tools/lightning-downgrade.c @@ -0,0 +1,262 @@ +/* Tool for limited downgrade of an offline node */ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ERROR_DBVERSION 1 +#define ERROR_DBFAIL 2 +#define ERROR_USAGE 3 +#define ERROR_INTERNAL 99 + +#define PREV_VERSION stringify(CLN_PREV_VERSION) + +struct db_version { + const char *name; + size_t db_height; + bool gossip_store_compatible; +}; + +static const struct db_version db_versions[] = { + { "v25.09", 276, false }, + /* When we implement v25.12 downgrade: { "v25.12", 280, ???}, */ +}; + +static const struct db_version *version_db(const char *version) +{ + for (size_t i = 0; i < ARRAY_SIZE(db_versions); i++) { + if (streq(db_versions[i].name, version)) + return &db_versions[i]; + } + errx(ERROR_INTERNAL, "Unknown version %s", version); +} + +static void db_error(void *unused, bool fatal, const char *fmt, va_list ap) +{ + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + if (fatal) + exit(ERROR_DBFAIL); +} + +/* The standard opt_log_stderr_exit exits with status 1 */ +static void opt_log_stderr_exit_usage(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + va_end(ap); + exit(ERROR_USAGE); +} + +int main(int argc, char *argv[]) +{ + char *config_filename, *base_dir, *net_dir, *rpc_filename, *wallet_dsn = NULL; + size_t current, prev_version_height, num_migrations; + struct db *db; + const struct db_migration *migrations; + struct db_stmt *stmt; + + setup_locale(); + err_set_progname(argv[0]); + + minimal_config_opts(tmpctx, argc, argv, &config_filename, &base_dir, + &net_dir, &rpc_filename); + opt_register_early_arg("--wallet", opt_set_talstr, NULL, + &wallet_dsn, + "Location of the wallet database."); + opt_register_noarg("--help|-h", opt_usage_and_exit, + "A tool to downgrade an offline Core Lightning Node to " PREV_VERSION, + "Print this message."); + opt_early_parse(argc, argv, opt_log_stderr_exit_usage); + opt_parse(&argc, argv, opt_log_stderr_exit_usage); + + if (argc != 1) + opt_usage_exit_fail("No arguments expected"); + + if (!wallet_dsn) + wallet_dsn = tal_fmt(tmpctx, "sqlite3://%s/lightningd.sqlite3", net_dir); + + if (path_is_file(path_join(tmpctx, base_dir, + tal_fmt(tmpctx, "lightningd-%s.pid", + chainparams->network_name)))) { + errx(ERROR_USAGE, + "Lightningd PID file exists, aborting: lightningd must not be running"); + } + + migrations = get_db_migrations(&num_migrations); + prev_version_height = version_db(PREV_VERSION)->db_height; + + /* Open db, check it's the expected version */ + db = db_open(tmpctx, wallet_dsn, false, false, db_error, NULL); + if (!db) + err(1, "Could not open database %s", wallet_dsn); + db->report_changes_fn = NULL; + + db_begin_transaction(db); + db->data_version = db_data_version_get(db); + current = db_get_version(db); + + if (current < prev_version_height) + errx(ERROR_DBVERSION, "Database version %zu already less than %zu expected for %s", + current, prev_version_height, PREV_VERSION); + if (current == prev_version_height) { + printf("Already compatible with %s\n", PREV_VERSION); + exit(0); + } + if (current >= num_migrations) + errx(ERROR_DBVERSION, "Unknown database version %zu: I only know up to %zu (%s)", + current, num_migrations, stringify(CLN_NEXT_VERSION)); + + /* current version is the last migration we did. */ + while (current > prev_version_height) { + if (migrations[current].revertsql) { + stmt = db_prepare_v2(db, migrations[current].revertsql); + db_exec_prepared_v2(stmt); + tal_free(stmt); + } + if (migrations[current].revertfn) { + const char *error = migrations[current].revertfn(tmpctx, db); + if (error) + errx(ERROR_DBFAIL, "Downgrade failed: %s", error); + } + current--; + } + + /* Finally update the version number in the version table */ + stmt = db_prepare_v2(db, SQL("UPDATE version SET version=?;")); + db_bind_int(stmt, current); + db_exec_prepared_v2(stmt); + tal_free(stmt); + + printf("Downgrade to %s succeeded. Committing.\n", PREV_VERSION); + db_commit_transaction(db); + tal_free(db); + + if (!version_db(PREV_VERSION)->gossip_store_compatible) { + printf("Deleting incompatible gossip_store\n"); + unlink(path_join(tmpctx, net_dir, "gossip_store")); + } +} + +/*** We don't actually perform migrations, so these are stubs which abort. ***/ +/* Remake with `make update-mocks` or `make update-mocks/tools/lightning-downgrade.c` */ + +/* AUTOGENERATED MOCKS START */ +/* Generated stub for fillin_missing_channel_blockheights */ +void fillin_missing_channel_blockheights(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "fillin_missing_channel_blockheights called!\n"); abort(); } +/* Generated stub for fillin_missing_channel_id */ +void fillin_missing_channel_id(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED) +{ fprintf(stderr, "fillin_missing_channel_id called!\n"); abort(); } +/* Generated stub for fillin_missing_lease_satoshi */ +void fillin_missing_lease_satoshi(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "fillin_missing_lease_satoshi called!\n"); abort(); } +/* Generated stub for fillin_missing_local_basepoints */ +void fillin_missing_local_basepoints(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "fillin_missing_local_basepoints called!\n"); abort(); } +/* Generated stub for fillin_missing_scriptpubkeys */ +void fillin_missing_scriptpubkeys(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED) +{ fprintf(stderr, "fillin_missing_scriptpubkeys called!\n"); abort(); } +/* Generated stub for insert_addrtype_to_addresses */ +void insert_addrtype_to_addresses(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "insert_addrtype_to_addresses called!\n"); abort(); } +/* Generated stub for migrate_channels_scids_as_integers */ +void migrate_channels_scids_as_integers(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_channels_scids_as_integers called!\n"); abort(); } +/* Generated stub for migrate_convert_old_channel_keyidx */ +void migrate_convert_old_channel_keyidx(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_convert_old_channel_keyidx called!\n"); abort(); } +/* Generated stub for migrate_datastore_commando_runes */ +void migrate_datastore_commando_runes(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_datastore_commando_runes called!\n"); abort(); } +/* Generated stub for migrate_fail_pending_payments_without_htlcs */ +void migrate_fail_pending_payments_without_htlcs(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_fail_pending_payments_without_htlcs called!\n"); abort(); } +/* Generated stub for migrate_fill_in_channel_type */ +void migrate_fill_in_channel_type(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_fill_in_channel_type called!\n"); abort(); } +/* Generated stub for migrate_forwards_add_rowid */ +void migrate_forwards_add_rowid(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_forwards_add_rowid called!\n"); abort(); } +/* Generated stub for migrate_from_account_db */ +void migrate_from_account_db(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_from_account_db called!\n"); abort(); } +/* Generated stub for migrate_inflight_last_tx_to_psbt */ +void migrate_inflight_last_tx_to_psbt(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_inflight_last_tx_to_psbt called!\n"); abort(); } +/* Generated stub for migrate_initialize_alias_local */ +void migrate_initialize_alias_local(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_initialize_alias_local called!\n"); abort(); } +/* Generated stub for migrate_initialize_channel_htlcs_wait_indexes_and_fixup_forwards */ +void migrate_initialize_channel_htlcs_wait_indexes_and_fixup_forwards(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_initialize_channel_htlcs_wait_indexes_and_fixup_forwards called!\n"); abort(); } +/* Generated stub for migrate_initialize_forwards_wait_indexes */ +void migrate_initialize_forwards_wait_indexes(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_initialize_forwards_wait_indexes called!\n"); abort(); } +/* Generated stub for migrate_initialize_invoice_wait_indexes */ +void migrate_initialize_invoice_wait_indexes(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_initialize_invoice_wait_indexes called!\n"); abort(); } +/* Generated stub for migrate_initialize_payment_wait_indexes */ +void migrate_initialize_payment_wait_indexes(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_initialize_payment_wait_indexes called!\n"); abort(); } +/* Generated stub for migrate_invalid_last_tx_psbts */ +void migrate_invalid_last_tx_psbts(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_invalid_last_tx_psbts called!\n"); abort(); } +/* Generated stub for migrate_invoice_created_index_var */ +void migrate_invoice_created_index_var(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_invoice_created_index_var called!\n"); abort(); } +/* Generated stub for migrate_last_tx_to_psbt */ +void migrate_last_tx_to_psbt(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_last_tx_to_psbt called!\n"); abort(); } +/* Generated stub for migrate_normalize_invstr */ +void migrate_normalize_invstr(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_normalize_invstr called!\n"); abort(); } +/* Generated stub for migrate_our_funding */ +void migrate_our_funding(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_our_funding called!\n"); abort(); } +/* Generated stub for migrate_payments_scids_as_integers */ +void migrate_payments_scids_as_integers(struct lightningd *ld UNNEEDED, + struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_payments_scids_as_integers called!\n"); abort(); } +/* Generated stub for migrate_pr2342_feerate_per_channel */ +void migrate_pr2342_feerate_per_channel(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_pr2342_feerate_per_channel called!\n"); abort(); } +/* Generated stub for migrate_remove_chain_moves_duplicates */ +void migrate_remove_chain_moves_duplicates(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_remove_chain_moves_duplicates called!\n"); abort(); } +/* Generated stub for migrate_runes_idfix */ +void migrate_runes_idfix(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED) +{ fprintf(stderr, "migrate_runes_idfix called!\n"); abort(); } +/* AUTOGENERATED MOCKS END */ diff --git a/wallet/Makefile b/wallet/Makefile index 21f32d339..121a2e783 100644 --- a/wallet/Makefile +++ b/wallet/Makefile @@ -41,6 +41,8 @@ WALLET_SQL_FILES := \ wallet/test/run-wallet.c \ wallet/test/run-chain_moves_duplicate-detect.c \ wallet/test/run-migrate_remove_chain_moves_duplicates.c \ + tools/lightning-downgrade.c \ + wallet/statements_gettextgen.po: $(WALLET_SQL_FILES) $(FORCE) @if $(call SHA256STAMP_CHANGED); then \