Skip to content

indexes: Don't commit ahead of the flushed chainstate#34897

Merged
achow101 merged 5 commits into
bitcoin:masterfrom
mzumsande:202603_index_sync_dont_commit_ahead
Jul 9, 2026
Merged

indexes: Don't commit ahead of the flushed chainstate#34897
achow101 merged 5 commits into
bitcoin:masterfrom
mzumsande:202603_index_sync_dont_commit_ahead

Conversation

@mzumsande

@mzumsande mzumsande commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

If indexes commit their data ahead of the flushed chainstate, and there is an unclean shutdown, the index will be corrupted. This is especially the case for the coinstatsindex, which has state (the muhash) which can't easily be rolled back without access to the blocks. This was only partly fixed in #33212 (for reorg scenarios) but could still happen during initial sync.

Fix this more thoroughly by having the node keep track of the last flushed block, and skipping index commits if the current block of the index is not an ancestor of the node's last flushed block (similar to the suggestion by stickies-v in #33212 (review).

Fixes #33208
Fixes #34261

@DrahtBot

DrahtBot commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34897.

Reviews

See the guideline and AI policy for information on the review process.

Type Reviewers
ACK sedited, fjahr, achow101
Concept ACK l0rinc, arejula27
Stale ACK furszy, ryanofsky

If your review is incorrectly listed, please copy-paste <!--meta-tag:bot-skip--> into the comment that the bot should ignore.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #35570 (refactor: Change some validation.cpp methods to return BlockValidationState by optout21)
  • #35465 (coins: compact chainstate regularly by l0rinc)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

LLM Linter (✨ experimental)

Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

  • sync_index(false, 100, 100) in src/test/baseindex_tests.cpp
  • sync_index(false, 101, 100) in src/test/baseindex_tests.cpp

2026-07-09 14:22:11

@fjahr

fjahr commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

Concept ACK

@l0rinc

l0rinc commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

I have reported something similar in #34489 (comment) and #34489 (comment) - will this PR fix those?
Concept ACK regardless, thanks for fixing them!

@arejula27 arejula27 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Concept ACK [5af4d6e]

However, I'm concerned about a potential starvation scenario during IBD. Two threads run in parallel: the validation thread connects blocks and periodically flushes the UTXO set to disk (~1h intervals or when the cache is full), while the index thread (Sync()) processes already-connected blocks and calls Commit() every 30 seconds. Since index processing (filters, tx lookups) is significantly lighter than UTXO set updates, the index will typically stay ahead of the flush point, even though it's always behind the chain tip.

This means that during IBD, every Commit() call could be skipped because the index is perpetually ahead of m_last_flushed_block. A crash at any point would lose all indexing progress back to the initial LoadChainTip value, potentially hours of work. I will run experiments to verify whether this actually happens and propose a solution in that case.

I also suggest discussing an alternative approach for index commits: committing the locator up to the flushed block instead of skipping entirely. For example, if the index has processed up to block 5000 but the last flush was at block 3000, the locator could be written at 3000. This would preserve the safety invariant (locator never exceeds the flush point) while still persisting progress. That said, this would require rethinking how Commit() and CustomCommit() work across index subclasses.

This request also concerns me from a performance perspective: if index commit progress is now bounded by the chainstate flush interval, any index-side speed optimization becomes less impactful. PRs like #34489 (batch DB writes) and #26966 (parallelized index sync) focus on making indexes faster, but with this change the bottleneck shifts to how often the chainstate flushes, something entirely outside the index's control. I would love to see those optimizations land in Core, so it would be worth ensuring this safety fix doesn't inadvertently cap their benefits.

Comment thread src/index/base.cpp Outdated
Comment thread src/test/coinstatsindex_tests.cpp
Comment thread src/index/base.cpp Outdated

@mzumsande mzumsande left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

However, I'm concerned about a potential starvation scenario during IBD. Two threads run in parallel: the validation thread connects blocks and periodically flushes the UTXO set to disk (~1h intervals or when the cache is full), while the index thread (Sync()) processes already-connected blocks and calls Commit() every 30 seconds. Since index processing (filters, tx lookups) is significantly lighter than UTXO set updates, the index will typically stay ahead of the flush point, even though it's always behind the chain tip.

The Sync thread runs only temporary, when the index is behind the chainstate (e.g. newly started on a node that was running without an index before). For example, if a new node starts IBD with an index, Sync will stop immediately and never do any work.
As soon as the index has caught up with the tip, the Sync thread is stopped and we use validation signals to keep on syncing. Once that happens, the index will commit with each ChainstateFlushed validation callback, and since validation signals are processed in order, I don't think its best block can ever be ahead of the flushed chainstate then.

So I don't think the problem you describe is an issue. Commits will only be skipped in the limited time span where the Sync thread is almost caught up, due to additional blocks being connected during the earlier sync. This should be a relatively short period. They won't be skipped if the index is catching up from far behind, or once the index has reached the tip.

This request also concerns me from a performance perspective

Commit doesn't do any significant work, this is being done in ProcessBlock(), which will write things to disk independently. Commit only writes the data required to know where we should pick up at the next start (including state data such as the current MuHash of the coinstatsindex:
If we don't Commit and have an unclean restart, we might have to do some work again in case of an unclean restart, if we do Commit ahead of the tip and have an unclean restart, the index is corrupted.

I'll address the other comments later.

@ryanofsky

Copy link
Copy Markdown
Contributor

Concept ACK 5af4d6e. Good to follow up on this issue.

I feel like this could skip adding the isBlockInFlushedChain method and access the chainstate directly since #24230 moves sync thread from the node to the index. But either way seems fine.

