From 2ba9dcd326e50c0996f1ff3720ce94a8eb00e6c1 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Fri, 3 Mar 2023 22:11:18 +0100 Subject: [PATCH 01/26] jubjub curve --- .pre-commit-config.yaml | 8 +- requirements-dev.txt | 2 +- zokrates_pycrypto/eddsa.py | 6 +- zokrates_pycrypto/jubjub.py | 208 ++++++++++++++++++++++++++++++ zokrates_pycrypto/jubjub_eddsa.py | 113 ++++++++++++++++ zokrates_pycrypto/jubjub_field.py | 134 +++++++++++++++++++ zokrates_pycrypto/utils.py | 3 + 7 files changed, 463 insertions(+), 11 deletions(-) create mode 100644 zokrates_pycrypto/jubjub.py create mode 100644 zokrates_pycrypto/jubjub_eddsa.py create mode 100644 zokrates_pycrypto/jubjub_field.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6749e0b..8dcfe2c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,15 +14,9 @@ repos: - id: name-tests-test - id: debug-statements - id: requirements-txt-fixer - - id: flake8 - args: [., --count, --select=E901,E999,F821,F822,F823, --show-source, --statistics] + # - id: flake8 - repo: https://github.com/stefandeml/pre_commit_hooks rev: 507a6c4e4bdb5ffc7d35c1227c177e7a9bb86965 hooks: - id: detect_tab - id: unittest -- repo: https://github.com/ambv/black - rev: 19.3b0 - hooks: - - id: black - language_version: python3.6 diff --git a/requirements-dev.txt b/requirements-dev.txt index fbca653..095263d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,5 +14,5 @@ pyflakes==2.1.1 pylint==2.3.1 six==1.12.0 toml==0.10.0 -typed-ast==1.3.2 +# typed-ast==1.3.2 wrapt==1.11.1 diff --git a/zokrates_pycrypto/eddsa.py b/zokrates_pycrypto/eddsa.py index 316ea34..d575311 100644 --- a/zokrates_pycrypto/eddsa.py +++ b/zokrates_pycrypto/eddsa.py @@ -60,13 +60,13 @@ def sign(self, msg, B=None): A = PublicKey.from_private(self) # A = kB M = msg - r = hash_to_scalar(self.fe, M) # r = H(k,M) mod L + r = hash_to_scalar(self.fe, M) % JUBJUB_L # r = H(k,M) mod L R = B.mult(r) # R = rB # Bind the message to the nonce, public key and message hRAM = hash_to_scalar(R, A, M) key_field = self.fe.n - S = (r + (key_field * hRAM)) % JUBJUB_E # r + (H(R,A,M) * k) + S = (r + (key_field * hRAM)) # r + (H(R,A,M) * k) return (R, S) @@ -110,4 +110,4 @@ def hash_to_scalar(*args): """ p = b"".join(to_bytes(_) for _ in args) digest = hashlib.sha256(p).digest() - return int(digest.hex(), 16) # mod JUBJUB_E here for optimized implementation + return int(digest.hex(), 16) % JUBJUB_E # mod JUBJUB_E here for optimized implementation diff --git a/zokrates_pycrypto/jubjub.py b/zokrates_pycrypto/jubjub.py new file mode 100644 index 0000000..671ee37 --- /dev/null +++ b/zokrates_pycrypto/jubjub.py @@ -0,0 +1,208 @@ +""" +This module implements the extended twisted edwards and extended affine coordinates +described in the paper "Twisted Edwards Curves Revisited": + - https://iacr.org/archive/asiacrypt2008/53500329/53500329.pdf + Huseyin Hisil, Kenneth Koon-Ho Wong, Gary Carter, and Ed Dawson + +based on: https://github.com/HarryR/ethsnarks +""" + +from collections import namedtuple +from .jubjub_field import FQ, inv, field_modulus +from .numbertheory import square_root_mod_prime, SquareRootError + +# order of the field +JUBJUB_Q = field_modulus +# order of the curve +JUBJUB_E = 21888242871839275222246405745257275088614511777268538073601725287587578984328 +JUBJUB_C = 8 # Cofactor +JUBJUB_L = 6554484396890773809930967563523245729705921265872317281365359162392183254199 # C*L == E +JUBJUB_A = -1 # Coefficient A +JUBJUB_D = 19257038036680949359750312669786877991949435402254120286184196891950884077233 # Coefficient D + + +def is_negative(v): + assert isinstance(v, FQ) + return v.n < (-v).n + + +class Point(namedtuple("_Point", ("x", "y"))): + def valid(self): + """ + Satisfies the relationship + ax^2 + y^2 = 1 + d x^2 y^2 + """ + xsq = self.x * self.x + ysq = self.y * self.y + return (JUBJUB_A * xsq) + ysq == (1 + JUBJUB_D * xsq * ysq) + + def add(self, other): + assert isinstance(other, Point) + if self.x == 0 and self.y == 0: + return other + (u1, v1) = (self.x, self.y) + (u2, v2) = (other.x, other.y) + u3 = (u1 * v2 + v1 * u2) / (FQ.one() + JUBJUB_D * u1 * u2 * v1 * v2) + v3 = (v1 * v2 - JUBJUB_A * u1 * u2) / (FQ.one() - JUBJUB_D * u1 * u2 * v1 * v2) + return Point(u3, v3) + + def mult(self, scalar): + if isinstance(scalar, FQ): + scalar = scalar.n + p = self + a = self.infinity() + i = 0 + while scalar != 0: + if (scalar & 1) != 0: + a = a.add(p) + p = p.double() + scalar = scalar // 2 + i += 1 + return a + + def neg(self): + """ + Twisted Edwards Curves, BBJLP-2008, section 2 pg 2 + """ + return Point(-self.x, self.y) + + @classmethod + def generator(cls): + x = 11076627216317271660298050606127911965867021807910416450833192264015104452986 + y = 44412834903739585386157632289020980010620626017712148233229312325549216099227 + return Point(FQ(x), FQ(y)) + + @staticmethod + def infinity(): + return Point(FQ(0), FQ(1)) + + def __str__(self): + return "x: {}, y:{}".format(*self) + + def __eq__(self, other): + return self.x == other.x and self.y == other.y + + def __neg__(self): + return self.neg() + + def __add__(self, other): + return self.add(other) + + def __sub__(self, other): + return self.add(other.neg()) + + def __mul__(self, n): + return self.mult(n) + + def double(self): + return self.add(self) + + @classmethod + def from_x(cls, x): + """ + y^2 = ((a * x^2) / (d * x^2 - 1)) - (1 / (d * x^2 - 1)) + For every x coordinate, there are two possible points: (x, y) and (x, -y) + """ + assert isinstance(x, FQ) + xsq = x * x + ax2 = JUBJUB_A * xsq + dxsqm1 = inv(JUBJUB_D * xsq - 1, JUBJUB_Q) + ysq = dxsqm1 * (ax2 - 1) + y = FQ(square_root_mod_prime(int(ysq), JUBJUB_Q)) + return cls(x, y) + + @classmethod + def from_y(cls, y, sign=None): + """ + x^2 = (y^2 - 1) / (d * y^2 - a) + """ + assert isinstance(y, FQ) + ysq = y * y + lhs = ysq - 1 + rhs = JUBJUB_D * ysq - JUBJUB_A + xsq = lhs / rhs + x = FQ(square_root_mod_prime(int(xsq), JUBJUB_Q)) + if sign is not None: + # Used for compress & decompress + if (x.n & 1) != sign: + x = -x + else: + if is_negative(x): + x = -x + return cls(x, y) + + @classmethod + def from_hash(cls, entropy): + """ + HashToPoint (or Point.from_hash) + + Parameters: + entropy (bytes): input entropy provided as byte array + + Hashes the input entropy and interprets the result as the Y coordinate + then recovers the X coordinate, if no valid point can be recovered + Y is incremented until a matching X coordinate is found. + The point is guaranteed to be prime order and not the identity. + From: https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/?include_text=1 + Page 6: + o HashToBase(x, i). This method is parametrized by p and H, where p + is the prime order of the base field Fp, and H is a cryptographic + hash function which outputs at least floor(log2(p)) + 2 bits. The + function first hashes x, converts the result to an integer, and + reduces modulo p to give an element of Fp. + """ + from hashlib import sha256 + + assert isinstance(entropy, bytes) + entropy = sha256(entropy).digest() + entropy_as_int = int.from_bytes(entropy, "big") + y = FQ(entropy_as_int) + while True: + try: + p = cls.from_y(y) + except SquareRootError: + y += 1 + continue + + # Multiply point by cofactor, ensures it's on the prime-order subgroup + p = p * JUBJUB_C + + # Verify point is on prime-ordered sub-group + if (p * JUBJUB_L) != Point.infinity(): + raise RuntimeError("Point not on prime-ordered subgroup") + + return p + + def compress(self): + x = self.x.n + y = self.y.n + # return int.to_bytes(y | ((x & 1) << 255), 32, "little") + return int.to_bytes(y | ((x & 1) << 255), 32, "big") + + @classmethod + def decompress(cls, point): + """ + From: https://ed25519.cr.yp.to/eddsa-20150704.pdf + + The encoding of F_q is used to define "negative" elements of F_q: + specifically, x is negative if the (b-1)-bit encoding of x is + lexiographically larger than the (b-1)-bit encoding of -x. In particular, + if q is prime and the (b-1)-bit encoding of F_q is the little-endian + encoding of {0, 1, ..., q-1}, then {1,3,5,...,q-2} are the negative element of F_q. + + This encoding is also used to define a b-bit encoding of each element `(x,y) ∈ E` + as a b-bit string (x,y), namely the (b-1)-bit encoding of y followed by the sign bit. + the sign bit is 1 if and only if x is negative. + + A parser recovers `(x,y)` from a b-bit string, while also verifying that `(x,y) ∈ E`, + as follows: parse the first b-1 bits as y, compute `xx = (y^2 - 1) / (dy^2 - a)`; + compute `x = [+ or -] sqrt(xx)` where the `[+ or -]` is chosen so that the sign of + `x` matches the `b`th bit of the string. if `xx` is not a square then parsing fails. + """ + if len(point) != 32: + raise ValueError("Invalid input length for decompression") + # y = int.from_bytes(point, "little") + y = int.from_bytes(point, "big") + sign = y >> 255 + y &= (1 << 255) - 1 + return cls.from_y(FQ(y), sign) diff --git a/zokrates_pycrypto/jubjub_eddsa.py b/zokrates_pycrypto/jubjub_eddsa.py new file mode 100644 index 0000000..24b82a1 --- /dev/null +++ b/zokrates_pycrypto/jubjub_eddsa.py @@ -0,0 +1,113 @@ +""" +This module implements EdDSA (https://en.wikipedia.org/wiki/EdDSA) signing and verification + +1) the signer has two secret values: + + * k = Secret key + * r = Per-(message,key) nonce + +2) the signer provides the verifier with their public key: + + * A = k*B + +3) the signer provides a signature consisting of two values: + + * R = Point, image of `r*B` + * s = Image of `r + (k*t)` + +The value `t` denotes the common reference string used by both parties: + * t = H(R, A, M) +where H() denotes a cryptographic hash function, SHA256 in this implementation. + +The nonce `r` is a random secret, and protects the value `s` from revealing the +signers secret key. + +4) the verifier can check the following statement: + `S*B = R + t*A` + +For further information see: https://eprint.iacr.org/2015/677.pdf +based on: https://github.com/HarryR/ethsnarks +""" + +import hashlib +from collections import namedtuple +from math import ceil, log2 +from os import urandom + +from .jubjub import JUBJUB_E, JUBJUB_L, JUBJUB_Q, Point +from .jubjub_field import FQ +from .utils import to_bytes + + +class PrivateKey(namedtuple("_PrivateKey", ("fe"))): + """ + Wraps field element + """ + + @classmethod + # FIXME: ethsnarks creates keys > 32bytes. Create issue. + def from_rand(cls): + mod = JUBJUB_L + # nbytes = ceil(ceil(log2(mod)) / 8) + 1 + nbytes = ceil(ceil(log2(mod)) / 8) + rand_n = int.from_bytes(urandom(nbytes), "little") + return cls(FQ(rand_n)) + + def sign(self, msg, B=None): + "Returns the signature (R,S) for a given private key and message." + B = B or Point.generator() + + A = PublicKey.from_private(self) # A = kB + + M = msg + r = hash_to_scalar(self.fe, M) % JUBJUB_L # r = H(k,M) mod L + R = B.mult(r) # R = rB + + # Bind the message to the nonce, public key and message + hRAM = hash_to_scalar(R, A, M) + key_field = self.fe.n + S = (r + (key_field * hRAM)) # r + (H(R,A,M) * k) + + return (R, S) + + +class PublicKey(namedtuple("_PublicKey", ("p"))): + """ + Wraps edwards point + """ + + @classmethod + def from_private(cls, sk, B=None): + "Returns public key for a private key. B denotes the group generator" + B = B or Point.generator() + if not isinstance(sk, PrivateKey): + sk = PrivateKey(sk) + A = B.mult(sk.fe) + return cls(A) + + def verify(self, sig, msg, B=None): + B = B or Point.generator() + + R, S = sig + M = msg + A = self.p + + lhs = B.mult(S) + + hRAM = hash_to_scalar(R, A, M) + rhs = (R + (A.mult(hRAM))) + + return lhs == rhs + + +def hash_to_scalar(*args): + """ + Hash the key and message to create `r`, the blinding factor for this signature. + If the same `r` value is used more than once, the key for the signature is revealed. + + Note that we take the entire 256bit hash digest as input for the scalar multiplication. + As the group is only of size JUBJUB_E (<256bit) we allow wrapping around the group modulo. + """ + p = b"".join(to_bytes(_) for _ in args) + digest = hashlib.sha256(p).digest() + return int(digest.hex(), 16) % JUBJUB_E # mod JUBJUB_E here for optimized implementation diff --git a/zokrates_pycrypto/jubjub_field.py b/zokrates_pycrypto/jubjub_field.py new file mode 100644 index 0000000..5f594ae --- /dev/null +++ b/zokrates_pycrypto/jubjub_field.py @@ -0,0 +1,134 @@ +""" +This code is copied from https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_curve.py +Author is Vitalik Buterin. +Unfortunately the field modulus is not generic in this implementation, hence we had to copy the file. +All changes from our side are denoted with #CHANGE. +""" + +from __future__ import absolute_import + +from typing import cast, List, Tuple, Sequence, Union + + +# The prime modulus of the field +# field_modulus = 21888242871839275222246405745257275088696311157297823662689037894645226208583 +field_modulus = ( + 52435875175126190479447740508185965837690552500527637822603658699938581184513 +) +# CHANGE: Changing the modulus to the embedded curve + +# See, it's prime! +assert pow(2, field_modulus, field_modulus) == 2 + +# The modulus of the polynomial in this representation of FQ12 +# FQ12_MODULUS_COEFFS = (82, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0) # Implied + [1] +# FQ2_MODULUS_COEFFS = (1, 0) +# CHANGE: No need for extended in this case + +# Extended euclidean algorithm to find modular inverses for +# integers +def inv(a: int, n: int) -> int: + if a == 0: + return 0 + lm, hm = 1, 0 + num = a if isinstance(a, int) else a.n + low, high = num % n, n + while low > 1: + r = high // low + nm, new = hm - lm * r, high - low * r + lm, low, hm, high = nm, new, lm, low + return lm % n + + +IntOrFQ = Union[int, "FQ"] + + +# A class for field elements in FQ. Wrap a number in this class, +# and it becomes a field element. +class FQ(object): + n = None # type: int + + def __init__(self, val: IntOrFQ) -> None: + if isinstance(val, FQ): + self.n = val.n + else: + self.n = val % field_modulus + assert isinstance(self.n, int) + + def __add__(self, other: IntOrFQ) -> "FQ": + on = other.n if isinstance(other, FQ) else other + return FQ((self.n + on) % field_modulus) + + def __mul__(self, other: IntOrFQ) -> "FQ": + on = other.n if isinstance(other, FQ) else other + return FQ((self.n * on) % field_modulus) + + def __rmul__(self, other: IntOrFQ) -> "FQ": + return self * other + + def __radd__(self, other: IntOrFQ) -> "FQ": + return self + other + + def __rsub__(self, other: IntOrFQ) -> "FQ": + on = other.n if isinstance(other, FQ) else other + return FQ((on - self.n) % field_modulus) + + def __sub__(self, other: IntOrFQ) -> "FQ": + on = other.n if isinstance(other, FQ) else other + return FQ((self.n - on) % field_modulus) + + def __div__(self, other: IntOrFQ) -> "FQ": + on = other.n if isinstance(other, FQ) else other + assert isinstance(on, int) + return FQ(self.n * inv(on, field_modulus) % field_modulus) + + def __truediv__(self, other: IntOrFQ) -> "FQ": + return self.__div__(other) + + def __rdiv__(self, other: IntOrFQ) -> "FQ": + on = other.n if isinstance(other, FQ) else other + assert isinstance(on, int), on + return FQ(inv(self.n, field_modulus) * on % field_modulus) + + def __rtruediv__(self, other: IntOrFQ) -> "FQ": + return self.__rdiv__(other) + + def __pow__(self, other: int) -> "FQ": + if other == 0: + return FQ(1) + elif other == 1: + return FQ(self.n) + elif other % 2 == 0: + return (self * self) ** (other // 2) + else: + return ((self * self) ** int(other // 2)) * self + + def __eq__( + self, other: IntOrFQ + ) -> bool: # type:ignore # https://github.com/python/mypy/issues/2783 # noqa: E501 + if isinstance(other, FQ): + return self.n == other.n + else: + return self.n == other + + def __ne__( + self, other: IntOrFQ + ) -> bool: # type:ignore # https://github.com/python/mypy/issues/2783 # noqa: E501 + return not self == other + + def __neg__(self) -> "FQ": + return FQ(-self.n) + + def __repr__(self) -> str: + return repr(self.n) + + def __int__(self) -> int: + return self.n + + @classmethod + def one(cls) -> "FQ": + return cls(1) + + @classmethod + def zero(cls) -> "FQ": + return cls(0) diff --git a/zokrates_pycrypto/utils.py b/zokrates_pycrypto/utils.py index 94ce92a..59608f8 100644 --- a/zokrates_pycrypto/utils.py +++ b/zokrates_pycrypto/utils.py @@ -2,6 +2,7 @@ from .babyjubjub import Point from .field import FQ +from .jubjub_field import FQ as JFQ import hashlib @@ -14,6 +15,8 @@ def to_bytes(*args): # result += to_bytes(M.y) elif isinstance(M, FQ): result += to_bytes(M.n) + elif isinstance(M, JFQ): + result += to_bytes(M.n) elif isinstance(M, int): result += M.to_bytes(32, "big") elif isinstance(M, BitArray): From 2b1777c219719aa457c3e91f31034aa16cd898f6 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Tue, 7 Mar 2023 16:31:51 +0100 Subject: [PATCH 02/26] add jubjub tests --- tests/jubjub_test.py | 76 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/jubjub_test.py diff --git a/tests/jubjub_test.py b/tests/jubjub_test.py new file mode 100644 index 0000000..343ec81 --- /dev/null +++ b/tests/jubjub_test.py @@ -0,0 +1,76 @@ +import unittest + +from os import urandom + +from zokrates_pycrypto.jubjub_field import FQ +from zokrates_pycrypto.jubjub_eddsa import Point +from zokrates_pycrypto.jubjub import JUBJUB_E, JUBJUB_C, JUBJUB_L + + +class TestJubjub(unittest.TestCase): + def _point_g(self): + return Point.generator() + + # Hardcoded for now till we have automatic test generation for ZoKrates test framework + def _fe_rnd(self): + return [FQ(1234), FQ(5678), FQ(7890)] + + def test_double(self): + G = self._point_g() + G_times_2 = G.mult(2) + G_dbl = G.add(G) + self.assertEqual(G_times_2, G_dbl) + + # test taken form: https://github.com/gtank/jubjub/blob/main/jubjub_test.go#L47 + def test_cyclic(self): + G = self._point_g() + scalar = 6554484396890773809930967563523245729705921265872317281365359162392183254199 + self.assertEqual(G.mult(JUBJUB_C).mult(scalar), G.infinity()) + + # TODO: find values for JubJub + # def test_lower_order_p(self): + # lp = Point( + # FQ( + # 4342719913949491028786768530115087822524712248835451589697801404893164183326 + # ), + # FQ( + # 4826523245007015323400664741523384119579596407052839571721035538011798951543 + # ), + # ) + # lp_c = lp.mult(JUBJUB_C) + # self.assertEqual(lp_c, Point.infinity()) + # lp_l = lp.mult(JUBJUB_L) + # self.assertEqual(lp_l, lp) + + def test_multiplicative(self): + G = self._point_g() + a, b, _ = self._fe_rnd() + A = G.mult(a) + B = G.mult(b) + + ab = (a.n * b.n) % JUBJUB_E # 7006652 + AB = G.mult(ab) + self.assertEqual(A.mult(b), AB) + self.assertEqual(B.mult(a), AB) + + def test_multiplicative_associativity(self): + G = self._point_g() + + a, b, c = self._fe_rnd() + + res1 = G.mult(a).mult(b).mult(c) + res2 = G.mult(b).mult(c).mult(a) + res3 = G.mult(c).mult(a).mult(b) + + self.assertEqual(res1, res2) + self.assertEqual(res2, res3) + self.assertEqual(res1, res3) + + def test_identities(self): + G = self._point_g() + self.assertEqual(G + Point.infinity(), G) + self.assertEqual(G + G.neg(), Point.infinity()) + + +if __name__ == "__main__": + unittest.main() From 2c9811b3c17b2064d57cc13666ca74666243b5ff Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Tue, 21 Mar 2023 16:17:27 +0100 Subject: [PATCH 03/26] scalar fields --- tests/babyjubjub_test.py | 6 +- tests/eddsa_test.py | 4 +- tests/field_test.py | 99 +++++++++++++ tests/jubjub_test.py | 4 +- tests/pedersenHasher_test.py | 2 +- zokrates_pycrypto/babyjubjub.py | 42 +++--- zokrates_pycrypto/eddsa.py | 6 +- zokrates_pycrypto/field.py | 146 ++++++++------------ zokrates_pycrypto/gadgets/pedersenHasher.py | 2 +- zokrates_pycrypto/jubjub.py | 20 +-- zokrates_pycrypto/jubjub_eddsa.py | 6 +- zokrates_pycrypto/jubjub_field.py | 134 ------------------ zokrates_pycrypto/utils.py | 3 - 13 files changed, 200 insertions(+), 274 deletions(-) create mode 100644 tests/field_test.py delete mode 100644 zokrates_pycrypto/jubjub_field.py diff --git a/tests/babyjubjub_test.py b/tests/babyjubjub_test.py index 26d0ec5..f217d47 100644 --- a/tests/babyjubjub_test.py +++ b/tests/babyjubjub_test.py @@ -2,7 +2,7 @@ from os import urandom -from zokrates_pycrypto.field import FQ +from zokrates_pycrypto.field import BN128Field as FQ from zokrates_pycrypto.babyjubjub import Point from zokrates_pycrypto.babyjubjub import JUBJUB_E, JUBJUB_C, JUBJUB_L @@ -54,8 +54,8 @@ def test_multiplicative(self): A = G.mult(a) B = G.mult(b) - ab = (a.n * b.n) % JUBJUB_E # 7006652 - AB = G.mult(ab) + ab = a.n * b.n % JUBJUB_E # 7006652 + AB = G.mult(FQ(ab)) self.assertEqual(A.mult(b), AB) self.assertEqual(B.mult(a), AB) diff --git a/tests/eddsa_test.py b/tests/eddsa_test.py index a87ae6b..f9ae73b 100644 --- a/tests/eddsa_test.py +++ b/tests/eddsa_test.py @@ -1,7 +1,7 @@ import unittest from os import urandom -from zokrates_pycrypto.field import FQ +from zokrates_pycrypto.field import BN128Field from zokrates_pycrypto.babyjubjub import Point from zokrates_pycrypto.eddsa import PublicKey, PrivateKey @@ -9,7 +9,7 @@ class TestEdDSA(unittest.TestCase): def test_signverify(self): # Hardcoded for now till we have automatic test generation for ZoKrates test framework - key = FQ( + key = BN128Field( 1997011358982923168928344992199991480689546837621580239342656433234255379025 ) diff --git a/tests/field_test.py b/tests/field_test.py new file mode 100644 index 0000000..10a2073 --- /dev/null +++ b/tests/field_test.py @@ -0,0 +1,99 @@ +import unittest + +from zokrates_pycrypto.field import FQ, BN128Field, BLS12_381Field + + +class TestField(unittest.TestCase): + + class F11(FQ): + FIELD = 11 + + def setUp(self): + self.zero = self.F11(0) + self.one = self.F11(1) + self.two = self.F11(2) + self.three = self.F11(3) + self.five = self.F11(5) + self.eight = self.F11(8) + + def test_zero(self): + zero = self.F11(11) + self.assertEqual(zero, self.zero) + + def test_one(self): + one = self.F11(12) + self.assertEqual(one, self.one) + + def test_equality(self): + self.assertEqual(self.one, self.F11(1)) + self.assertNotEqual(self.two, self.three) + self.assertNotEqual(BN128Field(1), BLS12_381Field(1)) + + def test_cyclic(self): + self.assertEqual(self.F11(12), self.one) + self.assertEqual(self.F11(23), self.one) + + def test_neg(self): + self.assertEqual(self.F11(-0), self.zero) + self.assertEqual(self.F11(-1), self.F11(10)) + self.assertEqual(-self.one, self.F11(10)) + self.assertEqual(-self.three, self.eight) + + def test_sum(self): + self.assertEqual(self.zero + self.one, self.one) + self.assertEqual(self.three + self.zero, self.three) + + self.assertEqual(self.three + self.five, self.eight) + self.assertEqual(self.five + self.three, self.eight) + + self.assertEqual(self.five + self.eight, self.two) + self.assertEqual(self.F11(12) + self.F11(23), self.two) + + def test_sub(self): + self.assertEqual(self.one - self.zero, self.one) + self.assertEqual(self.three - self.zero, self.three) + + self.assertEqual(self.three - self.five, self.F11(9)) + self.assertEqual(self.five - self.three, self.two) + + self.assertEqual(self.F11(12) - self.F11(23), self.zero) + + def test_mult(self): + self.assertEqual(self.zero * self.one, self.zero) + self.assertEqual(self.eight * self.one, self.eight) + self.assertEqual(self.three * self.three, self.F11(9)) + self.assertEqual(self.three * self.five, self.F11(4)) + + def test_div(self): + self.assertEqual(self.zero / self.three, self.zero) + self.assertEqual(self.eight / self.one, self.eight) + self.assertEqual(self.F11(9) / self.three, self.three) + + def test_inv(self): + self.assertEqual(self.zero.inv(), self.zero) + self.assertEqual(self.one.inv(), self.one) + self.assertEqual(self.eight.inv() * self.eight, self.one) + + def test_power(self): + self.assertEqual(self.zero ** 10, self.zero) + self.assertEqual(self.one ** 10, self.one) + self.assertEqual(self.three ** 0, self.one) + self.assertEqual(self.three ** 1, self.three) + self.assertEqual(self.three ** 3, self.five) + + def test_associativity(self): + res1 = (self.three + self.five) + self.eight + res2 = self.three + (self.five + self.eight) + self.assertEqual(res1, res2) + + res1 = (self.three * self.five) * self.eight + res2 = self.three * (self.five * self.eight) + self.assertEqual(res1, res2) + + def test_distributivity(self): + res1 = (self.three + self.five) * self.eight + res2 = (self.three * self.eight) + (self.five * self.eight) + self.assertEqual(res1, res2) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/jubjub_test.py b/tests/jubjub_test.py index 343ec81..fc04073 100644 --- a/tests/jubjub_test.py +++ b/tests/jubjub_test.py @@ -2,7 +2,7 @@ from os import urandom -from zokrates_pycrypto.jubjub_field import FQ +from zokrates_pycrypto.field import BLS12_381Field as FQ from zokrates_pycrypto.jubjub_eddsa import Point from zokrates_pycrypto.jubjub import JUBJUB_E, JUBJUB_C, JUBJUB_L @@ -49,7 +49,7 @@ def test_multiplicative(self): B = G.mult(b) ab = (a.n * b.n) % JUBJUB_E # 7006652 - AB = G.mult(ab) + AB = G.mult(FQ(ab)) self.assertEqual(A.mult(b), AB) self.assertEqual(B.mult(a), AB) diff --git a/tests/pedersenHasher_test.py b/tests/pedersenHasher_test.py index 3b7a1ab..396005c 100644 --- a/tests/pedersenHasher_test.py +++ b/tests/pedersenHasher_test.py @@ -3,7 +3,7 @@ from os import urandom from random import randint -from zokrates_pycrypto.babyjubjub import Point, FQ +from zokrates_pycrypto.babyjubjub import Point,BN128Field as FQ from zokrates_pycrypto.gadgets.pedersenHasher import PedersenHasher as P diff --git a/zokrates_pycrypto/babyjubjub.py b/zokrates_pycrypto/babyjubjub.py index d3689be..1beef04 100644 --- a/zokrates_pycrypto/babyjubjub.py +++ b/zokrates_pycrypto/babyjubjub.py @@ -8,21 +8,21 @@ """ from collections import namedtuple -from .field import FQ, inv, field_modulus +from .field import FQ, BN128Field from .numbertheory import square_root_mod_prime, SquareRootError # order of the field -JUBJUB_Q = field_modulus +JUBJUB_Q = BN128Field.FIELD # order of the curve JUBJUB_E = 21888242871839275222246405745257275088614511777268538073601725287587578984328 -JUBJUB_C = 8 # Cofactor -JUBJUB_L = JUBJUB_E // JUBJUB_C # C*L == E -JUBJUB_A = 168700 # Coefficient A -JUBJUB_D = 168696 # Coefficient D +JUBJUB_C = BN128Field(8) # Cofactor +JUBJUB_L = BN128Field(JUBJUB_E) / JUBJUB_C # C*L == E +JUBJUB_A = BN128Field(168700) # Coefficient A +JUBJUB_D = BN128Field(168696) # Coefficient D def is_negative(v): - assert isinstance(v, FQ) + assert isinstance(v, FQ), f"given type: {type(v)}" return v.n < (-v).n @@ -34,7 +34,7 @@ def valid(self): """ xsq = self.x * self.x ysq = self.y * self.y - return (JUBJUB_A * xsq) + ysq == (1 + JUBJUB_D * xsq * ysq) + return (JUBJUB_A * xsq) + ysq == (BN128Field(1) + JUBJUB_D * xsq * ysq) def add(self, other): assert isinstance(other, Point) @@ -42,8 +42,8 @@ def add(self, other): return other (u1, v1) = (self.x, self.y) (u2, v2) = (other.x, other.y) - u3 = (u1 * v2 + v1 * u2) / (FQ.one() + JUBJUB_D * u1 * u2 * v1 * v2) - v3 = (v1 * v2 - JUBJUB_A * u1 * u2) / (FQ.one() - JUBJUB_D * u1 * u2 * v1 * v2) + u3 = (u1 * v2 + v1 * u2) / (BN128Field(1) + JUBJUB_D * u1 * u2 * v1 * v2) + v3 = (v1 * v2 - JUBJUB_A * u1 * u2) / (BN128Field(1) - JUBJUB_D * u1 * u2 * v1 * v2) return Point(u3, v3) def mult(self, scalar): @@ -70,11 +70,11 @@ def neg(self): def generator(cls): x = 16540640123574156134436876038791482806971768689494387082833631921987005038935 y = 20819045374670962167435360035096875258406992893633759881276124905556507972311 - return Point(FQ(x), FQ(y)) + return Point(BN128Field(x), BN128Field(y)) @staticmethod def infinity(): - return Point(FQ(0), FQ(1)) + return Point(BN128Field(0), BN128Field(1)) def __str__(self): return "x: {}, y:{}".format(*self) @@ -103,12 +103,12 @@ def from_x(cls, x): y^2 = ((a * x^2) / (d * x^2 - 1)) - (1 / (d * x^2 - 1)) For every x coordinate, there are two possible points: (x, y) and (x, -y) """ - assert isinstance(x, FQ) + assert isinstance(x, BN128Field) xsq = x * x ax2 = JUBJUB_A * xsq - dxsqm1 = inv(JUBJUB_D * xsq - 1, JUBJUB_Q) - ysq = dxsqm1 * (ax2 - 1) - y = FQ(square_root_mod_prime(int(ysq), JUBJUB_Q)) + dxsqm1 = (JUBJUB_D * xsq - BN128Field(1)).inv() + ysq = dxsqm1 * (ax2 - BN128Field(1)) + y = square_root_mod_prime(int(ysq), JUBJUB_Q) return cls(x, y) @classmethod @@ -118,10 +118,10 @@ def from_y(cls, y, sign=None): """ assert isinstance(y, FQ) ysq = y * y - lhs = ysq - 1 + lhs = ysq - BN128Field(1) rhs = JUBJUB_D * ysq - JUBJUB_A xsq = lhs / rhs - x = FQ(square_root_mod_prime(int(xsq), JUBJUB_Q)) + x = BN128Field(square_root_mod_prime(int(xsq), JUBJUB_Q)) if sign is not None: # Used for compress & decompress if (x.n & 1) != sign: @@ -156,12 +156,12 @@ def from_hash(cls, entropy): assert isinstance(entropy, bytes) entropy = sha256(entropy).digest() entropy_as_int = int.from_bytes(entropy, "big") - y = FQ(entropy_as_int) + y = BN128Field(entropy_as_int) while True: try: p = cls.from_y(y) except SquareRootError: - y += 1 + y += BN128Field(1) continue # Multiply point by cofactor, ensures it's on the prime-order subgroup @@ -205,4 +205,4 @@ def decompress(cls, point): y = int.from_bytes(point, "big") sign = y >> 255 y &= (1 << 255) - 1 - return cls.from_y(FQ(y), sign) + return cls.from_y(BN128Field(y), sign) diff --git a/zokrates_pycrypto/eddsa.py b/zokrates_pycrypto/eddsa.py index d575311..40b4162 100644 --- a/zokrates_pycrypto/eddsa.py +++ b/zokrates_pycrypto/eddsa.py @@ -35,7 +35,7 @@ from os import urandom from .babyjubjub import JUBJUB_E, JUBJUB_L, JUBJUB_Q, Point -from .field import FQ +from .field import BN128Field as FQ from .utils import to_bytes @@ -47,7 +47,7 @@ class PrivateKey(namedtuple("_PrivateKey", ("fe"))): @classmethod # FIXME: ethsnarks creates keys > 32bytes. Create issue. def from_rand(cls): - mod = JUBJUB_L + mod = JUBJUB_L.n # nbytes = ceil(ceil(log2(mod)) / 8) + 1 nbytes = ceil(ceil(log2(mod)) / 8) rand_n = int.from_bytes(urandom(nbytes), "little") @@ -60,7 +60,7 @@ def sign(self, msg, B=None): A = PublicKey.from_private(self) # A = kB M = msg - r = hash_to_scalar(self.fe, M) % JUBJUB_L # r = H(k,M) mod L + r = hash_to_scalar(self.fe, M) % JUBJUB_L.n # r = H(k,M) mod L R = B.mult(r) # R = rB # Bind the message to the nonce, public key and message diff --git a/zokrates_pycrypto/field.py b/zokrates_pycrypto/field.py index 64be17d..fa42369 100644 --- a/zokrates_pycrypto/field.py +++ b/zokrates_pycrypto/field.py @@ -1,134 +1,98 @@ """ This code is copied from https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_curve.py Author is Vitalik Buterin. -Unfortunately the field modulus is not generic in this implementation, hence we had to copy the file. +Unfortunately the FIELD modulus is not generic in this implementation, hence we had to copy the file. All changes from our side are denoted with #CHANGE. """ from __future__ import absolute_import -from typing import cast, List, Tuple, Sequence, Union +from abc import ABC, abstractmethod -# The prime modulus of the field -# field_modulus = 21888242871839275222246405745257275088696311157297823662689037894645226208583 -field_modulus = ( - 21888242871839275222246405745257275088548364400416034343698204186575808495617 -) -# CHANGE: Changing the modulus to the embedded curve +# A class for FIELD elements in FQ. Wrap a number in this class, +# and it becomes a FIELD element. +class FQ(ABC): + FIELD = int + n: int -# See, it's prime! -assert pow(2, field_modulus, field_modulus) == 2 - -# The modulus of the polynomial in this representation of FQ12 -# FQ12_MODULUS_COEFFS = (82, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0) # Implied + [1] -# FQ2_MODULUS_COEFFS = (1, 0) -# CHANGE: No need for extended in this case - -# Extended euclidean algorithm to find modular inverses for -# integers -def inv(a: int, n: int) -> int: - if a == 0: - return 0 - lm, hm = 1, 0 - num = a if isinstance(a, int) else a.n - low, high = num % n, n - while low > 1: - r = high // low - nm, new = hm - lm * r, high - low * r - lm, low, hm, high = nm, new, lm, low - return lm % n - - -IntOrFQ = Union[int, "FQ"] - - -# A class for field elements in FQ. Wrap a number in this class, -# and it becomes a field element. -class FQ(object): - n = None # type: int - - def __init__(self, val: IntOrFQ) -> None: - if isinstance(val, FQ): - self.n = val.n - else: - self.n = val % field_modulus - assert isinstance(self.n, int) - - def __add__(self, other: IntOrFQ) -> "FQ": - on = other.n if isinstance(other, FQ) else other - return FQ((self.n + on) % field_modulus) - - def __mul__(self, other: IntOrFQ) -> "FQ": - on = other.n if isinstance(other, FQ) else other - return FQ((self.n * on) % field_modulus) - - def __rmul__(self, other: IntOrFQ) -> "FQ": - return self * other + @classmethod + @property + @abstractmethod + def FIELD(cls): + raise NotImplementedError - def __radd__(self, other: IntOrFQ) -> "FQ": - return self + other + def __init__(self, val: int) -> None: + assert isinstance(val, int) + self.n = val % self.FIELD - def __rsub__(self, other: IntOrFQ) -> "FQ": - on = other.n if isinstance(other, FQ) else other - return FQ((on - self.n) % field_modulus) + def __assert_field(self, other: "FQ"): + assert isinstance(other, FQ) + assert self.FIELD == other.FIELD - def __sub__(self, other: IntOrFQ) -> "FQ": - on = other.n if isinstance(other, FQ) else other - return FQ((self.n - on) % field_modulus) + def __int__(self): + return self.n - def __div__(self, other: IntOrFQ) -> "FQ": - on = other.n if isinstance(other, FQ) else other - assert isinstance(on, int) - return FQ(self.n * inv(on, field_modulus) % field_modulus) + def __add__(self, other: "FQ") -> "FQ": + self.__assert_field(other) + return type(self)(self.n + other.n) - def __truediv__(self, other: IntOrFQ) -> "FQ": - return self.__div__(other) + def __mul__(self, other: "FQ") -> "FQ": + self.__assert_field(other) + return type(self)(self.n * other.n) - def __rdiv__(self, other: IntOrFQ) -> "FQ": - on = other.n if isinstance(other, FQ) else other - assert isinstance(on, int), on - return FQ(inv(self.n, field_modulus) * on % field_modulus) + def __sub__(self, other: "FQ") -> "FQ": + self.__assert_field(other) + return type(self)(self.n - other.n) - def __rtruediv__(self, other: IntOrFQ) -> "FQ": - return self.__rdiv__(other) + def __truediv__(self, other: "FQ") -> "FQ": + self.__assert_field(other) + return self * other.inv() def __pow__(self, other: int) -> "FQ": if other == 0: - return FQ(1) + return type(self)(1) elif other == 1: - return FQ(self.n) + return self elif other % 2 == 0: return (self * self) ** (other // 2) else: return ((self * self) ** int(other // 2)) * self def __eq__( - self, other: IntOrFQ + self, other: "FQ" ) -> bool: # type:ignore # https://github.com/python/mypy/issues/2783 # noqa: E501 if isinstance(other, FQ): - return self.n == other.n + return self.n == other.n and self.FIELD == other.FIELD else: - return self.n == other + return False def __ne__( - self, other: IntOrFQ + self, other: "FQ" ) -> bool: # type:ignore # https://github.com/python/mypy/issues/2783 # noqa: E501 return not self == other def __neg__(self) -> "FQ": - return FQ(-self.n) + return type(self)(-self.n) def __repr__(self) -> str: return repr(self.n) - def __int__(self) -> int: - return self.n + def inv(self) -> "FQ": + if self.n == 0: + return type(self)(0) + lm, hm = 1, 0 + low, high = self.n % self.FIELD, self.FIELD + while low > 1: + r = high // low + nm, new = hm - lm * r, high - low * r + lm, low, hm, high = nm, new, lm, low + return type(self)(lm) - @classmethod - def one(cls) -> "FQ": - return cls(1) - @classmethod - def zero(cls) -> "FQ": - return cls(0) +class BN128Field(FQ): + FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617 + + +class BLS12_381Field(FQ): + FIELD = 52435875175126190479447740508185965837690552500527637822603658699938581184513 diff --git a/zokrates_pycrypto/gadgets/pedersenHasher.py b/zokrates_pycrypto/gadgets/pedersenHasher.py index 5768a48..af6dc99 100644 --- a/zokrates_pycrypto/gadgets/pedersenHasher.py +++ b/zokrates_pycrypto/gadgets/pedersenHasher.py @@ -4,7 +4,7 @@ from struct import pack from ..babyjubjub import Point, JUBJUB_L, JUBJUB_C -from ..field import FQ +from ..field import BN128Field as FQ WINDOW_SIZE_BITS = 2 # Size of the pre-computed look-up table diff --git a/zokrates_pycrypto/jubjub.py b/zokrates_pycrypto/jubjub.py index 671ee37..3c1ec6e 100644 --- a/zokrates_pycrypto/jubjub.py +++ b/zokrates_pycrypto/jubjub.py @@ -8,17 +8,17 @@ """ from collections import namedtuple -from .jubjub_field import FQ, inv, field_modulus +from .field import BLS12_381Field as FQ from .numbertheory import square_root_mod_prime, SquareRootError # order of the field -JUBJUB_Q = field_modulus +JUBJUB_Q = FQ.FIELD # order of the curve JUBJUB_E = 21888242871839275222246405745257275088614511777268538073601725287587578984328 -JUBJUB_C = 8 # Cofactor -JUBJUB_L = 6554484396890773809930967563523245729705921265872317281365359162392183254199 # C*L == E -JUBJUB_A = -1 # Coefficient A -JUBJUB_D = 19257038036680949359750312669786877991949435402254120286184196891950884077233 # Coefficient D +JUBJUB_C = FQ(8) # Cofactor +JUBJUB_L = FQ(6554484396890773809930967563523245729705921265872317281365359162392183254199) # C*L == E +JUBJUB_A = FQ(-1) # Coefficient A +JUBJUB_D = FQ(19257038036680949359750312669786877991949435402254120286184196891950884077233) # Coefficient D def is_negative(v): @@ -42,8 +42,8 @@ def add(self, other): return other (u1, v1) = (self.x, self.y) (u2, v2) = (other.x, other.y) - u3 = (u1 * v2 + v1 * u2) / (FQ.one() + JUBJUB_D * u1 * u2 * v1 * v2) - v3 = (v1 * v2 - JUBJUB_A * u1 * u2) / (FQ.one() - JUBJUB_D * u1 * u2 * v1 * v2) + u3 = (u1 * v2 + v1 * u2) / (FQ(1) + JUBJUB_D * u1 * u2 * v1 * v2) + v3 = (v1 * v2 - JUBJUB_A * u1 * u2) / (FQ(1) - JUBJUB_D * u1 * u2 * v1 * v2) return Point(u3, v3) def mult(self, scalar): @@ -106,9 +106,9 @@ def from_x(cls, x): assert isinstance(x, FQ) xsq = x * x ax2 = JUBJUB_A * xsq - dxsqm1 = inv(JUBJUB_D * xsq - 1, JUBJUB_Q) + dxsqm1 = (JUBJUB_D * xsq - FQ(1)).inv() ysq = dxsqm1 * (ax2 - 1) - y = FQ(square_root_mod_prime(int(ysq), JUBJUB_Q)) + y = square_root_mod_prime(int(ysq), JUBJUB_Q) return cls(x, y) @classmethod diff --git a/zokrates_pycrypto/jubjub_eddsa.py b/zokrates_pycrypto/jubjub_eddsa.py index 24b82a1..0444802 100644 --- a/zokrates_pycrypto/jubjub_eddsa.py +++ b/zokrates_pycrypto/jubjub_eddsa.py @@ -35,7 +35,7 @@ from os import urandom from .jubjub import JUBJUB_E, JUBJUB_L, JUBJUB_Q, Point -from .jubjub_field import FQ +from .field import BLS12_381Field as FQ from .utils import to_bytes @@ -47,7 +47,7 @@ class PrivateKey(namedtuple("_PrivateKey", ("fe"))): @classmethod # FIXME: ethsnarks creates keys > 32bytes. Create issue. def from_rand(cls): - mod = JUBJUB_L + mod = JUBJUB_L.n # nbytes = ceil(ceil(log2(mod)) / 8) + 1 nbytes = ceil(ceil(log2(mod)) / 8) rand_n = int.from_bytes(urandom(nbytes), "little") @@ -60,7 +60,7 @@ def sign(self, msg, B=None): A = PublicKey.from_private(self) # A = kB M = msg - r = hash_to_scalar(self.fe, M) % JUBJUB_L # r = H(k,M) mod L + r = hash_to_scalar(self.fe, M) % JUBJUB_L.n # r = H(k,M) mod L R = B.mult(r) # R = rB # Bind the message to the nonce, public key and message diff --git a/zokrates_pycrypto/jubjub_field.py b/zokrates_pycrypto/jubjub_field.py deleted file mode 100644 index 5f594ae..0000000 --- a/zokrates_pycrypto/jubjub_field.py +++ /dev/null @@ -1,134 +0,0 @@ -""" -This code is copied from https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_curve.py -Author is Vitalik Buterin. -Unfortunately the field modulus is not generic in this implementation, hence we had to copy the file. -All changes from our side are denoted with #CHANGE. -""" - -from __future__ import absolute_import - -from typing import cast, List, Tuple, Sequence, Union - - -# The prime modulus of the field -# field_modulus = 21888242871839275222246405745257275088696311157297823662689037894645226208583 -field_modulus = ( - 52435875175126190479447740508185965837690552500527637822603658699938581184513 -) -# CHANGE: Changing the modulus to the embedded curve - -# See, it's prime! -assert pow(2, field_modulus, field_modulus) == 2 - -# The modulus of the polynomial in this representation of FQ12 -# FQ12_MODULUS_COEFFS = (82, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0) # Implied + [1] -# FQ2_MODULUS_COEFFS = (1, 0) -# CHANGE: No need for extended in this case - -# Extended euclidean algorithm to find modular inverses for -# integers -def inv(a: int, n: int) -> int: - if a == 0: - return 0 - lm, hm = 1, 0 - num = a if isinstance(a, int) else a.n - low, high = num % n, n - while low > 1: - r = high // low - nm, new = hm - lm * r, high - low * r - lm, low, hm, high = nm, new, lm, low - return lm % n - - -IntOrFQ = Union[int, "FQ"] - - -# A class for field elements in FQ. Wrap a number in this class, -# and it becomes a field element. -class FQ(object): - n = None # type: int - - def __init__(self, val: IntOrFQ) -> None: - if isinstance(val, FQ): - self.n = val.n - else: - self.n = val % field_modulus - assert isinstance(self.n, int) - - def __add__(self, other: IntOrFQ) -> "FQ": - on = other.n if isinstance(other, FQ) else other - return FQ((self.n + on) % field_modulus) - - def __mul__(self, other: IntOrFQ) -> "FQ": - on = other.n if isinstance(other, FQ) else other - return FQ((self.n * on) % field_modulus) - - def __rmul__(self, other: IntOrFQ) -> "FQ": - return self * other - - def __radd__(self, other: IntOrFQ) -> "FQ": - return self + other - - def __rsub__(self, other: IntOrFQ) -> "FQ": - on = other.n if isinstance(other, FQ) else other - return FQ((on - self.n) % field_modulus) - - def __sub__(self, other: IntOrFQ) -> "FQ": - on = other.n if isinstance(other, FQ) else other - return FQ((self.n - on) % field_modulus) - - def __div__(self, other: IntOrFQ) -> "FQ": - on = other.n if isinstance(other, FQ) else other - assert isinstance(on, int) - return FQ(self.n * inv(on, field_modulus) % field_modulus) - - def __truediv__(self, other: IntOrFQ) -> "FQ": - return self.__div__(other) - - def __rdiv__(self, other: IntOrFQ) -> "FQ": - on = other.n if isinstance(other, FQ) else other - assert isinstance(on, int), on - return FQ(inv(self.n, field_modulus) * on % field_modulus) - - def __rtruediv__(self, other: IntOrFQ) -> "FQ": - return self.__rdiv__(other) - - def __pow__(self, other: int) -> "FQ": - if other == 0: - return FQ(1) - elif other == 1: - return FQ(self.n) - elif other % 2 == 0: - return (self * self) ** (other // 2) - else: - return ((self * self) ** int(other // 2)) * self - - def __eq__( - self, other: IntOrFQ - ) -> bool: # type:ignore # https://github.com/python/mypy/issues/2783 # noqa: E501 - if isinstance(other, FQ): - return self.n == other.n - else: - return self.n == other - - def __ne__( - self, other: IntOrFQ - ) -> bool: # type:ignore # https://github.com/python/mypy/issues/2783 # noqa: E501 - return not self == other - - def __neg__(self) -> "FQ": - return FQ(-self.n) - - def __repr__(self) -> str: - return repr(self.n) - - def __int__(self) -> int: - return self.n - - @classmethod - def one(cls) -> "FQ": - return cls(1) - - @classmethod - def zero(cls) -> "FQ": - return cls(0) diff --git a/zokrates_pycrypto/utils.py b/zokrates_pycrypto/utils.py index 59608f8..94ce92a 100644 --- a/zokrates_pycrypto/utils.py +++ b/zokrates_pycrypto/utils.py @@ -2,7 +2,6 @@ from .babyjubjub import Point from .field import FQ -from .jubjub_field import FQ as JFQ import hashlib @@ -15,8 +14,6 @@ def to_bytes(*args): # result += to_bytes(M.y) elif isinstance(M, FQ): result += to_bytes(M.n) - elif isinstance(M, JFQ): - result += to_bytes(M.n) elif isinstance(M, int): result += M.to_bytes(32, "big") elif isinstance(M, BitArray): From 6f272b48805aacc16684ba2120b2200310a312be Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Tue, 11 Apr 2023 15:39:19 +0200 Subject: [PATCH 04/26] edwardscurve refactor --- tests/babyjubjub_test.py | 25 +-- tests/eddsa_test.py | 3 +- tests/field_test.py | 2 +- tests/jubjub_test.py | 21 +- tests/pedersenHasher_test.py | 14 +- .../{babyjubjub.py => curves.py} | 188 ++++++++++++---- zokrates_pycrypto/eddsa.py | 16 +- zokrates_pycrypto/{field.py => fields.py} | 0 zokrates_pycrypto/gadgets/pedersenHasher.py | 10 +- zokrates_pycrypto/jubjub.py | 208 ------------------ zokrates_pycrypto/jubjub_eddsa.py | 16 +- zokrates_pycrypto/utils.py | 6 +- 12 files changed, 199 insertions(+), 310 deletions(-) rename zokrates_pycrypto/{babyjubjub.py => curves.py} (56%) rename zokrates_pycrypto/{field.py => fields.py} (100%) delete mode 100644 zokrates_pycrypto/jubjub.py diff --git a/tests/babyjubjub_test.py b/tests/babyjubjub_test.py index f217d47..e118fe5 100644 --- a/tests/babyjubjub_test.py +++ b/tests/babyjubjub_test.py @@ -2,19 +2,18 @@ from os import urandom -from zokrates_pycrypto.field import BN128Field as FQ -from zokrates_pycrypto.babyjubjub import Point -from zokrates_pycrypto.babyjubjub import JUBJUB_E, JUBJUB_C, JUBJUB_L +from zokrates_pycrypto.fields import BN128Field as FQ +from zokrates_pycrypto.curves import BabyJubJub class TestJubjub(unittest.TestCase): def _point_g(self): - return Point.generator() + return BabyJubJub.generator() def _point_g_dbl(self): x = 17324563846726889236817837922625232543153115346355010501047597319863650987830 y = 20022170825455209233733649024450576091402881793145646502279487074566492066831 - return Point(FQ(x), FQ(y)) + return BabyJubJub(FQ(x), FQ(y)) # Hardcoded for now till we have automatic test generation for ZoKrates test framework def _fe_rnd(self): @@ -27,7 +26,7 @@ def test_double_via_add(self): def test_cyclic(self): G = self._point_g() - self.assertEqual(G.mult(JUBJUB_E + 1), G) + self.assertEqual(G.mult(BabyJubJub.JUBJUB_E + 1), G) def test_mult_2(self): G = self._point_g() @@ -35,7 +34,7 @@ def test_mult_2(self): self.assertEqual(G_mult2, self._point_g_dbl()) def test_lower_order_p(self): - lp = Point( + lp = BabyJubJub( FQ( 4342719913949491028786768530115087822524712248835451589697801404893164183326 ), @@ -43,9 +42,9 @@ def test_lower_order_p(self): 4826523245007015323400664741523384119579596407052839571721035538011798951543 ), ) - lp_c = lp.mult(JUBJUB_C) - self.assertEqual(lp_c, Point.infinity()) - lp_l = lp.mult(JUBJUB_L) + lp_c = lp.mult(BabyJubJub.JUBJUB_C) + self.assertEqual(lp_c, BabyJubJub.infinity()) + lp_l = lp.mult(BabyJubJub.JUBJUB_L) self.assertEqual(lp_l, lp) def test_multiplicative(self): @@ -54,7 +53,7 @@ def test_multiplicative(self): A = G.mult(a) B = G.mult(b) - ab = a.n * b.n % JUBJUB_E # 7006652 + ab = a.n * b.n % BabyJubJub.JUBJUB_E # 7006652 AB = G.mult(FQ(ab)) self.assertEqual(A.mult(b), AB) self.assertEqual(B.mult(a), AB) @@ -74,8 +73,8 @@ def test_associativity(self): def test_identities(self): G = self._point_g() - self.assertEqual(G + Point.infinity(), G) - self.assertEqual(G + G.neg(), Point.infinity()) + self.assertEqual(G + BabyJubJub.infinity(), G) + self.assertEqual(G + G.neg(), BabyJubJub.infinity()) if __name__ == "__main__": diff --git a/tests/eddsa_test.py b/tests/eddsa_test.py index f9ae73b..9393819 100644 --- a/tests/eddsa_test.py +++ b/tests/eddsa_test.py @@ -1,8 +1,7 @@ import unittest from os import urandom -from zokrates_pycrypto.field import BN128Field -from zokrates_pycrypto.babyjubjub import Point +from zokrates_pycrypto.fields import BN128Field from zokrates_pycrypto.eddsa import PublicKey, PrivateKey diff --git a/tests/field_test.py b/tests/field_test.py index 10a2073..d494539 100644 --- a/tests/field_test.py +++ b/tests/field_test.py @@ -1,6 +1,6 @@ import unittest -from zokrates_pycrypto.field import FQ, BN128Field, BLS12_381Field +from zokrates_pycrypto.fields import FQ, BN128Field, BLS12_381Field class TestField(unittest.TestCase): diff --git a/tests/jubjub_test.py b/tests/jubjub_test.py index fc04073..85c9276 100644 --- a/tests/jubjub_test.py +++ b/tests/jubjub_test.py @@ -2,14 +2,17 @@ from os import urandom -from zokrates_pycrypto.field import BLS12_381Field as FQ -from zokrates_pycrypto.jubjub_eddsa import Point -from zokrates_pycrypto.jubjub import JUBJUB_E, JUBJUB_C, JUBJUB_L +from zokrates_pycrypto.fields import BLS12_381Field as FQ +from zokrates_pycrypto.curves import JubJub + + +JUBJUB_C = JubJub.JUBJUB_C +JUBJUB_E = JubJub.JUBJUB_E class TestJubjub(unittest.TestCase): def _point_g(self): - return Point.generator() + return JubJub.generator() # Hardcoded for now till we have automatic test generation for ZoKrates test framework def _fe_rnd(self): @@ -25,11 +28,11 @@ def test_double(self): def test_cyclic(self): G = self._point_g() scalar = 6554484396890773809930967563523245729705921265872317281365359162392183254199 - self.assertEqual(G.mult(JUBJUB_C).mult(scalar), G.infinity()) + self.assertEqual(G.mult(JUBJUB_C).mult(scalar), JubJub.infinity()) # TODO: find values for JubJub # def test_lower_order_p(self): - # lp = Point( + # lp = JubJub( # FQ( # 4342719913949491028786768530115087822524712248835451589697801404893164183326 # ), @@ -38,7 +41,7 @@ def test_cyclic(self): # ), # ) # lp_c = lp.mult(JUBJUB_C) - # self.assertEqual(lp_c, Point.infinity()) + # self.assertEqual(lp_c, JubJub.infinity()) # lp_l = lp.mult(JUBJUB_L) # self.assertEqual(lp_l, lp) @@ -68,8 +71,8 @@ def test_multiplicative_associativity(self): def test_identities(self): G = self._point_g() - self.assertEqual(G + Point.infinity(), G) - self.assertEqual(G + G.neg(), Point.infinity()) + self.assertEqual(G + JubJub.infinity(), G) + self.assertEqual(G + G.neg(), JubJub.infinity()) if __name__ == "__main__": diff --git a/tests/pedersenHasher_test.py b/tests/pedersenHasher_test.py index 396005c..b8caa1b 100644 --- a/tests/pedersenHasher_test.py +++ b/tests/pedersenHasher_test.py @@ -3,7 +3,7 @@ from os import urandom from random import randint -from zokrates_pycrypto.babyjubjub import Point,BN128Field as FQ +from zokrates_pycrypto.curves import BabyJubJub, BN128Field as FQ from zokrates_pycrypto.gadgets.pedersenHasher import PedersenHasher as P @@ -20,7 +20,7 @@ def test_zcash(self): def test_hash_scalars_known(self): self.assertEqual( P(b"test").hash_scalars(267), - Point( + BabyJubJub( FQ( 6790798216812059804926342266703617627640027902964190490794793207272357201212 ), @@ -34,7 +34,7 @@ def test_hash_scalars_known(self): P(b"test").hash_scalars( 6453482891510615431577168724743356132495662554103773572771861111634748265227 ), - Point( + BabyJubJub( FQ( 6545697115159207040330446958704617656199928059562637738348733874272425400594 ), @@ -48,7 +48,7 @@ def test_hash_scalars_known(self): P(b"test").hash_scalars( 21888242871839275222246405745257275088548364400416034343698204186575808495616 ), - Point( + BabyJubJub( FQ( 16322787121012335146141962340685388833598805940095898416175167744309692564601 ), @@ -61,7 +61,7 @@ def test_hash_scalars_known(self): def test_hash_bytes_known(self): self.assertEqual( P(b"test").hash_bytes(b"abc"), - Point( + BabyJubJub( FQ( 9869277320722751484529016080276887338184240285836102740267608137843906399765 ), @@ -73,7 +73,7 @@ def test_hash_bytes_known(self): self.assertEqual( P(b"test").hash_bytes(b"abcdefghijklmnopqrstuvwx"), - Point( + BabyJubJub( FQ( 3966548799068703226441887746390766667253943354008248106643296790753369303077 ), @@ -88,7 +88,7 @@ def test_hash_bits_known(self): P(b"EdDSA_Verify.RAM").hash_bits( "101100110011111001100100101100010100011010100100001011101001000100100000001111101101111001001010111011101101011010010101101101101000000010000000101010110100011110101110111100111100011110110011100101011000000000110101111001110000101011011110100100011110010000110111010011000001000100101100101111001100100010110101100010001000000101111011011010010011110001110111101011110001111111100010010000110101000001010111000111011110111010010010000101110000011001111000101010001101100000110111111110011001110101011000110010111111000101001100010001011011101010101011101010110000111100101000000110011000011001101000001010110110010000110101011111100010111011100110111101110111011001001110100100110010100111001000001010101010010100010100101101000010100010000111110101111000101110" ), - Point( + BabyJubJub( FQ( 16391910732431349989910402670442677728780476741314399751389577385062806845560 ), diff --git a/zokrates_pycrypto/babyjubjub.py b/zokrates_pycrypto/curves.py similarity index 56% rename from zokrates_pycrypto/babyjubjub.py rename to zokrates_pycrypto/curves.py index 1beef04..9d998ed 100644 --- a/zokrates_pycrypto/babyjubjub.py +++ b/zokrates_pycrypto/curves.py @@ -7,26 +7,82 @@ based on: https://github.com/HarryR/ethsnarks """ -from collections import namedtuple -from .field import FQ, BN128Field +from abc import ABC, abstractmethod +from .fields import FQ, BN128Field, BLS12_381Field from .numbertheory import square_root_mod_prime, SquareRootError -# order of the field -JUBJUB_Q = BN128Field.FIELD -# order of the curve -JUBJUB_E = 21888242871839275222246405745257275088614511777268538073601725287587578984328 -JUBJUB_C = BN128Field(8) # Cofactor -JUBJUB_L = BN128Field(JUBJUB_E) / JUBJUB_C # C*L == E -JUBJUB_A = BN128Field(168700) # Coefficient A -JUBJUB_D = BN128Field(168696) # Coefficient D - def is_negative(v): assert isinstance(v, FQ), f"given type: {type(v)}" return v.n < (-v).n -class Point(namedtuple("_Point", ("x", "y"))): +class EdwardsCurve(ABC): + FIELD_TYPE: type + JUBJUB_Q: int + JUBJUB_E: int + JUBJUB_C: FQ + JUBJUB_L: FQ + JUBJUB_A: FQ + JUBJUB_D: FQ + + def __init__(self, x: FQ, y: FQ): + assert type(x) == type(y) and type(x) == self.FIELD_TYPE + self.x = x + self.y = y + + @classmethod + @property + @abstractmethod + def FIELD_TYPE(cls): + raise NotImplementedError + + @classmethod + @property + @abstractmethod + def JUBJUB_Q(cls): + raise NotImplementedError + + @classmethod + @property + @abstractmethod + def JUBJUB_E(cls): + raise NotImplementedError + + @classmethod + @property + @abstractmethod + def JUBJUB_C(cls): + raise NotImplementedError + + @classmethod + @property + @abstractmethod + def JUBJUB_L(cls): + raise NotImplementedError + + @classmethod + @property + @abstractmethod + def JUBJUB_A(cls): + raise NotImplementedError + + @classmethod + @property + @abstractmethod + def JUBJUB_D(cls): + raise NotImplementedError + + @classmethod + @abstractmethod + def generator(cls): + raise NotImplementedError + + @staticmethod + @abstractmethod + def infinity(): + raise NotImplementedError + def valid(self): """ Satisfies the relationship @@ -34,23 +90,23 @@ def valid(self): """ xsq = self.x * self.x ysq = self.y * self.y - return (JUBJUB_A * xsq) + ysq == (BN128Field(1) + JUBJUB_D * xsq * ysq) + return (self.JUBJUB_A * xsq) + ysq == (self.FIELD_TYPE(1) + self.JUBJUB_D * xsq * ysq) def add(self, other): - assert isinstance(other, Point) + assert isinstance(other, type(self)) if self.x == 0 and self.y == 0: return other (u1, v1) = (self.x, self.y) (u2, v2) = (other.x, other.y) - u3 = (u1 * v2 + v1 * u2) / (BN128Field(1) + JUBJUB_D * u1 * u2 * v1 * v2) - v3 = (v1 * v2 - JUBJUB_A * u1 * u2) / (BN128Field(1) - JUBJUB_D * u1 * u2 * v1 * v2) - return Point(u3, v3) + u3 = (u1 * v2 + v1 * u2) / (self.FIELD_TYPE(1) + self.JUBJUB_D * u1 * u2 * v1 * v2) + v3 = (v1 * v2 - self.JUBJUB_A * u1 * u2) / (self.FIELD_TYPE(1) - self.JUBJUB_D * u1 * u2 * v1 * v2) + return type(self)(u3, v3) def mult(self, scalar): if isinstance(scalar, FQ): scalar = scalar.n p = self - a = self.infinity() + a = type(self).infinity() i = 0 while scalar != 0: if (scalar & 1) != 0: @@ -64,20 +120,10 @@ def neg(self): """ Twisted Edwards Curves, BBJLP-2008, section 2 pg 2 """ - return Point(-self.x, self.y) - - @classmethod - def generator(cls): - x = 16540640123574156134436876038791482806971768689494387082833631921987005038935 - y = 20819045374670962167435360035096875258406992893633759881276124905556507972311 - return Point(BN128Field(x), BN128Field(y)) - - @staticmethod - def infinity(): - return Point(BN128Field(0), BN128Field(1)) + return type(self)(-self.x, self.y) def __str__(self): - return "x: {}, y:{}".format(*self) + return "x: {}, y:{}".format(self.x, self.y) def __eq__(self, other): return self.x == other.x and self.y == other.y @@ -103,12 +149,12 @@ def from_x(cls, x): y^2 = ((a * x^2) / (d * x^2 - 1)) - (1 / (d * x^2 - 1)) For every x coordinate, there are two possible points: (x, y) and (x, -y) """ - assert isinstance(x, BN128Field) + assert isinstance(x, cls.FIELD_TYPE) xsq = x * x - ax2 = JUBJUB_A * xsq - dxsqm1 = (JUBJUB_D * xsq - BN128Field(1)).inv() - ysq = dxsqm1 * (ax2 - BN128Field(1)) - y = square_root_mod_prime(int(ysq), JUBJUB_Q) + ax2 = cls.JUBJUB_A * xsq + dxsqm1 = (cls.JUBJUB_D * xsq - cls.FIELD_TYPE(1)).inv() + ysq = dxsqm1 * (ax2 - cls.FIELD_TYPE(1)) + y = square_root_mod_prime(int(ysq), cls.JUBJUB_Q) return cls(x, y) @classmethod @@ -116,12 +162,12 @@ def from_y(cls, y, sign=None): """ x^2 = (y^2 - 1) / (d * y^2 - a) """ - assert isinstance(y, FQ) + assert isinstance(y, cls.FIELD_TYPE) ysq = y * y - lhs = ysq - BN128Field(1) - rhs = JUBJUB_D * ysq - JUBJUB_A + lhs = ysq - cls.FIELD_TYPE(1) + rhs = cls.JUBJUB_D * ysq - cls.JUBJUB_A xsq = lhs / rhs - x = BN128Field(square_root_mod_prime(int(xsq), JUBJUB_Q)) + x = cls.FIELD_TYPE(square_root_mod_prime(int(xsq), cls.JUBJUB_Q)) if sign is not None: # Used for compress & decompress if (x.n & 1) != sign: @@ -134,7 +180,7 @@ def from_y(cls, y, sign=None): @classmethod def from_hash(cls, entropy): """ - HashToPoint (or Point.from_hash) + HashToEdwardsCurve (or EdwardsCurve.from_hash) Parameters: entropy (bytes): input entropy provided as byte array @@ -156,20 +202,20 @@ def from_hash(cls, entropy): assert isinstance(entropy, bytes) entropy = sha256(entropy).digest() entropy_as_int = int.from_bytes(entropy, "big") - y = BN128Field(entropy_as_int) + y = cls.FIELD_TYPE(entropy_as_int) while True: try: p = cls.from_y(y) except SquareRootError: - y += BN128Field(1) + y += cls.FIELD_TYPE(1) continue # Multiply point by cofactor, ensures it's on the prime-order subgroup - p = p * JUBJUB_C + p = p * cls.JUBJUB_C # Verify point is on prime-ordered sub-group - if (p * JUBJUB_L) != Point.infinity(): - raise RuntimeError("Point not on prime-ordered subgroup") + if (p * cls.JUBJUB_L) != type(p).infinity(): + raise RuntimeError("EdwardsCurve not on prime-ordered subgroup") return p @@ -205,4 +251,54 @@ def decompress(cls, point): y = int.from_bytes(point, "big") sign = y >> 255 y &= (1 << 255) - 1 - return cls.from_y(BN128Field(y), sign) + return cls.from_y(cls.FIELD_TYPE(y), sign) + + +class BabyJubJub(EdwardsCurve): + FIELD_TYPE = BN128Field + # order of the field + JUBJUB_Q = BN128Field.FIELD + # order of the curve + JUBJUB_E = 21888242871839275222246405745257275088614511777268538073601725287587578984328 + JUBJUB_C = BN128Field(8) # Cofactor + JUBJUB_L = BN128Field(JUBJUB_E) / JUBJUB_C # C*L == E + JUBJUB_A = BN128Field(168700) # Coefficient A + JUBJUB_D = BN128Field(168696) # Coefficient D + + def __init__(self, x: BN128Field, y: BN128Field): + super().__init__(x, y) + + @classmethod + def generator(cls): + x = 16540640123574156134436876038791482806971768689494387082833631921987005038935 + y = 20819045374670962167435360035096875258406992893633759881276124905556507972311 + return cls(BN128Field(x), BN128Field(y)) + + @staticmethod + def infinity(): + return BabyJubJub(BN128Field(0), BN128Field(1)) + + +class JubJub(EdwardsCurve): + FIELD_TYPE = BLS12_381Field + # order of the field + JUBJUB_Q = BLS12_381Field.FIELD + # order of the curve + JUBJUB_E = 21888242871839275222246405745257275088614511777268538073601725287587578984328 + JUBJUB_C = BLS12_381Field(8) # Cofactor + JUBJUB_L = BLS12_381Field(6554484396890773809930967563523245729705921265872317281365359162392183254199) + JUBJUB_A = BLS12_381Field(-1) # Coefficient A + JUBJUB_D = BLS12_381Field(19257038036680949359750312669786877991949435402254120286184196891950884077233) # Coefficient D + + def __init__(self, x: BLS12_381Field, y: BLS12_381Field): + super().__init__(x, y) + + @classmethod + def generator(cls): + x = 11076627216317271660298050606127911965867021807910416450833192264015104452986 + y = 44412834903739585386157632289020980010620626017712148233229312325549216099227 + return cls(BLS12_381Field(x), BLS12_381Field(y)) + + @staticmethod + def infinity(): + return JubJub(BLS12_381Field(0), BLS12_381Field(1)) diff --git a/zokrates_pycrypto/eddsa.py b/zokrates_pycrypto/eddsa.py index 40b4162..37ef65e 100644 --- a/zokrates_pycrypto/eddsa.py +++ b/zokrates_pycrypto/eddsa.py @@ -34,8 +34,8 @@ from math import ceil, log2 from os import urandom -from .babyjubjub import JUBJUB_E, JUBJUB_L, JUBJUB_Q, Point -from .field import BN128Field as FQ +from .curves import BabyJubJub +from .fields import BN128Field as FQ from .utils import to_bytes @@ -47,7 +47,7 @@ class PrivateKey(namedtuple("_PrivateKey", ("fe"))): @classmethod # FIXME: ethsnarks creates keys > 32bytes. Create issue. def from_rand(cls): - mod = JUBJUB_L.n + mod = BabyJubJub.JUBJUB_L.n # nbytes = ceil(ceil(log2(mod)) / 8) + 1 nbytes = ceil(ceil(log2(mod)) / 8) rand_n = int.from_bytes(urandom(nbytes), "little") @@ -55,12 +55,12 @@ def from_rand(cls): def sign(self, msg, B=None): "Returns the signature (R,S) for a given private key and message." - B = B or Point.generator() + B = B or BabyJubJub.generator() A = PublicKey.from_private(self) # A = kB M = msg - r = hash_to_scalar(self.fe, M) % JUBJUB_L.n # r = H(k,M) mod L + r = hash_to_scalar(self.fe, M) % BabyJubJub.JUBJUB_L.n # r = H(k,M) mod L R = B.mult(r) # R = rB # Bind the message to the nonce, public key and message @@ -79,14 +79,14 @@ class PublicKey(namedtuple("_PublicKey", ("p"))): @classmethod def from_private(cls, sk, B=None): "Returns public key for a private key. B denotes the group generator" - B = B or Point.generator() + B = B or BabyJubJub.generator() if not isinstance(sk, PrivateKey): sk = PrivateKey(sk) A = B.mult(sk.fe) return cls(A) def verify(self, sig, msg, B=None): - B = B or Point.generator() + B = B or BabyJubJub.generator() R, S = sig M = msg @@ -110,4 +110,4 @@ def hash_to_scalar(*args): """ p = b"".join(to_bytes(_) for _ in args) digest = hashlib.sha256(p).digest() - return int(digest.hex(), 16) % JUBJUB_E # mod JUBJUB_E here for optimized implementation + return int(digest.hex(), 16) % BabyJubJub.JUBJUB_E # mod JUBJUB_E here for optimized implementation diff --git a/zokrates_pycrypto/field.py b/zokrates_pycrypto/fields.py similarity index 100% rename from zokrates_pycrypto/field.py rename to zokrates_pycrypto/fields.py diff --git a/zokrates_pycrypto/gadgets/pedersenHasher.py b/zokrates_pycrypto/gadgets/pedersenHasher.py index af6dc99..cd34f05 100644 --- a/zokrates_pycrypto/gadgets/pedersenHasher.py +++ b/zokrates_pycrypto/gadgets/pedersenHasher.py @@ -3,8 +3,8 @@ from math import floor, log2 from struct import pack -from ..babyjubjub import Point, JUBJUB_L, JUBJUB_C -from ..field import BN128Field as FQ +from ..curves import BabyJubJub +from ..fields import BN128Field as FQ WINDOW_SIZE_BITS = 2 # Size of the pre-computed look-up table @@ -14,7 +14,7 @@ def pedersen_hash_basepoint(name, i): Create a base point for use with the windowed Pedersen hash function. The name and sequence numbers are used as a unique identifier. - Then HashToPoint is run on the name+seq to get the base point. + Then HashToEdwardsCurve is run on the name+seq to get the base point. """ if not isinstance(name, bytes): if isinstance(name, str): @@ -26,7 +26,7 @@ def pedersen_hash_basepoint(name, i): if len(name) > 28: raise ValueError("Name too long") data = b"%-28s%04X" % (name, i) - return Point.from_hash(data) + return BabyJubJub.from_hash(data) def windows_to_dsl_array(windows): @@ -96,7 +96,7 @@ def __hash_windows(self, windows, witness): if witness: return windows_to_dsl_array(windows) - result = Point.infinity() + result = BabyJubJub.infinity() for (g, window) in zip(self.generators, windows): segment = g * ((window & 0b11) + 1) if window > 0b11: diff --git a/zokrates_pycrypto/jubjub.py b/zokrates_pycrypto/jubjub.py deleted file mode 100644 index 3c1ec6e..0000000 --- a/zokrates_pycrypto/jubjub.py +++ /dev/null @@ -1,208 +0,0 @@ -""" -This module implements the extended twisted edwards and extended affine coordinates -described in the paper "Twisted Edwards Curves Revisited": - - https://iacr.org/archive/asiacrypt2008/53500329/53500329.pdf - Huseyin Hisil, Kenneth Koon-Ho Wong, Gary Carter, and Ed Dawson - -based on: https://github.com/HarryR/ethsnarks -""" - -from collections import namedtuple -from .field import BLS12_381Field as FQ -from .numbertheory import square_root_mod_prime, SquareRootError - -# order of the field -JUBJUB_Q = FQ.FIELD -# order of the curve -JUBJUB_E = 21888242871839275222246405745257275088614511777268538073601725287587578984328 -JUBJUB_C = FQ(8) # Cofactor -JUBJUB_L = FQ(6554484396890773809930967563523245729705921265872317281365359162392183254199) # C*L == E -JUBJUB_A = FQ(-1) # Coefficient A -JUBJUB_D = FQ(19257038036680949359750312669786877991949435402254120286184196891950884077233) # Coefficient D - - -def is_negative(v): - assert isinstance(v, FQ) - return v.n < (-v).n - - -class Point(namedtuple("_Point", ("x", "y"))): - def valid(self): - """ - Satisfies the relationship - ax^2 + y^2 = 1 + d x^2 y^2 - """ - xsq = self.x * self.x - ysq = self.y * self.y - return (JUBJUB_A * xsq) + ysq == (1 + JUBJUB_D * xsq * ysq) - - def add(self, other): - assert isinstance(other, Point) - if self.x == 0 and self.y == 0: - return other - (u1, v1) = (self.x, self.y) - (u2, v2) = (other.x, other.y) - u3 = (u1 * v2 + v1 * u2) / (FQ(1) + JUBJUB_D * u1 * u2 * v1 * v2) - v3 = (v1 * v2 - JUBJUB_A * u1 * u2) / (FQ(1) - JUBJUB_D * u1 * u2 * v1 * v2) - return Point(u3, v3) - - def mult(self, scalar): - if isinstance(scalar, FQ): - scalar = scalar.n - p = self - a = self.infinity() - i = 0 - while scalar != 0: - if (scalar & 1) != 0: - a = a.add(p) - p = p.double() - scalar = scalar // 2 - i += 1 - return a - - def neg(self): - """ - Twisted Edwards Curves, BBJLP-2008, section 2 pg 2 - """ - return Point(-self.x, self.y) - - @classmethod - def generator(cls): - x = 11076627216317271660298050606127911965867021807910416450833192264015104452986 - y = 44412834903739585386157632289020980010620626017712148233229312325549216099227 - return Point(FQ(x), FQ(y)) - - @staticmethod - def infinity(): - return Point(FQ(0), FQ(1)) - - def __str__(self): - return "x: {}, y:{}".format(*self) - - def __eq__(self, other): - return self.x == other.x and self.y == other.y - - def __neg__(self): - return self.neg() - - def __add__(self, other): - return self.add(other) - - def __sub__(self, other): - return self.add(other.neg()) - - def __mul__(self, n): - return self.mult(n) - - def double(self): - return self.add(self) - - @classmethod - def from_x(cls, x): - """ - y^2 = ((a * x^2) / (d * x^2 - 1)) - (1 / (d * x^2 - 1)) - For every x coordinate, there are two possible points: (x, y) and (x, -y) - """ - assert isinstance(x, FQ) - xsq = x * x - ax2 = JUBJUB_A * xsq - dxsqm1 = (JUBJUB_D * xsq - FQ(1)).inv() - ysq = dxsqm1 * (ax2 - 1) - y = square_root_mod_prime(int(ysq), JUBJUB_Q) - return cls(x, y) - - @classmethod - def from_y(cls, y, sign=None): - """ - x^2 = (y^2 - 1) / (d * y^2 - a) - """ - assert isinstance(y, FQ) - ysq = y * y - lhs = ysq - 1 - rhs = JUBJUB_D * ysq - JUBJUB_A - xsq = lhs / rhs - x = FQ(square_root_mod_prime(int(xsq), JUBJUB_Q)) - if sign is not None: - # Used for compress & decompress - if (x.n & 1) != sign: - x = -x - else: - if is_negative(x): - x = -x - return cls(x, y) - - @classmethod - def from_hash(cls, entropy): - """ - HashToPoint (or Point.from_hash) - - Parameters: - entropy (bytes): input entropy provided as byte array - - Hashes the input entropy and interprets the result as the Y coordinate - then recovers the X coordinate, if no valid point can be recovered - Y is incremented until a matching X coordinate is found. - The point is guaranteed to be prime order and not the identity. - From: https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/?include_text=1 - Page 6: - o HashToBase(x, i). This method is parametrized by p and H, where p - is the prime order of the base field Fp, and H is a cryptographic - hash function which outputs at least floor(log2(p)) + 2 bits. The - function first hashes x, converts the result to an integer, and - reduces modulo p to give an element of Fp. - """ - from hashlib import sha256 - - assert isinstance(entropy, bytes) - entropy = sha256(entropy).digest() - entropy_as_int = int.from_bytes(entropy, "big") - y = FQ(entropy_as_int) - while True: - try: - p = cls.from_y(y) - except SquareRootError: - y += 1 - continue - - # Multiply point by cofactor, ensures it's on the prime-order subgroup - p = p * JUBJUB_C - - # Verify point is on prime-ordered sub-group - if (p * JUBJUB_L) != Point.infinity(): - raise RuntimeError("Point not on prime-ordered subgroup") - - return p - - def compress(self): - x = self.x.n - y = self.y.n - # return int.to_bytes(y | ((x & 1) << 255), 32, "little") - return int.to_bytes(y | ((x & 1) << 255), 32, "big") - - @classmethod - def decompress(cls, point): - """ - From: https://ed25519.cr.yp.to/eddsa-20150704.pdf - - The encoding of F_q is used to define "negative" elements of F_q: - specifically, x is negative if the (b-1)-bit encoding of x is - lexiographically larger than the (b-1)-bit encoding of -x. In particular, - if q is prime and the (b-1)-bit encoding of F_q is the little-endian - encoding of {0, 1, ..., q-1}, then {1,3,5,...,q-2} are the negative element of F_q. - - This encoding is also used to define a b-bit encoding of each element `(x,y) ∈ E` - as a b-bit string (x,y), namely the (b-1)-bit encoding of y followed by the sign bit. - the sign bit is 1 if and only if x is negative. - - A parser recovers `(x,y)` from a b-bit string, while also verifying that `(x,y) ∈ E`, - as follows: parse the first b-1 bits as y, compute `xx = (y^2 - 1) / (dy^2 - a)`; - compute `x = [+ or -] sqrt(xx)` where the `[+ or -]` is chosen so that the sign of - `x` matches the `b`th bit of the string. if `xx` is not a square then parsing fails. - """ - if len(point) != 32: - raise ValueError("Invalid input length for decompression") - # y = int.from_bytes(point, "little") - y = int.from_bytes(point, "big") - sign = y >> 255 - y &= (1 << 255) - 1 - return cls.from_y(FQ(y), sign) diff --git a/zokrates_pycrypto/jubjub_eddsa.py b/zokrates_pycrypto/jubjub_eddsa.py index 0444802..e85d706 100644 --- a/zokrates_pycrypto/jubjub_eddsa.py +++ b/zokrates_pycrypto/jubjub_eddsa.py @@ -34,8 +34,8 @@ from math import ceil, log2 from os import urandom -from .jubjub import JUBJUB_E, JUBJUB_L, JUBJUB_Q, Point -from .field import BLS12_381Field as FQ +from .curves import JubJub +from .fields import BLS12_381Field as FQ from .utils import to_bytes @@ -47,7 +47,7 @@ class PrivateKey(namedtuple("_PrivateKey", ("fe"))): @classmethod # FIXME: ethsnarks creates keys > 32bytes. Create issue. def from_rand(cls): - mod = JUBJUB_L.n + mod = JubJub.JUBJUB_L.n # nbytes = ceil(ceil(log2(mod)) / 8) + 1 nbytes = ceil(ceil(log2(mod)) / 8) rand_n = int.from_bytes(urandom(nbytes), "little") @@ -55,12 +55,12 @@ def from_rand(cls): def sign(self, msg, B=None): "Returns the signature (R,S) for a given private key and message." - B = B or Point.generator() + B = B or JubJub.generator() A = PublicKey.from_private(self) # A = kB M = msg - r = hash_to_scalar(self.fe, M) % JUBJUB_L.n # r = H(k,M) mod L + r = hash_to_scalar(self.fe, M) % JubJub.JUBJUB_L.n # r = H(k,M) mod L R = B.mult(r) # R = rB # Bind the message to the nonce, public key and message @@ -79,14 +79,14 @@ class PublicKey(namedtuple("_PublicKey", ("p"))): @classmethod def from_private(cls, sk, B=None): "Returns public key for a private key. B denotes the group generator" - B = B or Point.generator() + B = B or JubJub.generator() if not isinstance(sk, PrivateKey): sk = PrivateKey(sk) A = B.mult(sk.fe) return cls(A) def verify(self, sig, msg, B=None): - B = B or Point.generator() + B = B or JubJub.generator() R, S = sig M = msg @@ -110,4 +110,4 @@ def hash_to_scalar(*args): """ p = b"".join(to_bytes(_) for _ in args) digest = hashlib.sha256(p).digest() - return int(digest.hex(), 16) % JUBJUB_E # mod JUBJUB_E here for optimized implementation + return int(digest.hex(), 16) % JubJub.JUBJUB_E # mod JUBJUB_E here for optimized implementation diff --git a/zokrates_pycrypto/utils.py b/zokrates_pycrypto/utils.py index 94ce92a..e0d0ad9 100644 --- a/zokrates_pycrypto/utils.py +++ b/zokrates_pycrypto/utils.py @@ -1,7 +1,7 @@ from bitstring import BitArray -from .babyjubjub import Point -from .field import FQ +from .curves import EdwardsCurve +from .fields import FQ import hashlib @@ -9,7 +9,7 @@ def to_bytes(*args): "Returns byte representation for objects used in this module." result = b"" for M in args: - if isinstance(M, Point): + if isinstance(M, EdwardsCurve): result += to_bytes(M.x) # result += to_bytes(M.y) elif isinstance(M, FQ): From 8f61784b5611052b38727f33e7c378d92e4bb056 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Tue, 11 Apr 2023 15:52:23 +0200 Subject: [PATCH 05/26] #2 add sources und update values of jubjub curve --- zokrates_pycrypto/curves.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zokrates_pycrypto/curves.py b/zokrates_pycrypto/curves.py index 9d998ed..b02f8bb 100644 --- a/zokrates_pycrypto/curves.py +++ b/zokrates_pycrypto/curves.py @@ -254,6 +254,7 @@ def decompress(cls, point): return cls.from_y(cls.FIELD_TYPE(y), sign) +# values taken from: https://github.com/barryWhiteHat/baby_jubjub class BabyJubJub(EdwardsCurve): FIELD_TYPE = BN128Field # order of the field @@ -279,12 +280,13 @@ def infinity(): return BabyJubJub(BN128Field(0), BN128Field(1)) +# values taken from: https://github.com/daira/jubjub class JubJub(EdwardsCurve): FIELD_TYPE = BLS12_381Field # order of the field JUBJUB_Q = BLS12_381Field.FIELD # order of the curve - JUBJUB_E = 21888242871839275222246405745257275088614511777268538073601725287587578984328 + JUBJUB_E = 52435875175126190479447740508185965837647370126978538250922873299137466033592 # C*L == E JUBJUB_C = BLS12_381Field(8) # Cofactor JUBJUB_L = BLS12_381Field(6554484396890773809930967563523245729705921265872317281365359162392183254199) JUBJUB_A = BLS12_381Field(-1) # Coefficient A From 9c934087eb24a0e4a8e64a3f53f1e9371bce0ae4 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Tue, 11 Apr 2023 17:56:23 +0200 Subject: [PATCH 06/26] #2 eddsa refactor done --- README.md | 8 +-- tests/eddsa_test.py | 8 +-- zokrates_pycrypto/eddsa.py | 73 +++++++++++-------- zokrates_pycrypto/jubjub_eddsa.py | 113 ------------------------------ zokrates_pycrypto/utils.py | 4 +- 5 files changed, 53 insertions(+), 153 deletions(-) delete mode 100644 zokrates_pycrypto/jubjub_eddsa.py diff --git a/README.md b/README.md index dc9411a..ff378f3 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,8 @@ Let's create a simple demo, called `demo.py`: ```python import hashlib +from zokrates_pycrypto.curves import BabyJubJub from zokrates_pycrypto.eddsa import PrivateKey, PublicKey -from zokrates_pycrypto.field import FQ from zokrates_pycrypto.utils import write_signature_for_zokrates_cli if __name__ == "__main__": @@ -82,8 +82,8 @@ if __name__ == "__main__": # sk = PrivateKey.from_rand() # Seeded for debug purpose - key = FQ(1997011358982923168928344992199991480689546837621580239342656433234255379025) - sk = PrivateKey(key) + key = 1997011358982923168928344992199991480689546837621580239342656433234255379025 + sk = PrivateKey(key, curve=BabyJubJub) sig = sk.sign(msg) pk = PublicKey.from_private(sk) @@ -112,7 +112,7 @@ import "ecc/babyjubjubParams.code" as context def main(private field[2] R, private field S, field[2] A, u32[8] M0, u32[8] M1) -> (bool): BabyJubJubParams context = context() - + bool isVerified = verifyEddsa(R, S, A, M0, M1, context) return isVerified diff --git a/tests/eddsa_test.py b/tests/eddsa_test.py index 9393819..5b9bf48 100644 --- a/tests/eddsa_test.py +++ b/tests/eddsa_test.py @@ -1,18 +1,16 @@ import unittest from os import urandom -from zokrates_pycrypto.fields import BN128Field +from zokrates_pycrypto.curves import BabyJubJub from zokrates_pycrypto.eddsa import PublicKey, PrivateKey class TestEdDSA(unittest.TestCase): def test_signverify(self): # Hardcoded for now till we have automatic test generation for ZoKrates test framework - key = BN128Field( - 1997011358982923168928344992199991480689546837621580239342656433234255379025 - ) + key = 1997011358982923168928344992199991480689546837621580239342656433234255379025 - sk = PrivateKey(key) + sk = PrivateKey(key, curve=BabyJubJub) msg = urandom(32) sig = sk.sign(msg) diff --git a/zokrates_pycrypto/eddsa.py b/zokrates_pycrypto/eddsa.py index 37ef65e..fe57d4d 100644 --- a/zokrates_pycrypto/eddsa.py +++ b/zokrates_pycrypto/eddsa.py @@ -33,81 +33,96 @@ from collections import namedtuple from math import ceil, log2 from os import urandom +from abc import ABCMeta -from .curves import BabyJubJub -from .fields import BN128Field as FQ +from .curves import EdwardsCurve, BabyJubJub, JubJub +from .fields import FQ, BN128Field, BLS12_381Field from .utils import to_bytes -class PrivateKey(namedtuple("_PrivateKey", ("fe"))): +class PrivateKey: """ Wraps field element """ + def __init__(self, sk: int, curve: ABCMeta): + if curve == BabyJubJub: + field = BN128Field + elif curve == BLS12_381Field: + field = BLS12_381Field + else: + raise ValueError('Edwardscurve not supported') + self.curve = curve + self.fe = field(sk) @classmethod # FIXME: ethsnarks creates keys > 32bytes. Create issue. - def from_rand(cls): - mod = BabyJubJub.JUBJUB_L.n + def from_rand(cls, sk: int, curve: ABCMeta): + mod = curve.JUBJUB_L.n # nbytes = ceil(ceil(log2(mod)) / 8) + 1 nbytes = ceil(ceil(log2(mod)) / 8) rand_n = int.from_bytes(urandom(nbytes), "little") - return cls(FQ(rand_n)) + return cls(rand_n, curve) - def sign(self, msg, B=None): + def sign(self, msg, B: EdwardsCurve = None): "Returns the signature (R,S) for a given private key and message." - B = B or BabyJubJub.generator() + B = B or self.curve.generator() A = PublicKey.from_private(self) # A = kB M = msg - r = hash_to_scalar(self.fe, M) % BabyJubJub.JUBJUB_L.n # r = H(k,M) mod L + r = A.hash_to_scalar(self.fe, M) % self.curve.JUBJUB_L.n # r = H(k,M) mod L R = B.mult(r) # R = rB # Bind the message to the nonce, public key and message - hRAM = hash_to_scalar(R, A, M) + hRAM = A.hash_to_scalar(R, A.point, M) key_field = self.fe.n S = (r + (key_field * hRAM)) # r + (H(R,A,M) * k) return (R, S) - -class PublicKey(namedtuple("_PublicKey", ("p"))): +class PublicKey: """ Wraps edwards point """ + def __init__(self, point: EdwardsCurve): + assert issubclass(type(point), EdwardsCurve) + self.point = point + self.curve = type(point) @classmethod - def from_private(cls, sk, B=None): + def from_private(cls, sk: PrivateKey, B=None): "Returns public key for a private key. B denotes the group generator" - B = B or BabyJubJub.generator() - if not isinstance(sk, PrivateKey): - sk = PrivateKey(sk) + assert isinstance(sk, PrivateKey) and issubclass(type(sk.fe), FQ) + curve = sk.curve + if B: + assert type(B) == type(curve) + B = B or curve.generator() A = B.mult(sk.fe) return cls(A) def verify(self, sig, msg, B=None): - B = B or BabyJubJub.generator() + B = B or self.curve.generator() R, S = sig M = msg - A = self.p + A = self.point lhs = B.mult(S) - hRAM = hash_to_scalar(R, A, M) + hRAM = self.hash_to_scalar(R, A, M) rhs = R + (A.mult(hRAM)) return lhs == rhs -def hash_to_scalar(*args): - """ - Hash the key and message to create `r`, the blinding factor for this signature. - If the same `r` value is used more than once, the key for the signature is revealed. + def hash_to_scalar(self, *args): + """ + Hash the key and message to create `r`, the blinding factor for this signature. + If the same `r` value is used more than once, the key for the signature is revealed. - Note that we take the entire 256bit hash digest as input for the scalar multiplication. - As the group is only of size JUBJUB_E (<256bit) we allow wrapping around the group modulo. - """ - p = b"".join(to_bytes(_) for _ in args) - digest = hashlib.sha256(p).digest() - return int(digest.hex(), 16) % BabyJubJub.JUBJUB_E # mod JUBJUB_E here for optimized implementation + Note that we take the entire 256bit hash digest as input for the scalar multiplication. + As the group is only of size JUBJUB_E (<256bit) we allow wrapping around the group modulo. + """ + p = b"".join(to_bytes(_) for _ in args) + digest = hashlib.sha256(p).digest() + return int(digest.hex(), 16) % self.curve.JUBJUB_E # mod JUBJUB_E here for optimized implementation diff --git a/zokrates_pycrypto/jubjub_eddsa.py b/zokrates_pycrypto/jubjub_eddsa.py deleted file mode 100644 index e85d706..0000000 --- a/zokrates_pycrypto/jubjub_eddsa.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -This module implements EdDSA (https://en.wikipedia.org/wiki/EdDSA) signing and verification - -1) the signer has two secret values: - - * k = Secret key - * r = Per-(message,key) nonce - -2) the signer provides the verifier with their public key: - - * A = k*B - -3) the signer provides a signature consisting of two values: - - * R = Point, image of `r*B` - * s = Image of `r + (k*t)` - -The value `t` denotes the common reference string used by both parties: - * t = H(R, A, M) -where H() denotes a cryptographic hash function, SHA256 in this implementation. - -The nonce `r` is a random secret, and protects the value `s` from revealing the -signers secret key. - -4) the verifier can check the following statement: - `S*B = R + t*A` - -For further information see: https://eprint.iacr.org/2015/677.pdf -based on: https://github.com/HarryR/ethsnarks -""" - -import hashlib -from collections import namedtuple -from math import ceil, log2 -from os import urandom - -from .curves import JubJub -from .fields import BLS12_381Field as FQ -from .utils import to_bytes - - -class PrivateKey(namedtuple("_PrivateKey", ("fe"))): - """ - Wraps field element - """ - - @classmethod - # FIXME: ethsnarks creates keys > 32bytes. Create issue. - def from_rand(cls): - mod = JubJub.JUBJUB_L.n - # nbytes = ceil(ceil(log2(mod)) / 8) + 1 - nbytes = ceil(ceil(log2(mod)) / 8) - rand_n = int.from_bytes(urandom(nbytes), "little") - return cls(FQ(rand_n)) - - def sign(self, msg, B=None): - "Returns the signature (R,S) for a given private key and message." - B = B or JubJub.generator() - - A = PublicKey.from_private(self) # A = kB - - M = msg - r = hash_to_scalar(self.fe, M) % JubJub.JUBJUB_L.n # r = H(k,M) mod L - R = B.mult(r) # R = rB - - # Bind the message to the nonce, public key and message - hRAM = hash_to_scalar(R, A, M) - key_field = self.fe.n - S = (r + (key_field * hRAM)) # r + (H(R,A,M) * k) - - return (R, S) - - -class PublicKey(namedtuple("_PublicKey", ("p"))): - """ - Wraps edwards point - """ - - @classmethod - def from_private(cls, sk, B=None): - "Returns public key for a private key. B denotes the group generator" - B = B or JubJub.generator() - if not isinstance(sk, PrivateKey): - sk = PrivateKey(sk) - A = B.mult(sk.fe) - return cls(A) - - def verify(self, sig, msg, B=None): - B = B or JubJub.generator() - - R, S = sig - M = msg - A = self.p - - lhs = B.mult(S) - - hRAM = hash_to_scalar(R, A, M) - rhs = (R + (A.mult(hRAM))) - - return lhs == rhs - - -def hash_to_scalar(*args): - """ - Hash the key and message to create `r`, the blinding factor for this signature. - If the same `r` value is used more than once, the key for the signature is revealed. - - Note that we take the entire 256bit hash digest as input for the scalar multiplication. - As the group is only of size JUBJUB_E (<256bit) we allow wrapping around the group modulo. - """ - p = b"".join(to_bytes(_) for _ in args) - digest = hashlib.sha256(p).digest() - return int(digest.hex(), 16) % JubJub.JUBJUB_E # mod JUBJUB_E here for optimized implementation diff --git a/zokrates_pycrypto/utils.py b/zokrates_pycrypto/utils.py index e0d0ad9..9fdf376 100644 --- a/zokrates_pycrypto/utils.py +++ b/zokrates_pycrypto/utils.py @@ -30,7 +30,7 @@ def to_bytes(*args): def write_signature_for_zokrates_cli(pk, sig, msg, path): "Writes the input arguments for verifyEddsa in the ZoKrates stdlib to file." sig_R, sig_S = sig - args = [sig_R.x, sig_R.y, sig_S, pk.p.x.n, pk.p.y.n] + args = [sig_R.x, sig_R.y, sig_S, pk.point.x.n, pk.point.y.n] args = " ".join(map(str, args)) M0 = msg.hex()[:64] @@ -53,7 +53,7 @@ def pprint_hex_as_256bit(n, h): def pprint_point(n, p): "Takes a variable name and curve point and returns Zokrates assignment statement." - x, y = p + x, y = p.x, p.y return "field[2] {} = [{}, {}] \n".format(n, x, y) From b70a0efeda8f78ce17ba6e500cf1fc7c64ff509e Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Tue, 11 Apr 2023 17:57:51 +0200 Subject: [PATCH 07/26] #2 remove cli code --- README.md | 26 --------- cli.py | 167 ------------------------------------------------------ 2 files changed, 193 deletions(-) delete mode 100644 cli.py diff --git a/README.md b/README.md index ff378f3..a11e5a8 100644 --- a/README.md +++ b/README.md @@ -122,32 +122,6 @@ After compiling this file we can now pass our input arguments into witness gener `cat zokrates_inputs.txt | ./zokrates compute-witness` -## CLI Usage - -`pycrypto` also provides a simple command-line interface to make it easy to integrate the used crypto primitives into your existing application code. - -Some examples: - -### Compute SNARK-friendly Pedersen hash -```bash -python cli.py hash 3755668da8deabd8cafbe1c26cda5a837ed5f832665c5ef94725f6884054d9083755668da8deabd8cafbe1c26cda5a837ed5f832665c5ef94725f6884054d908 -``` -where the first argument denotes the preimage as a hexstring. - -### Create and verify an EdDSA signature -```bash -python cli.py keygen -# => 37e334c51386a5c92152f592ef264b82ad52cf2bbfb6cee1c363e67be97732a ab466cd8924518f07172c0f8c695c60f77c11357b461d787ef31864a163f3995 -# Private and public key - -python cli.py sig-gen 37e334c51386a5c92152f592ef264b82ad52cf2bbfb6cee1c363e67be97732a 11dd22 -# => 172a1794976d7d0272148c4be3b7ad74fd3a82376cd5995fc4d274e3593c0e6c 24e96be628208a9800336d23bd31318d8a9b95bc9bd8f6f01cae207c05062523 -# R and S element of EdDSA signature - -python cli.py sig-verify ab466cd8924518f07172c0f8c695c60f77c11357b461d787ef31864a163f3995 11dd22 172a1794976d7d0272148c4be3b7ad74fd3a82376cd5995fc4d274e3593c0e6c 24e96be628208a9800336d23bd31318d8a9b95bc9bd8f6f01cae207c05062523 -# => True -``` - ## Contributing We happily welcome contributions. You can either pick an existing issue, or reach out on [Gitter](https://gitter.im/ZoKrates/Lobby). diff --git a/cli.py b/cli.py deleted file mode 100644 index c45c928..0000000 --- a/cli.py +++ /dev/null @@ -1,167 +0,0 @@ -import argparse -import sys -from zokrates_pycrypto.gadgets.pedersenHasher import PedersenHasher -from zokrates_pycrypto.babyjubjub import Point -from zokrates_pycrypto.eddsa import PrivateKey, PublicKey -from zokrates_pycrypto.field import FQ - - -def main(): - parser = argparse.ArgumentParser(description="pycrypto command-line interface") - subparsers = parser.add_subparsers(dest="subparser_name") - - # pedersen hash subcommand - pedersen_parser = subparsers.add_parser( - "hash", - help="Compute a 256bit Pedersen hash. Preimage size is set to 512bit as default", - ) - pedersen_parser.add_argument( - "preimage", nargs=1, help="Provide preimage as hex string" - ) - pedersen_parser.add_argument( - "-s", "--size", type=int, help="Define message size in bits", default=64 - ) - pedersen_parser.add_argument( - "-p", "--personalisation", help="Provide personalisation string", default="test" - ) - - # batch pedersen hash subcommand - pedersen_hasher_parser = subparsers.add_parser( - "batch_hasher", - help="Efficiently compute multiple Pedersen hashes. Support for stdin and interactive promt", - ) - - pedersen_hasher_parser.add_argument( - "-s", "--size", type=int, help="Define message size in bits", default=64 - ) - pedersen_hasher_parser.add_argument( - "-p", "--personalisation", help="Provide personalisation string", default="test" - ) - - # keygen subcommand - keygen_parser = subparsers.add_parser( - "keygen", - help="Returns space separated hex-string for a random private/public keypair on BabyJubJub curve", - ) - keygen_parser.add_argument( - "-p", - "--from_private", - help="Provide existing private key as hex string (64 chars)", - ) - - # eddsa signature generation subcommand - sig_gen_parser = subparsers.add_parser( - "sig-gen", - help="Returns eddsa signature as space separated hex string. Private key and message needs to be provided", - ) - sig_gen_parser.add_argument( - "private_key", nargs=1, help="Provide private key as hexstring (64chars)" - ) - - sig_gen_parser.add_argument( - "message", nargs=1, help="Provide message as hex string" - ) - - # EdDSA signature verify subcommand - sig_verify_parser = subparsers.add_parser( - "sig-verify", help="Verifies a EdDSA signaure. Returns boolean flag for success" - ) - sig_verify_parser.add_argument( - "public_key", nargs=1, help="Provide public key as hex string (64 chars)" - ) - sig_verify_parser.add_argument( - "message", nargs=1, help="Provide message as hex string" - ) - sig_verify_parser.add_argument( - "signature", - nargs=2, - help="Provide signaure as space separated hex sting (2x 64 chars)", - ) - - args = parser.parse_args() - subparser_name = args.subparser_name - - if subparser_name == "hash": - preimage = bytes.fromhex(args.preimage[0]) - if len(preimage) != args.size: - raise ValueError( - "Bad length for preimage: {} vs {}".format(len(preimage), args.size) - ) - - personalisation = args.personalisation.encode("ascii") - point = PedersenHasher(personalisation).hash_bytes(preimage) - digest = point.compress() - - assert len(digest.hex()) == 32 * 2 # check for correct length - print("Hash digest", file=sys.stderr) - print(digest.hex()) - - elif subparser_name == "batch_hasher": - personalisation = args.personalisation.encode("ascii") - ph = PedersenHasher(personalisation) - try: - while True: - x = input() - if x == "exit": - sys.exit(0) - preimage = bytes.fromhex(x) - if len(preimage) != args.size: - raise ValueError( - "Bad length for preimage: {} vs {}".format(len(preimage), 64) - ) - point = ph.hash_bytes(preimage) - digest = point.compress() - assert len(digest.hex()) == 32 * 2 # check for correct length - print(digest.hex()) - except EOFError: - pass - - elif subparser_name == "keygen": - if args.from_private: - fe = FQ(int(args.from_private[0])) - sk = PrivateKey(fe) - else: - sk = PrivateKey.from_rand() - pk = PublicKey.from_private(sk) - - pk_hex = pk.p.compress().hex() - sk_hex = hex(sk.fe.n)[2:] - - print("PrivateKey PublicKey", file=sys.stderr) - print("{} {}".format(sk_hex, pk_hex)) - - elif subparser_name == "sig-gen": - sk_hex = int(args.private_key[0], 16) - sk = PrivateKey(FQ(sk_hex)) - msg = bytes.fromhex(args.message[0]) - - (r, s) = sk.sign(msg) - s_hex = hex(s)[2:] - r_hex = r.compress().hex() - - print("Signature_R Signature_S", file=sys.stderr) - print("{} {}".format(r_hex, s_hex)) - - elif subparser_name == "sig-verify": - r_hex, s_hex = args.signature[0], args.signature[1] - msg = bytes.fromhex(args.message[0]) - pk_hex = args.public_key[0] - - pk = PublicKey(Point.decompress(bytes.fromhex(pk_hex))) - r = Point.decompress(bytes.fromhex(r_hex)) - s = FQ(int(s_hex, 16)) - - success = pk.verify((r, s), msg) - if success: - sys.exit(0) - else: - sys.exit("Could not verfiy signature") - - else: - raise NotImplementedError( - "Sub-command not implemented: {}".format(subparser_name) - ) - - -if __name__ == "__main__": - main() From 8b2f7acc937a09795377e183213ef725129a9781 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Tue, 11 Apr 2023 22:00:21 +0200 Subject: [PATCH 08/26] #2 EdDSA tests --- tests/eddsa_test.py | 37 +++++++++++++++++++++++++++++++++++-- zokrates_pycrypto/eddsa.py | 4 ++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/tests/eddsa_test.py b/tests/eddsa_test.py index 5b9bf48..0fd4168 100644 --- a/tests/eddsa_test.py +++ b/tests/eddsa_test.py @@ -1,12 +1,12 @@ import unittest from os import urandom -from zokrates_pycrypto.curves import BabyJubJub +from zokrates_pycrypto.curves import BabyJubJub, JubJub from zokrates_pycrypto.eddsa import PublicKey, PrivateKey class TestEdDSA(unittest.TestCase): - def test_signverify(self): + def test_signverify_babyjubjub(self): # Hardcoded for now till we have automatic test generation for ZoKrates test framework key = 1997011358982923168928344992199991480689546837621580239342656433234255379025 @@ -17,6 +17,39 @@ def test_signverify(self): pk = PublicKey.from_private(sk) self.assertTrue(pk.verify(sig, msg)) + def test_signverify_jubjub(self): + # Hardcoded for now till we have automatic test generation for ZoKrates test framework + key = 1997011358982923168928344992199991480689546837621580239342656433234255379025 + + sk = PrivateKey(key, curve=JubJub) + msg = urandom(32) + sig = sk.sign(msg) + + pk = PublicKey.from_private(sk) + self.assertTrue(pk.verify(sig, msg)) + + def test_random_signverify_babyjubjub(self): + # Hardcoded for now till we have automatic test generation for ZoKrates test framework + key = 1997011358982923168928344992199991480689546837621580239342656433234255379025 + + sk = PrivateKey.from_rand(curve=BabyJubJub) + msg = urandom(32) + sig = sk.sign(msg) + + pk = PublicKey.from_private(sk) + self.assertTrue(pk.verify(sig, msg)) + + def test_random_signverify_jubjub(self): + # Hardcoded for now till we have automatic test generation for ZoKrates test framework + key = 1997011358982923168928344992199991480689546837621580239342656433234255379025 + + sk = PrivateKey.from_rand(curve=JubJub) + msg = urandom(32) + sig = sk.sign(msg) + + pk = PublicKey.from_private(sk) + self.assertTrue(pk.verify(sig, msg)) + if __name__ == "__main__": unittest.main() diff --git a/zokrates_pycrypto/eddsa.py b/zokrates_pycrypto/eddsa.py index fe57d4d..9a96d6a 100644 --- a/zokrates_pycrypto/eddsa.py +++ b/zokrates_pycrypto/eddsa.py @@ -47,7 +47,7 @@ class PrivateKey: def __init__(self, sk: int, curve: ABCMeta): if curve == BabyJubJub: field = BN128Field - elif curve == BLS12_381Field: + elif curve == JubJub: field = BLS12_381Field else: raise ValueError('Edwardscurve not supported') @@ -56,7 +56,7 @@ def __init__(self, sk: int, curve: ABCMeta): @classmethod # FIXME: ethsnarks creates keys > 32bytes. Create issue. - def from_rand(cls, sk: int, curve: ABCMeta): + def from_rand(cls, curve: ABCMeta): mod = curve.JUBJUB_L.n # nbytes = ceil(ceil(log2(mod)) / 8) + 1 nbytes = ceil(ceil(log2(mod)) / 8) From 6881086df25d8a9a5a89d4dd84ed2eb67f78401c Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Thu, 7 Dec 2023 17:30:04 +0100 Subject: [PATCH 09/26] shorter signatures --- zokrates_pycrypto/eddsa.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/zokrates_pycrypto/eddsa.py b/zokrates_pycrypto/eddsa.py index 9a96d6a..001540d 100644 --- a/zokrates_pycrypto/eddsa.py +++ b/zokrates_pycrypto/eddsa.py @@ -55,7 +55,6 @@ def __init__(self, sk: int, curve: ABCMeta): self.fe = field(sk) @classmethod - # FIXME: ethsnarks creates keys > 32bytes. Create issue. def from_rand(cls, curve: ABCMeta): mod = curve.JUBJUB_L.n # nbytes = ceil(ceil(log2(mod)) / 8) + 1 @@ -70,13 +69,13 @@ def sign(self, msg, B: EdwardsCurve = None): A = PublicKey.from_private(self) # A = kB M = msg - r = A.hash_to_scalar(self.fe, M) % self.curve.JUBJUB_L.n # r = H(k,M) mod L + r = A.hash_to_scalar(self.fe, M) # r = H(k,M) mod L R = B.mult(r) # R = rB # Bind the message to the nonce, public key and message hRAM = A.hash_to_scalar(R, A.point, M) key_field = self.fe.n - S = (r + (key_field * hRAM)) # r + (H(R,A,M) * k) + S = (r + (key_field * hRAM)) % self.curve.JUBJUB_E # r + (H(R,A,M) * k) return (R, S) From 4c5c5cf7b00824ad42759b3fba6eb9297c390c75 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Thu, 7 Dec 2023 17:52:04 +0100 Subject: [PATCH 10/26] extended .gitignore --- .gitignore | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index a98fc2b..75ab9bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,62 @@ include -lib share -src/.ipynb_checkpoints *.ipynb *.pyc +*.ipynb_checkpoints + .vscode .idea -.ipynb_checkpoints env zokrates_args +.DS_Store + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +# Sphinx documentation +docs/_build/ From 581297785c3f6bfebfb7e52ea7cc092c006301be Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Wed, 3 Jan 2024 16:13:53 +0100 Subject: [PATCH 11/26] #8 add CODE_OF_CONDUCT and CONTRIBUTING guidelines --- .github/CODE_OF_CONDUCT.md | 70 +++++++++++++ .github/CONTRIBUTING.md | 200 +++++++++++++++++++++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..3cf63bc --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,70 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting [ZKPlus team](https://zk-plus.github.io/about/contact). All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from [jessie Squire's code of conduct](https://github.com/jessesquires/.github/blob/main/CONTRIBUTING.md#book-code-of-conduct) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..6092ad4 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,200 @@ +# Contributing Guidelines + +*Pull requests, bug reports, and all other forms of contribution are welcomed and highly encouraged!* :octocat: + +### Contents + +- [Contributing Guidelines](#contributing-guidelines) + - [Contents](#contents) + - [:book: Code of Conduct](#book-code-of-conduct) + - [:bulb: Asking Questions](#bulb-asking-questions) + - [:inbox\_tray: Opening an Issue](#inbox_tray-opening-an-issue) + - [:lock: Reporting Security Issues](#lock-reporting-security-issues) + - [:beetle: Bug Reports and Other Issues](#beetle-bug-reports-and-other-issues) + - [:love\_letter: Feature Requests](#love_letter-feature-requests) + - [:mag: Triaging Issues](#mag-triaging-issues) + - [:repeat: Submitting Pull Requests](#repeat-submitting-pull-requests) + - [:memo: Writing Commit Messages](#memo-writing-commit-messages) + - [:white\_check\_mark: Code Review](#white_check_mark-code-review) + - [:nail\_care: Coding Style](#nail_care-coding-style) + - [:medal\_sports: Certificate of Origin](#medal_sports-certificate-of-origin) + - [No Brown M\&M's](#no-brown-mms) + - [:pray: Credits](#pray-credits) + +> **This guide serves to set clear expectations for everyone involved with the project so that we can improve it together while also creating a welcoming space for everyone to participate. Following these guidelines will help ensure a positive experience for contributors and maintainers.** + +## :book: Code of Conduct + +Please review our [Code of Conduct](https://github.com/ZK-Plus/ZnaKes/blob/main/CODE_OF_CONDUCT.md). It is in effect at all times. We expect it to be honored by everyone who contributes to this project. Acting like an asshole will not be tolerated. + +## :bulb: Asking Questions + +In short, GitHub issues are not the appropriate place to debug your specific project, but should be reserved for filing bugs and feature requests. + +## :inbox_tray: Opening an Issue + +Before [creating an issue](https://github.com/ZK-Plus/ZnaKes/issues), check if you are using the latest version of the project. If you are not up-to-date, see if updating fixes your issue first. + +### :lock: Reporting Security Issues + +This is an experimental project and should not be used in production environments. + +### :beetle: Bug Reports and Other Issues + +A great way to contribute to the project is to send a detailed issue when you encounter a problem. We always appreciate a well-written, thorough bug report. :v: + +In short, since you are most likely a developer, **provide a ticket that you would like to receive**. + +- **Review the documentation** before opening a new issue. + +- **Do not open a duplicate issue!** Search through existing issues to see if your issue has previously been reported. If your issue exists, comment with any additional information you have. You may simply note "I have this problem too", which helps prioritize the most common problems and requests. + +- **Prefer using [reactions](https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/)**, not comments, if you simply want to "+1" an existing issue. + +- **Fully complete the provided issue template.** The bug report template requests all the information we need to quickly and efficiently address your issue. Be clear, concise, and descriptive. Provide as much information as you can, including steps to reproduce, stack traces, compiler errors, library versions, OS versions, and screenshots (if applicable). + +- **Use [GitHub-flavored Markdown](https://help.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax).** Especially put code blocks and console outputs in backticks (```). This improves readability. + +## :love_letter: Feature Requests + +Feature requests are welcome! While we will consider all requests, we cannot guarantee your request will be accepted. We want to avoid [feature creep](https://en.wikipedia.org/wiki/Feature_creep). Your idea may be great, but also out-of-scope for the project. If accepted, we cannot make any commitments regarding the timeline for implementation and release. However, you are welcome to submit a pull request to help! + +- **Do not open a duplicate feature request.** Search for existing feature requests first. If you find your feature (or one very similar) previously requested, comment on that issue. + +- **Fully complete the provided issue template.** The feature request template asks for all necessary information for us to begin a productive conversation. + +- Be precise about the proposed outcome of the feature and how it relates to existing features. Include implementation details if possible. + +## :mag: Triaging Issues + +You can triage issues which may include reproducing bug reports or asking for additional information, such as version numbers or reproduction instructions. Any help you can provide to quickly resolve an issue is very much appreciated! + +## :repeat: Submitting Pull Requests + +We **love** pull requests! Before [forking the repo](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) and [creating a pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests) for non-trivial changes, it is usually best to first open an issue to discuss the changes, or discuss your intended approach for solving the problem in the comments for an existing issue. + +For most contributions, after your first pull request is accepted and merged, you will be [invited to the project](https://help.github.com/en/github/setting-up-and-managing-your-github-user-account/inviting-collaborators-to-a-personal-repository) and given **push access**. :tada: + +*Note: All contributions will be licensed under the project's license.* + +- **Smaller is better.** Submit **one** pull request per bug fix or feature. A pull request should contain isolated changes pertaining to a single bug fix or feature implementation. **Do not** refactor or reformat code that is unrelated to your change. It is better to **submit many small pull requests** rather than a single large one. Enormous pull requests will take enormous amounts of time to review, or may be rejected altogether. + +- **Coordinate bigger changes.** For large and non-trivial changes, open an issue to discuss a strategy with the maintainers. Otherwise, you risk doing a lot of work for nothing! + +- **Prioritize understanding over cleverness.** Write code clearly and concisely. Remember that source code usually gets written once and read often. Ensure the code is clear to the reader. The purpose and logic should be obvious to a reasonably skilled developer, otherwise you should add a comment that explains it. + +- **Follow existing coding style and conventions.** Keep your code consistent with the style, formatting, and conventions in the rest of the code base. When possible, these will be enforced with a linter. Consistency makes it easier to review and modify in the future. + +- **Include test coverage.** Add unit tests or UI tests when possible. Follow existing patterns for implementing tests. + +- **Update the example project** if one exists to exercise any new functionality you have added. + +- **Add documentation.** Document your changes with code doc comments or in existing guides. + +- **Update the CHANGELOG** for all enhancements and bug fixes. Include the corresponding issue number if one exists, and your GitHub username. (example: "- Fixed crash in profile view. #123 @jessesquires") + +- **Use the repo's default branch.** Branch from and [submit your pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) to the repo's default branch. Usually this is `main`, but it could be `dev`, `develop`, or `master`. + +- **[Resolve any merge conflicts](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/resolving-a-merge-conflict-on-github)** that occur. + +- **Promptly address any CI failures**. If your pull request fails to build or pass tests, please push another commit to fix it. + +- When writing comments, use properly constructed sentences, including punctuation. + +- Use spaces, not tabs. + +## :memo: Writing Commit Messages + +Please [write a great commit message](https://chris.beams.io/posts/git-commit/). + +1. Separate subject from body with a blank line +1. Limit the subject line to 50 characters +1. Capitalize the subject line +1. Do not end the subject line with a period +1. Use the imperative mood in the subject line (example: "Fix networking issue") +1. Wrap the body at about 72 characters +1. Use the body to explain **why**, *not what and how* (the code shows that!) +1. If applicable, prefix the title with the relevant component name. (examples: "[Docs] Fix typo", "[Profile] Fix missing avatar") + +``` +[TAG] Short summary of changes in 50 chars or less + +Add a more detailed explanation here, if necessary. Possibly give +some background about the issue being fixed, etc. The body of the +commit message can be several paragraphs. Further paragraphs come +after blank lines and please do proper word-wrap. + +Wrap it to about 72 characters or so. In some contexts, +the first line is treated as the subject of the commit and the +rest of the text as the body. The blank line separating the summary +from the body is critical (unless you omit the body entirely); +various tools like `log`, `shortlog` and `rebase` can get confused +if you run the two together. + +Explain the problem that this commit is solving. Focus on why you +are making this change as opposed to how or what. The code explains +how or what. Reviewers and your future self can read the patch, +but might not understand why a particular solution was implemented. +Are there side effects or other unintuitive consequences of this +change? Here's the place to explain them. + + - Bullet points are okay, too + + - A hyphen or asterisk should be used for the bullet, preceded + by a single space, with blank lines in between + +Note the fixed or relevant GitHub issues at the end: + +Resolves: #123 +See also: #456, #789 +``` + +## :white_check_mark: Code Review + +- **Review the code, not the author.** Look for and suggest improvements without disparaging or insulting the author. Provide actionable feedback and explain your reasoning. + +- **You are not your code.** When your code is critiqued, questioned, or constructively criticized, remember that you are not your code. Do not take code review personally. + +- **Always do your best.** No one writes bugs on purpose. Do your best, and learn from your mistakes. + +- Kindly note any violations to the guidelines specified in this document. + +## :nail_care: Coding Style + +Consistency is the most important. Following the existing style, formatting, and naming conventions of the file you are modifying and of the overall project. Failure to do so will result in a prolonged review process that has to focus on updating the superficial aspects of your code, rather than improving its functionality and performance. + +For example, if all private properties are prefixed with an underscore `_`, then new ones you add should be prefixed in the same way. Or, if methods are named using camelcase, like `thisIsMyNewMethod`, then do not diverge from that by writing `this_is_my_new_method`. You get the idea. If in doubt, please ask or search the codebase for something similar. + +When possible, style and format will be enforced with a linter. + +## :medal_sports: Certificate of Origin + +*Developer's Certificate of Origin 1.1* + +By making a contribution to this project, I certify that: + +> 1. The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or +> 1. The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or +> 1. The contribution was provided directly to me by some other person who certified (1), (2) or (3) and I have not modified it. +> 1. I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. + +## [No Brown M&M's](https://en.wikipedia.org/wiki/Van_Halen#Contract_riders) + +If you are reading this, bravo dear user and (hopefully) contributor for making it this far! You are awesome. :100: + +To confirm that you have read this guide and are following it as best as possible, **include this emoji at the top** of your issue or pull request: :black_heart: `:black_heart:` + +## :pray: Credits + +Written by [@jessesquires](https://github.com/jessesquires). + +**Please feel free to adopt this guide in your own projects. Fork it wholesale or remix it for your needs.** + +*Many of the ideas and prose for the statements in this document were based on or inspired by work from the following communities:* + +- [Alamofire](https://github.com/Alamofire/Alamofire/blob/master/CONTRIBUTING.md) +- [CocoaPods](https://github.com/CocoaPods/CocoaPods/blob/master/CONTRIBUTING.md) +- [Docker](https://github.com/moby/moby/blob/master/CONTRIBUTING.md) +- [Linux](https://elinux.org/Developer_Certificate_Of_Origin) + +*We commend them for their efforts to facilitate collaboration in their projects.* From 02d342bf0bae2c83295185bccc8aef132aaa6f10 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Wed, 3 Jan 2024 16:14:48 +0100 Subject: [PATCH 12/26] #8 git add README with logo and project description --- README.md | 52 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a11e5a8..78a609c 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,32 @@ -# ZoKrates pyCrypto + -This repository contains accompanying crypto application code for the zkSNARKs toolbox [ZoKrates](https://github.com/Zokrates/ZoKrates). -_This is a proof-of-concept implementation. It has not been tested for production._ +# ZnaKes +A one-stop client library which facilitates the creation of arguments for zero-knowledge Proofs. ZnaKes provide an easy interface to generate zk-friendly crypto primitives necessary in efficient circuits developed with popular tools such as ZoKrates, Circom, Noir and so on... -## Install +:warning: _This is a proof-of-concept implementation. It has not been tested for production._ + +This repository code is primarily based from [ZoKrates Pycrypto](https://github.com/Zokrates/pycrypto) for the zkSNARKs toolbox [ZoKrates](https://github.com/Zokrates/ZoKrates). +Nonetheless, we plan of adding more primitives support by other tools as well. +Some of these primitives are: + +- Poseidon, mimc and pedersen hashes. +- EdDSA signatures for multiple curves (`BN254`, `BLS12_381`...) +- ... and more! + + + -## Example + -### Compute SNARK-friendly Pedersen hash + -### Create and verify Eddsa signature + ## Contributing -We happily welcome contributions. You can either pick an existing issue, or reach out on [Gitter](https://gitter.im/ZoKrates/Lobby). +We happily welcome contributions. You can either pick an existing issue or create a new issue. Before that make sure you have read our [CODE_OF_CONDUCT](.github/CODE_OF_CONDUCT.md) and [CONTRIBUTION GUIDELINES](.github/CONTRIBUTING.md) -Unless you explicitly state otherwise, any contribution you intentionally submit for inclusion in the work shall be licensed as above, without any additional terms or conditions. +Please note that your submited contributions shall be licensed as below, without any additional terms or conditions. -### Setup + + +## Acknowledgements + +- [ZoKrates dev tem](https://github.com/Zokrates/ZoKrates/graphs/contributors) for providing a great starting point for this project and for the awesome tool ZoKrates. +- [jesse squires](https://github.com/jessesquires/.github) for the great `CONTRIBUTING.md` and `CODE_OF_CONDUCT` guidelines. +- [Josee9988](https://github.com/Josee9988/project-template) for the amazing issue templates. ## License From b67a85b00304ffc2cde3e0a68834cc3b55a32f98 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Wed, 3 Jan 2024 16:37:38 +0100 Subject: [PATCH 13/26] #8 git add icon to repo --- icon.jpg | Bin 0 -> 71488 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 icon.jpg diff --git a/icon.jpg b/icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b1ee28540bba6adba1599b9e3790a68e095c9d6f GIT binary patch literal 71488 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

