log: print every script verification state change #33336

pull l0rinc wants to merge 6 commits into bitcoin:master from l0rinc:l0rinc/log-initial-signature-verification-state changing 3 files +152 −52
  1. l0rinc commented at 6:40 am on September 8, 2025: contributor

    Summary

    Users can encounter cases where script checks are unexpectedly enabled (e.g. after reindex, or when assumevalid/minimumchainwork gates fail). Without an explicit line, they must infer state from the absence of a message, which is error-prone. The existing “Assuming ancestors of block …” line does not reliably indicate whether script checks are actually enabled, which makes debugging/benchmarking confusing.

    What this changes

    We make the initial script-verification state explicit and log why checks are enabled to avoid confusion.

    • Always log the first script-verification state on startup, before the first UpdateTip.
    • Flatten the nested assumevalid conditionals into a linear gating sequence for readability.
    • Extend the functional test to assert the old behavior with the new reason strings.

    This is a logging-only test change it shouldn’t change any other behavior.

    Safety

    • We log on the first observation only, using std::optional<bool>; this is safe because ConnectBlock already holds cs_main.
    • No consensus or networking changes; strictly diagnostic.

    Example output

    The state (with reason) is logged at startup and whenever the reason changes, e.g.:

    • Disabling script verification at block [#904336](/bitcoin-bitcoin/904336/) (000000000000000000014106b2082b1a18aaf3091e8b337c6fed110db8c56620).
    • Enabling script verification at block [#912527](/bitcoin-bitcoin/912527/) (000000000000000000010bb6aa3ecabd7d41738463b6c6621776c2e40dbe738a): block too recent relative to best header.
    • Enabling script verification at block [#912684](/bitcoin-bitcoin/912684/) (00000000000000000001375cf7b90b2b86e559d05ed92ca764d376702ead3858): block height above assumevalid height.

    Follow-up to #32975 (review)

  2. DrahtBot added the label Validation on Sep 8, 2025
  3. DrahtBot commented at 6:40 am on September 8, 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/33336.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK hodlinator, Eunovo, yuvicc, andrewtoth
    Approach ACK TheCharlatan
    Stale ACK w0xlt

    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:

    • #31615 (validation: ensure assumevalid is always used during reindex by Eunovo)

    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.

  4. l0rinc renamed this:
    validation: log initial signature verification state
    log: always print initial signature verification state
    on Sep 8, 2025
  5. yuvicc commented at 7:54 am on September 8, 2025: contributor

    Concept ACK.

    The approach of switching to std::optional<bool> here avoids the overhead of atomic operations relying on cs_main for synchronization makes sense.

    02025-09-08T07:43:58Z Disabling signature validations at block [#16893](/bitcoin-bitcoin/16893/) (00000000113c355415c3417c734a45b62790b91b41ff08028580254c584f5ea8).
    
  6. l0rinc commented at 4:28 am on September 10, 2025: contributor
    For more context, even just restarting the node that was reindexed before (and is at around 200k blocks) with -assumevalid=0000000000000000000087564caa77e7b3f29d0464256c04d5539e43663f8698 (block 914005), it still starts with signature verification enabled - using 70% of CPU when it should have been disabled in the first place, see: This PR makes it at least obvious that something’s off. #31615 (comment) doesn’t seem to fix the issue, but that’s orthogonal with at least making it obvious via the logs.
  7. in src/validation.h:563 in 64ba2821b5 outdated
    559@@ -560,7 +560,7 @@ class Chainstate
    560     //! Cached result of LookupBlockIndex(*m_from_snapshot_blockhash)
    561     mutable const CBlockIndex* m_cached_snapshot_base GUARDED_BY(::cs_main){nullptr};
    562 
    563-    std::atomic_bool m_prev_script_checks_logged{true};
    564+    std::optional<bool> m_prev_script_checks_logged{};
    


    Eunovo commented at 9:39 am on September 10, 2025:
    https://github.com/bitcoin/bitcoin/pull/33336/commits/64ba2821b5a55aa6d51fbb7a16470d4f7c41c92e: I believe you now need to add GUARDED_BY(::cs_main) here to use Clang’s Thread Safety Analysis

    l0rinc commented at 6:24 pm on September 10, 2025:
    Excellent, added you as a coathor, thanks.
  8. l0rinc force-pushed on Sep 10, 2025
  9. l0rinc force-pushed on Sep 10, 2025
  10. in test/functional/feature_assumevalid.py:151 in 8cb75ee0a6 outdated
    145@@ -146,7 +146,8 @@ def run_test(self):
    146         p2p0.send_header_for_blocks(self.blocks[2000:])
    147 
    148         # Send blocks to node0. Block 102 will be rejected.
    149-        self.send_blocks_until_disconnected(p2p0)
    150+        with self.nodes[0].assert_debug_log(expected_msgs=['Enabling signature validations at block #1']):
    151+            self.send_blocks_until_disconnected(p2p0)
    152         self.wait_until(lambda: self.nodes[0].getblockcount() >= COINBASE_MATURITY + 1)
    


    hodlinator commented at 8:02 pm on September 10, 2025:

    nit: In case we are ever able to send away all blocks before detecting the expected log message, we could also wait for the block height before stopping the search of the log:

    0            self.wait_until(lambda: self.nodes[0].getblockcount() >= COINBASE_MATURITY + 1)
    

    (Edit: I was modifying send_blocks_until_disconnected() and noticed a fairly high variability in how many blocks would be sent off, CI probably would have higher variability).


    l0rinc commented at 9:36 pm on September 10, 2025:
    This seems less racy, indeed, updated, added you as coauthor, thanks
  11. hodlinator approved
  12. hodlinator commented at 8:26 pm on September 10, 2025: contributor

    ACK 8cb75ee0a63880b55ac695fe884748aa7d604c38

    While it is unsettling that we don’t fully understand what causes script checking to occur prior to the assumevalid height (https://github.com/bitcoin/bitcoin/pull/32975#discussion_r2329269037) - having an initial “Enabling signature validations at block [#1](/bitcoin-bitcoin/1/) ...” in -assumevalid=0 cases as per this PR seems fine regardless.

    (Empty std::optional does not match bool fScriptChecks until set).

    https://github.com/bitcoin/bitcoin/blob/8cb75ee0a63880b55ac695fe884748aa7d604c38/src/validation.cpp#L2566-L2569

    (Memory behavior when locking/unlocking a mutex (cs_main) does AFAIK imply atomic_ on master was never necessary).

  13. DrahtBot requested review from yuvicc on Sep 10, 2025
  14. TheCharlatan approved
  15. TheCharlatan commented at 9:33 pm on September 10, 2025: contributor

    ACK 8cb75ee0a63880b55ac695fe884748aa7d604c38

    The log message is a bit clunky though. Since it now appears on startup I think it would be nice if it also mentioned that this just reflects assumevalid.

  16. l0rinc commented at 9:36 pm on September 10, 2025: contributor

    that this just reflects assumevalid

    Unfortunately it’s more complicated than that, see #33336 (comment) If you have a concrete suggestion for how to rephrase it, I don’t mind applying it.

  17. l0rinc force-pushed on Sep 10, 2025
  18. l0rinc commented at 5:38 am on September 11, 2025: contributor

    I have instrumented the code, ran some RPC commands and reindexed the whole thing again to see why script verification is always on. After some discussions with @ajtowns it turns out I needed both -assumevalid=<in-range header> and -minimumchainwork=0 to be able to disable script verification.


    After reindex, my node has ~841k headers, a lot lower than the current tip of ~914k. The default assumevalid block (height 912,683) is in that missing range, so the header lookup fails since the block isn’t found in the block index (even though the log claims “Assuming ancestors of block … have valid signatures”). Even when I set a custom assumevalid to a block I actually have, my chainwork is still below the bumped nMinimumChainWork from 755152ac.

    So the results were:

    • Default params: ./build/bin/bitcoind - script verification enabled since block is not among the headers
    • Custom assumevalid from the headers (841,149): ./build/bin/bitcoind -assumevalid=00000000000000000000529087e51068a98d16a071401d7d43494c4a3bf75cd6 - script verification still enabled. Header lookup works fine, but my chainwork (769fbaeb...) is below the required minimum (dee8e2a3...).
    • Default assumevalid with low chainwork: ./build/bin/bitcoind -minimumchainwork=0 - still doesn’t work because it falls back to the default assumevalid which isn’t in my headers.
    • Custom assumevalid with low chainwork: ./build/bin/bitcoind -assumevalid=00000000000000000000529087e51068a98d16a071401d7d43494c4a3bf75cd6 -minimumchainwork=0 - script verification finally gets disabled.

    So maybe we should update the logs to give the reason for the script verification more precisely: script verification is enabled because the assumevalid block cannot be found / the available nChainWork isn’t sufficient / the blocks are too close to the current tip.

    The early “Assuming ancestors of block …” line is emitted even when that header is not present locally yet - maybe we should adjust this, too.

  19. ajtowns commented at 6:04 am on September 11, 2025: contributor

    After reindex, my node has ~841k headers, a lot lower than the current tip of ~914k. The default assumevalid block (height 912,683) is in that missing range,

    I think the behaviour here is probably fine – it’s a weak way of preventing you from triggering assumevalid behaviour on an invalid chain with substantially lower total work than the main chain. It might be nice to have a warning that your headers chain doesn’t meet minchainwork though. It would probably be nicer to be able to obtain the missing headers though.

    EDIT: probably fine, compared to the alternative of dropping the best_header->nChainWork >= MinimumChainWork check, and just relying on the assumevalid block being in the best_header’s ancestry. That didn’t seem to have much rationale when it was introduced afaics: #9484 (comment)

  20. w0xlt commented at 6:55 am on September 11, 2025: contributor

    Approach ACK

    Using std::optional<bool> introduces a third state (unset), which guarantees exactly one initial log message—regardless of whether the first real value is true or false.

    For reviewers, here’s a simple way to verify the behavior change:

    0mkdir temp_data_dir
    1./build/bin/bitcoind -regtest -datadir=temp_data_dir -daemon
    2./build/bin/bitcoin-cli -regtest -datadir=temp_data_dir createwallet w
    3./build/bin/bitcoin-cli -regtest -datadir=temp_data_dir getnewaddress
    4./build/bin/bitcoin-cli -regtest -datadir=temp_data_dir generatetoaddress 1 <address>
    

    With this PR, the logs will include:

    0Enabling signature validations at block [#1](/bitcoin-bitcoin/1/)
    

    In master, this line does not appear.

  21. Eunovo commented at 8:02 am on September 11, 2025: contributor

    ACK https://github.com/bitcoin/bitcoin/pull/33336/commits/39f41135c764c6c4705ce2cd36781ca8d43f0114:

    Previously, users could only verify that script checks were enabled by confirming the absence of a “Disabling signature validations at block #1” message in the logs, which is a confusing negative confirmation pattern. This PR adds explicit positive feedback by ensuring an “Enabling signature validations at block #1” message appears when script checks are enabled, making the system state clearer and easier to verify.

  22. DrahtBot requested review from hodlinator on Sep 11, 2025
  23. DrahtBot requested review from TheCharlatan on Sep 11, 2025
  24. DrahtBot requested review from w0xlt on Sep 11, 2025
  25. Eunovo commented at 8:15 am on September 11, 2025: contributor

    After reindex, my node has ~841k headers, a lot lower than the current tip of ~914k. The default assumevalid block (height 912,683) is in that missing range, so the header lookup fails since the block isn’t found in the block index (even though the log claims “Assuming ancestors of block … have valid signatures”). Even when I set a custom assumevalid to a block I actually have, my chainwork is still below the bumped nMinimumChainWork from 755152ac.

    It’s surprising that #31615 didn’t help with this. If the block wasn’t found in the index, it’s supposed to skip script checks while reindexing. If the block was found in the index, it will skip the MinimumChainwork check. The only reason why script checks will not get skipped is if the chosen assumed valid block is in the index and it is less than 2 weeks away from the tip of the chain, due to this line fScriptChecks = (GetBlockProofEquivalentTime(*m_chainman.m_best_header, *pindex, *m_chainman.m_best_header, params.GetConsensus()) <= 60 * 60 * 24 * 7 * 2);

  26. hodlinator approved
  27. hodlinator commented at 8:18 am on September 11, 2025: contributor

    re-ACK 39f41135c764c6c4705ce2cd36781ca8d43f0114

    Only made test less racy (https://github.com/bitcoin/bitcoin/pull/33336#discussion_r2337774261) since the first ACK.


    After reindex, my node has ~841k headers, a lot lower than the current tip of ~914k. The default assumevalid block (height 912,683) is in that missing range, [would say: is outside that range]

    I think the behaviour here is probably fine – it’s a weak way of preventing you from triggering assumevalid behaviour on an invalid chain with substantially lower total work than the main chain. It might be nice to have a warning that your headers chain doesn’t meet minchainwork though. It would probably be nicer to be able to obtain the missing headers though.

    EDIT: probably fine, compared to the alternative of dropping the best_header->nChainWork >= MinimumChainWork check, and just relying on the assumevalid block being in the best_header’s ancestry. That didn’t seem to have much rationale when it was introduced afaics: #9484 (comment)

    (Bottom of https://github.com/bitcoin/bitcoin/pull/9484/commits/e440ac7ef3b6f3ad1cd8fc7027cece40413202d9 is interesting, enforcing script validation within the last 2 weeks regardless of assumevalid settings. It’s annoying that the current default assume valid block height of 912'683 is ~10 days ago, but shouldn’t matter much).

    I would hope this condition kicked in and filled in the missing headers from 841k to the nMinimumChainWork at 912'683 so that the best header could be updated and script validation could be turned off again until the last 2 weeks of blocks:

    https://github.com/bitcoin/bitcoin/blob/39f41135c764c6c4705ce2cd36781ca8d43f0114/src/net_processing.cpp#L5526-L5539

    Any clue to why missing headers are not being fetched / best header isn’t updated?

  28. achow101 added this to the milestone 30.0 on Sep 11, 2025
  29. w0xlt commented at 0:11 am on September 12, 2025: contributor

    ACK https://github.com/bitcoin/bitcoin/pull/33336/commits/39f41135c764c6c4705ce2cd36781ca8d43f0114

    Simple test on mainnet:

    0./build/bin/bitcoind -datadir=new_data_dir/ -assumevalid=000000009b7262315dbf071787ad3656097b892abffd1f95a1a022f896f533fc -stopatheight=10
    
  30. l0rinc commented at 1:12 am on September 14, 2025: contributor

    It might be nice to have a warning that your headers chain doesn’t meet minchainwork though

    Given how confusing #33336 (comment) was for many of us, it may be beneficial to extend this PR to show the reason for why script verification was enabled.

    Given the number of acks and time constraints, I have to ask @yuvicc, @Eunovo, @hodlinator, @TheCharlatan, @ajtowns, @w0xlt: would that be welcome change, see https://github.com/l0rinc/bitcoin/pull/38/files

    Edit: split the above suggestion into small focused commits and pushed here since I think it’s arguably superior to previous logging attempts.

  31. l0rinc force-pushed on Sep 14, 2025
  32. w0xlt commented at 4:24 am on September 15, 2025: contributor

    Approach ACK.

    beneficial to extend this PR to show the reason for why script verification was enabled.

    I agree.

    split the above suggestion into small focused commits

    Commits could be squashed but but keeping them separate makes it clearer to review.

  33. in src/validation.cpp:2459 in 63dc937eea outdated
    2475+            assume_valid = CHECKED_NOT_UNDER_ASSUMEVALID;
    2476+        } else if (m_chainman.m_best_header->GetAncestor(pindex->nHeight) != pindex) {
    2477+            assume_valid = CHECKED_OFF_BESTHEADER_PATH;
    2478+        } else if (m_chainman.m_best_header->nChainWork < m_chainman.MinimumChainWork()) {
    2479+            assume_valid = CHECKED_BELOW_MIN_CHAINWORK;
    2480+        } else if (GetBlockProofEquivalentTime(*m_chainman.m_best_header, *pindex, *m_chainman.m_best_header, params.GetConsensus()) <= 60 * 60 * 24 * 7 * 2) {
    


    w0xlt commented at 5:21 am on September 15, 2025:

    Just to make clearer what 60 * 60 * 24 * 7 * 2 means.

    0constexpr int64_t TWO_WEEKS = 14 * 24 * 60 * 60;
    1if (GetBlockProofEquivalentTime(... ) <= TWO_WEEKS) { ... }
    

    Or (not tested)

    0#include <chrono>
    1using namespace std::chrono;
    2
    3constexpr auto TWO_WEEKS = 14d; // 14 days
    4
    5if (GetBlockProofEquivalentTime(...) <= duration_cast<seconds>(TWO_WEEKS).count()) {
    6    ...
    7}
    

    l0rinc commented at 5:34 am on September 15, 2025:
    Seems a bit risky this close to the release, maybe we can do that in a follow-up

    l0rinc commented at 5:47 pm on September 15, 2025:
    But extracting TWO_WEEKS_IN_SECONDS is safe, did that, thanks. Added you as coauthor.
  34. l0rinc requested review from hodlinator on Sep 15, 2025
  35. l0rinc requested review from Eunovo on Sep 15, 2025
  36. l0rinc requested review from w0xlt on Sep 15, 2025
  37. in src/validation.cpp:122 in 63dc937eea outdated
    117+    case CHECKED_HASH_NOT_IN_HEADERS:   return "assumevalid hash not in headers";
    118+    case CHECKED_NOT_UNDER_ASSUMEVALID: return "block not under assumevalid anchor";
    119+    case CHECKED_OFF_BESTHEADER_PATH:   return "block not on best header path";
    120+    case CHECKED_BELOW_MIN_CHAINWORK:   return "best header chainwork below nMinimumChainWork";
    121+    case CHECKED_NOT_BURIED_ENOUGH:     return "too recent relative to best header";
    122+    default:                            std::abort();
    


    w0xlt commented at 5:36 am on September 15, 2025:

    Provide safe fallback in release builds. Aborting solely because of logging seems unnecessary.

    0    default:                            return "unknown reason";
    

    l0rinc commented at 5:51 am on September 15, 2025:
    I want a very obvious error if this isn’t correct - I don’t like these soft failures. What do other reviewers think?

    Eunovo commented at 8:48 am on September 15, 2025:

    l0rinc commented at 5:45 pm on September 15, 2025:
    Done

    hodlinator commented at 7:02 pm on September 15, 2025:

    Please consider triggering a compiler warning instead.

    0₿ git grep "// no default case, so the compiler can warn about missing cases" |wc -l
    170
    

    https://github.com/bitcoin/bitcoin/blob/9b04e4f96200daf7516d1b8b6b0dfc4c077837e8/src/deploymentinfo.cpp#L36-L37 Edit: could of course return “unknown reason” rather than empty string in this case, just outside the switch.

  38. in src/validation.cpp:116 in 63dc937eea outdated
    112+{
    113+    using enum AssumeValid;
    114+    switch (v) {
    115+    case SKIPPED:                       return "";
    116+    case CHECKED:                       return "assumevalid=0 (always verify)";
    117+    case CHECKED_HASH_NOT_IN_HEADERS:   return "assumevalid hash not in headers";
    


    w0xlt commented at 5:40 am on September 15, 2025:

    nit:

    0    case CHECKED_HASH_NOT_IN_HEADERS:   return "assumevalid hash not found in headers";
    

    l0rinc commented at 5:49 am on September 15, 2025:
    This was deliberate, are you suggesting that’s not grammatically correct? “File not found” -> we don’t usually say “File is not found”. The internets tell me this is called “elliptical sentence” with “implied copula”.
  39. in src/validation.cpp:120 in 63dc937eea outdated
    115+    case SKIPPED:                       return "";
    116+    case CHECKED:                       return "assumevalid=0 (always verify)";
    117+    case CHECKED_HASH_NOT_IN_HEADERS:   return "assumevalid hash not in headers";
    118+    case CHECKED_NOT_UNDER_ASSUMEVALID: return "block not under assumevalid anchor";
    119+    case CHECKED_OFF_BESTHEADER_PATH:   return "block not on best header path";
    120+    case CHECKED_BELOW_MIN_CHAINWORK:   return "best header chainwork below nMinimumChainWork";
    


    w0xlt commented at 5:42 am on September 15, 2025:

    nit: logs shouldn’t expose internal variable names

    0    case CHECKED_BELOW_MIN_CHAINWORK:   return "best header chainwork below -minimumchainwork";
    

    l0rinc commented at 5:50 am on September 15, 2025:
    I’m curious what others think. The current one makes the error searchable, that’s why I left it as such.

    l0rinc commented at 5:44 pm on September 15, 2025:
    Changed in latest push, thanks
  40. w0xlt commented at 5:46 am on September 15, 2025: contributor
  41. in test/functional/feature_assumevalid.py:194 in 3ddd4c84af outdated
    189+        assert_equal(self.nodes[3].getblockcount(), 1)
    190+
    191+        self.restart_node(3, extra_args=["-reindex-chainstate", "-assumevalid=" + block102.hash_hex, "-minimumchainwork=0xffff"])
    192+        assert_equal(self.nodes[3].getblockcount(), 1)
    193+
    194+        # TODO test what happens when block is not on the best header path
    


    Eunovo commented at 9:47 am on September 15, 2025:
    https://github.com/bitcoin/bitcoin/pull/33336/commits/3ddd4c84af2bf0b5d3e403b71f4750b2326118e1: I think it’s best to take out this TODO from the commit. You should also adjust the commit message to focus on what this commit is adding specifically.

    l0rinc commented at 5:51 pm on September 15, 2025:
    Lol, valid point, let’s document what we are doing, not just what we’re not :D Added comment and updated the commit message (but kept the todo for now), added you as coauthor.
  42. in test/functional/feature_assumevalid.py:68 in 3ddd4c84af outdated
    64@@ -64,11 +65,11 @@ def send_header_for_blocks(self, new_blocks):
    65 class AssumeValidTest(BitcoinTestFramework):
    66     def set_test_params(self):
    67         self.setup_clean_chain = True
    68-        self.num_nodes = 3
    69+        self.num_nodes = 4
    


    Eunovo commented at 9:48 am on September 15, 2025:
    https://github.com/bitcoin/bitcoin/pull/33336/commits/3ddd4c84af2bf0b5d3e403b71f4750b2326118e1: This should probably still be set to 3 since the 4th node is not used in this commit.

    l0rinc commented at 5:49 pm on September 15, 2025:
    the 4th is doing a reindex even in the first commit, right?
  43. in test/functional/feature_assumevalid.py:188 in 3ddd4c84af outdated
    183+        p2p3 = self.nodes[3].add_p2p_connection(BaseNode())
    184+        p2p3.send_header_for_blocks(self.blocks[0:200])
    185+        p2p3.send_without_ping(msg_block(self.blocks[0]))
    186+        self.wait_until(lambda: self.nodes[3].getblockcount())
    187+
    188+        self.restart_node(3, extra_args=["-reindex-chainstate", "-assumevalid=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"])
    


    Eunovo commented at 9:52 am on September 15, 2025:

    l0rinc commented at 5:58 pm on September 15, 2025:
    Done
  44. in test/functional/feature_assumevalid.py:191 in 3ddd4c84af outdated
    186+        self.wait_until(lambda: self.nodes[3].getblockcount())
    187+
    188+        self.restart_node(3, extra_args=["-reindex-chainstate", "-assumevalid=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"])
    189+        assert_equal(self.nodes[3].getblockcount(), 1)
    190+
    191+        self.restart_node(3, extra_args=["-reindex-chainstate", "-assumevalid=" + block102.hash_hex, "-minimumchainwork=0xffff"])
    


    Eunovo commented at 9:52 am on September 15, 2025:

    l0rinc commented at 5:58 pm on September 15, 2025:
    Added one above + explained lightly in commit message
  45. in src/validation.cpp:2441 in 6222d8cced outdated
    2422@@ -2423,7 +2423,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    2423         return true;
    2424     }
    2425 
    2426-    auto assume_valid{true};
    2427+    auto assume_valid{AssumeValid::CHECKED};
    


    Eunovo commented at 10:08 am on September 15, 2025:
    https://github.com/bitcoin/bitcoin/pull/33336/commits/6222d8cced4828b83a9dde20b9da9df44848b9bd: Why not AssumeValid::DISABLED instead of AssumeValid::CHECKED and AssumeValid::ENABLED instead of AssumeValid:SKIPPED? I find the current names to be confusing. I think using AssumeValid::ENABLED to indicate that assume_valid is enabled is better.

    l0rinc commented at 5:32 pm on September 15, 2025:

    I tried explaining it in 6222d8c (#33336)

    Instead of true/false or enabled/disabled, the checked/skipped naming should help with understanding when signature verification is actually performed.

    I found it confusing to say that we disable assume valid - so are we doing the script checks or not, will IBD be faster or slower is it’s disabled?! So what’s a better expression to avoid the confusion?


    l0rinc commented at 8:50 pm on September 16, 2025:
    Reverted since it caused more confusion that it fixed
  46. in src/validation.cpp:119 in 63dc937eea outdated
    114+    switch (v) {
    115+    case SKIPPED:                       return "";
    116+    case CHECKED:                       return "assumevalid=0 (always verify)";
    117+    case CHECKED_HASH_NOT_IN_HEADERS:   return "assumevalid hash not in headers";
    118+    case CHECKED_NOT_UNDER_ASSUMEVALID: return "block not under assumevalid anchor";
    119+    case CHECKED_OFF_BESTHEADER_PATH:   return "block not on best header path";
    


    Eunovo commented at 10:24 am on September 15, 2025:

    l0rinc commented at 5:43 pm on September 15, 2025:
    Done, thanks, added you as coauthor
  47. in src/validation.cpp:118 in 63dc937eea outdated
    113+    using enum AssumeValid;
    114+    switch (v) {
    115+    case SKIPPED:                       return "";
    116+    case CHECKED:                       return "assumevalid=0 (always verify)";
    117+    case CHECKED_HASH_NOT_IN_HEADERS:   return "assumevalid hash not in headers";
    118+    case CHECKED_NOT_UNDER_ASSUMEVALID: return "block not under assumevalid anchor";
    


    Eunovo commented at 10:25 am on September 15, 2025:

    l0rinc commented at 5:43 pm on September 15, 2025:
    Done, thanks
  48. Eunovo commented at 10:28 am on September 15, 2025: contributor
  49. l0rinc force-pushed on Sep 15, 2025
  50. l0rinc commented at 6:00 pm on September 15, 2025: contributor
    Thanks for the suggestions, applied most of them
  51. DrahtBot requested review from Eunovo on Sep 16, 2025
  52. in src/validation.cpp:2426 in e4077fe0d7 outdated
    2422@@ -2423,7 +2423,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    2423         return true;
    2424     }
    2425 
    2426-    bool fScriptChecks = true;
    2427+    auto assume_valid{true};
    


    hodlinator commented at 6:27 am on September 16, 2025:
    In e4077fe0d7ae245e1dd3e89903e818703c0ab130: fScriptChecks seems to be the opposite of what I would expect assume_valid to mean (no script checking)?

    l0rinc commented at 9:05 pm on September 16, 2025:
    Ii have reverted the naming
  53. in src/validation.h:525 in 9b04e4f962 outdated
    520+    CHECKED_HASH_NOT_IN_HEADERS = 2,   //!< assumevalid hash not found in m_block_index
    521+    CHECKED_NOT_UNDER_ASSUMEVALID = 3, //!< pindex is not an ancestor of the assumevalid anchor
    522+    CHECKED_OFF_BESTHEADER_PATH = 4,   //!< pindex is not an ancestor of m_best_header
    523+    CHECKED_BELOW_MIN_CHAINWORK = 5,   //!< best header's cumulative work is below the built-in minimum
    524+    CHECKED_NOT_BURIED_ENOUGH = 6,     //!< too recent compared to best header
    525+};
    


    hodlinator commented at 6:39 am on September 16, 2025:

    Again, AssumeValid::SKIPPED meaning script checking is off seems like some weird double-negation to me. Below naming would make more sense to me.

    0enum class ScriptCheck : uint8_t
    1{
    2    DISABLED_ASSUME_VALID,         //!< skip script verification
    3    ENABLED_NO_ASSUME_VALID,       //!< always verify scripts
    4    ENABLED_HASH_NOT_IN_HEADERS,   //!< assumevalid hash not found in m_block_index
    5    ENABLED_NOT_UNDER_ASSUMEVALID, //!< pindex is not an ancestor of the assumevalid anchor
    6    ENABLED_OFF_BESTHEADER_FORK,   //!< pindex is not an ancestor of m_best_header
    7    ENABLED_BELOW_MIN_CHAINWORK,   //!< best header's cumulative work is below the built-in minimum
    8    ENABLED_NOT_BURIED_ENOUGH,     //!< too recent compared to best header
    9};
    

    (Also removed explicit numbers as we aren’t serializing them to disk or even writing them to the log, but keep if you want).

    (PATH -> FORK).


    l0rinc commented at 6:43 pm on September 16, 2025:

    Cheap script checks (such as sigop counts) are still checked regardless of this option, it’s the signatures that aren’t (so some script sanity checking is still happening, but the scripts aren’t interpreted - that’s my understanding, please correct me if I’m wrong. Maybe I should update the warning message in that case to make it clear that a few other checks are also skipped, not just the signatures).

    And I want to propose a change to skip bip30 checks as part of “assumevalid” (and to skip other script related checks like the mentioned sigop count), so I don’t want to introduce name that forces the option to only apply to signatures. I’ll just revert the rename for now and we can decide later.

  54. in test/functional/feature_assumevalid.py:154 in 9b04e4f962 outdated
    151 
    152         # Send blocks to node0. Block 102 will be rejected.
    153-        self.send_blocks_until_disconnected(p2p0)
    154-        self.wait_until(lambda: self.nodes[0].getblockcount() >= COINBASE_MATURITY + 1)
    155+        with self.nodes[0].assert_debug_log(expected_msgs=['Enabling signature validations at block #1',
    156+                                                           'assumevalid=0 (always verify)']):  # AssumeValid::CHECKED
    


    hodlinator commented at 7:51 am on September 16, 2025:

    Could be more precise in that these strings are on the same log line:

     0diff --git a/test/functional/feature_assumevalid.py b/test/functional/feature_assumevalid.py
     1index 544edfdb99..9ebaddb74f 100755
     2--- a/test/functional/feature_assumevalid.py
     3+++ b/test/functional/feature_assumevalid.py
     4@@ -150,8 +150,9 @@ class AssumeValidTest(BitcoinTestFramework):
     5         p2p0.send_header_for_blocks(self.blocks[2000:])
     6 
     7         # Send blocks to node0. Block 102 will be rejected.
     8-        with self.nodes[0].assert_debug_log(expected_msgs=['Enabling signature validations at block [#1](/bitcoin-bitcoin/1/)',
     9-                                                           'assumevalid=0 (always verify)']):  # AssumeValid::CHECKED
    10+        block_1_hash = self.blocks[0].hash_hex
    11+        with self.nodes[0].assert_debug_log(expected_msgs=[f'Enabling signature validations at block [#1](/bitcoin-bitcoin/1/) ({block_1_hash}): assumevalid=0 (always verify)',  # AssumeValid::CHECKED
    12+                                                           'Block validation error: block-script-verify-flag-failed']):
    13             self.send_blocks_until_disconnected(p2p0)
    14             self.wait_until(lambda: self.nodes[0].getblockcount() >= COINBASE_MATURITY + 1)
    15         assert_equal(self.nodes[0].getblockcount(), COINBASE_MATURITY + 1)
    16@@ -162,8 +163,7 @@ class AssumeValidTest(BitcoinTestFramework):
    17         p2p1.send_header_for_blocks(self.blocks[0:2000])
    18         p2p1.send_header_for_blocks(self.blocks[2000:])
    19         with self.nodes[1].assert_debug_log(expected_msgs=['Disabling signature validations at block [#1](/bitcoin-bitcoin/1/)',  # AssumeValid::SKIPPED
    20-                                                           'Enabling signature validations at block [#103](/bitcoin-bitcoin/103/)',
    21-                                                           'block not part of assumevalid chain']):  # AssumeValid::CHECKED_NOT_UNDER_ASSUMEVALID
    22+                                                           f'Enabling signature validations at block [#103](/bitcoin-bitcoin/103/) ({self.blocks[102].hash_hex}): block not part of assumevalid chain']):  # AssumeValid::CHECKED_NOT_UNDER_ASSUMEVALID
    23             # Send all blocks to node1. All blocks will be accepted.
    24             for i in range(2202):
    25                 p2p1.send_without_ping(msg_block(self.blocks[i]))
    26@@ -177,8 +177,8 @@ class AssumeValidTest(BitcoinTestFramework):
    27         p2p2.send_header_for_blocks(self.blocks[0:200])
    28 
    29         # Send blocks to node2. Block 102 will be rejected.
    30-        with self.nodes[2].assert_debug_log(expected_msgs=["Enabling signature validations at block [#1](/bitcoin-bitcoin/1/)",
    31-                                                           "too recent relative to best header"]):  # AssumeValid::CHECKED_NOT_BURIED_ENOUGH
    32+        with self.nodes[2].assert_debug_log(expected_msgs=[f"Enabling signature validations at block [#1](/bitcoin-bitcoin/1/) ({block_1_hash}): too recent relative to best header",  # AssumeValid::CHECKED_NOT_BURIED_ENOUGH
    33+                                                           "Block validation error: block-script-verify-flag-failed"]):
    34             self.send_blocks_until_disconnected(p2p2)
    35             self.wait_until(lambda: self.nodes[2].getblockcount() >= COINBASE_MATURITY + 1)
    36         assert_equal(self.nodes[2].getblockcount(), COINBASE_MATURITY + 1)
    37@@ -191,13 +191,11 @@ class AssumeValidTest(BitcoinTestFramework):
    38         self.wait_until(lambda: self.nodes[3].getblockcount())
    39 
    40         # Reindex to deterministically hit specific assumevalid gates (no races with header downloads/chainwork during startup).
    41-        with self.nodes[3].assert_debug_log(expected_msgs=["Enabling signature validations at block [#1](/bitcoin-bitcoin/1/)",
    42-                                                           "assumevalid hash not in headers"]):  # AssumeValid::CHECKED_HASH_NOT_IN_HEADERS
    43+        with self.nodes[3].assert_debug_log(expected_msgs=[f"Enabling signature validations at block [#1](/bitcoin-bitcoin/1/) ({block_1_hash}): assumevalid hash not in headers"]):  # AssumeValid::CHECKED_HASH_NOT_IN_HEADERS
    44             self.restart_node(3, extra_args=["-reindex-chainstate", "-assumevalid=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"])
    45         assert_equal(self.nodes[3].getblockcount(), 1)
    46 
    47-        with self.nodes[3].assert_debug_log(expected_msgs=["Enabling signature validations at block [#1](/bitcoin-bitcoin/1/)",
    48-                                                           "best header chainwork below minimumchainwork"]):  # AssumeValid::CHECKED_BELOW_MIN_CHAINWORK
    49+        with self.nodes[3].assert_debug_log(expected_msgs=[f"Enabling signature validations at block [#1](/bitcoin-bitcoin/1/) ({block_1_hash}): best header chainwork below minimumchainwork"]):  # AssumeValid::CHECKED_BELOW_MIN_CHAINWORK
    50             self.restart_node(3, extra_args=["-reindex-chainstate", "-assumevalid=" + block102.hash_hex, "-minimumchainwork=0xffff"])
    51         assert_equal(self.nodes[3].getblockcount(), 1)
    52 
    

    (Also adds check for “Block validation error”).


    l0rinc commented at 5:32 pm on September 16, 2025:
    Was trying to recreate production code logic in test, but I don’t mind making it more explicit if you think it’s more descriptive. The block validation errors seem out-of-scope, but I don’t mind them either, thanks, added you as coauthor to the last commit as well.
  55. in test/functional/feature_assumevalid.py:166 in 9b04e4f962 outdated
    164         p2p1.send_header_for_blocks(self.blocks[0:2000])
    165         p2p1.send_header_for_blocks(self.blocks[2000:])
    166-        with self.nodes[1].assert_debug_log(expected_msgs=['Disabling signature validations at block #1', 'Enabling signature validations at block #103']):
    167+        with self.nodes[1].assert_debug_log(expected_msgs=['Disabling signature validations at block #1',  # AssumeValid::SKIPPED
    168+                                                           'Enabling signature validations at block #103',
    169+                                                           'block not part of assumevalid chain']):  # AssumeValid::CHECKED_NOT_UNDER_ASSUMEVALID
    


    hodlinator commented at 8:55 am on September 16, 2025:

    It feels a bit lacking to have CHECKED_NOT_UNDER_ASSUMEVALID when the block is based upon the assumevalid block. Would expect something like CHECKED_EXCEEDS_ASSUMEVALID. For validation.cpp:

    0        } else if (it->second.nHeight < pindex->nHeight) {
    1            assume_valid = CHECKED_EXCEEDS_ASSUMEVALID;
    2        } else if (it->second.GetAncestor(pindex->nHeight) != pindex) {
    3            assume_valid = CHECKED_NOT_UNDER_ASSUMEVALID;
    

    l0rinc commented at 8:57 pm on September 16, 2025:
    I’m not sure I fully understand - do you want me to add a different reason when the current block is valid, but just after the assumevalid block? That’s a very good idea, but I think we should do it separately since it modifies the behavior a tiny bit

    hodlinator commented at 11:56 am on September 18, 2025:

    Yes, I would prefer having a separate reason message for when we have passed beyond the assumevalid height since it’s so fundamental to assumevalid, and so different from being at a height below assumevalid on a forked chain.

    Agree it probably belongs in a separate commit, but still think it would be good to have in this PR. Your first commit that currently introduces "block not part of assumevalid chain" should probably state something more neutral like "block is not an ancestor of the assumevalid block". Then with the separate commit:

    0+        } else if (it->second.nHeight < pindex->nHeight) {
    1+            script_check_reason = "block height exceeds assumevalid height";
    2         } else if (it->second.GetAncestor(pindex->nHeight) != pindex) {
    3-            script_check_reason = "block is not an ancestor of the assumevalid block";
    4+            script_check_reason = "block not part of assumevalid chain";
    

    l0rinc commented at 10:54 pm on September 18, 2025:
    I missed this comment somehow. While this is a tiny change to the validation logic, I think we can make it more focused by putting it inside the original GetAncestor(pindex->nHeight) != pindex block. Now that it was removed from V30 we can safely do this I think - and the messages would be a lot more useful this way. Added a new test to cover this and unified the test styles as well since it’s not that urgent anymore.

    hodlinator commented at 8:47 am on September 19, 2025:
    Thanks!
  56. in src/validation.cpp:2456 in 9b04e4f962 outdated
    2472+        if (it == m_blockman.m_block_index.end()) {
    2473+            assume_valid = CHECKED_HASH_NOT_IN_HEADERS;
    2474+        } else if (it->second.GetAncestor(pindex->nHeight) != pindex) {
    2475+            assume_valid = CHECKED_NOT_UNDER_ASSUMEVALID;
    2476+        } else if (m_chainman.m_best_header->GetAncestor(pindex->nHeight) != pindex) {
    2477+            assume_valid = CHECKED_OFF_BESTHEADER_PATH;
    


    hodlinator commented at 9:36 am on September 16, 2025:

    If we get here, we know from the previous check (if (it->second.GetAncestor(pindex->nHeight) != pindex) assume_valid = CHECKED_NOT_UNDER_ASSUMEVALID) that pindex now does exist below the assumevalid header (or is the assumevalid block).

    For the check against best header chain to fail for pindex, it could either be:

    1. The best header height somehow being below both pindex and assumevalid. (Edit: I guess this could theoretically happen if a really strong miner ramped up the difficulty and mined a fork of the chain from below the assumevalid height, but still with more total chain work).
    2. The best header chain being a totally different chain that doesn’t even include the assumevalid hash (unless someone has broken SHA256).

    Why would we want to do script checking of the pindex block which is in the chain below the assumevalid hash for any of those cases? This is keeping the existing behavior on master, but that behavior is weird to me.

    I guess this is also why you’ve had issues adding a test for CHECKED_OFF_BESTHEADER_PATH.


    hodlinator commented at 10:31 am on September 16, 2025:

    Found a way to test 2):

     0        # nodes[4]
     1        self.start_node(4, extra_args=["-assumevalid=" + block102.hash_hex])
     2        second_chain_tip = int(self.nodes[4].getbestblockhash(), 16)
     3        second_chain_time = self.nodes[4].getblock(self.nodes[4].getbestblockhash())['time'] + 1
     4        second_chain_height = self.nodes[4].getblock(self.nodes[4].getbestblockhash())['height'] + 1
     5
     6        second_chain = []
     7        for _ in range(2100):
     8            block = create_block(second_chain_tip, create_coinbase(second_chain_height), second_chain_time)
     9            block.solve()
    10            second_chain.append(block)
    11            second_chain_tip = block.hash_int
    12            second_chain_time += 1
    13            second_chain_height += 1
    14
    15        p2p4 = self.nodes[4].add_p2p_connection(BaseNode())
    16        p2p4.send_header_for_blocks(second_chain[0:2000])
    17        p2p4.send_header_for_blocks(second_chain[2000:])
    18        p2p4.send_header_for_blocks(self.blocks[0:103])
    19
    20        with self.nodes[4].assert_debug_log(expected_msgs=[f'Enabling signature validations at block [#1](/bitcoin-bitcoin/1/) ({block_1_hash}): block not on best header chain']):  # AssumeValid::CHECKED_OFF_BESTHEADER_PATH
    21            # Send all blocks to node1. All blocks will be accepted.
    22            p2p4.send_without_ping(msg_block(self.blocks[0]))
    23            self.wait_until(lambda: self.nodes[2].getblockcount() >= 1)
    

    l0rinc commented at 8:52 pm on September 16, 2025:
    fantastic, I had something similar but only got to 90%. Added the change as node 3 since it has a similar starting condition as the rest of the nodes, simplifies the header send to only create the needed headers and send all of them and fixed the waiting condition. Added you as coauthor.
  57. hodlinator commented at 9:58 am on September 16, 2025: contributor
    Reviewed 9b04e4f96200daf7516d1b8b6b0dfc4c077837e8
  58. DrahtBot requested review from hodlinator on Sep 16, 2025
  59. in test/functional/feature_assumevalid.py:186 in 4cd11cef0f outdated
    178@@ -170,5 +179,21 @@ def run_test(self):
    179         assert_equal(self.nodes[2].getblockcount(), COINBASE_MATURITY + 1)
    180 
    181 
    182+        # nodes[3]
    183+        p2p3 = self.nodes[3].add_p2p_connection(BaseNode())
    184+        p2p3.send_header_for_blocks(self.blocks[0:200])
    185+        p2p3.send_without_ping(msg_block(self.blocks[0]))
    186+        self.wait_until(lambda: self.nodes[3].getblockcount())
    


    TheCharlatan commented at 10:11 am on September 16, 2025:
    I’m a bit confused what the condition in this test is supposed to trigger if all we are testing are invalid args. What is the rationale here?

    l0rinc commented at 5:36 pm on September 16, 2025:
    The args are only invalid in relation to the blocks, so we need at least one block to invalidate the args.

    hodlinator commented at 9:10 am on September 19, 2025:
    (Thanks for changing to self.wait_until(lambda: self.nodes[3].getblockcount() == 1)).
  60. in src/validation.cpp:2597 in 9b04e4f962 outdated
    2595+                    pindex->nHeight, block_hash.ToString());
    2596+        } else {
    2597+            LogInfo("Enabling signature validations at block #%d (%s): %s.",
    2598+                    pindex->nHeight, block_hash.ToString(), assumevalid_reason(assume_valid));
    2599+        }
    2600+        m_prev_assume_valid_logged = assume_valid;
    


    TheCharlatan commented at 11:15 am on September 16, 2025:
    Are the enums really only added for logging? That seems a bit heavy. How about moving this if block up and making it a lambda that gets called directly from your if else branches? afaict that could remove the need for them, which judged by the comments left here so far, seem to confuse people a bit with their communicated intent.

    l0rinc commented at 8:55 pm on September 16, 2025:
    I have added a reason string with limited scope next to the bool, let me know what you think. Added you ad coauthor.
  61. DrahtBot requested review from TheCharlatan on Sep 16, 2025
  62. l0rinc force-pushed on Sep 16, 2025
  63. l0rinc commented at 9:03 pm on September 16, 2025: contributor

    Thanks for the reviews, I have simplified the commits, removed the enum and replaced it with direct reason string, added an off best‑header path (thanks to @hodlinator), reworded the log to say “script verification” instead of “signature validations” to be in alignment with the “Script verification uses … additional threads” and reverted the fScriptChecks and m_prev_script_checks_logged symbols.

    There’s a remaining suggestion to have friendlier reason in case of:

    02025-09-16T21:00:03Z Enabling script verification at block [#915016](/bitcoin-bitcoin/915016/) (000000000000000000001a6f6115c32c340a2159b30f9ffd632f63424aa563f9): block not part of assumevalid chain.
    

    to separate when the block has the assumevalid as an ancestor. I found that a bit riskier change than the rest, I think we should do that in a follow-up instead.

  64. l0rinc requested review from w0xlt on Sep 16, 2025
  65. l0rinc force-pushed on Sep 16, 2025
  66. TheCharlatan commented at 11:52 am on September 17, 2025: contributor
    Approach ACK
  67. in src/validation.cpp:2582 in f6951cb74b outdated
    2579+            Assume(!script_check_reason.empty());
    2580+            LogInfo("Enabling script verification at block #%d (%s): %s.",
    2581+                    pindex->nHeight, block_hash.ToString(), script_check_reason);
    2582+        } else {
    2583+            LogInfo("Disabling script verification at block #%d (%s).",
    2584+                    pindex->nHeight, block_hash.ToString());
    


    luke-jr commented at 5:05 am on September 18, 2025:
    Should we be logging which chainstate this is, in case there’s multiple?

    l0rinc commented at 5:15 am on September 18, 2025:
    Currently we’re only logging this if there’s a single one, i.e. GetRole() == ChainstateRole::NORMAL
  68. in test/functional/feature_assumevalid.py:16 in 788e45a5b2 outdated
    22-              (2100 blocks)
    23+ 0:        genesis block
    24+ 1:        block 1 with coinbase transaction output.
    25+ 2-101:    bury that block with 100 blocks so the coinbase transaction output can be spent
    26+ 102:      a block containing a transaction spending the coinbase transaction output. The transaction has an invalid signature.
    27+ 103-2202: bury the bad block with just over two weeks' worth of blocks (2100 blocks)
    


    hodlinator commented at 9:06 am on September 18, 2025:
    nit in 788e45a5b210224e476543cbd31dffc7ae39b13e: Some sympathy for unwrapping the initial sentence since with the (and/an) typo fix it’s just at 80 chars, but 4 space indent seems less arbitrary than 1 space, and not worth touching these lines for.

    l0rinc commented at 5:30 pm on September 18, 2025:
    This seemed like the time to simplify these, but I don’t mind, reverted.

    achow101 commented at 7:35 pm on September 18, 2025:
    Looks like the revert is in the wrong commit.

    l0rinc commented at 9:11 pm on September 18, 2025:
    Done
  69. in test/functional/feature_assumevalid.py:142 in 9ecaadc94a outdated
    130@@ -129,19 +131,23 @@ def run_test(self):
    131             self.block_time += 1
    132             height += 1
    133 
    134-        # Start node1 and node2 with assumevalid so they accept a block with a bad signature.
    135-        self.start_node(1, extra_args=["-assumevalid=" + block102.hash_hex])
    136-        self.start_node(2, extra_args=["-assumevalid=" + block102.hash_hex])
    


    hodlinator commented at 9:12 am on September 18, 2025:

    nit in 9ecaadc94a5a4a61263aa17ddaba23ee5d155042: Think there is some value in benefit from nodes starting up in parallel with the python code running. Would prefer starting them all here or even sooner, before the loop generating 2100 blocks above.

    Sorry if my suggestion in #33336 (review) precipitated this, I was just trying to reduce complexity in the example.


    l0rinc commented at 5:30 pm on September 18, 2025:
    No problem, I put them back to the beginning
  70. in src/validation.cpp:2426 in f6951cb74b outdated
    2422@@ -2423,35 +2423,45 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    2423         return true;
    2424     }
    2425 
    2426-    bool fScriptChecks = true;
    2427+    auto fScriptChecks{true};
    


    hodlinator commented at 9:20 am on September 18, 2025:
    In b577becd7167593ba55f1f084cfded59f4c11e47: Touching this line to reduce it by 1 char seems uncalled for now that you are keeping the name.

    l0rinc commented at 5:30 pm on September 18, 2025:
    I think this is trivial to review. Edit: removed eventually because of reason refactor.
  71. in test/functional/feature_assumevalid.py:195 in f6951cb74b outdated
    198+            block = create_block(second_chain_tip, create_coinbase(second_chain_height), second_chain_time)
    199+            block.solve()
    200+            second_chain.append(block)
    201+            second_chain_tip, second_chain_time, second_chain_height = block.hash_int, second_chain_time + 1, second_chain_height + 1
    202+
    203+        p2p4 = self.nodes[3].add_p2p_connection(BaseNode())
    


    hodlinator commented at 9:38 am on September 18, 2025:
    0        p2p3 = self.nodes[3].add_p2p_connection(BaseNode())
    

    l0rinc commented at 5:30 pm on September 18, 2025:
    Fixed name
  72. in src/validation.cpp:2427 in f6951cb74b outdated
    2422@@ -2423,35 +2423,45 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    2423         return true;
    2424     }
    2425 
    2426-    bool fScriptChecks = true;
    2427+    auto fScriptChecks{true};
    2428+    std::string script_check_reason;
    


    hodlinator commented at 11:38 am on September 18, 2025:

    nit: I’d prefer avoiding having this dual state. Here are 2 variations which do that:

    Preserving behavior:

     0diff --git a/src/validation.cpp b/src/validation.cpp
     1index 7e82c3bbad..008bfe4d71 100644
     2--- a/src/validation.cpp
     3+++ b/src/validation.cpp
     4@@ -2423,8 +2423,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
     5         return true;
     6     }
     7 
     8-    auto fScriptChecks{true};
     9-    std::string script_check_reason;
    10+    const char* script_check_reason{nullptr};
    11     if (!m_chainman.AssumedValidBlock().IsNull()) {
    12         constexpr int64_t TWO_WEEKS_IN_SECONDS{60 * 60 * 24 * 7 * 2};
    13         // We've been configured with the hash of a block which has been externally verified to have a valid history.
    14@@ -2458,7 +2457,6 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    15             //  artificially set the default assumed verified block further back.
    16             // The test against the minimum chain work prevents the skipping when denied access to any chain at
    17             //  least as good as the expected chain.
    18-            fScriptChecks = false;
    19         }
    20     } else {
    21         script_check_reason = "assumevalid=0 (always verify)";
    22@@ -2573,16 +2571,15 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    23              Ticks<SecondsDouble>(m_chainman.time_forks),
    24              Ticks<MillisecondsDouble>(m_chainman.time_forks) / m_chainman.num_blocks_total);
    25 
    26-    if (fScriptChecks != m_prev_script_checks_logged && GetRole() == ChainstateRole::NORMAL) {
    27-        if (fScriptChecks) {
    28-            Assume(!script_check_reason.empty());
    29+    if ((script_check_reason != nullptr) != m_prev_script_checks_logged && GetRole() == ChainstateRole::NORMAL) {
    30+        if (script_check_reason) {
    31             LogInfo("Enabling script verification at block #%d (%s): %s.",
    32                     pindex->nHeight, block_hash.ToString(), script_check_reason);
    33         } else {
    34             LogInfo("Disabling script verification at block #%d (%s).",
    35                     pindex->nHeight, block_hash.ToString());
    36         }
    37-        m_prev_script_checks_logged = fScriptChecks;
    38+        m_prev_script_checks_logged = script_check_reason != nullptr;
    39     }
    40 
    41     CBlockUndo blockundo;
    42@@ -2593,7 +2590,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    43     // doesn't invalidate pointers into the vector, and keep txsdata in scope
    44     // for as long as `control`.
    45     std::optional<CCheckQueueControl<CScriptCheck>> control;
    46-    if (auto& queue = m_chainman.GetCheckQueue(); queue.HasThreads() && fScriptChecks) control.emplace(queue);
    47+    if (auto& queue = m_chainman.GetCheckQueue(); queue.HasThreads() && script_check_reason != nullptr) control.emplace(queue);
    48 
    49     std::vector<PrecomputedTransactionData> txsdata(block.vtx.size());
    50 
    51@@ -2652,7 +2649,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    52             break;
    53         }
    54 
    55-        if (!tx.IsCoinBase() && fScriptChecks)
    56+        if (!tx.IsCoinBase() && script_check_reason != nullptr)
    57         {
    58             bool fCacheResults = fJustCheck; /* Don't cache results if we're actually connecting blocks (still consult the cache, though) */
    59             bool tx_ok;
    
     0diff --git a/src/validation.cpp b/src/validation.cpp
     1index 7e82c3bbad..79ad71b64c 100644
     2--- a/src/validation.cpp
     3+++ b/src/validation.cpp
     4@@ -2423,8 +2423,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
     5         return true;
     6     }
     7 
     8-    auto fScriptChecks{true};
     9-    std::string script_check_reason;
    10+    const char* script_check_reason{nullptr};
    11     if (!m_chainman.AssumedValidBlock().IsNull()) {
    12         constexpr int64_t TWO_WEEKS_IN_SECONDS{60 * 60 * 24 * 7 * 2};
    13         // We've been configured with the hash of a block which has been externally verified to have a valid history.
    14@@ -2458,7 +2457,6 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    15             //  artificially set the default assumed verified block further back.
    16             // The test against the minimum chain work prevents the skipping when denied access to any chain at
    17             //  least as good as the expected chain.
    18-            fScriptChecks = false;
    19         }
    20     } else {
    21         script_check_reason = "assumevalid=0 (always verify)";
    22@@ -2573,16 +2571,15 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    23              Ticks<SecondsDouble>(m_chainman.time_forks),
    24              Ticks<MillisecondsDouble>(m_chainman.time_forks) / m_chainman.num_blocks_total);
    25 
    26-    if (fScriptChecks != m_prev_script_checks_logged && GetRole() == ChainstateRole::NORMAL) {
    27-        if (fScriptChecks) {
    28-            Assume(!script_check_reason.empty());
    29+    if (script_check_reason != m_prev_script_check_reason && GetRole() == ChainstateRole::NORMAL) {
    30+        if (script_check_reason) {
    31             LogInfo("Enabling script verification at block #%d (%s): %s.",
    32                     pindex->nHeight, block_hash.ToString(), script_check_reason);
    33         } else {
    34             LogInfo("Disabling script verification at block #%d (%s).",
    35                     pindex->nHeight, block_hash.ToString());
    36         }
    37-        m_prev_script_checks_logged = fScriptChecks;
    38+        m_prev_script_check_reason = script_check_reason;
    39     }
    40 
    41     CBlockUndo blockundo;
    42@@ -2593,7 +2590,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    43     // doesn't invalidate pointers into the vector, and keep txsdata in scope
    44     // for as long as `control`.
    45     std::optional<CCheckQueueControl<CScriptCheck>> control;
    46-    if (auto& queue = m_chainman.GetCheckQueue(); queue.HasThreads() && fScriptChecks) control.emplace(queue);
    47+    if (auto& queue = m_chainman.GetCheckQueue(); queue.HasThreads() && script_check_reason) control.emplace(queue);
    48 
    49     std::vector<PrecomputedTransactionData> txsdata(block.vtx.size());
    50 
    51@@ -2652,7 +2649,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    52             break;
    53         }
    54 
    55-        if (!tx.IsCoinBase() && fScriptChecks)
    56+        if (!tx.IsCoinBase() && script_check_reason)
    57         {
    58             bool fCacheResults = fJustCheck; /* Don't cache results if we're actually connecting blocks (still consult the cache, though) */
    59             bool tx_ok;
    60diff --git a/src/validation.h b/src/validation.h
    61index fea11c5515..6e426a4660 100644
    62--- a/src/validation.h
    63+++ b/src/validation.h
    64@@ -560,7 +560,7 @@ protected:
    65     //! Cached result of LookupBlockIndex(*m_from_snapshot_blockhash)
    66     mutable const CBlockIndex* m_cached_snapshot_base GUARDED_BY(::cs_main){nullptr};
    67 
    68-    std::optional<bool> m_prev_script_checks_logged GUARDED_BY(::cs_main){};
    69+    std::optional<const char*> m_prev_script_check_reason GUARDED_BY(::cs_main){};
    70 
    71 public:
    72     //! Reference to a BlockManager instance which itself is shared across all
    

    l0rinc commented at 5:30 pm on September 18, 2025:
    I did this deliberately (edit: I added enums originally to separate the validation decision from the error message), but thinking a bit more I think we can make it work. Pushed something similar, let me know what you think.

    hodlinator commented at 8:56 am on September 19, 2025:

    Thanks for combining the variables!

    Could you please lay down your aversion to using const char* here? Benefits I see over std::string:

    • Allocation-free.
    • Quick comparisons (pointer values rather than fetching the two cache lines where the strings are allocated on the heap and doing string comparisons).

    I know it’s not performance-critical, but don’t see any major drawbacks to it. std::string has never been claimed to be a zero-cost abstraction alternative to const char*.


    l0rinc commented at 10:12 pm on September 19, 2025:
    I don’t think it makes much difference, but looks like you do, so I have changed it to const char*, let me know what you think.

    hodlinator commented at 9:44 pm on September 20, 2025:
    Warp speed engaged, thank you!
  73. hodlinator commented at 11:44 am on September 18, 2025: contributor
    Reviewed f6951cb74bba60fedb2e0c041038561ba2583594
  74. l0rinc force-pushed on Sep 18, 2025
  75. achow101 commented at 8:26 pm on September 18, 2025: member
    Given that the scope of the changes in this PR has expanded quite a bit since it was added to the milestone, and since the changes take place within consensus validation, I think this should be removed from the milestone. I don’t think this will be able to get sufficient review for this release, and I don’t think the changes here are necessary for the upcoming release.
  76. achow101 removed this from the milestone 30.0 on Sep 18, 2025
  77. l0rinc force-pushed on Sep 18, 2025
  78. log: reword `signature validations` to `script verification` in `assumevalid` log
    Even though not all script verification is turned off currently (e.g. we're still doing the cheaper sigop counts), this naming is more consistent with other usages.
    91ac64b0a6
  79. l0rinc force-pushed on Sep 18, 2025
  80. l0rinc commented at 10:56 pm on September 18, 2025: contributor
    Thanks for the comments, adjusted the test and code to differentiate between cases when the block is after the assumevalid block from when it’s not even on the same chain. Also adjusted the style of the test to have more self-contained blocks separated by the with blocks, unified the test string styles, updated old commit messages. Pushed the change and the rebase separately.
  81. in src/validation.h:563 in ed9d573dbf outdated
    559@@ -560,7 +560,7 @@ class Chainstate
    560     //! Cached result of LookupBlockIndex(*m_from_snapshot_blockhash)
    561     mutable const CBlockIndex* m_cached_snapshot_base GUARDED_BY(::cs_main){nullptr};
    562 
    563-    std::optional<bool> m_prev_script_checks_logged GUARDED_BY(::cs_main){};
    564+    std::optional<std::string> m_last_script_check_reason_logged GUARDED_BY(::cs_main){""}; // init with foreign value
    


    hodlinator commented at 8:43 am on September 19, 2025:
    Don’t see much of a point to change m_prev_script_checks_logged types twice within the PR (atomic_bool -> optional<bool> -> optional<string>)? Would suggest swapping the order of 00194b5c and 0ab8939c and then squashing 0ab8939c together with ed9d573d.

    l0rinc commented at 9:25 pm on September 19, 2025:

    Don’t see much of a point to change m_prev_script_checks_logged types twice

    I deliberately separated “enabling” logs changes from enabling reasons to show the changes in small steps, reflected in the tests. They’re fundamentally different changes that are reflected in separate test and behavior changes, do you feel strongly about merging them?


    hodlinator commented at 9:56 pm on September 19, 2025:
    Curious what other reviewers think, but not a blocker.

    l0rinc commented at 10:03 pm on September 19, 2025:
    I find it important to modify this area of the code in tiny steps, it’s why I separated the two concerns in the first place.
  82. in test/functional/feature_assumevalid.py:35 in 38025edd19
    26@@ -28,6 +27,12 @@
    27     - node2 has -assumevalid set to the hash of block 102. Try to sync to
    28       block 200. node2 will reject block 102 since it's assumed valid, but it
    29       isn't buried by at least two weeks' work.
    30+    - node3 has -assumevalid set to the hash of block 102. Feed a longer
    31+      competing headers-only branch so block #1 is not on the best header chain.
    32+    - node4 has -assumevalid set to the hash of block 102. Submit an alternative
    33+      block #1 that is not part of the assumevalid chain.
    34+    - node5 has no -assumevalid parameter. Reindex to deterministically hit
    35+      "assumevalid hash not in headers" and "below minimum chainwork".
    


    hodlinator commented at 9:20 am on September 19, 2025:

    nit:

    0    - node5 starts with no -assumevalid parameter. Reindex to deterministically hit
    1      "assumevalid hash not in headers" and "below minimum chainwork".
    

    l0rinc commented at 10:12 pm on September 19, 2025:
    Done
  83. in test/functional/feature_assumevalid.py:166 in 38025edd19 outdated
    175+            p2p0.send_header_for_blocks(self.blocks[0:2000])
    176+            p2p0.send_header_for_blocks(self.blocks[2000:])
    177+
    178+            self.send_blocks_until_disconnected(p2p0)
    179+            self.wait_until(lambda: self.nodes[0].getblockcount() >= COINBASE_MATURITY + 1)
    180+            assert_equal(self.nodes[0].getblockcount(), COINBASE_MATURITY + 1)
    


    hodlinator commented at 9:33 am on September 19, 2025:
    nit: Had a back & forth out-of-band about increasing the with-block scopes. While it makes the code layout nicer, it makes for slightly sloppier tests. But initializing a p2p connection and then doing some asserts at the end of the block doesn’t seem too bad, so this is nit-level.
  84. hodlinator commented at 9:35 am on September 19, 2025: contributor
    Reviewed 38025edd1978670bfa3c32cf518acd0b01644167
  85. l0rinc renamed this:
    log: always print initial signature verification state
    log: always print initial script verification state
    on Sep 19, 2025
  86. l0rinc force-pushed on Sep 19, 2025
  87. hodlinator approved
  88. hodlinator commented at 9:47 pm on September 20, 2025: contributor
    re-ACK 010cf81407c0df8de766fd2a116463d180f25f33
  89. in src/validation.cpp:2426 in c99fde2c0a outdated
    2422@@ -2423,7 +2423,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    2423         return true;
    2424     }
    2425 
    2426-    bool fScriptChecks = true;
    2427+    const char* script_check_reason;
    


    Eunovo commented at 12:07 pm on September 22, 2025:
    https://github.com/bitcoin/bitcoin/pull/33336/commits/c99fde2c0ac181da656e8c942fa6fea93713de75: wouldn’t it be better to just do std::optional<std::string> script_check_reason?

    l0rinc commented at 1:18 am on September 23, 2025:
    That’s what I had before, but this was specifically requested: #33336 (review) It’s not a very big difference in my opinion, I don’t mind either.

    Eunovo commented at 5:32 am on September 23, 2025:
    Alright
  90. in src/validation.h:563 in c99fde2c0a outdated
    559@@ -560,7 +560,7 @@ class Chainstate
    560     //! Cached result of LookupBlockIndex(*m_from_snapshot_blockhash)
    561     mutable const CBlockIndex* m_cached_snapshot_base GUARDED_BY(::cs_main){nullptr};
    562 
    563-    std::optional<bool> m_prev_script_checks_logged GUARDED_BY(::cs_main){};
    564+    std::optional<const char*> m_last_script_check_reason_logged GUARDED_BY(::cs_main){};
    


    Eunovo commented at 12:07 pm on September 22, 2025:

    l0rinc commented at 3:18 pm on September 22, 2025:
    it was specifically requested in #33336 (review)

    Eunovo commented at 5:33 am on September 23, 2025:
    Alright
  91. Eunovo commented at 12:08 pm on September 22, 2025: contributor
    Left a few comments
  92. DrahtBot requested review from Eunovo on Sep 22, 2025
  93. in src/validation.cpp:2430 in 010cf81407 outdated
    2422@@ -2423,35 +2423,44 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    2423         return true;
    2424     }
    2425 
    2426-    bool fScriptChecks = true;
    2427+    const char* script_check_reason;
    2428     if (!m_chainman.AssumedValidBlock().IsNull()) {
    2429+        constexpr int64_t TWO_WEEKS_IN_SECONDS{60 * 60 * 24 * 7 * 2};
    


    ajtowns commented at 5:49 am on September 23, 2025:

    Perhaps:

    0constexpr std::chrono::days TOO_RECENT_FOR_ASSUMEVALID{14};
    1...
    2} else if (GBPET(...) <= count_seconds(TOO_RECENT_FOR_ASSUME_VALID)) {
    

    l0rinc commented at 5:02 pm on September 23, 2025:
    I don’t mind doing this in a separate PR, but here I have decided against it since it’s not strictly related to the change and is in consensus code, see: #33336 (review)
  94. in src/validation.cpp:2438 in 010cf81407
    2454-                fScriptChecks = (GetBlockProofEquivalentTime(*m_chainman.m_best_header, *pindex, *m_chainman.m_best_header, params.GetConsensus()) <= 60 * 60 * 24 * 7 * 2);
    2455-            }
    2456+        if (it == m_blockman.m_block_index.end()) {
    2457+            script_check_reason = "assumevalid hash not in headers";
    2458+        } else if (it->second.GetAncestor(pindex->nHeight) != pindex) {
    2459+            script_check_reason = (pindex->nHeight > it->second.nHeight) ? "block height above assumevalid height" : "block not part of assumevalid chain";
    


    ajtowns commented at 5:52 am on September 23, 2025:
    0} else if (pindex->nHeight > it->second.nHeight) {
    1    script_check_reason = "block height above ..";
    2} else if (it->second.GetAncestor(pindex->nHeight) != pindex) {
    3    script_check_Reason = "block not part of av chain";
    4...
    

    l0rinc commented at 5:02 pm on September 23, 2025:
    that was the original suggestion as well, see #33336 (review) I decided against it since I don’t want to introduce an extra explicit assumevalid branch, it’s still the same case (not part of av chain), just with better message - seems conceptually easier to grasp if we have fewer overall categories.
  95. in src/validation.cpp:2463 in 010cf81407
    2479+            // The test against the minimum chain work prevents the skipping when denied access to any chain at
    2480+            //  least as good as the expected chain.
    2481+            script_check_reason = nullptr;
    2482         }
    2483+    } else {
    2484+        script_check_reason = "assumevalid=0 (always verify)";
    


    ajtowns commented at 6:02 am on September 23, 2025:

    switch the check to:

    0if (AssumedValidBlock().IsNull()) {
    1    script_check_reason = "assumevalid=0";
    2} else {
    3     // ... long code block ...
    4}
    

    ?


    l0rinc commented at 5:02 pm on September 23, 2025:
    I thought of this also, we can even add this error message as default and overwrite in every other case. If other reviewers also thing this would be simpler, I can do it in the next push, if needed.

    l0rinc commented at 3:37 am on October 2, 2025:
    Done, thanks
  96. in src/validation.cpp:2453 in 010cf81407 outdated
    2467+            // This block is a member of the assumed verified chain and an ancestor of the best header.
    2468+            // Script verification is skipped when connecting blocks under the
    2469+            //  assumevalid block. Assuming the assumevalid block is valid this
    2470+            //  is safe because block merkle hashes are still computed and checked,
    2471+            // Of course, if an assumed valid block is invalid due to false scriptSigs
    2472+            //  this optimization would allow an invalid chain to be accepted.
    


    ajtowns commented at 6:03 am on September 23, 2025:
    Separate text with a blank line rather than indenting?

    l0rinc commented at 5:02 pm on September 23, 2025:
    The original message contained these indentations - except in a very few cases that I adjusted for consistency. It seemed simpler to me to unify the text that way - otherwise I would have rewritten it completely since I don’t particularly like the way it describes the situation.
  97. in src/validation.cpp:2584 in 010cf81407 outdated
    2581+                    pindex->nHeight, block_hash.ToString(), script_check_reason);
    2582+        } else {
    2583+            LogInfo("Disabling script verification at block #%d (%s).",
    2584+                    pindex->nHeight, block_hash.ToString());
    2585+        }
    2586+        m_last_script_check_reason_logged = script_check_reason;
    


    ajtowns commented at 6:23 am on September 23, 2025:
    consider setting const bool fScriptChecks = (script_check_reason != nullptr); here, and leaving the following code unchanged?

    l0rinc commented at 5:02 pm on September 23, 2025:
    You mean to only log enabled/disabled changes, but not enabled-after-assumevalid/enabled-no-burried changes? This was also deliberate to show the exact reason so that users understand the why and not just whether it’s enabled.

    andrewtoth commented at 2:39 pm on September 24, 2025:
    I believe the parent comment means to set a const bool fScriptChecks variable here below the closing if bracket, so that we don’t have to change any code below on lines 2594 and 2653. That makes sense to me.

    l0rinc commented at 6:33 pm on September 25, 2025:
    I think I understood that, but that will be a behavior change, since currently after Enabling... too recent... we’re still getting a log for Enabling ... block height above... while if we only check empty vs non-empty we’d only be logging Disabled -> Enabled (or vice versa) but not slightly more confusing cases like the one above. Given that the logging was added because even we got confused by this, I thought we should log every state change.

    andrewtoth commented at 6:37 pm on September 25, 2025:
    I don’t understand why you think it’s a behavior change. It has nothing to do with logging. Add this on line 2585: const bool fScriptChecks = (script_check_reason != nullptr); Remove changes on lines 2594, 2653.

    l0rinc commented at 7:02 pm on September 25, 2025:

    I don’t understand why you think it’s a behavior change

    Compared to the latest state of the code it’s a change (not compared master). If we only log when the nullnes changes that will be a different behavior than when the reason changes. Or am I misunderstanding the comments?

    Ah, you mean to keep lines

    • if (auto& queue = m_chainman.GetCheckQueue(); queue.HasThreads() && fScriptChecks)
    • if (!tx.IsCoinBase() && fScriptChecks) yes, I can do that, will do on next push.

    l0rinc commented at 3:36 am on October 2, 2025:
    Done, thanks
  98. ajtowns commented at 6:25 am on September 23, 2025: contributor
    Some possible nits
  99. in test/functional/feature_assumevalid.py:34 in 84a9adcfd9 outdated
    26@@ -27,6 +27,12 @@
    27     - node2 has -assumevalid set to the hash of block 102. Try to sync to
    28       block 200. node2 will reject block 102 since it's assumed valid, but it
    29       isn't buried by at least two weeks' work.
    30+    - node3 has -assumevalid set to the hash of block 102. Feed a longer
    31+      competing headers-only branch so block #1 is not on the best header chain.
    32+    - node4 has -assumevalid set to the hash of block 102. Submit an alternative
    33+      block #1 that is not part of the assumevalid chain.
    34+    - node5 starts with no -assumevalid parameter. Reindex to deterministically hit
    


    andrewtoth commented at 1:59 pm on September 24, 2025:
    nit: I think we can remove “deterministically” here. It’s implied.

    l0rinc commented at 3:35 am on October 2, 2025:
    Removed
  100. in test/functional/feature_assumevalid.py:235 in 84a9adcfd9 outdated
    250+            p2p4.send_without_ping(msg_block(alt1))
    251+            self.wait_until(lambda: self.nodes[4].getblockcount() == 1)
    252+
    253+
    254+        # nodes[5]
    255+        # Reindex to deterministically hit specific assumevalid gates (no races with header downloads/chainwork during startup).
    


    andrewtoth commented at 2:12 pm on September 24, 2025:
    nit: same, no need for “deterministically” here.

    l0rinc commented at 3:35 am on October 2, 2025:
    Removed
  101. in test/functional/feature_assumevalid.py:239 in 84a9adcfd9 outdated
    254+        # nodes[5]
    255+        # Reindex to deterministically hit specific assumevalid gates (no races with header downloads/chainwork during startup).
    256+        p2p5 = self.nodes[5].add_p2p_connection(BaseNode())
    257+        p2p5.send_header_for_blocks(self.blocks[0:200])
    258+        p2p5.send_without_ping(msg_block(self.blocks[0]))
    259+        self.wait_until(lambda: self.nodes[5].getblockcount())
    


    andrewtoth commented at 2:16 pm on September 24, 2025:
    Shouldn’t this be getblockcount() == 1?

    l0rinc commented at 6:13 pm on September 25, 2025:
    Wouldn’t that cause a race condition if the check misses it?

    hodlinator commented at 6:28 pm on September 25, 2025:
    But you’ve only sent it one block?

    l0rinc commented at 7:03 pm on September 25, 2025:
    Right, so what’s the advantage of getblockcount() == 1 compared to “non-zero”?

    hodlinator commented at 8:31 pm on September 25, 2025:
    Makes the int -> bool coercion explicit to the reader, see #33336 (review)

    l0rinc commented at 3:35 am on October 2, 2025:
    Done
  102. in src/validation.cpp:2440 in c99fde2c0a outdated
    2438         } else if (it->second.GetAncestor(pindex->nHeight) != pindex) {
    2439-            // TODO
    2440+            script_check_reason = "block not part of assumevalid chain";
    2441         } else if (m_chainman.m_best_header->GetAncestor(pindex->nHeight) != pindex) {
    2442-            // TODO
    2443+            script_check_reason = "block not on best header chain";
    


    andrewtoth commented at 2:29 pm on September 24, 2025:
    nit: “block not in best header chain”

    l0rinc commented at 6:12 pm on September 25, 2025:
    Not sure about this, I have never seen “inchain transaction” but I have seen “onchain”. I would also say “it’s not in the set” instead of “on the set” but I would say “it’s on the blockchain” not “in”. What do others think?

    hodlinator commented at 6:27 pm on September 25, 2025:

    Blocks are “in the chain” to me, like metal links being in a chain. Maybe on-chain tx is more in contrast to off-chain tx, but a tx is in a block, in a chain.

    Not important enough that I reacted upon first review, but would definitely suggest changing if you re-touch.


    l0rinc commented at 3:35 am on October 2, 2025:
    Fixed, thanks
  103. in src/validation.cpp:2438 in c99fde2c0a outdated
    2432@@ -2433,15 +2433,15 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
    2433         //  effectively caching the result of part of the verification.
    2434         BlockMap::const_iterator it{m_blockman.m_block_index.find(m_chainman.AssumedValidBlock())};
    2435         if (it == m_blockman.m_block_index.end()) {
    2436-            // TODO
    2437+            script_check_reason = "assumevalid hash not in headers";
    2438         } else if (it->second.GetAncestor(pindex->nHeight) != pindex) {
    2439-            // TODO
    2440+            script_check_reason = "block not part of assumevalid chain";
    


    andrewtoth commented at 2:29 pm on September 24, 2025:
    nit: for consistency “block not in assumevalid chain”

    l0rinc commented at 3:35 am on October 2, 2025:
    Good point, thanks
  104. in src/validation.cpp:2444 in c99fde2c0a outdated
    2444         } else if (m_chainman.m_best_header->nChainWork < m_chainman.MinimumChainWork()) {
    2445-            // TODO
    2446+            script_check_reason = "best header chainwork below minimumchainwork";
    2447         } else if (GetBlockProofEquivalentTime(*m_chainman.m_best_header, *pindex, *m_chainman.m_best_header, params.GetConsensus()) <= TWO_WEEKS_IN_SECONDS) {
    2448-            // TODO
    2449+            script_check_reason = "too recent relative to best header";
    


    andrewtoth commented at 2:30 pm on September 24, 2025:
    nit: “block too recent relative to best header”

    l0rinc commented at 3:35 am on October 2, 2025:
    Thanks
  105. andrewtoth approved
  106. andrewtoth commented at 2:43 pm on September 24, 2025: contributor

    Approach ACK.

    The way this PR untangles the assumevalid conditions and explicitly logs them is great for readability and debugging. I’m not sure this should go into v30 though since it does touch a lot of consensus code.

    The PR title understates quite a bit what this PR is doing. I assume since it has evolved since being initially proposed. I think it can be updated to better detail what this is doing.

  107. l0rinc renamed this:
    log: always print initial script verification state
    log: print every script verification state change
    on Sep 24, 2025
  108. test: add assumevalid scenarios scaffold
    Increase the test to 6 nodes and add flows for baseline, deep anchor, and too-recent cases, plus scaffolding for off-best-header, not-in-assumevalid,
    and reindex gates.
    Assertions are minimal here; follow-ups add reason checks.
    
    Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com>
    4fad4e992c
  109. validation: log initial script verification state
    Replaced `atomic<bool>` with `std::optional<bool>` (logged once on first observation). Safe because `ConnectBlock` holds `cs_main`.\
    After this change, the state is logged before the very first `UpdateTip` line.
    
    Co-authored-by: Eunovo <eunovo9@gmail.com>
    Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com>
    Co-authored-by: w0xlt <woltx@protonmail.com>
    9bc298556c
  110. refactor: untangle assumevalid decision branches
    Flatten nested conditionals into a linear gating sequence for readability and precise logging. No functional change, TODOs are addressed in next commit
    f2ea6f04e7
  111. log: separate script verification reasons
    Replace `fScriptChecks` with `script_check_reason` and log the precise reason when checks are enabled; log a plain "Disabling" when they are skipped.
    Adjust the functional test to assert the new reason strings.
    
    Co-authored-by: w0xlt <woltx@protonmail.com>
    Co-authored-by: Eunovo <eunovo9@gmail.com>
    Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com>
    Co-authored-by: TheCharlatan <seb.kung@gmail.com>
    Co-authored-by: Andrew Toth <andrewstoth@gmail.com>
    6c13a38ab5
  112. log: split assumevalid ancestry-failure-reason message
    When the assumevalid ancestry check fails, log a precise reason:
    - "block height above assumevalid height" if the block is above the assumevalid block (the default reason)
    - "block not in of assumevalid chain" otherwise
    
    The new split was added under the existing condition to simplify conceptually that the two cases are related.
    It could still be useful to know when the block is just above the assumevalid block or when it's not even on the same chain.
    
    Update the functional test to assert the new reason strings. No behavior change.
    
    Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com>
    45bd891465
  113. l0rinc force-pushed on Oct 2, 2025
  114. hodlinator approved
  115. hodlinator commented at 7:00 am on October 2, 2025: contributor

    re-ACK 45bd8914658a675d00aa9c83373d6903a8a9ece8

    Just fixed some nits since previous review.

  116. DrahtBot requested review from w0xlt on Oct 2, 2025
  117. DrahtBot requested review from andrewtoth on Oct 2, 2025
  118. DrahtBot requested review from Eunovo on Oct 2, 2025
  119. yuvicc commented at 5:51 am on October 3, 2025: contributor

    ACK 45bd8914658a675d00aa9c83373d6903a8a9ece8

    Reviewed all commits.

  120. andrewtoth approved
  121. andrewtoth commented at 4:57 pm on October 4, 2025: contributor
    ACK 45bd8914658a675d00aa9c83373d6903a8a9ece8

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-10-10 15:13 UTC

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