wallet: add optional sql and functions to downgrade db.
What changed, and why it matters
This commit adds infrastructure to Core Lightning's database code so that future versions can, in some cases, roll back (downgrade) a database to the previous software version. It is a feature addition, not a fix for an active security bug. The new code includes safety checks to refuse downgrade when data would be lost, such as when a newly added 'withheld' column has actually been used. There is no direct evidence in the commit that this resolves a known vulnerability or that an attacker can exploit it.
Treat as a normal feature/maintenance commit. No urgent security action is required. Review the downgrade tool that consumes this infrastructure when it appears, to ensure revert functions correctly preserve data integrity and do not introduce downgrade-time vulnerabilities.
Security signals we found
New downgrade/revert capability added to database migration framework
Revert function refuses downgrade when new schema data has been used (withheld column)
No input validation, parsing, or network-facing logic changed
No memory-management or cryptographic code modified
Evidence from the diff
The patch extends the struct migration record in wallet/db.c with two new fields: revertsql and revertfn. These allow each migration step to declare how to undo itself, and optionally provide a function that returns a reason if undoing is unsafe. The commit populates these for the most recent migrations (v25.09 and v25.12 boundaries), including a revert_withheld_column function that checks whether any channel has withheld != 0 before dropping the column. The change is defensive and preparatory; it does not alter runtime node behavior except to enable a future downgrade tool.
Changed components
wallet/db.cDatabase migration subsystemFuture downgrade toolingInspect captured patch +63 / −5
diff --git a/wallet/db.c b/wallet/db.c
index a038722..7aa4a2c 100644
--- a/wallet/db.c
+++ b/wallet/db.c
@@ -20,6 +20,9 @@
struct migration {
const char *sql;
void (*func)(struct lightningd *ld, struct db *db);
+ const char *revertsql;
+ /* If non-NULL, returns string explaining why downgrade is impossible */
+ const char *(*revertfn)(const tal_t *ctx, struct db *db);
};
static void migrate_pr2342_feerate_per_channel(struct lightningd *ld, struct db *db);
@@ -87,6 +90,9 @@ static void migrate_initialize_channel_htlcs_wait_indexes_and_fixup_forwards(str
static void migrate_fail_pending_payments_without_htlcs(struct lightningd *ld,
struct db *db);
+static const char *revert_too_early(const tal_t *ctx, struct db *db);
+static const char *revert_withheld_column(const tal_t *ctx, struct db *db);
+
/* Do not reorder or remove elements from this array, it is used to
* migrate existing databases from a previous state, based on the
* string indices */
@@ -1087,9 +1093,13 @@ static struct migration dbmigrations[] = {
")"), NULL},
/* We do a lookup before each append, to avoid duplicates */
{SQL("CREATE INDEX chain_moves_utxo_idx ON chain_moves (utxo)"), NULL},
- {NULL, migrate_from_account_db},
+ {NULL, migrate_from_account_db, NULL, revert_too_early},
+ /* ^v25.09 */
+
/* We accidentally allowed duplicate entries */
- {NULL, migrate_remove_chain_moves_duplicates},
+ {NULL, migrate_remove_chain_moves_duplicates,
+ /* Removing duplicates is idempotent, so no revert needed */
+ NULL, NULL},
{SQL("CREATE TABLE network_events ("
" id BIGSERIAL,"
" peer_id BLOB NOT NULL,"
@@ -1099,9 +1109,16 @@ static struct migration dbmigrations[] = {
" duration_nsec BIGINT,"
" connect_attempted INTEGER NOT NULL,"
" PRIMARY KEY (id)"
- ")"), NULL},
- {NULL, migrate_fail_pending_payments_without_htlcs},
- {SQL("ALTER TABLE channels ADD withheld INTEGER DEFAULT 0;"), NULL},
+ ")"), NULL,
+ /* Simply drop table. */
+ "DROP TABLE network_events"},
+ {NULL, migrate_fail_pending_payments_without_htlcs,
+ /* Failing pending payments is idempotent, so no revert needed */
+ NULL, NULL},
+ {SQL("ALTER TABLE channels ADD withheld INTEGER DEFAULT 0;"), NULL,
+ /* Need to make sure that withheld isn't used. */
+ NULL, revert_withheld_column},
+ /* ^v25.12 */
};
/**
@@ -2153,3 +2170,44 @@ static void migrate_fail_pending_payments_without_htlcs(struct lightningd *ld,
db_bind_int(stmt, payment_status_in_db(PAYMENT_PENDING));
db_exec_prepared_v2(take(stmt));
}
+
+static const char *revert_too_early(const tal_t *ctx, struct db *db)
+{
+ return tal_strdup(ctx, "Downgrade to before v25.09 not supported");
+}
+
+/* Don't allow downgrade if they've *used* the withheld column. */
+static const char *revert_withheld_column(const tal_t *ctx, struct db *db)
+{
+ struct db_stmt *stmt;
+ struct channel_id *cid;
+ struct short_channel_id *scid;
+
+ stmt = db_prepare_v2(db, SQL("SELECT full_channel_id, scid FROM channels WHERE withheld != 0"));
+ db_query_prepared(stmt);
+ if (db_step(stmt)) {
+ cid = tal(tmpctx, struct channel_id);
+ db_col_channel_id(stmt, "full_channel_id", cid);
+ if (db_col_is_null(stmt, "scid"))
+ scid = NULL;
+ else {
+ scid = tal(tmpctx, struct short_channel_id);
+ *scid = db_col_short_channel_id(stmt, "scid");
+ }
+ } else {
+ cid = NULL;
+ scid = NULL;
+ }
+ tal_free(stmt);
+
+ if (cid) {
+ return tal_fmt(tmpctx, "Channel %s (%s) used v25.12's withheld flag",
+ fmt_channel_id(tmpctx, cid),
+ scid ? fmt_short_channel_id(tmpctx, *scid) : "no short channel id");
+ }
+
+ /* For sqlite3 needs "2021-03-12 (3.35.0)" or above */
+ stmt = db_prepare_v2(db, SQL("ALTER TABLE channels DROP COLUMN withheld"));
+ db_exec_prepared_v2(take(stmt));
+ return NULL;
+}
Why this scored 18/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.