I also implemented a similar fix in 6537f96 from #24230 which has some differences like the check being done inside Sync() instead of Commit(). Not sure if the differences are significant though.

@furszy furszy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ACK 5af4d6e , left a few suggestions. Nothing blocking.

Comment thread src/validation.cpp Outdated
Comment on lines +2814 to +2816
// Flush the chainstate (which may refer to block index entries).
empty_cache ? CoinsTip().Flush() : CoinsTip().Sync();
m_last_flushed_block = m_chain.Tip();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Coming from the lack of m_chain usage in this function, I'm wondering if we should add a sanity-check here?

Assume(m_last_flushed_block->GetBlockHash() == CoinsTip().GetBestBlock());

or.. maybe directly use

m_last_flushed_block =  m_blockman.LookupBlockIndex(CoinsTip().GetBestBlock());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

489bb4a validation: track last flushed block:

Could this new ChainStateFlushed locator come from the recorded flushed block instead of rereading m_chain.Tip()?

diff --git a/src/validation.cpp b/src/validation.cpp
--- a/src/validation.cpp	(revision 30c92e346762ec5d046d81d46d08811ccd9e26b3)
+++ b/src/validation.cpp	(revision 2296c4e0101ee7de230478d0dd7323b04a849fe4)
@@ -2831,7 +2831,7 @@
     }
     if (full_flush_completed && m_chainman.m_options.signals) {
         // Update best block in wallet (so we can detect restored wallets).
-        m_chainman.m_options.signals->ChainStateFlushed(this->GetRole(), GetLocator(m_chain.Tip()));
+        m_chainman.m_options.signals->ChainStateFlushed(this->GetRole(), GetLocator(Assert(m_last_flushed_block)));
     }
     } catch (const std::runtime_error& e) {
         return FatalError(m_chainman.GetNotifications(), state, strprintf(_("System error while flushing: %s"), e.what()));

@mzumsande mzumsande Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added the suggested sanity check, and reused m_last_flushed. Note that we did use m_chain for the ChainStateFlushed() notification there.
However, I just noticed that some unit tests (validation_chainstate_resize_caches) don't populate m_chain and fail with the Assume. For now I have fixed this by making it conditional to a tip existing - I'll look later into changing the test instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Still doesn't work. The Assume is also hit by the validation_chainstatemanager_tests which manually changes the tip to be lagging behind the CoinsTip()- this is a dirty hack to make AssumeUtxo testable in unit tests (Snapshots will only activate when they have more work than the tip...).
I have removed the Assume() for now. Changing it to CoinsTip() would work, but then we should probably also change the notification. Would you prefer that?

@l0rinc l0rinc Jun 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@furszy is probably correct, the cached boundary should reflect the coins view that was actually flushed. But the ChainStateFlushed() locator change I asked about should probably stay as-is.

I understand that during IBD we can't have reorgs, but during steady state DisconnectTip(), the temporary coins view is flushed back to pindexDelete->pprev before the active tip is updated, so m_chain.Tip() can still name the disconnected block while CoinsTip().GetBestBlock() already names its parent:

struct TestChain100SetupNoMempoolHeadroom : TestChain100Setup {
    TestChain100SetupNoMempoolHeadroom() : TestChain100Setup{ChainType::REGTEST, TestOpts{.extra_args = {"-maxmempool=0"}}} {}
};

BOOST_FIXTURE_TEST_CASE(flushed_boundary_uses_disconnected_coins_tip, TestChain100SetupNoMempoolHeadroom)
{
    Chainstate& chainstate{Assert(m_node.chainman)->ActiveChainstate()};

    const CBlockIndex* tip{Assert(WITH_LOCK(::cs_main, return chainstate.m_chain.Tip()))};
    const CBlockIndex* parent{tip->pprev};

    {
        LOCK2(m_node.chainman->GetMutex(), chainstate.MempoolMutex());
        chainstate.m_coinstip_cache_size_bytes = 0;
        BlockValidationState state{};
        BOOST_REQUIRE(chainstate.DisconnectTip(state, nullptr));
    }

    BOOST_CHECK_EQUAL(WITH_LOCK(::cs_main, return chainstate.m_chain.Tip()), parent);
    BOOST_CHECK_EQUAL(WITH_LOCK(::cs_main, return chainstate.GetLastFlushedBlock()), parent);
    m_node.validation_signals->SyncWithValidationInterfaceQueue();
}

The test fails currently because GetLastFlushedBlock() records the disconnected tip instead of its parent:

m_last_flushed_block = m_blockman.LookupBlockIndex(CoinsTip().GetBestBlock());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That is an interesting situation, which I think is a bug in master. So the issue is that we call FlushStateToDisk(state, FlushStateMode::IF_NEEDED)) in DisconnectTip() after disconnecting the coins, but before setting the new tip (the IF_NEEDED makes this relatively unlikely, but it is still possible).

But the ChainStateFlushed() locator change I asked about should probably stay as-is.

why? It gives the wrong locator for the notification in this situation.
I changed it to use CoinsTip().GetBestBlock() for the last flushed block and also added a commit at the end to no longer use m_chain.Tip() for the existing ChainStateFlushed notification.

