Skip to content

Commit 013a56a

Browse files
committed
Non-atomic flushing using the blockchain as replay journal
1 parent b3a279c commit 013a56a

File tree

7 files changed

+186
-16
lines changed

7 files changed

+186
-16
lines changed

src/coins.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
bool CCoinsView::GetCoin(const COutPoint &outpoint, Coin &coin) const { return false; }
1414
bool CCoinsView::HaveCoin(const COutPoint &outpoint) const { return false; }
1515
uint256 CCoinsView::GetBestBlock() const { return uint256(); }
16+
std::vector<uint256> CCoinsView::GetHeadBlocks() const { return std::vector<uint256>(); }
1617
bool CCoinsView::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock) { return false; }
1718
CCoinsViewCursor *CCoinsView::Cursor() const { return 0; }
1819

@@ -21,6 +22,7 @@ CCoinsViewBacked::CCoinsViewBacked(CCoinsView *viewIn) : base(viewIn) { }
2122
bool CCoinsViewBacked::GetCoin(const COutPoint &outpoint, Coin &coin) const { return base->GetCoin(outpoint, coin); }
2223
bool CCoinsViewBacked::HaveCoin(const COutPoint &outpoint) const { return base->HaveCoin(outpoint); }
2324
uint256 CCoinsViewBacked::GetBestBlock() const { return base->GetBestBlock(); }
25+
std::vector<uint256> CCoinsViewBacked::GetHeadBlocks() const { return base->GetHeadBlocks(); }
2426
void CCoinsViewBacked::SetBackend(CCoinsView &viewIn) { base = &viewIn; }
2527
bool CCoinsViewBacked::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock) { return base->BatchWrite(mapCoins, hashBlock); }
2628
CCoinsViewCursor *CCoinsViewBacked::Cursor() const { return base->Cursor(); }
@@ -81,13 +83,14 @@ void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possi
8183
cachedCoinsUsage += it->second.coin.DynamicMemoryUsage();
8284
}
8385

84-
void AddCoins(CCoinsViewCache& cache, const CTransaction &tx, int nHeight) {
86+
void AddCoins(CCoinsViewCache& cache, const CTransaction &tx, int nHeight, bool check) {
8587
bool fCoinbase = tx.IsCoinBase();
8688
const uint256& txid = tx.GetHash();
8789
for (size_t i = 0; i < tx.vout.size(); ++i) {
88-
// Pass fCoinbase as the possible_overwrite flag to AddCoin, in order to correctly
90+
bool overwrite = check ? cache.HaveCoin(COutPoint(txid, i)) : fCoinbase;
91+
// Always set the possible_overwrite flag to AddCoin for coinbase txn, in order to correctly
8992
// deal with the pre-BIP30 occurrences of duplicate coinbase transactions.
90-
cache.AddCoin(COutPoint(txid, i), Coin(tx.vout[i], nHeight, fCoinbase), fCoinbase);
93+
cache.AddCoin(COutPoint(txid, i), Coin(tx.vout[i], nHeight, fCoinbase), overwrite);
9194
}
9295
}
9396

src/coins.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ class CCoinsView
155155
//! Retrieve the block hash whose state this CCoinsView currently represents
156156
virtual uint256 GetBestBlock() const;
157157

158+
//! Retrieve the range of blocks that may have been only partially written.
159+
//! If the database is in a consistent state, the result is the empty vector.
160+
//! Otherwise, a two-element vector is returned consisting of the new and
161+
//! the old block hash, in that order.
162+
virtual std::vector<uint256> GetHeadBlocks() const;
163+
158164
//! Do a bulk modification (multiple Coin changes + BestBlock change).
159165
//! The passed mapCoins can be modified.
160166
virtual bool BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock);
@@ -181,6 +187,7 @@ class CCoinsViewBacked : public CCoinsView
181187
bool GetCoin(const COutPoint &outpoint, Coin &coin) const override;
182188
bool HaveCoin(const COutPoint &outpoint) const override;
183189
uint256 GetBestBlock() const override;
190+
std::vector<uint256> GetHeadBlocks() const override;
184191
void SetBackend(CCoinsView &viewIn);
185192
bool BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock) override;
186193
CCoinsViewCursor *Cursor() const override;
@@ -289,10 +296,12 @@ class CCoinsViewCache : public CCoinsViewBacked
289296
};
290297

