Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/man/dash-qt.1
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ This will also rebuild active optional indexes.
.IP
Rebuild chain state from the currently indexed blocks. When in pruning
mode or if blocks on disk might be corrupted, use full \fB\-reindex\fR
instead. Deactivate all optional indexes before running this.
instead.
.HP
\fB\-spentindex\fR
.IP
Expand Down
2 changes: 1 addition & 1 deletion doc/man/dashd.1
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ This will also rebuild active optional indexes.
.IP
Rebuild chain state from the currently indexed blocks. When in pruning
mode or if blocks on disk might be corrupted, use full \fB\-reindex\fR
instead. Deactivate all optional indexes before running this.
instead.
.HP
\fB\-spentindex\fR
.IP
Expand Down
2 changes: 2 additions & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ BITCOIN_TESTS =\
test/base32_tests.cpp \
test/base58_tests.cpp \
test/base64_tests.cpp \
test/baseindex_tests.cpp \
test/bech32_tests.cpp \
test/bip32_tests.cpp \
test/bip324_tests.cpp \
Expand Down Expand Up @@ -138,6 +139,7 @@ BITCOIN_TESTS =\
test/hash_tests.cpp \
test/httpserver_tests.cpp \
test/i2p_tests.cpp \
test/index_migration_tests.cpp \
test/interfaces_tests.cpp \
test/key_io_tests.cpp \
test/key_tests.cpp \
Expand Down
4 changes: 3 additions & 1 deletion src/index/addressindex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,9 @@ bool AddressIndex::CustomRewind(const interfaces::BlockKey& current_tip, const i
}
}

return true;
CDBBatch locator_batch(GetDB());
WriteRewindLocator(locator_batch, new_tip_index);
return GetDB().WriteBatch(locator_batch);
Comment on lines +401 to +403

@thepastaclaw thepastaclaw Nov 3, 2026 •

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.

✅ Withdrawn at 8a1fa430; see the replies below.

🔴 Blocking: Persist the rewind locator atomically with each rewind batch

The locator update is a separate database write after all per-block rewind writes have completed. Start with chainstate and the index committed at A100, reorg without flushing chainstate or the replacement block index, and terminate the process after RewindBatch() removes A100's entries but before this locator write. Restart can restore A100 as the active tip, while the index still advertises its unchanged A100 locator. BaseIndex::Init() then considers it synced and never rebuilds the deleted history and unspent entries. SpentIndex::CustomRewind() and TimestampIndex::CustomRewind() have the same separation. These are entries for an already committed block, not the newly appended, uncommitted entries covered by the PR's documented limitation. Include the conditionally lowered locator in each block's rewind batch, targeting that block's parent, so every persisted intermediate state remains recoverable. Extend the restart coverage to exercise interruption between rewind batches; the current test completes CustomRewind() before restarting and cannot expose this boundary.

source: gpt-6.1-sol (phase2-reviewer: general)

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.

You're right that the window exists, but this head doesn't introduce or widen it. On v24.0.0-rc.6 (8d195f189d6), the base of this PR, AddressIndex::CustomRewind commits its erasures with RewindBatch(), and BaseIndex::Rewind then writes the locator through a separate Commit(). That is the same erasure-then-locator gap. It's also the same with the Commit() this push dropped. Spent and timestamp indexes work the same way. The window is between two back-to-back LevelDB writes on the same thread, so it needs a process kill at exactly that point.

The same finding is open on #8030 as r4452414669, where the reviewer rated it a non-blocking completeness gap that predates the PR. Writing the locator for each block into that block's rewind batch is on the follow-up list. It needs the database helpers to accept a caller-owned batch, so it changes more than this release backport should. Leaving this thread open for the maintainers to decide, together with the other open #8030 blocker.


🤖 Posted autonomously by Claude on behalf of pasta.

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.

You're right about the baseline: at 8d195f189d6, all three indexes persist their erasures inside CustomRewind(), and only afterward does BaseIndex::Rewind() call the separate Commit() that writes the locator. This head restores that post-rewind locator write without introducing the interruption window I flagged. I overstated this as a blocker for the release backport; atomic per-block locator updates remain a worthwhile follow-up, but I'm withdrawing this finding.

Withdrawing this finding.

}

BaseIndex::DB& AddressIndex::GetDB() const { return *m_db; }
Expand Down
55 changes: 43 additions & 12 deletions src/index/base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

