A node that has the assumeutxo snapshot base block on disk before loadtxoutset() runs ends up
in a state it cannot recover from. Depending on whether it is restarted, it either aborts or
refuses to start.
Aborting, when background validation reaches the base block:
node/blockstorage.cpp:985 bool node::BlockManager::WriteBlockUndo(const CBlockUndo &, BlockValidationState &, CBlockIndex &): Assertion `m_blockfile_cursors[type]' failed.
Refusing to start, if the node is restarted after the snapshot is loaded:
[ReadBlockUndo] OpenUndoFile failed for FlatFilePos(nFile=-1, nPos=0) while reading block undo
[DisconnectBlock] failure reading undo data
[VerifyDB] Verification error: irrecoverable inconsistency in block data at 299
Corrupted block database detected.
Please restart with -reindex or -reindex-chainstate to recover.
The block database is not corrupt, so the reindex that message asks for is wasted work.
Reaching this needs the base block stored before the snapshot is loaded, which submitblock
does: it is accepted and written while BlockManager::m_snapshot_height is still unset, so it
goes to the normal blockfile cursor. loadtxoutset() then sets m_snapshot_height to that same
height, and BlockfileTypeForHeight() starts reporting ASSUMED for it.
From there the two failures follow:
WriteBlockUndo()looks up the cursor for the block's height and does*Assert(m_blockfile_cursors[type]). The block is nowASSUMED, but nothing above the snapshot height has been written, so that cursor does not exist and the assertion fires when the background chainstate connects the base.FindNextBlockPos()creates the cursor lazily in the same situation;WriteBlockUndo()asserts instead.VerifyDB()skips blocks withoutBLOCK_HAVE_DATAon a snapshot chainstate (validation.cpp:4674), which is why the base is normally left alone. Here the base does have data, and no undo data because the snapshot chainstate never connected it, so the skip does not apply andDisconnectBlock()is attempted on it.
The base block is connected by the background chainstate, not the snapshot one, so it belongs to
the normal blockfile range. BlockfileTypeForHeight() now classifies only blocks above the
snapshot height as ASSUMED. A snapshot chainstate sitting at the base height must not flush the
normal cursor, so FlushChainstateBlockFile() takes the chainstate into account.
VerifyDB() stops before disconnecting the snapshot base, where the snapshot database does not
have the ancestor UTXO data the disconnect needs.
When a snapshot chainstate is loaded from disk, the base's hardcoded m_chain_tx_count does not
mean the background chainstate has processed its parents, so the base is kept in
m_blocks_unlinked until they arrive, and a historical chainstate does not add its target to the
block index candidates before then.
The functional test submits the base block, loads the snapshot, restarts, then feeds the missing historical blocks and checks that background validation completes. Both failures above are what it hits without this change.
Tested:
build/test/functional/test_runner.py feature_assumeutxo.py wallet_assumeutxo.py feature_pruning.py feature_reindex.py --timeout-factor=4
build/bin/test_bitcoin