Skip to content

Commit 066fca8

Browse files
validation: discard snapshot state when the snapshot chainstate goes away
Two ways a persisted snapshot chainstate could outlive itself: PopulateAndValidateSnapshot commits the SNAPSHOT best-block and dual-chainstate markers as its last step. If activation was then abandoned -- WriteSnapshotBaseBlockhash on an unwritable datadir, say -- only the coins directory was removed. The node went back to a single chainstate carrying a permanent dual-chainstate marker, which turns supported legacy missing-diff bootstrapping into unavailable history, and a stale SNAPSHOT marker that a future snapshot directory could satisfy ActivateExistingSnapshot with. Erase both when activation is rolled back. LoadChainstate constructs CEvoDB with wipe=true for -reindex and -reindex-chainstate, erasing the SNAPSHOT marker while chainstate_snapshot and its base-blockhash file stay on disk. DetectSnapshotChainstate then found the directory and ActivateExistingSnapshot rejected it on the marker just erased, so startup failed with advice ('reindex is required') the user had already followed -- on every retry. Discard the persisted snapshot chainstate's on-disk artifacts alongside the EvoDB wipe. This is the Dash-shaped equivalent of the reindex-time cleanup in bitcoin#27596/bitcoin#29726; it runs before any chainstate has coins views, so no mempool transfer or leveldb-lock dance is needed.
1 parent 6f2aabc commit 066fca8

7 files changed

Lines changed: 114 additions & 0 deletions

File tree

‎src/evo/evodb.cpp‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,11 @@ bool CEvoDB::HasDualChainstateMarker()
140140
LOCK(cs);
141141
return db->Exists(EVODB_DUAL_CHAINSTATE);
142142
}
143+
144+
void CEvoDB::EraseSnapshotMarkers()
145+
{
146+
LOCK(cs);
147+
auto& transaction = GetContext(GetCurrentIdentity()).cur_transaction;
148+
transaction.Erase(std::make_pair(EVODB_BEST_BLOCK, uint8_t{1}));
149+
transaction.Erase(EVODB_DUAL_CHAINSTATE);
150+
}

‎src/evo/evodb.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,12 @@ class CEvoDB
240240
void WriteBestBlock(EvoDbIdentity identity, const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs);
241241
void WriteDualChainstateMarker() EXCLUSIVE_LOCKS_REQUIRED(!cs);
242242
bool HasDualChainstateMarker() EXCLUSIVE_LOCKS_REQUIRED(!cs);
243+
//! Undo WriteBestBlock(SNAPSHOT) and WriteDualChainstateMarker(). Needed
244+
//! when snapshot activation is abandoned after those markers were already
245+
//! committed: a stale dual-chainstate marker turns supported legacy
246+
//! bootstrapping into "unavailable history", and a stale SNAPSHOT marker
247+
//! would let a later snapshot directory pass ActivateExistingSnapshot().
248+
void EraseSnapshotMarkers() EXCLUSIVE_LOCKS_REQUIRED(!cs);
243249

244250
bool VerifyBestBlock(const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs) { return VerifyBestBlock(EvoDbIdentity::NORMAL, hash); }
245251
void WriteBestBlock(const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs) { WriteBestBlock(EvoDbIdentity::NORMAL, hash); }

‎src/node/chainstate.cpp‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize
6565
// Load the fully validated chainstate.
6666
chainman.InitializeChainstate(options.mempool, *evodb, chain_helper);
6767

68+
// Wiping the shared EvoDB above erased the SNAPSHOT best-block marker that
69+
// ActivateExistingSnapshot() requires, so a persisted snapshot chainstate can
70+
// no longer be revived. Discard it here rather than letting startup fail with
71+
// advice ("reindex") the user has just followed, which would never recover.
72+
if (to_wipe_data && !DeleteSnapshotChainstateFromDisk()) {
73+
return {ChainstateLoadStatus::FAILURE,
74+
_("Failed to remove the snapshot chainstate directory. Remove it manually before restarting.")};
75+
}
76+
6877
// Load a chain created from a UTXO snapshot, if any exist.
6978
bilingual_str snapshot_error;
7079
if (!chainman.DetectSnapshotChainstate(options.mempool, snapshot_error)) {

‎src/test/evo_db_tests.cpp‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,4 +230,35 @@ BOOST_AUTO_TEST_CASE(open_transaction_does_not_capture_other_threads)
230230
BOOST_CHECK(other_thread == PayloadFor(30));
231231
}
232232

233+
BOOST_AUTO_TEST_CASE(snapshot_markers_can_be_discarded)
234+
{
235+
const fs::path path = m_args.GetDataDirBase() / "evodb_marker_rollback";
236+
{
237+
CEvoDB db{util::DbWrapperParams{.path = path, .memory = false, .wipe = true}};
238+
WriteMarker(db, EvoDbIdentity::SNAPSHOT, BlockHash(40));
239+
{
240+
auto tx = db.BeginTransaction(EvoDbIdentity::SNAPSHOT);
241+
db.WriteDualChainstateMarker();
242+
tx->Commit();
243+
}
244+
BOOST_REQUIRE(db.CommitRootTransaction(EvoDbIdentity::SNAPSHOT));
245+
BOOST_REQUIRE(db.HasDualChainstateMarker());
246+
247+
// Abandoning snapshot activation after the markers were committed must
248+
// leave no trace: a stale dual-chainstate marker breaks legacy
249+
// bootstrapping and a stale best-block marker would revive a future
250+
// snapshot directory.
251+
{
252+
auto tx = db.BeginTransaction(EvoDbIdentity::SNAPSHOT);
253+
db.EraseSnapshotMarkers();
254+
tx->Commit();
255+
}
256+
BOOST_REQUIRE(db.CommitRootTransaction(EvoDbIdentity::SNAPSHOT));
257+
}
258+
CEvoDB reopened{util::DbWrapperParams{.path = path, .memory = false, .wipe = false}};
259+
uint256 hash;
260+
BOOST_CHECK(!reopened.ReadBestBlock(EvoDbIdentity::SNAPSHOT, hash));
261+
BOOST_CHECK(!reopened.HasDualChainstateMarker());
262+
}
263+
233264
BOOST_AUTO_TEST_SUITE_END()

