wallet: make sure we re-watch outpoints after blocks are rolled back.
What changed, and why it matters
This fix corrects a startup bug in Core Lightning where, after rolling back recent blocks to re-read them, the node could forget to keep watching certain spent transaction outputs. The main visible symptom was stale network gossip, but it could also cause the node to miss on-chain transactions if it restarted at the wrong time. The patch makes the node refresh its watch list after block rollbacks.
Apply the patch and run the regression test test_unspend_during_reorg. Operators should upgrade nodes, especially those that may restart during or shortly after chain reorganizations.
Security signals we found
Missing on-chain transaction detection after reorg/rollback
In-memory state desynchronization from database state
Potential stale gossip validation due to incorrect spent-state view
Fix removes xfail marker from regression test test_unspend_during_reorg
Evidence from the diff
At startup, wallet.c loads the in-memory outpoint filter from utxoset rows with spendheight=NULL, then rolls back 15 blocks. Rolling back sets spendheight to NULL for any UTXO spent in the removed blocks, but the in-memory filter was not updated. When rolling forward again, those UTXOs were not watched. The fix extracts the filter population into a reusable refill_outpointfilters() function and calls it both at startup and after wallet_block_remove() / wallet_blocks_rollback(), so the in-memory filter stays synchronized with the database.
Changed components
wallet/wallet.ctests/test_wallet.pyInspect captured patch +21 / −11
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index f4aeae21..12aae68e 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -2538,7 +2538,6 @@ def test_hsm_wrong_passphrase_crash(node_factory):
os.close(slave_fd2)
-@pytest.mark.xfail(strict=True)
def test_unspend_during_reorg(node_factory, bitcoind):
l1, l2 = node_factory.line_graph(2)
scid = first_scid(l1, l2)
diff --git a/wallet/wallet.c b/wallet/wallet.c
index c6a272a2..28085444 100644
--- a/wallet/wallet.c
+++ b/wallet/wallet.c
@@ -180,25 +180,18 @@ static void our_addresses_init(struct wallet *w)
w->our_addresses_maxindex = w->keyscan_gap;
}
-static void outpointfilters_init(struct wallet *w)
+/* Idempotent: outpointfilter_add is a noop if it already exists. */
+static void refill_outpointfilters(struct wallet *w)
{
struct db_stmt *stmt;
- struct utxo **utxos = wallet_get_all_utxos(NULL, w);
- struct bitcoin_outpoint outpoint;
-
- w->owned_outpoints = outpointfilter_new(w);
- for (size_t i = 0; i < tal_count(utxos); i++)
- outpointfilter_add(w->owned_outpoints, &utxos[i]->outpoint);
- tal_free(utxos);
-
- w->utxoset_outpoints = outpointfilter_new(w);
stmt = db_prepare_v2(
w->db,
SQL("SELECT txid, outnum FROM utxoset WHERE spendheight is NULL"));
db_query_prepared(stmt);
while (db_step(stmt)) {
+ struct bitcoin_outpoint outpoint;
db_col_txid(stmt, "txid", &outpoint.txid);
outpoint.n = db_col_int(stmt, "outnum");
outpointfilter_add(w->utxoset_outpoints, &outpoint);
@@ -206,6 +199,20 @@ static void outpointfilters_init(struct wallet *w)
tal_free(stmt);
}
+static void outpointfilters_init(struct wallet *w)
+{
+ struct utxo **utxos = wallet_get_all_utxos(NULL, w);
+
+ w->owned_outpoints = outpointfilter_new(w);
+ for (size_t i = 0; i < tal_count(utxos); i++)
+ outpointfilter_add(w->owned_outpoints, &utxos[i]->outpoint);
+
+ tal_free(utxos);
+
+ w->utxoset_outpoints = outpointfilter_new(w);
+ refill_outpointfilters(w);
+}
+
struct wallet *wallet_new(struct lightningd *ld, struct timers *timers)
{
struct wallet *wallet = tal(ld, struct wallet);
@@ -4867,6 +4874,9 @@ void wallet_block_remove(struct wallet *w, struct block *b)
db_query_prepared(stmt);
assert(!db_step(stmt));
tal_free(stmt);
+
+ /* We might need to watch more now-unspent UTXOs */
+ refill_outpointfilters(w);
}
void wallet_blocks_rollback(struct wallet *w, u32 height)
@@ -4875,6 +4885,7 @@ void wallet_blocks_rollback(struct wallet *w, u32 height)
"WHERE height > ?"));
db_bind_int(stmt, height);
db_exec_prepared_v2(take(stmt));
+ refill_outpointfilters(w);
}
bool wallet_outpoint_spend(const tal_t *ctx, struct wallet *w, const u32 blockheight,
Why this scored 58/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.