[grade=B] fix(utxosnapshot): include block header in txindex disk positions

The snapshot loader's txindex walk started the first transaction at
nBlockStart + 8 (magic + size), omitting the 80-byte block header that
ConnectBlock's CDiskTxPos convention includes (nBlockPos + 88 for a
1-tx block). Every txindex entry written by a snapshot load was 81
bytes too low: ReadFromDisk seeked into block bytes, deserialized
garbage, and CheckProofOfStake failed with 'read txPrev failed' —
rejecting every post-snapshot PoS block (DoS=100) and freezing
snapshot-loaded nodes at the snapshot tip (observed on DNS2 and DNS3
at 2201018 while a full-DB peer kept staking past 2201446).

Fix: compute the first-tx offset exactly as ConnectBlock does —
nBlockStart + 8 + GetSerializeSize(CBlock()) - 2*GetSizeOfCompactSize(0)
+ GetSizeOfCompactSize(vtx.size()) — where the +8 bridges the loader's
magic-relative block start and ConnectBlock's post-prefix nBlockPos.

Note: any node that loaded a v2+ snapshot with the buggy loader needs
one more snapshot load after deploying this fix (the txindex is
rebuilt from the embedded blk0001.dat on every load).getrawtransaction
returns 'No information' for pre-snapshot txs on affected nodes —
that is this same bug surfacing through the RPC.

Test suites green (29 cases / 192 assertions, incl. checkpoint,
consensus, snapshotnet).
This commit is contained in:
Krystie
2026-09-06 16:11:16 -07:00
parent b70725da36
commit 649b5ba3f4
+18 -5
View File
@@ -699,11 +699,24 @@ bool LoadSnapshot(const fs::path& snapshotPath,
CBlock block;
blkdat >> block;
// For each tx in the block, record the disk position.
// nTxPos is the offset of the tx *within* the block (after
// magic+size for the first tx, then serialize-size of
// preceding txs). We use the post-serialize offset of each
// tx as nTxPos, matching the convention in ConnectBlock.
unsigned int nTxPos = sizeof(pchMessageStart) + sizeof(unsigned int); // offset of first tx in block
// ConnectBlock (main.cpp) computes the first tx as
// nBlockPos + GetSerializeSize(CBlock())
// - 2*GetSizeOfCompactSize(0)
// + GetSizeOfCompactSize(vtx.size())
// where nBlockPos points just AFTER the magic+size prefix
// (i.e. at the 80-byte header). Here nBlockStart points
// AT the magic, so add the 8-byte prefix first:
// first tx = nBlockStart + 8 + 80 + compactsize(vtx)
// The old code forgot the 80-byte header (started txs at
// +8), shifting every txindex entry 81 bytes low and
// making ReadFromDisk desync — "read txPrev failed" —
// which rejected all post-snapshot PoS blocks and froze
// snapshot-loaded nodes at the snapshot tip.
unsigned int nTxPos = nBlockStart
+ sizeof(pchMessageStart) + sizeof(unsigned int)
+ ::GetSerializeSize(CBlock(), SER_DISK, CLIENT_VERSION)
- (2 * GetSizeOfCompactSize(0))
+ GetSizeOfCompactSize(block.vtx.size());
for (const CTransaction& tx : block.vtx) {
CDiskTxPos posThisTx(1, nBlockStart, nTxPos);
txdb.UpdateTxIndex(tx.GetHash(), CTxIndex(posThisTx, tx.vout.size()));