wallet: handle reorgs for our_outputs/our_txs by hand
What changed, and why it matters
This commit fixes how Core Lightning's wallet records handle blockchain reorganizations (reorgs) for two newer database tables, our_outputs and our_txs. Previously, these tables were not properly updated when blocks were removed or rolled back, meaning the wallet could incorrectly believe funds were confirmed or spent when they no longer were. The fix explicitly resets those records to an unconfirmed state during reorgs and startup rescan windows, matching the behavior of older tables. This prevents internal accounting errors and potential loss or misreporting of funds after a reorg.
Review and merge this fix, then verify that reorg and rescan scenarios correctly demote our_outputs/our_txs rows without deleting them. Consider adding regression tests for reorg handling of these tables.
Security signals we found
Incorrect state persistence across blockchain reorgs
Missing reorg handling for wallet tables without foreign-key cascade
Potential stale spend/confirmation metadata in our_outputs/our_txs
Fix runs on every startup rescan, not just real reorgs
Evidence from the diff
The patch adds wallet_our_tables_reorg(), which updates our_outputs and our_txs to demote blockheight/txindex to 0 and clear spendheight when blocks at or above a given height are disconnected. This is called from wallet_block_remove() and wallet_blocks_rollback(). Unlike legacy tables that rely on blocks(height) ON DELETE SET NULL foreign keys, our_outputs/our_txs have no blocks FK, so reorg handling must be done manually. The demotion preserves non-chain state such as reserved_til and onchaind metadata, and spend watches remain active so rediscovery can re-promote rows later.
Changed components
wallet/wallet.cour_outputs database tableour_txs database tablewallet_block_remove()wallet_blocks_rollback()Inspect captured patch +40 / −0
diff --git a/wallet/wallet.c b/wallet/wallet.c
index a8ac0e90..b17de31e 100644
--- a/wallet/wallet.c
+++ b/wallet/wallet.c
@@ -4887,6 +4887,41 @@ void wallet_block_add(struct wallet *w, struct block *b)
db_exec_prepared_v2(take(stmt));
}
+/* Demote our_outputs/our_txs rows discovered in now-disconnected blocks
+ * back to unconfirmed (the 0 sentinel), and undo spends recorded there.
+ *
+ * Demote, never delete: a row also carries state the chain cannot
+ * re-deliver (reserved_til, onchaind close metadata), and this path runs
+ * not just on real reorgs but on every startup, when chaintopology
+ * invalidates its rescan window. Rediscovery re-promotes the row via
+ * wallet_add_our_output / wallet_transaction_add; a tx that never
+ * re-confirms just stays unconfirmed and unspendable. This matches the
+ * legacy tables, whose blocks(height) foreign keys demote rows to NULL
+ * when the block row is deleted. Spend watches stay armed: the outpoint
+ * is still ours, and it can confirm (and be spent) again. */
+static void wallet_our_tables_reorg(struct wallet *w, u32 height)
+{
+ struct db_stmt *stmt;
+
+ stmt = db_prepare_v2(w->db, SQL("UPDATE our_outputs "
+ "SET blockheight = 0, txindex = 0 "
+ "WHERE blockheight >= ?"));
+ db_bind_int(stmt, height);
+ db_exec_prepared_v2(take(stmt));
+
+ stmt = db_prepare_v2(w->db, SQL("UPDATE our_outputs "
+ "SET spendheight = NULL "
+ "WHERE spendheight >= ?"));
+ db_bind_int(stmt, height);
+ db_exec_prepared_v2(take(stmt));
+
+ stmt = db_prepare_v2(w->db, SQL("UPDATE our_txs "
+ "SET blockheight = 0, txindex = 0 "
+ "WHERE blockheight >= ?"));
+ db_bind_int(stmt, height);
+ db_exec_prepared_v2(take(stmt));
+}
+
void wallet_block_remove(struct wallet *w, struct block *b)
{
struct db_stmt *stmt =
@@ -4902,6 +4937,8 @@ void wallet_block_remove(struct wallet *w, struct block *b)
assert(!db_step(stmt));
tal_free(stmt);
+ wallet_our_tables_reorg(w, b->height);
+
/* We might need to watch more now-unspent UTXOs */
refill_outpointfilters(w);
}
@@ -4912,6 +4949,9 @@ void wallet_blocks_rollback(struct wallet *w, u32 height)
"WHERE height > ?"));
db_bind_int(stmt, height);
db_exec_prepared_v2(take(stmt));
+
+ wallet_our_tables_reorg(w, height + 1);
+
refill_outpointfilters(w);
}
Why this scored 54/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.