diff --git a/src/init.cpp b/src/init.cpp index ffa8241..e9d7bd1 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -707,6 +707,7 @@ std::string HelpMessage() " -checkblocks= " + _("How many blocks to check at startup (default: 2500, 0 = all)") + "\n" + " -checklevel= " + _("How thorough the block verification is (0-6, default: 1)") + "\n" + " -loadblock= " + _("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" + " -blockminsize= " + _("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 // and shutdown for clean restart. MaybeAutoRebuild(GetArg("-autorerebuild", 0)); diff --git a/src/main.cpp b/src/main.cpp index a625feb..3b38a2b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1755,7 +1755,15 @@ bool IsInitialBlockDownload() // stalled chain). Without this, a node that restarts on a frozen // chain thinks it's fully synced (tip < 24h old from restart) and // 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 false; } @@ -1859,54 +1867,7 @@ bool CTransaction::FetchInputs(CTxDBBase& txdb, const MapPrevTx& mapPendingUtxos continue; } - // Lazy fallback: try old CTxIndex path (for databases upgrading from pre-UTXO format) - { - 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 + // Not in UTXO DB — check mempool { LOCK(mempool.cs); if (mempool.exists(prevout.hash)) diff --git a/src/txdb-base.cpp b/src/txdb-base.cpp index 222a660..70cdaad 100644 --- a/src/txdb-base.cpp +++ b/src/txdb-base.cpp @@ -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); - 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); @@ -500,15 +440,6 @@ bool CTxDBBase::HaveUtxo(const uint256& hash, unsigned int n) if (Exists(make_pair(string("u"), make_pair(hash, n)))) 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; }