using node::PruneLockInfo;
using node::ReadBlockFromDisk;
using node::g_indexes_ready_to_sync;

constexpr uint8_t DB_BEST_BLOCK{'B'};

Expand Down Expand Up @@ -81,14 +82,22 @@ bool BaseIndex::Init()
if (locator.IsNull()) {
SetBestBlockIndex(nullptr);
} else {
SetBestBlockIndex(m_chainstate->FindForkInGlobalIndex(locator));
// Setting the best block to the locator's top block. If it is not part of the
// best chain, we will rewind to the fork point during index sync
const CBlockIndex* locator_index{m_chainstate->m_blockman.LookupBlockIndex(locator.vHave.at(0))};
if (!locator_index) {
return InitError(strprintf(Untranslated("%s: best block of the index not found. Please rebuild the index."), GetName()));
}
SetBestBlockIndex(locator_index);
}

// Note: this will latch to true immediately if the user starts up with an empty
// datadir and an index enabled. If this is the case, indexation will happen solely
// via `BlockConnected` signals until, possibly, the next restart.
m_synced = m_best_block_index.load() == active_chain.Tip();
if (!m_synced) {

// Skip pruning check if indexes are not ready to sync (because reindex-chainstate has wiped the chain).
if (!m_synced && g_indexes_ready_to_sync) {
bool prune_violation = false;
if (!m_best_block_index) {
// index is not built yet
Expand Down Expand Up @@ -143,6 +152,12 @@ static const CBlockIndex* NextSyncBlock(const CBlockIndex* pindex_prev, CChain&

void BaseIndex::ThreadSync()
{
// Wait for a possible reindex-chainstate to finish until continuing
// with the index sync
while (!g_indexes_ready_to_sync) {
if (!m_interrupt.sleep_for(std::chrono::milliseconds(500))) return;
}

const CBlockIndex* pindex = m_best_block_index.load();
if (!m_synced) {
auto& consensus_params = Params().GetConsensus();
Expand Down Expand Up @@ -215,12 +230,23 @@ void BaseIndex::ThreadSync()
}
}

bool BaseIndex::Commit()
void BaseIndex::Commit()
{
// Don't commit anything if we haven't indexed any block yet
// (this could happen if init is interrupted).
bool ok = m_best_block_index != nullptr;
if (ok) {
// Don't commit if the index best block is not an ancestor of the chainstate's last flushed
// block. Otherwise, after an unclean shutdown, the index could be
// persisted ahead of a chainstate it can no longer roll back to, which
// would corrupt indexes with state (e.g. coinstatsindex).
const CBlockIndex* index_tip = m_best_block_index.load();
const CBlockIndex* last_flushed = WITH_LOCK(::cs_main, return m_chainstate->GetLastFlushedBlock());
if (!last_flushed || last_flushed->GetAncestor(index_tip->nHeight) != index_tip) {
LogPrint(BCLog::COINDB, "Skipping commit, index is ahead of flushed chainstate (index height %d, last flush at height %d)\n",
index_tip->nHeight, last_flushed ? last_flushed->nHeight : -1);
return;
}
CDBBatch batch(GetDB());
ok = CustomCommit(batch);
if (ok) {
Expand All @@ -230,9 +256,19 @@ bool BaseIndex::Commit()
}
if (!ok) {
LogError("%s: Failed to commit latest %s state\n", __func__, GetName());
return false;
}
return true;
}

void BaseIndex::WriteRewindLocator(CDBBatch& batch, const CBlockIndex* new_tip) const
{
CBlockLocator locator;
if (!GetDB().ReadBestBlock(locator) || locator.IsNull()) return;
const CBlockIndex* committed = WITH_LOCK(::cs_main, return m_chainstate->m_blockman.LookupBlockIndex(locator.vHave.front()));
// The committed tip was an ancestor of a flushed block when it was written, so it and new_tip below it are in
// the block index after a restart. A committed tip at or below new_tip, or on another branch, needs no change.
if (committed && committed->nHeight > new_tip->nHeight && committed->GetAncestor(new_tip->nHeight) == new_tip) {
GetDB().WriteBestBlock(batch, GetLocator(*m_chain, new_tip->GetBlockHash()));
}
}

bool BaseIndex::Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_tip)
Expand All @@ -244,18 +280,13 @@ bool BaseIndex::Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_ti
return false;
}

// In the case of a reorg, ensure persisted block locator is not stale.
// Don't commit here - the committed index state must never be ahead of the
// flushed chainstate, otherwise unclean restarts would lead to index corruption.
Comment thread
PastaPastaPasta marked this conversation as resolved.
Resolved
// Pruning has a minimum of 288 blocks-to-keep and getting the index
// out of sync may be possible but a users fault.
// In case we reorg beyond the pruned depth, ReadBlockFromDisk would
// throw and lead to a graceful shutdown
SetBestBlockIndex(new_tip);
if (!Commit()) {
// If commit fails, revert the best block index to avoid corruption.
SetBestBlockIndex(current_tip);
return false;
}

return true;
}

Expand Down
17 changes: 9 additions & 8 deletions src/index/base.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,10 @@ class BaseIndex : public CValidationInterface
void ThreadSync();

/// Write the current index state (eg. chain block locator and subclass-specific items) to disk.
///
/// Recommendations for error handling:
/// If called on a successor of the previous committed best block in the index, the index can
/// continue processing without risk of corruption, though the index state will need to catch up
/// from further behind on reboot. If the new state is not a successor of the previous state (due
/// to a chain reorganization), the index must halt until Commit succeeds or else it could end up
/// getting corrupted.
bool Commit();
/// Will skip the commit if no block has been indexed yet or if the index's best block is
/// ahead of the chainstate's last flushed block. This avoids persisting state an unclean shutdown
/// could not roll back from. A later call commits when the chainstate has flushed far enough.
void Commit();

/// Loop over disconnected blocks and call CustomRewind.
bool Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_tip);
Expand Down Expand Up @@ -121,6 +117,11 @@ class BaseIndex : public CValidationInterface
/// be an ancestor of the current best block.
[[nodiscard]] virtual bool CustomRewind(const interfaces::BlockKey& current_tip, const interfaces::BlockKey& new_tip) { return true; }

/// For indexes whose CustomRewind() erases entries: if the committed locator is above new_tip on the same
/// branch, move it to new_tip. Commit() may not write new_tip yet (it can be ahead of the last flush), but
/// leaving the old locator above erased entries would make an unclean restart resume past them.
void WriteRewindLocator(CDBBatch& batch, const CBlockIndex* new_tip) const;

virtual DB& GetDB() const = 0;

/// Get the name of the index for display in logs.
Expand Down
4 changes: 3 additions & 1 deletion src/index/spentindex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@ bool SpentIndex::CustomRewind(const interfaces::BlockKey& current_tip, const int
}
}

