diff --git a/.github/scripts/build-windows-wheels.sh b/.github/scripts/build-windows-wheels.sh index 348bec9..b6ccce0 100755 --- a/.github/scripts/build-windows-wheels.sh +++ b/.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 } diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e287be1..0495a35 100644 --- a/.github/workflows/build.yml +++ b/.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 diff --git a/README.md b/README.md index ca07bb8..da79eaf 100644 --- a/README.md +++ b/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/)! diff --git a/_cffi_build/build.py b/_cffi_build/build.py index fa3843f..5625cc6 100644 --- a/_cffi_build/build.py +++ b/_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 '), Source('secp256k1_ecdh.h', '#include '), + Source('secp256k1_extrakeys.h', '#include '), Source('secp256k1_recovery.h', '#include '), + Source('secp256k1_schnorrsig.h', '#include '), Source('secp256k1_generator.h', '#include '), Source('secp256k1_ed25519.h', '#include '), Source('secp256k1_dleag.h', '#include '), diff --git a/_cffi_build/secp256k1_extrakeys.h b/_cffi_build/secp256k1_extrakeys.h new file mode 100644 index 0000000..36d4c57 --- /dev/null +++ b/_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 +); diff --git a/_cffi_build/secp256k1_schnorrsig.h b/_cffi_build/secp256k1_schnorrsig.h new file mode 100644 index 0000000..137022f --- /dev/null +++ b/_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 +); diff --git a/coincurve/__init__.py b/coincurve/__init__.py index f2289ab..17c56a1 100644 --- a/coincurve/__init__.py +++ b/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', +] diff --git a/coincurve/_windows_libsecp256k1.py b/coincurve/_windows_libsecp256k1.py index 8161357..34115a7 100644 --- a/coincurve/_windows_libsecp256k1.py +++ b/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__)) diff --git a/coincurve/context.py b/coincurve/context.py index e22f74e..f298f94 100644 --- a/coincurve/context.py +++ b/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__() diff --git a/coincurve/ecdsa.py b/coincurve/ecdsa.py index c062352..6c28af3 100644 --- a/coincurve/ecdsa.py +++ b/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 diff --git a/coincurve/keys.py b/coincurve/keys.py index d278bfa..e11e5a7 100644 --- a/coincurve/keys.py +++ b/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 diff --git a/coincurve/utils.py b/coincurve/utils.py index 1fbda65..b9d23e9 100644 --- a/coincurve/utils.py +++ b/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() diff --git a/docs/api.md b/docs/api.md index 566d56b..89dd943 100644 --- a/docs/api.md +++ b/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 diff --git a/docs/history.md b/docs/history.md index e546464..954d4d5 100644 --- a/docs/history.md +++ b/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 diff --git a/docs/index.md b/docs/index.md index 26ddea1..737545e 100644 --- a/docs/index.md +++ b/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)! diff --git a/docs/install.md b/docs/install.md index ddb8d20..5c4a999 100644 --- a/docs/install.md +++ b/docs/install.md @@ -19,6 +19,7 @@ Binary wheels are available for most platforms and require at least version `19. | CPython 3.8 |
  • x86_64
  • ARM64
|
  • x86_64
  • x86
|
  • x86_64
  • i686
  • AArch64
|
  • x86_64
  • i686
  • AArch64
| | CPython 3.9 |
  • x86_64
  • ARM64
|
  • x86_64
  • x86
|
  • x86_64
  • i686
  • AArch64
|
  • x86_64
  • i686
  • AArch64
| | CPython 3.10 |
  • x86_64
  • ARM64
|
  • x86_64
  • x86
|
  • x86_64
  • i686
  • AArch64
|
  • x86_64
  • i686
  • AArch64
| +| CPython 3.11 |
  • x86_64
  • ARM64
|
  • x86_64
  • x86
|
  • x86_64
  • i686
  • AArch64
|
  • x86_64
  • i686
  • AArch64
| ## Source diff --git a/docs/users.md b/docs/users.md index e118210..4a93a6e 100644 --- a/docs/users.md +++ b/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) diff --git a/pyproject.toml b/pyproject.toml index 50e14bb..2ad8f27 100644 --- a/pyproject.toml +++ b/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"] diff --git a/setup.py b/setup.py index eac4732..dd828e6 100644 --- a/setup.py +++ b/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', diff --git a/tests/samples.py b/tests/samples.py index 20faf0b..7280006 100644 --- a/tests/samples.py +++ b/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) diff --git a/tests/test_keys.py b/tests/test_keys.py index 89190c2..05616e8 100644 --- a/tests/test_keys.py +++ b/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 diff --git a/tox.ini b/tox.ini index eff729c..31da40d 100644 --- a/tox.ini +++ b/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}