Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

theStack
Copy link
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.

@DrahtBot
Copy link
Contributor

DrahtBot commented Aug 22, 2023

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

Code Coverage

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

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK tdb3
Concept ACK rkrux

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:

  • #29771 (test: Run framework unit tests in parallel by tdb3)
  • #28307 (rpc, wallet: fix incorrect segwit redeem script size limit by furszy)

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.

@DrahtBot DrahtBot added the Tests label Aug 22, 2023
@theStack theStack force-pushed the test-fix_keys_to_multisig_for_17+ branch 2 times, most recently from 73c2def to 85c01bd Compare August 22, 2023 09:57
@luke-jr
Copy link
Member

luke-jr commented Aug 23, 2023

Maybe add a functional test that would have failed too?

@theStack
Copy link
Contributor Author

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.

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
@theStack theStack force-pushed the test-fix_keys_to_multisig_for_17+ branch from 16269ef to d113709 Compare January 22, 2024 12:40
Comment on lines +136 to +142
self.assertEqual(len(normal_ms_script), 1 + 16*34 + 1 + 1)
self.assertTrue(normal_ms_script.startswith(bytes([OP_15])))
self.assertTrue(normal_ms_script.endswith(bytes([OP_16, OP_CHECKMULTISIG])))

# check correct encoding of P2MS script with n,k > 16
max_ms_script = keys_to_multisig_script([fake_pubkey]*20, k=19)
self.assertEqual(len(max_ms_script), 2 + 20*34 + 2 + 1)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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).

Comment on lines +122 to +123
self.log.info('Check correct encoding of multisig script for all n (1..20)')
for nkeys in range(1, 20+1):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +126 to +128
# note that the 'legacy' address type fails for n values larger than 15
# due to exceeding the P2SH size limit (520 bytes), so we use 'bech32' instead
# (for the purpose of this encoding test, we don't care about the resulting address)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the comment, helpful!

Copy link

@rkrux rkrux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK d113709

Testing Methodology

  1. Checked out PR
  2. Built core
  3. Ran make check
  4. Ran test/functional/test_runner.py

self.log.info('Check correct encoding of multisig script for all n (1..20)')
for nkeys in range(1, 20+1):
keys = [self.pub[0]]*nkeys
expected_ms_script = keys_to_multisig_script(keys, k=nkeys) # simply use n-of-n
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor

@tdb3 tdb3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work catching the bug and making testing more robust.

ACK for d113709.

One inline recommendation to cover an additional related edge case.

# (for the purpose of this encoding test, we don't care about the resulting address)
res = self.nodes[0].createmultisig(nrequired=nkeys, keys=keys, address_type='bech32')
assert_equal(res['redeemScript'], expected_ms_script.hex())

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@DrahtBot
Copy link
Contributor

🐙 This pull request conflicts with the target branch and needs rebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants