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:
@@ -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
@@ -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))
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user