consensus: Remove special treatment for single threaded script checking #32575

pull fjahr wants to merge 6 commits into bitcoin:master from fjahr:2025-05-CheckInputScript-split changing 4 files +110 −86
  1. fjahr commented at 8:52 PM on May 20, 2025: contributor

    This topic has been motivated by my work on batch validation and a related conversation just happened here: #32467 (review)

    CheckInputScripts currently only does what its name implies if there is no multithreading usage for validation. If there are worker threads available the function instead only creates the checks and puts them into a vector. Aside from some shared pre-checks the multithreaded code path is much simpler compared to the rest. This dual use makes the function hard to grasp and its naming confusing.

    This PR refactors this code to be more readable and improves documentation aside from unifying the single threaded and the multithreaded cases by letting checkqueue handle single threaded script checking as well. The code is already suited for this and there seems to be not noticable performance overhead from this.

  2. DrahtBot commented at 8:52 PM on May 20, 2025: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

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

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept NACK l0rinc
    Concept ACK theuni, stickies-v, darosior
    Approach ACK sedited

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

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34875 (refactor: separate deferred script check collection from CheckInputScripts by l0rinc)
    • #29843 (policy: Allow non-standard scripts with -acceptnonstdtxn=1 (test nets only) by ajtowns)

    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.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

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

    • CheckInputScripts(tx, state, &active_coins_tip, test_flags, true, add_to_cache, txdata, validation_cache) in src/test/txvalidationcache_tests.cpp
    • CheckInputScripts(CTransaction(spend_tx), state, &m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_DERSIG, true, true, ptd_spend_tx, m_node.chainman->m_validation_cache) in src/test/txvalidationcache_tests.cpp
    • PrepareScriptChecks(CTransaction(spend_tx), &m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_DERSIG, true, ptd_spend_tx, m_node.chainman->m_validation_cache) in src/test/txvalidationcache_tests.cpp
    • CheckInputScripts(CTransaction(invalid_with_cltv_tx), state, m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY, true, true, txdata, m_node.chainman->m_validation_cache) in src/test/txvalidationcache_tests.cpp
    • CheckInputScripts(CTransaction(invalid_with_csv_tx), state, &m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_CHECKSEQUENCEVERIFY, true, true, txdata, m_node.chainman->m_validation_cache) in src/test/txvalidationcache_tests.cpp
    • CheckInputScripts(CTransaction(tx), state, &m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_WITNESS, true, true, txdata, m_node.chainman->m_validation_cache) in src/test/txvalidationcache_tests.cpp

    <sup>2026-03-23 16:50:25</sup>

  3. theuni commented at 8:57 PM on May 20, 2025: member

    Concept ACK. Anything to clean this up :)

    See also #32317 which moves some of this functionality around similarly.

  4. fjahr force-pushed on May 20, 2025
  5. fjahr marked this as ready for review on May 20, 2025
  6. fjahr commented at 9:49 PM on May 20, 2025: contributor

    Cleaned up the comments so this should be ready for more detailed feedback, but expect a rebase once #32467 is merged.

  7. in src/validation.cpp:2207 in 4262b75726 outdated
    2202 | +std::vector<CScriptCheck> PrepareInputScriptChecks(const CTransaction& tx,
    2203 | +                       const CCoinsViewCache& inputs, unsigned int flags, bool cacheSigStore,
    2204 | +                       bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
    2205 | +                       ValidationCache& validation_cache) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
    2206 | +{
    2207 | +    if (tx.IsCoinBase()) return {};
    


    fjahr commented at 9:54 PM on May 20, 2025:

    I have been contemplating kicking this out (here and in CheckInputScripts) or even turning this into an assert because the callers already handle this everywhere. On the other hand it conceptually makes sense to return early if we know there is nothing to check because we don't expect any inputs. So I am still undecided on this one and happy to hear opinions.

  8. sedited commented at 10:12 PM on May 20, 2025: contributor

    Concept ACK

  9. fjahr renamed this:
    RFC: refactor: Split multithreaded case out of CheckInputScripts
    refactor: Split multithreaded case out of CheckInputScripts
    on May 20, 2025
  10. DrahtBot added the label Refactoring on May 20, 2025
  11. stickies-v commented at 10:28 AM on May 21, 2025: contributor

    Concept ACK

  12. DrahtBot added the label Needs rebase on May 22, 2025
  13. theuni commented at 6:29 PM on May 22, 2025: member

    Ready for rebase.

  14. fjahr force-pushed on May 22, 2025
  15. fjahr commented at 8:21 PM on May 22, 2025: contributor

    Ready for rebase.

    Done, ready for review :)

  16. DrahtBot removed the label Needs rebase on May 22, 2025
  17. in src/validation.cpp:2733 in 5dfd10c725 outdated
    2741 | -            if (!tx_ok) {
    2742 | -                // Any transaction validation failure in ConnectBlock is a block consensus failure
    2743 | -                state.Invalid(BlockValidationResult::BLOCK_CONSENSUS,
    2744 | -                              tx_state.GetRejectReason(), tx_state.GetDebugMessage());
    2745 | -                break;
    2746 | +                if (!CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache)) {
    


    theuni commented at 9:26 PM on May 22, 2025:

    I think this should go a little further. At this point, CheckInputScripts is essentially meant to be a mempool function and PrepareInputScriptChecks is for block validation, except for the weird edge-case when we're single-threaded.

    I think I'd rather just see this go all the way and make the split purposeful. Something like:

    diff --git a/src/validation.cpp b/src/validation.cpp
    index 1c81620889f..d9a447b460f 100644
    --- a/src/validation.cpp
    +++ b/src/validation.cpp
    @@ -2723,19 +2723,18 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
             {
                 bool fCacheResults = fJustCheck; /* Don't cache results if we're actually connecting blocks (still consult the cache, though) */
                 TxValidationState tx_state;
    -            // If parallel script checking is possible (worker threads are available) the checks are appended to a
    -            // vector without running them. The vector is then added to control which runs the checks asynchronously.
    -            // Otherwise, CheckInputScripts runs the checks on a single thread before returning.
    -            if (control) {
    -                std::vector<CScriptCheck> vChecks = PrepareInputScriptChecks(tx, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache);
    -                control->Add(std::move(vChecks));
    -            } else {
    -                if (!CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache)) {
    -                    // Any transaction validation failure in ConnectBlock is a block consensus failure
    -                    state.Invalid(BlockValidationResult::BLOCK_CONSENSUS,
    -                                  tx_state.GetRejectReason(), tx_state.GetDebugMessage());
    -                    break;
    +            // If parallel script checking is possible (worker threads are available), they are added to control which
    +            // which runs the checks asynchronously. Otherwise they are run here directly.
    +            std::vector<CScriptCheck> vChecks = PrepareInputScriptChecks(tx, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache);
    +            if (control) control->Add(std::move(vChecks));
    +            else {
    +                for (auto& check : vChecks) {
    +                    if (const auto& check_result = check()) {
    +                        state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, strprintf("mandatory-script-verify-flag-failed (%s)", ScriptErrorString(check_result->first)), check_result->second);
    +                        break;
    +                    }
                     }
    +                if (!state.IsValid()) break;
                 }
             }
    
    

    An alternative would be to make CCheckQueueControl work with 0 worker threads (maybe it already does?) and just always use it. That would completely unify the approach here.


    mzumsande commented at 10:54 PM on May 22, 2025:

    I like that idea.

    I think it would be a small change in behaviour, because if I read this line right it will add entries to the cache if 1)pvChecks is not set (single-threaded) 2) cacheFullScriptStore is set (i.e. if ConnectBlock() was called with fJustCheck=True, so currently only from TestBlockValidity). I wonder if the current behavior is intended - it seems very strange to me that mining rpcs would add entries to the script cache, but only in the -par=1 case.


    theuni commented at 6:01 PM on June 17, 2025:

    @mzumsande I think it's very unlikely that that behavior is intended :)

    Edit: likely -> unlikely.


    theuni commented at 6:02 PM on June 17, 2025:

    Whooops, *Very unlikely.


    fjahr commented at 10:04 PM on June 19, 2025:

    Took the suggestion here.

  18. in src/validation.cpp:2216 in 5dfd10c725 outdated
    2211 | +        return {};
    2212 | +    }
    2213 | +
    2214 | +    EnsureTxData(tx, inputs, txdata);
    2215 | +
    2216 | +    std::vector<CScriptCheck> pvChecks{};
    


    mzumsande commented at 9:38 PM on May 22, 2025:

    Just a rough idea, but did you consider the alternative approach of doing the pvChecks population not here (but in ConnectBlock() or a separate function), so that CheckInputScripts could also call PrepareInputScriptChecks and there would be no duplication of the coinbase check, cache check and TxData init?


    fjahr commented at 10:03 PM on June 19, 2025:

    I have implemented this at the cost of duplicating the cache entry calculation. I think it should be fine but I will try to run some benchmarks.


    l0rinc commented at 12:03 PM on March 5, 2026:

    This isn't a minor issue; this should be mentioned in the commit message and PR description.

  19. fjahr force-pushed on Jun 19, 2025
  20. fjahr commented at 10:00 PM on June 19, 2025: contributor

    An alternative would be to make CCheckQueueControl work with 0 worker threads (maybe it already does?) and just always use it. That would completely unify the approach here.

    Yeah, that does work basically out of the box. I tested it with this minimal change on top of the changes here: https://github.com/fjahr/bitcoin/commit/320b9a967f88ece5b98779948c47493ad7717da2 However I don't favor that approach at the moment since there seems to be a lot of overhead when using checkqueue with a single thread and a lot of docs would need to be changed accordingly as well. Just logically you wouldn't think about using a queue when you just have a single thread, so while it would definitely simplify the code in ConnectBlock, I am not sure it's a bigger win than the other approaches discussed here, given that it may cause irritation in other places and a performance penalty. I am happy to be convinced of this approach though if I overlooked something.

    I have now pushed here an updated version that addresses both the suggestions from @theuni and @mzumsande .

  21. in src/validation.cpp:2712 in edcc004c40 outdated
    2716 | -                tx_ok = CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache, &vChecks);
    2717 | -                if (tx_ok) control->Add(std::move(vChecks));
    2718 | -            } else {
    2719 | -                tx_ok = CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache);
    2720 | +            // If parallel script checking is possible (worker threads are available), they are added to control which
    2721 | +            // which runs the checks asynchronously. Otherwise they are run here directly.
    


    maflcko commented at 9:34 AM on June 20, 2025:
    “this transactions’s input scripts” -> “this transaction’s input scripts” [incorrect possessive form]
    duplicate “which” in “control which which runs the checks asynchronously” -> remove one “which” [typo duplication]

    fjahr commented at 12:59 PM on June 20, 2025:

    both typos fixed

  22. fjahr force-pushed on Jun 20, 2025
  23. DrahtBot added the label Needs rebase on Aug 12, 2025
  24. fjahr force-pushed on Aug 13, 2025
  25. DrahtBot removed the label Needs rebase on Aug 13, 2025
  26. DrahtBot added the label Needs rebase on Oct 8, 2025
  27. fjahr force-pushed on Oct 9, 2025
  28. DrahtBot removed the label Needs rebase on Oct 9, 2025
  29. fjahr force-pushed on Oct 11, 2025
  30. fjahr commented at 8:44 PM on October 11, 2025: contributor

    Silent merged conflict was resolved with the last push but the CI still fails with some problem fetching the qa-assets repo, seems to be a github issue since I also see it in master, retrying one more time with close/re-open to see if it works again now.

  31. fjahr closed this on Oct 11, 2025

  32. fjahr reopened this on Oct 11, 2025

  33. maflcko closed this on Oct 13, 2025

  34. maflcko reopened this on Oct 13, 2025

  35. fjahr commented at 5:41 PM on October 13, 2025: contributor

    Turning it off and on again finally worked :p

  36. in src/validation.cpp:2122 in 413f56423a outdated
    2118 | @@ -2119,6 +2119,32 @@ ValidationCache::ValidationCache(const size_t script_execution_cache_bytes, cons
    2119 |                approx_size_bytes >> 20, script_execution_cache_bytes >> 20, num_elems);
    2120 |  }
    2121 |  
    2122 | +uint256 GetHashCacheEntry(ValidationCache& validation_cache, const CTransaction& tx, script_verify_flags flags)
    


    sedited commented at 9:19 PM on November 30, 2025:

    Nit: Could make these functions static.


    fjahr commented at 11:03 PM on February 27, 2026:

    Done

  37. in src/validation.cpp:2137 in 413f56423a outdated
    2132 | + * flags. Note that this assumes that the inputs provided are
    2133 | + * correct (ie that the transaction hash which is in tx's prevouts
    2134 | + * properly commits to the scriptPubKey in the inputs view of that
    2135 | + * transaction).
    2136 | + */
    2137 | +bool IsScriptCached(ValidationCache& validation_cache, bool cacheFullScriptStore, uint256 hashCacheEntry)
    


    sedited commented at 9:37 PM on November 30, 2025:

    Why is this made a separate function?


    fjahr commented at 11:03 PM on February 27, 2026:

    I just found it nicer in terms of documentation and readability to extract this into a function. It isn't strictly necessary but it should make the actual behavior change easier to review IMO.


    l0rinc commented at 10:15 AM on March 5, 2026:

    I also don't see the advantage of simply extracting it locally (incorrectly, since we need AssertLockHeld for the insertion as well), but I could get behind properly encapsulating the cache and exposing only the contains/insert methods, something like:

    diff --git a/src/validation.cpp b/src/validation.cpp
    index 67130a31cf..3947d0dfde 100644
    --- a/src/validation.cpp
    +++ b/src/validation.cpp
    @@ -2036,6 +2036,18 @@ ValidationCache::ValidationCache(const size_t script_execution_cache_bytes, cons
                   approx_size_bytes >> 20, script_execution_cache_bytes >> 20, num_elems);
     }
     
    +bool ValidationCache::IsScriptValidated(const uint256& entry, bool erase) const
    +{
    +    AssertLockHeld(cs_main); //TODO: Remove this requirement by making CuckooCache not require external locks
    +    return m_script_execution_cache.contains(entry, erase);
    +}
    +
    +void ValidationCache::CacheScriptValidation(const uint256& entry)
    +{
    +    AssertLockHeld(cs_main); //TODO: Remove this requirement by making CuckooCache not require external locks
    +    m_script_execution_cache.insert(entry);
    +}
    +
     /**
      * Check whether all of this transaction's input scripts succeed.
      *
    @@ -2075,8 +2087,7 @@ bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
         uint256 hashCacheEntry;
         CSHA256 hasher = validation_cache.ScriptExecutionCacheHasher();
         hasher.Write(UCharCast(tx.GetWitnessHash().begin()), 32).Write((unsigned char*)&flags, sizeof(flags)).Finalize(hashCacheEntry.begin());
    -    AssertLockHeld(cs_main); //TODO: Remove this requirement by making CuckooCache not require external locks
    -    if (validation_cache.m_script_execution_cache.contains(hashCacheEntry, !cacheFullScriptStore)) {
    +    if (validation_cache.IsScriptValidated(hashCacheEntry, !cacheFullScriptStore)) {
             return true;
         }
     
    @@ -2124,7 +2135,7 @@ bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
         if (cacheFullScriptStore && !pvChecks) {
             // We executed all of the provided scripts, and were told to
             // cache the result. Do so now.
    -        validation_cache.m_script_execution_cache.insert(hashCacheEntry);
    +        validation_cache.CacheScriptValidation(hashCacheEntry);
         }
     
         return true;
    diff --git a/src/validation.h b/src/validation.h
    index 482772c0d6..2bec47aecc 100644
    --- a/src/validation.h
    +++ b/src/validation.h
    @@ -373,8 +373,10 @@ private:
         //! Pre-initialized hasher to avoid having to recreate it for every hash calculation.
         CSHA256 m_script_execution_cache_hasher;
     
    -public:
    +    //! Cache for script verification results to avoid re-validation.
         CuckooCache::cache<uint256, SignatureCacheHasher> m_script_execution_cache;
    +
    +public:
         SignatureCache m_signature_cache;
     
         ValidationCache(size_t script_execution_cache_bytes, size_t signature_cache_bytes);
    @@ -384,6 +386,9 @@ public:
     
         //! Return a copy of the pre-initialized hasher.
         CSHA256 ScriptExecutionCacheHasher() const { return m_script_execution_cache_hasher; }
    +
    +    bool IsScriptValidated(const uint256& entry, bool erase) const EXCLUSIVE_LOCKS_REQUIRED(cs_main);
    +    void CacheScriptValidation(const uint256& entry) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
     };
     
     /** Functions for validating blocks and updating the block tree */
    

    Given the sensitive nature of this area, I'd do it in a separate commit.

    [!NOTE] This change alone would have likely prevented the consensus bug introduced in https://github.com/bitcoinknots/bitcoin/pull/238#issuecomment-3896624715.


    fjahr commented at 4:41 PM on March 11, 2026:

    I agree that it is nicer to this in ValidationCache, I have taken these changes as a separate commit with a minor edit (the const on IsScriptValidated is misleading if erase is set to true) and removed the one that added IsScriptCached as a free function.

  38. in src/validation.cpp:2690 in 02e1c01ed4
    2699 | -                // Any transaction validation failure in ConnectBlock is a block consensus failure
    2700 | -                state.Invalid(BlockValidationResult::BLOCK_CONSENSUS,
    2701 | -                              tx_state.GetRejectReason(), tx_state.GetDebugMessage());
    2702 | -                break;
    2703 | +
    2704 | +            if (control) control->Add(std::move(vChecks));
    


    sedited commented at 9:46 PM on November 30, 2025:

    Can you add braces here? I find different inlining styles for the same if-else block slightly confusing to read.


    fjahr commented at 11:03 PM on February 27, 2026:

    This line is removed in the final commit. I have squashed that commit now as promised.

  39. sedited commented at 10:04 PM on November 30, 2025: contributor

    Looking at this patch, I am asking myself why we're keeping the single threaded / no checkqueue case in the first place. This seems to introduce a vector of jobs for both cases now, so maybe we could just create a queue without any workers?

  40. fjahr force-pushed on Feb 1, 2026
  41. fjahr commented at 7:08 PM on February 1, 2026: contributor

    Finally getting around to picking this up again.

    I am asking myself why we're keeping the single threaded / no checkqueue case in the first place. This seems to introduce a vector of jobs for both cases now, so maybe we could just create a queue without any workers?

    Checkqueue can run without worker threads as well but I still expected that some changes would be needed. That doesn't seem to be the case, it seems to work well as is. I also expected that there would be some performance penalty due to the overhead within checkqueue. This also doesn't seem to be the case, at least not to a degree that is measurable by our connectblock benchmark. So I pushed this as an additional refactoring commit, which of course get's rid of a few lines of extra code. Let me know what you all think.

    EDIT: The last commit should probably be squashed into the one before it but I am leaving it as standalone for now until I have gotten some feedback on the new approach. Unless there are issues noted I will probably squash it with the next push.

  42. fjahr renamed this:
    refactor: Split multithreaded case out of CheckInputScripts
    refactor: Remove special treatment for single threaded script checks
    on Feb 1, 2026
  43. fjahr renamed this:
    refactor: Remove special treatment for single threaded script checks
    refactor: Remove special treatment for single threaded script checking
    on Feb 1, 2026
  44. sedited commented at 5:24 PM on February 13, 2026: contributor

    Approach ACK

    This seems like a nice cleanup, will look at it in detail later.

  45. sedited requested review from theuni on Feb 13, 2026
  46. fjahr force-pushed on Feb 27, 2026
  47. DrahtBot added the label CI failed on Feb 28, 2026
  48. DrahtBot removed the label CI failed on Mar 2, 2026
  49. sedited requested review from l0rinc on Mar 3, 2026
  50. in src/validation.cpp:2064 in 043301a537 outdated
    2059 | + * transaction).
    2060 | + */
    2061 | +bool IsScriptCached(ValidationCache& validation_cache, bool cacheFullScriptStore, uint256 hashCacheEntry)
    2062 | +                    EXCLUSIVE_LOCKS_REQUIRED(cs_main)
    2063 | +{
    2064 | +    AssertLockHeld(cs_main); //TODO: Remove this requirement by making CuckooCache not require external locks
    


    l0rinc commented at 10:49 AM on March 5, 2026:

    043301a refactor: Extract cache check from CheckInputScripts:

    The comment says this is about the CuckooCache, which we are inserting into later, so we cannot just move the lock assertion to the contains method only.


    fjahr commented at 4:40 PM on March 11, 2026:

    This code was replaced with your suggestion, so this is now obsolete.

  51. in src/validation.cpp:2071 in 6b37953134
    2067 | @@ -2068,6 +2068,22 @@ bool IsScriptCached(ValidationCache& validation_cache, bool cacheFullScriptStore
    2068 |      return false;
    2069 |  }
    2070 |  
    2071 | +void EnsureTxData(const CTransaction& tx, const CCoinsViewCache& inputs, PrecomputedTransactionData& txdata)
    


    l0rinc commented at 11:54 AM on March 5, 2026:

    6b37953 refactor: Extract TxData initialization from CheckInputScripts:

    This new abstraction is doing multiple things (which makes it a bit hard to name):

    • asking txdata whether the payload will be accepted, to avoid calculating GetSpentOutputs needlessly
    • calculating it if it will be accepted
    • passing it to txdata.Init
    • asserting that everything makes sense regardless of previous actions

    Most of these make more sense at the call site. The only excessive calculation is the to-be-spent outputs calculation. We could extract only that (making sure no extra copies happen, of course), something like:

    diff --git a/src/validation.cpp b/src/validation.cpp
    --- a/src/validation.cpp	(revision d26ff1ff03da3531d1a6a28466784911ece28e0a)
    +++ b/src/validation.cpp	(date 1772711510934)
    @@ -2048,6 +2048,18 @@
         m_script_execution_cache.insert(entry);
     }
     
    +static std::vector<CTxOut> GetSpentOutputs(const CTransaction& tx, const CCoinsViewCache& inputs)
    +{
    +    std::vector<CTxOut> spent_outputs;
    +    spent_outputs.reserve(tx.vin.size());
    +    for (const auto& txin : tx.vin) {
    +        const Coin& coin = inputs.AccessCoin(txin.prevout);
    +        assert(!coin.IsSpent());
    +        spent_outputs.emplace_back(coin.out);
    +    }
    +    return spent_outputs;
    +}
    +
     /**
      * Check whether all of this transaction's input scripts succeed.
      *
    @@ -2091,18 +2103,7 @@
             return true;
         }
     
    -    if (!txdata.m_spent_outputs_ready) {
    -        std::vector<CTxOut> spent_outputs;
    -        spent_outputs.reserve(tx.vin.size());
    -
    -        for (const auto& txin : tx.vin) {
    -            const COutPoint& prevout = txin.prevout;
    -            const Coin& coin = inputs.AccessCoin(prevout);
    -            assert(!coin.IsSpent());
    -            spent_outputs.emplace_back(coin.out);
    -        }
    -        txdata.Init(tx, std::move(spent_outputs));
    -    }
    +    if (!txdata.m_spent_outputs_ready) txdata.Init(tx, GetSpentOutputs(tx, inputs));
         assert(txdata.m_spent_outputs.size() == tx.vin.size());
     
         for (unsigned int i = 0; i < tx.vin.size(); i++) {
    

    https://godbolt.org/z/jj3eGq8eY indicates that GetSpentOutputs allocates once via reserve, and Init's && parameter steals the buffer pointer directly, so it's a pure refactor.


    fjahr commented at 4:40 PM on March 11, 2026:

    EnsureTxData has a clear contract: it ensures that txdata is initialized. Your alternative pushes the m_spent_outputs_ready check and assertion back to every call site, which is what I'm trying to avoid. You say "most of these make more sense at the call site" but you don't say why. Can you elaborate? However, I don't feel too strongly about it and I'll adopt GetSpentOutputs as you suggest if other reviewers prefer that style.

  52. in src/validation.cpp:145 in 4e851e1745
     138 | @@ -139,6 +139,11 @@ const CBlockIndex* Chainstate::FindForkInGlobalIndex(const CBlockLocator& locato
     139 |      return m_chain.Genesis();
     140 |  }
     141 |  
     142 | +bool PreCheckInputScripts(const CTransaction& tx,
     143 | +                       const CCoinsViewCache& inputs, script_verify_flags flags,
     144 | +                       bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
     145 | +                       ValidationCache& validation_cache) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
    


    l0rinc commented at 11:58 AM on March 5, 2026:

    4e851e1 refactor: Extract PreCheckInputScripts:

    The commit message does not explain the motivation behind this change, or the extra work that we are doing now.

    nit: formatting of the new code is off


    fjahr commented at 4:40 PM on March 11, 2026:

    I have fixed the formatting and re-worked/extended the commit messages overall.

  53. in src/validation.cpp:2176 in 4e851e1745
    2172 | @@ -2153,6 +2173,7 @@ bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
    2173 |      if (cacheFullScriptStore && !pvChecks) {
    2174 |          // We executed all of the provided scripts, and were told to
    2175 |          // cache the result. Do so now.
    2176 | +        uint256 hashCacheEntry = GetHashCacheEntry(validation_cache, tx, flags);
    


    l0rinc commented at 12:00 PM on March 5, 2026:

    4e851e1 refactor: Extract PreCheckInputScripts:

    The commit forgot to mention that we're recalculating the hash now, instead of only doing it once. I don't consider that a refactor, like the commit title suggests.


    fjahr commented at 4:40 PM on March 11, 2026:

    Addressed as the newest push doesn't recalculate the hash anymore.

  54. in src/validation.cpp:2100 in 4e851e1745
    2095 | + * this is not a coinbase transaction. Returning false indicates that checks are not possible
    2096 | + * or necessary. Also ensures txdata is complete.
    2097 | + *
    2098 | + * Non-static (and redeclared) in src/test/txvalidationcache_tests.cpp
    2099 | + */
    2100 | +bool PreCheckInputScripts(const CTransaction& tx,
    


    l0rinc commented at 12:17 PM on March 5, 2026:

    4e851e1 refactor: Extract PreCheckInputScripts:

    The method name describes when this is executed, not what it does. It returns a bool, which forces us to recompute the cache key. What if we adjust it to return the cache key instead, something like:

    diff --git a/src/validation.cpp b/src/validation.cpp
    --- a/src/validation.cpp	(revision 4b2f347fc0495a9e4ddb9e47ec066f3b82a20d30)
    +++ b/src/validation.cpp	(date 1772727309644)
    @@ -2060,6 +2060,36 @@
         return spent_outputs;
     }
     
    +/** Perform pre-checks for CheckInputScripts: skip coinbase, check the script
    + *  execution cache, reserve space for script checks, and initialize txdata.
    + *  Returns nullopt if the transaction is already validated (caller should
    + *  return true), or the cache entry hash needed to store results later. */
    +static std::optional<uint256> LookupScriptValidation(
    +    const CTransaction& tx,
    +    const CCoinsViewCache& inputs, script_verify_flags flags,
    +    bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
    +    const ValidationCache& validation_cache) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
    +{
    +    if (tx.IsCoinBase()) return std::nullopt;
    +
    +    // First check if script executions have been cached with the same
    +    // flags. Note that this assumes that the inputs provided are
    +    // correct (ie that the transaction hash which is in tx's prevouts
    +    // properly commits to the scriptPubKey in the inputs view of that
    +    // transaction).
    +    uint256 hashCacheEntry;
    +    CSHA256 hasher = validation_cache.ScriptExecutionCacheHasher();
    +    hasher.Write(UCharCast(tx.GetWitnessHash().begin()), 32).Write((unsigned char*)&flags, sizeof(flags)).Finalize(hashCacheEntry.begin());
    +    if (validation_cache.IsScriptValidated(hashCacheEntry, !cacheFullScriptStore)) {
    +        return std::nullopt;
    +    }
    +
    +    if (!txdata.m_spent_outputs_ready) txdata.Init(tx, GetSpentOutputs(tx, inputs));
    +    assert(txdata.m_spent_outputs.size() == tx.vin.size());
    +
    +    return hashCacheEntry;
    +}
    +
     /**
      * Check whether all of this transaction's input scripts succeed.
      *
    @@ -2085,22 +2115,8 @@
                            ValidationCache& validation_cache,
                            std::vector<CScriptCheck>* pvChecks)
     {
    -    if (tx.IsCoinBase()) return true;
    -
    -    // First check if script executions have been cached with the same
    -    // flags. Note that this assumes that the inputs provided are
    -    // correct (ie that the transaction hash which is in tx's prevouts
    -    // properly commits to the scriptPubKey in the inputs view of that
    -    // transaction).
    -    uint256 hashCacheEntry;
    -    CSHA256 hasher = validation_cache.ScriptExecutionCacheHasher();
    -    hasher.Write(UCharCast(tx.GetWitnessHash().begin()), 32).Write((unsigned char*)&flags, sizeof(flags)).Finalize(hashCacheEntry.begin());
    -    if (validation_cache.IsScriptValidated(hashCacheEntry, !cacheFullScriptStore)) {
    -        return true;
    -    }
    -
    -    if (!txdata.m_spent_outputs_ready) txdata.Init(tx, GetSpentOutputs(tx, inputs));
    -    assert(txdata.m_spent_outputs.size() == tx.vin.size());
    +    const auto hash_cache_entry{LookupScriptValidation(tx, inputs, flags, cacheFullScriptStore, txdata, validation_cache)};
    +    if (!hash_cache_entry) return true;
     
         for (unsigned int i = 0; i < tx.vin.size(); i++) {
     
    @@ -2133,7 +2149,7 @@
         if (cacheFullScriptStore && !pvChecks) {
             // We executed all of the provided scripts, and were told to
             // cache the result. Do so now.
    -        validation_cache.CacheScriptValidation(hashCacheEntry);
    +        validation_cache.CacheScriptValidation(*hash_cache_entry);
         }
     
         return true;
    

    fjahr commented at 4:40 PM on March 11, 2026:

    I have changed this function to return the hash so we don't have to recalculate it. I am not sure about the naming change to LookupScriptValidation, that doesn't seem to communicate the checking part well. I have thought about a better name that captures this function and I went with PrepareScriptChecks.

  55. in src/validation.cpp:2142 in 4e851e1745
    2138 | @@ -2110,19 +2139,10 @@ bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
    2139 |                         ValidationCache& validation_cache,
    2140 |                         std::vector<CScriptCheck>* pvChecks)
    2141 |  {
    2142 | -    if (tx.IsCoinBase()) return true;
    2143 | -
    2144 | -    if (pvChecks) {
    2145 | -        pvChecks->reserve(tx.vin.size());
    2146 | +    if (!PreCheckInputScripts(tx, inputs, flags, cacheFullScriptStore, txdata, validation_cache)) {
    


    l0rinc commented at 1:42 PM on March 5, 2026:

    4e851e1 refactor: Extract PreCheckInputScripts:

    We could avoid recomputing the the hash:

        const auto hash_cache_entry{LookupScriptValidation(tx, inputs, flags, cacheFullScriptStore, txdata, validation_cache, pvChecks)};
        if (!hash_cache_entry) return true;
    

    fjahr commented at 4:39 PM on March 11, 2026:

    Addressed as mentioned in other comments.

  56. in src/test/txvalidationcache_tests.cpp:31 in 4e851e1745
      26 |                         const CCoinsViewCache& inputs, script_verify_flags flags, bool cacheSigStore,
      27 |                         bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
      28 |                         ValidationCache& validation_cache,
      29 |                         std::vector<CScriptCheck>* pvChecks) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
      30 |  
      31 | +bool PreCheckInputScripts(const CTransaction& tx,
    


    l0rinc commented at 1:45 PM on March 5, 2026:

    4e851e1 refactor: Extract PreCheckInputScripts:

    Could we do the test updates in a separate change? It doesn't seem strictly related to the refactor in src/validation.cpp (which is risky enough).


    fjahr commented at 4:39 PM on March 11, 2026:

    I moved them into a separate commit although I think they would have been fine to leave in the same commit as well. The tests are simplified significantly because this change gives us a cleaner interface to what the test actually want to check. And I don't think it added significant overhead.

  57. in src/validation.cpp:2631 in 7726a6a6c3 outdated
    2621 | +            ValidationCache& validation_cache{m_chainman.m_validation_cache};
    2622 | +            if (PreCheckInputScripts(tx, view, flags, fCacheResults, txsdata[i], validation_cache)) {
    2623 | +                // Turn all of this transaction's input scripts into script checks and add them to the queue
    2624 |                  std::vector<CScriptCheck> vChecks;
    2625 | -                tx_ok = CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache, &vChecks);
    2626 | -                if (tx_ok) control->Add(std::move(vChecks));
    


    l0rinc commented at 3:08 PM on March 5, 2026:

    7726a6a validation, refactor: Remove single threaded special case:

    This change is too big and contains too many subtle assumptions. For example, this seems to be dead code: it will always return true, since the only invalid path is when pvChecks is not provided. We should clarify that in smaller steps and iterate toward the unification in smaller, trivial steps, e.g. something like:

    diff --git a/src/validation.cpp b/src/validation.cpp
    --- a/src/validation.cpp	(revision c009adccee7a268ec844e29bf7070b543a26ae90)
    +++ b/src/validation.cpp	(date 1772723088401)
    @@ -2600,18 +2600,14 @@
             if (!tx.IsCoinBase() && fScriptChecks)
             {
                 bool fCacheResults = fJustCheck; /* Don't cache results if we're actually connecting blocks (still consult the cache, though) */
    -            bool tx_ok;
                 TxValidationState tx_state;
    -            // If CheckInputScripts is called with a pointer to a checks vector, the resulting checks are appended to it. In that case
    -            // they need to be added to control which runs them asynchronously. Otherwise, CheckInputScripts runs the checks before returning.
                 if (control) {
    +                // CheckInputScripts always returns true when pvChecks is provided
    +                // (checks are collected for deferred execution, not executed inline).
                     std::vector<CScriptCheck> vChecks;
    -                tx_ok = CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache, &vChecks);
    -                if (tx_ok) control->Add(std::move(vChecks));
    -            } else {
    -                tx_ok = CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache);
    -            }
    -            if (!tx_ok) {
    +                Assume(CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache, &vChecks));
    +                control->Add(std::move(vChecks));
    +            } else if (!CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache)) {
                     // Any transaction validation failure in ConnectBlock is a block consensus failure
                     state.Invalid(BlockValidationResult::BLOCK_CONSENSUS,
                                   tx_state.GetRejectReason(), tx_state.GetDebugMessage());
    

    fjahr commented at 4:38 PM on March 11, 2026:

    I'm confused by this comment, there may be dead code existing in some commit but it's only transient and the commit you are mentioning in the beginning of your comment is removing this code. So I am not sure where you would like me to apply what change.

  58. in src/validation.cpp:2117 in 4e851e1745 outdated
    2143 | -
    2144 | -    if (pvChecks) {
    2145 | -        pvChecks->reserve(tx.vin.size());
    2146 | +    if (!PreCheckInputScripts(tx, inputs, flags, cacheFullScriptStore, txdata, validation_cache)) {
    2147 | +         return true;
    2148 |      }
    


    l0rinc commented at 4:12 PM on March 5, 2026:

    4e851e1 refactor: Extract PreCheckInputScripts:

    What's the explanation for this removal? There are too many changes in this commit. If you want to move it closer to usage, we could do:

    diff --git a/src/validation.cpp b/src/validation.cpp
    --- a/src/validation.cpp	(revision b3b6dc9bb7165bbfd21cb1af636cdfc467de7f01)
    +++ b/src/validation.cpp	(revision 4b2f347fc0495a9e4ddb9e47ec066f3b82a20d30)
    @@ -2087,10 +2087,6 @@
     {
         if (tx.IsCoinBase()) return true;
     
    -    if (pvChecks) {
    -        pvChecks->reserve(tx.vin.size());
    -    }
    -
         // First check if script executions have been cached with the same
         // flags. Note that this assumes that the inputs provided are
         // correct (ie that the transaction hash which is in tx's prevouts
    @@ -2117,6 +2113,7 @@
             // Verify signature
             CScriptCheck check(txdata.m_spent_outputs[i], tx, validation_cache.m_signature_cache, i, flags, cacheSigStore, &txdata);
             if (pvChecks) {
    +            pvChecks->reserve(tx.vin.size());
                 pvChecks->emplace_back(std::move(check));
             } else if (auto result = check(); result.has_value()) {
                 // Tx failures never trigger disconnections/bans.
    
    

    in a separate commit


    fjahr commented at 4:38 PM on March 11, 2026:

    The reserve is removed because in the new structure, PreCheckInputScripts handles the early-return logic and the vector construction happens in ConnectBlock directly with its own reserve (as vChecks). The suggestion to move reserve inside the loop would call it each iteration of the loop unnecessarily.

  59. in src/validation.cpp:2165 in 7726a6a6c3 outdated
    2161 | @@ -2170,7 +2162,7 @@ bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
    2162 |          }
    2163 |      }
    2164 |  
    2165 | -    if (cacheFullScriptStore && !pvChecks) {
    2166 | +    if (cacheFullScriptStore) {
    


    l0rinc commented at 4:52 PM on March 5, 2026:

    7726a6a validation, refactor: Remove single threaded special case:

    We're mixing refactoring (separation of the two intertwined use cases) with a behavior change in a consensus area. I have a really hard time trusting this completely.

    It would help if we could separate "validation" from "refactor" here. We could split the collection use case from validation by duplicating CheckInputScripts into a CollectScriptChecks, which creates the vector, fills it, and returns the values, while making input script validation ignore the vector. The two algorithms would already be very different (probably already achieving the main goal of this change without introducing any potentially dangerous unification, which we can still do in a follow-up if needed). This is what that would look like:

    diff --git a/src/validation.cpp b/src/validation.cpp
    --- a/src/validation.cpp	(revision 87fcd645598d186bd1e4f68d5aca275dc7815fda)
    +++ b/src/validation.cpp	(date 1772729378036)
    @@ -137,11 +137,17 @@
         return m_chain.Genesis();
     }
     
    -bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
    -                       const CCoinsViewCache& inputs, script_verify_flags flags, bool cacheSigStore,
    -                       bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
    -                       ValidationCache& validation_cache,
    -                       std::vector<CScriptCheck>* pvChecks = nullptr)
    +void CollectScriptChecks(const CTransaction& tx,
    +                         const CCoinsViewCache& inputs, script_verify_flags flags, bool cacheSigStore,
    +                         bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
    +                         ValidationCache& validation_cache,
    +                         std::vector<CScriptCheck>& pvChecks)
    +                         EXCLUSIVE_LOCKS_REQUIRED(cs_main);
    +
    +bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
    +                       const CCoinsViewCache& inputs, script_verify_flags flags, bool cacheSigStore,
    +                       bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
    +                       ValidationCache& validation_cache)
                            EXCLUSIVE_LOCKS_REQUIRED(cs_main);
     
     bool CheckFinalTxAtTip(const CBlockIndex& active_chain_tip, const CTransaction& tx)
    @@ -2060,8 +2066,8 @@
         return spent_outputs;
     }
     
    -/** Perform pre-checks for CheckInputScripts: skip coinbase, check the script
    - *  execution cache, reserve space for script checks, and initialize txdata.
    +/** Perform pre-checks for script validation: skip coinbase, check the script
    + *  execution cache, and initialize txdata.
      *  Returns nullopt if the transaction is already validated (caller should
      *  return true), or the cache entry hash needed to store results later. */
     static std::optional<uint256> LookupScriptValidation(
    @@ -2090,16 +2096,28 @@
         return hashCacheEntry;
     }
     
    +/** Collect script checks for all inputs without executing them.
    + *  Non-static (and redeclared) in src/test/txvalidationcache_tests.cpp */
    +void CollectScriptChecks(const CTransaction& tx,
    +                         const CCoinsViewCache& inputs, script_verify_flags flags, bool cacheSigStore,
    +                         bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
    +                         ValidationCache& validation_cache,
    +                         std::vector<CScriptCheck>& checks) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
    +{
    +    if (LookupScriptValidation(tx, inputs, flags, cacheFullScriptStore, txdata, validation_cache)) {
    +        checks.reserve(tx.vin.size());
    +        for (unsigned int i = 0; i < tx.vin.size(); i++) {
    +            checks.emplace_back(txdata.m_spent_outputs[i], tx, validation_cache.m_signature_cache, i, flags, cacheSigStore, &txdata);
    +        }
    +    }
    +}
    +
     /**
      * Check whether all of this transaction's input scripts succeed.
      *
      * This involves ECDSA signature checks so can be computationally intensive. This function should
      * only be called after the cheap sanity checks in CheckTxInputs passed.
      *
    - * If pvChecks is not nullptr, script checks are pushed onto it instead of being performed inline. Any
    - * script checks which are not necessary (eg due to script execution cache hits) are, obviously,
    - * not pushed onto pvChecks/run.
    - *
      * Setting cacheSigStore/cacheFullScriptStore to false will remove elements from the corresponding cache
      * which are matched. This is useful for checking blocks where we will likely never need the cache
      * entry again.
    @@ -2112,8 +2130,7 @@
     bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
                            const CCoinsViewCache& inputs, script_verify_flags flags, bool cacheSigStore,
                            bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
    -                       ValidationCache& validation_cache,
    -                       std::vector<CScriptCheck>* pvChecks)
    +                       ValidationCache& validation_cache)
     {
         const auto hash_cache_entry{LookupScriptValidation(tx, inputs, flags, cacheFullScriptStore, txdata, validation_cache)};
         if (!hash_cache_entry) return true;
    @@ -2128,10 +2145,7 @@
     
             // Verify signature
             CScriptCheck check(txdata.m_spent_outputs[i], tx, validation_cache.m_signature_cache, i, flags, cacheSigStore, &txdata);
    -        if (pvChecks) {
    -            pvChecks->reserve(tx.vin.size());
    -            pvChecks->emplace_back(std::move(check));
    -        } else if (auto result = check(); result.has_value()) {
    +        if (auto result = check()) {
                 // Tx failures never trigger disconnections/bans.
                 // This is so that network splits aren't triggered
                 // either due to non-consensus relay policies (such as
    @@ -2146,7 +2160,7 @@
             }
         }
     
    -    if (cacheFullScriptStore && !pvChecks) {
    +    if (cacheFullScriptStore) {
             // We executed all of the provided scripts, and were told to
             // cache the result. Do so now.
             validation_cache.CacheScriptValidation(*hash_cache_entry);
    @@ -2596,18 +2610,20 @@
             if (!tx.IsCoinBase() && fScriptChecks)
             {
                 bool fCacheResults = fJustCheck; /* Don't cache results if we're actually connecting blocks (still consult the cache, though) */
    -            TxValidationState tx_state;
                 if (control) {
                     // CheckInputScripts always returns true when pvChecks is provided
                     // (checks are collected for deferred execution, not executed inline).
                     std::vector<CScriptCheck> vChecks;
    -                Assume(CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache, &vChecks));
    +                CollectScriptChecks(tx, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache, vChecks);
                     control->Add(std::move(vChecks));
    -            } else if (!CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache)) {
    -                // Any transaction validation failure in ConnectBlock is a block consensus failure
    -                state.Invalid(BlockValidationResult::BLOCK_CONSENSUS,
    -                              tx_state.GetRejectReason(), tx_state.GetDebugMessage());
    -                break;
    +            } else {
    +                TxValidationState tx_state;
    +                if (!CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache)) {
    +                    // Any transaction validation failure in ConnectBlock is a block consensus failure
    +                    state.Invalid(BlockValidationResult::BLOCK_CONSENSUS,
    +                                  tx_state.GetRejectReason(), tx_state.GetDebugMessage());
    +                    break;
    +                }
                 }
             }
    

    fjahr commented at 4:38 PM on March 11, 2026:

    This preserves the single-threaded special case and looks more like an earlier version of the code that I proposed here. Based on feedback from early review I have eliminated that and I was surprised that it worked better than I had expected so I am happy I adopted the current approach here. The whole motivation of this PR is to unify the paths through checkqueue, which simplifies the code and the benchmarks confirm has no performance cost. So I am continuing with the current approach here.


    l0rinc commented at 5:55 AM on March 20, 2026:

    I pushed a narrower alternative of this PR, keeping as much as I could, as long as it's still a pure refactor without any observable behavior change (+ tests for the two separate behaviors), see: https://github.com/bitcoin/bitcoin/pull/34875

  60. l0rinc changes_requested
  61. l0rinc commented at 5:06 PM on March 5, 2026: contributor

    Concept ACK. The recent consensus bug in the BIP110 soft fork proposal in this exact area indicates the current code is hard to understand.
    Approach NACK. The recent consensus bug in the BIP110 soft fork proposal in this exact area indicates we need to be extra careful, which isn't the case in this PR yet.

    I left a ton of suggestions as I recreated the change locally, experimenting with a few different implementations. We should definitely split the two intertwined functionalities, but not in the same PR that also modifies the behavior. There are too many unexplained changes here that I strongly object to.

    Let's untangle the two functionalities first. That refactor is verbose but quite safe. If it gains momentum and gets merged, we can build on it to unify the two even more, if needed.

    Edit: I measured multithreaded script verification, and I can confirm that it doesn't introduce a slowdown

    <details><summary>Benchmark results</summary>

    for DBCACHE in 5000; do \
        COMMITS="043301a537fdad469ef83ee3d1033404a0cefe16 7726a6a6c3e378e538ecb4b5bfd82c2880cc0688"; \
        STOP=936639; CC=gcc; CXX=g++; \
        BASE_DIR="/mnt/my_storage"; DATA_DIR="$BASE_DIR/BitcoinData"; LOG_DIR="$BASE_DIR/logs"; \
        (echo ""; for c in $COMMITS; do git fetch -q origin $c && git log -1 --pretty='%h %s' $c || exit 1; done) && \
        (echo "" && echo "$(date -I) | reindex-chainstate | ${STOP} blocks | dbcache ${DBCACHE} | $(hostname) | $(uname -m) | $(lscpu | grep 'Model name' | head -1 | cut -d: -f2 | xargs) | $(nproc) cores | $(free -h | awk '/^Mem:/{print $2}') RAM | $(lsblk -no ROTA $(df --output=source $BASE_DIR | tail -1) | grep -q 1 && echo HDD || echo SSD)"; echo "") && \
        hyperfine \
        --sort command \
        --runs 1 \
        --export-json "$BASE_DIR/rdx-$(sed -E 's/(\w{8})\w+ ?/\1-/g;s/-$//'<<<"$COMMITS")-$STOP-$DBCACHE-$CC.json" \
        --parameter-list COMMIT ${COMMITS// /,} \
        --prepare "killall -9 bitcoind 2>/dev/null; rm -f ./build/bin/bitcoind; git clean -fxd; git reset --hard {COMMIT} && \
          cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DENABLE_IPC=OFF && ninja -C build bitcoind -j1 && \
          ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=1000 -printtoconsole=0; sleep 20; rm -f $DATA_DIR/debug.log" \
        --conclude "killall bitcoind || true; sleep 5; grep -q 'height=0' $DATA_DIR/debug.log && grep -q 'Disabling script verification at block [#1](/bitcoin-bitcoin/1/)' $DATA_DIR/debug.log && grep -q 'height=$STOP' $DATA_DIR/debug.log && grep 'Bitcoin Core version' $DATA_DIR/debug.log | grep -q "$(printf %.12s {COMMIT})"; \
                    cp $DATA_DIR/debug.log $LOG_DIR/debug-{COMMIT}-$(date +%s).log" \
        "COMPILER=$CC ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=$DBCACHE -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0";
    done
    
    043301a537 refactor: Extract cache check from CheckInputScripts
    7726a6a6c3 validation, refactor: Remove single threaded special case
    
    2026-03-05 | reindex-chainstate | 936639 blocks | dbcache 5000 | i9-ssd | x86_64 | Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz | 16 cores | 62Gi RAM | SSD
    
    Benchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=936639 -dbcache=5000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 (COMMIT = 043301a537fdad469ef83ee3d1033404a0cefe16)
      Time (abs ≡):        37023.077 s               [User: 389375.279 s, System: 1125.389 s]
     
    Benchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=936639 -dbcache=5000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 (COMMIT = 7726a6a6c3e378e538ecb4b5bfd82c2880cc0688)
      Time (abs ≡):        37195.482 s               [User: 388819.276 s, System: 1134.977 s]
     
    Relative speed comparison
            1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=936639 -dbcache=5000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 (COMMIT = 043301a537fdad469ef83ee3d1033404a0cefe16)
            1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=936639 -dbcache=5000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 (COMMIT = 7726a6a6c3e378e538ecb4b5bfd82c2880cc0688)
    

    </details>

    I will measure single-threaded version next.

  62. refactor, validation: Encapsulate script execution cache access in ValidationCache
    Move m_script_execution_cache to private and expose IsScriptValidated()
    and CacheScriptValidation() methods. This ensures the AssertLockHeld
    check is consistently applied for both cache lookups and insertions,
    rather than relying on callers to assert the lock.
    
    Co-authored-by: l0rinc <pap.lorinc@gmail.com>
    5d6cbfff18
  63. refactor, validation: Extract EnsureTxData from CheckInputScripts
    Move the spent outputs initialization and txdata setup into a
    standalone helper.
    24f4a5168c
  64. refactor: Extract PrepareScriptChecks from CheckInputScripts
    Separate the pre-check logic (coinbase check, script execution cache
    lookup, txdata initialization) into PrepareScriptChecks. It returns
    std::optional<uint256> — nullopt if no script checks are needed
    (coinbase or cache hit), or the cache entry hash for use by the
    caller when storing results later.
    
    CheckInputScripts now calls PrepareScriptChecks internally.
    This prepares for ConnectBlock to call PrepareScriptChecks
    directly and construct script checks without going through
    CheckInputScripts.
    b6891931f7
  65. test: Use PrepareScriptChecks in txvalidationcache_tests
    Update cache-hit and cache-miss tests to call PrepareScriptChecks
    directly instead of checking the pvChecks vector populated by
    CheckInputScripts. Both functions use the same caching logic
    internally, and PrepareScriptChecks is the more direct way to
    test whether a transaction's scripts would need validation.
    07c677ec3a
  66. fjahr force-pushed on Mar 11, 2026
  67. fjahr commented at 4:41 PM on March 11, 2026: contributor

    Thanks for the review @l0rinc , I hope I have addressed all your comments either with a response here or with changes in the code.

    I have also tried to split up the commits a bit more where it made sense to me. Overall I think this is still a pretty concise and managable change that doesn't need to be split across multiple PRs. The refactorings are largely move-only and the behavior change is better isolated now.

    Approach NACK. The recent consensus bug in the BIP110 soft fork proposal in this exact area indicates we need to be extra careful, which isn't the case in this PR yet.

    Not sure what the relevance of this is. There will always be forks with bugs because they don't have the same standard of review as we have here. This isn't new and I don't think that should dictate our own processes. But anyway, I think all of the previous reviewers here are aware of the highly sensitive nature of this code and I would not have worked a refactoring of this code unless there had intested signaled in this even before opening. it is also not clear from your comments what it actually is that you are suggesting that is more careful.

  68. l0rinc commented at 12:42 PM on March 13, 2026: contributor

    A few of my concerns here were addressed, so I'm fine with merging the first commit only. ACK 5d6cbfff18e756fe4a31be6336eb2270a46ee4b2

    The rest of the commits still combine a consensus-sensitive behavior change with a structural refactor, so the risk seems significant. This is why I mentioned the BIP110 consensus bug: to highlight that this isn't just theoretical, it's dangerous territory, and yet it's treated lightly, even after a detailed review. I think we should split the behavior change from the refactor into separate PRs.

    We can get most of the advantage (intertwined, complicated script validation doing different things in the same method) simply by separating the two usages into two methods that reuse the components, which isn't risky and would solve the confusing and objectively hard-to-understand logic. And even for the separation part, we would need proper tests and reproducers and more accurate commit messages (refactor shouldn't change user behavior, and even the PR title still claims that).

    Lastly, you aren't backing your claims with reproducible data. It seems hand-wavy. For example, what makes you say this doesn't introduce any regression? I've been trying to measure the -assumevalid=0 -par=1 case for almost a week now. master finished in 60 hours; this branch is still running...

    re-NACK

  69. fjahr commented at 3:59 PM on March 14, 2026: contributor

    A few of my concerns here were addressed, so I'm fine with merging the first commit only. ACK 5d6cbff

    Thanks. But I don't think making this a series of one-commit PRs is a good path forward.

    The rest of the commits still combine a consensus-sensitive behavior change with a structural refactor, so the risk seems significant. This is why I mentioned the BIP110 consensus bug: to highlight that this isn't just theoretical, it's dangerous territory, and yet it's treated lightly, even after a detailed review. I think we should split the behavior change from the refactor into separate PRs.

    Please stop claiming this is being "treated lightly". That is not true, and repeating it does not make it so. Again, the BIP110 comparison is not relevant here. Bringing in an unrelated bug from a fork proposal does not strengthen your argument. It just escalates the rhetoric.

    I don't agree that splitting a PR is safer by default. Most of the refactoring here are simple extractions moving unchanged code around and are thus relatively easy to review. The behavior changing part is already isolated more, as you had requested. Splitting the PR into separate PRs makes it possible for the refactoing parts and the actual behavior change to be reviewed by different people at different times, which makes it easier to miss issues introduced by the combination. So I would say that, depending on the scenario, splitting can also be less safe.

    In fact I have been criticized for splitting a PR prematurely just earlier this week. So I would need more reviewers to share your view that I should indeed split the PR.

    We can get most of the advantage (intertwined, complicated script validation doing different things in the same method) simply by separating the two usages into two methods that reuse the components, which isn't risky and would solve the confusing and objectively hard-to-understand logic.

    As I have said before, I don't agree that it is a better approach. Keeping separate paths around longer is not obviously safer. It keeps more duplication and more room for subtle drift between paths. The point of this change is to unify the logic, not to preserve two partially disentangled variants of it, and that approach has already received positive review.

    Also, "objectively hard-to-understand" is overstated. It is fine to say you prefer a different decomposition. It is not fine to present that preference as if it were self-evidently superior. If there are objective criteria by which you can proof this claim in an measurable, verifiable way, please name them.

    And even for the separation part, we would need proper tests and reproducers and more accurate commit messages (refactor shouldn't change user behavior, and even the PR title still claims that).

    The behavior changing part is explicitly called out in the description of the commit that introduces it. So again, please stop implying that this is being hidden. It is not. If you think a specific test or reproducer is missing, please point to it directly, otherwise this feedback is not helpful at all.

    Lastly, you aren't backing your claims with reproducible data. It seems hand-wavy. For example, what makes you say this doesn't introduce any regression? I've been trying to measure the -assumevalid=0 -par=1 case for almost a week now. master finished in 60 hours; this branch is still running...

    "hand-wavy" is another unfair characterization. Nowhere in this thread have I talked about regressions, what are you referring to exactly? All I have said is that I have not seen a performance hit when running a specific benchmark. And then you yourself said you had done your own measurements and could confirm there was no slowdown. But now suddenly your objection has shifted to a specific -assumevalid=0 -par=1 case that you are still measuring but never mentioned before. That is fine as something to investigate, but it is not a basis for claiming that my statements were careless or unsupported.

    If you get reproducible results that provide new insights, please share them. Until then, I would appreciate sticking to concrete technical points instead of exaggerations, repeated insinuations and needlessly inflammatory language. Open source collaboration can not work if we try to shut people down that don't follow every single suggestion. We should rather try to help each other with the goal to make progress. Progress is already hard enough in this project as it is.

  70. l0rinc commented at 11:07 PM on March 15, 2026: contributor

    <img width="593" height="115" alt="image" src="https://github.com/user-attachments/assets/9507a5b9-0445-4e9c-b465-d919ac31ea21" />

    A week later and this is still running against your "refactor"

  71. darosior commented at 3:16 PM on March 17, 2026: member

    Concept ACK.

    In commit 92a9aabe784aa67e715c099834fed6d062582f56 you state:

    Previously, in the single-threaded case with fJustCheck=true (see TestBlockValidity), successful script results were added to the execution cache. This no longer happens, matching the existing multithreaded behavior. The caching there was likely unintended.

    I think this is incorrect. Script execution caching was introduced in commit b5fea8d0ccc8117644a4ea11256bc531b60fd9a3, it makes TBV populate this cache (likewise the pre-existing sigcache). This makes sense and i see no reason to believe this behaviour was unintended. On the contrary, the commit also updates a comment to explicitly mention TBV, and i would presume the author was well-aware.

    Besides, i think this is a regression to remove script execution caching from TBV. This introduces additional delay at block submission time, doing work that was already performed at block template generation time.

    Finally, i think this introduces a bug in that TBV will erase the entries that were cached at mempool-verification time, further delaying block submission for the lucky miner, and increasing validation time for all other miners. Actually i think that last part is incorrect, because TBV will set cacheFullScriptStore to true in calling PrepareScriptChecks, which will lookup the cache with erase set to false.

  72. darosior commented at 6:14 PM on March 17, 2026: member

    Besides, i think this is a regression to remove script execution caching from TBV. This introduces additional delay at block submission time, doing work that was already performed at block template generation time.

    Hmm but TBV would only help here insofar as it would re-cache whatever would have been previously cached by the mempool and then overridden, which could be negligible.

  73. svanstaa commented at 7:30 AM on March 22, 2026: none

    <img alt="image" width="593" height="115" src="https://private-user-images.githubusercontent.com/1841944/563832983-9507a5b9-0445-4e9c-b465-d919ac31ea21.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQxNjQzOTYsIm5iZiI6MTc3NDE2NDA5NiwicGF0aCI6Ii8xODQxOTQ0LzU2MzgzMjk4My05NTA3YTViOS0wNDQ1LTRlOWMtYjQ2NS1kOTE5YWMzMWVhMjEucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI2MDMyMiUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNjAzMjJUMDcyMTM2WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9NGI4ODU4ZTYxNThmYTM0ZTIyYmVhOGMzYTk2MjRlY2JiY2FhZWVkNDkzMTJjMzU5YzE1NjUxM2M1YTgwNjkzNCZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.xVrDIYyWt6vfHqSrg-KsbYeGP4P3wY3CJUEaIQFUeNE">

    A week later and this is still running against your "refactor"

    Hi, just ran a quick check for single-thread, the times are the same within noise, with the change being marginally faster. Will test again with more blocks. Are your results in for the full run already?

    2026-03-21 | reindex-chainstate | par=1 | 300000 blocks | dbcache 5000 | Fractal | x86_64 | AMD Ryzen 9 9950X3D 16-Core Processor | 32 cores | 59Gi RAM | SSD

    Benchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/home/sebastian/.bitcoin -stopatheight=300000 -dbcache=5000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 -par=1 (COMMIT = b6891931f7) Time (abs ≡): 2823.900 s [User: 2753.878 s, System: 9.790 s]

    Benchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/home/sebastian/.bitcoin -stopatheight=300000 -dbcache=5000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 -par=1 (COMMIT = 92a9aabe78) Time (abs ≡): 2782.771 s [User: 2773.661 s, System: 7.039 s]

    Relative speed comparison 1.01 COMPILER=gcc ./build/bin/bitcoind -datadir=/home/sebastian/.bitcoin -stopatheight=300000 -dbcache=5000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 -par=1 (COMMIT = b6891931f7) 1.00 COMPILER=gcc ./build/bin/bitcoind -datadir=/home/sebastian/.bitcoin -stopatheight=300000 -dbcache=5000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 -par=1 (COMMIT = 92a9aabe78)

  74. validation: Use checkqueue for all script checking in ConnectBlock
    Instead of having two code paths in ConnectBlock, one that collects
    script checks for the checkqueue (multithreaded) and one that runs
    CheckInputScripts inline (single-threaded), just always use thecheckqueue.
    ConnectBlock now calls PrepareScriptChecks directly and constructs
    CScriptCheck objects to add to the queue regardless of the thread count.
    
    This removes the HasThreads() check and the single-threaded special
    case. The checkqueue already handles the case of having zero worker
    threads by running checks on the calling thread.
    
    Previously, script execution cache population during TestBlockValidity
    (fJustCheck=true) only worked in the single-threaded case, because
    CheckInputScripts guarded the cache insertion with !pvChecks. In the
    multithreaded case, pvChecks was always set, so the cache was never
    populated. This inconsistency has existed since the script execution
    cache was introduced. Now that ConnectBlock no longer uses
    CheckInputScripts, cache population is done explicitly after it is
    confirmed that all script checks have passed. This means we populte
    the cache in both single-threaded and multithreaded cases.
    d84cbf4b53
  75. refactor: Remove pvChecks parameter from CheckInputScripts
    Since ConnectBlock now constructs script checks directly via
    PrepareScriptChecks, no caller passes a pvChecks vector to
    CheckInputScripts. Remove the parameter and the associated
    dead code paths.
    23ac5f3f48
  76. fjahr force-pushed on Mar 23, 2026
  77. fjahr commented at 4:50 PM on March 23, 2026: contributor

    I think this is incorrect. Script execution caching was introduced in commit https://github.com/bitcoin/bitcoin/commit/b5fea8d0ccc8117644a4ea11256bc531b60fd9a3, it makes TBV populate this cache (likewise the pre-existing sigcache). This makes sense and i see no reason to believe this behaviour was unintended. On the contrary, the commit also updates a comment to explicitly mention TBV, and i would presume the author was well-aware.

    Besides, i think this is a regression to remove script execution caching from TBV. This introduces additional delay at block submission time, doing work that was already performed at block template generation time.

    Thank you, yeah, you are right, I misunderstood @theuni 's comment of "likely unintended" slightly and went in the wrong direction here without fully thinking it through and checking the history. I have now re-introduced caching in the same commit and fixed the commit message accordingly.

    Hmm but TBV would only help here insofar as it would re-cache whatever would have been previously cached by the mempool and then overridden, which could be negligible.

    Right, I think for most standard scenarios there will be little to no benefit from caching here. But the cost is also minimal and I suspect there are some more rarer use-cases where this can still be beneficial. As we know not all transactions are coming through the mempool and also not all fees may be paid from within the transaction. Not something we focus on optimizing for usually but these are just examples for potential scenarios where it might matter.

  78. svanstaa commented at 4:36 PM on April 5, 2026: none

    Results are in for the full run. No measurable reduction of speed after the change. Benchmark-wise, the PR is good.

    2026-03-26 | reindex-chainstate | par=1 | 936639 blocks | dbcache 5000 | Fractal | x86_64 | AMD Ryzen 9 9950X3D 16-Core Processor | 32 cores | 59Gi RAM | SSD

    Benchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/home/sebastian/.bitcoin -stopatheight=936639 -dbcache=5000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 -par=1 (COMMIT = b6891931f7) Time (abs ≡): 132546.275 s [User: 128543.250 s, System: 505.881 s]

    Benchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/home/sebastian/.bitcoin -stopatheight=936639 -dbcache=5000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 -par=1 (COMMIT = 92a9aabe78) Time (abs ≡): 132469.850 s [User: 128467.273 s, System: 503.243 s]

    Relative speed comparison 1.00 COMPILER=gcc ./build/bin/bitcoind -datadir=/home/sebastian/.bitcoin -stopatheight=936639 -dbcache=5000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 -par=1 (COMMIT = b6891931f7) 1.00 COMPILER=gcc ./build/bin/bitcoind -datadir=/home/sebastian/.bitcoin -stopatheight=936639 -dbcache=5000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 -par=1 (COMMIT = 92a9aabe78)

    <details>

    <summary>additional full benchmark run data</summary>

    for DBCACHE in 5000; do
    COMMITS="b6891931f7 92a9aabe78";
    STOP=936639; CC=gcc; CXX=g++;
    BASE_DIR="$HOME/.bitcoin"; DATA_DIR="$BASE_DIR"; LOG_DIR="$HOME/bench-logs";
    mkdir -p $LOG_DIR;
    (echo ""; for c in $COMMITS; do git log -1 --pretty='%h %s' $c || exit 1; done) &&
    (echo "" && echo "$(date -I) | reindex-chainstate | par=1 | ${STOP} blocks | dbcache ${DBCACHE} | $(hostname) | $(uname -m) | $(lscpu | grep 'Model name' | head -1 | cut -d: -f2 | xargs) | $(nproc) cores | $(free -h | awk '/^Mem:/{print $2}') RAM | SSD"; echo "") &&
    hyperfine
    --sort command
    --runs 1
    --export-json "$LOG_DIR/rdx-par1-$STOP-$DBCACHE-$CC.json"
    --parameter-list COMMIT ${COMMITS// /,}
    --prepare "killall -9 bitcoind 2>/dev/null; sleep 3; rm -f ./build/bin/bitcoind; git clean -fxd; git reset --hard {COMMIT} &&
    cmake -B build -DCMAKE_BUILD_TYPE=Release -DENABLE_IPC=OFF && make -C build bitcoind -j$(nproc) &&
    ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=1000 -printtoconsole=0; sleep 10; rm -f $DATA_DIR/debug.log"
    --conclude "killall bitcoind 2>/dev/null || true; sleep 5; grep -q 'height=$STOP' $DATA_DIR/debug.log;
    cp $DATA_DIR/debug.log $LOG_DIR/debug-{COMMIT}-par1-$(date +%s).log"
    "COMPILER=$CC ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=$DBCACHE -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 -par=1";
    done

    b6891931f7 refactor: Extract PrepareScriptChecks from CheckInputScripts 92a9aabe78 validation: Use checkqueue for all script checking in ConnectBlock

    </details>

  79. l0rinc commented at 10:20 AM on April 6, 2026: contributor

    No measurable reduction of speed after the change. Benchmark-wise, the PR is good

    Thanks for checking. I ran it again and it crashed my server again. It's possible it's unrelated to this PR, will retry on a different machine, maybe the server is toast. Or maybe there's a race condition. Doesn't really matter, my main concerns are still:

    • The PR title still says "refactor" but commit d84cbf4b5366d4802f973f99e65c903daad80923 unifies the single-threaded and multithreaded paths, which is a behavior change in consensus-critical code. I still have a hard time trusting this completely, I think we should do the refactor and the behavior change separately.
    • The back-and-forth around cache population (e.g. darosior's correction) illustrates exactly why structural refactoring and behavioral changes shouldn't be done in the same PR for consensus critical code.
    • Missing upfront test coverage to demonstrate no behavior change - see my alternative which adds thread-mode tests first before any refactoring, so regressions are caught immediately. I'd be happy to close that PR if you decide to do only the refactoring here, but I don't like the behavior changes here.

    Edit: I managed to run -par=1 on a different server, it has very similar performance. My objections still stand, see above.

    <details><summary>Before</summary>

    for DBCACHE in 5000; do \
        COMMITS="544c15ff4e22e0271f91341b446ad84b6f4fd6c3"; \
        STOP=936639; CC=gcc; CXX=g++; \
        BASE_DIR="/mnt/my_storage"; DATA_DIR="$BASE_DIR/BitcoinData"; LOG_DIR="$BASE_DIR/logs"; \
        (echo ""; for c in $COMMITS; do git fetch -q origin $c && git log -1 --pretty='%h %s' $c || exit 1; done) && \
        (echo "" && echo "$(date -I) | reindex-chainstate | ${STOP} blocks | dbcache ${DBCACHE} | $(hostname) | $(uname -m) | $(lscpu | grep 'Model name' | head -1 | cut -d: -f2 | xargs) | $(nproc) cores | $(free -h | awk '/^Mem:/{print $2}') RAM | $(lsblk -no ROTA $(df --output=source $BASE_DIR | tail -1) | grep -q 1 && echo HDD || echo SSD)"; echo "") && \
        hyperfine \
        --sort command \
        --runs 1 \
        --export-json "$BASE_DIR/rdx-$(sed -E 's/(\w{8})\w+ ?/\1-/g;s/-$//'<<<"$COMMITS")-$STOP-$DBCACHE-$CC.json" \
        --parameter-list COMMIT ${COMMITS// /,} \
        --prepare "killall -9 bitcoind 2>/dev/null; rm -f ./build/bin/bitcoind; git clean -fxd; git reset --hard {COMMIT} && \
          cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DENABLE_IPC=OFF && ninja -C build bitcoind -j1 && \
          ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=1000 -printtoconsole=0; sleep 20; rm -f $DATA_DIR/debug.log" \
        --conclude "killall bitcoind || true; sleep 5; grep -q 'height=0' $DATA_DIR/debug.log && grep -q 'Disabling script verification at block [#1](/bitcoin-bitcoin/1/)' $DATA_DIR/debug.log && grep -q 'height=$STOP' $DATA_DIR/debug.log && grep 'Bitcoin Core version' $DATA_DIR/debug.log | grep -q "$(printf %.12s {COMMIT})"; \
                    cp $DATA_DIR/debug.log $LOG_DIR/debug-{COMMIT}-$(date +%s).log" \
        "COMPILER=$CC ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=$DBCACHE -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 -par=1";
    done
    
    544c15ff4e Merge bitcoin/bitcoin#34759: walletdb: hash pubkey/privkey in one shot to avoid leaking secret data
    
    2026-04-10 | reindex-chainstate | 936639 blocks | dbcache 5000 | i7-hdd | x86_64 | Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz | 8 cores | 62Gi RAM | HDD
    
    Benchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=936639 -dbcache=5000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 -par=1 (COMMIT = 544c15ff4e22e0271f91341b446ad84b6f4fd6c3)
      Time (abs ≡):        223862.666 s               [User: 220249.873 s, System: 1347.124 s]
    

    </details>

    <details><summary>After</summary>

    for DBCACHE in 5000; do \
        COMMITS="23ac5f3f48d00a2087f9676609eb2e4b48813235"; \
        STOP=936639; CC=gcc; CXX=g++; \
        BASE_DIR="/mnt/my_storage"; DATA_DIR="$BASE_DIR/BitcoinData"; LOG_DIR="$BASE_DIR/logs"; \
        (echo ""; for c in $COMMITS; do git fetch -q origin $c && git log -1 --pretty='%h %s' $c || exit 1; done) && \
        (echo "" && echo "$(date -I) | reindex-chainstate | ${STOP} blocks | dbcache ${DBCACHE} | $(hostname) | $(uname -m) | $(lscpu | grep 'Model name' | head -1 | cut -d: -f2 | xargs) | $(nproc) cores | $(free -h | awk '/^Mem:/{print $2}') RAM | $(lsblk -no ROTA $(df --output=source $BASE_DIR | tail -1) | grep -q 1 && echo HDD || echo SSD)"; echo "") && \
        hyperfine \
        --sort command \
        --runs 1 \
        --export-json "$BASE_DIR/rdx-$(sed -E 's/(\w{8})\w+ ?/\1-/g;s/-$//'<<<"$COMMITS")-$STOP-$DBCACHE-$CC.json" \
        --parameter-list COMMIT ${COMMITS// /,} \
        --prepare "killall -9 bitcoind 2>/dev/null; rm -f ./build/bin/bitcoind; git clean -fxd; git reset --hard {COMMIT} && \
          cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DENABLE_IPC=OFF && ninja -C build bitcoind -j1 && \
          ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=1000 -printtoconsole=0; sleep 20; rm -f $DATA_DIR/debug.log" \
        --conclude "killall bitcoind || true; sleep 5; grep -q 'height=0' $DATA_DIR/debug.log && grep -q 'Disabling script verification at block [#1](/bitcoin-bitcoin/1/)' $DATA_DIR/debug.log && grep -q 'height=$STOP' $DATA_DIR/debug.log && grep 'Bitcoin Core version' $DATA_DIR/debug.log | grep -q "$(printf %.12s {COMMIT})"; \
                    cp $DATA_DIR/debug.log $LOG_DIR/debug-{COMMIT}-$(date +%s).log" \
        "COMPILER=$CC ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=$DBCACHE -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 -par=1";
    done
    
    23ac5f3f48 refactor: Remove pvChecks parameter from CheckInputScripts
    
    2026-04-07 | reindex-chainstate | 936639 blocks | dbcache 5000 | i7-hdd | x86_64 | Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz | 8 cores | 62Gi RAM | HDD
    
    Benchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=936639 -dbcache=5000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0 -par=1 (COMMIT = 23ac5f3f48d00a2087f9676609eb2e4b48813235)
      Time (abs ≡):        230301.470 s               [User: 219951.017 s, System: 1338.768 s]
    

    </details>

  80. fjahr renamed this:
    refactor: Remove special treatment for single threaded script checking
    consensus: Remove special treatment for single threaded script checking
    on Apr 14, 2026
  81. fjahr commented at 12:28 PM on April 14, 2026: contributor

    I managed to run -par=1 on a different server, it has very similar performance.

    Thanks for finally confirming that.

    For the future: Maybe post a new comment with your benchmark results instead of editing a comment that is over a week old. This way, other reviewers will get a notification which may trigger them to continue their review. Review which might have been blocked by your earlier comments about a potential performance issue.

    The PR title still says "refactor" but commit https://github.com/bitcoin/bitcoin/commit/d84cbf4b5366d4802f973f99e65c903daad80923 unifies the single-threaded and multithreaded paths, which is a behavior change in consensus-critical code. I still have #32575 (review), I think we should do the refactor and the behavior change #34875.

    I have removed refactoring from the title although I think it's highly debatable if this is necessary. It seems like by your definition no code change is actually a refactor unless, maybe, it compiles to the exact same ASM. The way consensus rules are enforced is still exactly the same, there isn't even new code being used for it, it's using the same code just without worker threads which already works because the master thread has always been participating in the work. Thus this is still a refactor by my definition.

    The back-and-forth around cache population (e.g. darosior's #32575 (comment)) illustrates exactly why structural refactoring and behavioral changes shouldn't be done in the same PR for consensus critical code.

    Indeed @darosior made a great observation and I made an according change but as you can see from the prior comments, the code was previously broken and the benefit of this is not entirely clear either so this would not have been a dramatic issue either way it seems. I can also promise you that I would have likely made the same mistake if I had broken up the PR in two from the start and we don't know if @darosior would have caught the issue in that alternative universe. So, all the evidence that I see is that the review process works on this PR.

    Missing upfront test coverage to demonstrate no behavior change - see my alternative which adds thread-mode tests first before any refactoring, so regressions are caught immediately. I'd be happy to close that PR if you decide to do only the refactoring here, but I don't like the behavior changes here.

    The functional tests already have -par=1 coverage and the fuzz test also use 0 worker threads in deterministic mode so I think this doesn't make a huge difference but if this makes you happy finally, I can pull the tests commit in here since it doesn't seem to hurt at first glance. Otherwise you could also add it as a follow-up or open a stand-alone PR. Since your PR changes a similar number of LoCs I don't think it's any easier to review than this PR here and just because you don't use CheckQueue it isn't any less "dangerous". So stacking another PR on top of yours then to get what we actually want isn't helpful in terms of safety or review burden, but I have already laid this out previously in more detail. It also doesn't seem like your comments have changed the minds of the other, previous commenters, so I will leave the changes here as is.

  82. l0rinc commented at 2:05 PM on April 14, 2026: contributor

    Maybe post a new comment with your benchmark results instead of editing a comment that is over a week old

    I have explicitly pinged you and the original comment already stated that performance wasn't my main concern and that I was still measuring it on a different machine. Are you not curious why the other server failed? The updated results don't change my review - my objections are about mixing a behavior change with a refactor in consensus-critical code, not performance.

    The way consensus rules are enforced is still exactly the same

    I disagree. The BIP-110 consensus failure specifically relied on the -par=1 path, which is exactly the path being unified here. That bug demonstrates concretely that changes in this area can have non-obvious consensus implications - which is why I keep suggesting we separate the behavior change from the pure refactor.

    just because you don't use CheckQueue it isn't any less "dangerous"

    I think it is, precisely because of the above. Keeping both paths intact while cleaning up their implementations avoids introducing new interactions between previously independent code paths.

    but if this makes you happy finally

    I'm raising these concerns because I think they matter for the safety of the codebase, and I find the overall dismissive tone of this response concerning. We don't have to agree, but I'd appreciate the concerns being engaged with on their technical merits.

    Since your PR changes a similar number of LoCs

    The line count isn't the concern. It's combining a behavior change with a refactor in consensus-critical code. darosior's correction about cache population and the BIP-110 consensus bug are concrete examples of why that combination is risky - that issue was caught only after detailed review, and it directly resulted from mixing refactoring with behavioral changes. Separating the two would make each part individually easier to verify.

  83. fjahr commented at 2:35 PM on April 14, 2026: contributor

    Are you not curious why the other server failed?

    I am more than happy to look it when you actually post some information on what might have happened there. Until then it's just a distraction and I don't think this PR should be kept hostage by it after there was no follow-up for a long time. The performance issue that you were threatening for weeks didn't turn out to be a problem either.

    The updated results don't change my review

    That's fine, I think I have addressed your comments suffiently, taking into account earlier reviews and weighing them against your concerns as well as my personal view and laying out my arguments as clearly as I can. But I am always happy to hear from other reviewers and reconsider if they have changed their mind based on your comments. I will not engage further on you posting your earlier arguments repeatedly since it seems like a waste of everyone's time.

    We don't have to agree

    Right, let's move on.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-04-28 03:12 UTC

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