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 +505 −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
    Concept ACK hodlinator
    Stale ACK ryanofsky

    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:

    • #33163 (BIP360: Includes the following: by jbride)
    • #32554 (bench: replace embedded raw block with configurable block generator by l0rinc)
    • #31989 (BIP-119 (OP_CHECKTEMPLATEVERIFY) (regtest only) by jamesob)
    • #31868 ([IBD] specialize block serialization 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. 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.
    0abbad2de4
  15. bench: measure `SigOpsBlock` speed separately
    Add benchmark to measure the performance of counting legacy signature operations in a block, independent of other validation steps.
    37f72340f9
  16. l0rinc force-pushed on Jul 28, 2025
  17. 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.
  18. 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)

  19. l0rinc force-pushed on Aug 5, 2025
  20. l0rinc force-pushed on Aug 5, 2025
  21. DrahtBot added the label CI failed on Aug 5, 2025
  22. 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.

  23. l0rinc force-pushed on Aug 5, 2025
  24. DrahtBot removed the label CI failed on Aug 5, 2025
  25. 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
  26. 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
  27. 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

  28. 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.

  29. 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.
  30. 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
  31. 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
  32. 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.
  33. 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.
  34. 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.
  35. 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:25 pm on August 7, 2025:
    Done

    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”)
  36. 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)}
  37. 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.
  38. 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
  39. 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.
  40. 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.
  41. 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
  42. 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.
  43. 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
    
  44. ryanofsky approved
  45. 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.
  46. l0rinc force-pushed on Aug 6, 2025
  47. 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.
  48. l0rinc requested review from ryanofsky on Aug 6, 2025
  49. l0rinc force-pushed on Aug 6, 2025
  50. DrahtBot added the label CI failed on Aug 6, 2025
  51. 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.

  52. DrahtBot removed the label CI failed on Aug 6, 2025
  53. 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.
  54. 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.
  55. 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?

  56. 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.
  57. 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.
  58. 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
  59. 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
    
  60. 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
  61. 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
  62. 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.
  63. 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.
  64. 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
  65. ryanofsky approved
  66. 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.

  67. 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
  68. 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.
  69. l0rinc force-pushed on Aug 7, 2025
  70. 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.

  71. DrahtBot added the label CI failed on Aug 7, 2025
  72. 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.

  73. l0rinc force-pushed on Aug 7, 2025
  74. DrahtBot removed the label CI failed on Aug 8, 2025
  75. 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
  76. 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
  77. 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.
  78. 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)
    
  79. 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.
  80. 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
  81. 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.
  82. 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.

  83. ryanofsky approved
  84. 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!
  85. DrahtBot requested review from hodlinator on Aug 8, 2025
  86. 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>
    3ffce37c0c
  87. 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>
    799c86698d
  88. 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.
    957be9a4ed
  89. refactor: pull `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
    6b167d5835
  90. refactor: pull `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
    01d786b1ea
  91. refactor: pull `IsPayToAnchor` to header
    Moved the implementation to the header (implicitly inline + noexcept), removed redundant `this` and parentheses.
    a01ae48f25
  92. refactor: pull `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
    0529af84ec
  93. refactor: add `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
    31df4fb27e
  94. refactor: add `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
    21c78340c4
  95. refactor: add `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
    59ae11238a
  96. refactor: add `IsPayToWitnessPubKeyHash` helper to script.h
    See: https://learnmeabitcoin.com/technical/script/p2wpkh/#scriptpubkey
    dff42e3af7
  97. 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>
    5b81379972
  98. 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.
    
    Also fixes minor spacing issues in conditional statements.
    52faae762b
  99. 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`.
    4cacfaac56
  100. 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.
    cf8f476ef2
  101. l0rinc force-pushed on Aug 11, 2025
  102. 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.

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-08-12 09:13 UTC

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