test: fix `keys_to_multisig_script` (P2MS) helper for n/k > 16 #28312

pull theStack wants to merge 3 commits into bitcoin:master from theStack:test-fix_keys_to_multisig_for_17+ changing 3 files +33 −4
  1. theStack commented at 1:33 AM on August 22, 2023: contributor

    While reviewing #28307, I noticed that the test framework's key_to_multisig_script helper (introduced in #23305) is broken for pubkey count (n) and threshold (k) values larger than 16. This is due to the implementation currently enforcing a direct single-byte data push (using CScriptOp.encode_op_n), which obviously fails for values 17+. Fix that by passing the numbers directly to the CScript list, where it's automatically converted to minimally-encoded pushes (see class method CScript.__coerce_instance, branch isinstance(other, int)).

    The second commit adds a unit test to ensure that the encoding is correct.

  2. DrahtBot commented at 1:33 AM on August 22, 2023: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage

    For detailed information about the code coverage, see the test coverage report.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK tdb3, rkrux, achow101

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

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #28710 (Remove the legacy wallet and BDB dependency by achow101)

    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. DrahtBot added the label Tests on Aug 22, 2023
  4. theStack force-pushed on Aug 22, 2023
  5. theStack force-pushed on Aug 22, 2023
  6. luke-jr commented at 5:35 PM on August 23, 2023: member

    Maybe add a functional test that would have failed too?

  7. theStack commented at 10:14 PM on August 23, 2023: contributor

    Maybe add a functional test that would have failed too?

    Yeah, seems like a good idea. Added one in rpc_createmultisig which checks that the multisig script encoding (returned as 'redeemScript') is as expected for all possible n (1..20), only using n-of-n for now to keep it simple.

  8. DrahtBot added the label CI failed on Sep 3, 2023
  9. DrahtBot removed the label CI failed on Sep 5, 2023
  10. DrahtBot added the label CI failed on Sep 14, 2023
  11. DrahtBot removed the label CI failed on Sep 15, 2023
  12. DrahtBot added the label CI failed on Jan 12, 2024
  13. theStack force-pushed on Jan 22, 2024
  14. DrahtBot removed the label CI failed on Jan 22, 2024
  15. DrahtBot added the label CI failed on Mar 8, 2024
  16. DrahtBot removed the label CI failed on Mar 14, 2024
  17. in test/functional/test_framework/script_util.py:142 in a36ad0cb1d outdated
     137 | +        self.assertTrue(normal_ms_script.startswith(bytes([OP_15])))
     138 | +        self.assertTrue(normal_ms_script.endswith(bytes([OP_16, OP_CHECKMULTISIG])))
     139 | +
     140 | +        # check correct encoding of P2MS script with n,k > 16
     141 | +        max_ms_script = keys_to_multisig_script([fake_pubkey]*20, k=19)
     142 | +        self.assertEqual(len(max_ms_script), 2 + 20*34 + 2 + 1)
    


    rkrux commented at 11:12 AM on March 19, 2024:

    Should we add a comment explaining the logic behind the math in these script length assert calls? Or maybe refer an existing doc?


    rkrux commented at 11:56 AM on March 20, 2024:

    Trying to understand why is there this else block in coerce_instance here that encodes using pushdata. @theStack Do you know the reason by chance? https://github.com/bitcoin/bitcoin/blob/master/test/functional/test_framework/script.py#L451-L452


    sipa commented at 12:47 PM on March 20, 2024:

    @rkrux Numbers between -1 and 16 inclusive can use the 1-byte OP_n opcodes. Larger numbers require a script push (which is 2 bytes for numbers up to 127).


    tdb3 commented at 5:00 PM on June 7, 2024:

    nit: In case this file is touched, it might be clearer (as rkrux mentioned) to add a comment explicitly explaining the math, although this is just a nice-to-have since the addition breaks it out such that the reader is provided a hint (typical multisig script). Author's preference.

    diff --git a/test/functional/test_framework/script_util.py b/test/functional/test_framework/script_util.py
    index 855f3b8cf5..69be490a67 100755
    --- a/test/functional/test_framework/script_util.py
    +++ b/test/functional/test_framework/script_util.py
    @@ -133,7 +133,7 @@ class TestFrameworkScriptUtil(unittest.TestCase):
             fake_pubkey = bytes([0]*33)
             # check correct encoding of P2MS script with n,k <= 16
             normal_ms_script = keys_to_multisig_script([fake_pubkey]*16, k=15)
    -        self.assertEqual(len(normal_ms_script), 1 + 16*34 + 1 + 1)
    +        self.assertEqual(len(normal_ms_script), 1 + 16*34 + 1 + 1) # OP_k + 16*[OP_PUSHBYTES_33 + 33 byte compressed pubkey] + OP_n + OP_CHECKMULTISIG
             self.assertTrue(normal_ms_script.startswith(bytes([OP_15])))
             self.assertTrue(normal_ms_script.endswith(bytes([OP_16, OP_CHECKMULTISIG])))
     
    
  18. in test/functional/rpc_createmultisig.py:74 in d113709dc4 outdated
     118 | @@ -118,6 +119,16 @@ def run_test(self):
     119 |          # Check that bech32m is currently not allowed
     120 |          assert_raises_rpc_error(-5, "createmultisig cannot create bech32m multisig addresses", self.nodes[0].createmultisig, 2, self.pub, "bech32m")
     121 |  
     122 | +        self.log.info('Check correct encoding of multisig script for all n (1..20)')
     123 | +        for nkeys in range(1, 20+1):
    


    rkrux commented at 11:19 AM on March 19, 2024:

    Nit: Checking for integers between 15 and 17 would also suffice, right?

  19. in test/functional/rpc_createmultisig.py:79 in d113709dc4 outdated
     123 | +        for nkeys in range(1, 20+1):
     124 | +            keys = [self.pub[0]]*nkeys
     125 | +            expected_ms_script = keys_to_multisig_script(keys, k=nkeys)  # simply use n-of-n
     126 | +            # note that the 'legacy' address type fails for n values larger than 15
     127 | +            # due to exceeding the P2SH size limit (520 bytes), so we use 'bech32' instead
     128 | +            # (for the purpose of this encoding test, we don't care about the resulting address)
    


    rkrux commented at 11:19 AM on March 19, 2024:

    Thanks for adding the comment, helpful!

  20. in test/functional/rpc_createmultisig.py:76 in d113709dc4 outdated
     118 | @@ -118,6 +119,16 @@ def run_test(self):
     119 |          # Check that bech32m is currently not allowed
     120 |          assert_raises_rpc_error(-5, "createmultisig cannot create bech32m multisig addresses", self.nodes[0].createmultisig, 2, self.pub, "bech32m")
     121 |  
     122 | +        self.log.info('Check correct encoding of multisig script for all n (1..20)')
     123 | +        for nkeys in range(1, 20+1):
     124 | +            keys = [self.pub[0]]*nkeys
     125 | +            expected_ms_script = keys_to_multisig_script(keys, k=nkeys)  # simply use n-of-n
    


    rkrux commented at 11:30 AM on March 19, 2024:

    Curious: Is it common to test the correctness of a test utility function against the response from the bitcoin node?


    theStack commented at 2:30 PM on June 5, 2024:

    Good question. I'm not sure if it's really that common, but there was a wish to also test the utility function via a functional test (see comment #28312 (comment)) and it seemed to be a worthwhile idea to me.

  21. rkrux commented at 11:30 AM on March 19, 2024: contributor

    Concept ACK d113709

    Testing Methodology

    1. Checked out PR
    2. Built core
    3. Ran make check
    4. Ran test/functional/test_runner.py
  22. in test/functional/rpc_createmultisig.py:82 in d113709dc4 outdated
     126 | +            # note that the 'legacy' address type fails for n values larger than 15
     127 | +            # due to exceeding the P2SH size limit (520 bytes), so we use 'bech32' instead
     128 | +            # (for the purpose of this encoding test, we don't care about the resulting address)
     129 | +            res = self.nodes[0].createmultisig(nrequired=nkeys, keys=keys, address_type='bech32')
     130 | +            assert_equal(res['redeemScript'], expected_ms_script.hex())
     131 | +
    


    tdb3 commented at 1:09 AM on April 9, 2024:

    Since > 20 keys are not allowed when creating a multisig address, recommend adding a test case to check this.

    Something like:

    diff --git a/test/functional/rpc_createmultisig.py b/test/functional/rpc_createmultisig.py
    index 4bbd2a526d..88f4e44a65 100755
    --- a/test/functional/rpc_createmultisig.py
    +++ b/test/functional/rpc_createmultisig.py
    @@ -129,6 +129,9 @@ class RpcCreateMultiSigTest(BitcoinTestFramework):
                 res = self.nodes[0].createmultisig(nrequired=nkeys, keys=keys, address_type='bech32')
                 assert_equal(res['redeemScript'], expected_ms_script.hex())
     
    +        self.log.info('Check that number of keys greater than 20 is not allowed')
    +        assert_raises_rpc_error(-8, "Number of keys involved in the multisignature address creation > 20", self.nodes[0].createmultisig, 20, [self.pub[0]]*21, "bech32")
    +
         def check_addmultisigaddress_errors(self):
             if self.options.descriptors:
                 return
    
    

    theStack commented at 2:26 PM on June 5, 2024:

    Such a test case was already introduced in the recently merged PR #28307 (commit 9be6065cc03f2408f290a332b203eef9c9cebf24). Note that this PR is primarily focused on the keys_to_multisig_script helper in the test framework and only uses the createmultisig RPC to verify it.

  23. tdb3 commented at 1:11 AM on April 9, 2024: contributor

    Great work catching the bug and making testing more robust.

    ACK for d113709dc4e68535605d22814ea987b55469bd32.

    One inline recommendation to cover an additional related edge case.

  24. DrahtBot requested review from rkrux on Apr 9, 2024
  25. bitcoin deleted a comment on Apr 9, 2024
  26. DrahtBot added the label CI failed on Apr 21, 2024
  27. DrahtBot removed the label CI failed on Apr 23, 2024
  28. DrahtBot added the label Needs rebase on Apr 26, 2024
  29. test: fix `keys_to_multisig_script` (P2MS) helper for n/k > 16
    The helper assumes that the n and k values have to be provided as a
    single byte push operation, which is only possible for values up to 16.
    Fix that by passing the numbers directly to the CScript list, where it's
    automatically converted to minimally-encoded pushes (see class
    method `CScript.__coerce_instance`, branch `isinstance(other, int)`).
    
    In case of 17..20, this means that the data-pushes are done with two
    bytes using OP_PUSH1 (0x01), e.g. for n=20: 0x01,0x14
    0c41fc3fa5
  30. test: add unit test for `keys_to_multisig_script` 0570d2c204
  31. test: add `createmultisig` P2MS encoding test for all n (1..20) 5cf0a1f230
  32. theStack force-pushed on Jun 5, 2024
  33. theStack commented at 2:22 PM on June 5, 2024: contributor

    Rebased on master.

  34. DrahtBot removed the label Needs rebase on Jun 5, 2024
  35. tdb3 approved
  36. tdb3 commented at 5:01 PM on June 7, 2024: contributor

    ACK 5cf0a1f230389ef37e0ff65de5fc98394f32f60c Re-ran functional tests (passed) and induced a failure by hard-coding 1 as the k value.

  37. rkrux approved
  38. rkrux commented at 12:40 PM on June 15, 2024: contributor

    reACK 5cf0a1f

    Re-ran make and all functional tests, both are successful. Thanks @theStack for catching this.

  39. achow101 commented at 7:10 PM on June 17, 2024: member

    ACK 5cf0a1f230389ef37e0ff65de5fc98394f32f60c

  40. achow101 merged this on Jun 17, 2024
  41. achow101 closed this on Jun 17, 2024

  42. theStack deleted the branch on Jul 15, 2024
  43. bitcoin locked this on Jul 15, 2025

github-metadata-mirror

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

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