Skip to content

Commit 949ed44

Browse files
committed
tools/lightning-downgrade: tool to downgrade (offline) v25.12 to v25.09.
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au> Changelog-Added: tools: `lightningd-downgrade` can downgrade your database from v25.12 to v25.09 if something goes wrong.
1 parent 2fb48bc commit 949ed44

File tree

6 files changed

+337
-3
lines changed

6 files changed

+337
-3
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ $(info Building version $(VERSION))
99
# Next release.
1010
CLN_NEXT_VERSION := v25.12
1111

12+
# Previous release (for downgrade testing)
13+
CLN_PREV_VERSION := v25.09
14+
1215
# --quiet / -s means quiet, dammit!
1316
ifeq ($(findstring s,$(word 1, $(MAKEFLAGS))),s)
1417
ECHO := :

tests/test_downgrade.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from fixtures import * # noqa: F401,F403
2+
from fixtures import TEST_NETWORK
3+
from utils import (
4+
TIMEOUT # noqa: F401
5+
)
6+
7+
import os
8+
import subprocess
9+
10+
11+
def test_downgrade(node_factory, executor):
12+
l1, l2 = node_factory.line_graph(2, opts={'may_reconnect': True})
13+
14+
# From the binary:
15+
# ERROR_DBVERSION = 1
16+
# ERROR_DBFAIL = 2
17+
ERROR_USAGE = 3
18+
# ERROR_INTERNAL = 99
19+
20+
cmd_line = ["tools/lightning-downgrade",
21+
f"--network={TEST_NETWORK}",
22+
f"--lightning-dir={l1.daemon.lightning_dir}"]
23+
# No downgrade on live nodes!
24+
retcode = subprocess.call(cmd_line, timeout=TIMEOUT)
25+
assert retcode == ERROR_USAGE
26+
27+
l1.stop()
28+
subprocess.check_call(cmd_line)
29+
30+
# Test with old lightningd if it's available.
31+
old_cln = os.getenv('PREV_LIGHTNINGD')
32+
if old_cln:
33+
current_executable = l1.daemon.executable
34+
l1.daemon.executable = old_cln
35+
36+
l1.start()
37+
38+
# It should connect to l2 no problems, make payment.
39+
l1.connect(l2)
40+
inv = l2.rpc.invoice(1000, 'test_downgrade', 'test_downgrade')
41+
l1.rpc.xpay(inv['bolt11'])
42+
l1.stop()
43+
l1.daemon.executable = current_executable
44+
45+
# Another downgrade is a noop.
46+
assert subprocess.check_output(cmd_line).decode("utf8").startswith("Already compatible with ")
47+
48+
# Should be able to upgrade without any trouble
49+
l1.daemon.opts['database-upgrade'] = True
50+
l1.start()
51+
assert l1.daemon.is_in_log("Updating database from version")
52+
53+
l1.connect(l2)
54+
inv2 = l2.rpc.invoice(1000, 'test_downgrade2', 'test_downgrade2')
55+
l1.rpc.xpay(inv2['bolt11'])

tools/Makefile

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#! /usr/bin/make
2-
TOOLS := tools/lightning-hsmtool
2+
TOOLS := tools/lightning-hsmtool tools/lightning-downgrade
33
TOOLS_SRC := $(TOOLS:=.c)
44
TOOLS_OBJ := $(TOOLS_SRC:.c=.o)
55

@@ -13,8 +13,23 @@ ALL_PROGRAMS += $(TOOLS)
1313
tools/headerversions: $(FORCE) tools/headerversions.o libccan.a
1414
@trap "rm -f $@.tmp.$$$$" EXIT; $(LINK.o) tools/headerversions.o libccan.a $(LOADLIBES) $(LDLIBS) -o $@.tmp.$$$$ && mv -f $@.tmp.$$$$ $@
1515

