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() {
./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
}

8
.github/workflows/build.yml

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

3
coincurve/__init__.py

@ -1,5 +1,5 @@
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
__all__ = [
@ -7,5 +7,6 @@ __all__ = [
'Context',
'PrivateKey',
'PublicKey',
'PublicKeyXOnly',
'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.context = 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:
"""
@ -61,31 +61,40 @@ class PrivateKey:
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.
:param message: the message to be signed as an array of 32 bytes.
:param aux_randomness: an optional 32 bytes of fresh randomness.
:return: the 64-bytes Schnorr signature.
"""
if not isinstance(message, bytes) or len(message) != 32:
raise ValueError('"message" must be an array of 32 bytes')
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')
:param message: The message to sign.
:param aux_randomness: An optional 32 bytes of fresh randomness. By default (empty bytestring), this
will be generated automatically. Set to `None` to disable this behavior.
:return: The Schnorr signature.
:raises ValueError: If the message was not 32 bytes long, the optional auxiliary random data was not
32 bytes long, signing failed, or the signature was invalid.
"""
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 *')
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]')
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(
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))
@ -516,96 +525,106 @@ class PublicKey:
return self.format(compressed=False) == other.format(compressed=False)
class XonlyPublicKey:
def __init__(self, data: bytes, parity: bool = False, context: Context = GLOBAL_CONTEXT):
"""A BIP340 'x-only' public key.
class PublicKeyXOnly:
def __init__(self, data, parity: bool = False, context: Context = GLOBAL_CONTEXT):
"""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 parity: Whether the encoded point is the negation of the pubkey.
:param data: The formatted public key.
: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 len(data) != 32:
raise ValueError('"data" if in bytes must be an array of 32 bytes')
self.xonly_pubkey = ffi.new('secp256k1_xonly_pubkey *')
parsed = lib.secp256k1_xonly_pubkey_parse(context.ctx, self.xonly_pubkey, data)
if not isinstance(data, bytes):
self.public_key = data
else:
public_key = ffi.new('secp256k1_xonly_pubkey *')
parsed = lib.secp256k1_xonly_pubkey_parse(context.ctx, public_key, data)
if not parsed:
raise ValueError('The public key could not be parsed or is invalid.')
else:
# data must be a cdata 'secp256k1_xonly_pubkey *' type
self.xonly_pubkey = data
self.public_key = public_key
self.parity = parity
self.context = context
@classmethod
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.
"""
if not isinstance(secret, bytes) or len(secret) != 32:
raise ValueError('"data" must be an array of 32 bytes')
secret = validate_secret(secret)
keypair = ffi.new('secp256k1_keypair *')
res = lib.secp256k1_keypair_create(context.ctx, keypair, 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 *')
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 *')
pk_parity = ffi.new('int *')
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:
"""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]')
res = lib.secp256k1_xonly_pubkey_serialize(self.context.ctx, output32, self.xonly_pubkey)
assert res, 'Public key in self.xonly_pubkey must be valid'
res = lib.secp256k1_xonly_pubkey_serialize(self.context.ctx, output32, self.public_key)
if not res:
raise ValueError('Public key in self.public_key must be valid')
return bytes(ffi.buffer(output32, 32))
def verify(self, signature: bytes, message: bytes) -> bool:
"""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.
:return: A boolean indicating whether or not the signature is correct.
"""
if not isinstance(signature, bytes) or len(signature) != 64:
raise ValueError('The "signature" parameter must be an array of 64 bytes')
if not isinstance(message, bytes):
raise ValueError('The "message" parameter must be an array of bytes')
if len(signature) != 64:
raise ValueError('Signature must be 32 bytes long.')
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):
"""Tweak the public key by adding the generator multiplied with tweak32 to it.
def tweak_add(self, scalar: bytes):
"""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:
raise ValueError('"tweak" must be an array of 32 bytes')
scalar = pad_scalar(scalar)
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:
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 *')
res = lib.secp256k1_xonly_pubkey_from_pubkey(self.context.ctx, self.xonly_pubkey, pk_parity, out_pubkey)
assert res and pk_parity[0] in (0, 1), 'Must always return 1 and a boolean parity'
self.parity = bool(pk_parity[0])
lib.secp256k1_xonly_pubkey_from_pubkey(self.context.ctx, self.public_key, pk_parity, out_pubkey)
self.parity = not not pk_parity[0]
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

13
docs/api.md

@ -19,6 +19,7 @@ All objects are available directly under the root namespace `coincurve`.
- __init__
- sign
- sign_recoverable
- sign_schnorr
- ecdh
- add
- multiply
@ -48,3 +49,15 @@ All objects are available directly under the root namespace `coincurve`.
- from_signature_and_message
- from_secret
- 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
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 .samples import (
@ -38,7 +38,7 @@ class TestPrivateKey:
assert PrivateKey(PRIVATE_KEY_BYTES).public_key.format() == PUBLIC_KEY_COMPRESSED
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):
private_key = PrivateKey()
@ -74,11 +74,11 @@ class TestPrivateKey:
# We can provide supplementary randomness
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
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):
assert PrivateKey(PRIVATE_KEY_BYTES).to_hex() == PRIVATE_KEY_HEX
@ -173,35 +173,35 @@ class TestXonlyPubKey:
def test_parse_invalid(self):
# Must be 32 bytes
with pytest.raises(ValueError):
XonlyPublicKey(bytes(33))
PublicKeyXOnly.from_secret(bytes(33))
# Must be an x coordinate for a valid point
with pytest.raises(ValueError):
XonlyPublicKey(X_ONLY_PUBKEY_INVALID)
PublicKeyXOnly(X_ONLY_PUBKEY_INVALID)
def test_roundtrip(self):
assert XonlyPublicKey(X_ONLY_PUBKEY).format() == X_ONLY_PUBKEY
assert XonlyPublicKey(PUBLIC_KEY_COMPRESSED[1:]).format() == PUBLIC_KEY_COMPRESSED[1:]
assert PublicKeyXOnly(X_ONLY_PUBKEY).format() == X_ONLY_PUBKEY
assert PublicKeyXOnly(PUBLIC_KEY_COMPRESSED[1:]).format() == PUBLIC_KEY_COMPRESSED[1:]
# Test __eq__
assert XonlyPublicKey(X_ONLY_PUBKEY) == XonlyPublicKey(X_ONLY_PUBKEY)
assert PublicKeyXOnly(X_ONLY_PUBKEY) == PublicKeyXOnly(X_ONLY_PUBKEY)
def test_tweak(self):
# Taken from BIP341 test vectors.
# 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'))
assert pubkey.format() == bytes.fromhex('53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343')
def test_parity(self):
# Taken from BIP341 test vectors.
# 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'))
assert pubkey.format() == bytes.fromhex('147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3')
assert pubkey.parity
pubkey = XonlyPublicKey(bytes.fromhex('93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820'))
pubkey = PublicKeyXOnly(bytes.fromhex('93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820'))
pubkey.tweak_add(bytes.fromhex('6af9e28dbf9d6aaf027696e2598a5b3d056f5fd2355a7fd5a37a0e5008132d30'))
assert pubkey.format() == bytes.fromhex('e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e')
assert not pubkey.parity

Loading…
Cancel
Save