Skip to content

Commit

Permalink
error() in disconnect for disk corruption, not inconsistency
Browse files Browse the repository at this point in the history
The error() function unconditionally reports an error. It should only
be used for actually exception situations, and not for the type of
inconsistencies that ApplyTxInUndo/DisconnectBlock can graciously deal
with.

This also makes a subtle semantics change: in ApplyTxInUndo, when a
record with metadata is encountered (indicating it is the last spend
from a tx), don't wipe the CCoins record if it wasn't empty at that
point. This makes sure that UTXO operations never affect any other
UTXOs (including those from the same tx).
  • Loading branch information
sipa authored and xanimo committed May 1, 2024
1 parent 023c787 commit 6b34441
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 46 deletions.
2 changes: 1 addition & 1 deletion src/test/coins_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

#include <boost/test/unit_test.hpp>

bool ApplyTxInUndo(const CTxInUndo& undo, CCoinsViewCache& view, const COutPoint& out);
int ApplyTxInUndo(const CTxInUndo& undo, CCoinsViewCache& view, const COutPoint& out);
void UpdateCoins(const CTransaction& tx, CCoinsViewCache& inputs, CTxUndo &txundo, int nHeight);

namespace
Expand Down
85 changes: 46 additions & 39 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1593,57 +1593,64 @@ bool AbortNode(CValidationState& state, const std::string& strMessage, const std

} // anon namespace

enum DisconnectResult
{
DISCONNECT_OK, // All good.
DISCONNECT_UNCLEAN, // Rolled back, but UTXO set was inconsistent with block.
DISCONNECT_FAILED // Something else went wrong.
};

/**
* Apply the undo operation of a CTxInUndo to the given chain state.
* @param undo The undo object.
* @param view The coins view to which to apply the changes.
* @param out The out point that corresponds to the tx input.
* @return True on success.
* @return A DisconnectResult as an int
*/
bool ApplyTxInUndo(const CTxInUndo& undo, CCoinsViewCache& view, const COutPoint& out)
int ApplyTxInUndo(const CTxInUndo& undo, CCoinsViewCache& view, const COutPoint& out)
{
bool fClean = true;

CCoinsModifier coins = view.ModifyCoins(out.hash);
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("%s: undo data overwriting existing transaction", __func__);
coins->Clear();
if (!coins->IsPruned()) fClean = false; // overwriting existing transaction
coins->fCoinBase = undo.fCoinBase;
coins->nHeight = undo.nHeight;
coins->nVersion = undo.nVersion;
} else {
if (coins->IsPruned())
fClean = fClean && error("%s: undo data adding output to missing transaction", __func__);
if (coins->IsPruned()) fClean = false; // adding output to missing transaction
}
if (coins->IsAvailable(out.n))
fClean = fClean && error("%s: undo data overwriting existing output", __func__);
if (coins->IsAvailable(out.n)) fClean = false; // overwriting existing output
if (coins->vout.size() < out.n+1)
coins->vout.resize(out.n+1);
coins->vout[out.n] = undo.txout;

return fClean;
return fClean ? DISCONNECT_OK : DISCONNECT_UNCLEAN;
}

bool DisconnectBlock(const CBlock& block, CValidationState& state, const CBlockIndex* pindex, CCoinsViewCache& view, bool* pfClean)
/** Undo the effects of this block (with given index) on the UTXO set represented by coins.
* When UNCLEAN or FAILED is returned, view is left in an indeterminate state. */
static DisconnectResult DisconnectBlock(const CBlock& block, const CBlockIndex* pindex, CCoinsViewCache& view)
{
assert(pindex->GetBlockHash() == 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");
if (!UndoReadFromDisk(blockUndo, pos, pindex->pprev->GetBlockHash()))
return error("DisconnectBlock(): failure reading undo data");

if (blockUndo.vtxundo.size() + 1 != block.vtx.size())
return error("DisconnectBlock(): block and undo data inconsistent");
if (pos.IsNull()) {
error("DisconnectBlock(): no undo data available");
return DISCONNECT_FAILED;
}
if (!UndoReadFromDisk(blockUndo, pos, pindex->pprev->GetBlockHash())) {
error("DisconnectBlock(): failure reading undo data");
return DISCONNECT_FAILED;
}
if (blockUndo.vtxundo.size() + 1 != block.vtx.size()) {
error("DisconnectBlock(): block and undo data inconsistent");
return DISCONNECT_FAILED;
}

// undo transactions in reverse order
for (int i = block.vtx.size() - 1; i >= 0; i--) {
Expand All @@ -1662,8 +1669,7 @@ bool DisconnectBlock(const CBlock& block, CValidationState& state, const CBlockI
// but it must be corrected before txout nversion ever influences a network rule.
if (outsBlock.nVersion < 0)
outs->nVersion = outsBlock.nVersion;
if (*outs != outsBlock)
fClean = fClean && error("DisconnectBlock(): added transaction mismatch? database corrupted");
if (*outs != outsBlock) fClean = false; // transaction mismatch

// remove outputs
outs->Clear();
Expand All @@ -1672,26 +1678,24 @@ bool DisconnectBlock(const CBlock& block, CValidationState& state, const CBlockI
// restore inputs
if (i > 0) { // not coinbases
const CTxUndo &txundo = blockUndo.vtxundo[i-1];
if (txundo.vprevout.size() != tx.vin.size())
return error("DisconnectBlock(): transaction and undo data inconsistent");
if (txundo.vprevout.size() != tx.vin.size()) {
error("DisconnectBlock(): transaction and undo data inconsistent");
return DISCONNECT_FAILED;
}
for (unsigned int j = tx.vin.size(); j-- > 0;) {
const COutPoint &out = tx.vin[j].prevout;
const CTxInUndo &undo = txundo.vprevout[j];
if (!ApplyTxInUndo(undo, view, out))
fClean = false;
int res = ApplyTxInUndo(undo, view, out);
if (res == DISCONNECT_FAILED) return DISCONNECT_FAILED;
fClean = fClean && res != DISCONNECT_UNCLEAN;
}
}
}

// move best block pointer to prevout block
view.SetBestBlock(pindex->pprev->GetBlockHash());

if (pfClean) {
*pfClean = fClean;
return true;
}

return fClean;
return fClean ? DISCONNECT_OK : DISCONNECT_UNCLEAN;
}

void static FlushBlockFile(bool fFinalize = false)
Expand Down Expand Up @@ -2239,14 +2243,15 @@ bool static DisconnectTip(CValidationState& state, const CChainParams& chainpara
CBlockIndex *pindexDelete = chainActive.Tip();
assert(pindexDelete);
// Read block from disk.
CBlock block;
std::shared_ptr<CBlock> pblock = std::make_shared<CBlock>();
CBlock& block = *pblock;
if (!ReadBlockFromDisk(block, pindexDelete, chainparams.GetConsensus(chainActive.Height())))
return AbortNode(state, "Failed to read block");
// Apply the block atomically to the chain state.
int64_t nStart = GetTimeMicros();
{
CCoinsViewCache view(pcoinsTip);
if (!DisconnectBlock(block, state, pindexDelete, view))
if (DisconnectBlock(block, pindexDelete, view) != DISCONNECT_OK)
return error("DisconnectTip(): DisconnectBlock %s failed", pindexDelete->GetBlockHash().ToString());
bool flushed = view.Flush();
assert(flushed);
Expand Down Expand Up @@ -3825,15 +3830,17 @@ bool CVerifyDB::VerifyDB(const CChainParams& chainparams, CCoinsView *coinsview,
}
// check level 3: check for inconsistencies during memory-only disconnect of tip blocks
if (nCheckLevel >= 3 && pindex == pindexState && (coins.DynamicMemoryUsage() + pcoinsTip->DynamicMemoryUsage()) <= nCoinCacheUsage) {
bool fClean = true;
if (!DisconnectBlock(block, state, pindex, coins, &fClean))
DisconnectResult res = DisconnectBlock(block, pindex, coins);
if (res == DISCONNECT_FAILED) {
return error("VerifyDB(): *** irrecoverable inconsistency in block data at %d, hash=%s", pindex->nHeight, pindex->GetBlockHash().ToString());
}
pindexState = pindex->pprev;
if (!fClean) {
if (res == DISCONNECT_UNCLEAN) {
nGoodTransactions = 0;
pindexFailure = pindex;
} else
} else {
nGoodTransactions += block.vtx.size();
}
}
if (ShutdownRequested())
return true;
Expand Down
6 changes: 0 additions & 6 deletions src/validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -508,12 +508,6 @@ bool ContextualCheckBlock(const CBlock& block, CValidationState& state, const CB
bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pindex, CCoinsViewCache& coins,
const CChainParams& chainparams, bool fJustCheck = false);

/** 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(const CBlock& block, CValidationState& state, const CBlockIndex* pindex, CCoinsViewCache& coins, bool* pfClean = NULL);

/** Check a block is completely valid from start to finish (only works on top of our current best block, with cs_main held) */
bool TestBlockValidity(CValidationState& state, const CChainParams& chainparams, const CBlock& block, CBlockIndex* pindexPrev, bool fCheckPOW = true, bool fCheckMerkleRoot = true);

Expand Down

0 comments on commit 6b34441

Please sign in to comment.