test,refactor: extract script template helpers & widen sigop count coverage #32729

pull l0rinc wants to merge 17 commits into bitcoin:master from l0rinc:l0rinc/sigop-testing changing 26 files +504 −204
  1. l0rinc commented at 2:44 pm on June 11, 2025: contributor

    Summary

    This PR extracts a set of cheap, inline helpers for recognizing the most common script templates, clarifies the definition of valid op-codes for pre-Taproot scripts, and splits the existing “deserialize + check-block” benchmark into three orthogonal micro-benchmarks.
    The change is behavior-neutral for consensus and policy - every modified call-site performs the same checks as before, just through clearer helper functions.

    Context

    While working on GetSigOpCount optimizations for known script templates I noticed that feature-test coverage was thin for this code path.
    This PR therefore adds tests for error cases (malformed push-data encodings) and for the expected sigop totals of the various script templates (including edge cases like a sigop after an OP_RETURN).

    Given the difficulty of reviewing the original optimizations themselves, I split all test / benchmark work into this standalone PR.
    The recent burst of sigops related refactor activity (#31624, #32521, #32533) underlines the need for extra safety.

    The refactors here eliminate magic numbers, deduplicate template checks, and lay groundwork for future performance work.

    Structure

    • Benchmarks - first commits separate deserialization+hashing, validation, and sigop counting so each cost can be measured independently.
    • Template helpers - tiny, mechanical commits move each script-template check into script.h, replace all ad-hoc byte-checks, and add tests where necessary.
    • Tests - a full script-test suite covering malformed PUSHDATA sequences and the pre-taproot / accurate sigop totals for every standard template.
    • pre-taproot opcode ceiling - documents, enforces, and tests that OP_CHECKSIGADD > MAX_OPCODE.
    • Extra fuzzing - enabled all possible opcodes in ConsumeOpcodeType
  2. DrahtBot commented at 2:44 pm on June 11, 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/32729.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK ryanofsky, hodlinator

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

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #32554 (bench: replace embedded raw block with configurable block generator by l0rinc)
    • #32317 (kernel: Separate UTXO set access from validation functions by TheCharlatan)
    • #31989 (BIP-119 (OP_CHECKTEMPLATEVERIFY) (regtest only) by jamesob)
    • #31868 ([IBD] specialize block serialization by l0rinc)
    • #31682 ([IBD] specialize CheckBlock’s input & coinbase checks by l0rinc)
    • #29060 (Policy: Report reason inputs are non standard by ismaelsadeeq)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  3. l0rinc renamed this:
    test,refactor: sigops
    test: extract script template helpers & widen sigop count coverage
    on Jun 11, 2025
  4. DrahtBot added the label Tests on Jun 11, 2025
  5. l0rinc renamed this:
    test: extract script template helpers & widen sigop count coverage
    test,refactor: extract script template helpers & widen sigop count coverage
    on Jun 11, 2025
  6. l0rinc marked this as ready for review on Jun 11, 2025
  7. DrahtBot added the label Needs rebase on Jun 11, 2025
  8. l0rinc force-pushed on Jun 12, 2025
  9. DrahtBot removed the label Needs rebase on Jun 12, 2025
  10. DrahtBot added the label CI failed on Jul 21, 2025
  11. DrahtBot commented at 10:23 am on July 21, 2025: contributor

    🚧 At least one of the CI tasks failed. Task tidy: https://github.com/bitcoin/bitcoin/runs/43965124848 LLM reason (✨ experimental): The failure is caused by a compilation error in policy.cpp where a boolean value is incorrectly passed to a function expecting a CScript reference.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  12. l0rinc force-pushed on Jul 21, 2025
  13. DrahtBot removed the label CI failed on Jul 22, 2025
  14. l0rinc force-pushed on Jul 28, 2025
  15. l0rinc commented at 8:42 pm on July 28, 2025: contributor
    Rebased after #32279 - updating the tests with the new convenience template helpers instead of using Solver for them. Ready for review again.
  16. in src/script/script.h:537 in 06cb252c82 outdated
    533@@ -534,7 +534,7 @@ class CScript : public CScriptBase
    534      * counted more accurately, assuming they are of the form
    535      *  ... OP_N CHECKMULTISIG ...
    536      */
    537-    unsigned int GetSigOpCount(bool fAccurate) const;
    538+    unsigned int GetLegacySigOpCount(bool fAccurate) const;
    


    ryanofsky commented at 0:22 am on August 5, 2025:

    In commit “refactor: rename GetSigOpCount to GetLegacySigOpCount” (06cb252c8285746737e3385886da2efd1ab41f5a)

    I like getting rid of this overload, but I found the code here very confusing before this change, and I think renaming this to GetLegacySigOpCount makes the confusion worse because this method is not acting like a legacy function when it’s called with fAccurate = true. At first I though you might have been using legacy here to mean “pre-segwit” which is different than the way the GetLegacySigOpCount(CTransaction) uses legacy to mean “pre-p2sh”. But after noticing this is also called by the segwit WitnessSigOps function, I don’t see how the term “legacy” even applies here at all.

    I think I would suggest renaming this to CountSigOps to be consistent with the CountWitnessSigOps function, and giving it better documentation. I also think the other overload should be a standalone function not a CScript method because it is not really using the CScript.

    I think a change like the one below could be a lot clearer:

      0--- a/src/consensus/tx_verify.cpp
      1+++ b/src/consensus/tx_verify.cpp
      2@@ -113,10 +113,10 @@ unsigned int GetLegacySigOpCount(const CTransaction& tx)
      3 {
      4     unsigned int nSigOps = 0;
      5     for (const auto& txin : tx.vin) {
      6-        nSigOps += txin.scriptSig.GetLegacySigOpCount(/*fAccurate=*/false);
      7+        nSigOps += txin.scriptSig.CountSigOps(/*fAccurate=*/false);
      8     }
      9     for (const auto& txout : tx.vout) {
     10-        nSigOps += txout.scriptPubKey.GetLegacySigOpCount(/*fAccurate=*/false);
     11+        nSigOps += txout.scriptPubKey.CountSigOps(/*fAccurate=*/false);
     12     }
     13     return nSigOps;
     14 }
     15@@ -133,7 +133,7 @@ unsigned int GetP2SHSigOpCount(const CTransaction& tx, const CCoinsViewCache& in
     16         assert(!coin.IsSpent());
     17         const CTxOut &prevout = coin.out;
     18         if (prevout.scriptPubKey.IsPayToScriptHash())
     19-            nSigOps += prevout.scriptPubKey.GetSigOpCount(tx.vin[i].scriptSig);
     20+            nSigOps += CountP2SHSigOps(tx.vin[i].scriptSig);
     21     }
     22     return nSigOps;
     23 }
     24--- a/src/policy/policy.cpp
     25+++ b/src/policy/policy.cpp
     26@@ -181,8 +181,11 @@ static bool CheckSigopsBIP54(const CTransaction& tx, const CCoinsViewCache& inpu
     27         // `fAccurate` means correctly accounting sigops for CHECKMULTISIGs(VERIFY) with 16 pubkeys
     28         // or fewer. This method of accounting was introduced by BIP16, and BIP54 reuses it.
     29         // The GetSigOpCount call on the previous scriptPubKey counts both bare and P2SH sigops.
     30-        sigops += txin.scriptSig.GetLegacySigOpCount(/*fAccurate=*/true);
     31-        sigops += prev_txo.scriptPubKey.GetSigOpCount(txin.scriptSig);
     32+        sigops += txin.scriptSig.CountSigOps(/*fAccurate=*/true);
     33+        sigops += prev_txo.scriptPubKey.CountSigOps(/*fAccurate=*/true);
     34+        if (prev_txo.scriptPubKey.IsPayToScriptHash()) {
     35+           sigops += CountP2SHSigOps(txin.scriptSig);
     36+        }
     37 
     38         if (sigops > MAX_TX_LEGACY_SIGOPS) {
     39             return false;
     40@@ -239,7 +242,7 @@ bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
     41             if (stack.empty())
     42                 return false;
     43             CScript subscript(stack.back().begin(), stack.back().end());
     44-            if (subscript.GetLegacySigOpCount(/*fAccurate=*/true) > MAX_P2SH_SIGOPS) {
     45+            if (subscript.CountSigOps(/*fAccurate=*/true) > MAX_P2SH_SIGOPS) {
     46                 return false;
     47             }
     48         }
     49--- a/src/script/interpreter.cpp
     50+++ b/src/script/interpreter.cpp
     51@@ -2084,7 +2084,7 @@ size_t static WitnessSigOps(int witversion, const std::vector<unsigned char>& wi
     52 
     53         if (witprogram.size() == WITNESS_V0_SCRIPTHASH_SIZE && witness.stack.size() > 0) {
     54             CScript subscript(witness.stack.back().begin(), witness.stack.back().end());
     55-            return subscript.GetLegacySigOpCount(/*fAccurate=*/true);
     56+            return subscript.CountSigOps(/*fAccurate=*/true);
     57         }
     58     }
     59 
     60--- a/src/script/script.cpp
     61+++ b/src/script/script.cpp
     62@@ -156,7 +156,7 @@ std::string GetOpName(opcodetype opcode)
     63     }
     64 }
     65 
     66-unsigned int CScript::GetLegacySigOpCount(bool fAccurate) const
     67+unsigned int CScript::CountSigOps(bool fAccurate) const
     68 {
     69     unsigned int n = 0;
     70     const_iterator pc = begin();
     71@@ -180,15 +180,12 @@ unsigned int CScript::GetLegacySigOpCount(bool fAccurate) const
     72     return n;
     73 }
     74 
     75-unsigned int CScript::GetSigOpCount(const CScript& scriptSig) const
     76+unsigned int CountP2SHSigOps(const CScript& scriptSig)
     77 {
     78-    if (!IsPayToScriptHash())
     79-        return GetLegacySigOpCount(/*fAccurate=*/true);
     80-
     81     // This is a pay-to-script-hash scriptPubKey;
     82     // get the last item that the scriptSig
     83     // pushes onto the stack:
     84-    const_iterator pc = scriptSig.begin();
     85+    CScript::const_iterator pc = scriptSig.begin();
     86     std::vector<unsigned char> vData;
     87     while (pc < scriptSig.end())
     88     {
     89@@ -201,7 +198,7 @@ unsigned int CScript::GetSigOpCount(const CScript& scriptSig) const
     90 
     91     /// ... and return its opcount:
     92     CScript subscript(vData.begin(), vData.end());
     93-    return subscript.GetLegacySigOpCount(/*fAccurate=*/true);
     94+    return subscript.CountSigOps(/*fAccurate=*/true);
     95 }
     96 
     97 bool CScript::IsPayToAnchor() const
     98--- a/src/script/script.h
     99+++ b/src/script/script.h
    100@@ -528,19 +528,20 @@ public:
    101     }
    102 
    103     /**
    104-     * Pre-version-0.6, Bitcoin always counted CHECKMULTISIGs
    105-     * as 20 sigops. With pay-to-script-hash, that changed:
    106-     * CHECKMULTISIGs serialized in scriptSigs are
    107-     * counted more accurately, assuming they are of the form
    108-     *  ... OP_N CHECKMULTISIG ...
    109-     */
    110-    unsigned int GetLegacySigOpCount(bool fAccurate) const;
    111-
    112-    /**
    113-     * Accurately count sigOps, including sigOps in
    114-     * pay-to-script-hash transactions:
    115-     */
    116-    unsigned int GetSigOpCount(const CScript& scriptSig) const;
    117+    * Count the number of signature operations (sigops) in this script.
    118+    *
    119+    * The fAccurate parameter controls how sigops are counted for CHECKMULTISIG
    120+    * operations. Set fAccurate = true when analyzing P2SH redeem scripts or
    121+    * SegWit witness scripts, and false when analyzing scriptPubKeys or
    122+    * scriptSigs.
    123+    *
    124+    * Historically, Bitcoin always counted each CHECKMULTISIG as 20 sigops
    125+    * (MAX_PUBKEYS_PER_MULTISIG), regardless of the number of pubkeys. Starting
    126+    * with P2SH in version 0.6, CHECKMULTISIG operations inside wrapped scripts
    127+    * began to be counted more precisely, using the preceding OP_N opcode to
    128+    * determine the number of pubkeys and thus the number of sigops.
    129+    */
    130+    unsigned int CountSigOps(bool fAccurate) const;
    131 
    132     /*
    133      * OP_1 <0x4e73>
    134@@ -606,6 +607,15 @@ public:
    135     explicit CScriptID(const uint160& in) : BaseHash(in) {}
    136 };
    137 
    138+/**
    139+ * Count the number of signature operations (sigops) in a P2SH scriptSig.
    140+ *
    141+ * If the scriptSig contains a SegWit redeem script (i.e., a P2SH-P2WPKH or
    142+ * P2SH-P2WSH script), this function counts only the non-SegWit sigops.
    143+ * To count SegWit sigops in such cases, use CountWitnessSigOps.
    144+ */
    145+unsigned int CountP2SHSigOps(const CScript& scriptSig);unsigned int CountP2SHSigOps(const CScript& scriptSig);
    146+
    147 /** Test for OP_SUCCESSx opcodes as defined by BIP342. */
    148 bool IsOpSuccess(const opcodetype& opcode);
    149 
    

    l0rinc commented at 2:22 am on August 5, 2025:

    Thanks, love the inlining of GetSigOpCount (bothered me, too - it’s a lot clearer this way), and the new CountSigOps name as well. Added your comments (+ you as co-author, of course) and adjusted all the tests, now that CountSigOps is separated from CountP2SHSigOps: https://github.com/bitcoin/bitcoin/compare/6bf4c8d48f61089254532b065d9ca8e1c2bf6868..f8208e92bd103359f8c3ceb3361eb7904099e994

    Edit: note, that instead of

    0-        sigops += prev_txo.scriptPubKey.GetSigOpCount(txin.scriptSig);
    1+        sigops += prev_txo.scriptPubKey.CountSigOps(/*fAccurate=*/true);
    2+        if (prev_txo.scriptPubKey.IsPayToScriptHash()) {
    3+           sigops += CountP2SHSigOps(txin.scriptSig);
    4+        }
    

    I did:

    0-        sigops += prev_txo.scriptPubKey.GetSigOpCount(txin.scriptSig);
    1+        sigops += prev_txo.scriptPubKey.IsPayToScriptHash() ?
    2+                      CountP2SHSigOps(txin.scriptSig) :
    3+                      prev_txo.scriptPubKey.CountSigOps(/*fAccurate=*/true);
    

    since I don’t think CountSigOps should always be applied (even if always 0)

  17. l0rinc force-pushed on Aug 5, 2025
  18. l0rinc force-pushed on Aug 5, 2025
  19. DrahtBot added the label CI failed on Aug 5, 2025
  20. DrahtBot commented at 2:39 am on August 5, 2025: contributor

    🚧 At least one of the CI tasks failed. Task MSan, depends: https://github.com/bitcoin/bitcoin/runs/47383324322 LLM reason (✨ experimental): The CI failure is caused by a compilation error in script_ops.cpp: ‘GetSigOpCount’ is missing in ‘CScript’.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  21. l0rinc force-pushed on Aug 5, 2025
  22. DrahtBot removed the label CI failed on Aug 5, 2025
  23. in src/script/script.h:553 in 4cbd303ddd outdated
    544+     * Starting with P2SH in version 0.6, `CHECKMULTISIG` operations inside wrapped scripts
    545+     * began to be counted more precisely, using the preceding OP_N opcode to determine
    546+     * the number of pubkeys and thus the number of sigops.
    547      */
    548-    unsigned int GetSigOpCount(bool fAccurate) const;
    549+    unsigned int CountSigOps(bool fAccurate) const;
    


    ryanofsky commented at 11:14 am on August 5, 2025:

    In commit “refactor: rename GetSigOpCount to CountSigOps” (4cbd303ddd7ae500683a7a595f38c5b8ff2de31e)

    It’s not obvious from commit message why this rename is being done. I think it might help to add some explanation like:

    • Previous GetSigOpCount method was overloaded to take either a bool or scriptSig as a parameter, without an explanation of when to call each overload. New CountSigOps method avoids the overloading and documents how it should be called. The name was chosen to be clearer and consistent with the newer CountWitnessSigOps function.

    I also think it would be reasonable to keep the current method name and just document the method better without renaming it, but I do like the current approach.


    l0rinc commented at 9:11 pm on August 5, 2025:
    Done, thanks, kept the rename and added you as a co-author
  24. in src/script/script.h:680 in 37c1d8fc8d outdated
    611+ *
    612+ * If the scriptSig contains a SegWit redeem script (i.e., a P2SH-P2WPKH or P2SH-P2WSH script),
    613+ * this function counts only the non-SegWit sigops.
    614+ * To count SegWit sigops in such cases, use `CountWitnessSigOps`.
    615+ */
    616+unsigned int CountP2SHSigOps(const CScript& scriptSig);
    


    ryanofsky commented at 11:42 am on August 5, 2025:

    In commit “refactor: split off P2SH from GetSigOpCount” (37c1d8fc8db2f1ddd6ddf4d8fccbd13bbc9c9935)

    Might be helpful to say what is motivating this change in the commit message. Maybe add:

    • The name CountP2SHSigOps was chosen to match CountWitnessSigOps, since the two functions are counterparts for handling P2SH and SegWit scripts.

    ryanofsky commented at 3:07 pm on August 5, 2025:

    In commit “refactor: split off P2SH from GetSigOpCount” (37c1d8fc8db2f1ddd6ddf4d8fccbd13bbc9c9935)

    Two things:

    • It might be nice to make CountP2SHSigOps function more similar to CountWitnessSigOps and accept a scriptPubKey argument. It looks like this change also makes call sites simpler.
    • I noticed with BIP54 the CScript::CountSigOps comment when to use fAccurate=false will not be right anymore. Would suggest qualifying the comment with “When enforcing the MAX_BLOCK_SIGOPS_COST limit”.

    Both changes are included in the diff below. Feel free to use, or keep the current code, or do something different if you prefer

     0--- a/src/consensus/tx_verify.cpp
     1+++ b/src/consensus/tx_verify.cpp
     2@@ -131,9 +131,7 @@ unsigned int GetP2SHSigOpCount(const CTransaction& tx, const CCoinsViewCache& in
     3     {
     4         const Coin& coin = inputs.AccessCoin(tx.vin[i].prevout);
     5         assert(!coin.IsSpent());
     6-        if (coin.out.scriptPubKey.IsPayToScriptHash()) {
     7-            nSigOps += CountP2SHSigOps(tx.vin[i].scriptSig);
     8-        }
     9+        nSigOps += CountP2SHSigOps(tx.vin[i].scriptSig, coin.out.scriptPubKey);
    10     }
    11     return nSigOps;
    12 }
    13--- a/src/policy/policy.cpp
    14+++ b/src/policy/policy.cpp
    15@@ -180,10 +180,9 @@ static bool CheckSigopsBIP54(const CTransaction& tx, const CCoinsViewCache& inpu
    16         // This means sigops in the spent scriptPubKey count toward the limit.
    17         // `fAccurate` means correctly accounting sigops for CHECKMULTISIGs(VERIFY) with 16 pubkeys
    18         // or fewer. This method of accounting was introduced by BIP16, and BIP54 reuses it.
    19+        sigops += prev_txo.scriptPubKey.CountSigOps(/*fAccurate=*/true);
    20         sigops += txin.scriptSig.CountSigOps(/*fAccurate=*/true);
    21-        sigops += prev_txo.scriptPubKey.IsPayToScriptHash() ?
    22-                      CountP2SHSigOps(txin.scriptSig) :
    23-                      prev_txo.scriptPubKey.CountSigOps(/*fAccurate=*/true);
    24+        sigops += CountP2SHSigOps(txin.scriptSig, prev_txo.scriptPubKey);
    25 
    26         if (sigops > MAX_TX_LEGACY_SIGOPS) {
    27             return false;
    28--- a/src/script/script.cpp
    29+++ b/src/script/script.cpp
    30@@ -180,8 +180,12 @@ unsigned int CScript::CountSigOps(bool fAccurate) const
    31     return n;
    32 }
    33 
    34-unsigned int CountP2SHSigOps(const CScript& scriptSig)
    35+unsigned int CountP2SHSigOps(const CScript& scriptSig, const CScript& scriptPubKey)
    36 {
    37+    if (!scriptPubKey.IsPayToScriptHash()) {
    38+        return 0;
    39+    }
    40+
    41     // This is a pay-to-script-hash scriptPubKey;
    42     // get the last item that the scriptSig
    43     // pushes onto the stack:
    44--- a/src/script/script.h
    45+++ b/src/script/script.h
    46@@ -531,8 +531,9 @@ public:
    47      * Count the number of signature operations (sigops) in this script.
    48      *
    49      * The `fAccurate` parameter controls how sigops are counted for `CHECKMULTISIG` operations.
    50-     * Set `fAccurate` to `true` when analyzing P2SH redeem scripts or SegWit witness scripts,
    51-     * and to `false` when analyzing scriptPubKeys or scriptSigs.
    52+     * When enforcing the `MAX_BLOCK_SIGOPS_COST` limit, set `fAccurate` to `true` when
    53+     * analyzing P2SH redeem scripts or SegWit witness scripts, and to `false` when analyzing
    54+     * scriptPubKeys or scriptSigs.
    55      *
    56      * Historically, Bitcoin always counted each `CHECKMULTISIG` as
    57      * 20 sigops (`MAX_PUBKEYS_PER_MULTISIG`), regardless of the number of pubkeys.
    58@@ -613,7 +614,7 @@ public:
    59  * this function counts only the non-SegWit sigops.
    60  * To count SegWit sigops in such cases, use `CountWitnessSigOps`.
    61  */
    62-unsigned int CountP2SHSigOps(const CScript& scriptSig);
    63+unsigned int CountP2SHSigOps(const CScript& scriptSig, const CScript& scriptPubKey);
    64 
    65 /** Test for OP_SUCCESSx opcodes as defined by BIP342. */
    66 bool IsOpSuccess(const opcodetype& opcode);
    67--- a/src/test/sigopcount_tests.cpp
    68+++ b/src/test/sigopcount_tests.cpp
    69@@ -44,7 +44,7 @@ BOOST_AUTO_TEST_CASE(CountSigOps)
    70     CScript p2sh = GetScriptForDestination(ScriptHash(s1));
    71     CScript scriptSig;
    72     scriptSig << OP_0 << Serialize(s1);
    73-    BOOST_CHECK_EQUAL(CountP2SHSigOps(scriptSig), 3U);
    74+    BOOST_CHECK_EQUAL(CountP2SHSigOps(scriptSig, p2sh), 3U);
    75 
    76     std::vector<CPubKey> keys;
    77     for (int i = 0; i < 3; i++)
    78@@ -61,7 +61,7 @@ BOOST_AUTO_TEST_CASE(CountSigOps)
    79     BOOST_CHECK_EQUAL(p2sh.CountSigOps(/*fAccurate=*/false), 0U);
    80     CScript scriptSig2;
    81     scriptSig2 << OP_1 << ToByteVector(dummy) << ToByteVector(dummy) << Serialize(s2);
    82-    BOOST_CHECK_EQUAL(CountP2SHSigOps(scriptSig2), 3U);
    83+    BOOST_CHECK_EQUAL(CountP2SHSigOps(scriptSig2, p2sh), 3U);
    84 }
    85 
    86 /**
    

    EDIT: Simplified GetP2SHSigOpCount after earlier post


    l0rinc commented at 9:13 pm on August 5, 2025:
    Added your explanation to the commit message - even though the changes themselves made that obvious, since we’ve added new comments to the methods

    l0rinc commented at 1:17 am on August 6, 2025:

    I played with this, but really dislike the new version, mostly for how awkward CheckSigopsBIP54 has become after it. But I have kept your comment update and change the ternary, let me know what you think of this instead:

    0if (prev_txo.scriptPubKey.IsPayToScriptHash()) {
    1    Assert(prev_txo.scriptPubKey.CountSigOps(/*fAccurate=*/true) == 0);
    2    sigops += CountP2SHSigOps(txin.scriptSig);
    3} else {
    4    sigops += prev_txo.scriptPubKey.CountSigOps(/*fAccurate=*/true);
    5}
    

    If we get to my original proposal which optimizes CountSigOps for the templates, I don’t mind always executing it, even when IsPayToScriptHash() == true. But it seems more logical to me to have two sigop updates here, like before.


    ryanofsky commented at 4:45 pm on August 7, 2025:

    re: #32729 (review)

    Thanks. I think I just dislike reviewing changes before I know what purpose / motivation of the change is. I know you can often guess motivations after the fact, but it is usually helpful imo to see them stated explicitly.


    ryanofsky commented at 4:53 pm on August 7, 2025:

    re: #32729 (review)

    I played with this, but really dislike the new version, mostly for how awkward CheckSigopsBIP54 has become after it.

    I’m confused because the diff makes this code shorter than before, removes a conditional, and makes it match the BIP54 spec more closely (see other comment about BIP54 above). Also I liked the ternary version of this better than the current version. But whatever you prefer is fine with me and maybe some wires were just crossed here.


    l0rinc commented at 10:23 pm on August 7, 2025:
    Not after the fact, but that the commited code contained the added comments already explaining - not important, it’s done :)

    l0rinc commented at 10:29 pm on August 7, 2025:
    Done
  25. in src/script/script.h:562 in 0d51088d1d outdated
    554@@ -555,7 +555,13 @@ class CScript : public CScriptBase
    555      */
    556     static bool IsPayToAnchor(int version, const std::vector<unsigned char>& program);
    557 
    558-    bool IsPayToScriptHash() const;
    559+    bool IsPayToScriptHash() const noexcept
    560+    {
    561+        return size() == 23 &&
    562+               front() == OP_HASH160 &&
    563+               (*this)[1] == WITNESS_V0_KEYHASH_SIZE &&
    


    ryanofsky commented at 12:21 pm on August 5, 2025:

    In commit “refactor: pull IsPayToScriptHash to header” (0d51088d1daa15ade3bf3c57410a0c12df235615)

    It doesn’t seem like WITNESS_V0_KEYHASH_SIZE is really the right constant to use for a p2sh script. Maybe should introduce a SCRIPT_HASH_SIZE or similar constant instead? Could also make sense to use the same constant for the memcpy in IsToScriptID


    l0rinc commented at 9:50 pm on August 5, 2025:

    Subtle, thanks, extracted it to:

    0static constexpr size_t HASH160_OUTPUT_SIZE{20};
    1static constexpr size_t WITNESS_V0_KEYHASH_SIZE{HASH160_OUTPUT_SIZE};
    

    and used these where it explains the magic number better

  26. in src/test/script_tests.cpp:1229 in 38e89f6462 outdated
    1221@@ -1222,7 +1222,7 @@ BOOST_AUTO_TEST_CASE(script_size_and_capacity_test)
    1222     // P2TR has direct allocation
    1223     {
    1224         const auto script{GetScriptForDestination(WitnessV1Taproot{XOnlyPubKey{dummy_pubkey}})};
    1225-        BOOST_CHECK_EQUAL(GetTxoutType(script), TxoutType::WITNESS_V1_TAPROOT);
    1226+        BOOST_CHECK(script.IsPayToTaproot());
    


    ryanofsky commented at 12:27 pm on August 5, 2025:

    In commit “refactor: pull IsPayToTaproot to header” (38e89f646221ec0c15158b6ef0007336fec008e3)

    Can commit message explain reason for other changes in this commit, like this test check and the IsWitnessProgram reordering above (assuming they are intentional)


    l0rinc commented at 10:13 pm on August 5, 2025:
    There was no reordering, it was just inserted after the other helper, IsPayToWitnessScriptHash (looks like GitHub got confused). The BOOST_CHECK_EQUAL to BOOST_CHECK change is just to test the new helper method in a simpler way, now that we don’t have to use Solver. You still think it needs additional explanations?

    ryanofsky commented at 6:33 pm on August 7, 2025:

    re: #32729 (review)

    You still think it needs additional explanations?

    It seems ok, but maybe consider adding your explanation to the commit message: “The BOOST_CHECK_EQUAL to BOOST_CHECK change is just to test the new helper method in a simpler way, now that we don’t have to use Solver.”

    I wouldn’t have asked the question if I had known what the intention behind the solver change was, so this wasn’t obvious to me.

    Also I do see IsWitnessProgram followed by IsPayToTaproot in the header before b71e7a07becb5d74db4bc1fdd60899b5ec5157f4 and IsPayToTaproot followed by IsWitnessProgram after that commit, so I am still wondering if reordering was intended here. Seems fine either way though.


    l0rinc commented at 9:55 pm on August 7, 2025:

    Also I do see IsWitnessProgram followed by IsPayToTaproot in the header before https://github.com/bitcoin/bitcoin/commit/b71e7a07becb5d74db4bc1fdd60899b5ec5157f4 and IsPayToTaproot followed by IsWitnessProgram after that commit

    Yes, the quick template helpers are grouped above the IsWitnessProgram, the move is deliberate.

    test the new helper method in a simpler way, now that we don’t have to use Solver

    I don’t actually mind keeping both the solver and the new helpers - it’s even better to make sure they’re synchronized. Added both to each case.

  27. ryanofsky commented at 12:34 pm on August 5, 2025: contributor
    Code review f8208e92bd103359f8c3ceb3361eb7904099e994. Thanks for the updates, and I started reviewing the next chunk of this. Overall the changes seem helpful and well-considered and the extra test and benchmark coverage seem like they should be useful.
  28. in src/script/script.h:595 in 3c800a72b6 outdated
    561@@ -562,7 +562,14 @@ class CScript : public CScriptBase
    562                (*this)[1] == WITNESS_V0_KEYHASH_SIZE &&
    563                back() == OP_EQUAL;
    564     }
    565-    bool IsPayToWitnessScriptHash() const;
    566+
    567+    bool IsPayToWitnessScriptHash() const noexcept
    568+    {
    569+        return size() == 34 &&
    


    ryanofsky commented at 3:20 pm on August 5, 2025:

    In commit “refactor: pull IsPayToWitnessScriptHash to header” (3c800a72b6520d94bc3996fa31ecded0f6fab22d)

    Changing 34 to WITNESS_V0_SCRIPTHASH_SIZE + 2 might make this a little more self-documenting.

    (I don’t mean to bikeshed on use of literals vs constants and front/back in these functions though. I could imagine other developers preferring the literals for concreteness even if I like the constants, and I don’t have a strong preference.)


    l0rinc commented at 10:20 pm on August 5, 2025:
    In most other cases I used constants, but for the sizes I prefer redundancy and clarity, to make absolutely sure we’re not accidentally changing anything here
  29. in src/script/script.h:602 in 38e89f6462 outdated
    574@@ -575,9 +575,14 @@ class CScript : public CScriptBase
    575                (*this)[1] == WITNESS_V0_SCRIPTHASH_SIZE;
    576     }
    577 
    578-    bool IsWitnessProgram(int& version, std::vector<unsigned char>& program) const;
    579+    bool IsPayToTaproot() const noexcept
    580+    {
    581+        return size() == 34 &&
    


    ryanofsky commented at 3:26 pm on August 5, 2025:

    In commit “refactor: pull IsPayToTaproot to header” (38e89f646221ec0c15158b6ef0007336fec008e3)

    Similar to previous, could replace 34 with WITNESS_V1_TAPROOT_SIZE + 2 but no strong preference


    l0rinc commented at 10:20 pm on August 5, 2025:
    I prefer the constant here for redundancy
  30. in src/script/script.h:568 in 5d23e9a4c6 outdated
    559@@ -560,6 +560,16 @@ class CScript : public CScriptBase
    560                back() == 0x73;
    561     }
    562 
    563+    bool IsPayToPubKeyHash() const noexcept
    564+    {
    565+        return size() == 25 &&
    566+               front() == OP_DUP &&
    567+               (*this)[1] == OP_HASH160 &&
    568+               (*this)[2] == WITNESS_V0_KEYHASH_SIZE &&
    


    ryanofsky commented at 3:32 pm on August 5, 2025:

    In commit “refactor: add IsPayToPubKeyHash helper to script.h” (5d23e9a4c6fa4aab530889c54565954486572afd)

    Again it seems a little off to use a segwit constant in pre-segwit output. Might be better to stick with literal 20 or maybe introduce a HASH160_SIZE or PUBKEY_HASH_SIZE constant. No strong preference though and technically this should be ok.


    l0rinc commented at 10:22 pm on August 5, 2025:
    Yes, good point, already did that as part of the other similar comment you had.
  31. in src/script/script.h:610 in 017f58821f outdated
    592@@ -592,6 +593,13 @@ class CScript : public CScriptBase
    593                (*this)[1] == WITNESS_V1_TAPROOT_SIZE;
    594     }
    595 
    596+    bool IsCompressedPayToPubKey() const noexcept
    597+    {
    598+        return size() == 35 &&
    


    ryanofsky commented at 3:38 pm on August 5, 2025:

    In commit “refactor: add IsCompressedPayToPubKey helper to script.h” (017f58821fe857a2bb7ac14f2b00b349936391d8)

    I guess it is intentional to replace CPubKey::COMPRESSED_SIZE + 2 with 35 here?


    l0rinc commented at 10:25 pm on August 5, 2025:
    Yes, same reasoning as before - normally I’m all for deduplication, so that the reason is clear + if we change it, we don’t have to touch multiple places. But here I want a bit of extra redundancy to make sure that we do have to change in multiple places if we really want to change this value.

    hodlinator commented at 8:59 pm on August 7, 2025:

    Can you come up with any scenario where we would change a value of a named constant in this context of script types? In absence of that, the redundancy argument seems weak and I would rather make these numbers less magical as Russ seems to prefer as well. Better to have expressions that describe the contents of the script IMO. Slightly lower risk of off-by-one errors as well I would think. Using literal values >9 there’s also more room for confusion/errors due to hexadecimal/decimal mixups.

    For similar reasons, when accessing the last element I prefer either:

    0    (*this)[1 + CPubKey::COMPRESSED_SIZE] == OP_CHECKSIG;
    

    or (what you had in an earlier push):

    0    back() == OP_CHECKSIG;
    

    rather than: https://github.com/bitcoin/bitcoin/blob/748b10bbe864191ef49c32e0963e048b939f6088/src/script/script.h#L612

    (Even though I’ve been warming up to back(), I still prefer the aesthetics of (*this)[0] over front() in this context).


    l0rinc commented at 5:36 am on August 11, 2025:
    Not yet sure, we have a lot of places where we have similar indexes, I find them hard to follow and verify. I need more convincing to do it here - or maybe we can attempt it in a follow-up.

    hodlinator commented at 12:05 pm on August 12, 2025:
    I think my argument above is strong enough but this is not a blocker for me.

    l0rinc commented at 4:02 pm on August 12, 2025:
    There’s a fine line here, as @ryanofsky also mentioned, we can endlessly bikeshed here. I’m for deduplication, but in this case leaving the hard-coded indexes seems safer to me. I don’t mind doing this and other related hard-coded values in a follow-up.

    ryanofsky commented at 11:48 am on August 13, 2025:

    re: #32729 (review)

    Am fine with current approach just wanted to link #32729#pullrequestreview-3097866742 in this thread which shows the way I’d suggest writing this.

    The surface difference between two styles is that mine avoids magic numbers while current style doesn’t. I think the more fundamental difference is l0rinc thinks it’s good if this code would be broken with different constant sizes “to make sure that we do have to change in multiple places if we really want to change this value” and I don’t think that’s good. Imo, if it’s important for a constant to not change, unit tests, static asserts, and comments providing rationales are better mechanisms for ensuring this than segfaults.

    Practically speaking though I don’t think style of this code matters, and anything seems fine here.

  32. in src/script/script.h:12 in 017f58821f outdated
     8@@ -9,6 +9,7 @@
     9 #include <attributes.h>
    10 #include <crypto/common.h>
    11 #include <prevector.h> // IWYU pragma: export
    12+#include <pubkey.h>
    


    ryanofsky commented at 3:59 pm on August 5, 2025:

    In commit “refactor: add IsCompressedPayToPubKey helper to script.h” (017f58821fe857a2bb7ac14f2b00b349936391d8)

    Seems reasonable to include pubkey.h but since we are only using constants, it might be better to move constants somewhere.


    l0rinc commented at 10:28 pm on August 5, 2025:
    We kinda’ depend on pubkey here - if you have a more concrete suggestion, please let me know, all other ones I thought of were uglier than this.
  33. in src/script/script.h:608 in 017f58821f outdated
    592@@ -592,6 +593,13 @@ class CScript : public CScriptBase
    593                (*this)[1] == WITNESS_V1_TAPROOT_SIZE;
    594     }
    595 
    596+    bool IsCompressedPayToPubKey() const noexcept
    


    ryanofsky commented at 4:02 pm on August 5, 2025:

    In commit “refactor: add IsCompressedPayToPubKey helper to script.h” (017f58821fe857a2bb7ac14f2b00b349936391d8)

    Might be good to document that this method does not check the pubkey prefix (even/odd/uncompressed) and verify this is even or odd.


    l0rinc commented at 1:28 am on August 6, 2025:
    Not sure, would we not consider it a P2PK without a valid prefix? Otherwise that’s just an extra validation step that should be done elsewhere. We’re not always checking the prefixes (see: MatchPayToPubkey), and we don’t have extra info for other such quick checks - let me know if you have strong preferences here.

    ryanofsky commented at 6:58 pm on August 7, 2025:

    re: #32729 (review)

    To clarify I agree code is doing the right thing and the CScript class shouldn’t be looking inside the keys to check the format. I just think short comments could be helpful to prevent misunderstandings. Maybe:

    • //! Detect P2PK script with a compressed public key. Doesn't check the 0x02/0x03 key prefix.
    • //! Detect P2PK script with a uncompressed public key. Doesn't check the 0x04 key prefix.

    Feel free to leave as-is though, just a suggestion


    l0rinc commented at 10:29 pm on August 7, 2025:
    Thanks, done

    hodlinator commented at 8:49 am on August 8, 2025:
    nit: Agreed, comments are the absolute minimum, could even go so far as name them IsCompressedPayToPubKeyNoPrefix/IsUncompressedPayToPubKeyNoPrefix.

    l0rinc commented at 5:39 am on August 11, 2025:
    Already done (I don’t like the prefix, the script also checks CPubKey::ValidSize and the compressor also checks pubkey.IsFullyValid() - so it’s not just the prefix validation that’s “missing”)
  34. in src/test/sigopcount_tests.cpp:106 in c194618bb5 outdated
    101+
    102+// Asserts the expected legacy/accurate sig-op totals for all common known script templates
    103+BOOST_AUTO_TEST_CASE(CountSigOpsKnownTemplates)
    104+{
    105+    CKey dummyKey;
    106+    dummyKey.MakeNewKey(true);
    


    ryanofsky commented at 5:36 pm on August 5, 2025:

    In commit “test: add CountSigOps edge-cases & known-template coverage” (c194618bb587a2e15ed11f268898599c74f1aa74)

    Maybe add document boolean parameter with /*fCompressed=*/ here and below


    l0rinc commented at 10:59 pm on August 5, 2025:
    Even better, I used CKey dummyKey{GenerateRandomKey(/*compressed=*/true)}
  35. in src/test/sigopcount_tests.cpp:34 in c194618bb5 outdated
     98+        }
     99+    }
    100+}
    101+
    102+// Asserts the expected legacy/accurate sig-op totals for all common known script templates
    103+BOOST_AUTO_TEST_CASE(CountSigOpsKnownTemplates)
    


    ryanofsky commented at 5:41 pm on August 5, 2025:

    In commit “test: add CountSigOps edge-cases & known-template coverage” (c194618bb587a2e15ed11f268898599c74f1aa74)

    Maybe reorder the CountSigOpsKnownTemplates before CountSigOpsErrors to make it easier to understand how the fake scripts differ from real scripts.


    l0rinc commented at 11:01 pm on August 5, 2025:
    Done, changed the order in the commit message as well.
  36. in src/script/script.h:224 in f8208e92bd outdated
    217@@ -218,8 +218,8 @@ enum opcodetype
    218     OP_INVALIDOPCODE = 0xff,
    219 };
    220 
    221-// Maximum value that an opcode can be
    222-static const unsigned int MAX_OPCODE = OP_NOP10;
    223+// Highest opcode allowed in pre-Taproot scripts
    


    ryanofsky commented at 5:50 pm on August 5, 2025:

    In commit “test: make sure OP_CHECKSIGADD isn’t considered valid (legacy) sigop” (f8208e92bd103359f8c3ceb3361eb7904099e994)

    Would suggest dropping “test:” prefix from this commit since it is changing non-test code. Could also split this up into a renaming and a test commit.

    Also I’m not sure it is good to use LEGACY to mean pre-taproot here. In the OutputType enum, legacy seems to means “pre-segwit” and in GetLegacySigOpCount legacy seems to mean “pre-p2sh”, so adding a third definition of “legacy” here would be nice to avoid. Maybe call it something like MAX_PRE_TAPSCRIPT_OPCODE or MAX_BASE_OPCODE?


    l0rinc commented at 11:09 pm on August 5, 2025:

    Thanks for the new name suggestion, I didn’t like the legacy here, was hoping you will have a better suggestion!

    Also extracted the test with explanations.


    hodlinator commented at 8:18 pm on August 6, 2025:

    Related: @itornaza’s rename attempt in #30953. To me “legacy script” vs “tapscript” seems quite an established nomenclature, but “base” is acceptable and minimally more specific than his “script”.

    Your PR description still states: “Legacy opcode ceiling - final commit documents, enforces, and tests that OP_CHECKSIGADD > MAX_OPCODE.” - I believe it is no longer the final commit.


    ryanofsky commented at 6:15 pm on August 7, 2025:

    In commit “refactor: make sure OP_CHECKSIGADD isn’t considered valid (legacy) sigop” (a2cce66f11e635924c34b27b253214be8815b0bc)

    Thanks, maybe also replace “(legacy)” in commit description with “pre-tapscript” or “base”


    l0rinc commented at 9:48 pm on August 7, 2025:
    Reworded the commit message, thanks
  37. in src/test/sigopcount_tests.cpp:46 in c194618bb5 outdated
    111+            // P2A
    112+            const std::vector<unsigned char> anchor_data{0x4e, 0x73};
    113+            const auto script{CScript() << OP_1 << anchor_data};
    114+
    115+            BOOST_CHECK(script.IsPayToAnchor());
    116+            BOOST_CHECK(script.IsPayToAnchor(1, anchor_data));
    


    ryanofsky commented at 5:56 pm on August 5, 2025:

    In commit “test: add CountSigOps edge-cases & known-template coverage” (c194618bb587a2e15ed11f268898599c74f1aa74)

    Forgot to check sigops here?


    l0rinc commented at 11:12 pm on August 5, 2025:
    Overzealous merge conflict resolution - thanks, added it back.
  38. in src/test/sigopcount_tests.cpp:192 in c194618bb5 outdated
    187+BOOST_AUTO_TEST_CASE(GetOpName_no_missing_mnemonics)
    188+{
    189+    for (auto op{OP_0}; op < OP_INVALIDOPCODE; op = opcodetype(op + 1)) {
    190+        const auto op_name{GetOpName(op)};
    191+        BOOST_CHECK(!op_name.empty());
    192+        if (op >= OP_PUSHDATA1 && !IsOpSuccess(op)) {
    


    ryanofsky commented at 6:10 pm on August 5, 2025:

    In commit “test: add CountSigOps edge-cases & known-template coverage” (c194618bb587a2e15ed11f268898599c74f1aa74)

    Maybe it would make sense to check that some (most?) of these opcodes actually do satisfy op_name == "OP_UNKNOWN", if that is the case.


    l0rinc commented at 0:06 am on August 6, 2025:
    Yeah, I just implemented all of them instead - thanks for the push.
  39. in src/test/sigopcount_tests.cpp:38 in c194618bb5 outdated
    29@@ -28,6 +30,172 @@ Serialize(const CScript& s)
    30 
    31 BOOST_FIXTURE_TEST_SUITE(sigopcount_tests, BasicTestingSetup)
    32 
    33+// Feeds malformed PUSHDATA sequences to confirm the parser never crashes and still counts sig-ops that appear before the error.
    34+BOOST_AUTO_TEST_CASE(CountSigOpsErrors)
    35+{
    36+    for (const bool fAccurate : {false, true}) {
    37+        {
    38+            const auto script{CScript() << OP_CHECKSIG << opcodetype(0x4b)}; // push-75 with 0 bytes present
    


    ryanofsky commented at 6:14 pm on August 5, 2025:

    In commit “test: add CountSigOps edge-cases & known-template coverage” (c194618bb587a2e15ed11f268898599c74f1aa74)

    Maybe use CScript{} instead of CScript() since already using braced initialization for the variable


    l0rinc commented at 0:09 am on August 6, 2025:
    Done
  40. in src/test/sigopcount_tests.cpp:39 in c194618bb5 outdated
    34+BOOST_AUTO_TEST_CASE(CountSigOpsErrors)
    35+{
    36+    for (const bool fAccurate : {false, true}) {
    37+        {
    38+            const auto script{CScript() << OP_CHECKSIG << opcodetype(0x4b)}; // push-75 with 0 bytes present
    39+            BOOST_CHECK_EQUAL(script.CountSigOps(fAccurate), 1);
    


    ryanofsky commented at 6:17 pm on August 5, 2025:

    In commit “test: add CountSigOps edge-cases & known-template coverage” (c194618bb587a2e15ed11f268898599c74f1aa74)

    The checks in this test are very repetitive. You might be able to make it easier understand and maintain by building a std::vector<CScript> of invalid scripts and looping over it.


    l0rinc commented at 0:11 am on August 6, 2025:
    Yeah, I thought of deduplicating them, but in that case they’re not so explicit. I expect people to be interested in the behavior of a given template and see its behavior directly. If you insist, let me know and I’ll think of something.
  41. in src/core_read.cpp:30 in f8208e92bd outdated
    26@@ -27,7 +27,7 @@ class OpCodeParser
    27 public:
    28     OpCodeParser()
    29     {
    30-        for (unsigned int op = 0; op <= MAX_OPCODE; ++op) {
    31+        for (unsigned int op = 0; op <= MAX_LEGACY_OPCODE; ++op) {
    


    ryanofsky commented at 6:34 pm on August 5, 2025:

    In commit “test: make sure OP_CHECKSIGADD isn’t considered valid (legacy) sigop” (f8208e92bd103359f8c3ceb3361eb7904099e994)

    I’m confused about whether code that changes in this commit is actually doing the right thing currently, of if some of it should be improved to support taproot opcodes. Would be helpful if commit message could clarify.


    l0rinc commented at 9:11 pm on August 5, 2025:

    I also found this confusing, it’s why I have renamed the max in the first place and why I added tests for GetOpName. But given that we already had a test making sure that:

    0BOOST_CHECK_EXCEPTION(ParseScript("OP_CHECKSIGADD"), std::runtime_error, HasReason("script parse error: unknown opcode"));
    

    it seems to be the expected behavior. Added a short explanation to the commit message.


    However, the fuzz usage in ConsumeOpcodeType didn’t make sense to me, so I’ve extended that to test any kind of opcode - done in a separate commit at the end.

    0rm -rfd build_fuzz && \
    1cmake --preset=libfuzzer \
    2 -DCMAKE_C_COMPILER="$(brew --prefix llvm)/bin/clang" \
    3 -DCMAKE_CXX_COMPILER="$(brew --prefix llvm)/bin/clang++" \
    4 -DCMAKE_OSX_SYSROOT="$(xcrun --show-sdk-path)" \
    5 -DCMAKE_C_FLAGS="-target arm64-apple-macos11" \
    6 -DCMAKE_CXX_FLAGS="-target arm64-apple-macos11" \
    7 -DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" && \
    8cmake --build build_fuzz -j$(nproc) && \
    9FUZZ=script_ops build_fuzz/bin/fuzz
    
  42. ryanofsky approved
  43. ryanofsky commented at 6:41 pm on August 5, 2025: contributor
    Code review ACK f8208e92bd103359f8c3ceb3361eb7904099e994. I finished reviewing this and didn’t see any problems, just left various suggestions. The changes overall seem nice for making this code less confusing and adding better test & benchmark coverage.
  44. l0rinc force-pushed on Aug 6, 2025
  45. l0rinc commented at 2:15 am on August 6, 2025: contributor
    Addressed or responded to all concerns, thanks for spending the time to review this. I’ve extended the fuzzer to use the whole range of possible opcodes, I’m curious to see if it reveals any new failures. The change consists of tiny, simple changes, other reviewers are welcome.
  46. l0rinc requested review from ryanofsky on Aug 6, 2025
  47. l0rinc force-pushed on Aug 6, 2025
  48. DrahtBot added the label CI failed on Aug 6, 2025
  49. DrahtBot commented at 2:31 am on August 6, 2025: contributor

    🚧 At least one of the CI tasks failed. Task lint: https://github.com/bitcoin/bitcoin/runs/47470624304 LLM reason (✨ experimental): Lint check failed due to locale-dependent code usage in src/test/sigopcount_tests.cpp.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  50. DrahtBot removed the label CI failed on Aug 6, 2025
  51. in src/bench/checkblock.cpp:46 in 0abbad2de4 outdated
    38@@ -39,26 +39,18 @@ static void DeserializeBlockTest(benchmark::Bench& bench)
    39     });
    40 }
    41 
    42-static void DeserializeAndCheckBlockTest(benchmark::Bench& bench)
    43+static void CheckBlockBench(benchmark::Bench& bench)
    44 {
    45-    DataStream stream(benchmark::data::block413567);
    46-    std::byte a{0};
    47-    stream.write({&a, 1}); // Prevent compaction
    


    hodlinator commented at 8:30 pm on August 6, 2025:
    1. nit: Could remove that part from the benchmark-code above too?
    2. reflection: Seems like DataStream::Compact() is no longer called by anything.

    l0rinc commented at 5:57 pm on August 10, 2025:
    Good observation, but we still need this because of https://github.com/bitcoin/bitcoin/blob/master/src/streams.h#L213 - after read reaches the end, it wipes the Stream so we should add some garbage to the end to prevent that. But we could still do some cleanup in this area, in a separate PR.
  52. in src/bench/checkblock.cpp:60 in 37f72340f9 outdated
    55+    CBlock block;
    56+    DataStream(benchmark::data::block413567) >> TX_WITH_WITNESS(block);
    57+
    58+    constexpr auto expected_sigops{2841};
    59+    bench.batch(expected_sigops).unit("sigops").run([&] {
    60+        auto nSigOps{0};
    


    hodlinator commented at 8:43 pm on August 6, 2025:
    nSigOps => sig_op_count?

    l0rinc commented at 6:06 pm on August 10, 2025:
    It’s deliberate, we’re exercising https://github.com/bitcoin/bitcoin/blob/master/src/validation.cpp#L4080-L4084. Renaming it only here would only make searching more difficult. I’m all for modernizing, but this didn’t seem the right place to do it.

    hodlinator commented at 11:36 am on August 12, 2025:
    Did you mean these lines? (Better to not link master as it’s a moving target). https://github.com/bitcoin/bitcoin/blob/273e600e65c2e31a6e9a0bd72b40672aaa503b08/src/validation.cpp#L4075-L4079 Certainly nit-level.

    l0rinc commented at 3:43 pm on August 12, 2025:
    Yeah
  53. in src/script/script.h:549 in 006a428613 outdated
    539+     * Set `fAccurate` to `true` when analyzing P2SH redeem scripts or SegWit witness scripts,
    540+     * and to `false` when analyzing scriptPubKeys or scriptSigs.
    541+     *
    542+     * Historically, Bitcoin always counted each `CHECKMULTISIG` as
    543+     * 20 sigops (`MAX_PUBKEYS_PER_MULTISIG`), regardless of the number of pubkeys.
    544+     * Starting with P2SH in version 0.6, `CHECKMULTISIG` operations inside wrapped scripts
    


    hodlinator commented at 8:52 pm on August 6, 2025:
    Why is the wrapped-property worth calling out in a P2SH context? Do you mean in contrast to bare multisig? I’m not super clear on this area, so feel a bit uncomfortable reviewing this comment (easier to verify code changes).

    ryanofsky commented at 3:19 pm on August 8, 2025:

    re: #32729 (review)

    Why is the wrapped-property worth calling out in a P2SH context? Do you mean in contrast to bare multisig?

    Good point. By wrapped scripts I just meant redeemScripts and witnessScripts, but that term was confusing. I think a better version of this is probably:

     0/**
     1 * Count the number of signature operations (sigops) in this script.
     2 *
     3 * The `fAccurate` parameter controls how `CHECKMULTISIG` operations are counted.
     4 * When enforcing the `MAX_BLOCK_SIGOPS_COST` limit, set `fAccurate` to `true` when
     5 * counting a redeemScript (P2SH) or witnessScript (P2WSH), and set it to `false`
     6 * when counting a scriptPubKey or scriptSig.
     7 *
     8 * Historical note: before P2SH (v0.6), `CHECKMULTISIG` was always counted as
     9 * MAX_PUBKEYS_PER_MULTISIG (=20), regardless of the actual number of pubkeys.
    10 * Starting with P2SH — and similarly for P2WSH later — `CHECKMULTISIG` inside
    11 * the redeemScript/witnessScript began to be counted precisely, using the
    12 * preceding OP_N to determine the number of pubkeys.
    13 */
    14unsigned int CountSigOps(bool fAccurate) const;
    

    which cleans up the comment and mentions witness scripts explicitly in the “historical” section.

    If anything still is confusing here would be good to know because I spent hours being confused by the previous code while reviewing this PR and was hoping a little more documentation could avoid the need for others to go through that.


    l0rinc commented at 5:04 am on August 11, 2025:

    Updated the doc, thanks!

    I spent hours being confused by the previous code while reviewing this PR

    Does this PR make the situation clearer?

  54. in src/script/script.h:535 in 006a428613 outdated
    535-     *  ... OP_N CHECKMULTISIG ...
    536+     * Count the number of signature operations (sigops) in this script.
    537+     *
    538+     * The `fAccurate` parameter controls how sigops are counted for `CHECKMULTISIG` operations.
    539+     * Set `fAccurate` to `true` when analyzing P2SH redeem scripts or SegWit witness scripts,
    540+     * and to `false` when analyzing scriptPubKeys or scriptSigs.
    


    hodlinator commented at 8:56 pm on August 6, 2025:
    Follow-up idea: Instead of having these kinds of booleans, it might be cool to explore subclassing CScript into something like WitnessScript/LockScript/UnlockScript/TapScript someday. That could also prevent mixing up the parameter order for CountP2SHSigOps(UnlockScript, LockScript).

    l0rinc commented at 5:06 am on August 11, 2025:
    Yes, I also thought of that.
  55. in src/script/script.h:578 in 4df5614c46 outdated
    558@@ -556,7 +559,13 @@ class CScript : public CScriptBase
    559      */
    560     static bool IsPayToAnchor(int version, const std::vector<unsigned char>& program);
    561 
    562-    bool IsPayToScriptHash() const;
    563+    bool IsPayToScriptHash() const noexcept
    


    hodlinator commented at 9:37 pm on August 6, 2025:
    re noexcept: The base, prevector, is a header-only type, so compiler sees all and should be able to infer noexcept, so seems like noise to me? Same for IsPayToWitnessScriptHash() and others.

    l0rinc commented at 5:08 am on August 11, 2025:
    Maybe, I don’t know in which cases we can safely avoid it, hoping that all compilers will eliminate it.

    hodlinator commented at 11:27 am on August 12, 2025:
    I hoped it would be inferred by the compiler, but experiments such as static_assert(noexcept(script.IsPayToAnchor())) when removing the explicit specification showed that to not be the case. Guess we can open the floodgates of adding these identifiers to touched/new functions where appropriate.
  56. in src/compressor.cpp:7 in 2b48b56247 outdated
    3@@ -4,10 +4,11 @@
    4 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
    5 
    6 #include <compressor.h>
    7-
    


    hodlinator commented at 1:20 pm on August 7, 2025:
    meganit: This empty line is intentional and IMO preferable; increases the chance that the corresponding header doesn’t become alphabetically sorted after another header. Having the corresponding header first ensures missing #include’s inside it get caught.

    l0rinc commented at 5:11 am on August 11, 2025:
    Sure, done
  57. in src/test/compress_tests.cpp:186 in 2b48b56247 outdated
    181+    {
    182+        auto key{ToByteVector(GenerateRandomKey(/*compressed=*/true).GetPubKey())};
    183+        key[0] = 0x06; // 0x02/0x03 would be valid prefixes
    184+
    185+        const auto script{CScript() << key << OP_CHECKSIG};
    186+        BOOST_CHECK(script.IsCompressedPayToPubKey());
    


    hodlinator commented at 1:30 pm on August 7, 2025:
    Could add a comments about this check not catching invalid prefixes here and below, unless you alter the name to highlight that prefixes are not checked (see other comment in script.h).

    l0rinc commented at 5:13 am on August 11, 2025:

    Added a comment previously to IsCompressedPayToPubKey to explain that:

    0//! Detect P2PK script with a compressed public key. Doesn't check the 0x02/0x03 key prefix.
    1bool IsCompressedPayToPubKey() const noexcept
    

    hodlinator commented at 8:59 am on August 12, 2025:

    Yeah, saw that, but still thought a brief local comment would decrease the level of surprise for those reading the test. Please consider it a nit though.

    0        BOOST_CHECK(script.IsCompressedPayToPubKey()); // Prefix is not validated
    

    l0rinc commented at 3:53 pm on August 12, 2025:
    Since many other parts aren’t validated either, only the structure, I think I’ll leave it as is
  58. in src/compressor.cpp:36 in 2b48b56247 outdated
    30@@ -30,10 +31,9 @@ static bool IsToScriptID(const CScript& script, CScriptID& hash)
    31     return true;
    32 }
    33 
    34-static bool IsToPubKey(const CScript& script, CPubKey &pubkey)
    35+static bool IsToPubKey(const CScript& script, CPubKey& pubkey)
    36 {
    37-    if (script.size() == 35 && script[0] == 33 && script[34] == OP_CHECKSIG
    38-                            && (script[1] == 0x02 || script[1] == 0x03)) {
    39+    if (script.IsCompressedPayToPubKey() && (script[1] == 0x02 || script[1] == 0x03)) {
    


    hodlinator commented at 1:39 pm on August 7, 2025:
    Since we’re adding the secp256k1.h #include in commit 17922e88153b07f6146f5740dde69e8a3587220a, why not use SECP256K1_TAG_PUBKEY_EVEN/..._ODD directly instead of changing to them in 90df1ccfca9c0adfd1c61c1b07d8911ab632bc6a?

    l0rinc commented at 5:15 am on August 11, 2025:
    That’s more consistent, indeed
  59. in src/test/fuzz/script.cpp:22 in 9ee6977ce8 outdated
    20 #include <script/signingprovider.h>
    21 #include <script/solver.h>
    22 #include <streams.h>
    23-#include <test/fuzz/FuzzedDataProvider.h>
    24 #include <test/fuzz/fuzz.h>
    25+#include <test/fuzz/FuzzedDataProvider.h>
    


    hodlinator commented at 1:49 pm on August 7, 2025:
    So your editor’s sorting function orders script_error.h before script.h, and fuzz.h before FuzzedDataProvider.h? My editor sorts the headers as they are before this change. 🤔

    l0rinc commented at 5:18 am on August 11, 2025:
    Reverted
  60. in src/script/script.h:534 in 006a428613 outdated
    534-     * counted more accurately, assuming they are of the form
    535-     *  ... OP_N CHECKMULTISIG ...
    536+     * Count the number of signature operations (sigops) in this script.
    537+     *
    538+     * The `fAccurate` parameter controls how sigops are counted for `CHECKMULTISIG` operations.
    539+     * Set `fAccurate` to `true` when analyzing P2SH redeem scripts or SegWit witness scripts,
    


    ryanofsky commented at 4:14 pm on August 7, 2025:

    In commit “refactor: rename GetSigOpCount to CountSigOps” (006a428613710dde24866cf4f6efd7b68b6ee294)

    This sentence should be prefixed with “When enforcing the MAX_BLOCK_SIGOPS_COST limit” to be correct since it doesn’t apply in other cases. (Problem is fixed in the next commit but the change belongs this commit. Sorry my previous feedback was a bit jumbled and I mixed the commits together.)


    l0rinc commented at 9:43 pm on August 7, 2025:
    Brought the change one commit earlier to avoid modifying the same thing twice.
  61. in src/test/sigopcount_tests.cpp:217 in 9ee6977ce8 outdated
    212+    // Unused
    213+    for (auto op{opcodetype(OP_CHECKSIGADD + 1)}; op < OP_INVALIDOPCODE; op = opcodetype(op + 1)) {
    214+        BOOST_CHECK_EQUAL(GetOpName(op), "OP_UNKNOWN");
    215+    }
    216+    // Invalid
    217+    BOOST_CHECK_EQUAL(GetOpName(OP_INVALIDOPCODE), "OP_INVALIDOPCODE");
    


    hodlinator commented at 4:18 pm on August 7, 2025:
    1. Not sure this belongs in sigopcount_tests.cpp, script_tests.cpp seems more appropriate?
    2. Would feel more robust if written like this to eliminate any doubt about not covering the whole enum:
     0    static_assert(OP_FALSE == OP_0);
     1    for (auto op{opcodetype(0)}; op <= OP_INVALIDOPCODE; op = static_cast<opcodetype>(op + 1)) {
     2        switch (op) {
     3        // Special
     4        case OP_FALSE: BOOST_CHECK_EQUAL(GetOpName(op), "0"); break;
     5        case OP_TRUE: BOOST_CHECK_EQUAL(GetOpName(op), "1"); break;
     6        // Push data
     7        case OP_PUSHDATA1: BOOST_CHECK_EQUAL(GetOpName(op), "OP_PUSHDATA1"); break;
     8        case OP_PUSHDATA2: BOOST_CHECK_EQUAL(GetOpName(op), "OP_PUSHDATA2"); break;
     9        case OP_PUSHDATA4: BOOST_CHECK_EQUAL(GetOpName(op), "OP_PUSHDATA4"); break;
    10
    11        case OP_1NEGATE: BOOST_CHECK_EQUAL(GetOpName(op), "-1"); break;
    12        case OP_RESERVED: BOOST_CHECK_EQUAL(GetOpName(op), "OP_RESERVED"); break;
    13        case OP_INVALIDOPCODE: BOOST_CHECK_EQUAL(GetOpName(op), "OP_INVALIDOPCODE"); break;
    14
    15        default:
    16            // Direct push
    17            if (op >= OP_0 + 1 && op < OP_PUSHDATA1) {
    18                BOOST_CHECK_EQUAL(GetOpName(op), "OP_UNKNOWN");
    19            // Numbers
    20            } else if (op >= OP_RESERVED + 1 && op < OP_NOP) {
    21                BOOST_CHECK_EQUAL(GetOpName(op), util::ToString(op - OP_RESERVED));
    22            // Named operations
    23            } else if (op >= OP_NOP && op <= OP_CHECKSIGADD) {
    24                const auto op_name{GetOpName(op)};
    25                BOOST_CHECK_NE(op_name, "OP_UNKNOWN");
    26                BOOST_CHECK(op_name.starts_with("OP_"));
    27            // Unused
    28            } else if (op >= OP_CHECKSIGADD + 1 && op < OP_INVALIDOPCODE) {
    29                BOOST_CHECK_EQUAL(GetOpName(op), "OP_UNKNOWN");
    30            } else {
    31                BOOST_FAIL("Incomplete coverage");
    32            }
    33            break;
    34        }
    35    }
    

    ryanofsky commented at 3:43 pm on August 8, 2025:

    re: #32729 (review)

    Seems like a good idea. Maybe default section can be simplified too (untested)

     0        default:
     1            // Numbers
     2            if (op >= OP_RESERVED + 1 && op < OP_NOP) {
     3                BOOST_CHECK_EQUAL(GetOpName(op), util::ToString(op - OP_RESERVED));
     4            // Named operations
     5            } else if (op >= OP_NOP && op <= OP_CHECKSIGADD) {
     6                const auto op_name{GetOpName(op)};
     7                BOOST_CHECK_NE(op_name, "OP_UNKNOWN");
     8                BOOST_CHECK(op_name.starts_with("OP_"));
     9            } else {
    10                BOOST_CHECK_EQUAL(GetOpName(op), "OP_UNKNOWN");
    11            }
    12            break;
    

    l0rinc commented at 5:29 am on August 11, 2025:
    Excellent, did something very similar, thanks for the hints. Verified that all ops are indeed checked this way and added both of you as coauthors.

    hodlinator commented at 7:58 am on August 11, 2025:

    Thanks! Could avoid some more redundant GetOpName()-calls and use more information rich variable naming. (nit: Still have a minor preference for specifying which op-codes are expected to result in “OP_UNKNOWN”):

     0diff --git a/src/test/script_tests.cpp b/src/test/script_tests.cpp
     1index 60a730dfc2..a851bf6c9f 100644
     2--- a/src/test/script_tests.cpp
     3+++ b/src/test/script_tests.cpp
     4@@ -1620,29 +1620,31 @@ BOOST_AUTO_TEST_CASE(script_HasValidBaseOps)
     5 BOOST_AUTO_TEST_CASE(GetOpName_no_missing_mnemonics)
     6 {
     7     for (auto op{OP_0}; op <= OP_INVALIDOPCODE; op = opcodetype(op + 1)) {
     8-        switch (auto arg0{GetOpName(op)}; op) {
     9+        switch (auto name{GetOpName(op)}; op) {
    10         // Special
    11-        case OP_FALSE: BOOST_CHECK_EQUAL(arg0, "0"); break;
    12-        case OP_TRUE: BOOST_CHECK_EQUAL(arg0, "1"); break;
    13+        case OP_FALSE: BOOST_CHECK_EQUAL(name, "0"); break;
    14+        case OP_TRUE: BOOST_CHECK_EQUAL(name, "1"); break;
    15         // Push data
    16-        case OP_PUSHDATA1: BOOST_CHECK_EQUAL(arg0, "OP_PUSHDATA1"); break;
    17-        case OP_PUSHDATA2: BOOST_CHECK_EQUAL(arg0, "OP_PUSHDATA2"); break;
    18-        case OP_PUSHDATA4: BOOST_CHECK_EQUAL(arg0, "OP_PUSHDATA4"); break;
    19+        case OP_PUSHDATA1: BOOST_CHECK_EQUAL(name, "OP_PUSHDATA1"); break;
    20+        case OP_PUSHDATA2: BOOST_CHECK_EQUAL(name, "OP_PUSHDATA2"); break;
    21+        case OP_PUSHDATA4: BOOST_CHECK_EQUAL(name, "OP_PUSHDATA4"); break;
    22         // Other
    23-        case OP_1NEGATE: BOOST_CHECK_EQUAL(arg0, "-1"); break;
    24-        case OP_RESERVED: BOOST_CHECK_EQUAL(arg0, "OP_RESERVED"); break;
    25-        case OP_INVALIDOPCODE: BOOST_CHECK_EQUAL(arg0, "OP_INVALIDOPCODE"); break;
    26+        case OP_1NEGATE: BOOST_CHECK_EQUAL(name, "-1"); break;
    27+        case OP_RESERVED: BOOST_CHECK_EQUAL(name, "OP_RESERVED"); break;
    28+        case OP_INVALIDOPCODE: BOOST_CHECK_EQUAL(name, "OP_INVALIDOPCODE"); break;
    29         default:
    30             if (op >= OP_RESERVED + 1 && op < OP_NOP) {
    31                 // Numbers
    32-                BOOST_CHECK_EQUAL(GetOpName(op), util::ToString(op - OP_RESERVED));
    33+                BOOST_CHECK_EQUAL(name, util::ToString(op - OP_RESERVED));
    34             } else if (op >= OP_NOP && op <= OP_CHECKSIGADD) {
    35                 // Named operations
    36-                const auto op_name{GetOpName(op)};
    37-                BOOST_CHECK_NE(op_name, "OP_UNKNOWN");
    38-                BOOST_CHECK(op_name.starts_with("OP_"));
    39+                BOOST_CHECK_NE(name, "OP_UNKNOWN");
    40+                BOOST_CHECK(name.starts_with("OP_"));
    41+            } else if ((op >= OP_0 + 1 && op < OP_PUSHDATA1) || (op >= OP_CHECKSIGADD + 1 && op < OP_INVALIDOPCODE)) {
    42+                // Direct push or unused
    43+                BOOST_CHECK_EQUAL(name, "OP_UNKNOWN");
    44             } else {
    45-                BOOST_CHECK_EQUAL(GetOpName(op), "OP_UNKNOWN");
    46+                BOOST_FAIL("Incomplete coverage");
    47             }
    48             break;
    49         }
    

    hodlinator commented at 10:09 pm on August 12, 2025:
  62. in src/consensus/tx_verify.cpp:136 in 6bf5dce801 outdated
    130@@ -131,9 +131,9 @@ unsigned int GetP2SHSigOpCount(const CTransaction& tx, const CCoinsViewCache& in
    131     {
    132         const Coin& coin = inputs.AccessCoin(tx.vin[i].prevout);
    133         assert(!coin.IsSpent());
    134-        const CTxOut &prevout = coin.out;
    135-        if (prevout.scriptPubKey.IsPayToScriptHash())
    136-            nSigOps += prevout.scriptPubKey.GetSigOpCount(tx.vin[i].scriptSig);
    137+        if (coin.out.scriptPubKey.IsPayToScriptHash()) {
    138+            nSigOps += CountP2SHSigOps(tx.vin[i].scriptSig);
    139+        }
    


    ryanofsky commented at 4:35 pm on August 7, 2025:

    In commit “refactor: split off P2SH from GetSigOpCount” (6bf5dce801e0da9d372d42d2c0650c283ec787f5)

    I still think you should consider simplifying these lines to:

    0nSigOps += CountP2SHSigOps(tx.vin[i].scriptSig, coin.out.scriptPubKey);
    

    and simplifying CheckSigopsBIP54 to:

    0sigops += txin.scriptSig.CountSigOps(/*fAccurate=*/true);
    1sigops += prev_txo.scriptPubKey.CountSigOps(/*fAccurate=*/true);
    2sigops += CountP2SHSigOps(txin.scriptSig, prev_txo.scriptPubKey);
    

    Getting rid of conditionals in these counting functions make them safer and more readable. In the case of BIP54 it also makes the code literally match the specification which says “For each input in the transaction, count the number of CHECKSIG and CHECKMULTISIG in the input scriptSig and previous output’s scriptPubKey, including the P2SH redeemScript.”

    Additionally this would make the CountP2SHSigOps and CountWitnessSigOps functions take the same arguments and have the same semantics instead of having an unnecessary difference between them.


    l0rinc commented at 9:43 pm on August 7, 2025:
    Done

    l0rinc commented at 10:23 pm on August 7, 2025:
    If you say you find it more readable, I don’t mind - changed
  63. ryanofsky approved
  64. ryanofsky commented at 7:26 pm on August 7, 2025: contributor

    Code review ACK dd1a7ab428a7c7f87b2a02580aca0d593c2de68a. Thanks for the updates! I left a few more suggestions, but nothing important.

    I think it might be good to link directly to your optimization commit d76d7531dfcc7ef2d104b8977a2239cb1fc89119 in the PR description since it helps explain the changes here and bring them together.

    On style changes in the CScript methods I think they are an improvement overall, though personally I’d try to avoid front/back methods and magic numbers and write them more like:

    0        return size() == (5 + HASH160_SIZE) &&
    1               (*this)[0] == OP_DUP &&
    2               (*this)[1] == OP_HASH160 &&
    3               (*this)[2] == HASH160_SIZE &&
    4               (*this)[3 + HASH160_SIZE] == OP_EQUALVERIFY &&
    5               (*this)[4 + HASH160_SIZE] == OP_CHECKSIG;
    

    But no need to bikeshed and whatever you or other reviewers prefer seems fine. Existing style also is fine.

  65. in src/rpc/mempool.cpp:85 in a2cce66f11 outdated
    81@@ -82,7 +82,7 @@ static RPCHelpMan sendrawtransaction()
    82             }
    83 
    84             for (const auto& out : mtx.vout) {
    85-                if((out.scriptPubKey.IsUnspendable() || !out.scriptPubKey.HasValidOps()) && out.nValue > max_burn_amount) {
    86+                if((out.scriptPubKey.IsUnspendable() || !out.scriptPubKey.HasValidBaseOps()) && out.nValue > max_burn_amount) {
    


    hodlinator commented at 8:05 pm on August 7, 2025:
    nit: if (

    l0rinc commented at 5:30 am on August 11, 2025:
    Done
  66. in src/script/script.cpp:274 in a2cce66f11 outdated
    267     CScript::const_iterator it = begin();
    268     while (it < end()) {
    269         opcodetype opcode;
    270         std::vector<unsigned char> item;
    271-        if (!GetOp(it, opcode, item) || opcode > MAX_OPCODE || item.size() > MAX_SCRIPT_ELEMENT_SIZE) {
    272+        if (!GetOp(it, opcode, item) || opcode > MAX_BASE_OPCODE || item.size() > MAX_SCRIPT_ELEMENT_SIZE) {
    


    hodlinator commented at 8:10 pm on August 7, 2025:
    Was considering whether HasValidOps() instead should take a parameter like bool tapscript. But only found it being used for scriptPubKeys and scriptSigs, whereas tapscripts only exist in witness data AFAIK. Maybe worth calling out in the commit message of 94da3342dd1cfd21cc99c67c55ee4aa977e8b520?

    l0rinc commented at 5:33 am on August 11, 2025:
    Did something like that, let me know what you think.

    hodlinator commented at 8:32 am on August 12, 2025:

    Like the new version, makes it clearer!

    (nit: I would drop the “Also fixes minor spacing issues in conditional statements."-paragraph if you retouch).

  67. l0rinc force-pushed on Aug 7, 2025
  68. l0rinc commented at 10:33 pm on August 7, 2025: contributor

    Pushed a new set of smaller, mostly test fixes - thanks for detailed review again!

    though personally I’d try to avoid front/back methods

    Done, they’re more uniform this way. I left the hard-coded indexes for redundancy.

  69. DrahtBot added the label CI failed on Aug 7, 2025
  70. DrahtBot commented at 11:00 pm on August 7, 2025: contributor

    🚧 At least one of the CI tasks failed. Task no wallet, libbitcoinkernel: https://github.com/bitcoin/bitcoin/runs/47636996984 LLM reason (✨ experimental): The CI failure is caused by a compilation error due to calling CountP2SHSigOps with one argument instead of the required two.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  71. l0rinc force-pushed on Aug 7, 2025
  72. DrahtBot removed the label CI failed on Aug 8, 2025
  73. in src/test/sigopcount_tests.cpp:51 in 748b10bbe8 outdated
    47+            BOOST_CHECK(script.IsPayToAnchor(1, anchor_data));
    48+            BOOST_CHECK_EQUAL(script.CountSigOps(fAccurate), 0);
    49+        }
    50+        {
    51+            // OP_RETURN with sigop after (non-standard, found first in block 229,712)
    52+            const auto p2pkh_hash{"cd2b3298b7f455f39805377e5f213093df3cc09a"_hex_v_u8};
    


    hodlinator commented at 8:53 am on August 8, 2025:

    This seems fine since your #30765 / cac846c2fbf6fc69bfc288fd387aa3f68d84d584?

    0            const auto p2pkh_hash{"cd2b3298b7f455f39805377e5f213093df3cc09a"_hex};
    

    l0rinc commented at 5:40 am on August 11, 2025:
    Done, thanks
  74. in src/test/sigopcount_tests.cpp:42 in 748b10bbe8 outdated
    38+    const auto pubkey{dummyKey.GetPubKey()};
    39+
    40+    for (const bool fAccurate : {false, true}) {
    41+        {
    42+            // P2A
    43+            const std::vector<unsigned char> anchor_data{0x4e, 0x73};
    


    hodlinator commented at 9:40 am on August 8, 2025:

    reflection: Tempting to encourage something like:

    0            const auto anchor_data{std::to_array({std::byte{0x4e}, std::byte{0x73}})};
    

    But the second IsPayToAnchor() is not ready for it and I understand if you don’t want to modernize that within this PR.


    l0rinc commented at 5:42 am on August 11, 2025:
    Yeah, I don’t really like it, it has more moving parts
  75. in src/test/sigopcount_tests.cpp:180 in 748b10bbe8 outdated
    176+            BOOST_CHECK_EQUAL(script.CountSigOps(fAccurate), 1);
    177+        }
    178+        {
    179+            const auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA4
    180+                              << opcodetype(100) << opcodetype(0xff) << opcodetype(0xff) << opcodetype(0xff)
    181+                              << OP_CHECKSIG << OP_CHECKSIGVERIFY << OP_CHECKMULTISIG << OP_CHECKMULTISIGVERIFY}; // out of bounds OP_PUSHDATA4 data size ignoring OP_CHECKMULTISIGVERIFY
    


    hodlinator commented at 9:47 am on August 8, 2025:

    Starting a comment at column 115? More readable for multi-line statements here and above:

    0            // out of bounds OP_PUSHDATA4 data size ignoring OP_CHECKMULTISIGVERIFY
    1            const auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA4
    2                              << opcodetype(100) << opcodetype(0xff) << opcodetype(0xff) << opcodetype(0xff)
    3                              << OP_CHECKSIG << OP_CHECKSIGVERIFY << OP_CHECKMULTISIG << OP_CHECKMULTISIGVERIFY};
    

    l0rinc commented at 5:46 am on August 11, 2025:
    Not sure, but I don’t mind - done.
  76. in src/test/sigopcount_tests.cpp:142 in 748b10bbe8 outdated
    134+            const auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA1}; // not enough data after OP_PUSHDATA1
    135+            BOOST_CHECK_EQUAL(script.CountSigOps(fAccurate), 1);
    136+        }
    137+        {
    138+            const auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA1
    139+                              << opcodetype(0xff)
    


    hodlinator commented at 10:10 am on August 8, 2025:

    Why not use:

    0                              << OP_INVALIDOPCODE
    

    here and below?


    l0rinc commented at 5:47 am on August 11, 2025:

    Because it would look weird in cases such as:

    0<< opcodetype(100) << opcodetype(0xff) << opcodetype(0xff) << opcodetype(0xff)
    

    hodlinator commented at 8:28 am on August 12, 2025:
    Consider it a nit, but curious why it would look weird?

    l0rinc commented at 3:55 pm on August 12, 2025:

    just seems more consistent with the rest (I also considered it at first, changed it back, seems eaiser to parse).

    Also, these aren’t actual opcodes (edit: I’m explicitly creating invalid pushes here). I though of adding these as CScript{} << "ffff"_hex, but operator<<(std::span<const std::byte> b) starts with AppendDataSize, we don’t really have a way to just add raw bytes (except for .insert() which will likely make the code uglier).


    hodlinator commented at 7:17 am on August 13, 2025:

    I was glossing over the fact that you want to use these to represent numeric lengths, sorry. The old-style C++ opcodetype() casts derailed me.

    Have a slight preference for:

      0diff --git a/src/test/sigopcount_tests.cpp b/src/test/sigopcount_tests.cpp
      1index eeb9eec3f9..dbe1dd7ca2 100644
      2--- a/src/test/sigopcount_tests.cpp
      3+++ b/src/test/sigopcount_tests.cpp
      4@@ -125,71 +125,77 @@ BOOST_AUTO_TEST_CASE(CountSigOpsErrors)
      5         }
      6         {
      7             // push-2 with only 1 byte present ignoring OP_CHECKSIGVERIFY
      8-            const auto script{CScript{} << OP_CHECKSIG
      9-                              << opcodetype(0x02)
     10-                              << OP_CHECKSIGVERIFY};
     11+            auto script{CScript{} << OP_CHECKSIG};
     12+            script.push_back(0x02);
     13+            script << OP_CHECKSIGVERIFY;
     14             BOOST_CHECK_EQUAL(script.CountSigOps(fAccurate), 1);
     15         }
     16 
     17         {
     18-            // not enough data after OP_PUSHDATA1
     19+            // missing data after OP_PUSHDATA1
     20             const auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA1};
     21             BOOST_CHECK_EQUAL(script.CountSigOps(fAccurate), 1);
     22         }
     23         {
     24             // out of bounds OP_PUSHDATA1 data size ignoring OP_CHECKSIGVERIFY
     25-            const auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA1
     26-                              << opcodetype(0xff)
     27-                              << OP_CHECKSIGVERIFY};
     28+            auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA1};
     29+            script.push_back(0xff);
     30+            script << OP_CHECKSIGVERIFY;
     31             BOOST_CHECK_EQUAL(script.CountSigOps(fAccurate), 1);
     32         }
     33 
     34         {
     35-            // not enough data after OP_PUSHDATA2
     36+            // missing data after OP_PUSHDATA2
     37             const auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA2};
     38             BOOST_CHECK_EQUAL(script.CountSigOps(fAccurate), 1);
     39         }
     40         {
     41             // not enough data after OP_PUSHDATA2
     42-            const auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA2 << opcodetype(0xff)};
     43+            auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA2};
     44+            script.push_back(0xff);
     45             BOOST_CHECK_EQUAL(script.CountSigOps(fAccurate), 1);
     46         }
     47         {
     48             // out of bounds OP_PUSHDATA2 data size ignoring OP_CHECKSIGVERIFY
     49-            const auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA2
     50-                              << opcodetype(0xff) << opcodetype(0xff)
     51-                              << OP_CHECKSIG << OP_CHECKSIGVERIFY << OP_CHECKMULTISIG << OP_CHECKMULTISIGVERIFY};
     52+            auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA2};
     53+            script.push_back(0xff);
     54+            script.push_back(0xff);
     55+            script << OP_CHECKSIG << OP_CHECKSIGVERIFY << OP_CHECKMULTISIG << OP_CHECKMULTISIGVERIFY;
     56             BOOST_CHECK_EQUAL(script.CountSigOps(fAccurate), 1);
     57         }
     58 
     59         {
     60-            // not enough data after OP_PUSHDATA4
     61+            // missing data after OP_PUSHDATA4
     62             const auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA4};
     63             BOOST_CHECK_EQUAL(script.CountSigOps(fAccurate), 1);
     64         }
     65         {
     66             // not enough data after OP_PUSHDATA4
     67-            const auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA4
     68-                              << opcodetype(0xff)};
     69+            auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA4};
     70+            script.push_back(0xff);
     71             BOOST_CHECK_EQUAL(script.CountSigOps(fAccurate), 1);
     72         }
     73         {
     74             // not enough data after OP_PUSHDATA4
     75-            const auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA4
     76-                              << opcodetype(0xff) << opcodetype(0xff)};
     77+            auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA4};
     78+            script.push_back(0xff);
     79+            script.push_back(0xff);
     80             BOOST_CHECK_EQUAL(script.CountSigOps(fAccurate), 1);
     81         }
     82         {
     83             // not enough data after OP_PUSHDATA4
     84-            const auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA4
     85-                              << opcodetype(0xff) << opcodetype(0xff) << opcodetype(0xff)};
     86+            auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA4};
     87+            script.push_back(0xff);
     88+            script.push_back(0xff);
     89+            script.push_back(0xff);
     90             BOOST_CHECK_EQUAL(script.CountSigOps(fAccurate), 1);
     91         }
     92         {
     93             // out of bounds OP_PUSHDATA4 data size ignoring OP_CHECKMULTISIGVERIFY
     94-            const auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA4
     95-                              << opcodetype(100) << opcodetype(0xff) << opcodetype(0xff) << opcodetype(0xff)
     96-                              << OP_CHECKSIG << OP_CHECKSIGVERIFY << OP_CHECKMULTISIG << OP_CHECKMULTISIGVERIFY};
     97+            auto script{CScript{} << OP_CHECKSIG << OP_PUSHDATA4};
     98+            constexpr uint8_t length[]{100, 0xff, 0xff, 0xff};
     99+            script.insert(script.end(), length, length + sizeof(length));
    100+            script << OP_CHECKSIG << OP_CHECKSIGVERIFY << OP_CHECKMULTISIG << OP_CHECKMULTISIGVERIFY;
    101             BOOST_CHECK_EQUAL(script.CountSigOps(fAccurate), 1);
    102         }
    103     }
    
    • Avoids opcodetype() for non-opcodes.
    • Makes the CScript mutable, but the scopes are narrow.

    l0rinc commented at 5:17 pm on August 13, 2025:
    Thanks, I was explicitly trying to avoid these, but maybe we can polish this in a follow-up.
  77. in src/script/script.h:545 in 748b10bbe8 outdated
    551+     * Count the number of signature operations (sigops) in this script.
    552+     *
    553+     * The `fAccurate` parameter controls how sigops are counted for `CHECKMULTISIG` operations.
    554+     * When enforcing the `MAX_BLOCK_SIGOPS_COST` limit, set `fAccurate` to `true` when
    555+     * analyzing P2SH redeem scripts or SegWit witness scripts, and to `false` when analyzing
    556+     * scriptPubKeys or scriptSigs.
    


    hodlinator commented at 11:39 am on August 8, 2025:

    nit: Could include newer/clearer terms?

    0     * locking scripts (scriptPubKeys) or unlocking scripts (scriptSigs).
    

    or

    0     * scriptPubKeys (locking scripts) or scriptSigs (unlocking scripts).
    

    Same for CountP2SHSigOps().


    ryanofsky commented at 3:01 pm on August 8, 2025:

    re: #32729 (review)

    nit: Could include newer/clearer terms?

    Suggestion seems ok, but just to push back a little I like terms scriptPubKey and scriptSig because they are concrete, easy to google and grep, and I’d think probably familiar to most people reading this. But maybe I’m wrong if newer documentation uses different terms, and it seems fine to add them.


    l0rinc commented at 5:49 am on August 11, 2025:
    Sure, added it to Russ’s latest doc.
  78. in src/test/script_tests.cpp:1615 in 748b10bbe8 outdated
    1621-    BOOST_CHECK(!script.HasValidOps());
    1622+    BOOST_CHECK( ToScript("76a9141234567890abcdefa1a2a3a4a5a6a7a8a9a0aaab88ac"_hex).HasValidBaseOps()); // Normal script
    1623+    BOOST_CHECK( ToScript("76a914ff34567890abcdefa1a2a3a4a5a6a7a8a9a0aaab88ac"_hex).HasValidBaseOps());
    1624+    BOOST_CHECK(!ToScript("ff88ac"_hex).HasValidBaseOps()); // Script with OP_INVALIDOPCODE explicit
    1625+    BOOST_CHECK(!ToScript("88acc0"_hex).HasValidBaseOps()); // Script with undefined opcode
    1626+    BOOST_CHECK(!ToScript("ba"_hex).HasValidBaseOps());     // OP_CHECKSIGADD > MAX_BASE_OPCODE
    


    hodlinator commented at 12:24 pm on August 8, 2025:

    Could say it in code:

    0
    1    static_assert(OP_CHECKSIGADD > MAX_BASE_OPCODE);
    2    BOOST_CHECK(!CScript{CScript{} << OP_CHECKSIGADD}.HasValidBaseOps());
    

    l0rinc commented at 5:51 am on August 11, 2025:
    Added something similar, thanks
  79. in src/policy/policy.cpp:185 in 748b10bbe8 outdated
    179@@ -180,9 +180,9 @@ static bool CheckSigopsBIP54(const CTransaction& tx, const CCoinsViewCache& inpu
    180         // This means sigops in the spent scriptPubKey count toward the limit.
    181         // `fAccurate` means correctly accounting sigops for CHECKMULTISIGs(VERIFY) with 16 pubkeys
    182         // or fewer. This method of accounting was introduced by BIP16, and BIP54 reuses it.
    183-        // The GetSigOpCount call on the previous scriptPubKey counts both bare and P2SH sigops.
    184-        sigops += txin.scriptSig.GetSigOpCount(/*fAccurate=*/true);
    185-        sigops += prev_txo.scriptPubKey.GetSigOpCount(txin.scriptSig);
    186+        sigops += prev_txo.scriptPubKey.CountSigOps(/*fAccurate=*/true);
    187+        sigops += txin.scriptSig.CountSigOps(/*fAccurate=*/true);
    188+        sigops += CountP2SHSigOps(txin.scriptSig, prev_txo.scriptPubKey);
    


    hodlinator commented at 12:42 pm on August 8, 2025:

    This function is covered in transaction_tests.cpp/max_standard_legacy_sigops, but the change is still a bit scary.

    Old behavior:

    1. txin.scriptSig.GetSigOpCount(/*fAccurate=*/true) would count txin.scriptSig sigops.
    2. rev_txo.scriptPubKey.GetSigOpCount(txin.scriptSig) would count sigops from either the P2SH lock script or the previous txo’s lock script.

    New behavior:

    1. prev_txo.scriptPubKey.CountSigOps(/*fAccurate=*/true) counts sigops from the previous txo’s lock script.
    2. txin.scriptSig.CountSigOps(/*fAccurate=*/true) counts txin.scriptSig sigops (same as before).
    3. CountP2SHSigOps(txin.scriptSig, prev_txo.scriptPubKey) will count txin.scriptSig sigops, only if prev_txo.scriptPubKey is P2SH.

    I guess prev_txo.scriptPubKey.CountSigOps(/*fAccurate=*/true) will not find any sigops if prev_txo.scriptPubKey is P2SH, making the end result the same?

    Wish this were more clear through code, or commented.


    ryanofsky commented at 2:58 pm on August 8, 2025:

    re: #32729 (review)

    I guess prev_txo.scriptPubKey.CountSigOps(/*fAccurate=*/true) will not find any sigops if prev_txo.scriptPubKey is P2SH, making the end result the same?

    Yes results should be the same for that reason. A goal of this change is to make the CheckSigopsBIP54 function match the BIP54 specification which says “For each input in the transaction, count the number of CHECKSIG and CHECKMULTISIG in the input scriptSig and previous output’s scriptPubKey, including the P2SH redeemScript.”


    l0rinc commented at 5:54 am on August 11, 2025:
    Added Russ’s explanation to the commit message.
  80. hodlinator commented at 1:35 pm on August 8, 2025: contributor

    Concept ACK 748b10bbe864191ef49c32e0963e048b939f6088

    Overall incremental improvements, good to add more coverage of sigop-counting considering it is in vogue.

  81. ryanofsky approved
  82. ryanofsky commented at 3:24 pm on August 8, 2025: contributor
    Code review ACK 748b10bbe864191ef49c32e0963e048b939f6088. Just small cleanups and suggested changes since last review. Thanks for the updates!
  83. DrahtBot requested review from hodlinator on Aug 8, 2025
  84. l0rinc force-pushed on Aug 11, 2025
  85. l0rinc commented at 6:03 am on August 11, 2025: contributor
    Thanks for the reviews, addressed all concerns - either by applying it or explaining my take. Let me know if I left out something.
  86. in src/script/script.h:1 in dff42e3af7 outdated


    hodlinator commented at 12:38 pm on August 12, 2025:
    nit: The added IsPayToWitnessPubKeyHash() makes tests feel more complete, but is pretty much dead code.

    l0rinc commented at 3:50 pm on August 12, 2025:

    hodlinator commented at 9:46 pm on August 12, 2025:

    Ah, now I recall it from the PR description.

    nit: Would be nice to add that motivation in the commit message when adding IsPayToWitnessPubKeyHash().


    l0rinc commented at 10:21 pm on August 12, 2025:
    Sure, done
  87. in src/script/script.cpp:224 in 6b167d5835 outdated
    221@@ -222,15 +222,6 @@ bool CScript::IsPayToAnchor(int version, const std::vector<unsigned char>& progr
    222         program[1] == 0x73;
    223 }
    224 
    225-bool CScript::IsPayToScriptHash() const
    


    hodlinator commented at 12:56 pm on August 12, 2025:
    1. In 6b167d5835b68297b259cc99baee8eff3968155a etc, what is the gain from making these methods inline by moving them to the header?
    2. nit: If these moves are kept, would suggest using “move” rather than “pull” in the commit message.
    3. nit: Would suggest using “extract” rather than “add” (except for IsPayToWitnessPubKeyHash() which is new code).

    l0rinc commented at 3:46 pm on August 12, 2025:
    1. we don’t introduce a header + implementation duplication - what would be the advantage in this case of separating them to header + impl?

    2,3) done


    hodlinator commented at 10:01 pm on August 12, 2025:

    2,3) thanks!

    1): Arguments for avoiding the move:

    ➕: Avoids moving 4 functions to headers, minimizing the diff and making it clearer which parts of the body changed if any.

    ➕: Someone chose to have them like this, so best to avoid flip-flopping for minor reasons.

    ➕: Keeps the script.h header small and faster to re-parse for each compilation unit as it is a commonly included header, direct includes:

    0₿ git grep "<script/script\.h>" |wc -l
    198
    

    (➖): Not inlining adds some linking time, but those functions are rarely called.

    ➕: Increases readability for getting an overview of CScript for programmers using editors that don’t auto-collapse function bodies.


    l0rinc commented at 10:21 pm on August 12, 2025:
    I prefer having them in the same place, @ryanofsky, where do you think these helpers belong, in header or cpp?

    ryanofsky commented at 12:04 pm on August 13, 2025:

    re: #32729 (review)

    I prefer having them in the same place, @ryanofsky, where do you think these helpers belong, in header or cpp?

    No real opinion, I just assumed these needed to be moved for the optimization in d76d7531dfcc7ef2d104b8977a2239cb1fc89119.

    Abstractly, I think it makes sense for these to be in a header file since they are low-level and natural candidates to inline. Ideally I don’t think they’d be in this header file for reasons hodlinator mentioned, and I would probably want them to be standalone functions not class methods since I think the purpose of classes is to encapsulate internal state and these aren’t accessing anything private.

    But I think anything is fine here, and it makes sense to go with your preference if there aren’t objections.

  88. hodlinator commented at 1:15 pm on August 12, 2025: contributor

    Reviewed cf8f476ef25e4fe43e67970ee2ec6a265d49e763

    Main thing remaining is question about motivation behind moving methods (see inline comment).

  89. fanquake commented at 1:19 pm on August 12, 2025: member

    https://github.com/bitcoin/bitcoin/actions/runs/16872143934/job/47788785406?pr=32729#step:6:4338:

     0 test/script_tests.cpp(1620): Entering test case "GetOpName_no_missing_mnemonics"
     1/home/runner/work/_temp/src/test/script_tests.cpp:1622:25: runtime error: load of value 256, which is not a valid value for type 'opcodetype'
     2    [#0](/bitcoin-bitcoin/0/) 0x559b4df3e847 in script_tests::GetOpName_no_missing_mnemonics::test_method() /home/runner/work/_temp/build-asan/src/test/./test/script_tests.cpp:1622:25
     3    [#1](/bitcoin-bitcoin/1/) 0x559b4df3ba1b in script_tests::GetOpName_no_missing_mnemonics_invoker() /home/runner/work/_temp/build-asan/src/test/./test/script_tests.cpp:1620:1
     4    [#2](/bitcoin-bitcoin/2/) 0x559b4d0f360d in boost::function0<void>::operator()() const /usr/include/boost/function/function_template.hpp:771:14
     5    [#3](/bitcoin-bitcoin/3/) 0x559b4d174328 in boost::detail::forward::operator()() /usr/include/boost/test/impl/execution_monitor.ipp:1395:32
     6    [#4](/bitcoin-bitcoin/4/) 0x559b4d174328 in boost::detail::function::function_obj_invoker0<boost::detail::forward, int>::invoke(boost::detail::function::function_buffer&) /usr/include/boost/function/function_template.hpp:137:18
     7    [#5](/bitcoin-bitcoin/5/) 0x559b4d16de2d in boost::function0<int>::operator()() const /usr/include/boost/function/function_template.hpp:771:14
     8    [#6](/bitcoin-bitcoin/6/) 0x559b4d0598fc in int boost::detail::do_invoke<boost::shared_ptr<boost::detail::translator_holder_base>, boost::function<int ()>>(boost::shared_ptr<boost::detail::translator_holder_base> const&, boost::function<int ()> const&) /usr/include/boost/test/impl/execution_monitor.ipp:308:30
     9    [#7](/bitcoin-bitcoin/7/) 0x559b4d0598fc in boost::execution_monitor::catch_signals(boost::function<int ()> const&) /usr/include/boost/test/impl/execution_monitor.ipp:910:16
    10    [#8](/bitcoin-bitcoin/8/) 0x559b4d059e0d in boost::execution_monitor::execute(boost::function<int ()> const&) /usr/include/boost/test/impl/execution_monitor.ipp:1308:16
    11    [#9](/bitcoin-bitcoin/9/) 0x559b4d052498 in boost::execution_monitor::vexecute(boost::function<void ()> const&) /usr/include/boost/test/impl/execution_monitor.ipp:1404:5
    12    [#10](/bitcoin-bitcoin/10/) 0x559b4d052498 in boost::unit_test::unit_test_monitor_t::execute_and_translate(boost::function<void ()> const&, unsigned long) /usr/include/boost/test/impl/unit_test_monitor.ipp:49:9
    13    [#11](/bitcoin-bitcoin/11/) 0x559b4d0b5fd4 in boost::unit_test::framework::state::execute_test_tree(unsigned long, unsigned long, boost::unit_test::framework::state::random_generator_helper const*) /usr/include/boost/test/impl/framework.ipp:815:44
    14    [#12](/bitcoin-bitcoin/12/) 0x559b4d0b4e9b in boost::unit_test::framework::state::execute_test_tree(unsigned long, unsigned long, boost::unit_test::framework::state::random_generator_helper const*) /usr/include/boost/test/impl/framework.ipp:784:58
    15    [#13](/bitcoin-bitcoin/13/) 0x559b4d0b4e9b in boost::unit_test::framework::state::execute_test_tree(unsigned long, unsigned long, boost::unit_test::framework::state::random_generator_helper const*) /usr/include/boost/test/impl/framework.ipp:784:58
    16    [#14](/bitcoin-bitcoin/14/) 0x559b4d0508eb in boost::unit_test::framework::run(unsigned long, bool) /usr/include/boost/test/impl/framework.ipp:1722:29
    17    [#15](/bitcoin-bitcoin/15/) 0x559b4d07f350 in boost::unit_test::unit_test_main(boost::unit_test::test_suite* (*)(int, char**), int, char**) /usr/include/boost/test/impl/unit_test_main.ipp:250:9
    18    [#16](/bitcoin-bitcoin/16/) 0x7f2c01d841c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 282c2c16e7b6600b0b22ea0c99010d2795752b5f)
    19    [#17](/bitcoin-bitcoin/17/) 0x7f2c01d8428a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 282c2c16e7b6600b0b22ea0c99010d2795752b5f)
    20    [#18](/bitcoin-bitcoin/18/) 0x559b4cf44984 in _start (/home/runner/work/_temp/build-asan/bin/test_bitcoin+0x12c5984) (BuildId: 536afe9665aa0f1665ca9d21723b3d0c1c5aa9a6)
    21
    22SUMMARY: UndefinedBehaviorSanitizer: invalid-enum-load /home/runner/work/_temp/src/test/script_tests.cpp:1622:25 
    
  90. bitcoin deleted a comment on Aug 12, 2025
  91. l0rinc force-pushed on Aug 12, 2025
  92. bench: measure `CheckBlock` speed separately from serialization
    To measure CheckBlock performance in isolation from deserialization overhead, a ResetChecked() method was introduced to re-enable the block's validation state, allowing repeated validation of the same block instance.
    593354c457
  93. bench: measure `SigOpsBlock` speed separately
    Add benchmark to measure the performance of counting legacy signature operations in a block, independent of other validation steps.
    3173db17d1
  94. refactor: rename `GetSigOpCount` to `CountSigOps`
    Previous `GetSigOpCount` method was overloaded to take either a `bool` or `scriptSig` as a parameter, without an explanation of when to call each overload.
    New `CountSigOps` method avoids the overloading and documents how it should be called. The name was chosen to be clearer and consistent with the newer `CountWitnessSigOps` function.
    Besides the renames, also added primitive argument name reminders to the call sites to highlight the meaning of the call.
    
    Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
    74d3508cbe
  95. refactor: split off `P2SH` from `GetSigOpCount`
    The name `CountP2SHSigOps` was chosen to match `CountWitnessSigOps`, since the two functions are counterparts for handling `P2SH` and `SegWit` scripts.
    Also, it's called by `GetP2SHSigOpCount` in consensus, so the new name clarifies its significance.
    
    A goal of this change is to make the `CheckSigopsBIP54` function match the BIP54 specification which says "For each input in the transaction, count the number of CHECKSIG and CHECKMULTISIG in the input scriptSig and previous output's scriptPubKey, including the P2SH redeemScript."
    
    Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
    ef0e3856de
  96. refactor: move the script-hash-size constants to `script.h`
    This enables using these constants in script.h definitions in upcoming commits. No naming conflicts exist with these constant names.
    f480a57ec2
  97. refactor: move `IsPayToScriptHash` to header
    Moved the implementation to the header (implicitly inline + noexcept), removed redundant `this` and parentheses and changed the `0x14`/`20` magic constants to descriptive `HASH160_OUTPUT_SIZE`.
    See: https://learnmeabitcoin.com/technical/script/p2sh/#scriptpubkey
    a47d4f677b
  98. refactor: move `IsPayToWitnessScriptHash` to header
    Moved the implementation to the header (implicitly inline + noexcept), removed redundant `this` and parentheses, and changed the `0x20` magic constant to descriptive `WITNESS_V0_SCRIPTHASH_SIZE`.
    See: https://learnmeabitcoin.com/technical/script/p2wsh/#scriptpubkey
    10bff58c71
  99. refactor: move `IsPayToAnchor` to header
    Moved the implementation to the header (implicitly inline + noexcept), removed redundant `this` and parentheses.
    a7c29fed83
  100. refactor: move `IsPayToTaproot` to header
    Moved the implementation to the header (implicitly inline + noexcept), removed redundant `this` and parentheses, and changed the `0x20` magic constant to descriptive `WITNESS_V1_TAPROOT_SIZE`.
    See: https://learnmeabitcoin.com/technical/script/p2tr/#scriptpubkey
    75af3e6285
  101. refactor: extract `IsPayToPubKeyHash` helper to script.h
    The usages in `compressor.cpp` and `solver.cpp` were also updated to use the new method.
    See: https://learnmeabitcoin.com/technical/script/p2pkh/#scriptpubkey
    b9e2955855
  102. refactor: extract `IsCompressedPayToPubKey` helper to script.h
    The usages in `compressor.cpp` and `solver.cpp` were also updated to use the new method.
    Note that compressor has additional prefix checks as well, which are not properly exercised by the `compressed_p2pk` unit test.
    See: https://learnmeabitcoin.com/technical/script/p2pk/#scriptpubkey and https://learnmeabitcoin.com/technical/keys/public-key/#compressed
    4dea37b40f
  103. refactor: extract `IsUncompressedPayToPubKey` helper to script.h
    The usages in `compressor.cpp` and `solver.cpp` were also updated to use the new method.
    Note that compressor has additional prefix checks as well, which are not properly exercised by the `uncompressed_p2pk` unit test.
    See: https://learnmeabitcoin.com/technical/script/p2pk/#scriptpubkey and https://learnmeabitcoin.com/technical/keys/public-key/#uncompressed
    8e3c8afce4
  104. l0rinc force-pushed on Aug 12, 2025
  105. l0rinc commented at 4:26 pm on August 12, 2025: contributor
    Thanks, addresses remaining nits + fixed test opcode name iteration boundary failure: https://github.com/bitcoin/bitcoin/compare/cf8f476ef25e4fe43e67970ee2ec6a265d49e763..738b620f314780751c4db3af87c9f22792723125 (rebased in a separate push)
  106. refactor: add `IsPayToWitnessPubKeyHash` helper to script.h
    This template helper isn't used outside of tests yet, but for the sake of completeness - and since we're planning on using this for upcoming optimizations -, it's added as a last step.
    
    See: https://learnmeabitcoin.com/technical/script/p2wpkh/#scriptpubkey
    045daa973c
  107. test: add `CountSigOps` edge-cases & known-template coverage
    * `CountSigOpsKnownTemplates` asserts the expected legacy/accurate sig-op totals for all common known script templates (P2PKH, P2SH, P2WPKH/WSH, P2TR, compressed & uncompressed P2PK, OP_RETURN, multisig).
    * `CountSigOpsErrors` feeds malformed PUSHDATA sequences to confirm the parser never crashes and still counts sig-ops that appear before the error.
    * `GetOpName_no_missing_mnemonics` checks all opcode names by category.
    
    Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com>
    Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
    0324b9ed23
  108. refactor: distinguish pre-Taproot opcodes from all opcodes in script validation
    Renames `HasValidOps()` to `HasValidBaseOps()` and `MAX_OPCODE` to `MAX_BASE_OPCODE` to clarify that pre-Taproot script validation should only accept opcodes up to OP_NOP10 (0xb9).
    
    This prevents Tapscript-only opcodes like OP_CHECKSIGADD (0xba) from being considered valid in scriptPubKey and scriptSig contexts, where only pre-Taproot opcodes should be allowed.
    32f7c55eac
  109. test: exercise the `HasValidBaseOps` functionality
    Add explicit test case checking that OP_CHECKSIGADD > MAX_BASE_OPCODE (i.e. that we have opcodes bigger than the max).
    The other changes in the test are just inlines to reduce scope.
    
    Also note that `OpCodeParser` deliberately skips `OP_CHECKSIGADD`, see: `script_parse_tests/parse_script`.
    70706d9119
  110. fuzz: Extend `ConsumeOpcodeType` to cover all possible ops
    `OP_CHECKSIGADD` wasn't tested this way at all, but we should fuzz all possible values anyway.
    3219b59c47
  111. l0rinc force-pushed on Aug 12, 2025
  112. ryanofsky approved
  113. ryanofsky commented at 12:12 pm on August 13, 2025: contributor
    Code review ACK 3219b59c47d5a487a1129f702602f58ca9285342. A lot of test improvements since last review, only comment and whitespace changes in non-test code. I think I’m caught up on the discussion now too
  114. DrahtBot requested review from hodlinator on Aug 13, 2025
  115. hodlinator approved
  116. hodlinator commented at 7:49 pm on August 13, 2025: contributor

    ACK 3219b59c47d5a487a1129f702602f58ca9285342

    Renaming the sigop-counting functions while changing behavior of CScript::GetSigOpCount(const CScript& scriptSig)/CountP2SHSigOps(const CScript& scriptSig, const CScript& scriptPubKey) makes it relatively easy to verify the change, as no lingering GetSigOpCount overloads remain.

    Renaming HasValidOps() -> HasValidBaseOps() makes one paranoid that we haven’t been supporting Tapscript op codes everywhere, but the function is not used in those contexts.

    I disagree with moving IsPayToTaproot() etc from .CPP to .H, but it’s not blocking.

    Stay humble and stack commits! :) (Not advocating for merging commits, it is helpful to keep similar transforms applied to different areas in separate commits, other reviewers shouldn’t be scared by the high count of them).


github-metadata-mirror

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

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