diff --git a/bip-0374.mediawiki b/bip-0374.mediawiki
index 840bbf815d..a8588b96e8 100644
--- a/bip-0374.mediawiki
+++ b/bip-0374.mediawiki
@@ -116,6 +116,10 @@ This proposal is compatible with all older clients.
== Test Vectors and Reference Code ==
A reference python implementation is included [https://github.com/bitcoin/bips/blob/master/bip-0374/reference.py here].
+It uses a vendored copy of the [https://github.com/secp256k1lab/secp256k1lab/ secp256k1lab] library at version 1.0.0
+(commit [https://github.com/secp256k1lab/secp256k1lab/commit/44dc4bd893b8f03e621585e3bf255253e0e0fbfb
+44dc4bd893b8f03e621585e3bf255253e0e0fbfb]).
+
Test vectors can be generated by running ./bip-0374/gen_test_vectors.py which will produce a CSV file of random test vectors for both generating and verifying proofs. These can be run against the reference implementation with ./bip-0374/run_test_vectors.py.
== Changelog ==
diff --git a/bip-0374/gen_test_vectors.py b/bip-0374/gen_test_vectors.py
index 792a59a45b..a828074e4a 100755
--- a/bip-0374/gen_test_vectors.py
+++ b/bip-0374/gen_test_vectors.py
@@ -1,30 +1,29 @@
#!/usr/bin/env python3
"""Generate the BIP-0374 test vectors."""
import csv
-import os
-import sys
+from pathlib import Path
from reference import (
- TaggedHash,
dleq_generate_proof,
dleq_verify_proof,
)
-from secp256k1 import G as GENERATOR, GE
+from secp256k1lab.secp256k1 import G as GENERATOR, GE
+from secp256k1lab.util import tagged_hash
NUM_SUCCESS_TEST_VECTORS = 8
DLEQ_TAG_TESTVECTORS_RNG = "BIP0374/testvectors_rng"
-FILENAME_GENERATE_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_generate_proof.csv')
-FILENAME_VERIFY_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_verify_proof.csv')
+FILENAME_GENERATE_PROOF_TEST = Path(__file__).parent / 'test_vectors_generate_proof.csv'
+FILENAME_VERIFY_PROOF_TEST = Path(__file__).parent / 'test_vectors_verify_proof.csv'
def random_scalar_int(vector_i, purpose):
- rng_out = TaggedHash(DLEQ_TAG_TESTVECTORS_RNG, purpose.encode() + vector_i.to_bytes(4, 'little'))
+ rng_out = tagged_hash(DLEQ_TAG_TESTVECTORS_RNG, purpose.encode() + vector_i.to_bytes(4, 'little'))
return int.from_bytes(rng_out, 'big') % GE.ORDER
def random_bytes(vector_i, purpose):
- rng_out = TaggedHash(DLEQ_TAG_TESTVECTORS_RNG, purpose.encode() + vector_i.to_bytes(4, 'little'))
+ rng_out = tagged_hash(DLEQ_TAG_TESTVECTORS_RNG, purpose.encode() + vector_i.to_bytes(4, 'little'))
return rng_out
diff --git a/bip-0374/reference.py b/bip-0374/reference.py
index e0fcbeaf97..8068b9c25c 100755
--- a/bip-0374/reference.py
+++ b/bip-0374/reference.py
@@ -2,30 +2,22 @@
"""Reference implementation of DLEQ BIP for secp256k1 with unit tests."""
-from hashlib import sha256
+from pathlib import Path
import random
-from secp256k1 import G, GE
import sys
import unittest
+# Prefer the vendored copy of secp256k1lab
+sys.path.insert(0, str(Path(__file__).parent / "secp256k1lab/src"))
+from secp256k1lab.secp256k1 import G, GE
+from secp256k1lab.util import tagged_hash, xor_bytes
+
DLEQ_TAG_AUX = "BIP0374/aux"
DLEQ_TAG_NONCE = "BIP0374/nonce"
DLEQ_TAG_CHALLENGE = "BIP0374/challenge"
-def TaggedHash(tag: str, data: bytes) -> bytes:
- ss = sha256(tag.encode()).digest()
- ss += ss
- ss += data
- return sha256(ss).digest()
-
-
-def xor_bytes(lhs: bytes, rhs: bytes) -> bytes:
- assert len(lhs) == len(rhs)
- return bytes([lhs[i] ^ rhs[i] for i in range(len(lhs))])
-
-
def dleq_challenge(
A: GE, B: GE, C: GE, R1: GE, R2: GE, m: bytes | None, G: GE,
) -> int:
@@ -33,7 +25,7 @@ def dleq_challenge(
assert len(m) == 32
m = bytes([]) if m is None else m
return int.from_bytes(
- TaggedHash(
+ tagged_hash(
DLEQ_TAG_CHALLENGE,
A.to_bytes_compressed()
+ B.to_bytes_compressed()
@@ -59,9 +51,9 @@ def dleq_generate_proof(
assert len(m) == 32
A = a * G
C = a * B
- t = xor_bytes(a.to_bytes(32, "big"), TaggedHash(DLEQ_TAG_AUX, r))
+ t = xor_bytes(a.to_bytes(32, "big"), tagged_hash(DLEQ_TAG_AUX, r))
m_prime = bytes([]) if m is None else m
- rand = TaggedHash(
+ rand = tagged_hash(
DLEQ_TAG_NONCE, t + A.to_bytes_compressed() + C.to_bytes_compressed() + m_prime
)
k = int.from_bytes(rand, "big") % GE.ORDER
diff --git a/bip-0374/run_test_vectors.py b/bip-0374/run_test_vectors.py
index 4831fbe20b..54349aed48 100755
--- a/bip-0374/run_test_vectors.py
+++ b/bip-0374/run_test_vectors.py
@@ -1,17 +1,17 @@
#!/usr/bin/env python3
"""Run the BIP-DLEQ test vectors."""
import csv
-import os
+from pathlib import Path
import sys
from reference import (
dleq_generate_proof,
dleq_verify_proof,
)
-from secp256k1 import GE
+from secp256k1lab.secp256k1 import GE
-FILENAME_GENERATE_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_generate_proof.csv')
-FILENAME_VERIFY_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_verify_proof.csv')
+FILENAME_GENERATE_PROOF_TEST = Path(__file__).parent / 'test_vectors_generate_proof.csv'
+FILENAME_VERIFY_PROOF_TEST = Path(__file__).parent / 'test_vectors_verify_proof.csv'
all_passed = True
diff --git a/bip-0374/secp256k1lab/.github/workflows/main.yml b/bip-0374/secp256k1lab/.github/workflows/main.yml
new file mode 100644
index 0000000000..4950b96550
--- /dev/null
+++ b/bip-0374/secp256k1lab/.github/workflows/main.yml
@@ -0,0 +1,17 @@
+name: Tests
+on: [push, pull_request]
+jobs:
+ ruff:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install the latest version of uv
+ uses: astral-sh/setup-uv@v5
+ - run: uvx ruff check .
+ mypy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install the latest version of uv
+ uses: astral-sh/setup-uv@v5
+ - run: uvx mypy .
diff --git a/bip-0374/secp256k1lab/.python-version b/bip-0374/secp256k1lab/.python-version
new file mode 100644
index 0000000000..bd28b9c5c2
--- /dev/null
+++ b/bip-0374/secp256k1lab/.python-version
@@ -0,0 +1 @@
+3.9
diff --git a/bip-0374/secp256k1lab/CHANGELOG.md b/bip-0374/secp256k1lab/CHANGELOG.md
new file mode 100644
index 0000000000..15779717c4
--- /dev/null
+++ b/bip-0374/secp256k1lab/CHANGELOG.md
@@ -0,0 +1,10 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [1.0.0] - 2025-03-31
+
+Initial release.
diff --git a/bip-0374/secp256k1lab/COPYING b/bip-0374/secp256k1lab/COPYING
new file mode 100644
index 0000000000..e8f2163641
--- /dev/null
+++ b/bip-0374/secp256k1lab/COPYING
@@ -0,0 +1,23 @@
+The MIT License (MIT)
+
+Copyright (c) 2009-2024 The Bitcoin Core developers
+Copyright (c) 2009-2024 Bitcoin Developers
+Copyright (c) 2025- The secp256k1lab Developers
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/bip-0374/secp256k1lab/README.md b/bip-0374/secp256k1lab/README.md
new file mode 100644
index 0000000000..dbc9dbd04c
--- /dev/null
+++ b/bip-0374/secp256k1lab/README.md
@@ -0,0 +1,13 @@
+secp256k1lab
+============
+
+
+
+An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes written in Python, intended for prototyping, experimentation and education.
+
+Features:
+* Low-level secp256k1 field and group arithmetic.
+* Schnorr signing/verification and key generation according to [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki).
+* ECDH key exchange.
+
+WARNING: The code in this library is slow and trivially vulnerable to side channel attacks.
diff --git a/bip-0374/secp256k1lab/pyproject.toml b/bip-0374/secp256k1lab/pyproject.toml
new file mode 100644
index 0000000000..a0bdd19f42
--- /dev/null
+++ b/bip-0374/secp256k1lab/pyproject.toml
@@ -0,0 +1,34 @@
+[project]
+name = "secp256k1lab"
+version = "1.0.0"
+description = "An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes, intended for prototyping, experimentation and education"
+readme = "README.md"
+authors = [
+ { name = "Pieter Wuille", email = "pieter@wuille.net" },
+ { name = "Tim Ruffing", email = "me@real-or-random.org" },
+ { name = "Jonas Nick", email = "jonasd.nick@gmail.com" },
+ { name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" }
+]
+maintainers = [
+ { name = "Tim Ruffing", email = "me@real-or-random.org" },
+ { name = "Jonas Nick", email = "jonasd.nick@gmail.com" },
+ { name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" }
+]
+requires-python = ">=3.9"
+license = "MIT"
+license-files = ["COPYING"]
+keywords = ["secp256k1", "elliptic curves", "cryptography", "Bitcoin"]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Education",
+ "Intended Audience :: Science/Research",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python",
+ "Topic :: Security :: Cryptography",
+]
+dependencies = []
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
diff --git a/bip-0374/secp256k1lab/src/secp256k1lab/__init__.py b/bip-0374/secp256k1lab/src/secp256k1lab/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bip-0374/secp256k1lab/src/secp256k1lab/bip340.py b/bip-0374/secp256k1lab/src/secp256k1lab/bip340.py
new file mode 100644
index 0000000000..ba839d16e1
--- /dev/null
+++ b/bip-0374/secp256k1lab/src/secp256k1lab/bip340.py
@@ -0,0 +1,73 @@
+# The following functions are based on the BIP 340 reference implementation:
+# https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py
+
+from .secp256k1 import FE, GE, G
+from .util import int_from_bytes, bytes_from_int, xor_bytes, tagged_hash
+
+
+def pubkey_gen(seckey: bytes) -> bytes:
+ d0 = int_from_bytes(seckey)
+ if not (1 <= d0 <= GE.ORDER - 1):
+ raise ValueError("The secret key must be an integer in the range 1..n-1.")
+ P = d0 * G
+ assert not P.infinity
+ return P.to_bytes_xonly()
+
+
+def schnorr_sign(
+ msg: bytes, seckey: bytes, aux_rand: bytes, tag_prefix: str = "BIP0340"
+) -> bytes:
+ d0 = int_from_bytes(seckey)
+ if not (1 <= d0 <= GE.ORDER - 1):
+ raise ValueError("The secret key must be an integer in the range 1..n-1.")
+ if len(aux_rand) != 32:
+ raise ValueError("aux_rand must be 32 bytes instead of %i." % len(aux_rand))
+ P = d0 * G
+ assert not P.infinity
+ d = d0 if P.has_even_y() else GE.ORDER - d0
+ t = xor_bytes(bytes_from_int(d), tagged_hash(tag_prefix + "/aux", aux_rand))
+ k0 = (
+ int_from_bytes(tagged_hash(tag_prefix + "/nonce", t + P.to_bytes_xonly() + msg))
+ % GE.ORDER
+ )
+ if k0 == 0:
+ raise RuntimeError("Failure. This happens only with negligible probability.")
+ R = k0 * G
+ assert not R.infinity
+ k = k0 if R.has_even_y() else GE.ORDER - k0
+ e = (
+ int_from_bytes(
+ tagged_hash(
+ tag_prefix + "/challenge", R.to_bytes_xonly() + P.to_bytes_xonly() + msg
+ )
+ )
+ % GE.ORDER
+ )
+ sig = R.to_bytes_xonly() + bytes_from_int((k + e * d) % GE.ORDER)
+ assert schnorr_verify(msg, P.to_bytes_xonly(), sig, tag_prefix=tag_prefix)
+ return sig
+
+
+def schnorr_verify(
+ msg: bytes, pubkey: bytes, sig: bytes, tag_prefix: str = "BIP0340"
+) -> bool:
+ if len(pubkey) != 32:
+ raise ValueError("The public key must be a 32-byte array.")
+ if len(sig) != 64:
+ raise ValueError("The signature must be a 64-byte array.")
+ try:
+ P = GE.from_bytes_xonly(pubkey)
+ except ValueError:
+ return False
+ r = int_from_bytes(sig[0:32])
+ s = int_from_bytes(sig[32:64])
+ if (r >= FE.SIZE) or (s >= GE.ORDER):
+ return False
+ e = (
+ int_from_bytes(tagged_hash(tag_prefix + "/challenge", sig[0:32] + pubkey + msg))
+ % GE.ORDER
+ )
+ R = s * G - e * P
+ if R.infinity or (not R.has_even_y()) or (R.x != r):
+ return False
+ return True
diff --git a/bip-0374/secp256k1lab/src/secp256k1lab/ecdh.py b/bip-0374/secp256k1lab/src/secp256k1lab/ecdh.py
new file mode 100644
index 0000000000..73f47fa1a7
--- /dev/null
+++ b/bip-0374/secp256k1lab/src/secp256k1lab/ecdh.py
@@ -0,0 +1,16 @@
+import hashlib
+
+from .secp256k1 import GE, Scalar
+
+
+def ecdh_compressed_in_raw_out(seckey: bytes, pubkey: bytes) -> GE:
+ """TODO"""
+ shared_secret = Scalar.from_bytes_checked(seckey) * GE.from_bytes_compressed(pubkey)
+ assert not shared_secret.infinity # prime-order group
+ return shared_secret
+
+
+def ecdh_libsecp256k1(seckey: bytes, pubkey: bytes) -> bytes:
+ """TODO"""
+ shared_secret = ecdh_compressed_in_raw_out(seckey, pubkey)
+ return hashlib.sha256(shared_secret.to_bytes_compressed()).digest()
diff --git a/bip-0374/secp256k1lab/src/secp256k1lab/keys.py b/bip-0374/secp256k1lab/src/secp256k1lab/keys.py
new file mode 100644
index 0000000000..3e28897e99
--- /dev/null
+++ b/bip-0374/secp256k1lab/src/secp256k1lab/keys.py
@@ -0,0 +1,15 @@
+from .secp256k1 import GE, G
+from .util import int_from_bytes
+
+# The following function is based on the BIP 327 reference implementation
+# https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py
+
+
+# Return the plain public key corresponding to a given secret key
+def pubkey_gen_plain(seckey: bytes) -> bytes:
+ d0 = int_from_bytes(seckey)
+ if not (1 <= d0 <= GE.ORDER - 1):
+ raise ValueError("The secret key must be an integer in the range 1..n-1.")
+ P = d0 * G
+ assert not P.infinity
+ return P.to_bytes_compressed()
diff --git a/bip-0374/secp256k1lab/src/secp256k1lab/py.typed b/bip-0374/secp256k1lab/src/secp256k1lab/py.typed
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bip-0374/secp256k1.py b/bip-0374/secp256k1lab/src/secp256k1lab/secp256k1.py
old mode 100755
new mode 100644
similarity index 55%
rename from bip-0374/secp256k1.py
rename to bip-0374/secp256k1lab/src/secp256k1lab/secp256k1.py
index b83d028f92..6e262bf51e
--- a/bip-0374/secp256k1.py
+++ b/bip-0374/secp256k1lab/src/secp256k1lab/secp256k1.py
@@ -1,5 +1,3 @@
-#!/usr/bin/env python3
-
# Copyright (c) 2022-2023 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
@@ -17,31 +15,29 @@
* G: the secp256k1 generator point
"""
-import unittest
-from hashlib import sha256
-
-class FE:
- """Objects of this class represent elements of the field GF(2**256 - 2**32 - 977).
+# TODO Docstrings of methods still say "field element"
+class APrimeFE:
+ """Objects of this class represent elements of a prime field.
They are represented internally in numerator / denominator form, in order to delay inversions.
"""
# The size of the field (also its modulus and characteristic).
- SIZE = 2**256 - 2**32 - 977
+ SIZE: int
def __init__(self, a=0, b=1):
"""Initialize a field element a/b; both a and b can be ints or field elements."""
- if isinstance(a, FE):
+ if isinstance(a, type(self)):
num = a._num
den = a._den
else:
- num = a % FE.SIZE
+ num = a % self.SIZE
den = 1
- if isinstance(b, FE):
- den = (den * b._num) % FE.SIZE
- num = (num * b._den) % FE.SIZE
+ if isinstance(b, type(self)):
+ den = (den * b._num) % self.SIZE
+ num = (num * b._den) % self.SIZE
else:
- den = (den * b) % FE.SIZE
+ den = (den * b) % self.SIZE
assert den != 0
if num == 0:
den = 1
@@ -50,71 +46,74 @@ def __init__(self, a=0, b=1):
def __add__(self, a):
"""Compute the sum of two field elements (second may be int)."""
- if isinstance(a, FE):
- return FE(self._num * a._den + self._den * a._num, self._den * a._den)
- return FE(self._num + self._den * a, self._den)
+ if isinstance(a, type(self)):
+ return type(self)(self._num * a._den + self._den * a._num, self._den * a._den)
+ if isinstance(a, int):
+ return type(self)(self._num + self._den * a, self._den)
+ return NotImplemented
def __radd__(self, a):
"""Compute the sum of an integer and a field element."""
- return FE(a) + self
+ return type(self)(a) + self
+
+ @classmethod
+ # REVIEW This should be
+ # def sum(cls, *es: Iterable[Self]) -> Self:
+ # but Self needs the typing_extension package on Python <= 3.12.
+ def sum(cls, *es):
+ """Compute the sum of field elements.
+
+ sum(a, b, c, ...) is identical to (0 + a + b + c + ...)."""
+ return sum(es, start=cls(0))
def __sub__(self, a):
"""Compute the difference of two field elements (second may be int)."""
- if isinstance(a, FE):
- return FE(self._num * a._den - self._den * a._num, self._den * a._den)
- return FE(self._num - self._den * a, self._den)
+ if isinstance(a, type(self)):
+ return type(self)(self._num * a._den - self._den * a._num, self._den * a._den)
+ if isinstance(a, int):
+ return type(self)(self._num - self._den * a, self._den)
+ return NotImplemented
def __rsub__(self, a):
"""Compute the difference of an integer and a field element."""
- return FE(a) - self
+ return type(self)(a) - self
def __mul__(self, a):
"""Compute the product of two field elements (second may be int)."""
- if isinstance(a, FE):
- return FE(self._num * a._num, self._den * a._den)
- return FE(self._num * a, self._den)
+ if isinstance(a, type(self)):
+ return type(self)(self._num * a._num, self._den * a._den)
+ if isinstance(a, int):
+ return type(self)(self._num * a, self._den)
+ return NotImplemented
def __rmul__(self, a):
"""Compute the product of an integer with a field element."""
- return FE(a) * self
+ return type(self)(a) * self
def __truediv__(self, a):
"""Compute the ratio of two field elements (second may be int)."""
- return FE(self, a)
+ if isinstance(a, type(self)) or isinstance(a, int):
+ return type(self)(self, a)
+ return NotImplemented
def __pow__(self, a):
"""Raise a field element to an integer power."""
- return FE(pow(self._num, a, FE.SIZE), pow(self._den, a, FE.SIZE))
+ return type(self)(pow(self._num, a, self.SIZE), pow(self._den, a, self.SIZE))
def __neg__(self):
"""Negate a field element."""
- return FE(-self._num, self._den)
+ return type(self)(-self._num, self._den)
def __int__(self):
- """Convert a field element to an integer in range 0..p-1. The result is cached."""
+ """Convert a field element to an integer in range 0..SIZE-1. The result is cached."""
if self._den != 1:
- self._num = (self._num * pow(self._den, -1, FE.SIZE)) % FE.SIZE
+ self._num = (self._num * pow(self._den, -1, self.SIZE)) % self.SIZE
self._den = 1
return self._num
def sqrt(self):
- """Compute the square root of a field element if it exists (None otherwise).
-
- Due to the fact that our modulus is of the form (p % 4) == 3, the Tonelli-Shanks
- algorithm (https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm) is simply
- raising the argument to the power (p + 1) / 4.
-
- To see why: (p-1) % 2 = 0, so 2 divides the order of the multiplicative group,
- and thus only half of the non-zero field elements are squares. An element a is
- a (nonzero) square when Euler's criterion, a^((p-1)/2) = 1 (mod p), holds. We're
- looking for x such that x^2 = a (mod p). Given a^((p-1)/2) = 1, that is equivalent
- to x^2 = a^(1 + (p-1)/2) mod p. As (1 + (p-1)/2) is even, this is equivalent to
- x = a^((1 + (p-1)/2)/2) mod p, or x = a^((p+1)/4) mod p."""
- v = int(self)
- s = pow(v, (FE.SIZE + 1) // 4, FE.SIZE)
- if s**2 % FE.SIZE == v:
- return FE(s)
- return None
+ """Compute the square root of a field element if it exists (None otherwise)."""
+ raise NotImplementedError
def is_square(self):
"""Determine if this field element has a square root."""
@@ -122,26 +121,42 @@ def is_square(self):
return self.sqrt() is not None
def is_even(self):
- """Determine whether this field element, represented as integer in 0..p-1, is even."""
+ """Determine whether this field element, represented as integer in 0..SIZE-1, is even."""
return int(self) & 1 == 0
def __eq__(self, a):
"""Check whether two field elements are equal (second may be an int)."""
- if isinstance(a, FE):
- return (self._num * a._den - self._den * a._num) % FE.SIZE == 0
- return (self._num - self._den * a) % FE.SIZE == 0
+ if isinstance(a, type(self)):
+ return (self._num * a._den - self._den * a._num) % self.SIZE == 0
+ return (self._num - self._den * a) % self.SIZE == 0
def to_bytes(self):
"""Convert a field element to a 32-byte array (BE byte order)."""
return int(self).to_bytes(32, 'big')
- @staticmethod
- def from_bytes(b):
+ @classmethod
+ def from_int_checked(cls, v):
+ """Convert an integer to a field element (no overflow allowed)."""
+ if v >= cls.SIZE:
+ raise ValueError
+ return cls(v)
+
+ @classmethod
+ def from_int_wrapping(cls, v):
+ """Convert an integer to a field element (reduced modulo SIZE)."""
+ return cls(v % cls.SIZE)
+
+ @classmethod
+ def from_bytes_checked(cls, b):
"""Convert a 32-byte array to a field element (BE byte order, no overflow allowed)."""
v = int.from_bytes(b, 'big')
- if v >= FE.SIZE:
- return None
- return FE(v)
+ return cls.from_int_checked(v)
+
+ @classmethod
+ def from_bytes_wrapping(cls, b):
+ """Convert a 32-byte array to a field element (BE byte order, reduced modulo SIZE)."""
+ v = int.from_bytes(b, 'big')
+ return cls.from_int_wrapping(v)
def __str__(self):
"""Convert this field element to a 64 character hex string."""
@@ -149,12 +164,40 @@ def __str__(self):
def __repr__(self):
"""Get a string representation of this field element."""
- return f"FE(0x{int(self):x})"
+ return f"{type(self).__qualname__}(0x{int(self):x})"
+
+
+class FE(APrimeFE):
+ SIZE = 2**256 - 2**32 - 977
+
+ def sqrt(self):
+ # Due to the fact that our modulus p is of the form (p % 4) == 3, the Tonelli-Shanks
+ # algorithm (https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm) is simply
+ # raising the argument to the power (p + 1) / 4.
+
+ # To see why: (p-1) % 2 = 0, so 2 divides the order of the multiplicative group,
+ # and thus only half of the non-zero field elements are squares. An element a is
+ # a (nonzero) square when Euler's criterion, a^((p-1)/2) = 1 (mod p), holds. We're
+ # looking for x such that x^2 = a (mod p). Given a^((p-1)/2) = 1, that is equivalent
+ # to x^2 = a^(1 + (p-1)/2) mod p. As (1 + (p-1)/2) is even, this is equivalent to
+ # x = a^((1 + (p-1)/2)/2) mod p, or x = a^((p+1)/4) mod p.
+ v = int(self)
+ s = pow(v, (self.SIZE + 1) // 4, self.SIZE)
+ if s**2 % self.SIZE == v:
+ return type(self)(s)
+ return None
+
+
+class Scalar(APrimeFE):
+ """TODO Docstring"""
+ SIZE = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
class GE:
"""Objects of this class represent secp256k1 group elements (curve points or infinity)
+ GE objects are immutable.
+
Normal points on the curve have fields:
* x: the x coordinate (a field element)
* y: the y coordinate (a field element, satisfying y^2 = x^3 + 7)
@@ -164,26 +207,47 @@ class GE:
* infinity: True
"""
+ # TODO The following two class attributes should probably be just getters as
+ # classmethods to enforce immutability. Unfortunately Python makes it hard
+ # to create "classproperties". `G` could then also be just a classmethod.
+
# Order of the group (number of points on the curve, plus 1 for infinity)
- ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
+ ORDER = Scalar.SIZE
# Number of valid distinct x coordinates on the curve.
ORDER_HALF = ORDER // 2
+ @property
+ def infinity(self):
+ """Whether the group element is the point at infinity."""
+ return self._infinity
+
+ @property
+ def x(self):
+ """The x coordinate (a field element) of a non-infinite group element."""
+ assert not self.infinity
+ return self._x
+
+ @property
+ def y(self):
+ """The y coordinate (a field element) of a non-infinite group element."""
+ assert not self.infinity
+ return self._y
+
def __init__(self, x=None, y=None):
"""Initialize a group element with specified x and y coordinates, or infinity."""
if x is None:
# Initialize as infinity.
assert y is None
- self.infinity = True
+ self._infinity = True
else:
# Initialize as point on the curve (and check that it is).
fx = FE(x)
fy = FE(y)
assert fy**2 == fx**3 + 7
- self.infinity = False
- self.x = fx
- self.y = fy
+ self._infinity = False
+ self._x = fx
+ self._y = fy
def __add__(self, a):
"""Add two group elements together."""
@@ -209,13 +273,20 @@ def __add__(self, a):
return GE(x, y)
@staticmethod
- def mul(*aps):
+ def sum(*ps):
+ """Compute the sum of group elements.
+
+ GE.sum(a, b, c, ...) is identical to (GE() + a + b + c + ...)."""
+ return sum(ps, start=GE())
+
+ @staticmethod
+ def batch_mul(*aps):
"""Compute a (batch) scalar group element multiplication.
- GE.mul((a1, p1), (a2, p2), (a3, p3)) is identical to a1*p1 + a2*p2 + a3*p3,
+ GE.batch_mul((a1, p1), (a2, p2), (a3, p3)) is identical to a1*p1 + a2*p2 + a3*p3,
but more efficient."""
# Reduce all the scalars modulo order first (so we can deal with negatives etc).
- naps = [(a % GE.ORDER, p) for a, p in aps]
+ naps = [(int(a), p) for a, p in aps]
# Start with point at infinity.
r = GE()
# Iterate over all bit positions, from high to low.
@@ -231,8 +302,8 @@ def mul(*aps):
def __rmul__(self, a):
"""Multiply an integer with a group element."""
if self == G:
- return FAST_G.mul(a)
- return GE.mul((a, self))
+ return FAST_G.mul(Scalar(a))
+ return GE.batch_mul((Scalar(a), self))
def __neg__(self):
"""Compute the negation of a group element."""
@@ -244,11 +315,26 @@ def __sub__(self, a):
"""Subtract a group element from another."""
return self + (-a)
+ def __eq__(self, a):
+ """Check if two group elements are equal."""
+ return (self - a).infinity
+
+ def has_even_y(self):
+ """Determine whether a non-infinity group element has an even y coordinate."""
+ assert not self.infinity
+ return self.y.is_even()
+
def to_bytes_compressed(self):
"""Convert a non-infinite group element to 33-byte compressed encoding."""
assert not self.infinity
return bytes([3 - self.y.is_even()]) + self.x.to_bytes()
+ def to_bytes_compressed_with_infinity(self):
+ """Convert a group element to 33-byte compressed encoding, mapping infinity to zeros."""
+ if self.infinity:
+ return 33 * b"\x00"
+ return self.to_bytes_compressed()
+
def to_bytes_uncompressed(self):
"""Convert a non-infinite group element to 65-byte uncompressed encoding."""
assert not self.infinity
@@ -264,44 +350,51 @@ def lift_x(x):
"""Return group element with specified field element as x coordinate (and even y)."""
y = (FE(x)**3 + 7).sqrt()
if y is None:
- return None
+ raise ValueError
if not y.is_even():
y = -y
return GE(x, y)
+ @staticmethod
+ def from_bytes_compressed(b):
+ """Convert a compressed to a group element."""
+ assert len(b) == 33
+ if b[0] != 2 and b[0] != 3:
+ raise ValueError
+ x = FE.from_bytes_checked(b[1:])
+ r = GE.lift_x(x)
+ if b[0] == 3:
+ r = -r
+ return r
+
+ @staticmethod
+ def from_bytes_uncompressed(b):
+ """Convert an uncompressed to a group element."""
+ assert len(b) == 65
+ if b[0] != 4:
+ raise ValueError
+ x = FE.from_bytes_checked(b[1:33])
+ y = FE.from_bytes_checked(b[33:])
+ if y**2 != x**3 + 7:
+ raise ValueError
+ return GE(x, y)
+
@staticmethod
def from_bytes(b):
"""Convert a compressed or uncompressed encoding to a group element."""
assert len(b) in (33, 65)
if len(b) == 33:
- if b[0] != 2 and b[0] != 3:
- return None
- x = FE.from_bytes(b[1:])
- if x is None:
- return None
- r = GE.lift_x(x)
- if r is None:
- return None
- if b[0] == 3:
- r = -r
- return r
+ return GE.from_bytes_compressed(b)
else:
- if b[0] != 4:
- return None
- x = FE.from_bytes(b[1:33])
- y = FE.from_bytes(b[33:])
- if y**2 != x**3 + 7:
- return None
- return GE(x, y)
+ return GE.from_bytes_uncompressed(b)
@staticmethod
def from_bytes_xonly(b):
"""Convert a point given in xonly encoding to a group element."""
assert len(b) == 32
- x = FE.from_bytes(b)
- if x is None:
- return None
- return GE.lift_x(x)
+ x = FE.from_bytes_checked(b)
+ r = GE.lift_x(x)
+ return r
@staticmethod
def is_valid_x(x):
@@ -320,6 +413,13 @@ def __repr__(self):
return "GE()"
return f"GE(0x{int(self.x):x},0x{int(self.y):x})"
+ def __hash__(self):
+ """Compute a non-cryptographic hash of the group element."""
+ if self.infinity:
+ return 0 # 0 is not a valid x coordinate
+ return int(self.x)
+
+
# The secp256k1 generator point
G = GE.lift_x(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798)
@@ -344,7 +444,7 @@ def __init__(self, p):
def mul(self, a):
result = GE()
- a = a % GE.ORDER
+ a = int(a)
for bit in range(a.bit_length()):
if a & (1 << bit):
result += self.table[bit]
@@ -352,9 +452,3 @@ def mul(self, a):
# Precomputed table with multiples of G for fast multiplication
FAST_G = FastGEMul(G)
-
-class TestFrameworkSecp256k1(unittest.TestCase):
- def test_H(self):
- H = sha256(G.to_bytes_uncompressed()).digest()
- assert GE.lift_x(FE.from_bytes(H)) is not None
- self.assertEqual(H.hex(), "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0")
diff --git a/bip-0374/secp256k1lab/src/secp256k1lab/util.py b/bip-0374/secp256k1lab/src/secp256k1lab/util.py
new file mode 100644
index 0000000000..d8c744b795
--- /dev/null
+++ b/bip-0374/secp256k1lab/src/secp256k1lab/util.py
@@ -0,0 +1,24 @@
+import hashlib
+
+
+# This implementation can be sped up by storing the midstate after hashing
+# tag_hash instead of rehashing it all the time.
+def tagged_hash(tag: str, msg: bytes) -> bytes:
+ tag_hash = hashlib.sha256(tag.encode()).digest()
+ return hashlib.sha256(tag_hash + tag_hash + msg).digest()
+
+
+def bytes_from_int(x: int) -> bytes:
+ return x.to_bytes(32, byteorder="big")
+
+
+def xor_bytes(b0: bytes, b1: bytes) -> bytes:
+ return bytes(x ^ y for (x, y) in zip(b0, b1))
+
+
+def int_from_bytes(b: bytes) -> int:
+ return int.from_bytes(b, byteorder="big")
+
+
+def hash_sha256(b: bytes) -> bytes:
+ return hashlib.sha256(b).digest()