From e99cbbdd6d03517c7e99e8097078fb4ef41a2bf2 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 17 May 2017 17:30:37 +0000 Subject: [PATCH 1/6] core: add support for multiplication of secp256k1 points by scalars --- opentimestamps/core/secp256k1.py | 114 ++++++++++++++++++++ opentimestamps/tests/core/test_secp256k1.py | 101 +++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 opentimestamps/core/secp256k1.py create mode 100644 opentimestamps/tests/core/test_secp256k1.py diff --git a/opentimestamps/core/secp256k1.py b/opentimestamps/core/secp256k1.py new file mode 100644 index 0000000..48de306 --- /dev/null +++ b/opentimestamps/core/secp256k1.py @@ -0,0 +1,114 @@ +# Copyright (C) 2017 The OpenTimestamps developers +# +# This file is part of python-opentimestamps. +# +# It is subject to the license terms in the LICENSE file found in the top-level +# directory of this distribution. +# +# No part of python-opentimestamps including this file, may be copied, +# modified, propagated, or distributed except according to the terms contained +# in the LICENSE file. + +## What follows is a lot of inefficient but explicit secp256k1 math +class Point(object): + inf = True + x = 0 + y = 0 + + def __init__(self, x=0, y=0): + self.x = x + self.y = y + if x == 0 and y == 0: + self.inf = True + else: + self.inf = False + + def __repr__(self): + if self.inf: + return "Point(infinity)" + else: + return "Point(%x, %x)" % (self.x, self.y) + + def __eq__(self, other): + if isinstance(other, self.__class__): + return (self.inf == True and other.inf == True) or\ + (self.inf == False and other.inf == False and self.x == other.x and self.y == other.y) + else: + return False + + def __ne__(self, other): + return not self.__eq__(other) + + @staticmethod + def decode(data): + if len(data) != 33 or (data[0] != 2 and data[0] != 3): + raise MsgValueError("Incorrectly formatted public key") + + x = int.from_bytes(data[1:], 'big') + if x >= SECP256K1_P: + raise MsgValueError("out of range x coordinate for secp256k1 point") + + ysqr = (x ** 3 + 7) % SECP256K1_P + y = psqrt(ysqr) + if pow(y, 2, SECP256K1_P) != ysqr: + raise MsgValueError("invalid x coordinate for secp256k1 point") + + if y % 2 == 1 and data[0] == 2: + y = SECP256K1_P - y + if y % 2 == 0 and data[0] == 3: + y = SECP256K1_P - y + + return Point(x, y) + + def encode(self): + ret = bytearray(self.x.to_bytes(33, 'big')) + assert(ret[0] == 0) + if self.y % 2 == 1: + ret[0] = 3 + else: + ret[0] = 2 + return ret + + def add(self, pt): + if self.inf: + return pt + if pt.inf: + return self + + if self.x == pt.x: + if self.y == SECP256K1_P - pt.y: + return Point() + else: + assert(self.y == pt.y) + lam = (3 * self.x ** 2 * pinv(2 * self.y)) % SECP256K1_P + else: + lam = ((pt.y - self.y) * pinv(pt.x - self.x)) % SECP256K1_P + + x3 = (lam ** 2 - self.x - pt.x) % SECP256K1_P + y3 = (self.y + lam * (x3 - self.x)) % SECP256K1_P + + return Point(x3, SECP256K1_P - y3) + + def scalar_mul(self, s): + ret = Point() + add = self + s = s % SECP256K1_N + while s > 0: + if s % 2 == 1: + ret = ret.add(add) + add = add.add(add) # add + s >>= 1 + return ret + +SECP256K1_P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F +SECP256K1_N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 +SECP256K1_GEN = Point(0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798, + 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8) + +def pinv(x): + return pow(x, SECP256K1_P - 2, SECP256K1_P) + +def psqrt(x): + # using `>> 2` in place of `/ 4` keeps everything as an int rather than float + return pow(x, (SECP256K1_P + 1) >> 2, SECP256K1_P) + diff --git a/opentimestamps/tests/core/test_secp256k1.py b/opentimestamps/tests/core/test_secp256k1.py new file mode 100644 index 0000000..3d2226b --- /dev/null +++ b/opentimestamps/tests/core/test_secp256k1.py @@ -0,0 +1,101 @@ +# Copyright (C) 2017 The OpenTimestamps developers +# +# This file is part of python-opentimestamps. +# +# It is subject to the license terms in the LICENSE file found in the top-level +# directory of this distribution. +# +# No part of python-opentimestamps including this file, may be copied, +# modified, propagated, or distributed except according to the terms contained +# in the LICENSE file. + +import binascii +import unittest + +from opentimestamps.core.secp256k1 import * + +class Test_Secp256k1(unittest.TestCase): + def test_point_rt(self): + """Point encoding round trip""" + gen = SECP256K1_GEN + encode = gen.encode() + self.assertEqual(encode, binascii.unhexlify("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")) + gen2 = Point().decode(encode) + self.assertEqual(gen, gen2) + + def test_pinv(self): + """Field inversion mod p""" + self.assertEqual(pinv(1), 1) + self.assertEqual(pinv(2), 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffff7ffffe18) + self.assertEqual(pinv(3), 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9fffffd75) + self.assertEqual(2, pinv(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffff7ffffe18)) + self.assertEqual(3, pinv(0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9fffffd75)) + + def test_psqrt(self): + """Field square root mod p""" + self.assertEqual(psqrt(1), 1) + self.assertEqual(psqrt(2), 0x210c790573632359b1edb4302c117d8a132654692c3feeb7de3a86ac3f3b53f7) + self.assertEqual(psqrt(4), 2) + # may return the sqrt or its negative + self.assertEqual(psqrt(9), SECP256K1_P - 3) + self.assertEqual(psqrt(49), SECP256K1_P - 7) + + def test_point_add(self): + """Point adding and doubling""" + + inf = Point() + # P random chosen by dice roll + p1 = Point(0x394867ad93f5c9612e8d8b7600443334026e648e365337d799190e845d649e67, + 0x0b84af9a00c1a55a7ac03917e59b21c68d1ffdf18720c3ad279077049cfaaf63) + # 2P + p2 = Point(0x8e6575f6c759aea04a8ec65f61f71eba237a0af54292d41e3a4bac2efa922dea, + 0x2b3c07687787ff07ae312305f30481c451ae3b78d4f479a3b729615fedc040e4) + # -2P + np2 = Point(0x8e6575f6c759aea04a8ec65f61f71eba237a0af54292d41e3a4bac2efa922dea, + 0xd4c3f897887800f851cedcfa0cfb7e3bae51c4872b0b865c48d69e9f123fbb4b) + # 3P + p3 = Point(0x53dd5e495c7404790f9347470cc9c38ee239809c758f02ec04ba641ab3d0e043, + 0xd7a4f5e5bdf21000b1fe7216adbea92cb9917d8fea7b37628c1eddb409a5cd3f) + + self.assertEqual(inf.add(inf), inf) + self.assertEqual(p1.add(inf), p1) + self.assertEqual(inf.add(p1), p1) + self.assertEqual(p1.add(p1), p2) + self.assertEqual(p1.add(p2), p3) + self.assertEqual(p2.add(p1), p3) + self.assertEqual(p3.add(np2), p1) + self.assertEqual(np2.add(p3), p1) + self.assertEqual(p2.add(np2), inf) + self.assertEqual(np2.add(p2), inf) + + def test_scalar_mul(self): + inf = Point() + # P random chosen by dice roll + p1 = Point(0x394867ad93f5c9612e8d8b7600443334026e648e365337d799190e845d649e67, + 0x0b84af9a00c1a55a7ac03917e59b21c68d1ffdf18720c3ad279077049cfaaf63) + # 2P + p2 = Point(0x8e6575f6c759aea04a8ec65f61f71eba237a0af54292d41e3a4bac2efa922dea, + 0x2b3c07687787ff07ae312305f30481c451ae3b78d4f479a3b729615fedc040e4) + # -2P + np2 = Point(0x8e6575f6c759aea04a8ec65f61f71eba237a0af54292d41e3a4bac2efa922dea, + 0xd4c3f897887800f851cedcfa0cfb7e3bae51c4872b0b865c48d69e9f123fbb4b) + # 3P + p3 = Point(0x53dd5e495c7404790f9347470cc9c38ee239809c758f02ec04ba641ab3d0e043, + 0xd7a4f5e5bdf21000b1fe7216adbea92cb9917d8fea7b37628c1eddb409a5cd3f) + + # nP + n = 0xa91ce154dcab9adabe08cc1ee84ec3cd0f426bbc08a54a1c41bd25f2587caedd + pn = Point(0x9dc4b057a857ad2ef3535b4a207a7bfc9264e8fcacf718c895db7ead8d445b26, + 0x5af110ecb68636e5c352b69fc6348173932b83ca64587a91fd88af1446e33979) + + self.assertEqual(inf.scalar_mul(0), inf) + self.assertEqual(inf.scalar_mul(1000), inf) + self.assertEqual(inf.scalar_mul(-1), inf) + + self.assertEqual(p1.scalar_mul(0), inf) + self.assertEqual(p1.scalar_mul(1), p1) + self.assertEqual(p1.scalar_mul(2), p2) + self.assertEqual(p1.scalar_mul(-2), np2) + self.assertEqual(p2.scalar_mul(-1), np2) + self.assertEqual(p1.scalar_mul(3), p3) + From 487721027d40bec9e3576847f9eff0082f19eb62 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 17 May 2017 18:32:57 +0000 Subject: [PATCH 2/6] core: add OpSecp256k1Commitment --- opentimestamps/core/op.py | 3 +++ opentimestamps/core/secp256k1.py | 21 +++++++++++++++++++++ opentimestamps/tests/core/test_secp256k1.py | 9 ++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/opentimestamps/core/op.py b/opentimestamps/core/op.py index b67a2b7..d351d25 100644 --- a/opentimestamps/core/op.py +++ b/opentimestamps/core/op.py @@ -347,3 +347,6 @@ def _do_op_call(self, msg): r = sha3.keccak_256(bytes(msg)).digest() assert len(r) == self.DIGEST_LENGTH return r + +from opentimestamps.core.secp256k1 import OpSecp256k1Commitment + diff --git a/opentimestamps/core/secp256k1.py b/opentimestamps/core/secp256k1.py index 48de306..e99b8dc 100644 --- a/opentimestamps/core/secp256k1.py +++ b/opentimestamps/core/secp256k1.py @@ -9,6 +9,27 @@ # modified, propagated, or distributed except according to the terms contained # in the LICENSE file. +import hashlib + +from opentimestamps.core.op import BinaryOp, MsgValueError + +@BinaryOp._register_op +class OpSecp256k1Commitment(BinaryOp): + """Execute the map commit -> [P + sha256(P||commit)G]_x for a given secp256k1 point P""" + TAG = b'\x09' + TAG_NAME = 'secp256k1commitment' + + def _do_op_call(self, msg): + hasher = hashlib.sha256() + pt = Point.decode(self[0]) + hasher.update(pt.encode()) + hasher.update(msg) + tweak = int.from_bytes(hasher.digest(), 'big') + tweak_pt = SECP256K1_GEN.scalar_mul(tweak) + final_pt = pt.add(tweak_pt) + return final_pt.x.to_bytes(32, 'big') + + ## What follows is a lot of inefficient but explicit secp256k1 math class Point(object): inf = True diff --git a/opentimestamps/tests/core/test_secp256k1.py b/opentimestamps/tests/core/test_secp256k1.py index 3d2226b..6eaeed5 100644 --- a/opentimestamps/tests/core/test_secp256k1.py +++ b/opentimestamps/tests/core/test_secp256k1.py @@ -9,6 +9,7 @@ # modified, propagated, or distributed except according to the terms contained # in the LICENSE file. +import hashlib import binascii import unittest @@ -20,7 +21,7 @@ def test_point_rt(self): gen = SECP256K1_GEN encode = gen.encode() self.assertEqual(encode, binascii.unhexlify("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")) - gen2 = Point().decode(encode) + gen2 = Point.decode(encode) self.assertEqual(gen, gen2) def test_pinv(self): @@ -99,3 +100,9 @@ def test_scalar_mul(self): self.assertEqual(p2.scalar_mul(-1), np2) self.assertEqual(p1.scalar_mul(3), p3) + def test_op_signtocontract(self): + pt_encode = binascii.unhexlify("0308aec434612f56df3f02c4e678260424415882ebd3efc16d52e3f9c1e39afdb0") + msg = hashlib.sha256("This is andytoshi on 2017-05-16 21:30 UTC".encode()).digest() + result = binascii.unhexlify("d386ef692770fcecad43362cf541858662e4ebe31d3ad04d196f94168897947a") + self.assertEqual(OpSecp256k1Commitment(pt_encode)(msg), result) + From 05db51e0d4847614f5512f4cd8f1a8418fcfbafa Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 17 May 2017 22:12:39 +0000 Subject: [PATCH 3/6] Add examples/ directory with a sign-to-contract transaction --- examples/andytoshi | 1 + examples/andytoshi.ots | Bin 0 -> 708 bytes 2 files changed, 1 insertion(+) create mode 100644 examples/andytoshi create mode 100644 examples/andytoshi.ots diff --git a/examples/andytoshi b/examples/andytoshi new file mode 100644 index 0000000..1d17ac9 --- /dev/null +++ b/examples/andytoshi @@ -0,0 +1 @@ +This is andytoshi on 2017-05-16 21:30 UTC \ No newline at end of file diff --git a/examples/andytoshi.ots b/examples/andytoshi.ots new file mode 100644 index 0000000000000000000000000000000000000000..a2ccc3da1ef6b13a4fa134abd3ed143b4de3ba70 GIT binary patch literal 708 zcmV;#0z3TxPjF>!RB3Hxb97;CaB~0vP;zf?W&pp5;^>6vl9T}mxuX>k=orOWxlEvH zr(4}5kri7f=*AZ3c+%~RBHhqe2_XXruEaE9FIL|_0>tKcCIlowSc2=*@4;Ln6@gn=))hVO?nIgsz4=H5H8P_)O&lKk2ng^X7FXPt@|;7UdM*9KE2ng^Xvi{{Vd%<+&fOUm2 zD3@;s#9c&YZZe0R{$6tR-}0w82ng{YXOY!dhnbJCeF!@T#ge{&5)4snnMq{?QmhM$ z@3laW2ng^XAGY%}$a=jgsQab@(VtKA!*>E648oLVe2IM*hC6|<2ng^XS6^Kd3A)-R qhVBp#MkuR#V@7ra^pMp_BbW(>DsokY2nYZLh?Wg=*BJo=xW61?WjCw< literal 0 HcmV?d00001 From 5ddb84e3c13698e379695bc7bdd89154c5b3ea10 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Thu, 18 May 2017 11:25:08 -0400 Subject: [PATCH 4/6] Make OpSecp256k1Commitment a unary op --- examples/andytoshi.ots | Bin 708 -> 709 bytes opentimestamps/core/secp256k1.py | 21 ++++++++++++++------ opentimestamps/tests/core/test_secp256k1.py | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/examples/andytoshi.ots b/examples/andytoshi.ots index a2ccc3da1ef6b13a4fa134abd3ed143b4de3ba70..15b9c5ce0ce10c6ae11ad9e62eaef92fec3698bf 100644 GIT binary patch delta 17 YcmX@YdX#m7Bjd-3PD+fN8&eXP06GH&%>V!Z delta 15 WcmX@gdW3a?BO~WTC#8+42}}Sf{RG?q diff --git a/opentimestamps/core/secp256k1.py b/opentimestamps/core/secp256k1.py index e99b8dc..5da32e0 100644 --- a/opentimestamps/core/secp256k1.py +++ b/opentimestamps/core/secp256k1.py @@ -11,19 +11,28 @@ import hashlib -from opentimestamps.core.op import BinaryOp, MsgValueError +from opentimestamps.core.op import UnaryOp, MsgValueError -@BinaryOp._register_op -class OpSecp256k1Commitment(BinaryOp): - """Execute the map commit -> [P + sha256(P||commit)G]_x for a given secp256k1 point P""" +@UnaryOp._register_op +class OpSecp256k1Commitment(UnaryOp): + """Map (P || commit) -> [P + sha256(P||commit)G]_x for a given secp256k1 point P + + This is a unary op rather than a binary op to allow timestamps to also + timestamp the point itself; in the event of an ECC break this might be + relevant. + """ TAG = b'\x09' TAG_NAME = 'secp256k1commitment' def _do_op_call(self, msg): + if len(msg) < 33: + raise MsgValueError("Missing secp256k1 point") + + pt = Point.decode(msg[0:33]) + hasher = hashlib.sha256() - pt = Point.decode(self[0]) hasher.update(pt.encode()) - hasher.update(msg) + hasher.update(msg[33:]) tweak = int.from_bytes(hasher.digest(), 'big') tweak_pt = SECP256K1_GEN.scalar_mul(tweak) final_pt = pt.add(tweak_pt) diff --git a/opentimestamps/tests/core/test_secp256k1.py b/opentimestamps/tests/core/test_secp256k1.py index 6eaeed5..1b3de11 100644 --- a/opentimestamps/tests/core/test_secp256k1.py +++ b/opentimestamps/tests/core/test_secp256k1.py @@ -104,5 +104,5 @@ def test_op_signtocontract(self): pt_encode = binascii.unhexlify("0308aec434612f56df3f02c4e678260424415882ebd3efc16d52e3f9c1e39afdb0") msg = hashlib.sha256("This is andytoshi on 2017-05-16 21:30 UTC".encode()).digest() result = binascii.unhexlify("d386ef692770fcecad43362cf541858662e4ebe31d3ad04d196f94168897947a") - self.assertEqual(OpSecp256k1Commitment(pt_encode)(msg), result) + self.assertEqual(OpSecp256k1Commitment()(pt_encode + msg), result) From bbe30afa84290fe1e9da5c9b6571edae99c5496f Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 19 May 2017 17:41:26 -0400 Subject: [PATCH 5/6] Improve ECC break comment --- opentimestamps/core/secp256k1.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opentimestamps/core/secp256k1.py b/opentimestamps/core/secp256k1.py index 5da32e0..d9d5194 100644 --- a/opentimestamps/core/secp256k1.py +++ b/opentimestamps/core/secp256k1.py @@ -19,7 +19,8 @@ class OpSecp256k1Commitment(UnaryOp): This is a unary op rather than a binary op to allow timestamps to also timestamp the point itself; in the event of an ECC break this might be - relevant. + relevant. Such a break would not affect the integrity of the commitment, + but knowledge of the underlying key may be interesting in its own right. """ TAG = b'\x09' TAG_NAME = 'secp256k1commitment' From 6de546f2045d5c9d9ef934cb38a2614565dd8cad Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Fri, 13 Apr 2018 13:34:37 +0000 Subject: [PATCH 6/6] signtocontract: assert that points round-trip uniquely when hashing them --- opentimestamps/core/secp256k1.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opentimestamps/core/secp256k1.py b/opentimestamps/core/secp256k1.py index d9d5194..58cfcc5 100644 --- a/opentimestamps/core/secp256k1.py +++ b/opentimestamps/core/secp256k1.py @@ -30,9 +30,10 @@ def _do_op_call(self, msg): raise MsgValueError("Missing secp256k1 point") pt = Point.decode(msg[0:33]) + assert(pt.encode() == msg[0:33]) hasher = hashlib.sha256() - hasher.update(pt.encode()) + hasher.update(msg[0:33]) hasher.update(msg[33:]) tweak = int.from_bytes(hasher.digest(), 'big') tweak_pt = SECP256K1_GEN.scalar_mul(tweak)