16+
$(TOOLS): libcommon.a
17+
1618
tools/headerversions.o: ccan/config.h
17-
tools/lightning-hsmtool: tools/lightning-hsmtool.o libcommon.a
19+
tools/lightning-hsmtool: tools/lightning-hsmtool.o
20+
tools/lightning-downgrade.o: CFLAGS:=$(CFLAGS) -DCLN_PREV_VERSION=$(CLN_PREV_VERSION)
21+
22+
tools/lightning-downgrade: \
23+
db/exec.o \
24+
db/bindings.o \
25+
db/utils.o \
26+
tools/lightning-downgrade.o \
27+
wallet/migrations.o \
28+
$(DB_OBJS) \
29+
$(WALLET_DB_QUERIES:.c=.o) \
30+
tools/lightning-downgrade.o
31+
32+
update-mocks: $(tools/lightning-downgrade.c:%=update-mocks/%.c)
1833

1934
clean: tools-clean
2035

tools/lightning-downgrade.c

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/* Tool for limited downgrade of an offline node */
2+
#include "config.h"
3+
#include <bitcoin/chainparams.h>
4+
#include <ccan/array_size/array_size.h>
5+
#include <ccan/err/err.h>
6+
#include <ccan/opt/opt.h>
7+
#include <ccan/tal/path/path.h>
8+
#include <ccan/tal/str/str.h>
9+
#include <common/configdir.h>
10+
#include <common/utils.h>
11+
#include <db/bindings.h>
12+
#include <db/common.h>
13+
#include <db/exec.h>
14+
#include <db/utils.h>
15+
#include <stdio.h>
16+
#include <unistd.h>
17+
#include <wallet/migrations.h>
18+
19+
#define ERROR_DBVERSION 1
20+
#define ERROR_DBFAIL 2
21+
#define ERROR_USAGE 3
22+
#define ERROR_INTERNAL 99
23+
24+
#define PREV_VERSION stringify(CLN_PREV_VERSION)
25+
26+
struct db_version {
27+
const char *name;
28+
size_t db_height;
29+
bool gossip_store_compatible;
30+
};
31+
32+
static const struct db_version db_versions[] = {
33+
{ "v25.09", 276, false },
34+
/* When we implement v25.12 downgrade: { "v25.12", 280, ???}, */
35+
};
36+
37+
static const struct db_version *version_db(const char *version)
38+
{
39+
for (size_t i = 0; i < ARRAY_SIZE(db_versions); i++) {
40+
if (streq(db_versions[i].name, version))
41+
return &db_versions[i];
42+
}
43+
errx(ERROR_INTERNAL, "Unknown version %s", version);
44+
}
45+
46+
static void db_error(void *unused, bool fatal, const char *fmt, va_list ap)
47+
{
48+
vfprintf(stderr, fmt, ap);
49+
fprintf(stderr, "\n");
50+
if (fatal)
51+
exit(ERROR_DBFAIL);
52+
}
53+
54+
/* The standard opt_log_stderr_exit exits with status 1 */
55+
static void opt_log_stderr_exit_usage(const char *fmt, ...)
56+
{
57+
va_list ap;
58+
59+
va_start(ap, fmt);
60+
vfprintf(stderr, fmt, ap);
61+
fprintf(stderr, "\n");
62+
va_end(ap);
63+
exit(ERROR_USAGE);
64+
}
65+
66+
int main(int argc, char *argv[])
67+
{
68+
char *config_filename, *base_dir, *net_dir, *rpc_filename, *wallet_dsn = NULL;
69+
size_t current, prev_version_height, num_migrations;
70+
struct db *db;
71+
const struct db_migration *migrations;
72+
struct db_stmt *stmt;
73+
74+
setup_locale();
75+
err_set_progname(argv[0]);
76+
77+
minimal_config_opts(tmpctx, argc, argv, &config_filename, &base_dir,
78+
&net_dir, &rpc_filename);
79+
opt_register_early_arg("--wallet", opt_set_talstr, NULL,
80+
&wallet_dsn,
81+
"Location of the wallet database.");
82+
opt_register_noarg("--help|-h", opt_usage_and_exit,
83+
"A tool to downgrade an offline Core Lightning Node to " PREV_VERSION,
84+
"Print this message.");
85+
opt_early_parse(argc, argv, opt_log_stderr_exit_usage);
86+
opt_parse(&argc, argv, opt_log_stderr_exit_usage);
87+
88+
if (argc != 1)
89+
opt_usage_exit_fail("No arguments expected");
90+
91+
if (!wallet_dsn)
92+
wallet_dsn = tal_fmt(tmpctx, "sqlite3://%s/lightningd.sqlite3", net_dir);
93+
94+
if (path_is_file(path_join(tmpctx, base_dir,
95+
tal_fmt(tmpctx, "lightningd-%s.pid",
96+
chainparams->network_name)))) {
97+
errx(ERROR_USAGE,
98+
"Lightningd PID file exists, aborting: lightningd must not be running");
99+
}
100+
101+
migrations = get_db_migrations(&num_migrations);
102+
prev_version_height = version_db(PREV_VERSION)->db_height;
103+
104+
/* Open db, check it's the expected version */
105+
db = db_open(tmpctx, wallet_dsn, false, false, db_error, NULL);
106+
107+
db_begin_transaction(db);
108+
db->data_version = db_data_version_get(db);
109+
current = db_get_version(db);
110+
111+
if (current < prev_version_height)
112+
errx(ERROR_DBVERSION, "Database version %zu already less than %zu expected for %s",
113+
current, prev_version_height, PREV_VERSION);
114+
if (current == prev_version_height) {
115+
printf("Already compatible with %s\n", PREV_VERSION);
116+
exit(0);
117+
}
118+
if (current >= num_migrations)
119+
errx(ERROR_DBVERSION, "Unknown database version %zu: I only know up to %zu (%s)",
120+
current, num_migrations, stringify(CLN_NEXT_VERSION));
121+
122+
/* current version is the last migration we did. */
123+
while (current > prev_version_height) {
124+
if (migrations[current].revertsql) {
125+
stmt = db_prepare_v2(db, migrations[current].revertsql);
126+
db_exec_prepared_v2(stmt);
127+
tal_free(stmt);
128+
}
129+
if (migrations[current].revertfn) {
130+
const char *error = migrations[current].revertfn(tmpctx, db);
131+
if (error)
132+
errx(ERROR_DBFAIL, "Downgrade failed: %s", error);
133+
}
134+
current--;
135+
}
136+
137+
/* Finally update the version number in the version table */
138+
stmt = db_prepare_v2(db, SQL("UPDATE version SET version=?;"));
139+
db_bind_int(stmt, current);
140+
db_exec_prepared_v2(stmt);
141+
tal_free(stmt);
142+
143+
printf("Downgrade to %s succeeded. Committing.\n", PREV_VERSION);
144+
db_commit_transaction(db);
145+
tal_free(db);
146+
147+
if (!version_db(PREV_VERSION)->gossip_store_compatible) {
148+
printf("Deleting incompatible gossip_store\n");
149+
unlink(path_join(tmpctx, net_dir, "gossip_store"));
150+
}
151+
}
152+
153+
/*** We don't actually perform migrations, so these are stubs which abort. ***/
154+
/* Remake with `make update-mocks` or `make update-mocks/tools/lightning-downgrade.c` */
155+
156+
/* AUTOGENERATED MOCKS START */
157+
/* Generated stub for fillin_missing_channel_blockheights */
158+
void fillin_missing_channel_blockheights(struct lightningd *ld UNNEEDED,
159+
struct db *db UNNEEDED)
160+
{ fprintf(stderr, "fillin_missing_channel_blockheights called!\n"); abort(); }
161+
/* Generated stub for fillin_missing_channel_id */
162+
void fillin_missing_channel_id(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED)
163+
{ fprintf(stderr, "fillin_missing_channel_id called!\n"); abort(); }
164+
/* Generated stub for fillin_missing_lease_satoshi */
165+
void fillin_missing_lease_satoshi(struct lightningd *ld UNNEEDED,
166+
struct db *db UNNEEDED)
167+
{ fprintf(stderr, "fillin_missing_lease_satoshi called!\n"); abort(); }
168+
/* Generated stub for fillin_missing_local_basepoints */
169+
void fillin_missing_local_basepoints(struct lightningd *ld UNNEEDED,
170+
struct db *db UNNEEDED)
171+
{ fprintf(stderr, "fillin_missing_local_basepoints called!\n"); abort(); }
172+
/* Generated stub for fillin_missing_scriptpubkeys */
173+
void fillin_missing_scriptpubkeys(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED)
174+
{ fprintf(stderr, "fillin_missing_scriptpubkeys called!\n"); abort(); }
175+
/* Generated stub for insert_addrtype_to_addresses */
176+
void insert_addrtype_to_addresses(struct lightningd *ld UNNEEDED,
177+
struct db *db UNNEEDED)
178+
{ fprintf(stderr, "insert_addrtype_to_addresses called!\n"); abort(); }
179+
/* Generated stub for migrate_channels_scids_as_integers */
180+
void migrate_channels_scids_as_integers(struct lightningd *ld UNNEEDED,
181+
struct db *db UNNEEDED)
182+
{ fprintf(stderr, "migrate_channels_scids_as_integers called!\n"); abort(); }
183+
/* Generated stub for migrate_convert_old_channel_keyidx */
184+
void migrate_convert_old_channel_keyidx(struct lightningd *ld UNNEEDED,
185+
struct db *db UNNEEDED)
186+
{ fprintf(stderr, "migrate_convert_old_channel_keyidx called!\n"); abort(); }
187+
/* Generated stub for migrate_datastore_commando_runes */
188+
void migrate_datastore_commando_runes(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED)
189+
{ fprintf(stderr, "migrate_datastore_commando_runes called!\n"); abort(); }
190+
/* Generated stub for migrate_fail_pending_payments_without_htlcs */
191+
void migrate_fail_pending_payments_without_htlcs(struct lightningd *ld UNNEEDED,
192+
struct db *db UNNEEDED)
193+
{ fprintf(stderr, "migrate_fail_pending_payments_without_htlcs called!\n"); abort(); }
194+
/* Generated stub for migrate_fill_in_channel_type */
195+
void migrate_fill_in_channel_type(struct lightningd *ld UNNEEDED,
196+
struct db *db UNNEEDED)
197+
{ fprintf(stderr, "migrate_fill_in_channel_type called!\n"); abort(); }
198+
/* Generated stub for migrate_forwards_add_rowid */
199+
void migrate_forwards_add_rowid(struct lightningd *ld UNNEEDED,
200+
struct db *db UNNEEDED)
201+
{ fprintf(stderr, "migrate_forwards_add_rowid called!\n"); abort(); }
202+
/* Generated stub for migrate_from_account_db */
203+
void migrate_from_account_db(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED)
204+
{ fprintf(stderr, "migrate_from_account_db called!\n"); abort(); }
205+
/* Generated stub for migrate_inflight_last_tx_to_psbt */
206+
void migrate_inflight_last_tx_to_psbt(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED)
207+
{ fprintf(stderr, "migrate_inflight_last_tx_to_psbt called!\n"); abort(); }
208+
/* Generated stub for migrate_initialize_alias_local */
209+
void migrate_initialize_alias_local(struct lightningd *ld UNNEEDED,
210+
struct db *db UNNEEDED)
211+
{ fprintf(stderr, "migrate_initialize_alias_local called!\n"); abort(); }
212+
/* Generated stub for migrate_initialize_channel_htlcs_wait_indexes_and_fixup_forwards */
213+
void migrate_initialize_channel_htlcs_wait_indexes_and_fixup_forwards(struct lightningd *ld UNNEEDED,
214+
struct db *db UNNEEDED)
215+
{ fprintf(stderr, "migrate_initialize_channel_htlcs_wait_indexes_and_fixup_forwards called!\n"); abort(); }
216+
/* Generated stub for migrate_initialize_forwards_wait_indexes */
217+
void migrate_initialize_forwards_wait_indexes(struct lightningd *ld UNNEEDED,
218+
struct db *db UNNEEDED)
219+
{ fprintf(stderr, "migrate_initialize_forwards_wait_indexes called!\n"); abort(); }
220+
/* Generated stub for migrate_initialize_invoice_wait_indexes */
221+
void migrate_initialize_invoice_wait_indexes(struct lightningd *ld UNNEEDED,
222+
struct db *db UNNEEDED)
223+
{ fprintf(stderr, "migrate_initialize_invoice_wait_indexes called!\n"); abort(); }
224+
/* Generated stub for migrate_initialize_payment_wait_indexes */
225+
void migrate_initialize_payment_wait_indexes(struct lightningd *ld UNNEEDED,
226+
struct db *db UNNEEDED)
227+
{ fprintf(stderr, "migrate_initialize_payment_wait_indexes called!\n"); abort(); }
228+
/* Generated stub for migrate_invalid_last_tx_psbts */
229+
void migrate_invalid_last_tx_psbts(struct lightningd *ld UNNEEDED,
230+
struct db *db UNNEEDED)
231+
{ fprintf(stderr, "migrate_invalid_last_tx_psbts called!\n"); abort(); }
232+
/* Generated stub for migrate_invoice_created_index_var */
233+
void migrate_invoice_created_index_var(struct lightningd *ld UNNEEDED,
234+
struct db *db UNNEEDED)
235+
{ fprintf(stderr, "migrate_invoice_created_index_var called!\n"); abort(); }
236+
/* Generated stub for migrate_last_tx_to_psbt */
237+
void migrate_last_tx_to_psbt(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED)
238+
{ fprintf(stderr, "migrate_last_tx_to_psbt called!\n"); abort(); }
239+
/* Generated stub for migrate_normalize_invstr */
240+
void migrate_normalize_invstr(struct lightningd *ld UNNEEDED,
241+
struct db *db UNNEEDED)
242+
{ fprintf(stderr, "migrate_normalize_invstr called!\n"); abort(); }
243+
/* Generated stub for migrate_our_funding */
244+
void migrate_our_funding(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED)
245+
{ fprintf(stderr, "migrate_our_funding called!\n"); abort(); }
246+
/* Generated stub for migrate_payments_scids_as_integers */
247+
void migrate_payments_scids_as_integers(struct lightningd *ld UNNEEDED,
248+
struct db *db UNNEEDED)
249+
{ fprintf(stderr, "migrate_payments_scids_as_integers called!\n"); abort(); }
250+
/* Generated stub for migrate_pr2342_feerate_per_channel */
251+
void migrate_pr2342_feerate_per_channel(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED)
252+
{ fprintf(stderr, "migrate_pr2342_feerate_per_channel called!\n"); abort(); }
253+
/* Generated stub for migrate_remove_chain_moves_duplicates */
254+
void migrate_remove_chain_moves_duplicates(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED)
255+
{ fprintf(stderr, "migrate_remove_chain_moves_duplicates called!\n"); abort(); }
256+
/* Generated stub for migrate_runes_idfix */
257+
void migrate_runes_idfix(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED)
258+
{ fprintf(stderr, "migrate_runes_idfix called!\n"); abort(); }
259+
/* AUTOGENERATED MOCKS END */

wallet/Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ WALLET_SQL_FILES := \
4141
wallet/test/run-wallet.c \
4242
wallet/test/run-chain_moves_duplicate-detect.c \
4343
wallet/test/run-migrate_remove_chain_moves_duplicates.c \
44+
tools/lightning-downgrade.c \
45+
4446

4547
wallet/statements_gettextgen.po: $(WALLET_SQL_FILES) $(FORCE)
4648
@if $(call SHA256STAMP_CHANGED); then \

wallet/migrations.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1070,7 +1070,7 @@ static const struct db_migration dbmigrations[] = {
10701070
" PRIMARY KEY (id)"
10711071
")"), NULL,
10721072
/* Simply drop table. */
1073-
"DROP TABLE network_events"},
1073+
SQL("DROP TABLE network_events")},
10741074
{NULL, migrate_fail_pending_payments_without_htlcs,
10751075
/* Failing pending payments is idempotent, so no revert needed */
10761076
NULL, NULL},

0 commit comments

Comments
 (0)