Add iso-Pallas, SWU hash-to-curve, and Sinsemilla

Co-authored-by: Kris Nuttycombe <kris.nuttycombe@gmail.com>
This commit is contained in:
Taylor Hornby 2021-02-25 14:31:15 -07:00
parent 65ed28c661
commit c3a70e269b
3 changed files with 368 additions and 4 deletions

176
orchard_iso_pallas.py Normal file
View File

@ -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:
# <https://core.ac.uk/download/pdf/10898289.pdf> 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
# <https://core.ac.uk/download/pdf/10898289.pdf> 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

View File

@ -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')

175
orchard_sinsemilla.py Normal file
View File

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