validation: `Assertion failed: ((pindexFirstNeverProcessed == nullptr || pindex == snap_base) == pindex->HaveNumChainTxs())` #36021

issue marcofleon opened this issue on August 19, 2026
  1. marcofleon commented at 12:12 PM on August 19, 2026: contributor

    Can be triggered by submitting the missing parent block at the end of the feature_prune_stale_block functional test. The restart isn't even needed.

    
    diff --git a/test/functional/feature_prune_stale_fork.py b/test/functional/feature_prune_stale_fork.py
    index 5badca0a7c..0ecb9f2b47 100755
    --- a/test/functional/feature_prune_stale_fork.py
    +++ b/test/functional/feature_prune_stale_fork.py
    @@ -30,8 +30,12 @@ class FeaturePruneStaleForkTest(BitcoinTestFramework):
             node.pruneblockchain(node.getblockcount() - 100)
             assert_raises_rpc_error(-1, "Block not available (pruned data)", node.getblock, side_child.hash_hex)
     
    -        self.log.info("Restart and mine; node must reload cleanly after the stale-fork child was pruned")
    -        self.restart_node(0)
    +        self.log.info("Submit the missing parent block")
    +        node.submitblock(side_parent.serialize().hex())
    +
             self.generate(node, 1)
    
    

    So it’s the same stale-fork pruned block setup as #35050 and #35168, but without the restart requirement.

    This was found with a test running on Antithesis.

  2. marcofleon commented at 12:17 PM on August 19, 2026: contributor

    Still figuring out the best fix, but posting the issue in the meantime.

    When pruning, we remove the pruned child from m_blocks_unlinked. When the missing parent arrives, ReceivedBlockTransactions() uses m_blocks_unlinked to find descendants and calculate their m_chain_tx_count. Because the child was removed during pruning, its m_chain_tx_count remains unset, causing the assertion failure.

  3. marcofleon renamed this:
    validation: `Assertion failed: ((pindexFirstNeverProcessed == nullptr || pindex == snap_base) == pindex->HaveNumChainTxs()`)
    validation: `Assertion failed: ((pindexFirstNeverProcessed == nullptr || pindex == snap_base) == pindex->HaveNumChainTxs())`
    on Aug 19, 2026
  4. mzumsande commented at 12:25 PM on August 19, 2026: contributor

    Is this the same as #31512 ?

  5. dergoegge commented at 10:19 AM on August 20, 2026: member

    Is this the same as #31512 ?

    I think it is the same (at least by the sound of the issue description).

    To quote the other issue:

    we don't usually request parents of already pruned blocks like in step 3, unless we do getblockfrompeer or similar - so it can't be triggered remotely

    In Antithesis this was triggered through normal p2p interactions of the nodes in the test (i.e. getblockfrompeer wasn't used)

  6. mzumsande commented at 11:25 AM on August 20, 2026: contributor

    In Antithesis this was triggered through normal p2p interactions of the nodes in the test (i.e. getblockfrompeer wasn't used)

    Hmm, in order to trigger this you need to somehow request the parent of the pruned block because unrequested blocks would not be accepted due to fHasMoreOrSameWork. Since we don't prune blocks near the tip, I think this would have to happen through 1.) an extremely large reorg deeper than the pruned range (in which case a pruned node would become useless anyway). 2.) artificial means (getblockfrompeer, submitblock)

    Maybe the Antithesis test was creating some variation of 1)?

  7. dergoegge commented at 1:56 PM on August 20, 2026: member

    Maybe the Antithesis test was creating some variation of 1)?

    Yes, the nodes will go through crazy reorg scenarios in those tests. It's fine for a pruned node not to be able to perform the reorg but at least it shouldn't crash.

  8. marcofleon commented at 3:59 PM on August 27, 2026: contributor

    Here's another CheckBlockIndex failure that Antithesis found.

    [      2262.693] [               node2] [inf] 2026-08-01T21:18:21Z [all:error] Failed for FlatFilePos(nFile=-1, nPos=0) while reading raw block storage header
    [      2262.693] [               node2] [inf] 2026-08-01T21:18:21Z [all:error] DisconnectTip(): Failed to read block
    [      2262.693] [               node2] [inf] 2026-08-01T21:18:21Z [all:error] A fatal internal error occurred, see debug.log for details: Failed to disconnect block.
    [      2262.693] [               node2] [inf] SDK forwarding to libvoidstar for __sanitizer_cov_trace_pc_guard_init()
    [      2262.693] [               node2] [inf] TRYING TO LOAD libvoidstar
    [      2262.693] [               node2] [inf] LOADED libvoidstar
    [      2262.693] [               node2] [err] Error: A fatal internal error occurred, see debug.log for details: Failed to disconnect block.
    [      2262.693] [               node2] [inf] 2026-08-01T21:18:21Z [all:info] [default] AddToWallet 8e8be2b3cc3e1a0d57ed07798dba4178a6d701409ca1c7384e3569eb6f60f843 update Inactive (abandoned=1)
    [      2262.693] [               node2] [inf] 2026-08-01T21:18:21Z [all:error] ProcessNewBlock: ActivateBestChain failed (Failed to disconnect block.)
    [      2262.694] [               node2] [err] bitcoin-node: validation.cpp:5363: void ChainstateManager::CheckBlockIndex() const: Assertion `c->setBlockIndexCandidates.contains(pindex)' failed.
    

    I had claude write a quick functional test that fails on master. It's the same general case where we reorg past the last kept block on a pruned node.

    Considering these are unlikely scenarios, I agree that it's fine for a pruned node to not reorg correctly in these cases and that it doesn't make sense to overcomplicate things with specific fixes for each and every case. But maybe we could have a single and coherent failure for when an attempted deep reorg fails on a pruned node, as opposed to these scattered crashes.

  9. mzumsande commented at 3:51 PM on September 1, 2026: contributor

    I have been looking into these in the last days, and I currently think about it as follows:

    One way to fix these crashes would be to soften the CheckBlockIndex() checks to deal with these cases. While this would work (I have a branch for it), it doesn't fix the fact that we are in a situation where we receive the child of a pruned block and don't set m_chain_tx, when we should (and actually would after a restart). Even though m_chain_tx not being set would not be catastrophic, this is not be a very satisfying solution in my opinion.

    The alternative is to redefine the rules of m_blocks_unlinked, not removing entries while pruning. I am currently working on this approach and hope to open a draft PR soon - this would also allow us to remove the artificial fuzzing restriction from https://github.com/bitcoin/bitcoin/blob/dc0395c5858a1d55239b82a834e5075cf2069219/src/test/fuzz/block_index_tree.cpp#L155-L156

    Here's another CheckBlockIndex failure that Antithesis found.

    I think that's a separate issue from this and #31512, because we don't download the child of a pruned block there. We have decided to shut down anyway there (because we can't disconnect a block that has been pruned), and the CheckBlockIndex crash happens "on the way down" because ActivateBestChain is interrupted in an unclean state.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-09-09 07:56 UTC