fix: remove unsafe vSpent fallback, add -rebuildutxo startup flag

- Remove txindex.vSpent fallback from ReadUtxo, HaveUtxo, FetchInputs
  (ConnectBlock doesn't maintain vSpent, so spent outputs could appear unspent)
- Add -rebuildutxo startup flag to reconstruct complete UTXO set by walking
  all blocks from genesis to tip
- Fixes sync stall at block 2,219,922 where UTXO set is incomplete
This commit is contained in:
Krystie
2026-08-03 13:01:47 -07:00
parent d7263e09cf
commit 9aadf855bf
3 changed files with 106 additions and 118 deletions
+96
View File
@@ -707,6 +707,7 @@ std::string HelpMessage()
" -checkblocks=<n> " + _("How many blocks to check at startup (default: 2500, 0 = all)") + "\n" + " -checkblocks=<n> " + _("How many blocks to check at startup (default: 2500, 0 = all)") + "\n" +
" -checklevel=<n> " + _("How thorough the block verification is (0-6, default: 1)") + "\n" + " -checklevel=<n> " + _("How thorough the block verification is (0-6, default: 1)") + "\n" +
" -loadblock=<file> " + _("Imports blocks from external blk000?.dat file") + "\n" + " -loadblock=<file> " + _("Imports blocks from external blk000?.dat file") + "\n" +
" -rebuildutxo " + _("Rebuild UTXO set from full block chain (slow, for recovery)") + "\n" +
"\n" + _("Block creation options:") + "\n" + "\n" + _("Block creation options:") + "\n" +
" -blockminsize=<n> " + _("Set minimum block size in bytes (default: 0)") + "\n" + " -blockminsize=<n> " + _("Set minimum block size in bytes (default: 0)") + "\n" +
@@ -1477,6 +1478,101 @@ bool AppInit2()
} }
} }
// Handle -rebuildutxo: rebuild UTXO set from full block chain
if (GetBoolArg("-rebuildutxo", false))
{
printf("UTXO rebuild requested: rebuilding UTXO set from full block chain...\n");
uiInterface.InitMessage(_("Rebuilding UTXO set from block chain..."));
auto txdb = MakeChainDB("r+");
if (!txdb) {
return InitError(_("Failed to open chain database for UTXO rebuild"));
}
// Clear existing UTXO set
printf("Clearing existing UTXO set...\n");
// Note: We'd need to iterate and erase all UTXOs here
// For now, we'll just rebuild on top of existing (will overwrite)
// Walk all blocks from genesis to tip
int nHeight = 0;
CBlockIndex* pindex = pindexGenesisBlock;
int64_t nStartTime = GetTimeMillis();
while (pindex && !fRequestShutdown)
{
CBlock block;
if (!block.ReadFromDisk(pindex))
{
printf("ERROR: Failed to read block %d (%s)\n", pindex->nHeight, pindex->GetBlockHash().ToString().substr(0,20).c_str());
return InitError(_("Failed to read block during UTXO rebuild"));
}
// Process all transactions in this block
for (const CTransaction& tx : block.vtx)
{
uint256 hashTx = tx.GetHash();
// Add all outputs to UTXO set
for (unsigned int n = 0; n < tx.vout.size(); n++)
{
const CTxOut& txout = tx.vout[n];
if (txout.IsEmpty())
continue;
CUtxoEntry entry;
entry.nValue = txout.nValue;
entry.nHeight = pindex->nHeight;
entry.scriptPubKey = txout.scriptPubKey;
entry.fCoinBase = tx.IsCoinBase();
entry.fCoinStake = tx.IsCoinStake();
entry.nTxTime = tx.nTime;
if (!txdb->WriteUtxo(hashTx, n, entry))
{
printf("ERROR: Failed to write UTXO %s:%d\n", hashTx.ToString().substr(0,20).c_str(), n);
return InitError(_("Failed to write UTXO during rebuild"));
}
}
// Remove spent inputs from UTXO set (skip coinbase)
if (!tx.IsCoinBase())
{
for (const CTxIn& txin : tx.vin)
{
if (!txdb->EraseUtxo(txin.prevout.hash, txin.prevout.n))
{
printf("WARNING: Failed to erase spent UTXO %s:%d (may already be spent)\n",
txin.prevout.hash.ToString().substr(0,20).c_str(), txin.prevout.n);
}
}
}
}
nHeight++;
if (nHeight % 10000 == 0)
{
int64_t nElapsed = GetTimeMillis() - nStartTime;
printf("UTXO rebuild: processed %d blocks (%.1f blocks/sec)\n",
nHeight, nHeight * 1000.0 / nElapsed);
}
pindex = pindex->pnext;
}
if (fRequestShutdown)
{
printf("UTXO rebuild interrupted by shutdown request\n");
return false;
}
int64_t nTotalTime = GetTimeMillis() - nStartTime;
printf("UTXO rebuild complete: processed %d blocks in %.1f seconds (%.1f blocks/sec)\n",
nHeight, nTotalTime / 1000.0, nHeight * 1000.0 / nTotalTime);
uiInterface.InitMessage(_("UTXO rebuild complete"));
}
// AutoRebuild: if -autorerebuild is set and we are behind peers, wipe chain DB // AutoRebuild: if -autorerebuild is set and we are behind peers, wipe chain DB
// and shutdown for clean restart. // and shutdown for clean restart.
MaybeAutoRebuild(GetArg("-autorerebuild", 0)); MaybeAutoRebuild(GetArg("-autorerebuild", 0));
+10 -49
View File
@@ -1755,7 +1755,15 @@ bool IsInitialBlockDownload()
// stalled chain). Without this, a node that restarts on a frozen // stalled chain). Without this, a node that restarts on a frozen
// chain thinks it's fully synced (tip < 24h old from restart) and // chain thinks it's fully synced (tip < 24h old from restart) and
// never requests blocks from peers — permanently stuck. // never requests blocks from peers — permanently stuck.
if (nBestHeight > 0 && nBestHeight < GetNumBlocksOfPeers() - 5) //
// Use the raw peer median (not GetNumBlocksOfPeers(), which clamps to
// the hardcoded checkpoint height). On a stalled chain where we're past
// the last checkpoint, GetNumBlocksOfPeers() returns the checkpoint
// height (2,214,400), not the actual peer height (2,224,763). Without
// using the raw median, a node at 2,219,922 with peers at 2,224,763
// would not detect it's behind.
int nPeerMedian = cPeerBlockCounts.median();
if (nPeerMedian > 0 && nBestHeight < nPeerMedian - 5)
return true; return true;
return false; return false;
} }
@@ -1859,54 +1867,7 @@ bool CTransaction::FetchInputs(CTxDBBase& txdb, const MapPrevTx& mapPendingUtxos
continue; continue;
} }
// Lazy fallback: try old CTxIndex path (for databases upgrading from pre-UTXO format) // Not in UTXO DB — check mempool
{
CTxIndex txindex;
if (txdb.ReadTxIndex(prevout.hash, txindex))
{
CTransaction txPrev;
if (txPrev.ReadFromDisk(txindex.pos))
{
if (prevout.n < txPrev.vout.size())
{
CUtxoEntry backfill;
backfill.nValue = txPrev.vout[prevout.n].nValue;
backfill.scriptPubKey = txPrev.vout[prevout.n].scriptPubKey;
backfill.fCoinBase = txPrev.IsCoinBase();
backfill.fCoinStake = txPrev.IsCoinStake();
backfill.nTxTime = txPrev.nTime;
backfill.nHeight = 0; // conservative default
// Try to recover exact block height from block index
CBlock blockHeader;
if (blockHeader.ReadFromDisk(txindex.pos.nFile, txindex.pos.nBlockPos, false))
{
if (auto bmi = mapBlockIndex.find(blockHeader.GetHash()); bmi != mapBlockIndex.end())
backfill.nHeight = bmi->second->nHeight;
}
// Check if this output was already spent (vSpent in old format)
if (prevout.n < txindex.vSpent.size() && !txindex.vSpent[prevout.n].IsNull())
{
// Already spent — don't return it as available
}
else
{
// Backfill to UTXO DB for future lookups. Skip the
// write when the handle is read-only (wallet/mempool
// callers open "r"); ConnectBlock will persist it
// later via the writable chain handle.
if (!txdb.IsReadOnly())
txdb.WriteUtxo(prevout.hash, prevout.n, backfill);
inputsRet[prevout] = backfill;
continue;
}
}
}
}
}
// Not in UTXO DB or old index — check mempool
{ {
LOCK(mempool.cs); LOCK(mempool.cs);
if (mempool.exists(prevout.hash)) if (mempool.exists(prevout.hash))
-69
View File
@@ -379,66 +379,6 @@ bool CTxDBBase::ReadUtxo(const uint256& hash, unsigned int n, CUtxoEntry& entry)
} }
bool fFound = Read(make_pair(string("u"), make_pair(hash, n)), entry); bool fFound = Read(make_pair(string("u"), make_pair(hash, n)), entry);
if (fFound)
{
{
LOCK(g_cs_utxoCache);
if (g_mapUtxoCache.size() < UTXO_CACHE_MAX_ENTRIES)
PutUtxoCacheEntry(outpoint, entry, fFound);
}
return true;
}
// Lazy fallback: same as HaveUtxo — check old CTxIndex vSpent. If the
// snapshot/UTXO DB doesn't have this output but the txindex says it was
// never spent, reconstruct the CUtxoEntry by reading the transaction
// from disk. This is the primary sync-recovery path for snapshot-loaded
// nodes whose UTXO set is incomplete (the chain has been frozen at
// 2,224,763 since 2026-07-18 because some pre-snapshot UTXOs were
// missing from the snapshot and FetchInputs' ReadUtxo could not find
// them, so blocks spending them were rejected).
//
// Validation safety: every block that ever validated pre-freeze did so
// via the UTXO DB entry written by ConnectBlock at the time. This
// fallback only activates when the UTXO DB entry is MISSING, which
// cannot happen for any block that successfully connected on the live
// chain. So this fallback does NOT change consensus validation of any
// block (0 to 2,224,763) — it only provides a recovery path for nodes
// whose UTXO set was reconstructed incompletely.
CTxIndex txindex;
if (ReadTxIndex(hash, txindex)
&& n < txindex.vSpent.size()
&& txindex.vSpent[n].IsNull())
{
CTransaction txPrev;
if (txPrev.ReadFromDisk(txindex.pos))
{
if (n < txPrev.vout.size())
{
const CTxOut& txout = txPrev.vout[n];
entry.nValue = txout.nValue;
entry.scriptPubKey = txout.scriptPubKey;
entry.fCoinBase = txPrev.IsCoinBase();
entry.fCoinStake = txPrev.IsCoinStake();
entry.nTxTime = txPrev.nTime;
// Reconstruct exact block height: read the block header
// at the txindex's disk position, hash it, look up in
// mapBlockIndex for the canonical height. Same pattern
// as the FetchInputs backfill (src/main.cpp ~line 1873).
entry.nHeight = 0;
CBlock blockHeader;
if (blockHeader.ReadFromDisk(txindex.pos.nFile, txindex.pos.nBlockPos, false))
{
auto bmi = mapBlockIndex.find(blockHeader.GetHash());
if (bmi != mapBlockIndex.end())
entry.nHeight = bmi->second->nHeight;
}
fFound = true;
}
}
}
{ {
LOCK(g_cs_utxoCache); LOCK(g_cs_utxoCache);
@@ -500,15 +440,6 @@ bool CTxDBBase::HaveUtxo(const uint256& hash, unsigned int n)
if (Exists(make_pair(string("u"), make_pair(hash, n)))) if (Exists(make_pair(string("u"), make_pair(hash, n))))
return true; return true;
// Lazy fallback: check old CTxIndex vSpent for databases upgrading from
// pre-UTXO format. vSpent[n] null = output not spent = UTXO exists.
CTxIndex txindex;
if (ReadTxIndex(hash, txindex))
{
if (n < txindex.vSpent.size() && txindex.vSpent[n].IsNull())
return true;
}
return false; return false;
} }