From c3a70e269bc32a889177315733a8b48bf8d66036 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Thu, 25 Feb 2021 14:31:15 -0700 Subject: [PATCH 01/28] Add iso-Pallas, SWU hash-to-curve, and Sinsemilla Co-authored-by: Kris Nuttycombe --- orchard_iso_pallas.py | 176 ++++++++++++++++++++++++++++++++++++++++++ orchard_pallas.py | 21 ++++- orchard_sinsemilla.py | 175 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 368 insertions(+), 4 deletions(-) create mode 100644 orchard_iso_pallas.py create mode 100644 orchard_sinsemilla.py diff --git a/orchard_iso_pallas.py b/orchard_iso_pallas.py new file mode 100644 index 0000000..55398e6 --- /dev/null +++ b/orchard_iso_pallas.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +# -*- coding: utf8 -*- +import sys; assert sys.version_info[0] >= 3, "Python 3 required." + +import orchard_pallas +from orchard_pallas import Fp, p, q, Scalar + +# +# Point arithmetic +# + +PALLAS_ISO_B = Fp(1265) +PALLAS_ISO_A = Fp(0x18354a2eb0ea8c9c49be2d7258370742b74134581a27a59f92bb4b0b657a014b) + +class Point(object): + + @staticmethod + def from_bytes(buf): + assert len(buf) == 32 + if buf == bytes([0]*32): + return Point.identity() + + y_sign = buf[31] >> 7 + buf = buf[:31] + bytes([buf[31] & 0b01111111]) + try: + x = Fp.from_bytes(buf) + except ValueError: + return None + + x3 = x * x * x + y2 = x3 + PALLAS_ISO_A * x + PALLAS_ISO_B + + y = y2.sqrt() + if y is None: + return None + + if y.s % 2 != y_sign: + y = Fp.ZERO - y + + return Point(x, y) + + # Maps a point on iso-Pallas to a point on Pallas + def iso_map(self): + + c = [ + None, # make the indices 1-based + Fp(0x0e38e38e38e38e38e38e38e38e38e38e4081775473d8375b775f6034aaaaaaab), + Fp(0x3509afd51872d88e267c7ffa51cf412a0f93b82ee4b994958cf863b02814fb76), + Fp(0x17329b9ec525375398c7d7ac3d98fd13380af066cfeb6d690eb64faef37ea4f7), + Fp(0x1c71c71c71c71c71c71c71c71c71c71c8102eea8e7b06eb6eebec06955555580), + Fp(0x1d572e7ddc099cff5a607fcce0494a799c434ac1c96b6980c47f2ab668bcd71f), + Fp(0x325669becaecd5d11d13bf2a7f22b105b4abf9fb9a1fc81c2aa3af1eae5b6604), + Fp(0x1a12f684bda12f684bda12f684bda12f7642b01ad461bad25ad985b5e38e38e4), + Fp(0x1a84d7ea8c396c47133e3ffd28e7a09507c9dc17725cca4ac67c31d8140a7dbb), + Fp(0x3fb98ff0d2ddcadd303216cce1db9ff11765e924f745937802e2be87d225b234), + Fp(0x025ed097b425ed097b425ed097b425ed0ac03e8e134eb3e493e53ab371c71c4f), + Fp(0x0c02c5bcca0e6b7f0790bfb3506defb65941a3a4a97aa1b35a28279b1d1b42ae), + Fp(0x17033d3c60c68173573b3d7f7d681310d976bbfabbc5661d4d90ab820b12320a), + Fp(0x40000000000000000000000000000000224698fc094cf91b992d30ecfffffde5) + ] + + if self == Point.identity(): + return orchard_pallas.identity() + else: + numerator_a = c[1] * self.x * self.x * self.x + c[2] * self.x * self.x + c[3] * self.x + c[4] + denominator_a = self.x * self.x + c[5] * self.x + c[6] + + numerator_b = (c[7] * self.x * self.x * self.x + c[8] * self.x * self.x + c[9] * self.x + c[10]) * self.y + denominator_b = self.x * self.x * self.x + c[11] * self.x * self.x + c[12] * self.x + c[13] + + return orchard_pallas.Point(numerator_a / denominator_a, numerator_b / denominator_b) + + def __init__(self, x, y, is_identity=False): + self.x = x + self.y = y + self.is_identity = is_identity + + if is_identity: + assert self.x == Fp.ZERO + assert self.y == Fp.ZERO + else: + assert self.y * self.y == self.x * self.x * self.x + PALLAS_ISO_A * self.x + PALLAS_ISO_B + + def identity(): + p = Point(Fp.ZERO, Fp.ZERO, True) + return p + + def __neg__(self): + if self.is_identity: + return self + else: + return Point(Fp(self.x.s), -Fp(self.y.s)) + + def __add__(self, a): + if self.is_identity: + return a + elif a.is_identity: + return self + else: + # section 4.1 + (x1, y1) = (self.x, self.y) + (x2, y2) = (a.x, a.y) + + if x1 == x2: + if (y1 != y2) or (y1 == Fp(0)): + return Point.identity() + else: + return self.double() + else: + λ = (y1 - y2) / (x1 - x2) + x3 = λ*λ - x1 - x2 + y3 = λ*(x1 - x3) - y1 + return Point(x3, y3) + + def __sub__(self, a): + return (-a) + self + + def double(self): + if self.is_identity: + return self + + # section 4.1 + λ = (Fp(3) * self.x * self.x + PALLAS_ISO_A) / (self.y + self.y) + x3 = λ*λ - self.x - self.x + y3 = λ*(self.x - x3) - self.y + return Point(x3, y3) + + def __mul__(self, s): + s = format(s.s, '0256b') + ret = self.ZERO + for c in s: + ret = ret.double() + if int(c): + ret = ret + self + return ret + + def __bytes__(self): + if self.is_identity: + return bytes([0] * 32) + + buf = bytes(self.x) + if self.y.s % 2 == 1: + buf = buf[:31] + bytes([buf[31] | (1 << 7)]) + return buf + + def __eq__(self, a): + if a is None: + return False + if not (self.is_identity or a.is_identity): + return self.x == a.x and self.y == a.y + else: + return self.is_identity == a.is_identity + + def __str__(self): + if self.is_identity: + return 'Point(identity)' + else: + return 'Point(%s, %s)' % (self.x, self.y) + + +Point.ZERO = Point.identity() + +x = Fp(2) +y2 = x * x * x + PALLAS_ISO_A * x + PALLAS_ISO_B +y = y2.sqrt() +assert y is not None + +Point.GENERATOR = Point(x, y) + +assert Point.ZERO + Point.ZERO == Point.ZERO +assert Point.GENERATOR - Point.GENERATOR == Point.ZERO +assert Point.GENERATOR + Point.GENERATOR + Point.GENERATOR == Point.GENERATOR * Scalar(3) +assert Point.GENERATOR + Point.GENERATOR - Point.GENERATOR == Point.GENERATOR + +assert Point.from_bytes(bytes([0]*32)) == Point.ZERO +assert Point.from_bytes(bytes(Point.GENERATOR)) == Point.GENERATOR diff --git a/orchard_pallas.py b/orchard_pallas.py index 840ff50..9ac2717 100644 --- a/orchard_pallas.py +++ b/orchard_pallas.py @@ -36,6 +36,10 @@ class Fp(FieldElement): def __str__(self): return 'Fp(%s)' % self.s + def sgn0(self): + # https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-10#section-4.1 + return (self.s % 2) == 1 + def sqrt(self): # Tonelli-Shank's algorithm for p mod 16 = 1 # https://eprint.iacr.org/2012/685.pdf (page 12, algorithm 5) @@ -137,14 +141,19 @@ class Point(object): return Point(x, y) - def __init__(self, x, y): + def __init__(self, x, y, is_identity=False): self.x = x self.y = y - self.is_identity = False + self.is_identity = is_identity + + if is_identity: + assert self.x == Fp.ZERO + assert self.y == Fp.ZERO + else: + assert self.y * self.y == self.x * self.x * self.x + PALLAS_B def identity(): - p = Point(Fp.ZERO, Fp.ZERO) - p.is_identity = True + p = Point(Fp.ZERO, Fp.ZERO, True) return p def __neg__(self): @@ -185,6 +194,10 @@ class Point(object): x = λ*λ - self.x - self.x y = λ*(self.x - x) - self.y return Point(x, y) + + def extract(self): + assert not self.is_identity + return self.x def __mul__(self, s): s = format(s.s, '0256b') diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py new file mode 100644 index 0000000..5641c48 --- /dev/null +++ b/orchard_sinsemilla.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +import sys; assert sys.version_info[0] >= 3, "Python 3 required." + +import math + +import orchard_iso_pallas + +from pyblake2 import blake2b, blake2s +from orchard_pallas import Fp, p, q, PALLAS_B +from orchard_iso_pallas import PALLAS_ISO_B, PALLAS_ISO_A +from sapling_utils import i2beosp, cldiv, beos2ip, i2leosp, lebs2ip +from binascii import hexlify +from bitstring import BitArray + +# https://stackoverflow.com/questions/2612720/how-to-do-bitwise-exclusive-or-of-two-strings-in-python +def sxor(s1,s2): + return bytes([a ^ b for a,b in zip(s1,s2)]) + +def expand_message_xmd(msg, dst, len_in_bytes): + assert len(dst) <= 255 + + b_in_bytes = 64 # hash function output size + r_in_bytes = 128 + + ell = cldiv(len_in_bytes, b_in_bytes) + + assert ell <= 255 + + dst_prime = dst + i2beosp(8, len(dst)) + z_pad = b"\x00" * r_in_bytes + l_i_b_str = i2beosp(16, len_in_bytes) + msg_prime = z_pad + msg + l_i_b_str + i2beosp(8, 0) + dst_prime + + b = [] + + b0_ctx = blake2b(digest_size=64, person=i2beosp(128,0)) + b0_ctx.update(msg_prime) + b.append(b0_ctx.digest()) + assert len(b[0]) == b_in_bytes + + b1_ctx = blake2b(digest_size=64, person=i2beosp(128,0)) + b1_ctx.update(b[0] + i2beosp(8, 1) + dst_prime) + b.append(b1_ctx.digest()) + assert len(b[1]) == b_in_bytes + + for i in range(2, ell + 1): + bi_input = b"\x00" * b_in_bytes + + for j in range(0, i): + bi_input = sxor(bi_input, b[j]) + + assert len(bi_input) == b_in_bytes + + bi_input += i2beosp(8, i) + dst_prime + + bi_ctx = blake2b(digest_size=64, person=i2beosp(128,0)) + bi_ctx.update(bi_input) + + b.append(bi_ctx.digest()) + assert len(b[i]) == b_in_bytes + + return b''.join(b)[0:len_in_bytes] + +def hash_to_field(msg, dst): + k = 256 + count = 2 + m = 1 + + L = cldiv(math.ceil(math.log2(p)) + k, 8) + assert L == 512/8 + + len_in_bytes = count * 1 * L + uniform_bytes = expand_message_xmd(msg, dst, len_in_bytes) + + elements = [] + for i in range(0, count): + for j in range(0, m): + elm_offset = L * (j + i * m) + tv = uniform_bytes[elm_offset:elm_offset+L] + elements.append(Fp(beos2ip(tv), False)) + + assert len(elements) == 2 + + return elements + +def map_to_curve_simple_swu(u): + zero = Fp(0) + assert zero.inv() == Fp(0) + + A = PALLAS_ISO_A + B = PALLAS_ISO_B + Z = Fp(-13, False) + c1 = -B / A + c2 = Fp(-1) + + tv1 = Z * u.exp(2) + tv2 = tv1.exp(2) + x1 = tv1 + tv2 + + x1 = x1.inv() + e1 = x1 == Fp(0) + x1 = x1 + Fp(1) + + if e1: + x1 = c2 + else: + x1 = x1 + + x1 = x1 * c1 # x1 = (-B / A) * (1 + (1 / (Z^2 * u^4 + Z * u^2))) + gx1 = x1.exp(2) + gx1 = gx1 + A + gx1 = gx1 * x1 + gx1 = gx1 + B # gx1 = g(x1) = x1^3 + A * x1 + B + x2 = tv1 * x1 # x2 = Z * u^2 * x1 + tv2 = tv1 * tv2 + gx2 = gx1 * tv2 # gx2 = (Z * u^2)^3 * gx1 + + e2 = (gx1.sqrt() is not None) + + x = x1 if e2 else x2 # If is_square(gx1), x = x1, else x = x2 + y2 = gx1 if e2 else gx2 # If is_square(gx1), y2 = gx1, else y2 = gx2 + y = y2.sqrt() + + e3 = u.sgn0() == y.sgn0() + + y = y if e3 else -y #y = CMOV(-y, y, e3) + + return orchard_iso_pallas.Point(x, y) + + +def group_hash(d, m): + dst = d + b"-" + b"pallas" + b"_XMD:BLAKE2b_SSWU_RO_" + + elems = hash_to_field(m, dst) + assert len(elems) == 2 + + q = [map_to_curve_simple_swu(elems[0]), map_to_curve_simple_swu(elems[1]) ] + + return (q[0] + q[1]).iso_map() + +SINSEMILLA_K = 10 + +def pad(n, m): + padding_needed = n * SINSEMILLA_K - m.len + zeros = BitArray('0b' + ('0' * padding_needed)) + m = m + zeros + + pieces = [] + for i in range(0, n): + pieces.append( + lebs2ip(m[i*SINSEMILLA_K:i*(SINSEMILLA_K + 1)]) + ) + + return pieces + +def sinsemilla_hash_to_point(d, m): + n = cldiv(m.len, SINSEMILLA_K) + m = pad(n, m) + acc = group_hash(b"z.cash:SinsemillaQ", d) + + for m_i in m: + acc = acc + group_hash(b"z.cash:SinsemillaS", i2leosp(32, m_i)) + acc + + return acc + +def sinsemilla_hash(d, m): + return sinsemilla_hash_to_point(d, m).extract() + +# m_bytes MUST be a b"byte string", otherwise it could be parsed as hex! +def sinsemilla_hash_bytes(d, m_bytes): + return sinsemilla_hash(d, BitArray(m_bytes)) + +if __name__ == "__main__": + sh = sinsemilla_hash_bytes(b"whatever", b"whatever2") + print(sh) From 816cfa00592be0b22ee281ab7e3eb5b31617c44a Mon Sep 17 00:00:00 2001 From: Daira Hopwood Date: Thu, 1 Apr 2021 23:14:28 +0100 Subject: [PATCH 02/28] Apply suggestions from code review --- orchard_iso_pallas.py | 2 +- orchard_pallas.py | 3 ++- orchard_sinsemilla.py | 13 +++++-------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/orchard_iso_pallas.py b/orchard_iso_pallas.py index 55398e6..ef66e4f 100644 --- a/orchard_iso_pallas.py +++ b/orchard_iso_pallas.py @@ -35,7 +35,7 @@ class Point(object): return None if y.s % 2 != y_sign: - y = Fp.ZERO - y + y = -y return Point(x, y) diff --git a/orchard_pallas.py b/orchard_pallas.py index 9ac2717..e9022ed 100644 --- a/orchard_pallas.py +++ b/orchard_pallas.py @@ -196,7 +196,8 @@ class Point(object): return Point(x, y) def extract(self): - assert not self.is_identity + if self.is_identity: + return 0 return self.x def __mul__(self, s): diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py index 5641c48..7078c5c 100644 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -44,10 +44,7 @@ def expand_message_xmd(msg, dst, len_in_bytes): assert len(b[1]) == b_in_bytes for i in range(2, ell + 1): - bi_input = b"\x00" * b_in_bytes - - for j in range(0, i): - bi_input = sxor(bi_input, b[j]) + bi_input = sxor(b[0], b[i-1]) assert len(bi_input) == b_in_bytes @@ -59,7 +56,7 @@ def expand_message_xmd(msg, dst, len_in_bytes): b.append(bi_ctx.digest()) assert len(b[i]) == b_in_bytes - return b''.join(b)[0:len_in_bytes] + return b''.join(b[1:])[0:len_in_bytes] def hash_to_field(msg, dst): k = 256 @@ -79,7 +76,7 @@ def hash_to_field(msg, dst): tv = uniform_bytes[elm_offset:elm_offset+L] elements.append(Fp(beos2ip(tv), False)) - assert len(elements) == 2 + assert len(elements) == count return elements @@ -166,10 +163,10 @@ def sinsemilla_hash_to_point(d, m): def sinsemilla_hash(d, m): return sinsemilla_hash_to_point(d, m).extract() -# m_bytes MUST be a b"byte string", otherwise it could be parsed as hex! def sinsemilla_hash_bytes(d, m_bytes): + assert isinstance(m_bytes, bytes) return sinsemilla_hash(d, BitArray(m_bytes)) if __name__ == "__main__": - sh = sinsemilla_hash_bytes(b"whatever", b"whatever2") + sh = sinsemilla_hash_bytes(b"z.cash:test", b"Trans rights now!") print(sh) From 9184c78c0f890242087ba436a8fd58893b6b9465 Mon Sep 17 00:00:00 2001 From: Daira Hopwood Date: Fri, 2 Apr 2021 17:56:14 +0100 Subject: [PATCH 03/28] Remove trailing spaces, and make orchard_iso_pallas.py and orchard_sinsemilla.py executable. Signed-off-by: Daira Hopwood --- orchard_iso_pallas.py | 0 orchard_sinsemilla.py | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) mode change 100644 => 100755 orchard_iso_pallas.py mode change 100644 => 100755 orchard_sinsemilla.py diff --git a/orchard_iso_pallas.py b/orchard_iso_pallas.py old mode 100644 new mode 100755 diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py old mode 100644 new mode 100755 index 7078c5c..8c6d4d7 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -13,7 +13,7 @@ from binascii import hexlify from bitstring import BitArray # https://stackoverflow.com/questions/2612720/how-to-do-bitwise-exclusive-or-of-two-strings-in-python -def sxor(s1,s2): +def sxor(s1,s2): return bytes([a ^ b for a,b in zip(s1,s2)]) def expand_message_xmd(msg, dst, len_in_bytes): @@ -75,7 +75,7 @@ def hash_to_field(msg, dst): elm_offset = L * (j + i * m) tv = uniform_bytes[elm_offset:elm_offset+L] elements.append(Fp(beos2ip(tv), False)) - + assert len(elements) == count return elements @@ -148,7 +148,7 @@ def pad(n, m): lebs2ip(m[i*SINSEMILLA_K:i*(SINSEMILLA_K + 1)]) ) - return pieces + return pieces def sinsemilla_hash_to_point(d, m): n = cldiv(m.len, SINSEMILLA_K) From 28370d5fe99cc0347b10228ab08c6ccf42b15bf1 Mon Sep 17 00:00:00 2001 From: Daira Hopwood Date: Fri, 2 Apr 2021 17:57:29 +0100 Subject: [PATCH 04/28] orchard_sinsemilla.py: corrections, and add test vector for SinsemillaHash. Signed-off-by: Daira Hopwood --- orchard_sinsemilla.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py index 8c6d4d7..26b7afc 100755 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -6,7 +6,7 @@ import math import orchard_iso_pallas from pyblake2 import blake2b, blake2s -from orchard_pallas import Fp, p, q, PALLAS_B +from orchard_pallas import Fp, p, q, PALLAS_B, Point from orchard_iso_pallas import PALLAS_ISO_B, PALLAS_ISO_A from sapling_utils import i2beosp, cldiv, beos2ip, i2leosp, lebs2ip from binascii import hexlify @@ -139,25 +139,28 @@ SINSEMILLA_K = 10 def pad(n, m): padding_needed = n * SINSEMILLA_K - m.len - zeros = BitArray('0b' + ('0' * padding_needed)) + zeros = BitArray(bin='0' * padding_needed) m = m + zeros pieces = [] - for i in range(0, n): + for i in range(n): pieces.append( - lebs2ip(m[i*SINSEMILLA_K:i*(SINSEMILLA_K + 1)]) + lebs2ip(m[i*SINSEMILLA_K : (i+1)*SINSEMILLA_K]) ) return pieces def sinsemilla_hash_to_point(d, m): + assert isinstance(m, BitArray) n = cldiv(m.len, SINSEMILLA_K) m = pad(n, m) acc = group_hash(b"z.cash:SinsemillaQ", d) + #print("acc", acc) for m_i in m: acc = acc + group_hash(b"z.cash:SinsemillaS", i2leosp(32, m_i)) + acc - + #print("acc", acc) + return acc def sinsemilla_hash(d, m): @@ -168,5 +171,12 @@ def sinsemilla_hash_bytes(d, m_bytes): return sinsemilla_hash(d, BitArray(m_bytes)) if __name__ == "__main__": - sh = sinsemilla_hash_bytes(b"z.cash:test", b"Trans rights now!") - print(sh) + # This is the Pallas test vector from the Sage and Rust code (in affine coordinates). + gh = group_hash(b"z.cash:test", b"Trans rights now!") + assert gh == Point(Fp(10899331951394555178876036573383466686793225972744812919361819919497009261523), + Fp(851679174277466283220362715537906858808436854303373129825287392516025427980)) + + # 40 bits, so no padding + sh = sinsemilla_hash_to_point(b"z.cash:test-Sinsemilla", BitArray(bin='0001011010100110001101100011011011110110')) + assert sh == Point(Fp(19681977528872088480295086998934490146368213853811658798708435106473481753752), + Fp(14670850419772526047574141291705097968771694788047376346841674072293161339903)) From 57c28945221033813414f0531ef655c173b51098 Mon Sep 17 00:00:00 2001 From: Daira Hopwood Date: Tue, 6 Apr 2021 22:51:29 +0100 Subject: [PATCH 05/28] Update orchard_pallas.py Replace 0 with Fp.ZERO in `extract`. Co-authored-by: Taylor Hornby --- orchard_pallas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchard_pallas.py b/orchard_pallas.py index e9022ed..109fa1b 100644 --- a/orchard_pallas.py +++ b/orchard_pallas.py @@ -197,7 +197,7 @@ class Point(object): def extract(self): if self.is_identity: - return 0 + return Fp.ZERO return self.x def __mul__(self, s): From a5579bab846e399612076fc1164b00f49484ede4 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Thu, 22 Apr 2021 14:16:44 -0600 Subject: [PATCH 06/28] Update orchard_sinsemilla.py Co-authored-by: Daira Hopwood --- orchard_sinsemilla.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py index 26b7afc..844d357 100755 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -5,7 +5,7 @@ import math import orchard_iso_pallas -from pyblake2 import blake2b, blake2s +from pyblake2 import blake2b from orchard_pallas import Fp, p, q, PALLAS_B, Point from orchard_iso_pallas import PALLAS_ISO_B, PALLAS_ISO_A from sapling_utils import i2beosp, cldiv, beos2ip, i2leosp, lebs2ip From 75ce7b22be0683896d0f17d4883e19d7972026d7 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Thu, 22 Apr 2021 14:17:02 -0600 Subject: [PATCH 07/28] Update orchard_sinsemilla.py Co-authored-by: str4d --- orchard_sinsemilla.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py index 844d357..979737c 100755 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -33,7 +33,7 @@ def expand_message_xmd(msg, dst, len_in_bytes): b = [] - b0_ctx = blake2b(digest_size=64, person=i2beosp(128,0)) + b0_ctx = blake2b(digest_size=b_in_bytes, person=i2beosp(128,0)) b0_ctx.update(msg_prime) b.append(b0_ctx.digest()) assert len(b[0]) == b_in_bytes From 8c2275a4b55b0d204ff9a1f41430e52f2e55dc37 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Thu, 22 Apr 2021 14:17:12 -0600 Subject: [PATCH 08/28] Update orchard_sinsemilla.py Co-authored-by: str4d --- orchard_sinsemilla.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py index 979737c..183b904 100755 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -38,7 +38,7 @@ def expand_message_xmd(msg, dst, len_in_bytes): b.append(b0_ctx.digest()) assert len(b[0]) == b_in_bytes - b1_ctx = blake2b(digest_size=64, person=i2beosp(128,0)) + b1_ctx = blake2b(digest_size=b_in_bytes, person=i2beosp(128,0)) b1_ctx.update(b[0] + i2beosp(8, 1) + dst_prime) b.append(b1_ctx.digest()) assert len(b[1]) == b_in_bytes From b8dccde4c45b53e2f452f0901d7a6819719d7b37 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Thu, 22 Apr 2021 14:17:21 -0600 Subject: [PATCH 09/28] Update orchard_sinsemilla.py Co-authored-by: str4d --- orchard_sinsemilla.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py index 183b904..8b53a85 100755 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -50,7 +50,7 @@ def expand_message_xmd(msg, dst, len_in_bytes): bi_input += i2beosp(8, i) + dst_prime - bi_ctx = blake2b(digest_size=64, person=i2beosp(128,0)) + bi_ctx = blake2b(digest_size=b_in_bytes, person=i2beosp(128,0)) bi_ctx.update(bi_input) b.append(bi_ctx.digest()) From 4e7088691837da025bbf023b3c54ab66f2f44754 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Thu, 22 Apr 2021 14:30:45 -0600 Subject: [PATCH 10/28] Document the fact that the iso-Pallas generator is arbitrary --- orchard_iso_pallas.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/orchard_iso_pallas.py b/orchard_iso_pallas.py index ef66e4f..0f55f61 100755 --- a/orchard_iso_pallas.py +++ b/orchard_iso_pallas.py @@ -160,6 +160,8 @@ class Point(object): Point.ZERO = Point.identity() +# This is an arbitrarily-chosen generator for testing purposes only, NOT a +# formally-selected common generator for iso-Pallas. x = Fp(2) y2 = x * x * x + PALLAS_ISO_A * x + PALLAS_ISO_B y = y2.sqrt() From d9750457b94b78aaa8ff08132d249f7656d4a72a Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Thu, 22 Apr 2021 14:31:31 -0600 Subject: [PATCH 11/28] Update orchard_sinsemilla.py Co-authored-by: str4d --- orchard_sinsemilla.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py index 8b53a85..79fbaa2 100755 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -66,7 +66,7 @@ def hash_to_field(msg, dst): L = cldiv(math.ceil(math.log2(p)) + k, 8) assert L == 512/8 - len_in_bytes = count * 1 * L + len_in_bytes = count * m * L uniform_bytes = expand_message_xmd(msg, dst, len_in_bytes) elements = [] From e141b76203375eaad45ae571cee72a3f3d449c28 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Thu, 22 Apr 2021 14:31:55 -0600 Subject: [PATCH 12/28] Fix missing divide-by-Z Co-authored-by: str4d --- orchard_sinsemilla.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py index 79fbaa2..aef017d 100755 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -88,7 +88,7 @@ def map_to_curve_simple_swu(u): B = PALLAS_ISO_B Z = Fp(-13, False) c1 = -B / A - c2 = Fp(-1) + c2 = Fp(-1) / Z tv1 = Z * u.exp(2) tv2 = tv1.exp(2) From 9f0201846540fd46a75e79ab2612c1c2e0c032b9 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Thu, 22 Apr 2021 14:34:47 -0600 Subject: [PATCH 13/28] Clarify index-vs-exponent variable naming Co-authored-by: str4d --- orchard_sinsemilla.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py index aef017d..b64fae1 100755 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -115,8 +115,8 @@ def map_to_curve_simple_swu(u): e2 = (gx1.sqrt() is not None) x = x1 if e2 else x2 # If is_square(gx1), x = x1, else x = x2 - y2 = gx1 if e2 else gx2 # If is_square(gx1), y2 = gx1, else y2 = gx2 - y = y2.sqrt() + yy = gx1 if e2 else gx2 # If is_square(gx1), yy = gx1, else yy = gx2 + y = yy.sqrt() e3 = u.sgn0() == y.sgn0() From 756abfbb9c1f165e669699a3453f329d91136745 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Thu, 22 Apr 2021 14:36:38 -0600 Subject: [PATCH 14/28] Delete sinsemilla_hash_bytes which was unused and not guaranteed to have correct endianness --- orchard_sinsemilla.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py index b64fae1..054d4a0 100755 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -166,10 +166,6 @@ def sinsemilla_hash_to_point(d, m): def sinsemilla_hash(d, m): return sinsemilla_hash_to_point(d, m).extract() -def sinsemilla_hash_bytes(d, m_bytes): - assert isinstance(m_bytes, bytes) - return sinsemilla_hash(d, BitArray(m_bytes)) - if __name__ == "__main__": # This is the Pallas test vector from the Sage and Rust code (in affine coordinates). gh = group_hash(b"z.cash:test", b"Trans rights now!") From fb45bda972d8f2a2f64ac49334f176867969fea4 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Mon, 26 Apr 2021 18:06:04 -0600 Subject: [PATCH 15/28] Document bitstring dependency in the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 21e5a39..0196191 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Code to generate test vectors for various parts of Zcash. -Requires `pyblake2`. +Requires `pyblake2` and `bitstring`. `sapling_note_encryption.py` also requires `chacha20poly1305`. From a2bf6c5a04e2f0b117ea2dbb6a231367d6b0672e Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Mon, 26 Apr 2021 18:21:01 -0600 Subject: [PATCH 16/28] Check for incomplete addition exceptional cases in Sinsemilla --- orchard_pallas.py | 7 +++++++ orchard_sinsemilla.py | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/orchard_pallas.py b/orchard_pallas.py index 109fa1b..40c24da 100644 --- a/orchard_pallas.py +++ b/orchard_pallas.py @@ -182,6 +182,13 @@ class Point(object): else: return self.double() + def checked_incomplete_add(self, a): + assert self != a + assert self != -a + assert self != Point.identity() + assert self != Point.identity() + return self + a + def __sub__(self, a): return (-a) + self diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py index 054d4a0..e76224d 100755 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -155,11 +155,11 @@ def sinsemilla_hash_to_point(d, m): n = cldiv(m.len, SINSEMILLA_K) m = pad(n, m) acc = group_hash(b"z.cash:SinsemillaQ", d) - #print("acc", acc) for m_i in m: - acc = acc + group_hash(b"z.cash:SinsemillaS", i2leosp(32, m_i)) + acc - #print("acc", acc) + acc = acc.checked_incomplete_add( + group_hash(b"z.cash:SinsemillaS", i2leosp(32, m_i)) + ).checked_incomplete_add(acc) return acc From 027b3627736de87624b9167cafe19ff66e2ec321 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Mon, 26 Apr 2021 18:28:43 -0600 Subject: [PATCH 17/28] Some cleanup and move group_hash into its own file --- orchard_group_hash.py | 134 ++++++++++++++++++++++++++++++++++++++++++ orchard_sinsemilla.py | 131 +---------------------------------------- 2 files changed, 137 insertions(+), 128 deletions(-) create mode 100755 orchard_group_hash.py diff --git a/orchard_group_hash.py b/orchard_group_hash.py new file mode 100755 index 0000000..c1ea902 --- /dev/null +++ b/orchard_group_hash.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +import sys; assert sys.version_info[0] >= 3, "Python 3 required." + +import math + +import orchard_iso_pallas + +from pyblake2 import blake2b +from orchard_pallas import Fp, p, q, PALLAS_B, Point +from orchard_iso_pallas import PALLAS_ISO_B, PALLAS_ISO_A +from sapling_utils import i2beosp, cldiv, beos2ip, i2leosp, lebs2ip + +# https://stackoverflow.com/questions/2612720/how-to-do-bitwise-exclusive-or-of-two-strings-in-python +def sxor(s1,s2): + return bytes([a ^ b for a,b in zip(s1,s2)]) + +def expand_message_xmd(msg, dst, len_in_bytes): + assert len(dst) <= 255 + + b_in_bytes = 64 # hash function output size + r_in_bytes = 128 + + ell = cldiv(len_in_bytes, b_in_bytes) + + assert ell <= 255 + + dst_prime = dst + i2beosp(8, len(dst)) + z_pad = b"\x00" * r_in_bytes + l_i_b_str = i2beosp(16, len_in_bytes) + msg_prime = z_pad + msg + l_i_b_str + i2beosp(8, 0) + dst_prime + + b = [] + + b0_ctx = blake2b(digest_size=b_in_bytes, person=i2beosp(128,0)) + b0_ctx.update(msg_prime) + b.append(b0_ctx.digest()) + assert len(b[0]) == b_in_bytes + + b1_ctx = blake2b(digest_size=b_in_bytes, person=i2beosp(128,0)) + b1_ctx.update(b[0] + i2beosp(8, 1) + dst_prime) + b.append(b1_ctx.digest()) + assert len(b[1]) == b_in_bytes + + for i in range(2, ell + 1): + bi_input = sxor(b[0], b[i-1]) + + assert len(bi_input) == b_in_bytes + + bi_input += i2beosp(8, i) + dst_prime + + bi_ctx = blake2b(digest_size=b_in_bytes, person=i2beosp(128,0)) + bi_ctx.update(bi_input) + + b.append(bi_ctx.digest()) + assert len(b[i]) == b_in_bytes + + return b''.join(b[1:])[0:len_in_bytes] + +def hash_to_field(msg, dst): + k = 256 + count = 2 + m = 1 + + L = cldiv(math.ceil(math.log2(p)) + k, 8) + assert L == 512/8 + + len_in_bytes = count * m * L + uniform_bytes = expand_message_xmd(msg, dst, len_in_bytes) + + elements = [] + for i in range(0, count): + for j in range(0, m): + elm_offset = L * (j + i * m) + tv = uniform_bytes[elm_offset:elm_offset+L] + elements.append(Fp(beos2ip(tv), False)) + + assert len(elements) == count + + return elements + +def map_to_curve_simple_swu(u): + # The notation below follows Appendix F.2 of the Internet Draft + zero = Fp(0) + assert zero.inv() == Fp(0) + + A = PALLAS_ISO_A + B = PALLAS_ISO_B + Z = Fp(-13, False) + c1 = -B / A + c2 = Fp(-1) / Z + + tv1 = Z * u.exp(2) + tv2 = tv1.exp(2) + x1 = tv1 + tv2 + + x1 = x1.inv() + e1 = x1 == Fp(0) + x1 = x1 + Fp(1) + + if e1: + x1 = c2 + else: + x1 = x1 + + x1 = x1 * c1 # x1 = (-B / A) * (1 + (1 / (Z^2 * u^4 + Z * u^2))) + gx1 = x1.exp(2) + gx1 = gx1 + A + gx1 = gx1 * x1 + gx1 = gx1 + B # gx1 = g(x1) = x1^3 + A * x1 + B + x2 = tv1 * x1 # x2 = Z * u^2 * x1 + tv2 = tv1 * tv2 + gx2 = gx1 * tv2 # gx2 = (Z * u^2)^3 * gx1 + + e2 = (gx1.sqrt() is not None) + + x = x1 if e2 else x2 # If is_square(gx1), x = x1, else x = x2 + yy = gx1 if e2 else gx2 # If is_square(gx1), yy = gx1, else yy = gx2 + y = yy.sqrt() + + e3 = u.sgn0() == y.sgn0() + + y = y if e3 else -y #y = CMOV(-y, y, e3) + + return orchard_iso_pallas.Point(x, y) + +def group_hash(d, m): + dst = d + b"-" + b"pallas" + b"_XMD:BLAKE2b_SSWU_RO_" + + elems = hash_to_field(m, dst) + assert len(elems) == 2 + + q = [map_to_curve_simple_swu(elems[0]), map_to_curve_simple_swu(elems[1]) ] + + return (q[0] + q[1]).iso_map() \ No newline at end of file diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py index e76224d..6a70498 100755 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -5,135 +5,10 @@ import math import orchard_iso_pallas -from pyblake2 import blake2b -from orchard_pallas import Fp, p, q, PALLAS_B, Point -from orchard_iso_pallas import PALLAS_ISO_B, PALLAS_ISO_A -from sapling_utils import i2beosp, cldiv, beos2ip, i2leosp, lebs2ip -from binascii import hexlify +from orchard_pallas import Fp, Point +from sapling_utils import cldiv, lebs2ip, i2leosp from bitstring import BitArray - -# https://stackoverflow.com/questions/2612720/how-to-do-bitwise-exclusive-or-of-two-strings-in-python -def sxor(s1,s2): - return bytes([a ^ b for a,b in zip(s1,s2)]) - -def expand_message_xmd(msg, dst, len_in_bytes): - assert len(dst) <= 255 - - b_in_bytes = 64 # hash function output size - r_in_bytes = 128 - - ell = cldiv(len_in_bytes, b_in_bytes) - - assert ell <= 255 - - dst_prime = dst + i2beosp(8, len(dst)) - z_pad = b"\x00" * r_in_bytes - l_i_b_str = i2beosp(16, len_in_bytes) - msg_prime = z_pad + msg + l_i_b_str + i2beosp(8, 0) + dst_prime - - b = [] - - b0_ctx = blake2b(digest_size=b_in_bytes, person=i2beosp(128,0)) - b0_ctx.update(msg_prime) - b.append(b0_ctx.digest()) - assert len(b[0]) == b_in_bytes - - b1_ctx = blake2b(digest_size=b_in_bytes, person=i2beosp(128,0)) - b1_ctx.update(b[0] + i2beosp(8, 1) + dst_prime) - b.append(b1_ctx.digest()) - assert len(b[1]) == b_in_bytes - - for i in range(2, ell + 1): - bi_input = sxor(b[0], b[i-1]) - - assert len(bi_input) == b_in_bytes - - bi_input += i2beosp(8, i) + dst_prime - - bi_ctx = blake2b(digest_size=b_in_bytes, person=i2beosp(128,0)) - bi_ctx.update(bi_input) - - b.append(bi_ctx.digest()) - assert len(b[i]) == b_in_bytes - - return b''.join(b[1:])[0:len_in_bytes] - -def hash_to_field(msg, dst): - k = 256 - count = 2 - m = 1 - - L = cldiv(math.ceil(math.log2(p)) + k, 8) - assert L == 512/8 - - len_in_bytes = count * m * L - uniform_bytes = expand_message_xmd(msg, dst, len_in_bytes) - - elements = [] - for i in range(0, count): - for j in range(0, m): - elm_offset = L * (j + i * m) - tv = uniform_bytes[elm_offset:elm_offset+L] - elements.append(Fp(beos2ip(tv), False)) - - assert len(elements) == count - - return elements - -def map_to_curve_simple_swu(u): - zero = Fp(0) - assert zero.inv() == Fp(0) - - A = PALLAS_ISO_A - B = PALLAS_ISO_B - Z = Fp(-13, False) - c1 = -B / A - c2 = Fp(-1) / Z - - tv1 = Z * u.exp(2) - tv2 = tv1.exp(2) - x1 = tv1 + tv2 - - x1 = x1.inv() - e1 = x1 == Fp(0) - x1 = x1 + Fp(1) - - if e1: - x1 = c2 - else: - x1 = x1 - - x1 = x1 * c1 # x1 = (-B / A) * (1 + (1 / (Z^2 * u^4 + Z * u^2))) - gx1 = x1.exp(2) - gx1 = gx1 + A - gx1 = gx1 * x1 - gx1 = gx1 + B # gx1 = g(x1) = x1^3 + A * x1 + B - x2 = tv1 * x1 # x2 = Z * u^2 * x1 - tv2 = tv1 * tv2 - gx2 = gx1 * tv2 # gx2 = (Z * u^2)^3 * gx1 - - e2 = (gx1.sqrt() is not None) - - x = x1 if e2 else x2 # If is_square(gx1), x = x1, else x = x2 - yy = gx1 if e2 else gx2 # If is_square(gx1), yy = gx1, else yy = gx2 - y = yy.sqrt() - - e3 = u.sgn0() == y.sgn0() - - y = y if e3 else -y #y = CMOV(-y, y, e3) - - return orchard_iso_pallas.Point(x, y) - - -def group_hash(d, m): - dst = d + b"-" + b"pallas" + b"_XMD:BLAKE2b_SSWU_RO_" - - elems = hash_to_field(m, dst) - assert len(elems) == 2 - - q = [map_to_curve_simple_swu(elems[0]), map_to_curve_simple_swu(elems[1]) ] - - return (q[0] + q[1]).iso_map() +from orchard_group_hash import group_hash SINSEMILLA_K = 10 From 0c6c988e2ee079ed262bae3a802c05d840d9f9f3 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Mon, 26 Apr 2021 18:34:31 -0600 Subject: [PATCH 18/28] Call iso_map in map_to_curve_simple_swu --- orchard_group_hash.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/orchard_group_hash.py b/orchard_group_hash.py index c1ea902..a569bd3 100755 --- a/orchard_group_hash.py +++ b/orchard_group_hash.py @@ -121,7 +121,7 @@ def map_to_curve_simple_swu(u): y = y if e3 else -y #y = CMOV(-y, y, e3) - return orchard_iso_pallas.Point(x, y) + return orchard_iso_pallas.Point(x, y).iso_map() def group_hash(d, m): dst = d + b"-" + b"pallas" + b"_XMD:BLAKE2b_SSWU_RO_" @@ -129,6 +129,6 @@ def group_hash(d, m): elems = hash_to_field(m, dst) assert len(elems) == 2 - q = [map_to_curve_simple_swu(elems[0]), map_to_curve_simple_swu(elems[1]) ] + q = [map_to_curve_simple_swu(elems[0]), map_to_curve_simple_swu(elems[1])] - return (q[0] + q[1]).iso_map() \ No newline at end of file + return q[0] + q[1] \ No newline at end of file From 943aa69e66dcce387706bbb1e7a10fc5bea6cfe7 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 27 Apr 2021 15:12:20 +1200 Subject: [PATCH 19/28] Remove bitstring dependency Instead we just allow an iterable of bit-like elements. --- README.md | 2 +- orchard_sinsemilla.py | 27 +++++++++++++-------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 0196191..21e5a39 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Code to generate test vectors for various parts of Zcash. -Requires `pyblake2` and `bitstring`. +Requires `pyblake2`. `sapling_note_encryption.py` also requires `chacha20poly1305`. diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py index 6a70498..fe611a1 100755 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -7,27 +7,26 @@ import orchard_iso_pallas from orchard_pallas import Fp, Point from sapling_utils import cldiv, lebs2ip, i2leosp -from bitstring import BitArray from orchard_group_hash import group_hash SINSEMILLA_K = 10 +# Interprets a string or a list as a sequence of bits. +def str_to_bits(s): + for c in s: + assert c in ['0', '1', 0, 1, False, True] + # Regular Python truthiness is fine here except for bool('0') == True. + return [c != '0' and bool(c) for c in s] + def pad(n, m): - padding_needed = n * SINSEMILLA_K - m.len - zeros = BitArray(bin='0' * padding_needed) - m = m + zeros + padding_needed = n * SINSEMILLA_K - len(m) + zeros = [0] * padding_needed + m = list(m) + zeros - pieces = [] - for i in range(n): - pieces.append( - lebs2ip(m[i*SINSEMILLA_K : (i+1)*SINSEMILLA_K]) - ) - - return pieces + return [lebs2ip(str_to_bits(m[i*SINSEMILLA_K : (i+1)*SINSEMILLA_K])) for i in range(n)] def sinsemilla_hash_to_point(d, m): - assert isinstance(m, BitArray) - n = cldiv(m.len, SINSEMILLA_K) + n = cldiv(len(m), SINSEMILLA_K) m = pad(n, m) acc = group_hash(b"z.cash:SinsemillaQ", d) @@ -48,6 +47,6 @@ if __name__ == "__main__": Fp(851679174277466283220362715537906858808436854303373129825287392516025427980)) # 40 bits, so no padding - sh = sinsemilla_hash_to_point(b"z.cash:test-Sinsemilla", BitArray(bin='0001011010100110001101100011011011110110')) + sh = sinsemilla_hash_to_point(b"z.cash:test-Sinsemilla", '0001011010100110001101100011011011110110') assert sh == Point(Fp(19681977528872088480295086998934490146368213853811658798708435106473481753752), Fp(14670850419772526047574141291705097968771694788047376346841674072293161339903)) From 4052b6404b3104bf37303d1068a6bbbb411a260c Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 27 Apr 2021 15:41:19 +1200 Subject: [PATCH 20/28] Add rendering logic to Orchard GroupHash and Sinsemilla --- orchard_group_hash.py | 35 ++++++++++++++++++++++++++++++++++- orchard_sinsemilla.py | 41 ++++++++++++++++++++++++++++++++++------- tv_output.py | 13 +++++++++++++ 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/orchard_group_hash.py b/orchard_group_hash.py index a569bd3..0fda02a 100755 --- a/orchard_group_hash.py +++ b/orchard_group_hash.py @@ -9,6 +9,7 @@ from pyblake2 import blake2b from orchard_pallas import Fp, p, q, PALLAS_B, Point from orchard_iso_pallas import PALLAS_ISO_B, PALLAS_ISO_A from sapling_utils import i2beosp, cldiv, beos2ip, i2leosp, lebs2ip +from tv_output import render_args, render_tv # https://stackoverflow.com/questions/2612720/how-to-do-bitwise-exclusive-or-of-two-strings-in-python def sxor(s1,s2): @@ -131,4 +132,36 @@ def group_hash(d, m): q = [map_to_curve_simple_swu(elems[0]), map_to_curve_simple_swu(elems[1])] - return q[0] + q[1] \ No newline at end of file + return q[0] + q[1] + + +def main(): + test_vectors = [ + (b"z.cash:test", b"Trans rights now!"), + ] + + # This is the Pallas test vector from the Sage and Rust code (in affine coordinates). + gh = group_hash(test_vectors[0][0], test_vectors[0][1]) + assert gh == Point(Fp(10899331951394555178876036573383466686793225972744812919361819919497009261523), + Fp(851679174277466283220362715537906858808436854303373129825287392516025427980)) + + test_vectors = [{ + 'domain': domain, + 'msg': msg, + 'point': bytes(group_hash(domain, msg)), + } for (domain, msg) in test_vectors] + + render_tv( + render_args(), + 'orchard_group_hash', + ( + ('domain', 'Vec'), + ('msg', 'Vec'), + ('point', '[u8; 32]'), + ), + test_vectors, + ) + + +if __name__ == "__main__": + main() diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py index fe611a1..ca2304c 100755 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -8,6 +8,7 @@ import orchard_iso_pallas from orchard_pallas import Fp, Point from sapling_utils import cldiv, lebs2ip, i2leosp from orchard_group_hash import group_hash +from tv_output import render_args, render_tv SINSEMILLA_K = 10 @@ -40,13 +41,39 @@ def sinsemilla_hash_to_point(d, m): def sinsemilla_hash(d, m): return sinsemilla_hash_to_point(d, m).extract() -if __name__ == "__main__": - # This is the Pallas test vector from the Sage and Rust code (in affine coordinates). - gh = group_hash(b"z.cash:test", b"Trans rights now!") - assert gh == Point(Fp(10899331951394555178876036573383466686793225972744812919361819919497009261523), - Fp(851679174277466283220362715537906858808436854303373129825287392516025427980)) - # 40 bits, so no padding - sh = sinsemilla_hash_to_point(b"z.cash:test-Sinsemilla", '0001011010100110001101100011011011110110') +def main(): + test_vectors = [ + # 40 bits, so no padding + (b"z.cash:test-Sinsemilla", [0,0,0,1,0,1,1,0,1,0,1,0,0,1,1,0,0,0,1,1,0,1,1,0,0,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0]), + ] + + sh = sinsemilla_hash_to_point(test_vectors[0][0], test_vectors[0][1]) assert sh == Point(Fp(19681977528872088480295086998934490146368213853811658798708435106473481753752), Fp(14670850419772526047574141291705097968771694788047376346841674072293161339903)) + + test_vectors = [{ + 'domain': domain, + 'msg': msg, + 'point': bytes(sinsemilla_hash_to_point(domain, msg)), + 'hash': bytes(sinsemilla_hash(domain, msg)), + } for (domain, msg) in test_vectors] + + render_tv( + render_args(), + 'orchard_sinsemilla', + ( + ('domain', 'Vec'), + ('msg', { + 'rust_type': 'Vec', + 'rust_fmt': lambda x: str_to_bits(x), + }), + ('point', '[u8; 32]'), + ('hash', '[u8; 32]'), + ), + test_vectors, + ) + + +if __name__ == "__main__": + main() diff --git a/tv_output.py b/tv_output.py index fcb225d..90180a2 100644 --- a/tv_output.py +++ b/tv_output.py @@ -75,6 +75,17 @@ def tv_vec_bytes_rust(name, value, pad): pad, )) +def tv_vec_bool_rust(name, value, pad): + print('''%s%s: vec![ + %s%s +%s],''' % ( + pad, + name, + pad, + ', '.join(['true' if x else 'false' for x in value]), + pad, + )) + def tv_option_bytes_rust(name, value, pad): if value: print('''%s%s: Some([ @@ -121,6 +132,8 @@ def tv_part_rust(name, value, config, indent=3): tv_option_vec_bytes_rust(name, value, pad) elif config['rust_type'] == 'Vec': tv_vec_bytes_rust(name, value, pad) + elif config['rust_type'] == 'Vec': + tv_vec_bool_rust(name, value, pad) elif config['rust_type'].startswith('Option<['): tv_option_bytes_rust(name, value, pad) elif type(value) == bytes: From 86c2796de8263c870a83634c6812e0f992d098f9 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Mon, 26 Apr 2021 23:35:30 -0600 Subject: [PATCH 21/28] Fix broken incomplete addition case check Co-authored-by: str4d --- orchard_pallas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchard_pallas.py b/orchard_pallas.py index 40c24da..8a9268d 100644 --- a/orchard_pallas.py +++ b/orchard_pallas.py @@ -186,7 +186,7 @@ class Point(object): assert self != a assert self != -a assert self != Point.identity() - assert self != Point.identity() + assert a != Point.identity() return self + a def __sub__(self, a): From 322aff1777f8ccd74bbea85eb05d65d845a5827e Mon Sep 17 00:00:00 2001 From: Daira Hopwood Date: Tue, 27 Apr 2021 14:26:12 +0100 Subject: [PATCH 22/28] orchard_group_hash.py: don't apply iso_map as part of map_to_curve_simple_swu. Signed-off-by: Daira Hopwood --- orchard_group_hash.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/orchard_group_hash.py b/orchard_group_hash.py index 0fda02a..59cb36f 100755 --- a/orchard_group_hash.py +++ b/orchard_group_hash.py @@ -122,7 +122,7 @@ def map_to_curve_simple_swu(u): y = y if e3 else -y #y = CMOV(-y, y, e3) - return orchard_iso_pallas.Point(x, y).iso_map() + return orchard_iso_pallas.Point(x, y) def group_hash(d, m): dst = d + b"-" + b"pallas" + b"_XMD:BLAKE2b_SSWU_RO_" @@ -130,7 +130,7 @@ def group_hash(d, m): elems = hash_to_field(m, dst) assert len(elems) == 2 - q = [map_to_curve_simple_swu(elems[0]), map_to_curve_simple_swu(elems[1])] + q = [map_to_curve_simple_swu(elems[0]).iso_map(), map_to_curve_simple_swu(elems[1]).iso_map()] return q[0] + q[1] From 634c599fc627713609bcd6c4d702be115875e50e Mon Sep 17 00:00:00 2001 From: Daira Hopwood Date: Tue, 27 Apr 2021 14:28:08 +0100 Subject: [PATCH 23/28] orchard_group_hash: minor refactoring. Signed-off-by: Daira Hopwood --- orchard_group_hash.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/orchard_group_hash.py b/orchard_group_hash.py index 59cb36f..9420704 100755 --- a/orchard_group_hash.py +++ b/orchard_group_hash.py @@ -136,21 +136,17 @@ def group_hash(d, m): def main(): - test_vectors = [ - (b"z.cash:test", b"Trans rights now!"), + group_hash_test_vectors = [ + # This is the Pallas test vector from the Sage and Rust code (in affine coordinates). + (b"z.cash:test", b"Trans rights now!", Point(Fp(10899331951394555178876036573383466686793225972744812919361819919497009261523), + Fp(851679174277466283220362715537906858808436854303373129825287392516025427980))), ] - # This is the Pallas test vector from the Sage and Rust code (in affine coordinates). - gh = group_hash(test_vectors[0][0], test_vectors[0][1]) - assert gh == Point(Fp(10899331951394555178876036573383466686793225972744812919361819919497009261523), - Fp(851679174277466283220362715537906858808436854303373129825287392516025427980)) - - test_vectors = [{ - 'domain': domain, - 'msg': msg, - 'point': bytes(group_hash(domain, msg)), - } for (domain, msg) in test_vectors] + for (domain, msg, point) in group_hash_test_vectors: + gh = group_hash(domain, msg) + assert gh == point + print("group_hash (Pallas):") render_tv( render_args(), 'orchard_group_hash', @@ -159,8 +155,13 @@ def main(): ('msg', 'Vec'), ('point', '[u8; 32]'), ), - test_vectors, + [{ + 'domain': domain, + 'msg': msg, + 'point': bytes(point), + } for (domain, msg, point) in group_hash_test_vectors], ) + print("") if __name__ == "__main__": From e981001efc42a84d92e2d16ec7d9426aad231df8 Mon Sep 17 00:00:00 2001 From: Daira Hopwood Date: Tue, 27 Apr 2021 14:28:48 +0100 Subject: [PATCH 24/28] orchard_group_hash: add test vectors for map_to_curve_simple_swu. Signed-off-by: Daira Hopwood --- orchard_group_hash.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/orchard_group_hash.py b/orchard_group_hash.py index 9420704..4cdeffa 100755 --- a/orchard_group_hash.py +++ b/orchard_group_hash.py @@ -136,6 +136,35 @@ def group_hash(d, m): def main(): + map_to_curve_test_vectors = [ + (Fp(0), orchard_iso_pallas.Point(Fp(19938918781445865934736160264407396416050199005817793816893455093350997047296), + Fp(1448774895934493446148762800986014913165975534940595774801697325542407056356))), + (Fp(1), orchard_iso_pallas.Point(Fp(5290181550357368025040301950220623271393946308300025648720253222947454165280), + Fp(24520995241805476578231005891941079870703368870355132644748659103632565232759))), + (Fp(0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef0123), + orchard_iso_pallas.Point(Fp(16711718778908753690082328243251803703269853000652055785581237369882690082595), + Fp(1764705856161931038824461929646873031992914829456409784642560948827969833589))), + ] + + for (u, point) in map_to_curve_test_vectors: + P = map_to_curve_simple_swu(u) + assert P == point + + print("map_to_curve_simple_swu (Pallas):") + render_tv( + render_args(), + 'orchard_group_hash', + ( + ('u', '[u8; 32]'), + ('point', '[u8; 32]'), + ), + [{ + 'u': bytes(u), + 'point': bytes(point), + } for (u, point) in map_to_curve_test_vectors], + ) + print("") + group_hash_test_vectors = [ # This is the Pallas test vector from the Sage and Rust code (in affine coordinates). (b"z.cash:test", b"Trans rights now!", Point(Fp(10899331951394555178876036573383466686793225972744812919361819919497009261523), From 10bdd6c5f8c27e449b96e6bd98c60cbc393f899a Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 29 Apr 2021 11:52:59 +1200 Subject: [PATCH 25/28] Split apart MapToCurveSimpleSWU and GroupHash test vectors The test vector generators in this repository are meant to output data that can be copy-pasted or piped to a file. Generating multiple sets of test vectors from a single file interferes with this. --- orchard_group_hash.py | 41 ++++++----------------------------------- orchard_map_to_curve.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 35 deletions(-) mode change 100755 => 100644 orchard_group_hash.py create mode 100755 orchard_map_to_curve.py diff --git a/orchard_group_hash.py b/orchard_group_hash.py old mode 100755 new mode 100644 index 4cdeffa..76382e8 --- a/orchard_group_hash.py +++ b/orchard_group_hash.py @@ -136,46 +136,18 @@ def group_hash(d, m): def main(): - map_to_curve_test_vectors = [ - (Fp(0), orchard_iso_pallas.Point(Fp(19938918781445865934736160264407396416050199005817793816893455093350997047296), - Fp(1448774895934493446148762800986014913165975534940595774801697325542407056356))), - (Fp(1), orchard_iso_pallas.Point(Fp(5290181550357368025040301950220623271393946308300025648720253222947454165280), - Fp(24520995241805476578231005891941079870703368870355132644748659103632565232759))), - (Fp(0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef0123), - orchard_iso_pallas.Point(Fp(16711718778908753690082328243251803703269853000652055785581237369882690082595), - Fp(1764705856161931038824461929646873031992914829456409784642560948827969833589))), - ] - - for (u, point) in map_to_curve_test_vectors: - P = map_to_curve_simple_swu(u) - assert P == point - - print("map_to_curve_simple_swu (Pallas):") - render_tv( - render_args(), - 'orchard_group_hash', - ( - ('u', '[u8; 32]'), - ('point', '[u8; 32]'), - ), - [{ - 'u': bytes(u), - 'point': bytes(point), - } for (u, point) in map_to_curve_test_vectors], - ) - print("") - - group_hash_test_vectors = [ + fixed_test_vectors = [ # This is the Pallas test vector from the Sage and Rust code (in affine coordinates). (b"z.cash:test", b"Trans rights now!", Point(Fp(10899331951394555178876036573383466686793225972744812919361819919497009261523), Fp(851679174277466283220362715537906858808436854303373129825287392516025427980))), ] - for (domain, msg, point) in group_hash_test_vectors: + for (domain, msg, point) in fixed_test_vectors: gh = group_hash(domain, msg) assert gh == point - print("group_hash (Pallas):") + test_vectors = [(domain, msg) for (domain, msg, _) in fixed_test_vectors] + render_tv( render_args(), 'orchard_group_hash', @@ -187,10 +159,9 @@ def main(): [{ 'domain': domain, 'msg': msg, - 'point': bytes(point), - } for (domain, msg, point) in group_hash_test_vectors], + 'point': bytes(group_hash(domain, msg)), + } for (domain, msg) in test_vectors], ) - print("") if __name__ == "__main__": diff --git a/orchard_map_to_curve.py b/orchard_map_to_curve.py new file mode 100755 index 0000000..dc42916 --- /dev/null +++ b/orchard_map_to_curve.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +from orchard_group_hash import map_to_curve_simple_swu +from orchard_iso_pallas import Point as IsoPoint +from orchard_pallas import Fp +from tv_output import render_args, render_tv + + +def main(): + fixed_test_vectors = [ + (Fp(0), IsoPoint(Fp(19938918781445865934736160264407396416050199005817793816893455093350997047296), + Fp(1448774895934493446148762800986014913165975534940595774801697325542407056356))), + (Fp(1), IsoPoint(Fp(5290181550357368025040301950220623271393946308300025648720253222947454165280), + Fp(24520995241805476578231005891941079870703368870355132644748659103632565232759))), + (Fp(0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef0123), + IsoPoint(Fp(16711718778908753690082328243251803703269853000652055785581237369882690082595), + Fp(1764705856161931038824461929646873031992914829456409784642560948827969833589))), + ] + + for (u, point) in fixed_test_vectors: + P = map_to_curve_simple_swu(u) + assert P == point + + test_vectors = [u for (u, _) in fixed_test_vectors] + + render_tv( + render_args(), + 'orchard_map_to_curve', + ( + ('u', '[u8; 32]'), + ('point', '[u8; 32]'), + ), + [{ + 'u': bytes(u), + 'point': bytes(map_to_curve_simple_swu(u)), + } for u in test_vectors], + ) + + +if __name__ == "__main__": + main() From c6cd47a5dd27e972df5901cb8bdc8793f4832597 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 29 Apr 2021 12:57:40 +1200 Subject: [PATCH 26/28] Fix Rand.u8() to return unsigned integers Existing test vector generators are adjusted to use Rand.i8() so they generate the same test vectors. We should evaluate these later to determine whether they should actually use Rand.u8() (and update the test vectors across the ecosystem). --- transaction.py | 12 ++++++------ tv_rand.py | 5 ++++- zip_0143.py | 2 +- zip_0243.py | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/transaction.py b/transaction.py index 7a2582b..aa3ca64 100644 --- a/transaction.py +++ b/transaction.py @@ -155,7 +155,7 @@ RAND_OPCODES = [ class Script(object): def __init__(self, rand): self._script = bytes([ - rand.a(RAND_OPCODES) for i in range(rand.u8() % 10) + rand.a(RAND_OPCODES) for i in range(rand.i8() % 10) ]) def raw(self): @@ -212,11 +212,11 @@ class Transaction(object): self.nVersion = rand.u32() & ((1 << 31) - 1) self.vin = [] - for i in range(rand.u8() % 3): + for i in range(rand.i8() % 3): self.vin.append(TxIn(rand)) self.vout = [] - for i in range(rand.u8() % 3): + for i in range(rand.i8() % 3): self.vout.append(TxOut(rand)) self.nLockTime = rand.u32() @@ -227,14 +227,14 @@ class Transaction(object): self.vShieldedSpends = [] self.vShieldedOutputs = [] if self.nVersion >= SAPLING_TX_VERSION: - for _ in range(rand.u8() % 5): + for _ in range(rand.i8() % 5): self.vShieldedSpends.append(SpendDescription(rand)) - for _ in range(rand.u8() % 5): + for _ in range(rand.i8() % 5): self.vShieldedOutputs.append(OutputDescription(rand)) self.vJoinSplit = [] if self.nVersion >= 2: - for i in range(rand.u8() % 3): + for i in range(rand.i8() % 3): self.vJoinSplit.append(JoinSplit(rand, self.fOverwintered and self.nVersion >= SAPLING_TX_VERSION)) if len(self.vJoinSplit) > 0: self.joinSplitPubKey = rand.b(32) # Potentially invalid diff --git a/tv_rand.py b/tv_rand.py index 3035286..e064332 100644 --- a/tv_rand.py +++ b/tv_rand.py @@ -12,9 +12,12 @@ class Rand(object): def v(self, l, f): return struct.unpack(f, self.b(l))[0] - def u8(self): + def i8(self): return self.v(1, 'b') + def u8(self): + return self.v(1, 'B') + def u32(self): return self.v(4, ' Date: Thu, 29 Apr 2021 12:59:16 +1200 Subject: [PATCH 27/28] Generate random test vectors for MapToCurve, GroupHash, Sinsemilla --- orchard_group_hash.py | 20 ++++++++++++++++++++ orchard_map_to_curve.py | 15 +++++++++++++++ orchard_sinsemilla.py | 20 ++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/orchard_group_hash.py b/orchard_group_hash.py index 76382e8..e0779c7 100644 --- a/orchard_group_hash.py +++ b/orchard_group_hash.py @@ -10,6 +10,7 @@ from orchard_pallas import Fp, p, q, PALLAS_B, Point from orchard_iso_pallas import PALLAS_ISO_B, PALLAS_ISO_A from sapling_utils import i2beosp, cldiv, beos2ip, i2leosp, lebs2ip from tv_output import render_args, render_tv +from tv_rand import Rand # https://stackoverflow.com/questions/2612720/how-to-do-bitwise-exclusive-or-of-two-strings-in-python def sxor(s1,s2): @@ -148,6 +149,25 @@ def main(): test_vectors = [(domain, msg) for (domain, msg, _) in fixed_test_vectors] + from random import Random + rng = Random(0xabad533d) + def randbytes(l): + ret = [] + while len(ret) < l: + ret.append(rng.randrange(0, 256)) + return bytes(ret) + rand = Rand(randbytes) + + # Generate test vectors with the following properties: + # - One of two domains. + # - Random message lengths between 0 and 255 bytes. + # - Random message contents. + for _ in range(10): + domain = b"z.cash:test-longer" if rand.bool() else b"z.cash:test" + msg_len = rand.u8() + msg = bytes([rand.u8() for _ in range(msg_len)]) + test_vectors.append((domain, msg)) + render_tv( render_args(), 'orchard_group_hash', diff --git a/orchard_map_to_curve.py b/orchard_map_to_curve.py index dc42916..4d43dd1 100755 --- a/orchard_map_to_curve.py +++ b/orchard_map_to_curve.py @@ -3,7 +3,9 @@ from orchard_group_hash import map_to_curve_simple_swu from orchard_iso_pallas import Point as IsoPoint from orchard_pallas import Fp +from sapling_utils import leos2ip from tv_output import render_args, render_tv +from tv_rand import Rand def main(): @@ -23,6 +25,19 @@ def main(): test_vectors = [u for (u, _) in fixed_test_vectors] + from random import Random + rng = Random(0xabad533d) + def randbytes(l): + ret = [] + while len(ret) < l: + ret.append(rng.randrange(0, 256)) + return bytes(ret) + rand = Rand(randbytes) + + # Generate random test vectors + for _ in range(10): + test_vectors.append(Fp(leos2ip(rand.b(32)))) + render_tv( render_args(), 'orchard_map_to_curve', diff --git a/orchard_sinsemilla.py b/orchard_sinsemilla.py index ca2304c..2662272 100755 --- a/orchard_sinsemilla.py +++ b/orchard_sinsemilla.py @@ -9,6 +9,7 @@ from orchard_pallas import Fp, Point from sapling_utils import cldiv, lebs2ip, i2leosp from orchard_group_hash import group_hash from tv_output import render_args, render_tv +from tv_rand import Rand SINSEMILLA_K = 10 @@ -52,6 +53,25 @@ def main(): assert sh == Point(Fp(19681977528872088480295086998934490146368213853811658798708435106473481753752), Fp(14670850419772526047574141291705097968771694788047376346841674072293161339903)) + from random import Random + rng = Random(0xabad533d) + def randbytes(l): + ret = [] + while len(ret) < l: + ret.append(rng.randrange(0, 256)) + return bytes(ret) + rand = Rand(randbytes) + + # Generate test vectors with the following properties: + # - One of two domains. + # - Random message lengths between 0 and 255 bytes. + # - Random message bits. + for _ in range(10): + domain = b"z.cash:test-Sinsemilla-longer" if rand.bool() else b"z.cash:test-Sinsemilla" + msg_len = rand.u8() + msg = bytes([rand.bool() for _ in range(msg_len)]) + test_vectors.append((domain, msg)) + test_vectors = [{ 'domain': domain, 'msg': msg, From f8008a01fdc76f384cf62e3283ea775281070b82 Mon Sep 17 00:00:00 2001 From: Daira Hopwood Date: Thu, 29 Apr 2021 02:07:46 +0100 Subject: [PATCH 28/28] Apply suggestions from code review Comments and a minor refactor for consistency. --- orchard_group_hash.py | 5 +---- orchard_iso_pallas.py | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/orchard_group_hash.py b/orchard_group_hash.py index e0779c7..5ef0c9e 100644 --- a/orchard_group_hash.py +++ b/orchard_group_hash.py @@ -99,10 +99,7 @@ def map_to_curve_simple_swu(u): e1 = x1 == Fp(0) x1 = x1 + Fp(1) - if e1: - x1 = c2 - else: - x1 = x1 + x1 = c2 if e1 else x1 # If (tv1 + tv2) == 0, set x1 = -1 / Z x1 = x1 * c1 # x1 = (-B / A) * (1 + (1 / (Z^2 * u^4 + Z * u^2))) gx1 = x1.exp(2) diff --git a/orchard_iso_pallas.py b/orchard_iso_pallas.py index 0f55f61..fbdfe93 100755 --- a/orchard_iso_pallas.py +++ b/orchard_iso_pallas.py @@ -97,6 +97,7 @@ class Point(object): elif a.is_identity: return self else: + # Hüseyin Hışıl. “Elliptic Curves, Group Law, and Efficient Computation”. PhD thesis. # section 4.1 (x1, y1) = (self.x, self.y) (x2, y2) = (a.x, a.y) @@ -119,6 +120,7 @@ class Point(object): if self.is_identity: return self + # Hüseyin Hışıl. “Elliptic Curves, Group Law, and Efficient Computation”. PhD thesis. # section 4.1 λ = (Fp(3) * self.x * self.x + PALLAS_ISO_A) / (self.y + self.y) x3 = λ*λ - self.x - self.x