From 2cbd71da06a14ffe823440973f8a87032bbe0b1e Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Sun, 30 Dec 2012 15:29:39 +0100 Subject: [PATCH 1/3] Make DisconnectBlock fault-tolerant --- src/main.cpp | 55 +++++++++++++++++++++++++++++++++++----------------- src/main.h | 7 +++++-- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index b59e8a5a..bb39f422 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1464,23 +1464,32 @@ bool CTransaction::ClientCheckInputs() const -bool CBlock::DisconnectBlock(CBlockIndex *pindex, CCoinsViewCache &view) +bool CBlock::DisconnectBlock(CBlockIndex *pindex, CCoinsViewCache &view, bool *pfClean) { assert(pindex == view.GetBestBlock()); + if (pfClean) + *pfClean = false; + + bool fClean = true; + CBlockUndo blockUndo; { CDiskBlockPos pos = pindex->GetUndoPos(); if (pos.IsNull()) return error("DisconnectBlock() : no undo data available"); - FILE *file = OpenUndoFile(pos, true); - if (file == NULL) - return error("DisconnectBlock() : undo file not available"); - CAutoFile fileUndo(file, SER_DISK, CLIENT_VERSION); - fileUndo >> blockUndo; + CAutoFile fileUndo(OpenUndoFile(pos, true), SER_DISK, CLIENT_VERSION); + if (!fileUndo) + return error("DisconnectBlock() : cannot open undo file"); + try { + fileUndo >> blockUndo; + } catch(std::exception &e) { + return error("DisconnectBlock() : deserialize or I/O error reading udno data"); + } } - assert(blockUndo.vtxundo.size() + 1 == vtx.size()); + if (blockUndo.vtxundo.size() + 1 != vtx.size()) + return error("DisconnectBlock() : block and undo data inconsistent"); // undo transactions in reverse order for (int i = vtx.size() - 1; i >= 0; i--) { @@ -1488,13 +1497,15 @@ bool CBlock::DisconnectBlock(CBlockIndex *pindex, CCoinsViewCache &view) uint256 hash = tx.GetHash(); // check that all outputs are available - if (!view.HaveCoins(hash)) - return error("DisconnectBlock() : outputs still spent? database corrupted"); + if (!view.HaveCoins(hash)) { + fClean = fClean && error("DisconnectBlock() : outputs still spent? database corrupted"); + view.SetCoins(hash, CCoins()); + } CCoins &outs = view.GetCoins(hash); CCoins outsBlock = CCoins(tx, pindex->nHeight); if (outs != outsBlock) - return error("DisconnectBlock() : added transaction mismatch? database corrupted"); + fClean = fClean && error("DisconnectBlock() : added transaction mismatch? database corrupted"); // remove outputs outs = CCoins(); @@ -1502,24 +1513,27 @@ bool CBlock::DisconnectBlock(CBlockIndex *pindex, CCoinsViewCache &view) // restore inputs if (i > 0) { // not coinbases const CTxUndo &txundo = blockUndo.vtxundo[i-1]; - assert(txundo.vprevout.size() == tx.vin.size()); + if (txundo.vprevout.size() != tx.vin.size()) + return error("DisconnectBlock() : transaction and undo data inconsistent"); for (unsigned int j = tx.vin.size(); j-- > 0;) { const COutPoint &out = tx.vin[j].prevout; const CTxInUndo &undo = txundo.vprevout[j]; CCoins coins; view.GetCoins(out.hash, coins); // this can fail if the prevout was already entirely spent - if (coins.IsPruned()) { - if (undo.nHeight == 0) - return error("DisconnectBlock() : undo data doesn't contain tx metadata? database corrupted"); + if (undo.nHeight != 0) { + // undo data contains height: this is the last output of the prevout tx being spent + if (!coins.IsPruned()) + fClean = fClean && error("DisconnectBlock() : undo data overwriting existing transaction"); + coins = CCoins(); coins.fCoinBase = undo.fCoinBase; coins.nHeight = undo.nHeight; coins.nVersion = undo.nVersion; } else { - if (undo.nHeight != 0) - return error("DisconnectBlock() : undo data contains unneeded tx metadata? database corrupted"); + if (coins.IsPruned()) + fClean = fClean && error("DisconnectBlock() : undo data adding output to missing transaction"); } if (coins.IsAvailable(out.n)) - return error("DisconnectBlock() : prevout output not spent? database corrupted"); + fClean = fClean && error("DisconnectBlock() : undo data overwriting existing output"); if (coins.vout.size() < out.n+1) coins.vout.resize(out.n+1); coins.vout[out.n] = undo.txout; @@ -1532,7 +1546,12 @@ bool CBlock::DisconnectBlock(CBlockIndex *pindex, CCoinsViewCache &view) // move best block pointer to prevout block view.SetBestBlock(pindex->pprev); - return true; + if (pfClean) { + *pfClean = fClean; + return true; + } else { + return fClean; + } } void static FlushBlockFile() diff --git a/src/main.h b/src/main.h index 8bceffba..80fd7645 100644 --- a/src/main.h +++ b/src/main.h @@ -1305,8 +1305,11 @@ public: } - // Undo the effects of this block (with given index) on the UTXO set represented by coins - bool DisconnectBlock(CBlockIndex *pindex, CCoinsViewCache &coins); + /** Undo the effects of this block (with given index) on the UTXO set represented by coins. + * In case pfClean is provided, operation will try to be tolerant about errors, and *pfClean + * will be true if no problems were found. Otherwise, the return value will be false in case + * of problems. Note that in any case, coins may be modified. */ + bool DisconnectBlock(CBlockIndex *pindex, CCoinsViewCache &coins, bool *pfClean = NULL); // Apply the effects of this block (with given index) on the UTXO set represented by coins bool ConnectBlock(CBlockIndex *pindex, CCoinsViewCache &coins, bool fJustCheck=false); From 8539361e66eace839060f77f39b770d045e7ce28 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Sun, 30 Dec 2012 23:41:41 +0100 Subject: [PATCH 2/3] Add checksums to undo data This should be compatible with older code that didn't write checksums. --- src/main.cpp | 22 +++++++--------------- src/main.h | 46 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index bb39f422..2619062a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1474,19 +1474,11 @@ bool CBlock::DisconnectBlock(CBlockIndex *pindex, CCoinsViewCache &view, bool *p bool fClean = true; CBlockUndo blockUndo; - { - CDiskBlockPos pos = pindex->GetUndoPos(); - if (pos.IsNull()) - return error("DisconnectBlock() : no undo data available"); - CAutoFile fileUndo(OpenUndoFile(pos, true), SER_DISK, CLIENT_VERSION); - if (!fileUndo) - return error("DisconnectBlock() : cannot open undo file"); - try { - fileUndo >> blockUndo; - } catch(std::exception &e) { - return error("DisconnectBlock() : deserialize or I/O error reading udno data"); - } - } + CDiskBlockPos pos = pindex->GetUndoPos(); + if (pos.IsNull()) + return error("DisconnectBlock() : no undo data available"); + if (!blockUndo.ReadFromDisk(pos, pindex->pprev->GetBlockHash())) + return error("DisconnectBlock() : failure reading undo data"); if (blockUndo.vtxundo.size() + 1 != vtx.size()) return error("DisconnectBlock() : block and undo data inconsistent"); @@ -1670,9 +1662,9 @@ bool CBlock::ConnectBlock(CBlockIndex* pindex, CCoinsViewCache &view, bool fJust { if (pindex->GetUndoPos().IsNull()) { CDiskBlockPos pos; - if (!FindUndoPos(pindex->nFile, pos, ::GetSerializeSize(blockundo, SER_DISK, CLIENT_VERSION) + 8)) + if (!FindUndoPos(pindex->nFile, pos, ::GetSerializeSize(blockundo, SER_DISK, CLIENT_VERSION) + 40)) return error("ConnectBlock() : FindUndoPos failed"); - if (!blockundo.WriteToDisk(pos)) + if (!blockundo.WriteToDisk(pos, pindex->pprev->GetBlockHash())) return error("ConnectBlock() : CBlockUndo::WriteToDisk failed"); // update nUndoPos in block index diff --git a/src/main.h b/src/main.h index 80fd7645..557df8bc 100644 --- a/src/main.h +++ b/src/main.h @@ -746,7 +746,7 @@ public: READWRITE(vtxundo); ) - bool WriteToDisk(CDiskBlockPos &pos) + bool WriteToDisk(CDiskBlockPos &pos, const uint256 &hashBlock) { // Open history file to append CAutoFile fileout = CAutoFile(OpenUndoFile(pos), SER_DISK, CLIENT_VERSION); @@ -764,6 +764,12 @@ public: pos.nPos = (unsigned int)fileOutPos; fileout << *this; + // calculate & write checksum + CHashWriter hasher(SER_GETHASH, PROTOCOL_VERSION); + hasher << hashBlock; + hasher << *this; + fileout << hasher.GetHash(); + // Flush stdio buffers and commit to disk before returning fflush(fileout); if (!IsInitialBlockDownload()) @@ -771,6 +777,44 @@ public: return true; } + + bool ReadFromDisk(const CDiskBlockPos &pos, const uint256 &hashBlock) + { + // Open history file to read + CAutoFile filein = CAutoFile(OpenUndoFile(pos, true), SER_DISK, CLIENT_VERSION); + if (!filein) + return error("CBlockUndo::ReadFromDisk() : OpenBlockFile failed"); + + // Read block + uint256 hashChecksum; + try { + filein >> *this; + } + catch (std::exception &e) { + return error("%s() : deserialize or I/O error", __PRETTY_FUNCTION__); + } + + // for compatibility with pre-release code that didn't write checksums to undo data + // TODO: replace by a simply 'filein >> hashChecksum' in the above try block + try { + filein >> hashChecksum; + } catch (std::exception &e) { + hashChecksum = 0; + } + uint32_t hashInit = hashChecksum.Get64(0) & 0xFFFFFFFFUL; + if (hashChecksum == 0 || memcmp(&hashInit, pchMessageStart, 4) == 0) + return true; + + // Verify checksum + CHashWriter hasher(SER_GETHASH, PROTOCOL_VERSION); + hasher << hashBlock; + hasher << *this; + if (hashChecksum != hasher.GetHash()) + return error("CBlockUndo::ReadFromDisk() : checksum mismatch"); + + return true; + } + }; /** pruned version of CTransaction: only retains metadata and unspent transaction outputs From 1f355b66cd90f1bd96a862604a7216e8ba56f853 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Thu, 3 Jan 2013 15:29:07 +0100 Subject: [PATCH 3/3] New database check routine -checklevel gets a new meaning: 0: verify blocks can be read from disk (like before) 1: verify (contextless) block validity (like before) 2: verify undo files can be read and have good checksums 3: verify coin database is consistent with the last few blocks (close to level 6 before) 4: verify all validity rules of the last few blocks Level 3 is the new default, as it's reasonably fast. As level 3 and 4 are implemented using an in-memory rollback of the database, they are limited to as many blocks as possible without exceeding the limits set by -dbcache. The default of -dbcache=25 allows for some 150-200 blocks to be rolled back. In case an error is found, the application quits with a message instructing the user to restart with -reindex. Better instructions, and automatic recovery (when possible) or automatic reindexing are left as future work. --- src/init.cpp | 7 ++++-- src/main.cpp | 65 ++++++++++++++++++++++++++++++++++++++++++---------- src/main.h | 2 ++ 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 557c23fc..79e9b2fb 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -300,7 +300,7 @@ std::string HelpMessage() " -rescan " + _("Rescan the block chain for missing wallet transactions") + "\n" + " -salvagewallet " + _("Attempt to recover private keys from a corrupt wallet.dat") + "\n" + " -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" + + " -checklevel= " + _("How thorough the block verification is (0-4, default: 3)") + "\n" + " -loadblock= " + _("Imports blocks from external blk000??.dat file") + "\n" + " -reindex " + _("Rebuild blockchain index from current blk000??.dat files") + "\n" + @@ -752,7 +752,10 @@ bool AppInit2() pblocktree->WriteReindexing(true); if (!LoadBlockIndex()) - return InitError(_("Error loading blkindex.dat")); + return InitError(_("Error loading block/coin databases")); + + if (!VerifyDB()) + return InitError(_("Corrupted database detected. Please restart the client with -reindex.")); // as LoadBlockIndex can take several minutes, it's possible the user // requested to kill bitcoin-qt during the last operation. If so, exit. diff --git a/src/main.cpp b/src/main.cpp index 2619062a..3fd85131 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2388,36 +2388,77 @@ bool static LoadBlockIndexDB() BlockHashStr(hashBestChain).c_str(), nBestHeight, DateTimeStrFormat("%Y-%m-%d %H:%M:%S", pindexBest->GetBlockTime()).c_str()); + return true; +} + +bool VerifyDB() { + if (pindexBest == NULL || pindexBest->pprev == NULL) + return true; + // Verify blocks in the best chain - int nCheckLevel = GetArg("-checklevel", 1); + int nCheckLevel = GetArg("-checklevel", 3); int nCheckDepth = GetArg( "-checkblocks", 2500); if (nCheckDepth == 0) nCheckDepth = 1000000000; // suffices until the year 19000 if (nCheckDepth > nBestHeight) nCheckDepth = nBestHeight; + nCheckLevel = std::max(0, std::min(4, nCheckLevel)); printf("Verifying last %i blocks at level %i\n", nCheckDepth, nCheckLevel); - CBlockIndex* pindexFork = NULL; + CCoinsViewCache coins(*pcoinsTip, true); + CBlockIndex* pindexState = pindexBest; + CBlockIndex* pindexFailure = NULL; + int nGoodTransactions = 0; for (CBlockIndex* pindex = pindexBest; pindex && pindex->pprev; pindex = pindex->pprev) { if (fRequestShutdown || pindex->nHeight < nBestHeight-nCheckDepth) break; CBlock block; + // check level 0: read from disk if (!block.ReadFromDisk(pindex)) - return error("LoadBlockIndex() : block.ReadFromDisk failed"); + return error("VerifyDB() : *** block.ReadFromDisk failed at %d, hash=%s", pindex->nHeight, pindex->GetBlockHash().ToString().c_str()); // check level 1: verify block validity - if (nCheckLevel>0 && !block.CheckBlock()) - { - printf("LoadBlockIndex() : *** found bad block at %d, hash=%s\n", pindex->nHeight, pindex->GetBlockHash().ToString().c_str()); - pindexFork = pindex->pprev; + if (nCheckLevel >= 1 && !block.CheckBlock()) + return error("VerifyDB() : *** found bad block at %d, hash=%s\n", pindex->nHeight, pindex->GetBlockHash().ToString().c_str()); + // check level 2: verify undo validity + if (nCheckLevel >= 2 && pindex) { + CBlockUndo undo; + CDiskBlockPos pos = pindex->GetUndoPos(); + if (!pos.IsNull()) { + if (!undo.ReadFromDisk(pos, pindex->pprev->GetBlockHash())) + return error("VerifyDB() : *** found bad undo data at %d, hash=%s\n", pindex->nHeight, pindex->GetBlockHash().ToString().c_str()); + } + } + // check level 3: check for inconsistencies during memory-only disconnect of tip blocks + if (nCheckLevel >= 3 && pindex == pindexState && (coins.GetCacheSize() + pcoinsTip->GetCacheSize()) <= 2*nCoinCacheSize + 32000) { + bool fClean = true; + if (!block.DisconnectBlock(pindex, coins, &fClean)) + return error("VerifyDB() : *** irrecoverable inconsistency in block data at %d, hash=%s", pindex->nHeight, pindex->GetBlockHash().ToString().c_str()); + pindexState = pindex->pprev; + if (!fClean) { + nGoodTransactions = 0; + pindexFailure = pindex; + } else + nGoodTransactions += block.vtx.size(); } - // TODO: stronger verifications } - if (pindexFork && !fRequestShutdown) - { - // TODO: reorg back - return error("LoadBlockIndex(): chain database corrupted"); + if (pindexFailure) + return error("VerifyDB() : *** coin database inconsistencies found (last %i blocks, %i good transactions before that)\n", pindexBest->nHeight - pindexFailure->nHeight + 1, nGoodTransactions); + + // check level 4: try reconnecting blocks + if (nCheckLevel >= 4) { + CBlockIndex *pindex = pindexState; + while (pindex != pindexBest && !fRequestShutdown) { + pindex = pindex->pnext; + CBlock block; + if (!block.ReadFromDisk(pindex)) + return error("VerifyDB() : *** block.ReadFromDisk failed at %d, hash=%s", pindex->nHeight, pindex->GetBlockHash().ToString().c_str()); + if (!block.ConnectBlock(pindex, coins)) + return error("VerifyDB() : *** found unconnectable block at %d, hash=%s", pindex->nHeight, pindex->GetBlockHash().ToString().c_str()); + } } + printf("No coin database inconsistencies in last %i blocks (%i transactions)\n", pindexBest->nHeight - pindexState->nHeight, nGoodTransactions); + return true; } diff --git a/src/main.h b/src/main.h index 557df8bc..2e3e1c5e 100644 --- a/src/main.h +++ b/src/main.h @@ -126,6 +126,8 @@ FILE* OpenUndoFile(const CDiskBlockPos &pos, bool fReadOnly = false); bool LoadExternalBlockFile(FILE* fileIn, CDiskBlockPos *dbp = NULL); /** Load the block tree and coins database from disk */ bool LoadBlockIndex(); +/** Verify consistency of the block and coin databases */ +bool VerifyDB(); /** Print the loaded block tree */ void PrintBlockTree(); /** Find a block by height in the currently-connected chain */