snapshot loader: build txindex from blk0001.dat after extraction

A v3 snapshot carries the UTXO set and raw block data (blk0001.dat) but
does NOT rebuild the per-tx index (txindex) that maps CTransaction hashes
to CDiskTxPos. Without it, any new PoS block referencing a pre-snapshot
transaction fails CheckProofOfStake with 'read txPrev failed':

    CTransaction::ReadFromDisk(txdb, prevout, txindex)  // src/main.cpp:714
        if (!txdb.ReadTxIndex(prevout.hash, txindexRet))  // empty!
            return false;

This stalls the node at the snapshot height and triggers DoS=100 on
every inbound peer feeding canonical blocks, masking as a network
misbehavior issue. DNS3 was stuck at 2,214,547 for this reason despite
the UTXO and block data being present.

Fix: after extracting blk0001.dat, walk it linearly and call
txdb.UpdateTxIndex(hash, CTxIndex(CDiskTxPos, nVout)) for every
transaction. O(N) over the historical block range, batched every
5000 txs. Adds ~30-60s to snapshot load on modern hardware.

This is the third leg of the v3 self-contained snapshot story:
  v3 field       source                     purpose
  -----------    -------------------------  -------------------------
  headers        last 2000 block headers    block index continuity
  utxos          22k unspent outputs         UTXO set at tip
  blocks         blk0001.dat raw bytes       on-disk block storage
  setStakeSeen   last 5000 PoS seen stakes   stake collision dedup
  txindex        [this commit]               PoS signature verification

Future 'v4 snapshot' work should consolidate all five into a single
load pass with progress reporting.
This commit is contained in:
Krystie
2026-07-01 13:29:28 -07:00
parent 9b5c47f60f
commit ac0adfea15
+102
View File
@@ -644,6 +644,108 @@ bool LoadSnapshot(const fs::path& snapshotPath,
}
}
// Build the transaction index (txindex) from the freshly-extracted blk0001.dat.
// The snapshot loads the UTXO set and blk0001.dat but does NOT rebuild the
// per-tx index that CTransaction::ReadFromDisk requires for stake-input
// signature verification. Without this, a new PoS block referencing any
// pre-snapshot tx would fail CheckProofOfStake with "read txPrev failed"
// and be rejected with DoS=100, stalling the node at the snapshot height.
//
// Walk every block in blk0001.dat and record CDiskTxPos for each tx, so
// the loaded chain is fully self-contained. The walk is O(N) over the
// historical block range but uses the already-cached blocks on disk and
// batches the writes (every 5000 txs).
if (success) {
printf("UtxoSnapshot: building transaction index from blk0001.dat...\n");
fs::path blkPath = GetDataDir() / "blk0001.dat";
FILE* blkFile = fopen(blkPath.string().c_str(), "rb");
if (!blkFile) {
success = false;
strError = "Cannot open blk0001.dat for txindex build: " + blkPath.string();
} else {
CAutoFile blkdat(blkFile, SER_DISK, CLIENT_VERSION);
if (!txdb.TxnBegin()) {
success = false;
strError = "Failed to begin txindex build transaction";
} else {
unsigned int nPos = 0;
unsigned int nBlocksIndexed = 0;
unsigned int nTxsIndexed = 0;
unsigned int nBatchTxs = 0;
int64_t nLastReport = GetTimeMillis();
while (success && blkdat.good()) {
fseek(blkdat, nPos, SEEK_SET);
// Locate block magic
unsigned char pchData[65536];
int nRead = fread(pchData, 1, sizeof(pchData), blkdat);
if (nRead <= 8) break;
void* nFind = memchr(pchData, pchMessageStart[0], nRead + 1 - sizeof(pchMessageStart));
if (!nFind) {
// Reached the tail of the file
break;
}
if (memcmp(nFind, pchMessageStart, sizeof(pchMessageStart)) != 0) {
nPos += ((unsigned char*)nFind - pchData) + 1;
continue;
}
unsigned int nBlockStart = nPos + ((unsigned char*)nFind - pchData);
fseek(blkdat, nBlockStart + sizeof(pchMessageStart), SEEK_SET);
unsigned int nSize;
blkdat >> nSize;
if (nSize == 0 || nSize > MAX_BLOCK_SIZE) {
nPos = nBlockStart + sizeof(pchMessageStart) + 4;
continue;
}
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
for (const CTransaction& tx : block.vtx) {
CDiskTxPos posThisTx(1, nBlockStart, nTxPos);
txdb.UpdateTxIndex(tx.GetHash(), CTxIndex(posThisTx, tx.vout.size()));
nTxPos += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
nTxsIndexed++;
nBatchTxs++;
}
nBlocksIndexed++;
// Advance past this block to scan the next one
nPos = nBlockStart + sizeof(pchMessageStart) + sizeof(unsigned int) + nSize;
// Commit batch periodically to avoid unbounded memory
if (nBatchTxs >= 5000) {
if (!txdb.TxnCommit()) {
success = false;
strError = "txindex batch commit failed";
break;
}
if (!txdb.TxnBegin()) {
success = false;
strError = "txindex batch restart failed";
break;
}
nBatchTxs = 0;
if (GetTimeMillis() - nLastReport > 5000) {
printf("UtxoSnapshot: indexed %u blocks / %u txs (pos=%u)\n",
nBlocksIndexed, nTxsIndexed, nPos);
nLastReport = GetTimeMillis();
}
}
}
if (success && !txdb.TxnCommit()) {
success = false;
strError = "Final txindex commit failed";
}
if (success) {
printf("UtxoSnapshot: built txindex for %u blocks / %u transactions\n",
nBlocksIndexed, nTxsIndexed);
}
}
}
}
// Verify content hash
if (success) {
uint256 actualHash;