Browse Source

Merge remote-tracking branch 'ofek/master' into anonswap

anonswap
tecnovert 2 years ago
parent
commit
c5821d1402
No known key found for this signature in database GPG Key ID: 6C1A887B4701EAE3
  1. 2
      .github/scripts/build-windows-wheels.sh
  2. 10
      .github/workflows/build.yml
  3. 1
      README.md
  4. 4
      _cffi_build/build.py
  5. 78
      _cffi_build/secp256k1_extrakeys.h
  6. 51
      _cffi_build/secp256k1_schnorrsig.h
  7. 10
      coincurve/__init__.py
  8. 137
      coincurve/_windows_libsecp256k1.py
  9. 3
      coincurve/context.py
  10. 6
      coincurve/ecdsa.py
  11. 146
      coincurve/keys.py
  12. 4
      coincurve/utils.py
  13. 13
      docs/api.md
  14. 6
      docs/history.md
  15. 1
      docs/index.md
  16. 1
      docs/install.md
  17. 2
      docs/users.md
  18. 43
      pyproject.toml
  19. 7
      setup.py
  20. 3
      tests/samples.py
  21. 61
      tests/test_keys.py
  22. 25
      tox.ini

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
}

10
.github/workflows/build.yml

@ -15,7 +15,7 @@ concurrency:
cancel-in-progress: true
env:
COINCURVE_UPSTREAM_REF: d8a246324650c3df8d54d133a8ac3c1b857a7a4e
COINCURVE_UPSTREAM_REF: ddf2b2910eb19032f8dd657c66735115ae24bfba
COINCURVE_IGNORE_SYSTEM_LIB: '1'
CIBW_BEFORE_ALL_MACOS: ./.github/scripts/install-macos-build-deps.sh
CIBW_ENVIRONMENT_PASS_LINUX: >
@ -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

1
README.md

@ -18,7 +18,6 @@ Feel free to read the [documentation](https://ofek.dev/coincurve/)!
- [Ethereum](https://ethereum.org)
- [LBRY](https://lbry.com)
- [ZeroNet](https://zeronet.io)
- [libp2p](https://libp2p.io)
and [many more](https://ofek.dev/coincurve/users/)!

4
_cffi_build/build.py

@ -13,7 +13,7 @@ def _mk_ffi(sources, name='_libsecp256k1', **kwargs):
code = []
for source in sources:
with open(os.path.join(here, source.h), 'rt') as h:
with open(os.path.join(here, source.h)) as h:
_ffi.cdef(h.read())
code.append(source.include)
@ -26,7 +26,9 @@ def _mk_ffi(sources, name='_libsecp256k1', **kwargs):
modules = [
Source('secp256k1.h', '#include <secp256k1.h>'),
Source('secp256k1_ecdh.h', '#include <secp256k1_ecdh.h>'),
Source('secp256k1_extrakeys.h', '#include <secp256k1_extrakeys.h>'),
Source('secp256k1_recovery.h', '#include <secp256k1_recovery.h>'),
Source('secp256k1_schnorrsig.h', '#include <secp256k1_schnorrsig.h>'),
Source('secp256k1_generator.h', '#include <secp256k1_generator.h>'),
Source('secp256k1_ed25519.h', '#include <secp256k1_ed25519.h>'),
Source('secp256k1_dleag.h', '#include <secp256k1_dleag.h>'),

78
_cffi_build/secp256k1_extrakeys.h

@ -0,0 +1,78 @@
typedef struct {
unsigned char data[64];
} secp256k1_xonly_pubkey;
typedef struct {
unsigned char data[96];
} secp256k1_keypair;
int secp256k1_xonly_pubkey_parse(
const secp256k1_context* ctx,
secp256k1_xonly_pubkey* pubkey,
const unsigned char *input32
);
int secp256k1_xonly_pubkey_serialize(
const secp256k1_context* ctx,
unsigned char *output32,
const secp256k1_xonly_pubkey* pubkey
);
int secp256k1_xonly_pubkey_cmp(
const secp256k1_context* ctx,
const secp256k1_xonly_pubkey* pk1,
const secp256k1_xonly_pubkey* pk2
);
int secp256k1_xonly_pubkey_from_pubkey(
const secp256k1_context* ctx,
secp256k1_xonly_pubkey *xonly_pubkey,
int *pk_parity,
const secp256k1_pubkey *pubkey
);
int secp256k1_xonly_pubkey_tweak_add(
const secp256k1_context* ctx,
secp256k1_pubkey *output_pubkey,
const secp256k1_xonly_pubkey *internal_pubkey,
const unsigned char *tweak32
);
int secp256k1_xonly_pubkey_tweak_add_check(
const secp256k1_context* ctx,
const unsigned char *tweaked_pubkey32,
int tweaked_pk_parity,
const secp256k1_xonly_pubkey *internal_pubkey,
const unsigned char *tweak32
);
int secp256k1_keypair_create(
const secp256k1_context* ctx,
secp256k1_keypair *keypair,
const unsigned char *seckey
);
int secp256k1_keypair_sec(
const secp256k1_context* ctx,
unsigned char *seckey,
const secp256k1_keypair *keypair
);
int secp256k1_keypair_pub(
const secp256k1_context* ctx,
secp256k1_pubkey *pubkey,
const secp256k1_keypair *keypair
);
int secp256k1_keypair_xonly_pub(
const secp256k1_context* ctx,
secp256k1_xonly_pubkey *pubkey,
int *pk_parity,
const secp256k1_keypair *keypair
);
int secp256k1_keypair_xonly_tweak_add(
const secp256k1_context* ctx,
secp256k1_keypair *keypair,
const unsigned char *tweak32
);

51
_cffi_build/secp256k1_schnorrsig.h

@ -0,0 +1,51 @@
typedef int (*secp256k1_nonce_function_hardened)(
unsigned char *nonce32,
const unsigned char *msg,
size_t msglen,
const unsigned char *key32,
const unsigned char *xonly_pk32,
const unsigned char *algo,
size_t algolen,
void *data
);
extern const secp256k1_nonce_function_hardened secp256k1_nonce_function_bip340;
typedef struct {
unsigned char magic[4];
secp256k1_nonce_function_hardened noncefp;
void* ndata;
} secp256k1_schnorrsig_extraparams;
int secp256k1_schnorrsig_sign(
const secp256k1_context* ctx,
unsigned char *sig64,
const unsigned char *msg32,
const secp256k1_keypair *keypair,
const unsigned char *aux_rand32
);
int secp256k1_schnorrsig_sign32(
const secp256k1_context* ctx,
unsigned char *sig64,
const unsigned char *msg32,
const secp256k1_keypair *keypair,
const unsigned char *aux_rand32
);
int secp256k1_schnorrsig_sign_custom(
const secp256k1_context* ctx,
unsigned char *sig64,
const unsigned char *msg,
size_t msglen,
const secp256k1_keypair *keypair,
secp256k1_schnorrsig_extraparams *extraparams
);
int secp256k1_schnorrsig_verify(
const secp256k1_context* ctx,
const unsigned char *sig64,
const unsigned char *msg,
size_t msglen,
const secp256k1_xonly_pubkey *pubkey
);

10
coincurve/__init__.py

@ -1,4 +1,12 @@
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__ = [
'GLOBAL_CONTEXT',
'Context',
'PrivateKey',
'PublicKey',
'PublicKeyXOnly',
'verify_signature',
]

137
coincurve/_windows_libsecp256k1.py

@ -176,6 +176,87 @@ int secp256k1_ec_pubkey_combine(
);
"""
EXTRAKEYS_DEFINITIONS = """
typedef struct {
unsigned char data[64];
} secp256k1_xonly_pubkey;
typedef struct {
unsigned char data[96];
} secp256k1_keypair;
int secp256k1_xonly_pubkey_parse(
const secp256k1_context* ctx,
secp256k1_xonly_pubkey* pubkey,
const unsigned char *input32
);
int secp256k1_xonly_pubkey_serialize(
const secp256k1_context* ctx,
unsigned char *output32,
const secp256k1_xonly_pubkey* pubkey
);
int secp256k1_xonly_pubkey_cmp(
const secp256k1_context* ctx,
const secp256k1_xonly_pubkey* pk1,
const secp256k1_xonly_pubkey* pk2
);
int secp256k1_xonly_pubkey_from_pubkey(
const secp256k1_context* ctx,
secp256k1_xonly_pubkey *xonly_pubkey,
int *pk_parity,
const secp256k1_pubkey *pubkey
);
int secp256k1_xonly_pubkey_tweak_add(
const secp256k1_context* ctx,
secp256k1_pubkey *output_pubkey,
const secp256k1_xonly_pubkey *internal_pubkey,
const unsigned char *tweak32
);
int secp256k1_xonly_pubkey_tweak_add_check(
const secp256k1_context* ctx,
const unsigned char *tweaked_pubkey32,
int tweaked_pk_parity,
const secp256k1_xonly_pubkey *internal_pubkey,
const unsigned char *tweak32
);
int secp256k1_keypair_create(
const secp256k1_context* ctx,
secp256k1_keypair *keypair,
const unsigned char *seckey
);
int secp256k1_keypair_sec(
const secp256k1_context* ctx,
unsigned char *seckey,
const secp256k1_keypair *keypair
);
int secp256k1_keypair_pub(
const secp256k1_context* ctx,
secp256k1_pubkey *pubkey,
const secp256k1_keypair *keypair
);
int secp256k1_keypair_xonly_pub(
const secp256k1_context* ctx,
secp256k1_xonly_pubkey *pubkey,
int *pk_parity,
const secp256k1_keypair *keypair
);
int secp256k1_keypair_xonly_tweak_add(
const secp256k1_context* ctx,
secp256k1_keypair *keypair,
const unsigned char *tweak32
);
"""
RECOVERY_DEFINITIONS = """
typedef struct {
unsigned char data[65];
@ -218,6 +299,60 @@ int secp256k1_ecdsa_recover(
);
"""
SCHNORRSIG_DEFINITIONS = """
typedef int (*secp256k1_nonce_function_hardened)(
unsigned char *nonce32,
const unsigned char *msg,
size_t msglen,
const unsigned char *key32,
const unsigned char *xonly_pk32,
const unsigned char *algo,
size_t algolen,
void *data
);
extern const secp256k1_nonce_function_hardened secp256k1_nonce_function_bip340;
typedef struct {
unsigned char magic[4];
secp256k1_nonce_function_hardened noncefp;
void* ndata;
} secp256k1_schnorrsig_extraparams;
int secp256k1_schnorrsig_sign(
const secp256k1_context* ctx,
unsigned char *sig64,
const unsigned char *msg32,
const secp256k1_keypair *keypair,
const unsigned char *aux_rand32
);
int secp256k1_schnorrsig_sign32(
const secp256k1_context* ctx,
unsigned char *sig64,
const unsigned char *msg32,
const secp256k1_keypair *keypair,
const unsigned char *aux_rand32
);
int secp256k1_schnorrsig_sign_custom(
const secp256k1_context* ctx,
unsigned char *sig64,
const unsigned char *msg,
size_t msglen,
const secp256k1_keypair *keypair,
secp256k1_schnorrsig_extraparams *extraparams
);
int secp256k1_schnorrsig_verify(
const secp256k1_context* ctx,
const unsigned char *sig64,
const unsigned char *msg,
size_t msglen,
const secp256k1_xonly_pubkey *pubkey
);
"""
ECDH_DEFINITIONS = """
int secp256k1_ecdh(
const secp256k1_context* ctx,
@ -232,7 +367,9 @@ int secp256k1_ecdh(
ffi = FFI()
ffi.cdef(BASE_DEFINITIONS)
ffi.cdef(EXTRAKEYS_DEFINITIONS)
ffi.cdef(RECOVERY_DEFINITIONS)
ffi.cdef(SCHNORRSIG_DEFINITIONS)
ffi.cdef(ECDH_DEFINITIONS)
here = os.path.dirname(os.path.abspath(__file__))

3
coincurve/context.py

@ -24,7 +24,8 @@ class Context:
with self._lock:
seed = urandom(32) if not seed or len(seed) != 32 else seed
res = lib.secp256k1_context_randomize(self.ctx, ffi.new('unsigned char [32]', seed))
assert res == 1
if not res:
raise ValueError('secp256k1_context_randomize')
def __repr__(self):
return self.name or super().__repr__()

6
coincurve/ecdsa.py

@ -87,7 +87,8 @@ def serialize_compact(raw_sig, context: Context = GLOBAL_CONTEXT): # no cov
output = ffi.new('unsigned char[%d]' % CDATA_SIG_LENGTH)
res = lib.secp256k1_ecdsa_signature_serialize_compact(context.ctx, output, raw_sig)
assert res == 1
if not res:
raise ValueError('secp256k1_ecdsa_signature_serialize_compact')
return bytes(ffi.buffer(output, CDATA_SIG_LENGTH))
@ -98,7 +99,8 @@ def deserialize_compact(ser_sig: bytes, context: Context = GLOBAL_CONTEXT): # n
raw_sig = ffi.new('secp256k1_ecdsa_signature *')
res = lib.secp256k1_ecdsa_signature_parse_compact(context.ctx, raw_sig, ser_sig)
assert res == 1
if not res:
raise ValueError('secp256k1_ecdsa_signature_parse_compact')
return raw_sig

146
coincurve/keys.py

@ -1,3 +1,4 @@
import os
from typing import Tuple
from asn1crypto.keys import ECDomainParameters, ECPointBitString, ECPrivateKey, PrivateKeyAlgorithm, PrivateKeyInfo
@ -31,6 +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.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:
"""
@ -59,6 +61,43 @@ class PrivateKey:
return cdata_to_der(signature, self.context)
def sign_schnorr(self, message: bytes, aux_randomness: bytes = b'') -> bytes:
"""Create a Schnorr signature.
: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)
if not res:
raise ValueError('Secret was invalid')
signature = ffi.new('unsigned char[64]')
res = lib.secp256k1_schnorrsig_sign32(self.context.ctx, signature, message, keypair, aux_randomness)
if not res:
raise ValueError('Signing failed')
res = lib.secp256k1_schnorrsig_verify(
self.context.ctx, signature, message, len(message), self.public_key_xonly.public_key
)
if not res:
raise ValueError('Invalid signature')
return bytes(ffi.buffer(signature))
def sign_recoverable(self, message: bytes, hasher: Hasher = sha256, custom_nonce: Nonce = DEFAULT_NONCE) -> bytes:
"""
Create a recoverable ECDSA signature.
@ -368,7 +407,7 @@ class PublicKey:
return PublicKey(public_key, context)
def format(self, compressed: bool = True) -> bytes:
def format(self, compressed: bool = True) -> bytes: # noqa: A003
"""
Format the public key.
@ -494,3 +533,108 @@ class PublicKey:
def __eq__(self, other) -> bool:
return self.format(compressed=False) == other.format(compressed=False)
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.
:type data: bytes
:param parity: Whether the encoded point is the negation of the public key.
:param context:
"""
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.')
self.public_key = public_key
self.parity = parity
self.context = context
@classmethod
def from_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT):
"""Derive an x-only public key from a private key secret.
:param secret: The private key secret.
:param context:
:return: The x-only public key.
"""
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)
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)
def format(self) -> bytes: # noqa: A003
"""Serialize the public key.
: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.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-byte Schnorr signature to verify.
:param message: The message to be verified.
:return: A boolean indicating whether or not the signature is correct.
"""
if len(signature) != 64:
raise ValueError('Signature must be 32 bytes long.')
return not not lib.secp256k1_schnorrsig_verify(
self.context.ctx, signature, message, len(message), self.public_key
)
def tweak_add(self, scalar: bytes):
"""Add a scalar to the public key.
: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.
"""
scalar = pad_scalar(scalar)
out_pubkey = ffi.new('secp256k1_pubkey *')
res = lib.secp256k1_xonly_pubkey_tweak_add(self.context.ctx, out_pubkey, self.public_key, scalar)
if not res:
raise ValueError('The tweak was out of range, or the resulting public key would be invalid')
pk_parity = ffi.new('int *')
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.public_key, other.public_key)
return res == 0

4
coincurve/utils.py

@ -27,11 +27,11 @@ if environ.get('COINCURVE_BUILDING_DOCS') != 'true':
else: # no cov
class __Nonce(tuple):
class __Nonce(tuple): # noqa: N801
def __repr__(self):
return '(ffi.NULL, ffi.NULL)'
class __HasherSHA256:
class __HasherSHA256: # noqa: N801
def __call__(self, bytestr: bytes) -> bytes:
return _sha256(bytestr).digest()

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

6
docs/history.md

@ -8,6 +8,12 @@ Important changes are emphasized.
## Unreleased
## 18.0.0
- Support Schnorr signatures
- Add support for Python 3.11
- Upgrade [libsecp256k1][] to the latest available version
## 17.0.0
- **Breaking:** Drop support for Python 3.6

1
docs/index.md

@ -26,7 +26,6 @@ C library used by [Bitcoin Core][] for operations on the elliptic curve [secp256
- [Ethereum](https://ethereum.org)
- [LBRY](https://lbry.com)
- [ZeroNet](https://zeronet.io)
- [libp2p](https://libp2p.io)
and [many more](users.md)!

1
docs/install.md

@ -19,6 +19,7 @@ Binary wheels are available for most platforms and require at least version `19.
| CPython 3.8 | <ul><li>x86_64</li><li>ARM64</li></ul> | <ul><li>x86_64</li><li>x86</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> |
| CPython 3.9 | <ul><li>x86_64</li><li>ARM64</li></ul> | <ul><li>x86_64</li><li>x86</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> |
| CPython 3.10 | <ul><li>x86_64</li><li>ARM64</li></ul> | <ul><li>x86_64</li><li>x86</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> |
| CPython 3.11 | <ul><li>x86_64</li><li>ARM64</li></ul> | <ul><li>x86_64</li><li>x86</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> |
## Source

2
docs/users.md

@ -40,4 +40,4 @@
- [python-idex](https://github.com/sammchardy/python-idex/blob/24cee970172491a7f7d5f52558727a77384cce26/requirements.txt#L2)
- [Rotki](https://github.com/rotki/rotki/blob/70508f99f890bcbd520f1efe7776194d6a5e5e06/requirements.txt#L8)
- [Vyper](https://github.com/vyperlang/vyper/blob/3bd0bf96856554810065fa9cfb89afef7625d436/Dockerfile#L15)
- [ZeroNet](https://github.com/HelloZeroNet/ZeroNet/blob/454c0b2e7e000fda7000cba49027541fbf327b96/requirements.txt#L12)
- [ZeroNet](https://github.com/zeronet-conservancy/zeronet-conservancy/blob/b6e18fd3738b4725726c5e170040deb3048c9048/requirements.txt#L12)

43
pyproject.toml

@ -1,9 +1,7 @@
[tool.black]
target-version = ["py37"]
line-length = 120
py36 = true
skip-string-normalization = true
include = '\.pyi?$'
exclude = '''
/(
\.eggs
@ -24,12 +22,33 @@ exclude = '''
)
'''
[tool.isort]
default_section = 'THIRDPARTY'
force_grid_wrap = 0
include_trailing_comma = true
known_first_party = 'coincurve'
line_length = 120
multi_line_output = 3
skip_glob = 'setup.py'
use_parentheses = true
[tool.ruff]
target-version = "py37"
line-length = 120
select = ["A", "B", "C", "E", "F", "I", "M", "N", "Q", "RUF", "S", "T", "U", "W", "YTT"]
ignore = [
# Allow non-abstract empty methods in abstract base classes
"B027",
# Ignore McCabe complexity
"C901",
# Allow boolean positional values in function calls, like `dict.get(... True)`
"FBT003",
# Ignore checks for possible passwords
"S105", "S106", "S107",
]
unfixable = [
# Don't touch unused imports
"F401",
]
[tool.ruff.isort]
known-first-party = ["coincurve"]
[tool.ruff.flake8-quotes]
inline-quotes = "single"
[tool.ruff.per-file-ignores]
"setup.py" = ["B", "C", "I", "N", "U"]
# Tests can use assertions
"tests/*" = ["S101"]
"tests/**/*" = ["S101"]

7
setup.py

@ -36,7 +36,7 @@ MAKE = 'gmake' if platform.system() in ['FreeBSD', 'OpenBSD'] else 'make'
# IMPORTANT: keep in sync with .github/workflows/build.yml
#
# Version of libsecp256k1 to download if none exists in the `libsecp256k1` directory
UPSTREAM_REF = os.getenv('COINCURVE_UPSTREAM_REF') or 'd8a246324650c3df8d54d133a8ac3c1b857a7a4e'
UPSTREAM_REF = os.getenv('COINCURVE_UPSTREAM_REF') or 'ddf2b2910eb19032f8dd657c66735115ae24bfba'
LIB_TARBALL_URL = f'https://github.com/bitcoin-core/secp256k1/archive/{UPSTREAM_REF}.tar.gz'
@ -190,7 +190,9 @@ class build_clib(_build_clib):
'--enable-static',
'--disable-dependency-tracking',
'--with-pic',
'--enable-module-extrakeys',
'--enable-module-recovery',
'--enable-module-schnorrsig',
'--prefix',
os.path.abspath(self.build_clib),
'--enable-experimental',
@ -278,7 +280,7 @@ else:
setup(
name='coincurve',
version='17.0.2',
version='18.0.2',
description='Cross-platform Python CFFI bindings for libsecp256k1',
long_description=open('README.md', 'r').read(),
@ -320,6 +322,7 @@ setup(
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Software Development :: Libraries',

3
tests/samples.py

@ -48,3 +48,6 @@ RECOVERABLE_SIGNATURE = (
b'\x92G\x80&8\x1cVz%2\xb0\x8a\xd0l\x0b4\x9c~\x93\x18\xad'
b'\xe4J\x9c-\n\x00'
)
X_ONLY_PUBKEY = b"Ncx\x00\xf1_'BV\x9ac\x0b\xec)\x0eH\xdf\xebc\xa9\\\x85\x19:\xf9L{B\xe6\x14\xfe\xa8"
X_ONLY_PUBKEY_INVALID = bytes(32)

61
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
from coincurve.keys import PrivateKey, PublicKey, PublicKeyXOnly
from coincurve.utils import bytes_to_int, int_to_bytes_padded, verify_signature
from .samples import (
@ -20,6 +20,8 @@ from .samples import (
PUBLIC_KEY_Y,
RECOVERABLE_SIGNATURE,
SIGNATURE,
X_ONLY_PUBKEY,
X_ONLY_PUBKEY_INVALID,
)
G = PublicKey(
@ -35,6 +37,9 @@ class TestPrivateKey:
def test_public_key(self):
assert PrivateKey(PRIVATE_KEY_BYTES).public_key.format() == PUBLIC_KEY_COMPRESSED
def test_xonly_pubkey(self):
assert PrivateKey(PRIVATE_KEY_BYTES).public_key_xonly.format() == PUBLIC_KEY_COMPRESSED[1:]
def test_signature_correct(self):
private_key = PrivateKey()
public_key = private_key.public_key
@ -59,6 +64,22 @@ class TestPrivateKey:
== PublicKey(recover(MESSAGE, deserialize_recoverable(private_key.sign_recoverable(MESSAGE)))).format()
)
def test_schnorr_signature(self):
private_key = PrivateKey()
message = urandom(32)
# Message must be 32 bytes
with pytest.raises(ValueError):
private_key.sign_schnorr(message + b'\x01')
# We can provide supplementary randomness
sig = private_key.sign_schnorr(message, urandom(32))
assert private_key.public_key_xonly.verify(sig, message)
# Or not
sig = private_key.sign_schnorr(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
@ -146,3 +167,41 @@ class TestPublicKey:
b = PrivateKey().public_key
assert PublicKey.combine_keys([a, b]) == a.combine([b])
class TestXonlyPubKey:
def test_parse_invalid(self):
# Must be 32 bytes
with pytest.raises(ValueError):
PublicKeyXOnly.from_secret(bytes(33))
# Must be an x coordinate for a valid point
with pytest.raises(ValueError):
PublicKeyXOnly(X_ONLY_PUBKEY_INVALID)
def test_roundtrip(self):
assert PublicKeyXOnly(X_ONLY_PUBKEY).format() == X_ONLY_PUBKEY
assert PublicKeyXOnly(PUBLIC_KEY_COMPRESSED[1:]).format() == PUBLIC_KEY_COMPRESSED[1:]
# Test __eq__
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 = 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 = PublicKeyXOnly(bytes.fromhex('187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27'))
pubkey.tweak_add(bytes.fromhex('cbd8679ba636c1110ea247542cfbd964131a6be84f873f7f3b62a777528ed001'))
assert pubkey.format() == bytes.fromhex('147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3')
assert pubkey.parity
pubkey = PublicKeyXOnly(bytes.fromhex('93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820'))
pubkey.tweak_add(bytes.fromhex('6af9e28dbf9d6aaf027696e2598a5b3d056f5fd2355a7fd5a37a0e5008132d30'))
assert pubkey.format() == bytes.fromhex('e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e')
assert not pubkey.parity

25
tox.ini

@ -5,6 +5,7 @@ envlist =
3.8
3.9
3.10
3.11
pypy3
bench
lint
@ -35,23 +36,19 @@ commands =
envdir = {toxworkdir}/lint
skip_install = true
deps =
flake8>=3.9
flake8-bugbear>=20.1.4
flake8-quotes>=3.2.0
black>=21.12b0
isort[pyproject]>=5
ruff
commands =
flake8 .
ruff .
black --check --diff .
isort --check-only --diff .
[testenv:fmt]
envdir = {[testenv:lint]envdir}
skip_install = true
deps = {[testenv:lint]deps}
commands =
isort .
black .
ruff --fix .
{[testenv:lint]commands}
[testenv:typing]
@ -70,19 +67,19 @@ setenv =
; See https://reproducible-builds.org/specs/source-date-epoch/
SOURCE_DATE_EPOCH=1580601600
deps =
mkdocs~=1.2.2
mkdocs~=1.3.1
; theme
mkdocs-material~=7.3.1
mkdocs-material~=8.3.9
; plugins
mkdocs-minify-plugin~=0.4.1
mkdocs-git-revision-date-localized-plugin~=0.10.0
mkdocstrings~=0.16.2
mkdocs-minify-plugin~=0.5.0
mkdocs-git-revision-date-localized-plugin~=1.1.0
mkdocstrings~=0.18.1
; Extensions
pymdown-extensions~=9.0
pymdown-extensions~=9.5.0
mkdocs-material-extensions~=1.0.3
mkpatcher~=1.0.2
; Necessary for syntax highlighting in code blocks
Pygments~=2.10.0
Pygments~=2.12.0
commands =
python -m mkdocs {posargs}

Loading…
Cancel
Save