Skip to content

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

Merged
merged 2 commits into from
Jun 28, 2023

Conversation

sipa
Copy link
Member

@sipa sipa commented Oct 1, 2022

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

@fanquake fanquake added the Tests label Oct 1, 2022
@sipa sipa force-pushed the 202209_fe_ge_classes branch 2 times, most recently from 4d22863 to 4d6be31 Compare October 1, 2022 18:02
@DrahtBot
Copy link
Contributor

DrahtBot commented Oct 1, 2022

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

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK theStack, stratospher, achow101
Concept ACK hebasto, pinheadmz, real-or-random

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:

  • #27653 (test: add unit test coverage for Python ECDSA implementation by theStack)
  • #24005 (test: add python implementation of Elligator swift by stratospher)

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.

Copy link

@amovfx amovfx left a comment

Choose a reason for hiding this comment

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

Reviewed to learn.

@sipa
Copy link
Member Author

sipa commented Dec 13, 2022

Rebased.

@sipa sipa force-pushed the 202209_fe_ge_classes branch 2 times, most recently from 5801cd5 to 8851fea Compare December 13, 2022 16:35
@sipa sipa force-pushed the 202209_fe_ge_classes branch from 8851fea to 5b4477d Compare January 4, 2023 20:20
Copy link
Contributor

@theStack theStack 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, +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 :)

@sipa sipa mentioned this pull request May 12, 2023
43 tasks
@sipa sipa force-pushed the 202209_fe_ge_classes branch from 5b4477d to b1797e3 Compare May 12, 2023 07:18
@sipa sipa force-pushed the 202209_fe_ge_classes branch from 60e1938 to 28ab6c3 Compare May 12, 2023 07:35
@sipa
Copy link
Member Author

sipa commented May 12, 2023

@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 i*16^j*G for all i=0..15, and j=0..63 are precomputed, leaving us with ~63 point additions. An even more advanced approach is discussed in bitcoin-core/secp256k1#1058, if you're interested. All of that is IMO out of scope for the test framework, though.

@fanquake fanquake requested a review from stratospher May 12, 2023 08:48
@fanquake
Copy link
Member

cc @real-or-random

@sipa sipa force-pushed the 202209_fe_ge_classes branch 2 times, most recently from c573249 to 2745abf Compare May 31, 2023 18:49
@sipa
Copy link
Member Author

sipa commented May 31, 2023

Addressed review comments. I've also renamed FE.num and FE.den to FE._num and FE._den to mark them as private (to the extent that Python allows that). All interactions with these objects should be done through their methods.

@sipa sipa force-pushed the 202209_fe_ge_classes branch from 2745abf to ab30e81 Compare June 20, 2023 18:03
Copy link
Contributor

@theStack theStack left a comment

Choose a reason for hiding this comment

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

ACK ab30e81

sipa and others added 2 commits June 27, 2023 09:34
…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>
@sipa sipa force-pushed the 202209_fe_ge_classes branch from ab30e81 to d4fb58a Compare June 27, 2023 13:35
Copy link
Contributor

@theStack theStack left a 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)

Copy link
Contributor

@stratospher stratospher left a 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!

@fanquake fanquake requested a review from stickies-v June 28, 2023 10:21
@achow101
Copy link
Member

ACK d4fb58a

Much simpler to understand, and the more complicated stuff matches up with the descriptions in WIkipedia.

@achow101 achow101 merged commit 626d346 into bitcoin:master Jun 28, 2023
sidhujag pushed a commit to syscoin/syscoin that referenced this pull request Jun 30, 2023
…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
fanquake added a commit to bitcoin-core/gui that referenced this pull request Jun 30, 2023
…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
sidhujag pushed a commit to syscoin/syscoin that referenced this pull request Jul 1, 2023
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
achow101 added a commit that referenced this pull request Sep 29, 2023
…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
Frank-GER pushed a commit to syscoin/syscoin that referenced this pull request Oct 5, 2023
…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
kwvg added a commit to kwvg/dash that referenced this pull request Feb 14, 2024
…p classes to test framework

notes:
- excludes changes to test/functional/feature_taproot.py
kwvg added a commit to kwvg/dash that referenced this pull request Feb 14, 2024
…p classes to test framework

notes:
- excludes changes to test/functional/feature_taproot.py
kwvg added a commit to kwvg/dash that referenced this pull request Feb 14, 2024
…p classes to test framework

notes:
- excludes changes to test/functional/feature_taproot.py
kwvg added a commit to kwvg/dash that referenced this pull request Feb 15, 2024
…p classes to test framework

notes:
- excludes changes to test/functional/feature_taproot.py
PastaPastaPasta pushed a commit to kwvg/dash that referenced this pull request Feb 19, 2024
…p classes to test framework

notes:
- excludes changes to test/functional/feature_taproot.py
@bitcoin bitcoin locked and limited conversation to collaborators Jun 27, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.