test: cover P2SH sigop counting in test_witness_sigops #35164

pull musaHaruna wants to merge 2 commits into bitcoin:master from musaHaruna:test/p2sh-sigop-counting changing 1 files +31 −2
  1. musaHaruna commented at 8:25 AM on April 27, 2026: contributor

    Add test coverage for sigop counting in P2SH spends in test_witness_sigops(), addressing the existing TODO.

    The new cases mirror the existing P2WSH sigop tests by constructing transactions that:

    • remain below the block sigop limit (accepted),
    • exceed the limit (rejected with bad-blk-sigops)

    Since P2SH sigops are accounted as legacy sigops, the expected sigop cost accounts for the 4× legacy weighting applied during consensus validation.

    The added coverage verifies the enforcement of the block sigop limit for both witness and P2SH spends, including mixed P2SH/witness transactions.

    Acknowledgement: During review (comment), l0rinc demonstrated, using mutation testing on his branch here, that the original test suite would not detect two consensus sigop undercounting bugs. Those experiments helped validate the coverage added by this PR and motivated the inclusion of the mixed P2SH/witness regression test.

  2. DrahtBot added the label Tests on Apr 27, 2026
  3. DrahtBot commented at 8:25 AM on April 27, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

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

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Stale ACK haishmg, l0rinc

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

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    No conflicts as of last run.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

    Possible places where comparison-specific test macros should replace generic comparisons:

    • test/functional/p2p_segwit.py assert extra_sigops_available < 100 -> assert_greater_than(100, extra_sigops_available)
    • test/functional/p2p_segwit.py assert p2sh_extra_sigops_available < 100 -> assert_greater_than(100, p2sh_extra_sigops_available)
    • test/functional/p2p_segwit.py assert p2sh_witness_p2sh_sigops_cost + p2sh_witness_witness_sigops_cost > MAX_SIGOP_COST -> assert_greater_than(p2sh_witness_p2sh_sigops_cost + p2sh_witness_witness_sigops_cost, MAX_SIGOP_COST)

    <sup>2026-08-05 13:23:48</sup>

  4. haishmg commented at 4:41 PM on April 28, 2026: none

    ACK f3f1a703137bd9adf492dd67cda3779ae3a2cd8e

    Reviewed the added P2SH sigop coverage in test_witness_sigops. The new cases mirror the existing P2WSH boundary checks and exercise below-limit, over-limit, and exact-limit P2SH spends with legacy sigop cost accounting.

    Ran locally on macOS arm64:

    • build/test/functional/p2p_segwit.py
    • git diff --check upstream/master...HEAD

    I also tried test/lint/lint-python.py test/functional/p2p_segwit.py, but it skipped locally because lief is not installed.

  5. luke-jr referenced this in commit ccffd11372 on May 3, 2026
  6. in test/functional/p2p_segwit.py:2034 in f3f1a70313
    2030 | +
    2031 | +        tx5.vout.append(CTxOut(total_value, CScript([OP_TRUE])))
    2032 | +
    2033 | +        # This block should be accepted (sigops exactly at limit)
    2034 | +        block_8 = self.build_next_block()
    2035 | +        self.update_witness_block_with_transactions(block_8, [tx5])
    


    Bicaru20 commented at 8:14 PM on May 15, 2026:

    Instead of creating a new transaction, I think we could edit the tx4. That way I think it is clear what is happening and way now the tx is accpeted. Doing this we also keep the behaviour constant as with tx2.

            tx4.vin.pop() # Eliminate the last input with too many sigops
            tx4.vin.append(CTxIn(COutPoint(tx3.txid_int, p2sh_outputs - 1),
                                CScript([redeem_script_justright])))
    
            # This block should be accepted (sigops exactly at limit)
            block_8 = self.build_next_block()
            self.update_witness_block_with_transactions(block_8, [tx4])
    

    Bicaru20 commented at 8:18 PM on May 15, 2026:

    Also this way we can add the test of what happens if we add an output with too many sigops (just as in the previous case with tx2). However I am not sure if this is needed in this case.

    tx4.vout.append(CTxOut(0, CScript([OP_CHECKSIG]*1))) # Add the output with too man sigops
    block_8 = self.build_next_block()
    self.update_witness_block_with_transactions(block_8, [tx4])
    test_witness_block(self.nodes[0], self.test_node, block_8, accepted=False, reason='bad-blk-sigops')
    
    tx4.vout.pop() # Eliminate the outputs with too many sigops
    
  7. Bicaru20 commented at 8:24 PM on May 15, 2026: contributor

    It looks good. I left some comments to try to optimize the code.

  8. musaHaruna force-pushed on May 16, 2026
  9. musaHaruna commented at 8:59 PM on May 16, 2026: contributor

    Thanks for the review @Bicaru20.

    Instead of creating a new transaction, I think we could edit the tx4. That way I think it is clear what is happening and way now the tx is accpeted. Doing this we also keep the behaviour constant as with tx2.

    Suggestion taken in e2a4948

    Also this way we can add the test of what happens if we add an output with too many sigops (just as in the previous case with tx2). However I am not sure if this is needed in this case.

    I think this additional check is unnecessary because the interaction between input sigops and output sigops is already covered earlier in the test (block_3 and block_4).

  10. luke-jr referenced this in commit e70953e9ee on Jul 23, 2026
  11. in test/functional/p2p_segwit.py:1952 in e2a49487af
    1944 | @@ -1945,11 +1945,83 @@ def test_witness_sigops(self):
    1945 |          self.update_witness_block_with_transactions(block_5, [tx2])
    1946 |          test_witness_block(self.nodes[0], self.test_node, block_5, accepted=True)
    1947 |  
    1948 | -        # TODO: test p2sh sigop counting
    1949 | +        # In P2SH, sigops are counted as *legacy sigops* (no witness discount),
    1950 | +        # meaning each sigop costs 4x more than in witness.
    1951 | +        p2sh_sigops_per_script = sigops_per_script * 4
    1952 | +
    1953 | +        # Compute how many outputs we can create before exceeding MAX_SIGOP_COST
    


    sedited commented at 8:18 PM on July 23, 2026:

    These comments are excessive. They make it seem like this was copy-pasted from an LLM without much understanding. I'd prefer if most of them were removed, which would probably also more than halve the diff.

  12. l0rinc commented at 8:35 PM on July 23, 2026: contributor

    I'll review this a bit later, seems like a good addition before/after https://github.com/bitcoin/bitcoin/pull/32729

  13. in test/functional/p2p_segwit.py:2012 in e2a49487af outdated
    2008 | +        tx4.vout.append(CTxOut(total_value, CScript([OP_TRUE])))
    2009 | +
    2010 | +        # This block should be rejected due to too many sigops
    2011 | +        block_7 = self.build_next_block()
    2012 | +        self.update_witness_block_with_transactions(block_7, [tx4])
    2013 | +        test_witness_block(self.nodes[0], self.test_node, block_7, accepted=False, reason='bad-blk-sigops')
    


    l0rinc commented at 1:25 AM on July 24, 2026:

    This does indeed catch a consensus drift that would otherwise pass CI:

    diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp
    --- a/src/consensus/tx_verify.cpp	(revision afa5e46bbc6dd750bd71920b659162a945abf0ae)
    +++ b/src/consensus/tx_verify.cpp	(revision 0175450a6ae5dc2dd5e8770492d46ca183fcbedd)
    @@ -148,7 +148,7 @@
             return nSigOps;
    
         if (flags & SCRIPT_VERIFY_P2SH) {
    -        nSigOps += GetP2SHSigOpCount(tx, inputs) * WITNESS_SCALE_FACTOR;
    +        nSigOps += std::min(GetP2SHSigOpCount(tx, inputs), 2'500U) * WITNESS_SCALE_FACTOR;
         }
    
         for (unsigned int i = 0; i < tx.vin.size(); i++)
    

    With this PR it fails with:

    2026-07-24T01:23:19.021298Z TestFramework (ERROR): Unexpected exception:
    Traceback (most recent call last):
      File "bitcoin/test/functional/test_framework/test_framework.py", line 145, in main
        self.run_test()
        ~~~~~~~~~~~~~^^
      File "bitcoin/build/test/functional/p2p_segwit.py", line 291, in run_test
        self.test_witness_sigops()
        ~~~~~~~~~~~~~~~~~~~~~~~~^^
      File "bitcoin/build/test/functional/p2p_segwit.py", line 112, in func_wrapper
        func(self, *args, **kwargs)
        ~~~~^^^^^^^^^^^^^^^^^^^^^^^
      File "bitcoin/build/test/functional/p2p_segwit.py", line 2012, in test_witness_sigops
        test_witness_block(self.nodes[0], self.test_node, block_7, accepted=False, reason='bad-blk-sigops')
        ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "bitcoin/build/test/functional/p2p_segwit.py", line 146, in test_witness_block
        assert_equal(node.getbestblockhash() == block.hash_hex, accepted)
        ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "bitcoin/test/functional/test_framework/util.py", line 94, in assert_equal
        raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
    AssertionError: not(True == False)
    

    Which revealed another mutation drift that wouldn't be caught by any existing test:

    diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp
    --- a/src/consensus/tx_verify.cpp	(revision 7a585306f2f6fea79706b496b2ebf306e78dd484)
    +++ b/src/consensus/tx_verify.cpp	(revision 309a37ffdfb419c87f2fc001fad3b0c611574843)
    @@ -147,7 +147,7 @@
         if (tx.IsCoinBase())
             return nSigOps;
    
    -    if (flags & SCRIPT_VERIFY_P2SH) {
    +    if ((flags & SCRIPT_VERIFY_P2SH) && !tx.HasWitness()) {
             nSigOps += GetP2SHSigOpCount(tx, inputs) * WITNESS_SCALE_FACTOR;
         }
    

    I added a bug-injection-with-passing-CI/failing-test/revert-bug-passing-CI PR: https://github.com/l0rinc/bitcoin/pull/248

    I have simplified the fix presented here, feel free to cherry-pick the fixes (but not the injected bugs) here. Alternatively I can open a new PR and add you as coauthor.

  14. l0rinc changes_requested
  15. l0rinc commented at 3:07 AM on July 24, 2026: contributor

    This does reveal lack of coverage, but there's easier way to test it.

  16. musaHaruna force-pushed on Jul 24, 2026
  17. musaHaruna force-pushed on Jul 24, 2026
  18. DrahtBot added the label CI failed on Jul 24, 2026
  19. test: add P2SH sigop counting coverage
    Implement the TODO in `test_witness_sigops()` by adding coverage
    for sigop accounting in P2SH spends.
    
    Co-authored-by: Lőrinc <pap.lorinc@gmail.com>
    6e60c362bc
  20. musaHaruna force-pushed on Jul 24, 2026
  21. musaHaruna commented at 2:45 PM on July 24, 2026: contributor

    Hi @sedited and @l0rinc , thank you both for the reviews! @l0rinc, I cherry-picked both test commits from your branch. For the P2SH sigop counting commit, I incorporated it into my existing changes and added you as a co-author. For the mixed P2SH/witness sigop accounting commit, I kept you as the author. Thank you for simplifying the tests and for demonstrating the coverage gaps using the mutation examples—they made it much clearer what the missing coverage was and helped improve this PR.

    I've also split the changes into two commits:

    test: add P2SH sigop counting coverage

    test: add mixed P2SH/witness sigop accounting

    I also updated the PR description to acknowledge your contribution and the mutation-testing work that motivated the additional test. @sedited, thanks for pointing out the excessive comments. I removed them. Looking back, I agree they were more verbose than necessary. They started out as an attempt to explain the code to myself while I was learning, and then grew even longer as I polished the wording. In the end, they added noise to the diff without improving the test itself.

  22. DrahtBot removed the label CI failed on Jul 24, 2026
  23. l0rinc approved
  24. l0rinc commented at 6:48 PM on July 24, 2026: contributor

    tested ACK f188469573679743f1612e29b90128c0692243b4

    Rebased, tests passing locally, reinjecting the bugs catches them correctly. Changes were cherry-picked correctly. PR description is slightly off: the "reach the limit exactly (accepted)" case describes block_5, which is the pre-existing witness case, not a new P2SH case. The new additions cover the over-limit side only.

  25. luke-jr referenced this in commit afae36ae76 on Jul 25, 2026
  26. luke-jr referenced this in commit 8ca64ed611 on Jul 25, 2026
  27. test: add mixed P2SH/witness sigop accounting
    Add test covering transactions containing both
    P2SH and witness inputs when enforcing the block sigop limit.
    9d047dbd65
  28. in test/functional/p2p_segwit.py:1958 in f188469573
    1954 | @@ -1955,6 +1955,13 @@ def test_witness_sigops(self):
    1955 |          self.update_witness_block_with_transactions(block_6, [p2sh_tx])
    1956 |          test_witness_block(self.nodes[0], self.test_node, block_6, accepted=False, reason='bad-blk-sigops')
    1957 |  
    1958 | +        p2sh_tx.vin.append(CTxIn(COutPoint(tx.txid_int, outputs - 2), b""))
    


    sedited commented at 3:00 PM on August 4, 2026:

    What is this line doing? The test passes without it. I'm also not sure about this case in general. What kind of regression is this supposed to protect against?


    musaHaruna commented at 1:33 PM on August 5, 2026:

    I added temporary debug logs around the block_6 / block_7 construction in test/functional/p2p_segwit.py, and in GetTransactionSigOpCost() in src/consensus/tx_verify.cpp, to confirm the actual sigop counts.

    The logs show that the old block_7 still failed without the appended witness input because the P2SH portion was already over the limit:

    block_6: p2sh_cost=80868 witness_cost=0  total=80868
    block_7: p2sh_cost=80868 witness_cost=12 total=80880
    

    The intended regression coverage is mixed P2SH + witness sigop accounting in GetTransactionSigOpCost(): the P2SH cost should be valid by itself, and the added witness input should be what pushes the transaction over the limit.

    So the test did not isolate mixed P2SH + witness sigop accounting.

    I updated the test so the cases are distinct:

    block_6: pure P2SH over limit
             p2sh_cost=80868 witness_cost=0 total=80868
             rejected with bad-blk-sigops
    
    block_7: P2SH + witness over limit
             p2sh_cost=80000 witness_cost=12 total=80012
             rejected with bad-blk-sigops
    

    This catches mutations like:

    if ((flags & SCRIPT_VERIFY_P2SH) && !tx.HasWitness()) {
        nSigOps += GetP2SHSigOpCount(tx, inputs) * WITNESS_SCALE_FACTOR;
    }
    

    That would incorrectly skip P2SH sigop counting for transactions with witness data. With the new block_7, the mutated code would count only witness_cost=12 and incorrectly accept the block. @l0rinc, hope this matches your original intent.


    l0rinc commented at 11:03 PM on August 5, 2026:

    What is this line doing?

    As explained above by @musaHaruna, I suggested this case in #35164 (review) to catch consensus-accounting mutations. The test passes without it because the current consensus code is correct. With the mutation that skips P2SH sigops for transactions containing witness data, it fails as intended.


    l0rinc commented at 11:09 PM on August 5, 2026:

    There are lots of additions in latest push, wouldn't it suffice to comment the line in question (without which consensus invalid code would pass CI):

      # Add witness data to verify that the transaction's P2SH sigops are still counted.
      p2sh_tx.vin.append(CTxIn(COutPoint(tx.txid_int, outputs - 2), b""))
    
  29. musaHaruna force-pushed on Aug 5, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-08-11 10:51 UTC

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