‎src/test/validation_chainstatemanager_tests.cpp‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,4 +833,26 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init_missing_evodb_marker, Sn
833833
this->LoadVerifyActivateChainstate();
834834
}
835835

836+
//! Reindexing wipes the shared EvoDB, taking the SNAPSHOT best-block marker with
837+
//! it. Startup must discard the persisted snapshot chainstate too, or every
838+
//! subsequent start would fail on the erased marker and tell the user to reindex.
839+
BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_discarded_on_reindex, SnapshotTestSetup)
840+
{
841+
this->SetupSnapshot();
842+
BOOST_REQUIRE(node::FindSnapshotChainstateDir());
843+
844+
this->SimulateNodeRestart();
845+
m_args.ForceSetArg("-reindex-chainstate", "1");
846+
this->LoadVerifyActivateChainstate();
847+
m_args.ForceSetArg("-reindex-chainstate", "0");
848+
849+
ChainstateManager& reindexed = *Assert(m_node.chainman);
850+
LOCK(::cs_main);
851+
BOOST_CHECK(!node::FindSnapshotChainstateDir());
852+
BOOST_CHECK(!reindexed.IsSnapshotActive());
853+
BOOST_CHECK_EQUAL(reindexed.GetAll().size(), 1);
854+
uint256 stale_marker;
855+
BOOST_CHECK(!m_node.evodb->ReadBestBlock(EvoDbIdentity::SNAPSHOT, stale_marker));
856+
}
857+
836858
BOOST_AUTO_TEST_SUITE_END()

‎src/validation.cpp‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5448,6 +5448,19 @@ static bool DeleteCoinsDBFromDisk(const fs::path db_path, bool is_snapshot)
54485448
return destroyed && !fs::exists(db_path);
54495449
}
54505450

5451+
bool DeleteSnapshotChainstateFromDisk()
5452+
{
5453+
AssertLockHeld(::cs_main);
5454+
5455+
auto snapshot_datadir = node::FindSnapshotChainstateDir();
5456+
if (!snapshot_datadir) {
5457+
return true;
5458+
}
5459+
LogPrintf("[snapshot] discarding persisted snapshot chainstate at %s\n",
5460+
fs::PathToString(*snapshot_datadir));
5461+
return DeleteCoinsDBFromDisk(*snapshot_datadir, /*is_snapshot=*/true);
5462+
}
5463+
54515464
bool ChainstateManager::ActivateSnapshot(
54525465
AutoFile& coins_file,
54535466
const SnapshotMetadata& metadata,
@@ -5524,6 +5537,21 @@ bool ChainstateManager::ActivateSnapshot(
55245537
LOCK(::cs_main);
55255538
this->MaybeRebalanceCaches();
55265539

5540+
// PopulateAndValidateSnapshot commits the snapshot best-block and
5541+
// dual-chainstate markers as its last step, so a later failure (e.g.
5542+
// WriteSnapshotBaseBlockhash on an unwritable datadir) would leave them
5543+
// behind on a node that is back to a single chainstate. Erasing them is
5544+
// a no-op when they were never written.
5545+
CEvoDB& evo_db = this->ActiveChainstate().m_evoDb;
5546+
{
5547+
auto db_tx = evo_db.BeginTransaction(EvoDbIdentity::SNAPSHOT);
5548+
evo_db.EraseSnapshotMarkers();
5549+
db_tx->Commit();
5550+
}
5551+
if (!evo_db.CommitRootTransaction(EvoDbIdentity::SNAPSHOT)) {
5552+
AbortNode("Failed to roll back the snapshot EvoDB markers");
5553+
}
5554+
55275555
// PopulateAndValidateSnapshot can return (in error) before the leveldb datadir
55285556
// has been created, so only attempt removal if we got that far.
55295557
if (auto snapshot_datadir = node::FindSnapshotChainstateDir()) {

‎src/validation.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,6 +1162,16 @@ MnRewardEra GetMnRewardEraAfter(const CBlockIndex* pindexPrev, const ChainstateM
11621162
*/
11631163
const AssumeutxoData* ExpectedAssumeutxo(const int height, const CChainParams& params);
11641164

1165+
/**
1166+
* Remove a persisted snapshot chainstate's on-disk artifacts: its coins database
1167+
* and the base-blockhash file identifying it. Only valid while no snapshot
1168+
* Chainstate object exists, i.e. at startup before DetectSnapshotChainstate().
1169+
*
1170+
* @returns false only if a snapshot chainstate was found but could not be fully
1171+
* removed; true when there was nothing to remove.
1172+
*/
1173+
bool DeleteSnapshotChainstateFromDisk() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
1174+
11651175
/** Identifies blocks that overwrote an existing coinbase output in the UTXO set (see BIP30) */
11661176
bool IsBIP30Repeat(const CBlockIndex& block_index);
11671177

0 commit comments

Comments
 (0)