Browse Source

updates (#118)

* updates

* touch-ups

* Update test_keys.py

* typing

* Update test_keys.py

* Update test_keys.py

* docs

* .
anonswap
Ofek Lev 2 years ago
committed by GitHub
parent
commit
367ee22857
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/scripts/build-windows-wheels.sh
  2. 8
      .github/workflows/build.yml
  3. 3
      coincurve/__init__.py
  4. 135
      coincurve/keys.py
  5. 13
      docs/api.md
  6. 24
      tests/test_keys.py

2
.github/scripts/build-windows-wheels.sh

@ -3,7 +3,7 @@ set -ex
build_dll() { build_dll() {
./autogen.sh ./autogen.sh
./configure --host=$1 --enable-module-recovery --enable-experimental --enable-module-ecdh --enable-benchmark=no --enable-tests=no --enable-openssl-tests=no --enable-exhaustive-tests=no --enable-static --disable-dependency-tracking --with-pic ./configure --host=$1 --enable-module-recovery --enable-experimental --enable-module-ecdh --enable-module-extrakeys --enable-module-schnorrsig --enable-benchmark=no --enable-tests=no --enable-openssl-tests=no --enable-exhaustive-tests=no --enable-static --disable-dependency-tracking --with-pic
make make
} }

8
.github/workflows/build.yml

@ -80,7 +80,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Build wheels - name: Build wheels
uses: pypa/cibuildwheel@v2.3.1 uses: pypa/cibuildwheel@v2.11.2
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
with: with:
@ -98,7 +98,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Build wheels - name: Build wheels
uses: pypa/cibuildwheel@v2.3.1 uses: pypa/cibuildwheel@v2.11.2
env: env:
CIBW_ARCHS_MACOS: x86_64 CIBW_ARCHS_MACOS: x86_64
@ -118,7 +118,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Build wheels - name: Build wheels
uses: pypa/cibuildwheel@v2.3.1 uses: pypa/cibuildwheel@v2.11.2
env: env:
CIBW_ARCHS_MACOS: arm64 CIBW_ARCHS_MACOS: arm64
COINCURVE_CROSS_HOST: aarch64-apple-darwin COINCURVE_CROSS_HOST: aarch64-apple-darwin
@ -173,7 +173,7 @@ jobs:
platforms: arm64 platforms: arm64
- name: Build wheels - name: Build wheels
uses: pypa/cibuildwheel@v2.3.1 uses: pypa/cibuildwheel@v2.11.2
env: env:
CIBW_ARCHS_LINUX: aarch64 CIBW_ARCHS_LINUX: aarch64

3
coincurve/__init__.py

@ -1,5 +1,5 @@
from coincurve.context import GLOBAL_CONTEXT, Context from coincurve.context import GLOBAL_CONTEXT, Context
from coincurve.keys import PrivateKey, PublicKey from coincurve.keys import PrivateKey, PublicKey, PublicKeyXOnly
from coincurve.utils import verify_signature from coincurve.utils import verify_signature
__all__ = [ __all__ = [
@ -7,5 +7,6 @@ __all__ = [
'Context', 'Context',
'PrivateKey', 'PrivateKey',
'PublicKey', 'PublicKey',
'PublicKeyXOnly',
'verify_signature', 'verify_signature',
] ]

135
coincurve/keys.py

@ -32,7 +32,7 @@ class PrivateKey:
self.secret: bytes = validate_secret(secret) if secret is not None else get_valid_secret() self.secret: bytes = validate_secret(secret) if secret is not None else get_valid_secret()
self.context = context self.context = context
self.public_key: PublicKey = PublicKey.from_valid_secret(self.secret, self.context) self.public_key: PublicKey = PublicKey.from_valid_secret(self.secret, self.context)
self.xonly_pubkey: XonlyPublicKey = XonlyPublicKey.from_secret(self.secret, self.context) self.public_key_xonly: PublicKeyXOnly = PublicKeyXOnly.from_valid_secret(self.secret, self.context)
def sign(self, message: bytes, hasher: Hasher = sha256, custom_nonce: Nonce = DEFAULT_NONCE) -> bytes: def sign(self, message: bytes, hasher: Hasher = sha256, custom_nonce: Nonce = DEFAULT_NONCE) -> bytes:
""" """
@ -61,31 +61,40 @@ class PrivateKey:
return cdata_to_der(signature, self.context) return cdata_to_der(signature, self.context)
def sign_schnorr(self, message: bytes, aux_randomness: bytes = None) -> bytes: def sign_schnorr(self, message: bytes, aux_randomness: bytes = b'') -> bytes:
"""Create a Schnorr signature. """Create a Schnorr signature.
:param message: the message to be signed as an array of 32 bytes. :param message: The message to sign.
:param aux_randomness: an optional 32 bytes of fresh randomness. :param aux_randomness: An optional 32 bytes of fresh randomness. By default (empty bytestring), this
:return: the 64-bytes Schnorr signature. will be generated automatically. Set to `None` to disable this behavior.
""" :return: The Schnorr signature.
if not isinstance(message, bytes) or len(message) != 32: :raises ValueError: If the message was not 32 bytes long, the optional auxiliary random data was not
raise ValueError('"message" must be an array of 32 bytes') 32 bytes long, signing failed, or the signature was invalid.
if aux_randomness is not None and (not isinstance(aux_randomness, bytes) or len(aux_randomness) != 32): """
raise ValueError('"aux_randomness" must be an array of 32 bytes') if len(message) != 32:
raise ValueError('Message must be 32 bytes long.')
elif aux_randomness == b'':
aux_randomness = os.urandom(32)
elif aux_randomness is None:
aux_randomness = ffi.NULL
elif len(aux_randomness) != 32:
raise ValueError('Auxiliary random data must be 32 bytes long.')
keypair = ffi.new('secp256k1_keypair *') keypair = ffi.new('secp256k1_keypair *')
res = lib.secp256k1_keypair_create(self.context.ctx, keypair, self.secret) res = lib.secp256k1_keypair_create(self.context.ctx, keypair, self.secret)
assert res, 'Secret must be valid, we just checked it' if not res:
raise ValueError('Secret was invalid')
aux_randomness = aux_randomness or os.urandom(32)
signature = ffi.new('unsigned char[64]') signature = ffi.new('unsigned char[64]')
res = lib.secp256k1_schnorrsig_sign32(self.context.ctx, signature, message, keypair, aux_randomness) res = lib.secp256k1_schnorrsig_sign32(self.context.ctx, signature, message, keypair, aux_randomness)
assert res, 'Secret key is valid, signing must not fail' if not res:
raise ValueError('Signing failed')
res = lib.secp256k1_schnorrsig_verify( res = lib.secp256k1_schnorrsig_verify(
self.context.ctx, signature, message, len(message), self.xonly_pubkey.xonly_pubkey self.context.ctx, signature, message, len(message), self.public_key_xonly.public_key
) )
assert res, 'libsecp must not give us an invalid signature' if not res:
raise ValueError('Invalid signature')
return bytes(ffi.buffer(signature)) return bytes(ffi.buffer(signature))
@ -516,96 +525,106 @@ class PublicKey:
return self.format(compressed=False) == other.format(compressed=False) return self.format(compressed=False) == other.format(compressed=False)
class XonlyPublicKey: class PublicKeyXOnly:
def __init__(self, data: bytes, parity: bool = False, context: Context = GLOBAL_CONTEXT): def __init__(self, data, parity: bool = False, context: Context = GLOBAL_CONTEXT):
"""A BIP340 'x-only' public key. """A BIP340 `x-only` public key.
:param data: The formatted public key as a 32 bytes array or as an ffi 'secp256k1_xonly_pubkey *' type. :param data: The formatted public key.
:param parity: Whether the encoded point is the negation of the pubkey.
:type data: bytes :type data: bytes
:param context: a reference to a verification context. :param parity: Whether the encoded point is the negation of the public key.
:param context:
""" """
if isinstance(data, bytes): if not isinstance(data, bytes):
if len(data) != 32: self.public_key = data
raise ValueError('"data" if in bytes must be an array of 32 bytes') else:
public_key = ffi.new('secp256k1_xonly_pubkey *')
self.xonly_pubkey = ffi.new('secp256k1_xonly_pubkey *') parsed = lib.secp256k1_xonly_pubkey_parse(context.ctx, public_key, data)
parsed = lib.secp256k1_xonly_pubkey_parse(context.ctx, self.xonly_pubkey, data)
if not parsed: if not parsed:
raise ValueError('The public key could not be parsed or is invalid.') raise ValueError('The public key could not be parsed or is invalid.')
else:
# data must be a cdata 'secp256k1_xonly_pubkey *' type self.public_key = public_key
self.xonly_pubkey = data
self.parity = parity self.parity = parity
self.context = context self.context = context
@classmethod @classmethod
def from_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT): def from_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT):
"""Create an x-only public key from a given secret. """Derive an x-only public key from a private key secret.
:param secret: the private key as an array of 32 bytes. :param secret: The private key secret.
:param context:
:return: The x-only public key. :return: The x-only public key.
""" """
if not isinstance(secret, bytes) or len(secret) != 32: keypair = ffi.new('secp256k1_keypair *')
raise ValueError('"data" must be an array of 32 bytes') res = lib.secp256k1_keypair_create(context.ctx, keypair, validate_secret(secret))
secret = validate_secret(secret) if not res:
raise ValueError('Secret was invalid')
xonly_pubkey = ffi.new('secp256k1_xonly_pubkey *')
pk_parity = ffi.new('int *')
res = lib.secp256k1_keypair_xonly_pub(context.ctx, xonly_pubkey, pk_parity, keypair)
return cls(xonly_pubkey, parity=not not pk_parity[0], context=context)
@classmethod
def from_valid_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT):
keypair = ffi.new('secp256k1_keypair *') keypair = ffi.new('secp256k1_keypair *')
res = lib.secp256k1_keypair_create(context.ctx, keypair, secret) res = lib.secp256k1_keypair_create(context.ctx, keypair, secret)
assert res, 'Secret must be valid, we just checked it' if not res:
raise ValueError('Secret was invalid')
xonly_pubkey = ffi.new('secp256k1_xonly_pubkey *') xonly_pubkey = ffi.new('secp256k1_xonly_pubkey *')
pk_parity = ffi.new('int *') pk_parity = ffi.new('int *')
res = lib.secp256k1_keypair_xonly_pub(context.ctx, xonly_pubkey, pk_parity, keypair) res = lib.secp256k1_keypair_xonly_pub(context.ctx, xonly_pubkey, pk_parity, keypair)
assert res and pk_parity[0] in (0, 1), 'Must always return 1 and a boolean parity'
return cls(xonly_pubkey, parity=bool(pk_parity[0]), context=context) return cls(xonly_pubkey, parity=not not pk_parity[0], context=context)
def format(self) -> bytes: def format(self) -> bytes:
"""Serialize the public key. """Serialize the public key.
:return: The public key serialized as an array of 32 bytes. :return: The public key serialized as 32 bytes.
""" """
output32 = ffi.new('unsigned char [32]') output32 = ffi.new('unsigned char [32]')
res = lib.secp256k1_xonly_pubkey_serialize(self.context.ctx, output32, self.xonly_pubkey) res = lib.secp256k1_xonly_pubkey_serialize(self.context.ctx, output32, self.public_key)
assert res, 'Public key in self.xonly_pubkey must be valid' if not res:
raise ValueError('Public key in self.public_key must be valid')
return bytes(ffi.buffer(output32, 32)) return bytes(ffi.buffer(output32, 32))
def verify(self, signature: bytes, message: bytes) -> bool: def verify(self, signature: bytes, message: bytes) -> bool:
"""Verify a Schnorr signature over a given message. """Verify a Schnorr signature over a given message.
:param signature: The 64-bytes Schnorr signature to verify. :param signature: The 64-byte Schnorr signature to verify.
:param message: The message to be verified. :param message: The message to be verified.
:return: A boolean indicating whether or not the signature is correct. :return: A boolean indicating whether or not the signature is correct.
""" """
if not isinstance(signature, bytes) or len(signature) != 64: if len(signature) != 64:
raise ValueError('The "signature" parameter must be an array of 64 bytes') raise ValueError('Signature must be 32 bytes long.')
if not isinstance(message, bytes):
raise ValueError('The "message" parameter must be an array of bytes')
return lib.secp256k1_schnorrsig_verify(self.context.ctx, signature, message, len(message), self.xonly_pubkey) return not not lib.secp256k1_schnorrsig_verify(
self.context.ctx, signature, message, len(message), self.public_key
)
def tweak_add(self, tweak: bytes): def tweak_add(self, scalar: bytes):
"""Tweak the public key by adding the generator multiplied with tweak32 to it. """Add a scalar to the public key.
:param tweak: A 32 bytes tweak. :param scalar: The scalar with which to add.
:return: The modified public key.
:rtype: PublicKeyXOnly
:raises ValueError: If the tweak was out of range or the resulting public key was invalid.
""" """
if not isinstance(tweak, bytes) or len(tweak) != 32: scalar = pad_scalar(scalar)
raise ValueError('"tweak" must be an array of 32 bytes')
out_pubkey = ffi.new('secp256k1_pubkey *') out_pubkey = ffi.new('secp256k1_pubkey *')
res = lib.secp256k1_xonly_pubkey_tweak_add(self.context.ctx, out_pubkey, self.xonly_pubkey, tweak) res = lib.secp256k1_xonly_pubkey_tweak_add(self.context.ctx, out_pubkey, self.public_key, scalar)
if not res: if not res:
raise ValueError('Resulting public key would be invalid') raise ValueError('The tweak was out of range, or the resulting public key would be invalid')
pk_parity = ffi.new('int *') pk_parity = ffi.new('int *')
res = lib.secp256k1_xonly_pubkey_from_pubkey(self.context.ctx, self.xonly_pubkey, pk_parity, out_pubkey) lib.secp256k1_xonly_pubkey_from_pubkey(self.context.ctx, self.public_key, pk_parity, out_pubkey)
assert res and pk_parity[0] in (0, 1), 'Must always return 1 and a boolean parity' self.parity = not not pk_parity[0]
self.parity = bool(pk_parity[0])
def __eq__(self, other) -> bool: def __eq__(self, other) -> bool:
res = lib.secp256k1_xonly_pubkey_cmp(self.context.ctx, self.xonly_pubkey, other.xonly_pubkey) res = lib.secp256k1_xonly_pubkey_cmp(self.context.ctx, self.public_key, other.public_key)
return res == 0 return res == 0

13
docs/api.md

@ -19,6 +19,7 @@ All objects are available directly under the root namespace `coincurve`.
- __init__ - __init__
- sign - sign
- sign_recoverable - sign_recoverable
- sign_schnorr
- ecdh - ecdh
- add - add
- multiply - multiply
@ -48,3 +49,15 @@ All objects are available directly under the root namespace `coincurve`.
- from_signature_and_message - from_signature_and_message
- from_secret - from_secret
- from_point - from_point
::: coincurve.PublicKeyXOnly
rendering:
show_root_full_path: false
selection:
docstring_style: restructured-text
members:
- __init__
- verify
- format
- tweak_add
- from_secret

24
tests/test_keys.py

@ -4,7 +4,7 @@ from os import urandom
import pytest import pytest
from coincurve.ecdsa import deserialize_recoverable, recover from coincurve.ecdsa import deserialize_recoverable, recover
from coincurve.keys import PrivateKey, PublicKey, XonlyPublicKey from coincurve.keys import PrivateKey, PublicKey, PublicKeyXOnly
from coincurve.utils import bytes_to_int, int_to_bytes_padded, verify_signature from coincurve.utils import bytes_to_int, int_to_bytes_padded, verify_signature
from .samples import ( from .samples import (
@ -38,7 +38,7 @@ class TestPrivateKey:
assert PrivateKey(PRIVATE_KEY_BYTES).public_key.format() == PUBLIC_KEY_COMPRESSED assert PrivateKey(PRIVATE_KEY_BYTES).public_key.format() == PUBLIC_KEY_COMPRESSED
def test_xonly_pubkey(self): def test_xonly_pubkey(self):
assert PrivateKey(PRIVATE_KEY_BYTES).xonly_pubkey.format() == PUBLIC_KEY_COMPRESSED[1:] assert PrivateKey(PRIVATE_KEY_BYTES).public_key_xonly.format() == PUBLIC_KEY_COMPRESSED[1:]
def test_signature_correct(self): def test_signature_correct(self):
private_key = PrivateKey() private_key = PrivateKey()
@ -74,11 +74,11 @@ class TestPrivateKey:
# We can provide supplementary randomness # We can provide supplementary randomness
sig = private_key.sign_schnorr(message, urandom(32)) sig = private_key.sign_schnorr(message, urandom(32))
assert private_key.xonly_pubkey.verify(sig, message) assert private_key.public_key_xonly.verify(sig, message)
# Or not # Or not
sig = private_key.sign_schnorr(message) sig = private_key.sign_schnorr(message)
assert private_key.xonly_pubkey.verify(sig, message) assert private_key.public_key_xonly.verify(sig, message)
def test_to_hex(self): def test_to_hex(self):
assert PrivateKey(PRIVATE_KEY_BYTES).to_hex() == PRIVATE_KEY_HEX assert PrivateKey(PRIVATE_KEY_BYTES).to_hex() == PRIVATE_KEY_HEX
@ -173,35 +173,35 @@ class TestXonlyPubKey:
def test_parse_invalid(self): def test_parse_invalid(self):
# Must be 32 bytes # Must be 32 bytes
with pytest.raises(ValueError): with pytest.raises(ValueError):
XonlyPublicKey(bytes(33)) PublicKeyXOnly.from_secret(bytes(33))
# Must be an x coordinate for a valid point # Must be an x coordinate for a valid point
with pytest.raises(ValueError): with pytest.raises(ValueError):
XonlyPublicKey(X_ONLY_PUBKEY_INVALID) PublicKeyXOnly(X_ONLY_PUBKEY_INVALID)
def test_roundtrip(self): def test_roundtrip(self):
assert XonlyPublicKey(X_ONLY_PUBKEY).format() == X_ONLY_PUBKEY assert PublicKeyXOnly(X_ONLY_PUBKEY).format() == X_ONLY_PUBKEY
assert XonlyPublicKey(PUBLIC_KEY_COMPRESSED[1:]).format() == PUBLIC_KEY_COMPRESSED[1:] assert PublicKeyXOnly(PUBLIC_KEY_COMPRESSED[1:]).format() == PUBLIC_KEY_COMPRESSED[1:]
# Test __eq__ # Test __eq__
assert XonlyPublicKey(X_ONLY_PUBKEY) == XonlyPublicKey(X_ONLY_PUBKEY) assert PublicKeyXOnly(X_ONLY_PUBKEY) == PublicKeyXOnly(X_ONLY_PUBKEY)
def test_tweak(self): def test_tweak(self):
# Taken from BIP341 test vectors. # Taken from BIP341 test vectors.
# See github.com/bitcoin/bips/blob/6545b81022212a9f1c814f6ce1673e84bc02c910/bip-0341/wallet-test-vectors.json # See github.com/bitcoin/bips/blob/6545b81022212a9f1c814f6ce1673e84bc02c910/bip-0341/wallet-test-vectors.json
pubkey = XonlyPublicKey(bytes.fromhex('d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d')) pubkey = PublicKeyXOnly(bytes.fromhex('d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d'))
pubkey.tweak_add(bytes.fromhex('b86e7be8f39bab32a6f2c0443abbc210f0edac0e2c53d501b36b64437d9c6c70')) pubkey.tweak_add(bytes.fromhex('b86e7be8f39bab32a6f2c0443abbc210f0edac0e2c53d501b36b64437d9c6c70'))
assert pubkey.format() == bytes.fromhex('53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343') assert pubkey.format() == bytes.fromhex('53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343')
def test_parity(self): def test_parity(self):
# Taken from BIP341 test vectors. # Taken from BIP341 test vectors.
# See github.com/bitcoin/bips/blob/6545b81022212a9f1c814f6ce1673e84bc02c910/bip-0341/wallet-test-vectors.json # See github.com/bitcoin/bips/blob/6545b81022212a9f1c814f6ce1673e84bc02c910/bip-0341/wallet-test-vectors.json
pubkey = XonlyPublicKey(bytes.fromhex('187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27')) pubkey = PublicKeyXOnly(bytes.fromhex('187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27'))
pubkey.tweak_add(bytes.fromhex('cbd8679ba636c1110ea247542cfbd964131a6be84f873f7f3b62a777528ed001')) pubkey.tweak_add(bytes.fromhex('cbd8679ba636c1110ea247542cfbd964131a6be84f873f7f3b62a777528ed001'))
assert pubkey.format() == bytes.fromhex('147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3') assert pubkey.format() == bytes.fromhex('147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3')
assert pubkey.parity assert pubkey.parity
pubkey = XonlyPublicKey(bytes.fromhex('93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820')) pubkey = PublicKeyXOnly(bytes.fromhex('93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820'))
pubkey.tweak_add(bytes.fromhex('6af9e28dbf9d6aaf027696e2598a5b3d056f5fd2355a7fd5a37a0e5008132d30')) pubkey.tweak_add(bytes.fromhex('6af9e28dbf9d6aaf027696e2598a5b3d056f5fd2355a7fd5a37a0e5008132d30'))
assert pubkey.format() == bytes.fromhex('e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e') assert pubkey.format() == bytes.fromhex('e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e')
assert not pubkey.parity assert not pubkey.parity

Loading…
Cancel
Save