-
Notifications
You must be signed in to change notification settings - Fork 37.2k
Introduce secp256k1 module with field and group classes to test framework #26222
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
Conversation
4d22863
to
4d6be31
Compare
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. 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. |
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.
Reviewed to learn.
4d6be31
to
f124dcb
Compare
f124dcb
to
5b69570
Compare
Rebased. |
5801cd5
to
8851fea
Compare
8851fea
to
5b4477d
Compare
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, +1 on increased readability and reusability 💯 (found my way here through #24748)
The slow-down is unfortunately pretty significant on the two x86_64 machines I tested, feature_taproot.py
taking almost twice as long compared to master here. Results measured via time
:
Machine / OS | master | pr26222 (rebased) |
---|---|---|
AMD EPYC 7702P (6) / Ubuntu 22.04.1 LTS | 40.496s | 1m16.587s |
Intel (Broadwell) (2) / OpenBSD 7.2 | 2m57.43s | 5m45.90s |
For the fun of it, I looked a bit into other ways how to compensate the performance loss. Considering that scalar multiplication with G is a frequent operation, precomputing G's doubled-up values (G, 2G, 4G, ..., (2^254)G, (2^255)G) in a lookup table seemed promising, with the goal of needing only ~128 point additions on average (one for each bit set in the scalar), without any extra double operations. Using that, the execution time is still not matching, but at least pretty close to the master branch (45.334s for first, 3m23.48s for second machine), without too much additional code (~25 LOC):
diff for commit "test: EC: optimize scalar multiplication of G by using lookup table"
index 7434ba49e..1ad1bc3e4 100644
--- a/test/functional/test_framework/key.py
+++ b/test/functional/test_framework/key.py
@@ -233,6 +233,8 @@ class GE:
def __mul__(self, a):
"""Multiply a point with an integer (scalar multiplication)."""
+ if self == SECP256K1_G: # optimize generator multiplication using precomputed data
+ return fast_g.mul(a)
r = None
for i in range(a.bit_length() - 1, -1, -1):
if r is not None:
@@ -596,6 +598,31 @@ def sign_schnorr(key, msg, aux=None, flip_p=False, flip_r=False):
e = int.from_bytes(TaggedHash("BIP0340/challenge", R.to_bytes_xonly() + P.to_bytes_xonly() + msg), 'big') % GE.ORDER
return R.to_bytes_xonly() + ((k + e * sec) % GE.ORDER).to_bytes(32, 'big')
+
+class FastG:
+ """Speed up scalar multiplication with the generator point G
+ by using a precomputed lookup table with its powers of 2:
+ g_table = [G, G*2, G*4, G*(2^3), G*(2^4), ..., G*(2^255)]
+ The points corresponding to each bit set in the scalar are
+ added up, i.e. on average ~128 point additions take place.
+ """
+ def __init__(self):
+ self.g_table = [] # g_table[i] = G * (2^i)
+ g_i = SECP256K1_G
+ for bit in range(256):
+ self.g_table.append(g_i)
+ g_i = g_i.double()
+
+ def mul(self, a):
+ result = None
+ for bit in range(a.bit_length()):
+ if (a & (1 << bit)):
+ result += self.g_table[bit]
+ return result
+
+fast_g = FastG()
+
+
class TestFrameworkKey(unittest.TestCase):
def test_schnorr(self):
"""Test the Python Schnorr implementation."""
(see branch https://github.com/theStack/bitcoin/tree/pr26222_followup_precompute_g_doubles)
Not 100% sure if we even want optimizations like this in test framwork (also, immediately generating data at module import seems kind of hacky), but it may be worth it in this case? I'm pretty sure there are more efficient ways to pursue this idea (e.g. extending the lookup table for larger chunks than only 1-bit pieces to need even less additions), but probably the extra implementation / code clutter / maintenance effort is not worth it. Any other ideas? Happy to hear shadowy secp256k1 super-magicians' thoughts here :)
5b4477d
to
b1797e3
Compare
60e1938
to
28ab6c3
Compare
@theStack I've included your commit to add the precomputed G table. The speedup is significant enough that it's worth it, I think. You've indeed discovered one of the techniques that are used for speeding up EC multiplications with precomputed tables. Libsecp256k1 today uses a more advanced version of that idea, where all multiples of the form |
c573249
to
2745abf
Compare
Addressed review comments. I've also renamed |
2745abf
to
ab30e81
Compare
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 ab30e81
…ent) classes These are primarily designed for ease of understanding, not performance.
On my machine, this speeds up the functional test feature_taproot.py by a factor of >1.66x (runtime decrease from 1m16.587s to 45.334s). Co-authored-by: Pieter Wuille <pieter@wuille.net>
ab30e81
to
d4fb58a
Compare
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.
re-ACK d4fb58a
(as per $ git range-diff ab30e81b...d4fb58ae
, verified that the only changes since the last ACK were a rebase on master and #26222 (comment) tackled)
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.
tested ACK d4fb58a. really liked how this PR makes the secp256k1 code in the tests more intuitive and easier to follow!
ACK d4fb58a Much simpler to understand, and the more complicated stuff matches up with the descriptions in WIkipedia. |
…classes to test framework d4fb58a test: EC: optimize scalar multiplication of G by using lookup table (Sebastian Falbesoner) 1830dd8 test: add secp256k1 module with FE (field element) and GE (group element) classes (Pieter Wuille) Pull request description: This PR rewrites a portion of `test_framework/key.py`, in a compatible way, by introducing classes that encapsulate field element and group element logic, in an attempt to be more readable and reusable. To maximize readability, the group element logic does not use Jacobian coordinates. Instead, group elements just store (affine) X and Y coordinates directly. To compensate for the performance loss this causes, field elements are represented as fractions. This undoes most, but not all, of the performance loss, and there is a few % slowdown (as measured in `feature_taproot.py`, which heavily uses this). The upside is that the implementation for group laws (point doubling, addition, subtraction, ...) is very close to the mathematical description of elliptic curves, and this extends to potential future extensions (e.g. ElligatorSwift as needed by bitcoin#27479). ACKs for top commit: achow101: ACK d4fb58a theStack: re-ACK d4fb58a stratospher: tested ACK d4fb58a. really liked how this PR makes the secp256k1 code in the tests more intuitive and easier to follow! Tree-SHA512: 9e0d65d7de0d4fb35ad19a1c19da7f41e5e1db33631df898c6d18ea227258a8ba80c893dab862b0fa9b0fb2efd0406ad4a72229ee26d7d8d733dee1d56947f18
…ator swift 4f4d039 test: add ellswift test vectors from BIP324 (stratospher) a312877 test: Add ellswift unit tests (stratospher) 714fb2c test: Add python ellswift implementation to test framework (stratospher) Pull request description: Built on top of bitcoin/bitcoin#26222. This PR introduces Elligator swift encoding and decoding in the functional test framework. It's used in #24748 for writing p2p encryption tests. ACKs for top commit: sipa: ACK 4f4d039 theStack: ACK 4f4d039 🐊 Tree-SHA512: 32bc8e88f715f2cd67dc04cd38db92680872072cb3775478e2c30da89aa2da2742992779ea14da2f1faca09228942cfbd86d6957402b24bf560244b389e03540
4f4d039 test: add ellswift test vectors from BIP324 (stratospher) a312877 test: Add ellswift unit tests (stratospher) 714fb2c test: Add python ellswift implementation to test framework (stratospher) Pull request description: Built on top of bitcoin#26222. This PR introduces Elligator swift encoding and decoding in the functional test framework. It's used in bitcoin#24748 for writing p2p encryption tests. ACKs for top commit: sipa: ACK 4f4d039 theStack: ACK 4f4d039 🐊 Tree-SHA512: 32bc8e88f715f2cd67dc04cd38db92680872072cb3775478e2c30da89aa2da2742992779ea14da2f1faca09228942cfbd86d6957402b24bf560244b389e03540
…ation 96b3f2d test: add unit test coverage for Python ECDSA implementation (Sebastian Falbesoner) Pull request description: This PR adds missing unit test coverage for the Python ECDSA implementation, which should be useful for detecting potential problems early whenever changes in the test framework's Python implementation of secp256k1 are made (e.g. #26222). Note that right now we don't call `ECPubKey.verify_ecdsa` anywhere in our tests, so we wouldn't notice if it is broken at some point. To keep it simple, the already existing unit test for Schnorr signatures is extended to also check ECDSA signatures. For that purpose, the dictionary storing private-key/public-key entries use their legacy types `ECKey/ECPubKey` instead of bare byte-arrays, and for Schnorr signing/verification the necessary conversions (ECKey -> bare private key, ECPubKey -> x-only pubkey) is done later when needed. To avoid code duplication, a helper function `random_bitflip` for damaging signatures is introduced. The unit test can be run by either calling it for this single module: `$ python3 -m unittest ./test/functional/test_framework/key.py` or simply running `$ ./test/functional/test_runner.py` which calls all test framework module's unit tests at the start (see TEST_FRAMEWORK_MODULES list). ACKs for top commit: achow101: ACK 96b3f2d sipa: utACK 96b3f2d stratospher: tested ACK 96b3f2d. Tree-SHA512: b993f25b843fa047376addda4ce4b0f15750ffba926528b5cca4c5f99b9af456206f4e8af885d25a017dddddf382ddebf38765819b3d16a3f28810d03b010808
…plementation 96b3f2d test: add unit test coverage for Python ECDSA implementation (Sebastian Falbesoner) Pull request description: This PR adds missing unit test coverage for the Python ECDSA implementation, which should be useful for detecting potential problems early whenever changes in the test framework's Python implementation of secp256k1 are made (e.g. bitcoin#26222). Note that right now we don't call `ECPubKey.verify_ecdsa` anywhere in our tests, so we wouldn't notice if it is broken at some point. To keep it simple, the already existing unit test for Schnorr signatures is extended to also check ECDSA signatures. For that purpose, the dictionary storing private-key/public-key entries use their legacy types `ECKey/ECPubKey` instead of bare byte-arrays, and for Schnorr signing/verification the necessary conversions (ECKey -> bare private key, ECPubKey -> x-only pubkey) is done later when needed. To avoid code duplication, a helper function `random_bitflip` for damaging signatures is introduced. The unit test can be run by either calling it for this single module: `$ python3 -m unittest ./test/functional/test_framework/key.py` or simply running `$ ./test/functional/test_runner.py` which calls all test framework module's unit tests at the start (see TEST_FRAMEWORK_MODULES list). ACKs for top commit: achow101: ACK 96b3f2d sipa: utACK 96b3f2d stratospher: tested ACK 96b3f2d. Tree-SHA512: b993f25b843fa047376addda4ce4b0f15750ffba926528b5cca4c5f99b9af456206f4e8af885d25a017dddddf382ddebf38765819b3d16a3f28810d03b010808
…p classes to test framework notes: - excludes changes to test/functional/feature_taproot.py
…p classes to test framework notes: - excludes changes to test/functional/feature_taproot.py
…p classes to test framework notes: - excludes changes to test/functional/feature_taproot.py
…p classes to test framework notes: - excludes changes to test/functional/feature_taproot.py
…p classes to test framework notes: - excludes changes to test/functional/feature_taproot.py
backport: merge bitcoin#22029, bitcoin#27538, bitcoin#21100, bitcoin#27542, bitcoin#24005, partial bitcoin#19953, bitcoin#23394, bitcoin#20292, bitcoin#20161, bitcoin#20842, bitcoin#26222 (secp256k1 tests)
This PR rewrites a portion of
test_framework/key.py
, in a compatible way, by introducing classes that encapsulate field element and group element logic, in an attempt to be more readable and reusable.To maximize readability, the group element logic does not use Jacobian coordinates. Instead, group elements just store (affine) X and Y coordinates directly. To compensate for the performance loss this causes, field elements are represented as fractions. This undoes most, but not all, of the performance loss, and there is a few % slowdown (as measured in
feature_taproot.py
, which heavily uses this).The upside is that the implementation for group laws (point doubling, addition, subtraction, ...) is very close to the mathematical description of elliptic curves, and this extends to potential future extensions (e.g. ElligatorSwift as needed by #27479).