1
0
mirror of https://github.com/bitcoin/bips.git synced 2026-02-09 15:23:09 +00:00

Merge pull request #2084 from theStack/bip374-vendor-secp256k1lab

BIP-374: vendor secp256k1lab and use it for reference implementation
This commit is contained in:
Mark "Murch" Erhardt 2026-01-28 10:31:32 -08:00 committed by GitHub
commit e169a61940
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 442 additions and 127 deletions

View File

@ -116,6 +116,10 @@ This proposal is compatible with all older clients.
== Test Vectors and Reference Code == == Test Vectors and Reference Code ==
A reference python implementation is included [https://github.com/bitcoin/bips/blob/master/bip-0374/reference.py here]. 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 <code>./bip-0374/gen_test_vectors.py</code> 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 <code>./bip-0374/run_test_vectors.py</code>. Test vectors can be generated by running <code>./bip-0374/gen_test_vectors.py</code> 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 <code>./bip-0374/run_test_vectors.py</code>.
== Changelog == == Changelog ==

View File

@ -1,30 +1,29 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Generate the BIP-0374 test vectors.""" """Generate the BIP-0374 test vectors."""
import csv import csv
import os from pathlib import Path
import sys
from reference import ( from reference import (
TaggedHash,
dleq_generate_proof, dleq_generate_proof,
dleq_verify_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 NUM_SUCCESS_TEST_VECTORS = 8
DLEQ_TAG_TESTVECTORS_RNG = "BIP0374/testvectors_rng" DLEQ_TAG_TESTVECTORS_RNG = "BIP0374/testvectors_rng"
FILENAME_GENERATE_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_generate_proof.csv') FILENAME_GENERATE_PROOF_TEST = Path(__file__).parent / 'test_vectors_generate_proof.csv'
FILENAME_VERIFY_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_verify_proof.csv') FILENAME_VERIFY_PROOF_TEST = Path(__file__).parent / 'test_vectors_verify_proof.csv'
def random_scalar_int(vector_i, purpose): 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 return int.from_bytes(rng_out, 'big') % GE.ORDER
def random_bytes(vector_i, purpose): 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 return rng_out

View File

@ -2,30 +2,22 @@
"""Reference implementation of DLEQ BIP for secp256k1 with unit tests.""" """Reference implementation of DLEQ BIP for secp256k1 with unit tests."""
from hashlib import sha256 from pathlib import Path
import random import random
from secp256k1 import G, GE
import sys import sys
import unittest 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_AUX = "BIP0374/aux"
DLEQ_TAG_NONCE = "BIP0374/nonce" DLEQ_TAG_NONCE = "BIP0374/nonce"
DLEQ_TAG_CHALLENGE = "BIP0374/challenge" 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( def dleq_challenge(
A: GE, B: GE, C: GE, R1: GE, R2: GE, m: bytes | None, G: GE, A: GE, B: GE, C: GE, R1: GE, R2: GE, m: bytes | None, G: GE,
) -> int: ) -> int:
@ -33,7 +25,7 @@ def dleq_challenge(
assert len(m) == 32 assert len(m) == 32
m = bytes([]) if m is None else m m = bytes([]) if m is None else m
return int.from_bytes( return int.from_bytes(
TaggedHash( tagged_hash(
DLEQ_TAG_CHALLENGE, DLEQ_TAG_CHALLENGE,
A.to_bytes_compressed() A.to_bytes_compressed()
+ B.to_bytes_compressed() + B.to_bytes_compressed()
@ -59,9 +51,9 @@ def dleq_generate_proof(
assert len(m) == 32 assert len(m) == 32
A = a * G A = a * G
C = a * B 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 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 DLEQ_TAG_NONCE, t + A.to_bytes_compressed() + C.to_bytes_compressed() + m_prime
) )
k = int.from_bytes(rand, "big") % GE.ORDER k = int.from_bytes(rand, "big") % GE.ORDER

View File

@ -1,17 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Run the BIP-DLEQ test vectors.""" """Run the BIP-DLEQ test vectors."""
import csv import csv
import os from pathlib import Path
import sys import sys
from reference import ( from reference import (
dleq_generate_proof, dleq_generate_proof,
dleq_verify_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_GENERATE_PROOF_TEST = Path(__file__).parent / 'test_vectors_generate_proof.csv'
FILENAME_VERIFY_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_verify_proof.csv') FILENAME_VERIFY_PROOF_TEST = Path(__file__).parent / 'test_vectors_verify_proof.csv'
all_passed = True all_passed = True

View File

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

View File

@ -0,0 +1 @@
3.9

View File

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

View File

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

View File

@ -0,0 +1,13 @@
secp256k1lab
============
![Dependencies: None](https://img.shields.io/badge/dependencies-none-success)
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.

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# Copyright (c) 2022-2023 The Bitcoin Core developers # Copyright (c) 2022-2023 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php. # file COPYING or http://www.opensource.org/licenses/mit-license.php.
@ -17,31 +15,29 @@ Exports:
* G: the secp256k1 generator point * G: the secp256k1 generator point
""" """
import unittest # TODO Docstrings of methods still say "field element"
from hashlib import sha256 class APrimeFE:
"""Objects of this class represent elements of a prime field.
class FE:
"""Objects of this class represent elements of the field GF(2**256 - 2**32 - 977).
They are represented internally in numerator / denominator form, in order to delay inversions. They are represented internally in numerator / denominator form, in order to delay inversions.
""" """
# The size of the field (also its modulus and characteristic). # 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): def __init__(self, a=0, b=1):
"""Initialize a field element a/b; both a and b can be ints or field elements.""" """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 num = a._num
den = a._den den = a._den
else: else:
num = a % FE.SIZE num = a % self.SIZE
den = 1 den = 1
if isinstance(b, FE): if isinstance(b, type(self)):
den = (den * b._num) % FE.SIZE den = (den * b._num) % self.SIZE
num = (num * b._den) % FE.SIZE num = (num * b._den) % self.SIZE
else: else:
den = (den * b) % FE.SIZE den = (den * b) % self.SIZE
assert den != 0 assert den != 0
if num == 0: if num == 0:
den = 1 den = 1
@ -50,71 +46,74 @@ class FE:
def __add__(self, a): def __add__(self, a):
"""Compute the sum of two field elements (second may be int).""" """Compute the sum of two field elements (second may be int)."""
if isinstance(a, FE): if isinstance(a, type(self)):
return FE(self._num * a._den + self._den * a._num, self._den * a._den) return type(self)(self._num * a._den + self._den * a._num, self._den * a._den)
return FE(self._num + self._den * a, self._den) if isinstance(a, int):
return type(self)(self._num + self._den * a, self._den)
return NotImplemented
def __radd__(self, a): def __radd__(self, a):
"""Compute the sum of an integer and a field element.""" """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): def __sub__(self, a):
"""Compute the difference of two field elements (second may be int).""" """Compute the difference of two field elements (second may be int)."""
if isinstance(a, FE): if isinstance(a, type(self)):
return FE(self._num * a._den - self._den * a._num, self._den * a._den) return type(self)(self._num * a._den - self._den * a._num, self._den * a._den)
return FE(self._num - self._den * a, self._den) if isinstance(a, int):
return type(self)(self._num - self._den * a, self._den)
return NotImplemented
def __rsub__(self, a): def __rsub__(self, a):
"""Compute the difference of an integer and a field element.""" """Compute the difference of an integer and a field element."""
return FE(a) - self return type(self)(a) - self
def __mul__(self, a): def __mul__(self, a):
"""Compute the product of two field elements (second may be int).""" """Compute the product of two field elements (second may be int)."""
if isinstance(a, FE): if isinstance(a, type(self)):
return FE(self._num * a._num, self._den * a._den) return type(self)(self._num * a._num, self._den * a._den)
return FE(self._num * a, self._den) if isinstance(a, int):
return type(self)(self._num * a, self._den)
return NotImplemented
def __rmul__(self, a): def __rmul__(self, a):
"""Compute the product of an integer with a field element.""" """Compute the product of an integer with a field element."""
return FE(a) * self return type(self)(a) * self
def __truediv__(self, a): def __truediv__(self, a):
"""Compute the ratio of two field elements (second may be int).""" """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): def __pow__(self, a):
"""Raise a field element to an integer power.""" """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): def __neg__(self):
"""Negate a field element.""" """Negate a field element."""
return FE(-self._num, self._den) return type(self)(-self._num, self._den)
def __int__(self): 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: 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 self._den = 1
return self._num return self._num
def sqrt(self): def sqrt(self):
"""Compute the square root of a field element if it exists (None otherwise). """Compute the square root of a field element if it exists (None otherwise)."""
raise NotImplementedError
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
def is_square(self): def is_square(self):
"""Determine if this field element has a square root.""" """Determine if this field element has a square root."""
@ -122,26 +121,42 @@ class FE:
return self.sqrt() is not None return self.sqrt() is not None
def is_even(self): 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 return int(self) & 1 == 0
def __eq__(self, a): def __eq__(self, a):
"""Check whether two field elements are equal (second may be an int).""" """Check whether two field elements are equal (second may be an int)."""
if isinstance(a, FE): if isinstance(a, type(self)):
return (self._num * a._den - self._den * a._num) % FE.SIZE == 0 return (self._num * a._den - self._den * a._num) % self.SIZE == 0
return (self._num - self._den * a) % FE.SIZE == 0 return (self._num - self._den * a) % self.SIZE == 0
def to_bytes(self): def to_bytes(self):
"""Convert a field element to a 32-byte array (BE byte order).""" """Convert a field element to a 32-byte array (BE byte order)."""
return int(self).to_bytes(32, 'big') return int(self).to_bytes(32, 'big')
@staticmethod @classmethod
def from_bytes(b): 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).""" """Convert a 32-byte array to a field element (BE byte order, no overflow allowed)."""
v = int.from_bytes(b, 'big') v = int.from_bytes(b, 'big')
if v >= FE.SIZE: return cls.from_int_checked(v)
return None
return FE(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): def __str__(self):
"""Convert this field element to a 64 character hex string.""" """Convert this field element to a 64 character hex string."""
@ -149,12 +164,40 @@ class FE:
def __repr__(self): def __repr__(self):
"""Get a string representation of this field element.""" """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: class GE:
"""Objects of this class represent secp256k1 group elements (curve points or infinity) """Objects of this class represent secp256k1 group elements (curve points or infinity)
GE objects are immutable.
Normal points on the curve have fields: Normal points on the curve have fields:
* x: the x coordinate (a field element) * x: the x coordinate (a field element)
* y: the y coordinate (a field element, satisfying y^2 = x^3 + 7) * y: the y coordinate (a field element, satisfying y^2 = x^3 + 7)
@ -164,26 +207,47 @@ class GE:
* infinity: True * 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 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. # Number of valid distinct x coordinates on the curve.
ORDER_HALF = ORDER // 2 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): def __init__(self, x=None, y=None):
"""Initialize a group element with specified x and y coordinates, or infinity.""" """Initialize a group element with specified x and y coordinates, or infinity."""
if x is None: if x is None:
# Initialize as infinity. # Initialize as infinity.
assert y is None assert y is None
self.infinity = True self._infinity = True
else: else:
# Initialize as point on the curve (and check that it is). # Initialize as point on the curve (and check that it is).
fx = FE(x) fx = FE(x)
fy = FE(y) fy = FE(y)
assert fy**2 == fx**3 + 7 assert fy**2 == fx**3 + 7
self.infinity = False self._infinity = False
self.x = fx self._x = fx
self.y = fy self._y = fy
def __add__(self, a): def __add__(self, a):
"""Add two group elements together.""" """Add two group elements together."""
@ -209,13 +273,20 @@ class GE:
return GE(x, y) return GE(x, y)
@staticmethod @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. """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.""" but more efficient."""
# Reduce all the scalars modulo order first (so we can deal with negatives etc). # 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. # Start with point at infinity.
r = GE() r = GE()
# Iterate over all bit positions, from high to low. # Iterate over all bit positions, from high to low.
@ -231,8 +302,8 @@ class GE:
def __rmul__(self, a): def __rmul__(self, a):
"""Multiply an integer with a group element.""" """Multiply an integer with a group element."""
if self == G: if self == G:
return FAST_G.mul(a) return FAST_G.mul(Scalar(a))
return GE.mul((a, self)) return GE.batch_mul((Scalar(a), self))
def __neg__(self): def __neg__(self):
"""Compute the negation of a group element.""" """Compute the negation of a group element."""
@ -244,11 +315,26 @@ class GE:
"""Subtract a group element from another.""" """Subtract a group element from another."""
return self + (-a) 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): def to_bytes_compressed(self):
"""Convert a non-infinite group element to 33-byte compressed encoding.""" """Convert a non-infinite group element to 33-byte compressed encoding."""
assert not self.infinity assert not self.infinity
return bytes([3 - self.y.is_even()]) + self.x.to_bytes() 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): def to_bytes_uncompressed(self):
"""Convert a non-infinite group element to 65-byte uncompressed encoding.""" """Convert a non-infinite group element to 65-byte uncompressed encoding."""
assert not self.infinity assert not self.infinity
@ -264,44 +350,51 @@ class GE:
"""Return group element with specified field element as x coordinate (and even y).""" """Return group element with specified field element as x coordinate (and even y)."""
y = (FE(x)**3 + 7).sqrt() y = (FE(x)**3 + 7).sqrt()
if y is None: if y is None:
return None raise ValueError
if not y.is_even(): if not y.is_even():
y = -y y = -y
return GE(x, 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 @staticmethod
def from_bytes(b): def from_bytes(b):
"""Convert a compressed or uncompressed encoding to a group element.""" """Convert a compressed or uncompressed encoding to a group element."""
assert len(b) in (33, 65) assert len(b) in (33, 65)
if len(b) == 33: if len(b) == 33:
if b[0] != 2 and b[0] != 3: return GE.from_bytes_compressed(b)
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
else: else:
if b[0] != 4: return GE.from_bytes_uncompressed(b)
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)
@staticmethod @staticmethod
def from_bytes_xonly(b): def from_bytes_xonly(b):
"""Convert a point given in xonly encoding to a group element.""" """Convert a point given in xonly encoding to a group element."""
assert len(b) == 32 assert len(b) == 32
x = FE.from_bytes(b) x = FE.from_bytes_checked(b)
if x is None: r = GE.lift_x(x)
return None return r
return GE.lift_x(x)
@staticmethod @staticmethod
def is_valid_x(x): def is_valid_x(x):
@ -320,6 +413,13 @@ class GE:
return "GE()" return "GE()"
return f"GE(0x{int(self.x):x},0x{int(self.y):x})" 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 # The secp256k1 generator point
G = GE.lift_x(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798) G = GE.lift_x(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798)
@ -344,7 +444,7 @@ class FastGEMul:
def mul(self, a): def mul(self, a):
result = GE() result = GE()
a = a % GE.ORDER a = int(a)
for bit in range(a.bit_length()): for bit in range(a.bit_length()):
if a & (1 << bit): if a & (1 << bit):
result += self.table[bit] result += self.table[bit]
@ -352,9 +452,3 @@ class FastGEMul:
# Precomputed table with multiples of G for fast multiplication # Precomputed table with multiples of G for fast multiplication
FAST_G = FastGEMul(G) 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")

View File

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