Skip to content

Commit 21937db

Browse files
committed
core: add support for multiplication of secp256k1 points by scalars
1 parent 6046834 commit 21937db

File tree

2 files changed

+215
-0
lines changed

2 files changed

+215
-0
lines changed

opentimestamps/core/secp256k1.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Copyright (C) 2017 The OpenTimestamps developers
2+
#
3+
# This file is part of python-opentimestamps.
4+
#
5+
# It is subject to the license terms in the LICENSE file found in the top-level
6+
# directory of this distribution.
7+
#
8+
# No part of python-opentimestamps including this file, may be copied,
9+
# modified, propagated, or distributed except according to the terms contained
10+
# in the LICENSE file.
11+
12+
## What follows is a lot of inefficient but explicit secp256k1 math
13+
class Point(object):
14+
inf = True
15+
x = 0
16+
y = 0
17+
18+
def __init__(self, x=0, y=0):
19+
self.x = x
20+
self.y = y
21+
if x == 0 and y == 0:
22+
self.inf = True
23+
else:
24+
self.inf = False
25+
26+
def __repr__(self):
27+
if self.inf:
28+
return "Point(infinity)"
29+
else:
30+
return "Point(%x, %x)" % (self.x, self.y)
31+
32+
def __eq__(self, other):
33+
if isinstance(other, self.__class__):
34+
return (self.inf == True and other.inf == True) or\
35+
(self.inf == False and other.inf == False and self.x == other.x and self.y == other.y)
36+
else:
37+
return False
38+
39+
def __ne__(self, other):
40+
return not self.__eq__(other)
41+
42+
@staticmethod
43+
def decode(data):
44+
if len(data) != 33 or (data[0] != 2 and data[0] != 3):
45+
raise MsgValueError("Incorrectly formatted public key")
46+
47+
x = int.from_bytes(data[1:], 'big')
48+
if x >= SECP256K1_P:
49+
raise MsgValueError("out of range x coordinate for secp256k1 point")
50+
51+
ysqr = (x ** 3 + 7) % SECP256K1_P
52+
y = psqrt(ysqr)
53+
if pow(y, 2, SECP256K1_P) != ysqr:
54+
raise MsgValueError("invalid x coordinate for secp256k1 point")
55+
56+
if y % 2 == 1 and data[0] == 2:
57+
y = SECP256K1_P - y
58+
if y % 2 == 0 and data[0] == 3:
59+
y = SECP256K1_P - y
60+
61+
return Point(x, y)
62+
63+
def encode(self):
64+
ret = bytearray(self.x.to_bytes(33, 'big'))
65+
assert(ret[0] == 0)
66+
if self.y % 2 == 1:
67+
ret[0] = 3
68+
else:
69+
ret[0] = 2
70+
return ret
71+
72+
def add(self, pt):
73+
if self.inf:
74+
return pt
75+
if pt.inf:
76+
return self
77+
78+
if self.x == pt.x:
79+
if self.y == SECP256K1_P - pt.y:
80+
return Point()
81+
else:
82+
assert(self.y == pt.y)
83+
lam = (3 * self.x ** 2 * pinv(2 * self.y)) % SECP256K1_P
84+
else:
85+
lam = ((pt.y - self.y) * pinv(pt.x - self.x)) % SECP256K1_P
86+
87+
x3 = (lam ** 2 - self.x - pt.x) % SECP256K1_P
88+
y3 = (self.y + lam * (x3 - self.x)) % SECP256K1_P
89+
90+
return Point(x3, SECP256K1_P - y3)
91+
92+
def scalar_mul(self, s):
93+
ret = Point()
94+
add = self
95+
s = s % SECP256K1_N
96+
while s > 0:
97+
if s % 2 == 1:
98+
ret = ret.add(add)
99+
add = add.add(add) # add
100+
s >>= 1
101+
return ret
102+
103+
SECP256K1_P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
104+
SECP256K1_N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
105+
SECP256K1_GEN = Point(0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,
106+
0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8)
107+
108+
def pinv(x):
109+
return pow(x, SECP256K1_P - 2, SECP256K1_P)
110+
111+
def psqrt(x):
112+
# using `>> 2` in place of `/ 4` keeps everything as an int rather than float
113+
return pow(x, (SECP256K1_P + 1) >> 2, SECP256K1_P)
114+
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Copyright (C) 2017 The OpenTimestamps developers
2+
#
3+
# This file is part of python-opentimestamps.
4+
#
5+
# It is subject to the license terms in the LICENSE file found in the top-level
6+
# directory of this distribution.
7+
#
8+
# No part of python-opentimestamps including this file, may be copied,
9+
# modified, propagated, or distributed except according to the terms contained
10+
# in the LICENSE file.
11+
12+
import binascii
13+
import unittest
14+
15+
from opentimestamps.core.secp256k1 import *
16+
17+
class Test_Secp256k1(unittest.TestCase):
18+
def test_point_rt(self):
19+
"""Point encoding round trip"""
20+
gen = SECP256K1_GEN
21+
encode = gen.encode()
22+
self.assertEqual(encode, binascii.unhexlify("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"))
23+
gen2 = Point().decode(encode)
24+
self.assertEqual(gen, gen2)
25+
26+
def test_pinv(self):
27+
"""Field inversion mod p"""
28+
self.assertEqual(pinv(1), 1)
29+
self.assertEqual(pinv(2), 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffff7ffffe18)
30+
self.assertEqual(pinv(3), 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9fffffd75)
31+
self.assertEqual(2, pinv(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffff7ffffe18))
32+
self.assertEqual(3, pinv(0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9fffffd75))
33+
34+
def test_psqrt(self):
35+
"""Field square root mod p"""
36+
self.assertEqual(psqrt(1), 1)
37+
self.assertEqual(psqrt(2), 0x210c790573632359b1edb4302c117d8a132654692c3feeb7de3a86ac3f3b53f7)
38+
self.assertEqual(psqrt(4), 2)
39+
# may return the sqrt or its negative
40+
self.assertEqual(psqrt(9), SECP256K1_P - 3)
41+
self.assertEqual(psqrt(49), SECP256K1_P - 7)
42+
43+
def test_point_add(self):
44+
"""Point adding and doubling"""
45+
46+
inf = Point()
47+
# P random chosen by dice roll
48+
p1 = Point(0x394867ad93f5c9612e8d8b7600443334026e648e365337d799190e845d649e67,
49+
0x0b84af9a00c1a55a7ac03917e59b21c68d1ffdf18720c3ad279077049cfaaf63)
50+
# 2P
51+
p2 = Point(0x8e6575f6c759aea04a8ec65f61f71eba237a0af54292d41e3a4bac2efa922dea,
52+
0x2b3c07687787ff07ae312305f30481c451ae3b78d4f479a3b729615fedc040e4)
53+
# -2P
54+
np2 = Point(0x8e6575f6c759aea04a8ec65f61f71eba237a0af54292d41e3a4bac2efa922dea,
55+
0xd4c3f897887800f851cedcfa0cfb7e3bae51c4872b0b865c48d69e9f123fbb4b)
56+
# 3P
57+
p3 = Point(0x53dd5e495c7404790f9347470cc9c38ee239809c758f02ec04ba641ab3d0e043,
58+
0xd7a4f5e5bdf21000b1fe7216adbea92cb9917d8fea7b37628c1eddb409a5cd3f)
59+
60+
self.assertEqual(inf.add(inf), inf)
61+
self.assertEqual(p1.add(inf), p1)
62+
self.assertEqual(inf.add(p1), p1)
63+
self.assertEqual(p1.add(p1), p2)
64+
self.assertEqual(p1.add(p2), p3)
65+
self.assertEqual(p2.add(p1), p3)
66+
self.assertEqual(p3.add(np2), p1)
67+
self.assertEqual(np2.add(p3), p1)
68+
self.assertEqual(p2.add(np2), inf)
69+
self.assertEqual(np2.add(p2), inf)
70+
71+
def test_scalar_mul(self):
72+
inf = Point()
73+
# P random chosen by dice roll
74+
p1 = Point(0x394867ad93f5c9612e8d8b7600443334026e648e365337d799190e845d649e67,
75+
0x0b84af9a00c1a55a7ac03917e59b21c68d1ffdf18720c3ad279077049cfaaf63)
76+
# 2P
77+
p2 = Point(0x8e6575f6c759aea04a8ec65f61f71eba237a0af54292d41e3a4bac2efa922dea,
78+
0x2b3c07687787ff07ae312305f30481c451ae3b78d4f479a3b729615fedc040e4)
79+
# -2P
80+
np2 = Point(0x8e6575f6c759aea04a8ec65f61f71eba237a0af54292d41e3a4bac2efa922dea,
81+
0xd4c3f897887800f851cedcfa0cfb7e3bae51c4872b0b865c48d69e9f123fbb4b)
82+
# 3P
83+
p3 = Point(0x53dd5e495c7404790f9347470cc9c38ee239809c758f02ec04ba641ab3d0e043,
84+
0xd7a4f5e5bdf21000b1fe7216adbea92cb9917d8fea7b37628c1eddb409a5cd3f)
85+
86+
# nP
87+
n = 0xa91ce154dcab9adabe08cc1ee84ec3cd0f426bbc08a54a1c41bd25f2587caedd
88+
pn = Point(0x9dc4b057a857ad2ef3535b4a207a7bfc9264e8fcacf718c895db7ead8d445b26,
89+
0x5af110ecb68636e5c352b69fc6348173932b83ca64587a91fd88af1446e33979)
90+
91+
self.assertEqual(inf.scalar_mul(0), inf)
92+
self.assertEqual(inf.scalar_mul(1000), inf)
93+
self.assertEqual(inf.scalar_mul(-1), inf)
94+
95+
self.assertEqual(p1.scalar_mul(0), inf)
96+
self.assertEqual(p1.scalar_mul(1), p1)
97+
self.assertEqual(p1.scalar_mul(2), p2)
98+
self.assertEqual(p1.scalar_mul(-2), np2)
99+
self.assertEqual(p2.scalar_mul(-1), np2)
100+
self.assertEqual(p1.scalar_mul(3), p3)
101+

0 commit comments

Comments
 (0)