return true;
CDBBatch locator_batch(GetDB());
WriteRewindLocator(locator_batch, new_tip_index);
return GetDB().WriteBatch(locator_batch);
}

BaseIndex::DB& SpentIndex::GetDB() const { return *m_db; }
Expand Down
4 changes: 3 additions & 1 deletion src/index/timestampindex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ bool TimestampIndex::CustomRewind(const interfaces::BlockKey& current_tip, const
}
}

return true;
CDBBatch locator_batch(GetDB());
WriteRewindLocator(locator_batch, new_tip_index);
return GetDB().WriteBatch(locator_batch);
}

BaseIndex::DB& TimestampIndex::GetDB() const { return *m_db; }
Expand Down
19 changes: 5 additions & 14 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ using node::CalculateCacheSizes;
using node::DEFAULT_PERSIST_MEMPOOL;
using node::DEFAULT_PRINTPRIORITY;
using node::DEFAULT_STOPAFTERBLOCKIMPORT;
using node::g_indexes_ready_to_sync;
using node::LoadChainstate;
using node::MempoolPath;
using node::ShouldPersistMempool;
Expand Down Expand Up @@ -639,7 +640,7 @@ void SetupServerArgs(ArgsManager& argsman)

argsman.AddArg("-addressindex", strprintf("Maintain a full address index, used to query for the balance, txids and unspent outputs for addresses (default: %u)", DEFAULT_ADDRESSINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING);
argsman.AddArg("-reindex", "Rebuild chain state and block index from the blk*.dat files on disk. This will also rebuild active optional indexes.", ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING);
argsman.AddArg("-reindex-chainstate", "Rebuild chain state from the currently indexed blocks. When in pruning mode or if blocks on disk might be corrupted, use full -reindex instead. Deactivate all optional indexes before running this.", ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING);
argsman.AddArg("-reindex-chainstate", "Rebuild chain state from the currently indexed blocks. When in pruning mode or if blocks on disk might be corrupted, use full -reindex instead.", ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING);
argsman.AddArg("-spentindex", strprintf("Maintain a full spent index, used to query the spending txid and input index for an outpoint (default: %u)", DEFAULT_SPENTINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING);
argsman.AddArg("-timestampindex", strprintf("Maintain a timestamp index for block hashes, used to query blocks hashes by a range of timestamps (default: %u)", DEFAULT_TIMESTAMPINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING);
argsman.AddArg("-txindex", strprintf("Maintain a full transaction index, used by the getrawtransaction rpc call (default: %u)", DEFAULT_TXINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING);
Expand Down Expand Up @@ -1372,19 +1373,6 @@ bool AppInitParameterInteraction(const ArgsManager& args)
if (args.GetBoolArg("-peerbloomfilters", DEFAULT_PEERBLOOMFILTERS))
nLocalServices = ServiceFlags(nLocalServices | NODE_BLOOM);

if (args.GetBoolArg("-reindex-chainstate", false)) {
// indexes that must be deactivated to prevent index corruption, see #24630
if (args.GetBoolArg("-coinstatsindex", DEFAULT_COINSTATSINDEX)) {
return InitError(_("-reindex-chainstate option is not compatible with -coinstatsindex. Please temporarily disable coinstatsindex while using -reindex-chainstate, or replace -reindex-chainstate with -reindex to fully rebuild all indexes."));
}
if (g_enabled_filter_types.count(BlockFilterType::BASIC_FILTER)) {
return InitError(_("-reindex-chainstate option is not compatible with -blockfilterindex. Please temporarily disable blockfilterindex while using -reindex-chainstate, or replace -reindex-chainstate with -reindex to fully rebuild all indexes."));
}
if (args.GetBoolArg("-txindex", DEFAULT_TXINDEX)) {
return InitError(_("-reindex-chainstate option is not compatible with -txindex. Please temporarily disable txindex while using -reindex-chainstate, or replace -reindex-chainstate with -reindex to fully rebuild all indexes."));
}
}

try {
const bool fQuorumVvecRequestsEnabled{llmq::GetEnabledQuorumVvecSyncEntries(args).size() > 0};
if (!args.GetBoolArg("-llmq-data-recovery", llmq::DEFAULT_ENABLE_QUORUM_DATA_RECOVERY) && fQuorumVvecRequestsEnabled) {
Expand Down Expand Up @@ -2283,6 +2271,9 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
node.peerman->AddExtraHandler(std::make_unique<SyncManager>(node.peerman.get(), *node.govman, *node.mn_sync, *node.connman, *node.netfulfilledman));

// ********************************************************* Step 8: start indexers

// If reindex-chainstate was specified, delay syncing indexes until ThreadImport has reindexed the chain
if (!fReindexChainState) g_indexes_ready_to_sync = true;
if (args.GetBoolArg("-txindex", DEFAULT_TXINDEX)) {
g_txindex = std::make_unique<TxIndex>(interfaces::MakeChain(node), cache_sizes.tx_index, false, fReindex);
if (!g_txindex->Start()) {
Expand Down
2 changes: 2 additions & 0 deletions src/node/blockstorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ std::atomic_bool fImporting(false);
std::atomic_bool fReindex(false);
bool fPruneMode = false;
uint64_t nPruneTarget = 0;
std::atomic_bool g_indexes_ready_to_sync{false};

bool CBlockIndexWorkComparator::operator()(const CBlockIndex* pa, const CBlockIndex* pb) const
{
Expand Down Expand Up @@ -961,5 +962,6 @@ void ThreadImport(ChainstateManager& chainman, std::vector<fs::path> vImportFile
}
} // End scope of CImportingNow
chainman.ActiveChainstate().LoadMempool(mempool_path);
g_indexes_ready_to_sync = true;
}
} // namespace node
1 change: 1 addition & 0 deletions src/node/blockstorage.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ extern std::atomic_bool fImporting;
extern std::atomic_bool fReindex;
extern bool fPruneMode;
extern uint64_t nPruneTarget;
extern std::atomic_bool g_indexes_ready_to_sync;

// Because validation code takes pointers to the map's CBlockIndex objects, if
// we ever switch to another associative container, we need to either use a
Expand Down
Loading
Loading