Skip to content
4 changes: 4 additions & 0 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2075,6 +2075,10 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
const bool quorums_watch = args.GetBoolArg("-watchquorums", llmq::DEFAULT_WATCH_QUORUMS);
const llmq::QvvecSyncModeMap sync_map{llmq::GetEnabledQuorumVvecSyncEntries(args)};
const util::DbWrapperParams dash_db_params{.path = args.GetDataDirNet(), .memory = false, .wipe = (fReindex || fReindexChainState)};
// TODO(assumeutxo M5): runtime loadtxoutset must recreate/rebind the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this commit is a fix for already merged M3 and it is currently broken at develop somehow, let's have a commit fix: assumeutxo init-order integration (B6) as a new PR

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split out, with one correction to the premise: develop is not actually broken here — DeleteSnapshotChainstateFromDisk() already removes chainstate_snapshot on reindex. What survives are _INVALID and _todelete, plus an ordering wart (cleanup after EvoDB recreation). That standalone piece is now #7588, which also removes the old partial-cleanup function it obsoletes. The prune-lock half of this commit only exists for the completion-time CbTx read introduced later in the series, so it travels with that PR instead of standing alone.


🤖 Posted autonomously by Claude on behalf of pasta.

// Chainstate&-holding ActiveContext signers and NetInstantSend below after
// ChainstateManager switches active chainstates. Startup detection and the
// completion-time ResetChainstates path finish before these are constructed.
if (const auto operator_sk_str = args.GetArg("-masternodeblsprivkey", ""); !operator_sk_str.empty()) {
const CBLSSecretKey operator_sk{ParseHex(operator_sk_str)};
if (!operator_sk.IsValid()) {
Expand Down
6 changes: 6 additions & 0 deletions src/node/blockstorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,12 @@ void BlockManager::UpdatePruneLock(const std::string& name, const PruneLockInfo&
m_prune_locks[name] = lock_info;
}

bool BlockManager::DeletePruneLock(const std::string& name)
{
AssertLockHeld(::cs_main);
return m_prune_locks.erase(name) > 0;
}

CBlockIndex* BlockManager::InsertBlockIndex(const uint256& hash)
{
AssertLockHeld(cs_main);
Expand Down
3 changes: 3 additions & 0 deletions src/node/blockstorage.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ class BlockManager

//! Create or update a prune lock identified by its name
void UpdatePruneLock(const std::string& name, const PruneLockInfo& lock_info) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);

//! Delete a prune lock identified by its name. Returns true if the lock existed.
bool DeletePruneLock(const std::string& name) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
};

void CleanupBlockRevFiles();
Expand Down
31 changes: 31 additions & 0 deletions src/node/chainstate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@
#include <vector>

namespace node {
static bool RemoveSnapshotChainstateArtifacts(const fs::path& data_dir, bilingual_str& error)
{
// Explicit reindexing discards both coins databases and EvoDB. Remove every
// snapshot lifecycle directory at the same time so a directory whose b_dcs*
// markers were wiped cannot be detected as a resumable snapshot below.
for (const auto& name : {"chainstate_snapshot", "chainstate_snapshot_INVALID", "chainstate_todelete"}) {
const fs::path path{data_dir / name};
if (!fs::exists(path)) continue;
try {
fs::remove_all(path);
DirectoryCommit(data_dir);
} catch (const fs::filesystem_error& e) {
error = strprintf(_("Failed to remove snapshot chainstate artifact %s for reindex: %s"),
fs::PathToString(path), e.what());
return false;
}
}
return true;
}

static bool RecoverSnapshotCleanup(CEvoDB& evodb, const fs::path& data_dir, bilingual_str& error)
{
const fs::path normal{data_dir / "chainstate"};
Expand Down Expand Up @@ -189,6 +209,10 @@ static ChainstateLoadResult CompleteChainstateInitialization(ChainstateManager&
return {ChainstateLoadStatus::FAILURE, _("Error loading block database")};
}

// Detection happens before LoadBlockIndex. Once the base is resolvable,
// keep its full block available for Dash's completion-time CbTx check.
chainman.ProtectSnapshotBaseFromPruning();

if (!chainman.BlockIndex().empty() &&
!chainman.m_blockman.LookupBlockIndex(chainman.GetConsensus().hashGenesisBlock)) {
// If the loaded chain has a wrong genesis, bail out immediately
Expand Down Expand Up @@ -326,6 +350,13 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize

LOCK(cs_main);

if (options.reindex || options.reindex_chainstate) {
bilingual_str cleanup_error;
if (!RemoveSnapshotChainstateArtifacts(options.data_dir, cleanup_error)) {
return {ChainstateLoadStatus::FAILURE, cleanup_error};
}
}

evodb.reset();
// TODO: pass DbWrapperParams as options instead multiple params
evodb = std::make_unique<CEvoDB>(util::DbWrapperParams{
Expand Down
13 changes: 13 additions & 0 deletions src/test/blockmanager_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,17 @@ BOOST_FIXTURE_TEST_CASE(blockmanager_scan_unlink_already_pruned_files, TestChain
BOOST_CHECK(!CAutoFile(OpenBlockFile(new_pos, true), SER_DISK, CLIENT_VERSION).IsNull());
}

BOOST_FIXTURE_TEST_CASE(prune_lock_update_and_delete, TestingSetup)
{
LOCK(::cs_main);
auto& chainman{*Assert(m_node.chainman)};
auto& blockman{chainman.m_blockman};

blockman.UpdatePruneLock("test_lock", node::PruneLockInfo{.height_first = 100});
blockman.UpdatePruneLock("test_lock", node::PruneLockInfo{.height_first = 200});
BOOST_CHECK(blockman.DeletePruneLock("test_lock"));
BOOST_CHECK(!blockman.DeletePruneLock("test_lock"));
BOOST_CHECK(!blockman.DeletePruneLock("nonexistent"));
}

BOOST_AUTO_TEST_SUITE_END()
51 changes: 51 additions & 0 deletions src/test/validation_chainstatemanager_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,57 @@ BOOST_AUTO_TEST_CASE(chainstatemanager)
m_node.dmnman.reset();
}

BOOST_AUTO_TEST_CASE(snapshot_startup_missing_base_header_is_nonfatal)
{
ChainstateManager& manager = *m_node.chainman;
Chainstate& background = WITH_LOCK(::cs_main, return manager.InitializeChainstate(
m_node.mempool.get(), *m_node.evodb, m_node.chain_helper));
background.InitCoinsDB(/*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false);
WITH_LOCK(::cs_main, background.InitCoinsCache(1 << 23));
m_node.dmnman = std::make_unique<CDeterministicMNManager>(*m_node.evodb, *Assert(m_node.mn_metaman.get()));
DashChainstateSetup(manager, m_node, /*llmq_dbs_in_memory=*/true, /*llmq_dbs_wipe=*/false);
BOOST_REQUIRE(background.LoadGenesisBlock());

const uint256 missing_base{GetRandHash()};
SeedSnapshotMarker(*m_node.evodb, missing_base);
Chainstate* snapshot = WITH_LOCK(::cs_main, return manager.ActivateExistingSnapshot(
m_node.mempool.get(), missing_base));
BOOST_REQUIRE(snapshot);

// Startup detection is allowed to precede receipt/loading of the base
// header. Accessors and background candidate setup must fail softly.
WITH_LOCK(::cs_main, {
BOOST_CHECK(snapshot->SnapshotBase() == nullptr);
const size_t candidates_before{background.setBlockIndexCandidates.size()};
background.TryAddBlockIndexCandidate(manager.m_blockman.LookupBlockIndex(
manager.GetConsensus().hashGenesisBlock));
BOOST_CHECK_EQUAL(background.setBlockIndexCandidates.size(), candidates_before);
});

DashChainstateSetupClose(m_node);
// dmnman holds a reference to m_node.evodb, it mustn't outlive it
m_node.dmnman.reset();
}

BOOST_FIXTURE_TEST_CASE(snapshot_prune_lock_release_survives_disconnect, TestChain100Setup)
{
ChainstateManager& manager{*Assert(m_node.chainman)};

WITH_LOCK(::cs_main, {
manager.m_blockman.UpdatePruneLock("assumeutxo", {.height_first = manager.ActiveHeight()});
manager.ReleaseSnapshotPruneLock();
BOOST_CHECK(!manager.m_blockman.DeletePruneLock("assumeutxo"));
});

BlockValidationState state;
BOOST_REQUIRE(manager.ActiveChainstate().InvalidateBlock(
state, WITH_LOCK(::cs_main, return manager.ActiveTip())));

// DisconnectTip rewinds every remaining prune lock. The released snapshot
// lock must not be recreated or start constraining pruning after a reorg.
BOOST_CHECK(WITH_LOCK(::cs_main, return !manager.m_blockman.DeletePruneLock("assumeutxo")));
}

//! Test rebalancing the caches associated with each chainstate.
BOOST_FIXTURE_TEST_CASE(chainstatemanager_rebalance_caches, TestChain100Setup)
{
Expand Down
41 changes: 36 additions & 5 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1668,9 +1668,10 @@ std::string Chainstate::EvoDbInconsistencyMessage()
const CBlockIndex* Chainstate::SnapshotBase()
{
if (!m_from_snapshot_blockhash) return nullptr;
// Unlike upstream, a missing base block is not Assert()ed away: synthetic
// unit fixtures activate a snapshot chainstate before inserting its base
// into the block index, and ChainstateManager::LoadBlockIndex() reports a
// Unlike upstream, a missing base block is not Assert()ed away: snapshot
// detection precedes LoadBlockIndex during startup, synthetic unit
// fixtures activate a snapshot chainstate before inserting its base into
// the block index, and ChainstateManager::LoadBlockIndex() reports a
// missing on-disk base as a startup error rather than an abort. Callers
// that require existence Assert at the call site.
if (!m_cached_snapshot_base) m_cached_snapshot_base = m_chainman.m_blockman.LookupBlockIndex(*m_from_snapshot_blockhash);
Expand Down Expand Up @@ -3917,7 +3918,8 @@ void Chainstate::TryAddBlockIndexCandidate(CBlockIndex* pindex)
// For the background chainstate, we only consider connecting blocks
// towards the snapshot base (which can't be nullptr or else we'll
// never make progress).
const CBlockIndex* snapshot_base{Assert(m_chainman.GetSnapshotBaseBlock())};
const CBlockIndex* snapshot_base{m_chainman.GetSnapshotBaseBlock()};
if (!snapshot_base) return;
if (snapshot_base->GetAncestor(pindex->nHeight) == pindex) {
setBlockIndexCandidates.insert(pindex);
}
Expand Down Expand Up @@ -5699,6 +5701,7 @@ bool ChainstateManager::ActivateSnapshot(
}
if (!snapshot_ok) {
LOCK(::cs_main);
this->ReleaseSnapshotPruneLock();
this->MaybeRebalanceCaches();

// PopulateAndValidateSnapshot commits the snapshot best-block and
Expand Down Expand Up @@ -5807,6 +5810,11 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
return false;
}

// Protect the full base block before the long-running population step.
// Snapshot activation is not visible yet, so use the resolved base directly.
WITH_LOCK(::cs_main, m_blockman.UpdatePruneLock(
"assumeutxo", {.height_first = snapshot_start_block->nHeight}));

int base_height = snapshot_start_block->nHeight;
auto maybe_au_data = ExpectedAssumeutxo(base_height, GetParams());

Expand Down Expand Up @@ -6273,6 +6281,7 @@ bool ChainstateManager::HandleSnapshotStateMismatch(
m_ibd_chainstate->m_mempool = m_snapshot_chainstate->m_mempool;
m_snapshot_chainstate->m_mempool = nullptr;
Comment on lines +5790 to +5795

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Preserve the mempool lock through invalid-snapshot cleanup

Background ActivateBestChain enters with LOCK(MempoolMutex()), but that lock is a no-op because the background chainstate has m_mempool == nullptr. HandleSnapshotStateMismatch() then transfers the active snapshot's mempool pointer to the background chainstate and returns to ActivateBestChainStep, whose failed ConnectTip path calls MaybeUpdateMempoolForReorg(). That function now operates on the transferred mempool without its mutex, violating its explicit lock precondition, triggering lock assertions in checked builds, and racing concurrent mempool users otherwise. Defer ownership transfer until ActivateBestChain has unwound, or restructure the failure path so the snapshot mempool mutex remains owned through MaybeUpdateMempoolForReorg and the rest of the failed-step cleanup.

source: ['coderabbit']

m_snapshot_chainstate->m_disabled = true;
ReleaseSnapshotPruneLock();
assert(!IsUsable(m_snapshot_chainstate.get()));
assert(IsUsable(m_ibd_chainstate.get()));
Comment on lines +5790 to +5799

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect MaybeUpdateMempoolForReorg and every call reachable after a failed background ConnectTip.
set -euo pipefail

ast-grep run --pattern 'void Chainstate::MaybeUpdateMempoolForReorg($$$) { $$$ }' --lang cpp src/validation.cpp

rg -nP -C 8 '\bMaybeUpdateMempoolForReorg\s*\(' src/validation.cpp

rg -nP -C 6 '\bMempoolMutex\s*\(' src/validation.cpp src/validation.h

Repository: dashpay/dash

Length of output: 150


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- validation.cpp locations ---'
rg -n -C 12 'HandleSnapshotStateMismatch|MaybeUpdateMempoolForReorg|ActivateBestChainStep|MempoolMutex|m_mempool' src/validation.cpp src/validation.h | head -n 1200

printf '%s\n' '--- relevant source ranges ---'
sed -n '3000,3420p' src/validation.cpp
sed -n '5680,5785p' src/validation.cpp

printf '%s\n' '--- declarations and call sites ---'
rg -n -C 8 'MaybeUpdateMempoolForReorg|HandleSnapshotStateMismatch|ActivateBestChainStep' src --glob '*.{cpp,h}'

Repository: dashpay/dash

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ConnectTip and ActivateBestChainStep ---'
sed -n '3025,3105p' src/validation.cpp
sed -n '3215,3305p' src/validation.cpp
sed -n '3360,3475p' src/validation.cpp

printf '%s\n' '--- snapshot mismatch implementation and callers ---'
rg -n -C 30 'HandleSnapshotStateMismatch|MaybeCompleteSnapshotValidation|ReleaseSnapshotPruneLock|m_active_chainstate|m_ibd_chainstate|m_snapshot_chainstate' src/validation.cpp | tail -n 500

printf '%s\n' '--- lock macro definitions and nullable-lock behavior ---'
rg -n -C 12 '`#define` LOCK|class?UniqueLock|UniqueLock|MempoolMutex\(\)' src sync | head -n 500

Repository: dashpay/dash

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- lock headers and macros ---'
git ls-files | rg '(^|/)(sync|lock|thread).*\.(h|cpp)$|lock'
rg -n -C 15 '`#define` (LOCK|LOCK2|WITH_LOCK)|MakeUniqueLock|UniqueLock<|class UniqueLock|AssertLockHeld' src --glob '*.{h,cpp}' | head -n 1000

printf '%s\n' '--- tests for mismatch and shutdown handoff ---'
rg -n -C 20 'HandleSnapshotStateMismatch|SnapshotStateMismatch|invalid assumeutxo|m_mempool.*nullptr|GetMempool\(\)' src/test --glob '*.{cpp,h}' | head -n 1200

printf '%s\n' '--- exact declarations and construction paths ---'
sed -n '430,510p' src/validation.h
sed -n '1070,1100p' src/validation.h
rg -n -C 15 'MakeChainstate|m_ibd_chainstate\s*=|m_ibd_chainstate\)|new Chainstate|Chainstate\(' src/validation.cpp src/*.cpp src/*.h | head -n 1000

Repository: dashpay/dash

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '175,325p' src/sync.h

printf '%s\n' '--- mismatch-specific tests ---'
rg -n -C 30 'HandleSnapshotStateMismatch|SnapshotStateMismatch|invalid assumeutxo|evo state mismatch' src/test --glob '*.{cpp,h}'

printf '%s\n' '--- snapshot completion handoff and mempool use ---'
sed -n '517,620p' src/evo/snapshot_load.cpp
rg -n -C 12 'm_mempool|MempoolMutex|MaybeUpdateMempoolForReorg' src/evo/snapshot_load.cpp src/test/validation_chainstatemanager_tests.cpp

Repository: dashpay/dash

Length of output: 19783


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

validation = Path("src/validation.cpp").read_text()
sync = Path("src/sync.h").read_text()
header = Path("src/validation.h").read_text()

def section(text, start, end):
    a = text.index(start)
    b = text.index(end, a)
    return text[a:b]

activate = section(validation, "bool Chainstate::ActivateBestChainStep", "static SynchronizationState")
mismatch = section(validation, "bool ChainstateManager::HandleSnapshotStateMismatch", "Chainstate& ChainstateManager::ActiveChainstate")
mempool_mutex = section(header, "RecursiveMutex* MempoolMutex()", "private:")

assert "LOCK(MempoolMutex());" in section(validation, "bool Chainstate::ActivateBestChain", "bool Chainstate::PreciousBlock")
assert "if (!pmutexIn) return;" in sync
assert "m_ibd_chainstate->m_mempool = m_snapshot_chainstate->m_mempool;" in mismatch
assert "m_snapshot_chainstate->m_mempool = nullptr;" in mismatch
assert "MaybeUpdateMempoolForReorg(disconnectpool, false);" in activate

handoff = mismatch.index("m_ibd_chainstate->m_mempool =")
failure_cleanup = activate.index("MaybeUpdateMempoolForReorg(disconnectpool, false);")
assert handoff >= 0 and failure_cleanup >= 0

print("MempoolMutex() returns m_mempool ? &m_mempool->cs : nullptr:", "m_mempool ? &m_mempool->cs : nullptr" in mempool_mutex)
print("LOCK(nullptr) constructs without owning a lock:", "if (!pmutexIn) return;" in sync)
print("Mismatch hands off mempool before failed-step cleanup:", True)
print("ActivateBestChainStep performs failed-step cleanup after ConnectTip:", True)
PY

Repository: dashpay/dash

Length of output: 403


Keep the mempool mutex held across the mempool handoff. LOCK(MempoolMutex()) is a no-op while the background chainstate has m_mempool == nullptr. After HandleSnapshotStateMismatch assigns the snapshot mempool to m_ibd_chainstate, the failed ActivateBestChainStep path calls MaybeUpdateMempoolForReorg without holding that mempool's mutex. Defer the handoff until ActivateBestChain unwinds, or acquire the snapshot mempool mutex before transferring ownership.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/validation.cpp` around lines 5748 - 5757, Update the
snapshot-to-background mempool handoff in HandleSnapshotStateMismatch so
MempoolMutex() remains held across ownership transfer and through the failed
ActivateBestChainStep cleanup path. Acquire the snapshot mempool mutex before
assigning m_snapshot_chainstate->m_mempool to m_ibd_chainstate, or defer the
transfer until ActivateBestChain unwinds; preserve the existing chainstate
shutdown assertions.


Expand Down Expand Up @@ -6406,7 +6415,10 @@ SnapshotCompletionResult ChainstateManager::MaybeCompleteSnapshotValidation(
}

// The base block is necessarily available after background validation
// reaches it. Complete any CbTx checks that could not run at snapshot load.
// reaches it. The assumeutxo prune lock is held until this check completes,
// so the shared BlockManager cannot prune the base out from under the
// snapshot chainstate. Complete any CbTx checks deferred at snapshot load.
assert(index_new.nStatus & BLOCK_HAVE_DATA);
if (DeploymentActiveAt(index_new, GetConsensus(), Consensus::DEPLOYMENT_DIP0003)) {
evo::CEvoSnapshot retained_snapshot;
CBlock base_block;
Expand Down Expand Up @@ -6481,6 +6493,7 @@ SnapshotCompletionResult ChainstateManager::MaybeCompleteSnapshotValidation(
snapshot_blockhash.ToString());

m_ibd_chainstate->m_disabled = true;
ReleaseSnapshotPruneLock();
this->MaybeRebalanceCaches();

return SnapshotCompletionResult::SUCCESS;
Expand Down Expand Up @@ -6608,6 +6621,24 @@ void ChainstateManager::ResetChainstates()
m_active_chainstate = nullptr;
}

void ChainstateManager::ProtectSnapshotBaseFromPruning()
{
AssertLockHeld(::cs_main);
const CBlockIndex* base{GetSnapshotBaseBlock()};
if (!base) return;

// The generic prune-lock buffer makes this conservative: automatic and
// manual pruning both stop below the base, keeping its full block available
// for Dash's deferred CbTx/evo check at background-validation completion.
m_blockman.UpdatePruneLock("assumeutxo", {.height_first = base->nHeight});
}

void ChainstateManager::ReleaseSnapshotPruneLock()
{
AssertLockHeld(::cs_main);
m_blockman.DeletePruneLock("assumeutxo");
}

ChainstateManager::~ChainstateManager()
{
LOCK(::cs_main);
Expand Down
4 changes: 4 additions & 0 deletions src/validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,10 @@ class ChainstateManager

void ResetChainstates() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);

//! Keep the snapshot base block available for deferred Dash evo validation.
void ProtectSnapshotBaseFromPruning() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
void ReleaseSnapshotPruneLock() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);

//! Switch the active chainstate to one based on a UTXO snapshot that was loaded
//! previously.
Chainstate* ActivateExistingSnapshot(CTxMemPool* mempool, uint256 base_blockhash)
Expand Down
11 changes: 11 additions & 0 deletions test/functional/feature_reindex.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"""

import os
from pathlib import Path
from test_framework.test_framework import BitcoinTestFramework
from test_framework.messages import MAGIC_BYTES
from test_framework.util import assert_equal
Expand All @@ -25,9 +26,19 @@ def reindex(self, justchainstate=False, txindex=0):
self.generatetoaddress(self.nodes[0], 3, self.nodes[0].get_deterministic_priv_key().address)
blockcount = self.nodes[0].getblockcount()
self.stop_nodes()
chain_dir = Path(self.nodes[0].datadir) / self.nodes[0].chain
snapshot_artifacts = [
chain_dir / "chainstate_snapshot",
chain_dir / "chainstate_snapshot_INVALID",
chain_dir / "chainstate_todelete",
]
for artifact in snapshot_artifacts:
artifact.mkdir()
(artifact / "stale").touch()
extra_args = [["-reindex-chainstate", "-txindex=0"]] if justchainstate else [["-reindex", f"-txindex={txindex}"]]
self.start_nodes(extra_args)
assert_equal(self.nodes[0].getblockcount(), blockcount) # start_node is blocking on reindex
assert all(not artifact.exists() for artifact in snapshot_artifacts)
self.log.info("Success")

# Check that blocks can be processed out of order
Expand Down