vMFlIMw9ElkaUfp{e@G4dldtnK8A66f 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 P!7XptBk>~t{P_O>PjRS& literal 0 HcmV?d00001 From d56ab9b250fe44deebb95e99ad4b19a635b129fb Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Wed, 3 Jan 2024 17:22:56 +0100 Subject: [PATCH 14/26] #8 working example in README and package renaming --- README.md | 105 ++---------------- tests/babyjubjub_test.py | 4 +- tests/eddsa_test.py | 4 +- tests/field_test.py | 2 +- tests/jubjub_test.py | 4 +- tests/pedersenHasher_test.py | 4 +- {zokrates_pycrypto => znakes}/__init__.py | 0 {zokrates_pycrypto => znakes}/curves.py | 0 {zokrates_pycrypto => znakes}/eddsa.py | 0 {zokrates_pycrypto => znakes}/fields.py | 0 .../gadgets/__init__.py | 0 .../gadgets/pedersenHasher.py | 0 {zokrates_pycrypto => znakes}/numbertheory.py | 0 {zokrates_pycrypto => znakes}/utils.py | 0 14 files changed, 20 insertions(+), 103 deletions(-) rename {zokrates_pycrypto => znakes}/__init__.py (100%) rename {zokrates_pycrypto => znakes}/curves.py (100%) rename {zokrates_pycrypto => znakes}/eddsa.py (100%) rename {zokrates_pycrypto => znakes}/fields.py (100%) rename {zokrates_pycrypto => znakes}/gadgets/__init__.py (100%) rename {zokrates_pycrypto => znakes}/gadgets/pedersenHasher.py (100%) rename {zokrates_pycrypto => znakes}/numbertheory.py (100%) rename {zokrates_pycrypto => znakes}/utils.py (100%) diff --git a/README.md b/README.md index 78a609c..363fe64 100644 --- a/README.md +++ b/README.md @@ -16,135 +16,52 @@ Some of these primitives are: - ... and more! - - - - - +## Example - - ## Contributing We happily welcome contributions. You can either pick an existing issue or create a new issue. Before that make sure you have read our [CODE_OF_CONDUCT](.github/CODE_OF_CONDUCT.md) and [CONTRIBUTION GUIDELINES](.github/CONTRIBUTING.md) Please note that your submited contributions shall be licensed as below, without any additional terms or conditions. - +Then you just need to call `pre-commit install`. ## Acknowledgements diff --git a/tests/babyjubjub_test.py b/tests/babyjubjub_test.py index e118fe5..6e664c9 100644 --- a/tests/babyjubjub_test.py +++ b/tests/babyjubjub_test.py @@ -2,8 +2,8 @@ from os import urandom -from zokrates_pycrypto.fields import BN128Field as FQ -from zokrates_pycrypto.curves import BabyJubJub +from znakes.fields import BN128Field as FQ +from znakes.curves import BabyJubJub class TestJubjub(unittest.TestCase): diff --git a/tests/eddsa_test.py b/tests/eddsa_test.py index 0fd4168..e5deeb5 100644 --- a/tests/eddsa_test.py +++ b/tests/eddsa_test.py @@ -1,8 +1,8 @@ import unittest from os import urandom -from zokrates_pycrypto.curves import BabyJubJub, JubJub -from zokrates_pycrypto.eddsa import PublicKey, PrivateKey +from znakes.curves import BabyJubJub, JubJub +from znakes.eddsa import PublicKey, PrivateKey class TestEdDSA(unittest.TestCase): diff --git a/tests/field_test.py b/tests/field_test.py index d494539..c80f1bc 100644 --- a/tests/field_test.py +++ b/tests/field_test.py @@ -1,6 +1,6 @@ import unittest -from zokrates_pycrypto.fields import FQ, BN128Field, BLS12_381Field +from znakes.fields import FQ, BN128Field, BLS12_381Field class TestField(unittest.TestCase): diff --git a/tests/jubjub_test.py b/tests/jubjub_test.py index 85c9276..c276f08 100644 --- a/tests/jubjub_test.py +++ b/tests/jubjub_test.py @@ -2,8 +2,8 @@ from os import urandom -from zokrates_pycrypto.fields import BLS12_381Field as FQ -from zokrates_pycrypto.curves import JubJub +from znakes.fields import BLS12_381Field as FQ +from znakes.curves import JubJub JUBJUB_C = JubJub.JUBJUB_C diff --git a/tests/pedersenHasher_test.py b/tests/pedersenHasher_test.py index b8caa1b..18c16c5 100644 --- a/tests/pedersenHasher_test.py +++ b/tests/pedersenHasher_test.py @@ -3,8 +3,8 @@ from os import urandom from random import randint -from zokrates_pycrypto.curves import BabyJubJub, BN128Field as FQ -from zokrates_pycrypto.gadgets.pedersenHasher import PedersenHasher as P +from znakes.curves import BabyJubJub, BN128Field as FQ +from znakes.gadgets.pedersenHasher import PedersenHasher as P class TestPedersenHash(unittest.TestCase): diff --git a/zokrates_pycrypto/__init__.py b/znakes/__init__.py similarity index 100% rename from zokrates_pycrypto/__init__.py rename to znakes/__init__.py diff --git a/zokrates_pycrypto/curves.py b/znakes/curves.py similarity index 100% rename from zokrates_pycrypto/curves.py rename to znakes/curves.py diff --git a/zokrates_pycrypto/eddsa.py b/znakes/eddsa.py similarity index 100% rename from zokrates_pycrypto/eddsa.py rename to znakes/eddsa.py diff --git a/zokrates_pycrypto/fields.py b/znakes/fields.py similarity index 100% rename from zokrates_pycrypto/fields.py rename to znakes/fields.py diff --git a/zokrates_pycrypto/gadgets/__init__.py b/znakes/gadgets/__init__.py similarity index 100% rename from zokrates_pycrypto/gadgets/__init__.py rename to znakes/gadgets/__init__.py diff --git a/zokrates_pycrypto/gadgets/pedersenHasher.py b/znakes/gadgets/pedersenHasher.py similarity index 100% rename from zokrates_pycrypto/gadgets/pedersenHasher.py rename to znakes/gadgets/pedersenHasher.py diff --git a/zokrates_pycrypto/numbertheory.py b/znakes/numbertheory.py similarity index 100% rename from zokrates_pycrypto/numbertheory.py rename to znakes/numbertheory.py diff --git a/zokrates_pycrypto/utils.py b/znakes/utils.py similarity index 100% rename from zokrates_pycrypto/utils.py rename to znakes/utils.py From 5caa047a96d2382a6e355debb36cfe892fcee7f9 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Wed, 3 Jan 2024 17:37:35 +0100 Subject: [PATCH 15/26] #8 github issue templates --- .github/ISSUE_TEMPLATE.md | 40 ++++++++ .github/ISSUE_TEMPLATE/1-bug-report.md | 78 +++++++++++++++ .github/ISSUE_TEMPLATE/2-failing-test.md | 41 ++++++++ .github/ISSUE_TEMPLATE/3-docs-bug.md | 60 +++++++++++ .github/ISSUE_TEMPLATE/4-feature-request.md | 45 +++++++++ .../ISSUE_TEMPLATE/5-enhancement-request.md | 45 +++++++++ .github/ISSUE_TEMPLATE/6-security-report.md | 99 +++++++++++++++++++ .github/ISSUE_TEMPLATE/7-question-support.md | 28 ++++++ .github/pull_request_template.md | 29 ++++++ 9 files changed, 465 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/1-bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/2-failing-test.md create mode 100644 .github/ISSUE_TEMPLATE/3-docs-bug.md create mode 100644 .github/ISSUE_TEMPLATE/4-feature-request.md create mode 100644 .github/ISSUE_TEMPLATE/5-enhancement-request.md create mode 100644 .github/ISSUE_TEMPLATE/6-security-report.md create mode 100644 .github/ISSUE_TEMPLATE/7-question-support.md create mode 100644 .github/pull_request_template.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..0afd952 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,40 @@ + + +# **Blank Issue Report** + +## **Describe the issue** + + +* + +--- + +### **Media prove** + + +--- + +### **Your environment** + + + +* OS: +* Python version: +* Pip version: + +--- + +### **Additional context** + + +* diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.md b/.github/ISSUE_TEMPLATE/1-bug-report.md new file mode 100644 index 0000000..4d6ab31 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug-report.md @@ -0,0 +1,78 @@ +--- +name: "🐞 Bug Report" +about: "Report an issue to help the project improve." +title: "[Bug] " +labels: "Type: Bug" + + +--- + +# **🐞 Bug Report** + +## **Describe the bug** + + +* + +--- + +### **Is this a regression?** + + + +--- + +### **To Reproduce** + + + + + +1. +2. +3. +4. + +--- + +### **Expected behaviour** + + +* + +--- + +### **Media prove** + + +--- + +### **Your environment** + + + +* OS: +* Python version: +* Pip version: + +--- + +### **Additional context** + + +* + + diff --git a/.github/ISSUE_TEMPLATE/2-failing-test.md b/.github/ISSUE_TEMPLATE/2-failing-test.md new file mode 100644 index 0000000..2b47c47 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-failing-test.md @@ -0,0 +1,41 @@ +--- +name: "💉 Failing Test" +about: "Report failing tests or CI jobs." +title: "[Test] " +labels: "Type: Test" + + +--- + +# **💉 Failing Test** + +## **Which jobs/test(s) are failing** + + +* + +--- + +## **Reason for failure/description** + + +--- + +### **Media prove** + + +--- + +### **Additional context** + + +* + + diff --git a/.github/ISSUE_TEMPLATE/3-docs-bug.md b/.github/ISSUE_TEMPLATE/3-docs-bug.md new file mode 100644 index 0000000..170ea83 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3-docs-bug.md @@ -0,0 +1,60 @@ +--- +name: "📚 Documentation or README.md issue report" +about: "Report an issue in the project's documentation or README.md file." +title: "" +labels: "Documentation" + + +--- +# **📚 Documentation Issue Report** + +## **Describe the bug** + + +* + +--- + +### **To Reproduce** + + + + + +1. +2. +3. +4. + +--- + +### **Media prove** + + +--- + +## **Describe the solution you'd like** + + +* + +--- + +### **Additional context** + + +* + + diff --git a/.github/ISSUE_TEMPLATE/4-feature-request.md b/.github/ISSUE_TEMPLATE/4-feature-request.md new file mode 100644 index 0000000..9638748 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4-feature-request.md @@ -0,0 +1,45 @@ +--- +name: "🚀🆕 Feature Request" +about: "Suggest an idea or possible new feature for this project." +title: "" +labels: "Type: Feature" + + +--- + +# **🚀 Feature Request** + +## **Is your feature request related to a problem? Please describe.** + + +* + +--- + +## **Describe the solution you'd like** + + +* + +--- + +## **Describe alternatives you've considered** + + +* + +--- + +### **Additional context** + + +* + + diff --git a/.github/ISSUE_TEMPLATE/5-enhancement-request.md b/.github/ISSUE_TEMPLATE/5-enhancement-request.md new file mode 100644 index 0000000..aed25c2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/5-enhancement-request.md @@ -0,0 +1,45 @@ +--- +name: "🚀➕ Enhancement Request" +about: "Suggest an enhancement for this project. Improve an existing feature" +title: "" +labels: "Type: Enhancement" + + +--- + +# **🚀 Enhancement Request** + +## **Is your enhancement request related to a problem? Please describe.** + + +* + +--- + +## **Describe the solution you'd like** + + +* + +--- + +## **Describe alternatives you've considered** + + +* + +--- + +### **Additional context** + + +* + + diff --git a/.github/ISSUE_TEMPLATE/6-security-report.md b/.github/ISSUE_TEMPLATE/6-security-report.md new file mode 100644 index 0000000..fcb7990 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/6-security-report.md @@ -0,0 +1,99 @@ +--- +name: "⚠️ Security Report" +about: "Report an issue to help the project improve." +title: "" +labels: "Type: Security" + + +--- + + + +# **⚠️ Security Report** + +## **Describe the security issue** + + +* + +--- + +### **To Reproduce** + + + + + +1. +2. +3. +4. + +--- + +### **Expected behaviour** + + +* + +--- + +### **Media prove** + + +--- + +### **Your environment** + + + +* OS: +* Python version: +* Pip version: + +--- + +### **Additional context** + + +* diff --git a/.github/ISSUE_TEMPLATE/7-question-support.md b/.github/ISSUE_TEMPLATE/7-question-support.md new file mode 100644 index 0000000..1ad3ed4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/7-question-support.md @@ -0,0 +1,28 @@ +--- +name: "❓ Question or Support Request" +about: "Questions and requests for support." +title: "" +labels: "Type: Question" + + +--- + +# **❓ Question or Support Request** + +## **Describe your question or ask for support.** + + +* + + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..4cb113f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,29 @@ +# **Name of PR** + + + +## **Description** + + + +* + +--- + +### **Additional context** + + + +* + + From 4ff038e1ce1c99eb47c1e56294538c9c34ba8709 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Wed, 3 Jan 2024 17:46:08 +0100 Subject: [PATCH 16/26] #8 add pytest for github action --- .pre-commit-config.yaml | 2 +- requirements-dev.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8dcfe2c..57faa7b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: name-tests-test - id: debug-statements - id: requirements-txt-fixer - # - id: flake8 + - id: flake8 - repo: https://github.com/stefandeml/pre_commit_hooks rev: 507a6c4e4bdb5ffc7d35c1227c177e7a9bb86965 hooks: diff --git a/requirements-dev.txt b/requirements-dev.txt index 095263d..2af8ac1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,6 +12,7 @@ mccabe==0.6.1 pycodestyle==2.5.0 pyflakes==2.1.1 pylint==2.3.1 +pytest==7.4.4 six==1.12.0 toml==0.10.0 # typed-ast==1.3.2 From 73ba7705370eaf3d55d6f016d5cf2627d5fd30d8 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Wed, 3 Jan 2024 19:06:05 +0100 Subject: [PATCH 17/26] #8 add test-and-lint action and include python version badge --- .github/workflows/test-and-lint.yml | 40 +++++++++++++++++++++++++++++ README.md | 2 ++ requirements-dev.txt | 2 ++ 3 files changed, 44 insertions(+) create mode 100644 .github/workflows/test-and-lint.yml diff --git a/.github/workflows/test-and-lint.yml b/.github/workflows/test-and-lint.yml new file mode 100644 index 0000000..c9fdff9 --- /dev/null +++ b/.github/workflows/test-and-lint.yml @@ -0,0 +1,40 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python tests and lint + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest diff --git a/README.md b/README.md index 363fe64..bb42bec 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ # ZnaKes +pythonpython3.9, 3.10, 3.11, 3.123.9, 3.10, 3.11, 3.12 + A one-stop client library which facilitates the creation of arguments for zero-knowledge Proofs. ZnaKes provide an easy interface to generate zk-friendly crypto primitives necessary in efficient circuits developed with popular tools such as ZoKrates, Circom, Noir and so on... :warning: _This is a proof-of-concept implementation. It has not been tested for production._ diff --git a/requirements-dev.txt b/requirements-dev.txt index 2af8ac1..6a4f279 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,10 +9,12 @@ flake8==3.7.7 isort==4.3.15 lazy-object-proxy==1.3.1 mccabe==0.6.1 +pybadges==3.0.1 pycodestyle==2.5.0 pyflakes==2.1.1 pylint==2.3.1 pytest==7.4.4 +pytest-cov==4.1.0 six==1.12.0 toml==0.10.0 # typed-ast==1.3.2 From 19a775c79d79dea2ef2338af7b2ba0b7edc70df4 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Wed, 3 Jan 2024 19:28:36 +0100 Subject: [PATCH 18/26] #8 fix badge display --- README.md | 4 ++-- icon.jpg => assets/icon.jpg | Bin assets/python-version-badge.svg | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) rename icon.jpg => assets/icon.jpg (100%) create mode 100644 assets/python-version-badge.svg diff --git a/README.md b/README.md index bb42bec..c00fee7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ - + # ZnaKes -pythonpython3.9, 3.10, 3.11, 3.123.9, 3.10, 3.11, 3.12 +![python-version](./assets/python-version-badge.svg) A one-stop client library which facilitates the creation of arguments for zero-knowledge Proofs. ZnaKes provide an easy interface to generate zk-friendly crypto primitives necessary in efficient circuits developed with popular tools such as ZoKrates, Circom, Noir and so on... diff --git a/icon.jpg b/assets/icon.jpg similarity index 100% rename from icon.jpg rename to assets/icon.jpg diff --git a/assets/python-version-badge.svg b/assets/python-version-badge.svg new file mode 100644 index 0000000..8855e36 --- /dev/null +++ b/assets/python-version-badge.svg @@ -0,0 +1 @@ +pythonpython3.9, 3.10, 3.11, 3.123.9, 3.10, 3.11, 3.12 From 5f6b1aaddf7bad4cd4ba59a5032ed3e2a3475dd8 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Thu, 4 Jan 2024 10:22:01 +0100 Subject: [PATCH 19/26] #8 add badges to README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index c00fee7..baa72b6 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ # ZnaKes ![python-version](./assets/python-version-badge.svg) +[![License: LGPL v3](https://img.shields.io/badge/license-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) +[![FLAKE8](https://img.shields.io/badge/code%20style-flake8-orange.svg)](https://flake8.pycqa.org/en/3.7.7/) A one-stop client library which facilitates the creation of arguments for zero-knowledge Proofs. ZnaKes provide an easy interface to generate zk-friendly crypto primitives necessary in efficient circuits developed with popular tools such as ZoKrates, Circom, Noir and so on... From 32e18a8c73803e5692b5232a823da25836f5dd0f Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Thu, 4 Jan 2024 10:48:11 +0100 Subject: [PATCH 20/26] #8 codecov workflow --- .github/workflows/code-coverage.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/code-coverage.yml diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml new file mode 100644 index 0000000..29793ee --- /dev/null +++ b/.github/workflows/code-coverage.yml @@ -0,0 +1,21 @@ +name: Workflow for Codecov +on: [push, pull_request] +jobs: + run: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install dependencies + run: pip install -r requirements.txt + - name: Run tests and collect coverage + run: pytest --cov znakes + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 From 41b91749d913277ab8ea70272bbd05da94278ff4 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Thu, 4 Jan 2024 10:52:10 +0100 Subject: [PATCH 21/26] #8 codecov workflow install pytest dependencies --- .github/workflows/code-coverage.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 29793ee..cee7bad 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -14,7 +14,10 @@ jobs: with: python-version: '3.10' - name: Install dependencies - run: pip install -r requirements.txt + run: | + python -m pip install --upgrade pip + python -m pip install pytest pytest-cov + pip install -r requirements.txt - name: Run tests and collect coverage run: pytest --cov znakes - name: Upload coverage to Codecov From e8582ec7772af9a9e2295a50c83dd4b94efe9ae2 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Thu, 4 Jan 2024 10:56:17 +0100 Subject: [PATCH 22/26] #8 add codecov badge to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index baa72b6..2f28305 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ # ZnaKes ![python-version](./assets/python-version-badge.svg) +[![codecov](https://codecov.io/gh/ZK-Plus/ZnaKes/graph/badge.svg?token=70C58SFGGK)](https://codecov.io/gh/ZK-Plus/ZnaKes) [![License: LGPL v3](https://img.shields.io/badge/license-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![FLAKE8](https://img.shields.io/badge/code%20style-flake8-orange.svg)](https://flake8.pycqa.org/en/3.7.7/) From f7de52209467b6c7115e48df52562f419527a776 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Wed, 10 Jan 2024 16:42:31 +0100 Subject: [PATCH 23/26] #8 Setup repo to be uploaded to pypi --- README.md | 3 ++- pyproject.toml | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 pyproject.toml diff --git a/README.md b/README.md index 2f28305..3c51218 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ - + + # ZnaKes diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..08076f0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "ZnaKes" +version = "0.0.1" +authors = [ + { name="alvaro-alonso", email="aa@ise.tu-berlin.de" }, +] +description = "A python client library to easily generate arguments for zero-knowledge proofs (ZKPs)" +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Topic :: Education", + "Topic :: Security :: Cryptography", + "Operating System :: OS Independent", + "Intended Audience :: Developers" +] + +[project.urls] +Homepage = "https://github.com/ZK-Plus/ZnaKes" +Issues = "https://github.com/ZK-Plus/ZnaKes/issues" From bfb48b8fcfed14871b2de996fb043cff9ee410c3 Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Wed, 10 Jan 2024 17:37:38 +0100 Subject: [PATCH 24/26] build: #8 add commitment message checker --- .github/CONTRIBUTING.md | 55 ++++++++++--------------- .github/workflows/build-and-publish.yml | 34 +++++++++++++++ .pre-commit-config.yaml | 7 ++++ 3 files changed, 63 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/build-and-publish.yml diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6092ad4..f30956c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -105,49 +105,38 @@ For most contributions, after your first pull request is accepted and merged, yo ## :memo: Writing Commit Messages -Please [write a great commit message](https://chris.beams.io/posts/git-commit/). +Please follow [Angular commit style](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commit-message-format). -1. Separate subject from body with a blank line -1. Limit the subject line to 50 characters -1. Capitalize the subject line -1. Do not end the subject line with a period -1. Use the imperative mood in the subject line (example: "Fix networking issue") -1. Wrap the body at about 72 characters -1. Use the body to explain **why**, *not what and how* (the code shows that!) -1. If applicable, prefix the title with the relevant component name. (examples: "[Docs] Fix typo", "[Profile] Fix missing avatar") ``` -[TAG] Short summary of changes in 50 chars or less +(optional scope): short summary in present tense -Add a more detailed explanation here, if necessary. Possibly give -some background about the issue being fixed, etc. The body of the -commit message can be several paragraphs. Further paragraphs come -after blank lines and please do proper word-wrap. +(optional body: explains motivation for the change) -Wrap it to about 72 characters or so. In some contexts, -the first line is treated as the subject of the commit and the -rest of the text as the body. The blank line separating the summary -from the body is critical (unless you omit the body entirely); -various tools like `log`, `shortlog` and `rebase` can get confused -if you run the two together. +(optional footer: note BREAKING CHANGES here, and issues to be closed) +``` -Explain the problem that this commit is solving. Focus on why you -are making this change as opposed to how or what. The code explains -how or what. Reviewers and your future self can read the patch, -but might not understand why a particular solution was implemented. -Are there side effects or other unintuitive consequences of this -change? Here's the place to explain them. +`type` refers to the kind of change made and is usually one of: - - Bullet points are okay, too + feat: A new feature. - - A hyphen or asterisk should be used for the bullet, preceded - by a single space, with blank lines in between + fix: A bug fix. -Note the fixed or relevant GitHub issues at the end: + docs: Documentation changes. -Resolves: #123 -See also: #456, #789 -``` + style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc). + + refactor: A code change that neither fixes a bug nor adds a feature. + + perf: A code change that improves performance. + + test: Changes to the test framework. + + build: Changes to the build process or tools. + +scope is an optional keyword that provides context for where the change was made. It can be anything relevant to your package or development workflow (e.g., it could be the module or function name affected by the change). + +For more information on commit message refer to the [site](https://py-pkgs.org/07-releasing-versioning.html#automatic-version-bumping) ## :white_check_mark: Code Review diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 0000000..ca0ed18 --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,34 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# GitHub recommends pinning actions to a commit SHA. +# To get a newer version, you will need to update the SHA. +# You can also reference a tag or branch, but the action may change without warning. + +name: Build and Upload Python Package + +on: + release: + types: [published] + +jobs: + release-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57faa7b..cf96948 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,4 @@ +default_stages: [commit] repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.1.0 @@ -20,3 +21,9 @@ repos: hooks: - id: detect_tab - id: unittest +- repo: https://github.com/lumapps/commit-message-validator + rev: af6ccfc300388bbac591fdd52467cee58d5bd918 + hooks: + - id: commit-message-validator + stages: [commit-msg] + args: [--allow-temp] From b0a0e3663fd768e2db20bb0f9a06bd9aa63a29fb Mon Sep 17 00:00:00 2001 From: alvaro-alonso Date: Wed, 10 Jan 2024 18:21:59 +0100 Subject: [PATCH 25/26] build: #8 add versioning --- CHANGELOG.md | 21 +++++++++++++++++++++ pyproject.toml | 14 ++++++++++++-- requirements-dev.txt | 3 ++- znakes/__about__.py | 1 + 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 znakes/__about__.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dae8676 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + + + + + +## v0.1.0 (10/01/2024) + +- First release of `ZnaKes` diff --git a/pyproject.toml b/pyproject.toml index 08076f0..08e71a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,15 @@ [build-system] -requires = ["hatchling"] +requires = [ + "hatchling>=1.8.0", + "hatch-semver", +] build-backend = "hatchling.build" [project] name = "ZnaKes" -version = "0.0.1" +dynamic = [ + "version" +] authors = [ { name="alvaro-alonso", email="aa@ise.tu-berlin.de" }, ] @@ -23,3 +28,8 @@ classifiers = [ [project.urls] Homepage = "https://github.com/ZK-Plus/ZnaKes" Issues = "https://github.com/ZK-Plus/ZnaKes/issues" + +[tool.hatch.version] +path = "znakes/__about__.py" +validate-bump = true +scheme = "semver" diff --git a/requirements-dev.txt b/requirements-dev.txt index 6a4f279..8a8ddfc 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,6 +6,8 @@ black==19.3b0 Click==7.0 entrypoints==0.3 flake8==3.7.7 +hatch==1.9.1 +hatchling==1.21.0 isort==4.3.15 lazy-object-proxy==1.3.1 mccabe==0.6.1 @@ -17,5 +19,4 @@ pytest==7.4.4 pytest-cov==4.1.0 six==1.12.0 toml==0.10.0 -# typed-ast==1.3.2 wrapt==1.11.1 diff --git a/znakes/__about__.py b/znakes/__about__.py new file mode 100644 index 0000000..3dc1f76 --- /dev/null +++ b/znakes/__about__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" From a43d499ce02f9e1e7656f3eac7d9fbdb5654e73f Mon Sep 17 00:00:00 2001 From: nikiblume Date: Mon, 12 Feb 2024 23:01:56 +0100 Subject: [PATCH 26/26] test: working through the readme demo and further testing --- demo.py | 20 ++++++++++++++++++++ zokrates_inputs.txt | 1 + 2 files changed, 21 insertions(+) create mode 100644 demo.py create mode 100644 zokrates_inputs.txt diff --git a/demo.py b/demo.py new file mode 100644 index 0000000..72c8cd7 --- /dev/null +++ b/demo.py @@ -0,0 +1,20 @@ +import hashlib + +from znakes.curves import JubJub +from znakes.eddsa import PrivateKey, PublicKey +from znakes.utils import write_signature_for_zokrates_cli + +if __name__ == "__main__": + + raw_msg = "This is my secret message" + msg = hashlib.sha512(raw_msg.encode("utf-8")).digest() + + sk = PrivateKey.from_rand(JubJub) + sig = sk.sign(msg) + + pk = PublicKey.from_private(sk) + is_verified = pk.verify(sig, msg) + print(is_verified) + write_signature_for_zokrates_cli(pk, sig, msg, "zokrates_inputs.txt") + + \ No newline at end of file diff --git a/zokrates_inputs.txt b/zokrates_inputs.txt new file mode 100644 index 0000000..baefe9f --- /dev/null +++ b/zokrates_inputs.txt @@ -0,0 +1 @@ +34447792987282659365761597972717728360572980490871459312286337313069481686981 5039489385779586464014678750834763525469837680048282742624492890153053968061 45914481514916287065509252353231787689584860054352686331997448774730479811501 42292624373983263331103349716850210388552074626710054328139942260264232291443 23051959298980289391849938317855738719500599241239385491207151286970159962540 3814687126 4207057211 2301474087 1696421512 1054042432 4114589074 2402006685 2358319779 2636307903 771130895 3338794104 910337493 3941248527 2566242658 3403499691 2178970740 \ No newline at end of file