Comment thread src/test/coinstatsindex_tests.cpp Outdated
Comment thread src/test/coinstatsindex_tests.cpp Outdated
Comment thread src/test/coinstatsindex_tests.cpp Outdated
Comment thread src/index/base.cpp Outdated
// Don't commit if the index is ahead of the chainstate's last flushed block
const CBlockIndex* index_tip = m_best_block_index.load();
if (index_tip && !m_chain->isBlockInFlushedChain(index_tip->GetBlockHash(), index_tip->nHeight)) {
LogInfo("Skipping commit, index is ahead of flushed chainstate");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: Could log where the index is at vs the chain height, so the diff gets logged.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Also LogInfo on every skip seems a bit excessive. Maybe make this a debug log?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added the heights. It should be quite rare though (only happens at the end of Sync), so I'm not sure if it's worth adding a new debug log category for indexes just for this.

Comment thread src/index/base.cpp Outdated
Comment thread src/index/base.cpp Outdated

@arejula27 arejula27 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ACK 5af4d6e

I ran the experiments I suggested in my previous Concept ACK.

The skip path does trigger, but the loss window is bounded by 30s, which is negligible compared to a multi-day IBD, so the changes are correct.

I wopudl encorague again to add a few more tests:

  • Reorg scenario: Verify behavior when the index is on one fork and the flushed chainstate is on another.
  • Recovery: Verify the index resumes from the last chainstate flush point. I think will be interesting to verify the corret initialization behaviour of isBlockInFlushedChain

@sedited

sedited commented May 29, 2026

Copy link
Copy Markdown
Contributor

@mzumsande There's a silent merge conflict here:

/home/drgrid/bitcoin/src/test/coinstatsindex_tests.cpp:154:72: error: too many arguments to function call, expected 2, have 3
  154 |             const CBlock block = this->CreateBlock({}, script_pub_key, chainstate);
      |                                  ~~~~~~~~~~~~~~~~~                     ^~~~~~~~~~
/home/drgrid/bitcoin/src/test/util/setup_common.h:153:12: note: 'CreateBlock' declared here
  153 |     CBlock CreateBlock(
      |            ^
  154 |         const std::vector<CMutableTransaction>& txns,
      |         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  155 |         const CScript& scriptPubKey);
      |         ~~~~~~~~~~~~~~~~~~~~~~~~~~~

@fanquake fanquake added this to the 32.0 milestone May 29, 2026

@fjahr fjahr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ACK 5af4d6e

Would be ok for me to merge as-is but I think particularly @furszy 's comments here and here would be good to address. Maybe some of mine as well but I don't consider them strictly blocking and I would open a follow-up if they are not addressed. I will re-review quickly if you decide to address the open comments.

EDIT: Never mind, I guess you have to push anyway due to the silent merge conflict.

Comment thread src/validation.h Outdated
Comment thread src/index/base.cpp Outdated
// Don't commit if the index is ahead of the chainstate's last flushed block
const CBlockIndex* index_tip = m_best_block_index.load();
if (index_tip && !m_chain->isBlockInFlushedChain(index_tip->GetBlockHash(), index_tip->nHeight)) {
LogInfo("Skipping commit, index is ahead of flushed chainstate");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Also LogInfo on every skip seems a bit excessive. Maybe make this a debug log?

Comment thread src/node/interfaces.cpp Outdated
Comment thread src/index/base.cpp Outdated
Comment thread src/test/coinstatsindex_tests.cpp Outdated
Comment thread src/test/coinstatsindex_tests.cpp Outdated
@DrahtBot DrahtBot requested a review from arejula27 May 29, 2026 22:09

@l0rinc l0rinc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Left a few more comments - given the other comments and the silent merge conflict.
It would also be good to confirm whether the same unsafe restart boundary applies to other indexes using eager write paths, such as BlockFilterIndex.

Comment thread src/test/coinstatsindex_tests.cpp Outdated
Comment thread src/test/coinstatsindex_tests.cpp Outdated
// flushed block. If it did, a subsequent unclean shutdown would corrupt
// the index, because during reverting it would require blocks that were
// never flushed to disk.
BOOST_FIXTURE_TEST_CASE(coinstatsindex_no_commit_ahead_of_flush, TestChain100Setup)

@l0rinc l0rinc May 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

5af4d6e test: add test to ensure indexes dont commit too early:

This seems like a good example for #35260 - it would help me with the review to know what the state was before the change and which commit changes the assumption and how and where it changes it.
Maybe something like this documenting the state before the fix:

BOOST_FIXTURE_TEST_CASE(coinstatsindex_commits_ahead_of_chainstate_flush, TestChain100Setup)
{
    Chainstate& chainstate = Assert(m_node.chainman)->ActiveChainstate();
    BlockValidationState state;

    auto current_tip{[&] { return Assert(WITH_LOCK(::cs_main, return chainstate.m_chain.Tip())); }};
    auto coinsdb_best_is{[&](const CBlockIndex& block) {
        LOCK(::cs_main);
        BOOST_CHECK(chainstate.CoinsDB().GetBestBlock() == block.GetBlockHash());
    }};
    auto sync_fresh_index{[&](int expected_height) {
        CoinStatsIndex index{interfaces::MakeChain(m_node), /*n_cache_size=*/1_MiB, /*f_memory=*/false, /*f_wipe=*/true};
        BOOST_REQUIRE(index.Init());
        index.Sync();
        BOOST_CHECK_EQUAL(index.GetSummary().best_block_height, expected_height);
        index.Stop();
    }};
    auto read_persisted_index_height{[&] {
        CoinStatsIndex index{interfaces::MakeChain(m_node), /*n_cache_size=*/1_MiB};
        BOOST_REQUIRE(index.Init());
        const int height{index.GetSummary().best_block_height};
        index.Stop();
        return height;
    }};
    auto flush_and_commit_index{[&] {
        CoinStatsIndex index{interfaces::MakeChain(m_node), /*n_cache_size=*/1_MiB};
        BOOST_REQUIRE(index.Init());
        index.Sync();
        BOOST_REQUIRE(chainstate.FlushStateToDisk(state, FlushStateMode::FORCE_FLUSH));
        m_node.validation_signals->SyncWithValidationInterfaceQueue();
        index.Stop();
    }};
    // For one unflushed index tip, check the durable height before and after the chainstate catches up.
    auto check_unflushed_index_commit{[&](const CBlockIndex& index_tip, int expected_persisted_height) {
        sync_fresh_index(index_tip.nHeight);
        BOOST_CHECK_EQUAL(read_persisted_index_height(), expected_persisted_height);
        flush_and_commit_index();
        BOOST_CHECK_EQUAL(read_persisted_index_height(), index_tip.nHeight);
    }};

    // Start from a known durable chainstate boundary.
    BOOST_REQUIRE(chainstate.FlushStateToDisk(state, FlushStateMode::FORCE_FLUSH));
    auto* flushed_tip{current_tip()};
    coinsdb_best_is(*flushed_tip);

    // Straight-line case: connect one more block without flushing the chainstate.
    CreateAndProcessBlock({}, CScript() << OP_TRUE);
    auto* unflushed_tip{current_tip()};
    BOOST_REQUIRE_GT(unflushed_tip->nHeight, flushed_tip->nHeight);
    coinsdb_best_is(*flushed_tip);

    check_unflushed_index_commit(*unflushed_tip, /*expected_persisted_height=*/unflushed_tip->nHeight); // TODO: Persists ahead of the flushed chainstate.

    auto* reorg_flushed_tip{current_tip()};
    coinsdb_best_is(*reorg_flushed_tip);

    // Fork case: leave the flushed chainstate behind, then index a competing unflushed tip.
    BOOST_REQUIRE(chainstate.InvalidateBlock(state, reorg_flushed_tip));
    NodeClockContext clock_ctx{};
    clock_ctx += 1s;
    CreateAndProcessBlock({}, CScript() << OP_TRUE);
    clock_ctx += 1s;
    CreateAndProcessBlock({}, CScript() << OP_TRUE);

    auto* fork_tip{current_tip()};
    BOOST_REQUIRE_EQUAL(fork_tip->nHeight, reorg_flushed_tip->nHeight + 1);
    BOOST_CHECK(fork_tip->GetAncestor(reorg_flushed_tip->nHeight) != reorg_flushed_tip);
    coinsdb_best_is(*reorg_flushed_tip);

    check_unflushed_index_commit(*fork_tip, /*expected_persisted_height=*/fork_tip->nHeight); // TODO: Persists a fork from the flushed chainstate.
}

so that the commit fixing it would have a minimal diff of what it affects exactly:

diff --git a/src/test/coinstatsindex_tests.cpp b/src/test/coinstatsindex_tests.cpp
--- a/src/test/coinstatsindex_tests.cpp	(revision 7177975c1d50fd4e0164fa6093a06b68302f0115)
+++ b/src/test/coinstatsindex_tests.cpp	(revision e8197142a66c98e72d2f8c6f138f4e29a9d83b94)
@@ -124,7 +124,7 @@
     }
 }
 
-BOOST_FIXTURE_TEST_CASE(coinstatsindex_commits_ahead_of_chainstate_flush, TestChain100Setup)
+BOOST_FIXTURE_TEST_CASE(coinstatsindex_commit_is_limited_by_chainstate_flush, TestChain100Setup)
 {
     Chainstate& chainstate = Assert(m_node.chainman)->ActiveChainstate();
     BlockValidationState state;
@@ -175,7 +175,7 @@
     BOOST_REQUIRE_GT(unflushed_tip->nHeight, flushed_tip->nHeight);
     coinsdb_best_is(*flushed_tip);
 
-    check_unflushed_index_commit(*unflushed_tip, /*expected_persisted_height=*/unflushed_tip->nHeight); // TODO: Persists ahead of the flushed chainstate.
+    check_unflushed_index_commit(*unflushed_tip, /*expected_persisted_height=*/0);
 
     auto* reorg_flushed_tip{current_tip()};
     coinsdb_best_is(*reorg_flushed_tip);
@@ -193,7 +193,7 @@
     BOOST_CHECK(fork_tip->GetAncestor(reorg_flushed_tip->nHeight) != reorg_flushed_tip);
     coinsdb_best_is(*reorg_flushed_tip);
 
-    check_unflushed_index_commit(*fork_tip, /*expected_persisted_height=*/fork_tip->nHeight); // TODO: Persists a fork from the flushed chainstate.
+    check_unflushed_index_commit(*fork_tip, /*expected_persisted_height=*/0);
 }
 
 BOOST_AUTO_TEST_SUITE_END()

@mzumsande mzumsande Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have tried this approach in the past, I prefer to add the full test (which can be cherry-picked on master to see the failure). Otherwise, it adds a lot of churn to check every comment etc. if it already made sense before, without the context of the fix. But I did rewrite the unit test, it should be very obvious how it fails on master.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

re: #34897 (comment)

Agree with l0rinc, there is no significant churn here and adding the test first would make the bugfix commit easier to understand, and avoid the need for reviewers to do manual work to verify the test meaningfully checks the changed behavior, when this could be checked by the "test ancestor commits" CI job.

My diff to confirm the test was:

--- b/src/test/coinstatsindex_tests.cpp
+++ a/src/test/coinstatsindex_tests.cpp
@@ -145,7 +145,7 @@ BOOST_FIXTURE_TEST_CASE(coinstatsindex_no_commit_ahead_of_flush, TestChain100Set
     {
         CoinStatsIndex index{interfaces::MakeChain(m_node), /*n_cache_size=*/1_MiB};
         BOOST_REQUIRE(index.Init());
-        BOOST_CHECK_EQUAL(index.GetSummary().best_block_height, 100);
+        BOOST_CHECK_EQUAL(index.GetSummary().best_block_height, 0);
         index.Stop();
     }
 
@@ -182,7 +182,7 @@ BOOST_FIXTURE_TEST_CASE(coinstatsindex_no_commit_ahead_of_flush, TestChain100Set
     {
         CoinStatsIndex index{interfaces::MakeChain(m_node), /*n_cache_size=*/1_MiB};
         BOOST_REQUIRE(index.Init());
-        BOOST_CHECK_EQUAL(index.GetSummary().best_block_height, 101);
+        BOOST_CHECK_EQUAL(index.GetSummary().best_block_height, 100);
         index.Stop();
     }
 }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I have tried this approach in the past, I prefer to add the full test (which can be cherry-picked on master to see the failure). Otherwise, it adds a lot of churn to check every comment etc.

That's exactly what this technique is solving: you don't need to cherry-pick on master, because you're already proving that the fix makes you adjust the test for it to pass. The CI already proved that the previous version passed and that the modified version also passes with the fix - no need to cherry-pick back and forth.


Also, the current regression coverage primarily demonstrates that the index will not commit if it is ahead by height. Could we extend it with a same-height competing block case, so a height-only check would fail and the ancestry requirement is tested directly?
That would make it clear that the gate is not only "height <= flushed height", but "same block or ancestor of the flushed block".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok, I changed it. There is still churn: Now I had to separate out comments that refer to the last flushed block, since they make no sense in a commit before it was introduced. Whenever I will add things to the test, I will likely have to resolve conflicts since it's now spread over two commits. Also, ideally, one should not just have a succeeding texts but also add motivating comments in the original test why the behavior is not ideal and should be fixed (and then delete these comments when fixed), which would create even more churn.

So for the future I definitely prefer to have less churn in the git history and instead have reviewers cherry-pick (and, as a frequent reviewer myself, have absolutely no problem doing that).

Comment thread src/index/base.cpp Outdated
if (ok) {
// Don't commit if the index is ahead of the chainstate's last flushed block
const CBlockIndex* index_tip = m_best_block_index.load();
if (index_tip && !m_chain->isBlockInFlushedChain(index_tip->GetBlockHash(), index_tip->nHeight)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

7933e27 index: Don't commit ahead of the flushed chainstate:

coinstatsindex doesn't seem to be only index with a similar race, see: #34489 (comment)

Can we cover it in this PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All indexes are affected by it because we don't have a special logic for indexes without state. I added it to the coinstatsindex file because that index has state and actually needs to rewind. I think it would make sense to add a base_index tests for generic functionality that affects all indexes (and run the test for all index types, or maybe pick a random one), but I think it would be better to do that in a follow-up because it should affect other existing tests too.

Comment thread src/test/coinstatsindex_tests.cpp Outdated
index.Stop();
}

// Reload the index and verify the committed state.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't we also test the reorg scenario?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I hope the updated test covers it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not the same-height reorg, I still think those should be covered (and docs updated)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Can you explain more? Unless I’m missing something, this doesn’t seem like a practical scenario to me:

  1. same-height reorgs don’t really happen, outside of artificial situations such a preciousblock. You reorg to a better chain, and that chain is usually longer.
  2. Even for a reorg scenario to a longer chain, I think that the following would need to happen: The Sync() thread reaches the tip, then while the index thread pauses in between

    bitcoin/src/index/base.cpp

    Lines 228 to 231 in f0da26c

    if (!pindex_next) {
    SetBestBlockIndex(pindex);
    // No need to handle errors in Commit. See rationale above.
    Commit();
    both a reorg and a flush must happen in the main thread. This seems like a very artificial situation to me timing-wise, but it is also hard to replicate in a unit test, because we cannot just do the reorg before calling Sync() as we do in the additional block scenario:
    If the reorg happens while the index is syncing earlier block the index never notices it (it chooses the next block sync based on the current chain), if a reorg happens after Sync() finished and we are in notification mode we will never flush before also doing the reorg in the index because notifications are processed in order.

Comment thread src/index/base.cpp Outdated
if (ok) {
// Don't commit if the index is ahead of the chainstate's last flushed block
const CBlockIndex* index_tip = m_best_block_index.load();
if (index_tip && !m_chain->isBlockInFlushedChain(index_tip->GetBlockHash(), index_tip->nHeight)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

7933e27 index: Don't commit ahead of the flushed chainstate:

Since this is basically the only usage of this method, we should be able to tailor it more, to let the chain interface take just the candidate block hash instead of a hash and height pair from the caller:

Suggested change
if (index_tip && !m_chain->isBlockInFlushedChain(index_tip->GetBlockHash(), index_tip->nHeight)) {
if (!m_chain->isAncestorOfLastFlushedBlock(best_block_index->GetBlockHash())) {

calling:

bool isAncestorOfLastFlushedBlock(const uint256& block_hash) override
{
    LOCK(::cs_main);
    const CBlockIndex* block{chainman().m_blockman.LookupBlockIndex(block_hash)};
    const CBlockIndex* last_flushed{chainman().ValidatedChainstate().GetLastFlushedBlock()};
    return block && last_flushed && last_flushed->GetAncestor(block->nHeight) == block;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I took this change, seems like a simpler interface with just one parameter, and the added lookup should not be performance critical since commits are rare.

Comment thread src/interfaces/chain.h Outdated
Comment thread src/node/interfaces.cpp Outdated
Comment thread src/index/base.cpp Outdated
Comment thread src/index/base.cpp
@@ -272,7 +272,15 @@ bool 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

7933e27 index: Don't commit ahead of the flushed chainstate:

This isn't a "real" nullptr check, so we could probably simplify the structure a bit by something like:

const CBlockIndex* best_block_index{m_best_block_index.load()};
if (!best_block_index) return true;

if (!m_chain->isAncestorOfLastFlushedBlock(best_block_index->GetBlockHash())) {
    return true;
}
...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this would change the logic, we currently return false if best_block_index is not set. This relates to above discussion about the (unused) return value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, if we remove the return value we could apply this - which would simplify the logic, reuse, and indentation:

diff --git a/src/index/base.cpp b/src/index/base.cpp
index 0be591e7da..3ee70fdd8a 100644
--- a/src/index/base.cpp
+++ b/src/index/base.cpp
@@ -273,31 +273,29 @@ void BaseIndex::Sync()
     }
 }
 
-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;
+    const CBlockIndex* index_tip{m_best_block_index.load()};
+    if (!index_tip) return;
+
+    // Don't commit if the index is ahead of the chainstate's last flushed block
+    if (!m_chain->isAncestorOfLastFlushedBlock(index_tip->GetBlockHash())) {
+        LogInfo("Skipping commit, index is ahead of flushed chainstate (index height %d, chain height %d)",
+                index_tip->nHeight, m_chain->getHeight().value_or(-1));
+        return;
+    }
+
+    CDBBatch batch(GetDB());
+    bool ok = CustomCommit(batch);
     if (ok) {
-        // Don't commit if the index is ahead of the chainstate's last flushed block
-        const CBlockIndex* index_tip = m_best_block_index.load();
-        if (!m_chain->isAncestorOfLastFlushedBlock(index_tip->GetBlockHash())) {
-            LogInfo("Skipping commit, index is ahead of flushed chainstate (index height %d, chain height %d)",
-                    index_tip->nHeight, m_chain->getHeight().value_or(-1));
-            return false;
-        }
-        CDBBatch batch(GetDB());
-        ok = CustomCommit(batch);
-        if (ok) {
-            GetDB().WriteBestBlock(batch, GetLocator(*m_chain, m_best_block_index.load()->GetBlockHash()));
-            GetDB().WriteBatch(batch);
-        }
+        GetDB().WriteBestBlock(batch, GetLocator(*m_chain, index_tip->GetBlockHash()));
+        GetDB().WriteBatch(batch);
     }
     if (!ok) {
         LogError("Failed to commit latest %s state", GetName());
-        return false;
     }
-    return true;
 }
 
 bool BaseIndex::Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_tip)
diff --git a/src/index/base.h b/src/index/base.h
index 4e4cd603c0..5f67b368ae 100644
--- a/src/index/base.h
+++ b/src/index/base.h
@@ -93,12 +93,11 @@ private:
     std::thread m_thread_sync;
     CThreadInterrupt m_interrupt;
 
-    /// Write the current index state (eg. chain block locator and subclass-specific items) to disk.
-    /// 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.
-    /// Returns whether a commit was done.
-    bool Commit();
+    /// Try to write the current index state (eg. chain block locator and subclass-specific items) to disk.
+    /// Returns without writing 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 CustomRemove.
     bool Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_tip);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I did inline it, but didn't do any further refactors of the function. The suggestion above changes behavior such that we no longer log an error if m_best_block_index is nullptr. That change may be desirable, but I want to keep it out of the scope of this PR.

Comment thread src/validation.cpp Outdated
Comment on lines +2814 to +2816
// Flush the chainstate (which may refer to block index entries).
empty_cache ? CoinsTip().Flush() : CoinsTip().Sync();
m_last_flushed_block = m_chain.Tip();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

489bb4a validation: track last flushed block:

Could this new ChainStateFlushed locator come from the recorded flushed block instead of rereading m_chain.Tip()?

diff --git a/src/validation.cpp b/src/validation.cpp
--- a/src/validation.cpp	(revision 30c92e346762ec5d046d81d46d08811ccd9e26b3)
+++ b/src/validation.cpp	(revision 2296c4e0101ee7de230478d0dd7323b04a849fe4)
@@ -2831,7 +2831,7 @@
     }
     if (full_flush_completed && m_chainman.m_options.signals) {
         // Update best block in wallet (so we can detect restored wallets).
-        m_chainman.m_options.signals->ChainStateFlushed(this->GetRole(), GetLocator(m_chain.Tip()));
+        m_chainman.m_options.signals->ChainStateFlushed(this->GetRole(), GetLocator(Assert(m_last_flushed_block)));
     }
     } catch (const std::runtime_error& e) {
         return FatalError(m_chainman.GetNotifications(), state, strprintf(_("System error while flushing: %s"), e.what()));

@DrahtBot DrahtBot requested a review from l0rinc May 31, 2026 22:28

@mzumsande mzumsande left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sorry for the delay, I am currently working on addressing all comments and will rebase/push a new version in a few days.

@mzumsande mzumsande force-pushed the 202603_index_sync_dont_commit_ahead branch 2 times, most recently from 0f13529 to 37849c2 Compare June 8, 2026 18:54
@DrahtBot

DrahtBot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🚧 At least one of the CI tasks failed.
Task macOS native: https://github.com/bitcoin/bitcoin/actions/runs/27158855154/job/80168666015
LLM reason (✨ experimental): CI failed because CTest crashed with a segmentation fault in validation_chainstate_tests (test validation_chainstate_resize_caches).

Hints

Try to run the tests locally, according to the documentation. However, a CI failure may still
happen due to a number of reasons, for example:

  • Possibly due to a silent merge conflict (the changes in this pull request being
    incompatible with the current code in the target branch). If so, make sure to rebase on the latest
    commit of the target branch.

  • A sanitizer issue, which can only be found by compiling with the sanitizer and running the
    affected test.

  • An intermittent issue.

Leave a comment here, if you need help tracking down a confusing failure.

@mzumsande

Copy link
Copy Markdown
Contributor Author

Rebased and addressed feedback. Major changes:

  • Removed Commit() return value.
  • The interface method was removed, we now directly check in Commit()
  • the test is now in baseindex_tests.cpp, but I didn't move other tests there in this PR or introduced a framework to run t a test with multiple indexes.
  • fixed an issue to not use m_chain.Tip() in FlushStateToDisk - this is not directly related to the changes here, but was found by @l0rinc in review.

mzumsande and others added 4 commits July 9, 2026 16:21
This will be used to prevent the indexes from flushing their state ahead of
the chainstate.

Co-authored-by: Lőrinc <pap.lorinc@gmail.com>
Since it unused in the current code.
Otherwise, if the node has an unclean restart,
indexes with state (coinstatsindex) couldn't reorg to the
last flushed tip and would be corrupted.

Also updates documentation of Commit() -
the locator functionality isn't used, so the previous text was wrong:
We must have the best block in our block index after a restart.

Co-authored-by: Fabian Jahr <fjahr@protonmail.com>
In DisconnectBlock(), we can call FlushStateToDisk after updating
the coins but before changing the tip, which is still at the
disconnected block. This means that the ChainStateFlushed
signal would have the wrong block in the locator.

Also remove an outdated comment - the wallet doesn't use
ChainStateFlushed anymore, currently only indexes do.

Co-authored-by: Lőrinc <pap.lorinc@gmail.com>
@mzumsande mzumsande force-pushed the 202603_index_sync_dont_commit_ahead branch from 5ca05a6 to e9ed898 Compare July 9, 2026 14:21
@DrahtBot DrahtBot removed the CI failed label Jul 9, 2026

@sedited sedited left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-ACK e9ed898

@DrahtBot DrahtBot requested review from fjahr and ryanofsky July 9, 2026 16:31

@fjahr fjahr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

re-ACK e9ed898

@@ -0,0 +1,62 @@
// Copyright (c) 2020-present The Bitcoin Core developers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit

Suggested change
// Copyright (c) 2020-present The Bitcoin Core developers
// Copyright (c) The Bitcoin Core developers

@achow101

achow101 commented Jul 9, 2026

Copy link
Copy Markdown
Member

ACK e9ed898

@achow101 achow101 merged commit 7d8137c into bitcoin:master Jul 9, 2026
28 checks passed
Kino1994 pushed a commit to Kino1994/bitcoin-full-history that referenced this pull request Jul 10, 2026
…ed chainstate

c5471a0 validation: Don't use m_chain.Tip() in FlushStateToDisk (Martin Zumsande)
904cb97 index: Don't commit ahead of the flushed chainstate (Martin Zumsande)
e3468cf index: Remove return value from Commit() (Martin Zumsande)
ae5ca14 validation: track last flushed block (Martin Zumsande)
b5a10fc test: add test for index commits ahead of the last flushed block (Martin Zumsande)

Pull request description:

  If indexes commit their data ahead of the flushed chainstate, and there is an unclean shutdown, the index will be corrupted. This is especially the case for the coinstatsindex, which has state (the muhash) which can't easily be rolled back without access to the blocks. This was only partly fixed in #33212 (for reorg scenarios) but could still happen during initial sync.

  Fix this more thoroughly by having the node keep track of the last flushed block, and skipping index commits if the current block of the index is not an ancestor of the node's last flushed block (similar to the suggestion by stickies-v in bitcoin/bitcoin#33212 (review).

  Fixes #33208
  Fixes #34261

ACKs for top commit:
  achow101:
    ACK c5471a0
  sedited:
    Re-ACK c5471a0
  fjahr:
    re-ACK c5471a0

Tree-SHA512: 7f4dc6fb942d6726587eb75dece24c79c679d8630320502aca9fa2d2b03b1d25999cd11255ec20344ebbb8985552747e2554e2557b9d2ad0c75db71652d615ab
@l0rinc

l0rinc commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Added a follow-up: #35714 makes the new m_last_flushed_block boundary also hold when flushing blocks/undo data fails.

lattica-core added a commit to lattica-core/lattica that referenced this pull request Jul 14, 2026
… flushed chainstate

Track the chainstate's last flushed block and skip a BaseIndex commit when the
index tip is ahead of it, so an unclean shutdown can't leave a stateful index
(e.g. coinstatsindex/txindex) persisted ahead of a chainstate it can no longer
roll back to. Verbatim port of upstream Bitcoin Core PR 34897.
alexanderwiederin added a commit to alexanderwiederin/rust-bitcoinkernel that referenced this pull request Jul 14, 2026
…4e48e968a

114e48e968a kernel: Add script tracer
54e1a95a12e Merge bitcoin/bitcoin#35719: ci: disable Qt build in OpenBSD cross job
b0e09511586 ci: disable Qt build in OpenBSD cross job
734c34bafda Merge bitcoin/bitcoin#35427: depends: Build `qt` and `qrencode` packages on OpenBSD
ee61b11a9e7 Merge bitcoin/bitcoin#35200: node: smooth oversized `dbcache` warnings
e3554bf361f Merge bitcoin/bitcoin#35579: wallet: reserve walletrescan before checking wallet is at the tip
11ae4265522 Merge bitcoin/bitcoin#35715: cmake: Fix WITH_EXTERNAL_LIBMULTIPROCESS + BUILD_FUZZ_BINARY
e544413c0de Merge bitcoin/bitcoin#32763: wallet: Replace CWalletTx::mapValue and vOrderForm with explicit class members
fe1cb6e40d7 Merge bitcoin/bitcoin#35690: wallet: Introduce WalletError with machine-readable error code
441f3114f57 Merge bitcoin/bitcoin#35659: Clarify supported *BSD releases and drop outdated workarounds
0399df827c6 Merge bitcoin/bitcoin#35708: depends: capnp 1.5.0
1ab1fdd4696 Merge bitcoin/bitcoin#35705: bench: replace CreateMockableWalletDatabase with MakeInMemoryWalletDatabase
db35b9238fc ipc # build: Fix fuzz target CMakeLists.txt for external libmultiprocess
d18fec892e2 Merge bitcoin/bitcoin#35698: doc: Update enum class constant naming style guide
a2e4cd7ad2a depends: capnp 1.5.0
7508ac319d9 bench: replace CreateMockableWalletDatabase with MakeInMemoryWalletDatabase
907e284e303 Merge bitcoin/bitcoin#35701: test: Remove `mock_process.cpp`
c8459b6bdcd Merge bitcoin/bitcoin#35568: txospenderindex: disable bloom filters to optimize disk usage
63c5f9d22c0 test: Remove `mock_process.cpp`
ef101b04a8d Merge bitcoin/bitcoin#35655: wallet: Use in-memory SQLite for temporary wallet in exportwatchonlywallet
b6becf3534c Merge bitcoin/bitcoin#35684: Update libmultiprocess subtree to add `max_connections` option
930f25050f7 Merge bitcoin/bitcoin#35700: doc: archive release notes for v29.4
9b2b3f4ec6f doc: archive release notes for v29.4
297fd1489bb Merge bitcoin/bitcoin#35412: ci: add NetBSD Clang cross job
e314869066b Merge bitcoin/bitcoin#35695: Remove myself as security contact
e3d67a5eae5 Merge bitcoin/bitcoin#35691: chainparams: delete my DNS seed
a8223bb4e62 wallet: Introduce WalletError with machine-readable error code
fad5809cb92 doc: Update enum class constant naming style guide
7d8137c1417 Merge bitcoin/bitcoin#34897: indexes: Don't commit ahead of the flushed chainstate
629df81e4c6 Remove myself as security contact
d164a043426 node: smooth oversized `-dbcache` warnings
d9080639804 chainparams: delete my DNS seed
e9ed898a0da validation: Don't use m_chain.Tip() in FlushStateToDisk
3679f1ecf5e index: Don't commit ahead of the flushed chainstate
65735728a5a index: Remove return value from Commit()
09c06960c6a validation: track last flushed block
13c02b5466d test: add test for index commits ahead of the last flushed block
c43b7a1115f ci: add netBSD cross CI job
699c21aea47 depends: add netbsd_LDFLAGS
777d23f25c7 test: add regression test for in-memory SQLiteDatabase reopen
d1e7f8c986f wallet: use in-memory SQLite for temporary wallet in exportwatchonlywallet
ee43743f126 wallet: store m_additional_flags in SQLiteDatabase to fix reopen path
a9d1b652f32 Merge commit '707d0ded84563386f770ec17970834c65f8fa938' into pr/subtree-12
707d0ded845 Squashed 'src/ipc/libmultiprocess/' changes from 16bf05dea02..28e056576a3
2bab6bc73f2 refactor: Drop support for FreeBSD < 14
91b5c8a07c6 refactor: Remove FreeBSD-specific workaround
56701ff6d5c doc: Clarify supported *BSD releases
6d0ea4cf5bd doc: add release notes
a2b1c86903a txospenderindex: disable bloom filters to optimize disk usage
fed3cf6f0ed wallet: Replace CWalletTx's vOrderForm with specific fields
4f8823e8e11 wallet: Drop vOrderForm from CommitTransaction
9e62e4b1f34 test: slow down rescaning process
a2b0bfcd854 wallet: Drop mapValue from CWalletTx
cb99864c91c wallet: Throw if unknown entry is found in mapValue
98d5cdae663 wallet: Make CWalletTx "replaces_txid" and "replaced_by_txid" member variables
7ef8a6efc2a wallet: Make CWalletTx "comment" and "to" member variables
2155e913d3e wallet: Make CWalletTx "from" and "message" member variables
c6ba98dcc8a wallet: Drop mapValue from CommitTransaction
00abb174a80 wallet: Pass comment and comment_to to CommitTransaction
1a219a37a21 wallet: Pass replaces_txid to CommitTransaction outside of mapValue
336f5a738b3 wallet: reserve walletrescan before checking wallet is at the tip
a54ec373a69 depends: Build `qt` and `qrencode` packages for OpenBSD hosts

git-subtree-dir: libbitcoinkernel-sys/bitcoin
git-subtree-split: 114e48e968a087f74c1ab611ac2a31a9266812e1
@mzumsande mzumsande deleted the 202603_index_sync_dont_commit_ahead branch July 15, 2026 15:17
@mzumsande

Copy link
Copy Markdown
Contributor Author

Added a follow-up: #35714 makes the new m_last_flushed_block boundary also hold when flushing blocks/undo data fails.

sounds good, I'll review soon. I am working on a test follow-up (moving more stuff to baseindex_tests and executing the tests for different indexes).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Block filter index corruption post reorg and unclean shutdown Indexes stuck on unknown best block after unclean shutdown

10 participants