291298
//! Utility function to add all of a transaction's outputs to a cache.
292-
// It assumes that overwrites are only possible for coinbase transactions,
299+
// When check is false, this assumes that overwrites are only possible for coinbase transactions.
300+
// When check is true, the underlying view may be queried to determine whether an addition is
301+
// an overwrite.
293302
// TODO: pass in a boolean to limit these possible overwrites to known
294303
// (pre-BIP34) cases.
295-
void AddCoins(CCoinsViewCache& cache, const CTransaction& tx, int nHeight);
304+
void AddCoins(CCoinsViewCache& cache, const CTransaction& tx, int nHeight, bool check = false);
296305

297306
//! Utility function to find any unspent output with a given txid.
298307
const Coin& AccessByTxid(const CCoinsViewCache& cache, const uint256& txid);

src/init.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,9 @@ std::string HelpMessage(HelpMessageMode mode)
336336
#endif
337337
}
338338
strUsage += HelpMessageOpt("-datadir=<dir>", _("Specify data directory"));
339+
if (showDebug) {
340+
strUsage += HelpMessageOpt("-dbbatchsize", strprintf("Maximum database write batch size in bytes (default: %u)", nDefaultDbBatchSize));
341+
}
339342
strUsage += HelpMessageOpt("-dbcache=<n>", strprintf(_("Set database cache size in megabytes (%d to %d, default: %d)"), nMinDbCache, nMaxDbCache, nDefaultDbCache));
340343
if (showDebug)
341344
strUsage += HelpMessageOpt("-feefilter", strprintf("Tell other nodes to filter invs to us by our mempool min fee (default: %u)", DEFAULT_FEEFILTER));
@@ -1426,6 +1429,13 @@ bool AppInitMain(boost::thread_group& threadGroup, CScheduler& scheduler)
14261429
break;
14271430
}
14281431

