refactor: 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

    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/32575.

    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 <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34875 (refactor: separate deferred script check collection from CheckInputScripts by l0rinc)
    • #32317 (kernel: Separate UTXO set access from validation functions by sedited)
    • #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.

    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

    2026-03-23 16:50:25

  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:

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

    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:

     0diff --git a/src/validation.cpp b/src/validation.cpp
     1index 67130a31cf..3947d0dfde 100644
     2--- a/src/validation.cpp
     3+++ b/src/validation.cpp
     4@@ -2036,6 +2036,18 @@ ValidationCache::ValidationCache(const size_t script_execution_cache_bytes, cons
     5               approx_size_bytes >> 20, script_execution_cache_bytes >> 20, num_elems);
     6 }
     7 
     8+bool ValidationCache::IsScriptValidated(const uint256& entry, bool erase) const
     9+{
    10+    AssertLockHeld(cs_main); //TODO: Remove this requirement by making CuckooCache not require external locks
    11+    return m_script_execution_cache.contains(entry, erase);
    12+}
    13+
    14+void ValidationCache::CacheScriptValidation(const uint256& entry)
    15+{
    16+    AssertLockHeld(cs_main); //TODO: Remove this requirement by making CuckooCache not require external locks
    17+    m_script_execution_cache.insert(entry);
    18+}
    19+
    20 /**
    21  * Check whether all of this transaction's input scripts succeed.
    22  *
    23@@ -2075,8 +2087,7 @@ bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
    24     uint256 hashCacheEntry;
    25     CSHA256 hasher = validation_cache.ScriptExecutionCacheHasher();
    26     hasher.Write(UCharCast(tx.GetWitnessHash().begin()), 32).Write((unsigned char*)&flags, sizeof(flags)).Finalize(hashCacheEntry.begin());
    27-    AssertLockHeld(cs_main); //TODO: Remove this requirement by making CuckooCache not require external locks
    28-    if (validation_cache.m_script_execution_cache.contains(hashCacheEntry, !cacheFullScriptStore)) {
    29+    if (validation_cache.IsScriptValidated(hashCacheEntry, !cacheFullScriptStore)) {
    30         return true;
    31     }
    32 
    33@@ -2124,7 +2135,7 @@ bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
    34     if (cacheFullScriptStore && !pvChecks) {
    35         // We executed all of the provided scripts, and were told to
    36         // cache the result. Do so now.
    37-        validation_cache.m_script_execution_cache.insert(hashCacheEntry);
    38+        validation_cache.CacheScriptValidation(hashCacheEntry);
    39     }
    40 
    41     return true;
    42diff --git a/src/validation.h b/src/validation.h
    43index 482772c0d6..2bec47aecc 100644
    44--- a/src/validation.h
    45+++ b/src/validation.h
    46@@ -373,8 +373,10 @@ private:
    47     //! Pre-initialized hasher to avoid having to recreate it for every hash calculation.
    48     CSHA256 m_script_execution_cache_hasher;
    49 
    50-public:
    51+    //! Cache for script verification results to avoid re-validation.
    52     CuckooCache::cache<uint256, SignatureCacheHasher> m_script_execution_cache;
    53+
    54+public:
    55     SignatureCache m_signature_cache;
    56 
    57     ValidationCache(size_t script_execution_cache_bytes, size_t signature_cache_bytes);
    58@@ -384,6 +386,9 @@ public:
    59 
    60     //! Return a copy of the pre-initialized hasher.
    61     CSHA256 ScriptExecutionCacheHasher() const { return m_script_execution_cache_hasher; }
    62+
    63+    bool IsScriptValidated(const uint256& entry, bool erase) const EXCLUSIVE_LOCKS_REQUIRED(cs_main);
    64+    void CacheScriptValidation(const uint256& entry) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
    65 };
    66 
    67 /** 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:

     0diff --git a/src/validation.cpp b/src/validation.cpp
     1--- a/src/validation.cpp	(revision d26ff1ff03da3531d1a6a28466784911ece28e0a)
     2+++ b/src/validation.cpp	(date 1772711510934)
     3@@ -2048,6 +2048,18 @@
     4     m_script_execution_cache.insert(entry);
     5 }
     6 
     7+static std::vector<CTxOut> GetSpentOutputs(const CTransaction& tx, const CCoinsViewCache& inputs)
     8+{
     9+    std::vector<CTxOut> spent_outputs;
    10+    spent_outputs.reserve(tx.vin.size());
    11+    for (const auto& txin : tx.vin) {
    12+        const Coin& coin = inputs.AccessCoin(txin.prevout);
    13+        assert(!coin.IsSpent());
    14+        spent_outputs.emplace_back(coin.out);
    15+    }
    16+    return spent_outputs;
    17+}
    18+
    19 /**
    20  * Check whether all of this transaction's input scripts succeed.
    21  *
    22@@ -2091,18 +2103,7 @@
    23         return true;
    24     }
    25 
    26-    if (!txdata.m_spent_outputs_ready) {
    27-        std::vector<CTxOut> spent_outputs;
    28-        spent_outputs.reserve(tx.vin.size());
    29-
    30-        for (const auto& txin : tx.vin) {
    31-            const COutPoint& prevout = txin.prevout;
    32-            const Coin& coin = inputs.AccessCoin(prevout);
    33-            assert(!coin.IsSpent());
    34-            spent_outputs.emplace_back(coin.out);
    35-        }
    36-        txdata.Init(tx, std::move(spent_outputs));
    37-    }
    38+    if (!txdata.m_spent_outputs_ready) txdata.Init(tx, GetSpentOutputs(tx, inputs));
    39     assert(txdata.m_spent_outputs.size() == tx.vin.size());
    40 
    41     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:

     0diff --git a/src/validation.cpp b/src/validation.cpp
     1--- a/src/validation.cpp	(revision 4b2f347fc0495a9e4ddb9e47ec066f3b82a20d30)
     2+++ b/src/validation.cpp	(date 1772727309644)
     3@@ -2060,6 +2060,36 @@
     4     return spent_outputs;
     5 }
     6 
     7+/** Perform pre-checks for CheckInputScripts: skip coinbase, check the script
     8+ *  execution cache, reserve space for script checks, and initialize txdata.
     9+ *  Returns nullopt if the transaction is already validated (caller should
    10+ *  return true), or the cache entry hash needed to store results later. */
    11+static std::optional<uint256> LookupScriptValidation(
    12+    const CTransaction& tx,
    13+    const CCoinsViewCache& inputs, script_verify_flags flags,
    14+    bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
    15+    const ValidationCache& validation_cache) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
    16+{
    17+    if (tx.IsCoinBase()) return std::nullopt;
    18+
    19+    // First check if script executions have been cached with the same
    20+    // flags. Note that this assumes that the inputs provided are
    21+    // correct (ie that the transaction hash which is in tx's prevouts
    22+    // properly commits to the scriptPubKey in the inputs view of that
    23+    // transaction).
    24+    uint256 hashCacheEntry;
    25+    CSHA256 hasher = validation_cache.ScriptExecutionCacheHasher();
    26+    hasher.Write(UCharCast(tx.GetWitnessHash().begin()), 32).Write((unsigned char*)&flags, sizeof(flags)).Finalize(hashCacheEntry.begin());
    27+    if (validation_cache.IsScriptValidated(hashCacheEntry, !cacheFullScriptStore)) {
    28+        return std::nullopt;
    29+    }
    30+
    31+    if (!txdata.m_spent_outputs_ready) txdata.Init(tx, GetSpentOutputs(tx, inputs));
    32+    assert(txdata.m_spent_outputs.size() == tx.vin.size());
    33+
    34+    return hashCacheEntry;
    35+}
    36+
    37 /**
    38  * Check whether all of this transaction's input scripts succeed.
    39  *
    40@@ -2085,22 +2115,8 @@
    41                        ValidationCache& validation_cache,
    42                        std::vector<CScriptCheck>* pvChecks)
    43 {
    44-    if (tx.IsCoinBase()) return true;
    45-
    46-    // First check if script executions have been cached with the same
    47-    // flags. Note that this assumes that the inputs provided are
    48-    // correct (ie that the transaction hash which is in tx's prevouts
    49-    // properly commits to the scriptPubKey in the inputs view of that
    50-    // transaction).
    51-    uint256 hashCacheEntry;
    52-    CSHA256 hasher = validation_cache.ScriptExecutionCacheHasher();
    53-    hasher.Write(UCharCast(tx.GetWitnessHash().begin()), 32).Write((unsigned char*)&flags, sizeof(flags)).Finalize(hashCacheEntry.begin());
    54-    if (validation_cache.IsScriptValidated(hashCacheEntry, !cacheFullScriptStore)) {
    55-        return true;
    56-    }
    57-
    58-    if (!txdata.m_spent_outputs_ready) txdata.Init(tx, GetSpentOutputs(tx, inputs));
    59-    assert(txdata.m_spent_outputs.size() == tx.vin.size());
    60+    const auto hash_cache_entry{LookupScriptValidation(tx, inputs, flags, cacheFullScriptStore, txdata, validation_cache)};
    61+    if (!hash_cache_entry) return true;
    62 
    63     for (unsigned int i = 0; i < tx.vin.size(); i++) {
    64 
    65@@ -2133,7 +2149,7 @@
    66     if (cacheFullScriptStore && !pvChecks) {
    67         // We executed all of the provided scripts, and were told to
    68         // cache the result. Do so now.
    69-        validation_cache.CacheScriptValidation(hashCacheEntry);
    70+        validation_cache.CacheScriptValidation(*hash_cache_entry);
    71     }
    72 
    73     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:

    0    const auto hash_cache_entry{LookupScriptValidation(tx, inputs, flags, cacheFullScriptStore, txdata, validation_cache, pvChecks)};
    1    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:

     0diff --git a/src/validation.cpp b/src/validation.cpp
     1--- a/src/validation.cpp	(revision c009adccee7a268ec844e29bf7070b543a26ae90)
     2+++ b/src/validation.cpp	(date 1772723088401)
     3@@ -2600,18 +2600,14 @@
     4         if (!tx.IsCoinBase() && fScriptChecks)
     5         {
     6             bool fCacheResults = fJustCheck; /* Don't cache results if we're actually connecting blocks (still consult the cache, though) */
     7-            bool tx_ok;
     8             TxValidationState tx_state;
     9-            // If CheckInputScripts is called with a pointer to a checks vector, the resulting checks are appended to it. In that case
    10-            // they need to be added to control which runs them asynchronously. Otherwise, CheckInputScripts runs the checks before returning.
    11             if (control) {
    12+                // CheckInputScripts always returns true when pvChecks is provided
    13+                // (checks are collected for deferred execution, not executed inline).
    14                 std::vector<CScriptCheck> vChecks;
    15-                tx_ok = CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache, &vChecks);
    16-                if (tx_ok) control->Add(std::move(vChecks));
    17-            } else {
    18-                tx_ok = CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache);
    19-            }
    20-            if (!tx_ok) {
    21+                Assume(CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache, &vChecks));
    22+                control->Add(std::move(vChecks));
    23+            } else if (!CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache)) {
    24                 // Any transaction validation failure in ConnectBlock is a block consensus failure
    25                 state.Invalid(BlockValidationResult::BLOCK_CONSENSUS,
    26                               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:

     0diff --git a/src/validation.cpp b/src/validation.cpp
     1--- a/src/validation.cpp	(revision b3b6dc9bb7165bbfd21cb1af636cdfc467de7f01)
     2+++ b/src/validation.cpp	(revision 4b2f347fc0495a9e4ddb9e47ec066f3b82a20d30)
     3@@ -2087,10 +2087,6 @@
     4 {
     5     if (tx.IsCoinBase()) return true;
     6 
     7-    if (pvChecks) {
     8-        pvChecks->reserve(tx.vin.size());
     9-    }
    10-
    11     // First check if script executions have been cached with the same
    12     // flags. Note that this assumes that the inputs provided are
    13     // correct (ie that the transaction hash which is in tx's prevouts
    14@@ -2117,6 +2113,7 @@
    15         // Verify signature
    16         CScriptCheck check(txdata.m_spent_outputs[i], tx, validation_cache.m_signature_cache, i, flags, cacheSigStore, &txdata);
    17         if (pvChecks) {
    18+            pvChecks->reserve(tx.vin.size());
    19             pvChecks->emplace_back(std::move(check));
    20         } else if (auto result = check(); result.has_value()) {
    21             // 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:

      0diff --git a/src/validation.cpp b/src/validation.cpp
      1--- a/src/validation.cpp	(revision 87fcd645598d186bd1e4f68d5aca275dc7815fda)
      2+++ b/src/validation.cpp	(date 1772729378036)
      3@@ -137,11 +137,17 @@
      4     return m_chain.Genesis();
      5 }
      6 
      7-bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
      8-                       const CCoinsViewCache& inputs, script_verify_flags flags, bool cacheSigStore,
      9-                       bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
     10-                       ValidationCache& validation_cache,
     11-                       std::vector<CScriptCheck>* pvChecks = nullptr)
     12+void CollectScriptChecks(const CTransaction& tx,
     13+                         const CCoinsViewCache& inputs, script_verify_flags flags, bool cacheSigStore,
     14+                         bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
     15+                         ValidationCache& validation_cache,
     16+                         std::vector<CScriptCheck>& pvChecks)
     17+                         EXCLUSIVE_LOCKS_REQUIRED(cs_main);
     18+
     19+bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
     20+                       const CCoinsViewCache& inputs, script_verify_flags flags, bool cacheSigStore,
     21+                       bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
     22+                       ValidationCache& validation_cache)
     23                        EXCLUSIVE_LOCKS_REQUIRED(cs_main);
     24 
     25 bool CheckFinalTxAtTip(const CBlockIndex& active_chain_tip, const CTransaction& tx)
     26@@ -2060,8 +2066,8 @@
     27     return spent_outputs;
     28 }
     29 
     30-/** Perform pre-checks for CheckInputScripts: skip coinbase, check the script
     31- *  execution cache, reserve space for script checks, and initialize txdata.
     32+/** Perform pre-checks for script validation: skip coinbase, check the script
     33+ *  execution cache, and initialize txdata.
     34  *  Returns nullopt if the transaction is already validated (caller should
     35  *  return true), or the cache entry hash needed to store results later. */
     36 static std::optional<uint256> LookupScriptValidation(
     37@@ -2090,16 +2096,28 @@
     38     return hashCacheEntry;
     39 }
     40 
     41+/** Collect script checks for all inputs without executing them.
     42+ *  Non-static (and redeclared) in src/test/txvalidationcache_tests.cpp */
     43+void CollectScriptChecks(const CTransaction& tx,
     44+                         const CCoinsViewCache& inputs, script_verify_flags flags, bool cacheSigStore,
     45+                         bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
     46+                         ValidationCache& validation_cache,
     47+                         std::vector<CScriptCheck>& checks) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
     48+{
     49+    if (LookupScriptValidation(tx, inputs, flags, cacheFullScriptStore, txdata, validation_cache)) {
     50+        checks.reserve(tx.vin.size());
     51+        for (unsigned int i = 0; i < tx.vin.size(); i++) {
     52+            checks.emplace_back(txdata.m_spent_outputs[i], tx, validation_cache.m_signature_cache, i, flags, cacheSigStore, &txdata);
     53+        }
     54+    }
     55+}
     56+
     57 /**
     58  * Check whether all of this transaction's input scripts succeed.
     59  *
     60  * This involves ECDSA signature checks so can be computationally intensive. This function should
     61  * only be called after the cheap sanity checks in CheckTxInputs passed.
     62  *
     63- * If pvChecks is not nullptr, script checks are pushed onto it instead of being performed inline. Any
     64- * script checks which are not necessary (eg due to script execution cache hits) are, obviously,
     65- * not pushed onto pvChecks/run.
     66- *
     67  * Setting cacheSigStore/cacheFullScriptStore to false will remove elements from the corresponding cache
     68  * which are matched. This is useful for checking blocks where we will likely never need the cache
     69  * entry again.
     70@@ -2112,8 +2130,7 @@
     71 bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
     72                        const CCoinsViewCache& inputs, script_verify_flags flags, bool cacheSigStore,
     73                        bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
     74-                       ValidationCache& validation_cache,
     75-                       std::vector<CScriptCheck>* pvChecks)
     76+                       ValidationCache& validation_cache)
     77 {
     78     const auto hash_cache_entry{LookupScriptValidation(tx, inputs, flags, cacheFullScriptStore, txdata, validation_cache)};
     79     if (!hash_cache_entry) return true;
     80@@ -2128,10 +2145,7 @@
     81 
     82         // Verify signature
     83         CScriptCheck check(txdata.m_spent_outputs[i], tx, validation_cache.m_signature_cache, i, flags, cacheSigStore, &txdata);
     84-        if (pvChecks) {
     85-            pvChecks->reserve(tx.vin.size());
     86-            pvChecks->emplace_back(std::move(check));
     87-        } else if (auto result = check(); result.has_value()) {
     88+        if (auto result = check()) {
     89             // Tx failures never trigger disconnections/bans.
     90             // This is so that network splits aren't triggered
     91             // either due to non-consensus relay policies (such as
     92@@ -2146,7 +2160,7 @@
     93         }
     94     }
     95 
     96-    if (cacheFullScriptStore && !pvChecks) {
     97+    if (cacheFullScriptStore) {
     98         // We executed all of the provided scripts, and were told to
     99         // cache the result. Do so now.
    100         validation_cache.CacheScriptValidation(*hash_cache_entry);
    101@@ -2596,18 +2610,20 @@
    102         if (!tx.IsCoinBase() && fScriptChecks)
    103         {
    104             bool fCacheResults = fJustCheck; /* Don't cache results if we're actually connecting blocks (still consult the cache, though) */
    105-            TxValidationState tx_state;
    106             if (control) {
    107                 // CheckInputScripts always returns true when pvChecks is provided
    108                 // (checks are collected for deferred execution, not executed inline).
    109                 std::vector<CScriptCheck> vChecks;
    110-                Assume(CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache, &vChecks));
    111+                CollectScriptChecks(tx, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache, vChecks);
    112                 control->Add(std::move(vChecks));
    113-            } else if (!CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache)) {
    114-                // Any transaction validation failure in ConnectBlock is a block consensus failure
    115-                state.Invalid(BlockValidationResult::BLOCK_CONSENSUS,
    116-                              tx_state.GetRejectReason(), tx_state.GetDebugMessage());
    117-                break;
    118+            } else {
    119+                TxValidationState tx_state;
    120+                if (!CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache)) {
    121+                    // Any transaction validation failure in ConnectBlock is a block consensus failure
    122+                    state.Invalid(BlockValidationResult::BLOCK_CONSENSUS,
    123+                                  tx_state.GetRejectReason(), tx_state.GetDebugMessage());
    124+                    break;
    125+                }
    126             }
    127         }
    

    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

     0for DBCACHE in 5000; do \
     1    COMMITS="043301a537fdad469ef83ee3d1033404a0cefe16 7726a6a6c3e378e538ecb4b5bfd82c2880cc0688"; \
     2    STOP=936639; CC=gcc; CXX=g++; \
     3    BASE_DIR="/mnt/my_storage"; DATA_DIR="$BASE_DIR/BitcoinData"; LOG_DIR="$BASE_DIR/logs"; \
     4    (echo ""; for c in $COMMITS; do git fetch -q origin $c && git log -1 --pretty='%h %s' $c || exit 1; done) && \
     5    (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 "") && \
     6    hyperfine \
     7    --sort command \
     8    --runs 1 \
     9    --export-json "$BASE_DIR/rdx-$(sed -E 's/(\w{8})\w+ ?/\1-/g;s/-$//'<<<"$COMMITS")-$STOP-$DBCACHE-$CC.json" \
    10    --parameter-list COMMIT ${COMMITS// /,} \
    11    --prepare "killall -9 bitcoind 2>/dev/null; rm -f ./build/bin/bitcoind; git clean -fxd; git reset --hard {COMMIT} && \
    12      cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DENABLE_IPC=OFF && ninja -C build bitcoind -j1 && \
    13      ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=1000 -printtoconsole=0; sleep 20; rm -f $DATA_DIR/debug.log" \
    14    --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})"; \
    15                cp $DATA_DIR/debug.log $LOG_DIR/debug-{COMMIT}-$(date +%s).log" \
    16    "COMPILER=$CC ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=$DBCACHE -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 -assumevalid=0";
    17done
    18
    19043301a537 refactor: Extract cache check from CheckInputScripts
    207726a6a6c3 validation, refactor: Remove single threaded special case
    21
    222026-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
    23
    24Benchmark 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)
    25  Time (abs ):        37023.077 s               [User: 389375.279 s, System: 1125.389 s]
    26 
    27Benchmark 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)
    28  Time (abs ):        37195.482 s               [User: 388819.276 s, System: 1134.977 s]
    29 
    30Relative speed comparison
    31        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)
    32        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)
    

    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

    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

    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.


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-03-30 00:13 UTC

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