Fixes #29422
Motivation
A pruned node can end up with an empty chainstate (the UTXO database) while its block index already records that the genesis block was pruned. In that state the node cannot start: an empty chainstate is rebuilt by connecting blocks from genesis, but the genesis block data is gone, so no tip can ever be connected. On current master the node waits for a chain tip indefinitely and never prints an actionable error. (The assertion failure in getBlockHash() reported in #29422 was against an older version; on master it hangs instead.)
How the node reaches this state: during a flush, FlushStateToDisk() writes the block index before it writes the coins database. If the process is interrupted between those two writes during the chainstate's first flush, and that flush also prunes block files, the on-disk block index marks the early blocks (including genesis) as no longer stored, while the coins database was never written. An OOM kill during IBD on a low-memory machine is one way to hit this; deleting the chainstate directory of a pruned node produces the same on-disk state.
Change
During chainstate initialization, when a chainstate's coins view is empty, check whether the genesis block is known to the block index but has had its block data pruned. If so, return ChainstateLoadStatus::FAILURE instead of continuing into the wait-for-tip path. The existing failure handling then tells the user to restart with -reindex, and the GUI offers its usual reindex prompt.
Normal startup, a first startup on an empty datadir, and an explicit -reindex are unaffected, because in those cases the genesis block is not marked as pruned.
The change does not start a reindex automatically, because recovering a pruned node this way means downloading the whole chain again, which should be the user's choice.
Testing
feature_reindex_init.py is extended to reproduce the state directly:
- Start a node with -fastprune -prune=1.
- Generate 400 blocks and prune, so genesis block data is removed.
- Stop the node and delete its chainstate directory.
- Assert startup fails with the new error instead of hanging.
- Allow the suggested reindex and assert the node comes back up at height 0.
Without the fix, step 4 never returns and the test hits the 60s startup timeout.
Verification
Built and tested on macOS arm64 (Apple Clang 17):
cmake -B build -DENABLE_IPC=OFF
cmake --build build -j10
ctest --test-dir build -j4
python3 build/test/functional/test_runner.py -j10
All runnable unit tests passed (script_assets_tests was skipped, its external test data was not available). The default functional suite passed, as did the extended pruning/index tests (feature_pruning.py, feature_index_prune.py) and the assumeutxo tests. No new compiler warnings.
Not tested: Linux/Windows CI, the Qt GUI path, the IPC build, and a real OOM interruption (the test constructs the resulting on-disk state rather than triggering an actual OOM).