-
Notifications
You must be signed in to change notification settings - Fork 38.7k
test: fix keys_to_multisig_script (P2MS) helper for n/k > 16
#28312
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
Conversation
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code CoverageFor detailed information about the code coverage, see the test coverage report. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update. ConflictsReviewers, this pull request conflicts with the following ones:
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. |
73c2def to
85c01bd
Compare
|
Maybe add a functional test that would have failed too? |
Yeah, seems like a good idea. Added one in |
16269ef to
d113709
Compare
| 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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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])))
| self.log.info('Check correct encoding of multisig script for all n (1..20)') | ||
| for nkeys in range(1, 20+1): |
There was a problem hiding this comment.
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?
| # 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) |
There was a problem hiding this comment.
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!
There was a problem hiding this 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
- Checked out PR
- Built core
- Ran make check
- 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
tdb3
left a comment
There was a problem hiding this 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()) | ||
|
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
d113709 to
5cf0a1f
Compare
|
Rebased on master. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 5cf0a1f
Re-ran functional tests (passed) and induced a failure by hard-coding 1 as the k value.
| 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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])))
rkrux
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
ACK 5cf0a1f |
While reviewing #28307, I noticed that the test framework's
key_to_multisig_scripthelper (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 (usingCScriptOp.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 methodCScript.__coerce_instance, branchisinstance(other, int)).The second commit adds a unit test to ensure that the encoding is correct.