1432+
if (!ReplayBlocks(chainparams, pcoinsdbview)) {
1433+
strLoadError = _("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate.");
1434+
break;
1435+
}
1436+
pcoinsTip->SetBestBlock(pcoinsdbview->GetBestBlock()); // TODO: only initialize pcoinsTip after ReplayBlocks
1437+
LoadChainTip(chainparams);
1438+
14291439
if (!fReindex && chainActive.Tip() != NULL) {
14301440
uiInterface.InitMessage(_("Rewinding blocks..."));
14311441
if (!RewindBlockIndex(chainparams)) {

src/txdb.cpp

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ static const char DB_TXINDEX = 't';
2121
static const char DB_BLOCK_INDEX = 'b';
2222

2323
static const char DB_BEST_BLOCK = 'B';
24+
static const char DB_HEAD_BLOCKS = 'H';
2425
static const char DB_FLAG = 'F';
2526
static const char DB_REINDEX_FLAG = 'R';
2627
static const char DB_LAST_BLOCK = 'l';
@@ -68,10 +69,45 @@ uint256 CCoinsViewDB::GetBestBlock() const {
6869
return hashBestChain;
6970
}
7071

72+
std::vector<uint256> CCoinsViewDB::GetHeadBlocks() const {
73+
std::vector<uint256> vhashHeadBlocks;
74+
if (!db.Read(DB_HEAD_BLOCKS, vhashHeadBlocks)) {
75+
return std::vector<uint256>();
76+
}
77+
return vhashHeadBlocks;
78+
}
79+
7180
bool CCoinsViewDB::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock) {
7281
CDBBatch batch(db);
7382
size_t count = 0;
7483
size_t changed = 0;
84+
size_t batch_size = (size_t)GetArg("-dbbatchsize", nDefaultDbBatchSize);
85+
86+
87+
uint256 old_tip = GetBestBlock();
88+
if (old_tip.IsNull()) {
89+
// We may be in the middle of replaying.
90+
std::vector<uint256> old_heads = GetHeadBlocks();
91+
if (old_heads.size() == 2) {
92+
assert(old_heads[0] == hashBlock);
93+
old_tip = old_heads[1];
94+
}
95+
}
96+
97+
if (hashBlock.IsNull()) {
98+
// Initial flush, nothing to write.
99+
assert(mapCoins.empty());
100+
assert(old_tip.IsNull());
101+
return true;
102+
}
103+
104+
// In the first batch, mark the database as being in the middle of a
105+
// transition from old_tip to hashBlock.
106+
// A vector is used for future extensibility, as we may want to support
107+
// interrupting after partial writes from multiple independent reorgs.
108+
batch.Erase(DB_BEST_BLOCK);
109+
batch.Write(DB_HEAD_BLOCKS, std::vector<uint256>{hashBlock, old_tip});
110+
75111
for (CCoinsMap::iterator it = mapCoins.begin(); it != mapCoins.end();) {
76112
if (it->second.flags & CCoinsCacheEntry::DIRTY) {
77113
CoinEntry entry(&it->first);
@@ -84,10 +120,18 @@ bool CCoinsViewDB::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock) {
84120
count++;
85121
CCoinsMap::iterator itOld = it++;
86122
mapCoins.erase(itOld);
123+
if (batch.SizeEstimate() > batch_size) {
124+
LogPrint(BCLog::COINDB, "Writing partial batch of %.2f MiB\n", batch.SizeEstimate() * (1.0 / 1048576.0));
125+
db.WriteBatch(batch);
126+
batch.Clear();
127+
}
87128
}
88-
if (!hashBlock.IsNull())
89-
batch.Write(DB_BEST_BLOCK, hashBlock);
90129

130+
// In the last batch, mark the database as consistent with hashBlock again.
131+
batch.Erase(DB_HEAD_BLOCKS);
132+
batch.Write(DB_BEST_BLOCK, hashBlock);
133+
134+
LogPrint(BCLog::COINDB, "Writing final batch of %.2f MiB\n", batch.SizeEstimate() * (1.0 / 1048576.0));
91135
bool ret = db.WriteBatch(batch);
92136
LogPrint(BCLog::COINDB, "Committed %u changed transaction outputs (out of %u) to coin database...\n", (unsigned int)changed, (unsigned int)count);
93137
return ret;

src/txdb.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ static constexpr int DB_PEAK_USAGE_FACTOR = 2;
2525
static constexpr int MAX_BLOCK_COINSDB_USAGE = 10 * DB_PEAK_USAGE_FACTOR;
2626
//! -dbcache default (MiB)
2727
static const int64_t nDefaultDbCache = 450;
28+
//! -dbbatchsize default (bytes)
29+
static const int64_t nDefaultDbBatchSize = 16 << 20;
2830
//! max. -dbcache (MiB)
2931
static const int64_t nMaxDbCache = sizeof(void*) > 4 ? 16384 : 1024;
3032
//! min. -dbcache (MiB)
@@ -74,6 +76,7 @@ class CCoinsViewDB : public CCoinsView
7476
bool GetCoin(const COutPoint &outpoint, Coin &coin) const override;
7577
bool HaveCoin(const COutPoint &outpoint) const override;
7678
uint256 GetBestBlock() const override;
79+
std::vector<uint256> GetHeadBlocks() const override;
7780
bool BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock) override;
7881
CCoinsViewCursor *Cursor() const override;
7982

src/validation.cpp

Lines changed: 105 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ namespace {
9696

9797
struct CBlockIndexWorkComparator
9898
{
99-
bool operator()(CBlockIndex *pa, CBlockIndex *pb) const {
99+
bool operator()(const CBlockIndex *pa, const CBlockIndex *pb) const {
100100
// First sort by most total work, ...
101101
if (pa->nChainWork > pb->nChainWork) return false;
102102
if (pa->nChainWork < pb->nChainWork) return true;
@@ -1331,17 +1331,19 @@ int ApplyTxInUndo(Coin&& undo, CCoinsViewCache& view, const COutPoint& out)
13311331
return DISCONNECT_FAILED; // adding output for transaction without known metadata
13321332
}
13331333
}
1334-
view.AddCoin(out, std::move(undo), undo.fCoinBase);
1334+
// The potential_overwrite parameter to AddCoin is only allowed to be false if we know for
1335+
// sure that the coin did not already exist in the cache. As we have queried for that above
1336+
// using HaveCoin, we don't need to guess. When fClean is false, a coin already existed and
1337+
// it is an overwrite.
1338+
view.AddCoin(out, std::move(undo), !fClean);
13351339

13361340
return fClean ? DISCONNECT_OK : DISCONNECT_UNCLEAN;
13371341
}
13381342

13391343
/** Undo the effects of this block (with given index) on the UTXO set represented by coins.
1340-
* When UNCLEAN or FAILED is returned, view is left in an indeterminate state. */
1344+
* When FAILED is returned, view is left in an indeterminate state. */
13411345
static DisconnectResult DisconnectBlock(const CBlock& block, const CBlockIndex* pindex, CCoinsViewCache& view)
13421346
{
1343-
assert(pindex->GetBlockHash() == view.GetBestBlock());
1344-
13451347
bool fClean = true;
13461348

13471349
CBlockUndo blockUndo;
@@ -1946,6 +1948,7 @@ bool static DisconnectTip(CValidationState& state, const CChainParams& chainpara
19461948
int64_t nStart = GetTimeMicros();
19471949
{
19481950
CCoinsViewCache view(pcoinsTip);
1951+
assert(view.GetBestBlock() == pindexDelete->GetBlockHash());
19491952
if (DisconnectBlock(block, pindexDelete, view) != DISCONNECT_OK)
19501953
return error("DisconnectTip(): DisconnectBlock %s failed", pindexDelete->GetBlockHash().ToString());
19511954
bool flushed = view.Flush();
@@ -3417,20 +3420,26 @@ bool static LoadBlockIndexDB(const CChainParams& chainparams)
34173420
pblocktree->ReadFlag("txindex", fTxIndex);
34183421
LogPrintf("%s: transaction index %s\n", __func__, fTxIndex ? "enabled" : "disabled");
34193422

3423+
LoadChainTip(chainparams);
3424+
return true;
3425+
}
3426+
3427+
void LoadChainTip(const CChainParams& chainparams)
3428+
{
3429+
if (chainActive.Tip() && chainActive.Tip()->GetBlockHash() == pcoinsTip->GetBestBlock()) return;
3430+
34203431
// Load pointer to end of best chain
34213432
BlockMap::iterator it = mapBlockIndex.find(pcoinsTip->GetBestBlock());
34223433
if (it == mapBlockIndex.end())
3423-
return true;
3434+
return;
34243435
chainActive.SetTip(it->second);
34253436

34263437
PruneBlockIndexCandidates();
34273438

3428-
LogPrintf("%s: hashBestChain=%s height=%d date=%s progress=%f\n", __func__,
3439+
LogPrintf("Loaded best chain: hashBestChain=%s height=%d date=%s progress=%f\n",
34293440
chainActive.Tip()->GetBlockHash().ToString(), chainActive.Height(),
34303441
DateTimeStrFormat("%Y-%m-%d %H:%M:%S", chainActive.Tip()->GetBlockTime()),
34313442
GuessVerificationProgress(chainparams.TxData(), chainActive.Tip()));
3432-
3433-
return true;
34343443
}
34353444

34363445
CVerifyDB::CVerifyDB()
@@ -3499,6 +3508,7 @@ bool CVerifyDB::VerifyDB(const CChainParams& chainparams, CCoinsView *coinsview,
34993508
}
35003509
// check level 3: check for inconsistencies during memory-only disconnect of tip blocks
35013510
if (nCheckLevel >= 3 && pindex == pindexState && (coins.DynamicMemoryUsage() + pcoinsTip->DynamicMemoryUsage()) <= nCoinCacheUsage) {
3511+
assert(coins.GetBestBlock() == pindex->GetBlockHash());
35023512
DisconnectResult res = DisconnectBlock(block, pindex, coins);
35033513
if (res == DISCONNECT_FAILED) {
35043514
return error("VerifyDB(): *** irrecoverable inconsistency in block data at %d, hash=%s", pindex->nHeight, pindex->GetBlockHash().ToString());
@@ -3538,6 +3548,92 @@ bool CVerifyDB::VerifyDB(const CChainParams& chainparams, CCoinsView *coinsview,
35383548
return true;
35393549
}
35403550

3551+
/** Apply the effects of a block on the utxo cache, ignoring that it may already have been applied. */
3552+
static bool RollforwardBlock(const CBlockIndex* pindex, CCoinsViewCache& inputs, const CChainParams& params)
3553+
{
3554+
// TODO: merge with ConnectBlock
3555+
CBlock block;
3556+
if (!ReadBlockFromDisk(block, pindex, params.GetConsensus())) {
3557+
return error("ReplayBlock(): ReadBlockFromDisk failed at %d, hash=%s", pindex->nHeight, pindex->GetBlockHash().ToString());
3558+
}
3559+
3560+
for (const CTransactionRef& tx : block.vtx) {
3561+
if (!tx->IsCoinBase()) {
3562+
for (const CTxIn &txin : tx->vin) {
3563+
inputs.SpendCoin(txin.prevout);
3564+
}
3565+
}
3566+
// Pass check = true as every addition may be an overwrite.
3567+
AddCoins(inputs, *tx, pindex->nHeight, true);
3568+
}
3569+
return true;
3570+
}
3571+
3572+
bool ReplayBlocks(const CChainParams& params, CCoinsView* view)
3573+
{
3574+
LOCK(cs_main);
3575+
3576+
CCoinsViewCache cache(view);
3577+
3578+
std::vector<uint256> hashHeads = view->GetHeadBlocks();
3579+
if (hashHeads.empty()) return true; // We're already in a consistent state.
3580+
if (hashHeads.size() != 2) return error("ReplayBlocks(): unknown inconsistent state");
3581+
3582+
uiInterface.ShowProgress(_("Replaying blocks..."), 0);
3583+
LogPrintf("Replaying blocks\n");
3584+
3585+
const CBlockIndex* pindexOld = nullptr; // Old tip during the interrupted flush.
3586+
const CBlockIndex* pindexNew; // New tip during the interrupted flush.
3587+
const CBlockIndex* pindexFork = nullptr; // Latest block common to both the old and the new tip.
3588+
3589+
if (mapBlockIndex.count(hashHeads[0]) == 0) {
3590+
return error("ReplayBlocks(): reorganization to unknown block requested");
3591+
}
3592+
pindexNew = mapBlockIndex[hashHeads[0]];
3593+
3594+
if (!hashHeads[1].IsNull()) { // The old tip is allowed to be 0, indicating it's the first flush.
3595+
if (mapBlockIndex.count(hashHeads[1]) == 0) {
3596+
return error("ReplayBlocks(): reorganization from unknown block requested");
3597+
}
3598+
pindexOld = mapBlockIndex[hashHeads[1]];
3599+
pindexFork = LastCommonAncestor(pindexOld, pindexNew);
3600+
assert(pindexFork != nullptr);
3601+
}
3602+
3603+
// Rollback along the old branch.
3604+
while (pindexOld != pindexFork) {
3605+
if (pindexOld->nHeight > 0) { // Never disconnect the genesis block.
3606+
CBlock block;
3607+
if (!ReadBlockFromDisk(block, pindexOld, params.GetConsensus())) {
3608+
return error("RollbackBlock(): ReadBlockFromDisk() failed at %d, hash=%s", pindexOld->nHeight, pindexOld->GetBlockHash().ToString());
3609+
}
3610+
LogPrintf("Rolling back %s (%i)\n", pindexOld->GetBlockHash().ToString(), pindexOld->nHeight);
3611+
DisconnectResult res = DisconnectBlock(block, pindexOld, cache);
3612+
if (res == DISCONNECT_FAILED) {
3613+
return error("RollbackBlock(): DisconnectBlock failed at %d, hash=%s", pindexOld->nHeight, pindexOld->GetBlockHash().ToString());
3614+
}
3615+
// If DISCONNECT_UNCLEAN is returned, it means a non-existing UTXO was deleted, or an existing UTXO was
3616+
// overwritten. It corresponds to cases where the block-to-be-disconnect never had all its operations
3617+
// applied to the UTXO set. However, as both writing a UTXO and deleting a UTXO are idempotent operations,
3618+
// the result is still a version of the UTXO set with the effects of that block undone.
3619+
}
3620+
pindexOld = pindexOld->pprev;
3621+
}
3622+
3623+
// Roll forward from the forking point to the new tip.
3624+
int nForkHeight = pindexFork ? pindexFork->nHeight : 0;
3625+
for (int nHeight = nForkHeight + 1; nHeight <= pindexNew->nHeight; ++nHeight) {
3626+
const CBlockIndex* pindex = pindexNew->GetAncestor(nHeight);
3627+
LogPrintf("Rolling forward %s (%i)\n", pindex->GetBlockHash().ToString(), nHeight);
3628+
if (!RollforwardBlock(pindex, cache, params)) return false;
3629+
}
3630+
3631+
cache.SetBestBlock(pindexNew->GetBlockHash());
3632+
cache.Flush();
3633+
uiInterface.ShowProgress("", 100);
3634+
return true;
3635+
}
3636+
35413637
bool RewindBlockIndex(const CChainParams& params)
35423638
{
35433639
LOCK(cs_main);

src/validation.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,8 @@ bool LoadExternalBlockFile(const CChainParams& chainparams, FILE* fileIn, CDiskB
260260
bool InitBlockIndex(const CChainParams& chainparams);
261261
/** Load the block tree and coins database from disk */
262262
bool LoadBlockIndex(const CChainParams& chainparams);
263+
/** Update the chain tip based on database information. */
264+
void LoadChainTip(const CChainParams& chainparams);
263265
/** Unload database information */
264266
void UnloadBlockIndex();
265267
/** Run an instance of the script checking thread */
@@ -424,6 +426,9 @@ class CVerifyDB {
424426
bool VerifyDB(const CChainParams& chainparams, CCoinsView *coinsview, int nCheckLevel, int nCheckDepth);
425427
};
426428

429+
/** Replay blocks that aren't fully applied to the database. */
430+
bool ReplayBlocks(const CChainParams& params, CCoinsView* view);
431+
427432
/** Find the last common block between the parameter chain and a locator. */
428433
CBlockIndex* FindForkInGlobalIndex(const CChain& chain, const CBlockLocator& locator);
429434

0 commit comments

Comments
 (0)