Skip to content

Write reorg data for blocks near the tip during initial block download - #14

Merged
DeviaVir merged 2 commits into
masterfrom
fix/undo-records-near-tip-during-ibd
Sep 17, 2026
Merged

DeviaVir merged 2 commits into
masterfrom
fix/undo-records-near-tip-during-ibd

Conversation

@DeviaVir

@DeviaVir DeviaVir commented Sep 16, 2026

Copy link
Copy Markdown

Problem

Blocks indexed while ibd == true get no REORG_CF undo record (src/store/db.rs, the ibd gate in Store::update). ibd starts true on every process start and only flips to false on the first ChainStatus::Tip, so every block indexed while catching up after a restart is permanently rollback-incapable. When the chain later reorgs across one of them, store.reorg() fails with No reorg data found for height N, the blocks task panics (the HTTP listener survives, so the process keeps answering 503), and restarting reproduces the same panic because the reorged block is still the last entry in HASHES_CF.

--reorg-data-keep-heights does not help: it only controls how long a written record is kept, and here the record was never written. Observed on testnet4 with --reorg-data-keep-heights 144 after a 7-block reorg; the only recovery was a reindex or a restore from an older snapshot.

Fix

Gate the undo write on block age instead of process state:

  • threads::blocks reads the node tip (chain_info) at startup and every 60 s while catching up, and calls ibd_finished() as soon as the block about to be indexed satisfies node_tip - height < reorg_data_keep_heights. From then on every block gets reorg data, exactly as after reaching the tip. Genuine initial sync of old blocks still skips the write. A stale (lower) tip only enables writes earlier, never later.
  • The blocks task also calls ibd_finished() itself when the tip is reached, before the next block is indexed, instead of the mempool task that receives the initial-sync signal. This removes a small race where a block could be indexed between the signal and the flag flip.
  • DBStore::ibd_finished is idempotent (swap) and logs only on the transition.
  • reorg_data_keep_heights is threaded to the blocks task; the default 6 is now a named constant.

Esplora mode (chain_info returns None) keeps the previous behaviour: reorg data starts at the first tip.

Tests

  • test_bitcoin_reorg_after_restart (integration, regtest): funds a UTXO, broadcasts tx A, stops the server, mines block A and one more block, restarts the server on the same database, then invalidates block A and mines a longer chain double-spending the UTXO. Asserts the server follows the new tip and the histories are corrected.
    • On master this fails exactly like production: reorg failed: No reorg data found for height 105 → tip never advances → no tip hash after 10s.
    • With this change it passes.
  • test_reorg_data_written_only_after_ibd_finished (unit): no reorg data and _reorg fails while in IBD; reorg data written and reorg succeeds after ibd_finished; ibd_finished is idempotent.
  • reorg_window (unit): boundary cases of the window check, including saturating behaviour past a stale tip.
  • Test-env helpers added: TestEnv::shutdown_server() (stops the server, waits for it, returns the node) and generate_blocks(&node, n) for mining while no server is running.

Ran locally: cargo test --features test_env (unit + regtest integration), the reorg_crash_test exact test, the cargo check matrix from CI (--benches, --tests --features test_env, --no-default-features with and without test_env), cargo fmt, cargo clippy (only warning is pre-existing in src/fetch.rs).

Not in this PR

  • _reorg() still leaves the rolled-back height in HASHES_CF (it only mutates UTXO/HISTORY and deletes the REORG_CF entry); a crash between a successful rollback and the re-index of that height would replay the reorg against an already-consumed undo record. Deleting the row in the same batch is a two-line follow-up.
  • A failed rollback still panics inside the spawned blocks task while the HTTP server keeps serving 503. Surfacing that as a process exit (or an automatic reindex) is a separate change.
  • Esplora mode: chain_info returns None there, so catch-up blocks after a restart still get no reorg data and behaviour is unchanged from before this PR. Documented on node_tip_height and on the CLI flag. Fixing it needs a tip-height lookup for esplora (GET /blocks/tip/height) if that backend stays.

@DeviaVir DeviaVir self-assigned this Sep 16, 2026
@RCasatta

Copy link
Copy Markdown

Concept ACK

There is an issue with esplora backend where chain_info is not implemented thus the reorg data will never be written, since esplora is not used too much (I even think about removing it to simplify) it's not a priority but since now it exist we can at least add a comment

`chain_info` is not implemented for the esplora backend, so the early exit
from IBD never triggers there and catch-up blocks after a restart remain
rollback-incapable. Note this on the helper and on the CLI flag.
@DeviaVir

Copy link
Copy Markdown
Author

Thanks. Added the esplora-mode note on node_tip_height and on the --reorg-data-keep-heights flag doc, plus a line under "Not in this PR". With --use-esplora behaviour is unchanged: reorg data starts at the first tip. If the esplora backend stays, a GET /blocks/tip/height lookup would close that gap too.

@DeviaVir
DeviaVir marked this pull request as ready for review September 16, 2026 16:50
@DeviaVir
DeviaVir merged commit 976497d into master Sep 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants