-
Notifications
You must be signed in to change notification settings - Fork 38.7k
test: add script compression coverage for not-on-curve P2PK outputs #28193
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: add script compression coverage for not-on-curve P2PK outputs #28193
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. ConflictsNo conflicts as of last run. |
f17ad2f to
4f2f66f
Compare
4f2f66f to
3cc5328
Compare
3cc5328 to
28287cf
Compare
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.
Good work increasing unit test robustness. Looks good to me. Only a nit was observed. Author's choice for nit inclusion/exclusion.
ACK for 28287cf.
Test actions taken:
Checkout the PR branch, built, ran all unit and functional tests (all passed).
| XOnlyPubKey x_not_on_curve; | ||
| do { | ||
| x_not_on_curve = XOnlyPubKey(g_insecure_rand_ctx.randbytes(32)); | ||
| } while (x_not_on_curve.IsFullyValid()); |
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: Looks like the approach employed is to pick a random X value until one is found that is not on the curve. What is the rationale for using randomness, rather than choosing a fixed X value that is not on the curve (e.g. fd0473a380ddf239accbc31770fcc6e64096930eaa8bc57c10f5868f3596fe1e)? Seems like the other tests in the test file use randomness as well (e.g. GenerateRandomKey()), so maybe I'm missing something. Selecting a known invalid X value would technically increase test repeatability and consistency of test execution performance (although all of the 10 runs executed locally resulted in no more than a handful of X-picking attempts before finding an invalid X value, so performance is hardly a concern here).
If I'm not overlooking something, slightly less than half of the 32-byte space is valid X coordinates on the curve, so random selection would seem to have a negligible risk of starvation in finding an off-curve X value.
https://bitcoin.stackexchange.com/questions/55196/is-there-a-point-on-the-secp256k1-curve-for-any-given-x-coordinate
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.
I also lean towards preferring a const/constexpr containing an always-invalid X-only-pubkey. Adding some logging in the loop and running the tests a bunch of times does show that we usually finds an invalid key on the first or second attempt. (This confirms the answer in the Stack Exchange link above). Only once over ~50 runs did it try 6 times.
(MakeNewKeyWithFastRandomContext() in orphanage_tests also uses .randbytes(32) but that's for calculating private keys which don't have invalid values (except possibly 0?). GenerateRandomKey() is also for private keys.)
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.
Using random values adds robustness which is nice. However, given that there are 13 real cases in the UTXO set, incorporating a couple of those as constants could work and add reproducibility for this test. I think it's up to the author ultimately, either way works.
cbergqvist
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 28287cf!
| XOnlyPubKey x_not_on_curve; | ||
| do { | ||
| x_not_on_curve = XOnlyPubKey(g_insecure_rand_ctx.randbytes(32)); | ||
| } while (x_not_on_curve.IsFullyValid()); |
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.
I also lean towards preferring a const/constexpr containing an always-invalid X-only-pubkey. Adding some logging in the loop and running the tests a bunch of times does show that we usually finds an invalid key on the first or second attempt. (This confirms the answer in the Stack Exchange link above). Only once over ~50 runs did it try 6 times.
(MakeNewKeyWithFastRandomContext() in orphanage_tests also uses .randbytes(32) but that's for calculating private keys which don't have invalid values (except possibly 0?). GenerateRandomKey() is also for private keys.)
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.
Nicely done, ACK 28287cf. Built the PR branch, ran the unit and functional tests, everything passed.
To further validate the changes, I executed some small mutation tests checking the robustness of the test. Here's the summary:
Altering the Initial Byte of pubkey_raw: Changed the initial byte from 4 to 2 (and tried 3), testing the assumption that the script specifically handles uncompressed pubkeys not on the curve. As expected, the test failed, validating the test's accuracy in identifying uncompressed pubkeys.
Making the Pubkey Fully Valid: Modified the generation of x_not_on_curve to produce fully valid pubkeys. The test failed, showing its ability to differentiate between compressible and non-compressible pubkeys based on curve validity.
Forcing CompressScript and DecompressScript to Succeed: Introduced changes to make these functions return true regardless of the actual input. The test identified these forced successes as incorrect outcomes, indicating their reliability in catching improper script (de)compression behaviors.
Altering the Script Size Check: Changed the expected script size to an incorrect value (68U instead of 67U). The test failed, highlighting its precision in validating script size expectations.
The new unit test robustly identifies and responds correctly to various edge cases. It's effective in handling script compression/decompression for uncompressed P2PK outputs with invalid pubkeys. Everything looks good to me.
|
|
||
| // Check that compressed P2PK script with uncompressed pubkey that is not fully | ||
| // valid (i.e. x coordinate of the pubkey is not on curve) can't be decompressed | ||
| CompressedScript compressed_script(x_not_on_curve.begin(), x_not_on_curve.end()); |
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.
Would it be good idea to change the Decompress test below to use CompressedScript out
Or maybe instead to validate the assumption that compressed_script == out here?
|
ACK 28287cf |
This PR adds unit test coverage for the script compression functions
{Compress,Decompress}Scriptin the special case of uncompressed P2PK outputs (scriptPubKey: OP_PUSH65 <0x04 ....> OP_CHECKSIG) with pubkeys that are not fully valid, i.e. where the encoded point is not on the secp256k1 curve. For those outputs, script compression is not possible, as the y coordinate of the pubkey can't be recovered (see also call-site ofIsToPubKey):bitcoin/src/compressor.cpp
Lines 49 to 50 in 44b05bf
Likewise, for a compressed script of an uncompressed P2PK script (i.e. compression ids 4 and 5) where the x coordinate is not on the curve, decompression fails:
bitcoin/src/compressor.cpp
Lines 122 to 129 in 44b05bf
Note that the term "compression" is used here in two different meanings (though they are related), which might be a little confusing. The encoding of a pubkey can either be compressed (33-bytes with 0x02/0x03 prefixes) or uncompressed (65-bytes with 0x04 prefix). On the other hand there is also compression for whole output scripts, which is used for storing scriptPubKeys in the UTXO set in a compact way (and also for the
dumptxoutsetresult, accordingly). P2PK output scripts with uncompressed pubkeys get compressed by storing only the x-coordinate and the sign as a prefix (0x04 = even, 0x05 = odd). Was diving deeper into the subject while working on #27432, where the script decompression of uncompressed P2PK needed special handling (see also #24628 (comment)).Trivia: as of now (block 801066), there are 13 uncompressed P2PK outputs in the UTXO set with a pubkey not on the curve (which obviously means they are unspendable).