validation: ensure assumevalid is always used during reindex #31615

pull Eunovo wants to merge 2 commits into bitcoin:master from Eunovo:31494-reindex-assumevalid changing 5 files +141 −16
  1. Eunovo commented at 11:52 am on January 7, 2025: contributor

    assumevalid may be ignored during reindex for the following reasons:

    • the assumevalid block is not in the block index: this happens when the assumevalid block could not be downloaded before the previous IBD run was interrupted
    • the chainwork of the best header is less than the minimumchainwork: happens if the previous IBD was interrupted before it could connect blocks up to minimumchainwork. See Issue #31494

    This PR fixes this by skipping script verification during reindex if assumevalid is enabled.

  2. DrahtBot commented at 11:52 am on January 7, 2025: 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/31615.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK stratospher
    Concept ACK l0rinc, mzumsande

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #30214 (refactor: Improve assumeutxo state representation by ryanofsky)

    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.

  3. in src/validation.cpp:2502 in 951812b0ac outdated
    2498@@ -2499,7 +2499,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    2499         if (it != m_blockman.m_block_index.end()) {
    2500             if (it->second.GetAncestor(pindex->nHeight) == pindex &&
    2501                 m_chainman.m_best_header->GetAncestor(pindex->nHeight) == pindex &&
    2502-                m_chainman.m_best_header->nChainWork >= m_chainman.MinimumChainWork()) {
    2503+                (m_chainman.m_best_header->nChainWork >= m_chainman.MinimumChainWork() || m_chainman.m_blockman.LoadingBlocks())) {
    


    mzumsande commented at 3:39 pm on January 7, 2025:
    I think that this won’t achieve the desired result: If the assumevalid block is not in the block index (which will be the case if IBD crashed midway before that block was downloaded, and a reindex is done) we’ll abort above in line 2499 and never get to this spot.

    Eunovo commented at 6:29 pm on January 7, 2025:
    That’s true. I thought #31494 was focused on the best header chainwork not being up to minimumchainwork. I could change this to always skip script checks during a reindex, but IIUC having a block on disk doesn’t mean that it was connected during the previous IBD. Might be worth it to sync headers first then connect blocks even during reindex.

    mzumsande commented at 9:23 pm on January 7, 2025:

    I really wasn’t sure what the best solution is when I opened the issue and still am not, but these are my thoughts:

    Yes, there are two reasons why we might not apply assumevalid during a reindex currently: 1.) The best header has not enough chainwork 2.) The assumevalid block is not in our block index

    I could change this to always skip script checks during a reindex, but IIUC having a block on disk doesn’t mean that it was connected during the previous IBD.

    I think the question is rather how well we can avoid to connect blocks skipping the script checks during the reindex that we would have fully validated in the original run if we hadn’t crashed. Assuming that the user provides the same -assumevalid arg to the reindex run that they did in the original run I can see the following cases: 1.) The user provides an existing block (or nothing, so that the default hash from chainparams is used) that we just haven’t downloaded yet because it’s later in the best chain - if we don’t have it in our index we’d be justified to skip script checks - if we do have it in our index, no special rules are necessary for reindexing. I think that’s the most common case for users. 2.) The user provides -assumevalid=0 - we should respect that also in the reindex run and fully validate everything. 3.) The user provides a block hash with -assumevalid that is non-existing / not in the best chain. I think that’s the critical case because here we would have fully validated everything in the original run, but won’t validate anything during the reindex. But arguably that’s not a common case / the user’s fault for providing this bad block, so maybe it can be tolerated?

    Might be worth it to sync headers first then connect blocks even during reindex.

    Yes, that’s the second option I thought of. I think we can’t just stop and wait for the headers to be downloaded - reindex should be able to finish without having a p2p connection, even if that means no assumevalid. But I was thinking of maybe just disabling the relevant LoadingBlocks() guards in net_processing, so that we’d download headers in parallel to ActivateBestChain(), and would change to use assumevalid after a few thousand blocks or so. That seem a bit messy (we’d still connect some blocks without assumevalid) and requires testing but might work well in practice?!


    Eunovo commented at 1:59 pm on January 10, 2025:

    I created another test to check if blocks rejected in the normal IBD run get loaded during a -reindex. The test reveals that it does load the invalid block and it will connect it if script checks are skipped, see https://github.com/Eunovo/bitcoin/commit/1ff4e5ee78418a30ba1ea9c384b8c7ee43435ce6

    IIUC During a reindex, the node does not know what blocks were connected in the previous run so it can’t safely skip checks. It looks to me like we have to:

    1. Sync headers before connecting blocks during reindex OR
    2. Persist data to show that we previously connected this block (IIUC this exact data exists but gets deleted during reindex, maybe we can preserve the relevant data we need?)

    Eunovo commented at 2:04 pm on January 10, 2025:

    The test reveals that it does load the invalid block and it will connect it if script checks are skipped, see Eunovo@1ff4e5e

    I’m still not sure if this causes any damage because I expect the node to eventually pick the most-work chain when it syncs headers

  4. Eunovo commented at 11:56 am on January 18, 2025: contributor

    I had an offline conversation with @mzumsande. During the reindex, we can apply assumevalid for following cases:

    • If the assumevalid block is the index, then we can skip script checks for all ancestors of the assumevalid block
    • If the assumevalid block is not in the index, then the initial IBD run had not downloaded the assumevalid block yet then the blocks on disk are ancestors of the assumevalid block and we can still skip script verification

    Then we download headers from peers in parallel to ActivateBestChain()

    I’ll put this in draft while I implement and test this over the next few days

  5. Eunovo marked this as a draft on Jan 18, 2025
  6. Eunovo force-pushed on Jan 29, 2025
  7. DrahtBot added the label CI failed on Jan 29, 2025
  8. Eunovo force-pushed on Jan 29, 2025
  9. DrahtBot removed the label CI failed on Jan 29, 2025
  10. l0rinc commented at 3:17 pm on January 30, 2025: contributor

    I’m not sure whether LoadingBlocks is the correct way to solve it or not (I would need someone else to opine on that, I can’t tell if this can err on the side of not checking the scripts at all for some edge case).

    I can confirm however that this does seem to solve the issue I had, reported in #31494 (i.e. reindex(-chainstate) after a partial IBD always enabling script verification).


    To test it, I have added a LogWarning("fScriptChecks is %s", fScriptChecks); after the assignments and ran it with master vs the branch:

    0   cmake -B build -DCMAKE_BUILD_TYPE=Debug && cmake --build build -j$(nproc) \
    1&& mkdir -p demo && build/src/bitcoind -datadir=demo -stopatheight=100 \
    2&& build/src/bitcoind -datadir=demo -reindex-chainstate -stopatheight=10
    

    master

    0[warning] fScriptChecks is true
    

    Eunovo:31494-reindex-assumevalid

    0[warning] fScriptChecks is false
    

    I can also confirm that adding -assumevalid=0 will enable script checking from the beginning and that -assumevalid=000000009b7262315dbf071787ad3656097b892abffd1f95a1a022f896f533fc has 5 fScriptChecks=false values after which script checking is (correctly) enabled again.


    I also checked that the new assume_valid test passes here and fails correctly on master (though it’s very slow, we have to check if there’s a way to speed it up somewhat)

     02025-01-30T15:08:22.897000Z TestFramework (INFO): Success
     12025-01-30T15:08:31.343000Z TestFramework (INFO): Restarting node while reindexing..
     22025-01-30T15:08:32.551000Z TestFramework (INFO): Testing reindex with -assumevalid
     32025-01-30T15:09:23.963000Z TestFramework (INFO): Testing reindex with assumevalid block in block index and minchainwork > best_header chainwork
     42025-01-30T15:09:52.592000Z TestFramework (ERROR): Assertion failed
     5Traceback (most recent call last):
     6  File "/Users/lorinc/IdeaProjects/bitcoin/test/functional/test_framework/test_framework.py", line 135, in main
     7    self.run_test()
     8    ~~~~~~~~~~~~~^^
     9  File "/Users/lorinc/IdeaProjects/bitcoin/build/test/functional/feature_reindex.py", line 203, in run_test
    10    self.assume_valid()
    11    ~~~~~~~~~~~~~~~~~^^
    

    So from me it’s an approach ACK, but someone else needs to validate the implementation details and the corner cases that can result from this.

  11. Eunovo commented at 3:51 pm on January 30, 2025: contributor

    I’m not sure whether LoadingBlocks is the correct way to solve it or not (I would need someone else to opine on that, I can’t tell if this can err on the side of not checking the scripts at all for some edge case).

    I agree too. LoadingBlocks() is true when blocks are being reindexed or when new blocks are being loaded with -loadblocks . I intend to move away from LoadingBlocks() to something that’s only true when blockfiles are being reindexed.

    I also checked that the new assume_valid test passes here and fails correctly on master (though it’s very slow, we have to check if there’s a way to speed it up somewhat)

    I’ll look into this!

  12. Eunovo force-pushed on Feb 13, 2025
  13. mzumsande commented at 9:49 pm on March 7, 2025: contributor

    Concept ACK

    I think the current approach would work. Even though it means that if the user provided a bad / non-existing assumeutxo hash they could skip script validation during a reindex when they wouldn’t have in the original run, I’d say that’s not a major problem because a reindex should only be necessary in case of a a db corruption, which should not be remotely triggerable.

    So it will hopefully make the reindex a bit faster in case of a database corruption, while not changing the assumevalid security assumption very much, and it does respect -assumevalid=0.

    Though it would be good to have some experienced contributors chime in that were around when assumevalid was introduced (#9484).

  14. Eunovo marked this as ready for review on Mar 8, 2025
  15. Eunovo commented at 6:33 am on March 8, 2025: contributor
    Thanks for the reviews @l0rinc and @mzumsande . Due to your feedback, I have opened the PR for general review.
  16. DrahtBot added the label CI failed on Mar 21, 2025
  17. DrahtBot commented at 7:47 am on March 21, 2025: contributor
    Needs rebase, if still relevant
  18. Eunovo force-pushed on Mar 24, 2025
  19. Eunovo renamed this:
    Ensure assumevalid is always used during reindex
    validation: ensure assumevalid is always used during reindex
    on Mar 24, 2025
  20. Eunovo commented at 11:39 am on March 24, 2025: contributor

    Rebased on master https://github.com/bitcoin/bitcoin/commit/770d39a37652d40885533fecce37e9f71cc0d051

  21. DrahtBot removed the label CI failed on Mar 24, 2025
  22. DrahtBot added the label Validation on Mar 24, 2025
  23. in src/node/blockstorage.cpp:1232 in e1b42458cc outdated
    1227@@ -1213,6 +1228,11 @@ void ImportBlocks(ChainstateManager& chainman, std::span<const fs::path> import_
    1228             }
    1229             nFile++;
    1230         }
    1231+
    1232+        // Call ActivateBestChain here so we can skip script verification
    


    mzumsande commented at 8:41 pm on May 15, 2025:
    nit: I’d replace “here” with “before setting m_blockfiles_indexed” so the reason becomes more clear.

    Eunovo commented at 11:59 am on June 18, 2025:
    Done
  24. in src/node/blockstorage.cpp:1193 in e1b42458cc outdated
    1189@@ -1190,6 +1190,21 @@ void ImportBlocks(ChainstateManager& chainman, std::span<const fs::path> import_
    1190 {
    1191     ImportingNow imp{chainman.m_blockman.m_importing};
    1192 
    1193+    auto update_all_chainstate = [&]() {
    


    mzumsande commented at 8:42 pm on May 15, 2025:
    I’d prefer to call it activate_all_chainstates to not introduce a less precise term (“update”).

    Eunovo commented at 11:59 am on June 18, 2025:
    Done
  25. in src/node/blockstorage.cpp:1258 in e1b42458cc outdated
    1265-        if (!chainstate->ActivateBestChain(state, nullptr)) {
    1266-            chainman.GetNotifications().fatalError(strprintf(_("Failed to connect best block (%s)."), state.ToString()));
    1267-            return;
    1268-        }
    1269-    }
    1270+    update_all_chainstate();
    


    mzumsande commented at 9:27 pm on May 15, 2025:
    This means that in case of a reindex, ActivateBestChain(), will be called twice now from ImportBlocks, but that shouldn’t be a problem because the second call just won’t do any work.
  26. in test/functional/feature_reindex.py:206 in cb29bc3c30 outdated
    188+
    189+        # node0 should still sync to tip of the chain, ignoring the invalid signature
    190+        assert_equal(node.getblock(node.getbestblockhash())['height'], expected_height)
    191+        assert_equal(int(node.getbestblockhash(), 16), blocks[2100].sha256)
    192+
    193+        self.log.info("Success")
    


    mzumsande commented at 9:54 pm on May 15, 2025:
    would be good to add a subtest that a run without -assumevalid will not connect the invalid block.

    Eunovo commented at 8:12 am on May 16, 2025:
    Will do

    Eunovo commented at 11:59 am on June 18, 2025:
    Done
  27. in test/functional/feature_reindex.py:158 in cb29bc3c30 outdated
    153+            block = create_block(tip, create_coinbase(height), block_time)
    154+            block.solve()
    155+            blocks.append(block)
    156+            tip = block.sha256
    157+
    158+        # Start node0 with -assumevalid pointing to the block with invalid signature
    


    mzumsande commented at 9:57 pm on May 15, 2025:
    Comment needs updating, -assumevalid points to the last block, not the invalid one.

    Eunovo commented at 12:00 pm on June 18, 2025:
    Done
  28. in test/functional/feature_reindex.py:151 in cb29bc3c30 outdated
    145+        invalid_block.solve()
    146+        tip = invalid_block.sha256
    147+        blocks.append(invalid_block)
    148+
    149+        # Bury that block with roughly two weeks' worth of blocks (2100 blocks) and an extra 100 blocks
    150+        for _ in range(2200):
    


    mzumsande commented at 10:02 pm on May 15, 2025:
    is there a reason we need to mine that many blocks, and not just a few more to make the test faster?

    Eunovo commented at 8:31 am on May 16, 2025:
    Are you referring to the 100 blocks mined after the first 2100 blocks?

    mzumsande commented at 3:31 pm on May 16, 2025:
    I meant the 2100 (two weeks) of blocks. I didn’t test this, but is there a reason this couldn’t just be a few blocks (plus a few more that aren’t sent to the node)?

    Eunovo commented at 4:42 pm on May 16, 2025:

    Script checks are only skipped when the current block is roughly 2 weeks away from the tip. See https://github.com/bitcoin/bitcoin/blob/3f83c744ac28b700090e15b5dda2260724a56f49/src/validation.cpp#L2476-L2494

    We have to bury the test block 2 weeks deep for script verification to the skipped


    mzumsande commented at 5:30 pm on May 16, 2025:
    oh I see, forgot about that check - can be resolved.

    stratospher commented at 8:15 am on July 4, 2025:
    the tests now take 2m34.878s on my system (was around 18 seconds before). since the tests takes much more time now, maybe remove it from the “Tests less than 30s” section in test_runner.py?

    Eunovo commented at 1:53 pm on July 4, 2025:
    @stratospher the test completes much faster on my system, but by checking the timing of other tests, I put it among “Tests less than 2m” section because it completes faster than mining_getblocktemplate_longpoll.py than slower than mempool_persist.py
  29. DrahtBot added the label CI failed on Jun 15, 2025
  30. in test/functional/feature_reindex.py:143 in cb29bc3c30 outdated
    138+
    139+        # Create a block with an invalid signature
    140+        tx = CTransaction()
    141+        tx.vin.append(CTxIn(COutPoint(int(coinbase_to_spend, 16), 0), scriptSig=b""))
    142+        tx.vout.append(CTxOut(49 * 10000, CScript([OP_TRUE])))
    143+        tx.calc_sha256()
    


    DrahtBot commented at 8:49 am on June 16, 2025:
                                   AttributeError: 'CTransaction' object has no attribute 'calc_sha256'
    
  31. validation.cpp: use assumevalid during reindex
    -reindex deletes the block tree db and attempts to rebuild it based on local block files,
    without attempting to re-request headers for which we don't have the full block yet
    This might cause:
    - the best known chain to have a chainwork less than the minimumchainwork
    - the assumedvalid block to not be in the block index if the previous IBD was interrupted
    before downloading it
    63bba358d7
  32. Eunovo force-pushed on Jun 18, 2025
  33. Eunovo commented at 12:02 pm on June 18, 2025: contributor
  34. DrahtBot removed the label CI failed on Jun 18, 2025
  35. in test/functional/feature_reindex.py:178 in 11a9d55e84 outdated
    173+        expected_height = height - 100
    174+        # node0 should sync to tip of the chain, ignoring the invalid signature
    175+        assert_equal(node.getblock(node.getbestblockhash())['height'], expected_height)
    176+
    177+        self.log.info("Testing reindex with assumevalid block in block index and minchainwork > best_header chainwork")
    178+        # Restart node0 but raise minimumchainwork to that of a chain 32256 blocks long
    


    stratospher commented at 7:04 am on July 4, 2025:
    any particular reason for having this high minimum chain work? (ex: for 32256 blocks instead of 3256 blocks). also curious how you computed it?

    Eunovo commented at 1:46 pm on July 4, 2025:

    For this test, I need the minimumchainwork to be higher than the best_header chainwork. The height of the best_header here is 3714, so the minimumchainwork needs to be for a chain considerably higher than 3714 blocks. Antoine had provided the exact chain work for 32256 blocks in this stack exchange answer, so I used that.

    Note that the minimumchainwork used doesn’t affect the duration of the test, it just checks that the minimumchainwork parameter is ignored during the reindex.


    stratospher commented at 3:09 pm on July 4, 2025:
    ah I see, but that’s on mainnet and not on regtest, so comment might need an edit. agree that the value doesn’t affect the test!

    Eunovo commented at 3:51 am on July 7, 2025:
    I modified the test to calculate chainwork to be approximately 100 blocks greater than the current best_header, and I checked that the test still verifies the expected behaviour. See https://github.com/bitcoin/bitcoin/pull/31615/commits/a53d43bc0f010513b0922ad48d611f402ec0e511
  36. stratospher commented at 8:31 am on July 4, 2025: contributor

    ACK 11a9d55e.

    Agree that applying reindex-assumevalid combo can be useful in the special cases mentioned above to speedup reindexing when we don’t have all blocks.

    A behaviour change during assumevalid+reindex now would be:

    • we’d skip script checks even when the assumevalid blockindex doesn’t exist (ex:we put assumevalid=somegarbage(or)blockhash-with-typo)
    • this was not the case on master where we skip script checks only when know that the assumevalid blockindex exists.

    Don’t think it’s a problem since assumevalid already puts responsibility on the user and we’d need to have tampered block files on disk from some external source (in which case user shouldn’t be using assumevalid in the first place). just wanted to mention it + curious about what others think about this/any other possible behaviour change.

  37. DrahtBot requested review from mzumsande on Jul 4, 2025
  38. DrahtBot requested review from l0rinc on Jul 4, 2025
  39. Eunovo force-pushed on Jul 4, 2025
  40. Eunovo commented at 1:49 pm on July 4, 2025: contributor

    Rebased https://github.com/bitcoin/bitcoin/pull/31615/commits/11a9d55e84af2d6ff138b99310720667cd59dcc3 to https://github.com/bitcoin/bitcoin/pull/31615/commits/e260e23d88cd6b2aaa03d9d45dae8b174f2cfe1f

    • moved feature_reindex.py test to “Tests less than 2m” section in test_runner.py, because it is now much slower and has similar runtimes to other tests in this section
  41. test: Check that reindex always uses -assumevalid a53d43bc0f
  42. Eunovo force-pushed on Jul 7, 2025
  43. stratospher commented at 8:25 am on July 7, 2025: contributor
    reACK a53d43b.

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: 2025-07-14 18:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me