Repository: ofek/coincurve Branch: master Commit: 7829b29c08eb Files: 65 Total size: 358.0 KB Directory structure: gitextract_tbfrxx9v/ ├── .codecov.yml ├── .conda/ │ ├── environment-dev.yml │ └── environment.yml ├── .gemini/ │ └── config.yaml ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── dependabot.yml │ ├── scripts/ │ │ └── install-macos-build-deps.sh │ └── workflows/ │ ├── build.yml │ ├── docs.yml │ ├── verify_conda_build.yml │ └── verify_shared_build.yml ├── .gitignore ├── .linkcheckerrc ├── CMakeLists.txt ├── LICENSE-APACHE ├── LICENSE-MIT ├── NOTICE ├── README.md ├── cm_library_c_binding/ │ ├── CMakeLists.txt │ └── build.py ├── cm_library_cffi_headers/ │ ├── CMakeLists.txt │ └── compose_cffi_headers.py ├── cm_python_module/ │ └── CMakeLists.txt ├── cm_vendored_library/ │ └── CMakeLists.txt ├── cmake/ │ ├── SetCrossCompilerGithubActions.cmake │ ├── SetDefaultVendoredLibrary.cmake │ ├── SetSystemLibIfExists.cmake │ ├── UnsetVendoredLibraryOptions.cmake │ ├── UpdateVendoredLibraryOptions.cmake │ └── VerifyPythonModule.cmake ├── docs/ │ ├── .snippets/ │ │ ├── abbrs.txt │ │ └── links.txt │ ├── api.md │ ├── assets/ │ │ └── css/ │ │ └── custom.css │ ├── benchmarks.md │ ├── history.md │ ├── index.md │ ├── install.md │ └── users.md ├── hatch.toml ├── hatch_build.py ├── mkdocs.yml ├── pyproject.toml ├── ruff.toml ├── ruff_defaults.toml ├── scripts/ │ ├── README.md │ └── bench.py ├── src/ │ └── coincurve/ │ ├── __init__.py │ ├── context.py │ ├── der.py │ ├── ecdsa.py │ ├── flags.py │ ├── keys.py │ ├── py.typed │ ├── types.py │ └── utils.py └── tests/ ├── __init__.py ├── conftest.py ├── data/ │ ├── ecdsa_sig.json │ └── pubkey.json ├── test_ecdsa.py ├── test_flags.py ├── test_keys.py └── test_utils.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codecov.yml ================================================ comment: layout: "diff, files" behavior: default coverage: range: 50..100 round: down precision: 2 status: project: default: target: '70' patch: default: target: '70' ================================================ FILE: .conda/environment-dev.yml ================================================ name: coincurve-with-conda-dev channels: - conda-forge - defaults dependencies: - cffi >=1.3.0 - cmake - libsecp256k1 - pkg-config - pytest - pytest-benchmark - python =3.12 - tox-conda ================================================ FILE: .conda/environment.yml ================================================ name: coincurve-with-conda channels: - conda-forge - defaults dependencies: - libsecp256k1 - cffi >=1.3.0 - asn1crypto ================================================ FILE: .gemini/config.yaml ================================================ # https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github have_fun: false code_review: pull_request_opened: summary: false code_review: false ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto # Custom for Visual Studio *.cs diff=csharp # Standard to msysgit *.doc diff=astextplain *.DOC diff=astextplain *.docx diff=astextplain *.DOCX diff=astextplain *.dot diff=astextplain *.DOT diff=astextplain *.pdf diff=astextplain *.PDF diff=astextplain *.rtf diff=astextplain *.RTF diff=astextplain ================================================ FILE: .github/FUNDING.yml ================================================ github: - ofek custom: - https://ofek.dev/donate/ - https://paypal.me/ofeklev ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" ================================================ FILE: .github/scripts/install-macos-build-deps.sh ================================================ #!/bin/bash set -ex # update brew brew update # Update openssl if necessary brew outdated openssl || brew upgrade openssl # Install packages needed to build lib-secp256k1 for pkg in pkg-config; do brew list $pkg > /dev/null || brew install $pkg brew outdated --quiet $pkg || brew upgrade $pkg done ================================================ FILE: .github/workflows/build.yml ================================================ name: build on: push: tags: - v* branches: - master pull_request: branches: - master concurrency: group: build-${{ github.head_ref }} cancel-in-progress: true env: PYTHON_VERSION: '3.13' COINCURVE_IGNORE_SYSTEM_LIB: 'ON' COINCURVE_SECP256K1_STATIC: 'ON' COINCURVE_CROSS_HOST: '' CIBW_ENVIRONMENT_PASS_LINUX: > COINCURVE_IGNORE_SYSTEM_LIB COINCURVE_SECP256K1_STATIC COINCURVE_CROSS_HOST CIBW_BEFORE_ALL_MACOS: ./.github/scripts/install-macos-build-deps.sh CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: > python -c "from coincurve import PrivateKey; a=PrivateKey(); b=PrivateKey(); assert a.ecdh(b.public_key.format())==b.ecdh(a.public_key.format()) " && python -m pytest {project} CIBW_SKIP: > pp* jobs: test: name: Test Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Checkout code uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install system dependencies if: runner.os == 'macOS' run: ./.github/scripts/install-macos-build-deps.sh - name: Install Hatch uses: pypa/hatch@install - name: Run static analysis run: hatch fmt --check - name: Check types run: hatch run types:check - name: Run tests run: hatch test --python ${{ matrix.python-version }} --cover-quiet --randomize - name: Create coverage report run: hatch run hatch-test.py${{ matrix.python-version }}:coverage xml - name: Upload coverage data uses: actions/upload-artifact@v4 with: name: coverage-${{ matrix.os }}-${{ matrix.python-version }} path: coverage.xml - name: Install uv uses: astral-sh/setup-uv@v5 - name: Benchmark run: uv run --python-preference system scripts/bench.py coverage: name: Upload coverage needs: - test runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - name: Download coverage data uses: actions/download-artifact@v5 with: pattern: coverage-* path: coverage_data - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: directory: coverage_data use_oidc: true linux-wheels-x86_64: name: Build Linux wheels for x86-64 needs: - test runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Build wheels uses: pypa/cibuildwheel@v2.23 - uses: actions/upload-artifact@v4 with: name: artifact-linux-wheels-x86_64 path: wheelhouse/*.whl if-no-files-found: error macos-wheels-x86_64: name: Build macOS wheels for x86-64 needs: - test runs-on: macos-15-intel steps: - uses: actions/checkout@v5 - name: Build wheels uses: pypa/cibuildwheel@v3.2.0 env: CIBW_ARCHS_MACOS: x86_64 CIBW_SKIP: "cp314t-*" MACOSX_DEPLOYMENT_TARGET: 10.13 - uses: actions/upload-artifact@v4 with: name: artifact-macos-wheels-x86_64 path: wheelhouse/*.whl if-no-files-found: error macos-wheels-arm64: name: Build macOS wheels for ARM64 needs: - test runs-on: macos-latest steps: - uses: actions/checkout@v5 - name: Build wheels uses: pypa/cibuildwheel@v2.23 - uses: actions/upload-artifact@v4 with: name: artifact-macos-wheels-arm64 path: wheelhouse/*.whl if-no-files-found: error windows-wheels-x86_64: name: Build Windows wheels for x86-64 needs: - test runs-on: windows-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - name: Build wheels uses: pypa/cibuildwheel@v2.23 env: CIBW_ARCHS_WINDOWS: 'AMD64' CIBW_BEFORE_ALL: choco install -y --no-progress --no-color cmake>=3.28 - uses: actions/upload-artifact@v4 with: name: artifact-windows-wheels-x86_64 path: wheelhouse/*.whl if-no-files-found: error windows-wheels-arm64: name: Build Windows wheels for ARM64 needs: - test runs-on: windows-latest steps: - uses: actions/checkout@v5 - name: Build wheels uses: pypa/cibuildwheel@v2.23 env: COINCURVE_CROSS_HOST: 'arm64' CIBW_ARCHS_WINDOWS: 'ARM64' CIBW_BEFORE_ALL: choco install -y --no-progress --no-color cmake>=3.28 - uses: actions/upload-artifact@v4 with: name: artifact-windows-wheels-arm64 path: wheelhouse/*.whl if-no-files-found: error sdist: name: Build source distribution needs: - test runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Install build dependencies run: python -m pip install build - name: Build source distribution run: python -m build --sdist - uses: actions/upload-artifact@v4 with: name: artifact-sdist path: dist/* if-no-files-found: error linux-wheels-arm64: name: Build Linux wheels for ARM64 needs: - test runs-on: ubuntu-latest if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.event.ref, 'refs/tags')) steps: - uses: actions/checkout@v5 - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: platforms: arm64 - name: Build wheels uses: pypa/cibuildwheel@v2.23 env: CIBW_ARCHS_LINUX: aarch64 - uses: actions/upload-artifact@v4 with: name: artifact-linux-wheels-arm64 path: wheelhouse/*.whl if-no-files-found: error publish: name: Publish release needs: - linux-wheels-x86_64 - macos-wheels-x86_64 - macos-wheels-arm64 - windows-wheels-x86_64 - windows-wheels-arm64 - sdist - linux-wheels-arm64 runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') permissions: id-token: write steps: - uses: actions/download-artifact@v5 with: pattern: artifact-* merge-multiple: true path: dist - run: ls -l dist - name: Push build artifacts to PyPI uses: pypa/gh-action-pypi-publish@v1.13.0 with: skip-existing: true ================================================ FILE: .github/workflows/docs.yml ================================================ name: docs on: push: branches: - master pull_request: branches: - master jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: # Fetch all history for applying timestamps to every page fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 with: python-version: '3.12' - name: Install Hatch uses: pypa/hatch@install - name: Check documentation run: hatch run docs:build-check - name: Build documentation run: hatch run docs:build - uses: actions/upload-artifact@v4 with: name: documentation path: site publish: runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/master' needs: - build steps: - uses: actions/download-artifact@v5 with: name: documentation path: site - uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./site commit_message: ${{ github.event.head_commit.message }} # Write .nojekyll at the root, see: # https://help.github.com/en/github/working-with-github-pages/about-github-pages#static-site-generators enable_jekyll: false # Only deploy if there were changes allow_empty_commit: false ================================================ FILE: .github/workflows/verify_conda_build.yml ================================================ name: conda_build on: push: tags: - v* branches: - master pull_request: branches: - master concurrency: group: build_conda-${{ github.head_ref }} cancel-in-progress: true env: PYTHON_VERSION: '3.12' COINCURVE_UPSTREAM_REF: __no_upstream__ COINCURVE_IGNORE_SYSTEM_LIB: 'OFF' COINCURVE_SECP256K1_STATIC: 'OFF' COINCURVE_CROSS_HOST: '' CIBW_ENVIRONMENT_PASS_LINUX: > COINCURVE_UPSTREAM_REF COINCURVE_IGNORE_SYSTEM_LIB COINCURVE_SECP256K1_STATIC COINCURVE_CROSS_HOST CIBW_BEFORE_ALL_MACOS: ./.github/scripts/install-macos-build-deps.sh CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: > python -c "from coincurve import PrivateKey; a=PrivateKey(); b=PrivateKey(); assert a.ecdh(b.public_key.format())==b.ecdh(a.public_key.format()) " && python -m pytest {project} CIBW_SKIP: > pp* jobs: test: name: Test with Conda libsecp256k1 runs-on: ubuntu-latest defaults: run: shell: bash -el {0} steps: - uses: actions/checkout@v5 - name: Install Miniconda uses: conda-incubator/setup-miniconda@v3 with: environment-file: ./.conda/environment-dev.yml activate-environment: coincurve-with-conda python-version: ${{ env.PYTHON_VERSION }} auto-activate-base: false - name: Install Hatch uses: pypa/hatch@install - name: Run static analysis run: hatch fmt --check - name: Check types run: hatch run types:check - name: Run tests run: LD_LIBRARY_PATH=$CONDA_PREFIX/lib hatch test - name: Install uv uses: astral-sh/setup-uv@v5 - name: Benchmark run: LD_LIBRARY_PATH=$CONDA_PREFIX/lib uv run --python-preference system scripts/bench.py linux-wheels-x86_64: name: Build Linux wheels needs: - test runs-on: ubuntu-latest defaults: run: shell: bash -el {0} steps: - uses: actions/checkout@v5 - name: Install Miniconda uses: conda-incubator/setup-miniconda@v3 with: environment-file: ./.conda/environment-dev.yml activate-environment: coincurve-with-conda python-version: ${{ env.PYTHON_VERSION }} auto-activate-base: false - name: Build sdist & wheel run: | conda install python-build python -m build --outdir conda_dist - name: Test wheel in a clean environment run: | conda install pip pip install conda_dist/*.whl python -m pytest tests ================================================ FILE: .github/workflows/verify_shared_build.yml ================================================ name: shared_build on: push: tags: - v* branches: - master pull_request: branches: - master concurrency: group: build_shared-${{ github.head_ref }} cancel-in-progress: true env: PYTHON_VERSION: '3.12' COINCURVE_IGNORE_SYSTEM_LIB: '1' # Only 'SHARED' is recognized, any other string means 'not SHARED' COINCURVE_SECP256K1_BUILD: 'SHARED' CIBW_ENVIRONMENT_PASS_LINUX: > COINCURVE_IGNORE_SYSTEM_LIB COINCURVE_SECP256K1_BUILD CIBW_BEFORE_ALL_MACOS: ./.github/scripts/install-macos-build-deps.sh CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: > python -c "from coincurve import PrivateKey; a=PrivateKey(); b=PrivateKey(); assert a.ecdh(b.public_key.format())==b.ecdh(a.public_key.format()) " && python -m pytest {project} CIBW_TEST_SKIP: "*-macosx_arm64" CIBW_SKIP: > pp* jobs: test: name: Test latest Python runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install system dependencies if: runner.os == 'macOS' run: ./.github/scripts/install-macos-build-deps.sh - name: Install Hatch uses: pypa/hatch@install - name: Run static analysis run: hatch fmt --check - name: Check types run: hatch run types:check - name: Run tests run: hatch test - name: Install uv uses: astral-sh/setup-uv@v5 - name: Benchmark run: uv run --python-preference system scripts/bench.py linux-wheels-x86_64: name: Build Linux wheels for x86-64 needs: - test runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Build wheels uses: pypa/cibuildwheel@v2.23 macos-wheels-x86_64: name: Build macOS wheels for x86-64 needs: - test runs-on: macos-15-intel steps: - uses: actions/checkout@v5 - name: Build wheels uses: pypa/cibuildwheel@v3.2.0 env: CIBW_ARCHS_MACOS: x86_64 CIBW_SKIP: "cp314t-*" MACOSX_DEPLOYMENT_TARGET: 10.13 macos-wheels-arm64: name: Build macOS wheels for ARM64 needs: - test runs-on: macos-latest steps: - uses: actions/checkout@v5 - name: Build wheels uses: pypa/cibuildwheel@v2.23 windows-wheels-x86_64: name: Build Windows wheels for x86-64 needs: - test runs-on: windows-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - name: Build wheels uses: pypa/cibuildwheel@v2.23 env: CIBW_ARCHS_WINDOWS: 'AMD64' CIBW_BEFORE_ALL: choco install -y --no-progress --no-color cmake>=3.28 windows-wheels-arm64: name: Build Windows wheels for ARM64 needs: - test runs-on: windows-latest steps: - uses: actions/checkout@v5 - name: Build wheels uses: pypa/cibuildwheel@v2.23 env: COINCURVE_CROSS_HOST: 'arm64' CIBW_ARCHS_WINDOWS: 'ARM64' CIBW_BEFORE_ALL: choco install -y --no-progress --no-color cmake>=3.28 ================================================ FILE: .gitignore ================================================ *.log *.pyc /.cache /.coverage /.eggs /.idea /.mypy_cache /.tox /coincurve.egg-info /build /dist /site ================================================ FILE: .linkcheckerrc ================================================ [AnchorCheck] ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.26) project(${SKBUILD_PROJECT_NAME} VERSION ${SKBUILD_PROJECT_VERSION} LANGUAGES C ) # Path to custom CMake functions set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) # Name of the vendored library CMake target include(SetDefaultVendoredLibrary) SetDefaultVendoredLibrary() set(CFFI_INPUT_LIBRARY ${VENDORED_LIBRARY_CMAKE_TARGET}) # Set the output directories for the generated C code and headers set(CFFI_C_CODE_DIR ${PROJECT_BINARY_DIR}/_gen_c_file) set(CFFI_C_CODE "${CFFI_INPUT_LIBRARY}_cffi_bindings.c") set(CFFI_HEADERS_DIR ${PROJECT_BINARY_DIR}/_gen_cffi_headers) # Shared object that wraps the CFFI binding of the vendored library set(CFFI_OUTPUT_LIBRARY "_${VENDORED_LIBRARY_PKG_CONFIG}") # Setting python for the host system (before change in CMAKE_SYSTEM_PROCESSOR) find_package(Python 3 REQUIRED COMPONENTS Interpreter Development.Module Development.SABIModule) include(SetCrossCompilerGithubActions) SetCrossCompilerGithubActions() # Add the subdirectories. Append CONDA to the PKG_CONFIG_PATH if (CMAKE_SYSTEM_NAME STREQUAL "Windows") set(PKG_CONFIG_ARGN "--dont-define-prefix") endif() find_package(PkgConfig REQUIRED) # Set VENDORED_AS_SYSTEM_LIB to true if the vendored library is installed as a system library include(SetSystemLibIfExists) SetSystemLibIfExists() add_subdirectory(cm_vendored_library) add_subdirectory(cm_library_cffi_headers) add_subdirectory(cm_library_c_binding) add_subdirectory(cm_python_module) # Configure installation of the shared library ${CFFI_OUTPUT_LIBRARY} in the package install(TARGETS ${CFFI_OUTPUT_LIBRARY} LIBRARY DESTINATION ${SKBUILD_PLATLIB_DIR}/${SKBUILD_PROJECT_NAME}) ================================================ FILE: LICENSE-APACHE ================================================ Copyright 2017 Ofek Lev Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: LICENSE-MIT ================================================ MIT License Copyright (c) 2017 Ofek Lev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: NOTICE ================================================ This package is dual-licensed under MIT or Apache-2.0. The final distribution includes the following compiled artifacts: * `_cffi_backend` shared library from the CFFI project (https://github.com/python-cffi/cffi) which is licensed under the MIT license. See `coincurve-X.Y.Z.dist-info/licenses/LICENSE-cffi` for the license text. * Code from the libsecp256k1 project (https://github.com/bitcoin-core/secp256k1) which is licensed under the MIT license. ================================================ FILE: README.md ================================================ # coincurve | | | | --- | --- | | CI/CD | [![CI - Test](https://github.com/ofek/coincurve/actions/workflows/build.yml/badge.svg)](https://github.com/ofek/coincurve/actions/workflows/build.yml) [![CI - Coverage](https://img.shields.io/codecov/c/github/ofek/coincurve/master.svg?logo=codecov&logoColor=red)](https://codecov.io/github/ofek/coincurve) | | Docs | [![CI - Docs](https://github.com/ofek/coincurve/actions/workflows/docs.yml/badge.svg)](https://github.com/ofek/coincurve/actions/workflows/docs.yml) | | Package | [![PyPI - Version](https://img.shields.io/pypi/v/coincurve.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/coincurve/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/coincurve.svg?color=blue&label=Downloads&logo=pypi&logoColor=gold)](https://pypi.org/project/coincurve/) [![Required Python Version](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fofek%2Fcoincurve%2FHEAD%2Fpyproject.toml)](https://pypi.org/project/coincurve/) | | Meta | [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/ofek/dep-sync) [![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![types - Mypy](https://img.shields.io/badge/types-Mypy-blue.svg)](https://github.com/python/mypy) [![License - MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-9400d3.svg)](https://spdx.org/licenses/) [![GitHub Sponsors](https://img.shields.io/github/sponsors/ofek?logo=GitHub%20Sponsors&style=social)](https://github.com/sponsors/ofek) | ----- This library provides well-tested Python bindings for [libsecp256k1](https://github.com/bitcoin-core/secp256k1), the heavily optimized C library used by [Bitcoin Core](https://github.com/bitcoin/bitcoin) for operations on the elliptic curve [secp256k1](https://en.bitcoin.it/wiki/Secp256k1). Feel free to read the [documentation](https://ofek.dev/coincurve/)! ## Users - [Ethereum](https://ethereum.org) - [LBRY](https://lbry.com) - [libp2p](https://libp2p.io) and [many more](https://ofek.dev/coincurve/users/)! ## License `coincurve` is distributed under the terms of any of the following licenses: - [MIT](https://spdx.org/licenses/MIT.html) - [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) ================================================ FILE: cm_library_c_binding/CMakeLists.txt ================================================ # create folder for _gen code file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/_gen_c_file) if (NOT CFFI_C_CODE) set(CFFI_C_CODE _cffi_c_code.c) endif() if (NOT CFFI_C_CODE_DIR) set(CFFI_C_CODE_DIR "${PROJECT_BINARY_DIR}/_gen_c_file") endif() # Copy the build.py file to the build directory file(COPY ${CMAKE_CURRENT_LIST_DIR}/build.py DESTINATION ${CFFI_C_CODE_DIR}) include(VerifyPythonModule) VerifyPythonModule(cffi ${Python_EXECUTABLE}) if (VENDORED_HEADERS_DIR) set(_static_build 'ON') else() message(STATUS "CFFI C-file is built for a SHARED system library") set(_static_build 'OFF') endif() # Generate the CFFI source file add_custom_command( OUTPUT ${CFFI_C_CODE_DIR}/${CFFI_C_CODE} COMMAND ${Python_EXECUTABLE} ${CFFI_C_CODE_DIR}/build.py ${CFFI_HEADERS_DIR} ${CFFI_C_CODE_DIR}/${CFFI_C_CODE} ${_static_build} MAIN_DEPENDENCY ${CMAKE_CURRENT_LIST_DIR}/build.py DEPENDS headers-for-cffi WORKING_DIRECTORY ${CFFI_C_CODE_DIR} COMMENT "Generating CFFI source file" ) add_custom_target(cffi-c-binding ALL DEPENDS ${CFFI_C_CODE_DIR}/${CFFI_C_CODE}) add_dependencies(cffi-c-binding headers-for-cffi) ================================================ FILE: cm_library_c_binding/build.py ================================================ from __future__ import annotations import argparse import logging import os from typing import NamedTuple from cffi import FFI logging.basicConfig(level=logging.INFO) here = os.path.dirname(os.path.abspath(__file__)) class Source(NamedTuple): h: str include: str def gather_sources_from_directory(directory: str) -> list[Source]: """ Gather source files from a given directory. :param directory: The directory where source files are located. :return: A list of Source namedtuples. """ sources = [] for filename in os.listdir(directory): if filename.endswith(".h"): include_line = f"#include <{filename}>" sources.append(Source(filename, include_line)) return sorted(sources) define_static_lib = """ #if defined(_WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) # define SECP256K1_STATIC 1 #endif """ def mk_ffi( directory: str, sources: list[Source], static_lib: bool = False, # noqa: FBT001, FBT002 name: str = "_libsecp256k1", ) -> FFI: """ Create an FFI object. :param sources: A list of Source namedtuples. :param static_lib: Whether to generate a static lib in Windows. :param name: The name of the FFI object. :return: An FFI object. """ _ffi = FFI() code = [define_static_lib] if static_lib else [] logging.info(" Static %s...", static_lib) for source in sources: with open(os.path.join(directory, source.h), encoding="utf-8") as h: logging.info(" Including %s...", source.h) c_header = h.read() _ffi.cdef(c_header) code.append(source.include) code.append("#define PY_USE_BUNDLED") _ffi.set_source(name, "\n".join(code)) return _ffi if __name__ == "__main__": logging.info("Starting CFFI build process...") parser = argparse.ArgumentParser(description="Generate C code using CFFI.") parser.add_argument("headers_dir", help="Path to the header files.", type=str) parser.add_argument("c_file", help="Generated C code filename.", type=str) parser.add_argument("static_lib", help="Generate static lib in Windows.", default="0N", type=str) args = parser.parse_args() modules = gather_sources_from_directory(args.headers_dir) ffi = mk_ffi(args.headers_dir, modules, args.static_lib == "ON") ffi.emit_c_code(args.c_file) vendor_cffi = os.environ.get("COINCURVE_VENDOR_CFFI", "1") == "1" if vendor_cffi: with open(args.c_file, encoding="utf-8") as f: source = f.read() expected_text = 'PyImport_ImportModule("_cffi_backend")' if expected_text not in source: msg = f"{expected_text} not found in {args.c_file}" raise ValueError(msg) new_source = source.replace(expected_text, 'PyImport_ImportModule("coincurve._cffi_backend")') with open(args.c_file, "w", encoding="utf-8") as f: f.write(new_source) logging.info(" Generated C code: %s", args.c_file) ================================================ FILE: cm_library_cffi_headers/CMakeLists.txt ================================================ # create folder for _gen code file(MAKE_DIRECTORY ${CFFI_HEADERS_DIR}) macro(generate_cffi_header src_header cffi_header cffi_dir) add_custom_command( OUTPUT ${cffi_dir}/${cffi_header} COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_LIST_DIR}/compose_cffi_headers.py ${src_header} ${cffi_header} ${cffi_dir} MAIN_DEPENDENCY ${CMAKE_CURRENT_LIST_DIR}/compose_cffi_headers.py DEPENDS ${src_header} WORKING_DIRECTORY ${cffi_dir} ) add_custom_target(${cffi_header} ALL DEPENDS ${cffi_dir}/${cffi_header}) endmacro() # Extract files from full path of src_headers if(VENDORED_HEADERS_DIR) file(GLOB src_headers ${VENDORED_HEADERS_DIR}/*.h) elseif(VENDORED_AS_SYSTEM_LIB_FOUND) message(WARNING "Using system library ${VENDORED_LIBRARY_CMAKE_TARGET}. The list of headers is set to:" " /*${VENDORED_LIBRARY_CMAKE_TARGET}/*.h") file(GLOB src_headers ${VENDORED_AS_SYSTEM_LIB_INCLUDE_DIRS}/*${VENDORED_LIBRARY_CMAKE_TARGET}*.h) message(STATUS " Generating CFFI header for ${src_headers}") else() message(FATAL_ERROR "Headers for CFFI cannot be found. Exiting") endif() add_custom_target(headers-for-cffi) foreach(src_header ${src_headers}) get_filename_component(cffi_header ${src_header} NAME) get_filename_component(src_header_dir ${src_header} DIRECTORY) generate_cffi_header(${src_header_dir} ${cffi_header} ${CFFI_HEADERS_DIR}) add_dependencies(headers-for-cffi ${cffi_header}) endforeach() ================================================ FILE: cm_library_cffi_headers/compose_cffi_headers.py ================================================ import argparse import logging import os import re import sys logging.basicConfig(level=logging.ERROR) def remove_c_comments_emptylines(text): text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL) # Remove multi-line comments text = re.sub(r"//.*", "", text) # Remove single-line comments return re.sub(r"\n\s*\n+", "\n", text) # Remove empty lines def remove_c_includes(lines): return [line for line in lines if not re.match(r"^\s*#include\s", line)] def remove_special_defines(lines, defines): return [line for line in lines if not any(f"#define {define}" in line for define in defines)] def apply_cffi_defines_syntax(lines): return [re.sub(r"#\s*define\s+(\w+).*", r"#define \1 ...", line) for line in lines] def remove_c_ifdef(lines): processed_lines = [] # The first #if is the multi-inclusion guard ifdef_count = -1 for line in lines: stripped_line = line.rstrip() if re.match(r"^#\s*(if|el|endif)", stripped_line): stripped_line = stripped_line.replace(" ", "") ifdef_count += stripped_line.count("#if") - stripped_line.count("#endif") continue if ifdef_count == 0: processed_lines.append(stripped_line) elif ifdef_count < 0 and line != lines[-1]: msg = "Unbalanced #if/#endif preprocessor directives." raise ValueError(msg) return processed_lines def concatenate_c_defines(lines): buffer = [] processed_lines = [] in_define = False for line in lines: stripped_line = line.rstrip() if (re.match(r"#\s*define", stripped_line) or in_define) and stripped_line.endswith("\\"): in_define = True buffer.append( re.sub(r"#\s*define", "#define", stripped_line).rstrip("\\").strip() ) # Normalize #define and remove trailing backslash continue # Skip the rest of the loop to avoid resetting the buffer if in_define: buffer.append(stripped_line) processed_lines.append(" ".join(buffer)) buffer = [] # Reset the buffer for the next definition in_define = False continue # Skip the rest of the loop to avoid adding the line again processed_lines.append(stripped_line) return processed_lines def remove_deprecated_functions(lines, deprecation): buffer = [] processed_lines = [] in_struct = False in_define = False brace_count = 0 for line in lines: stripped_line = line.rstrip() if re.match(r"#\s*define", stripped_line) or in_define: in_define = bool(stripped_line.endswith("\\")) processed_lines.append(stripped_line) continue if stripped_line.startswith("struct") or re.match(r"typedef\s+struct", stripped_line) or in_struct: in_struct = True processed_lines.append(stripped_line) brace_count += stripped_line.count("{") - stripped_line.count("}") if brace_count == 0: # End of struct block in_struct = False continue buffer.append(stripped_line) # Check for the end of a function declaration if stripped_line.endswith(";") and not in_struct: # Extend if not DEPRECATED if not any(d in " ".join(buffer) for d in deprecation): processed_lines.extend(buffer) buffer = [] # Reset the buffer for the next definition return processed_lines def remove_function_attributes(lines, attributes): processed_lines = [] for line in lines: stripped_line = line.rstrip() for attribute, replacement in attributes.items(): # Attributes can be functions with (...), so using regular expression # Remove the definition if re.search(rf"#\s*define\s+{attribute}(\(.*\))?\b", stripped_line): stripped_line = None break if re.search(rf"\b{attribute}(\(.*\))?\b", stripped_line): stripped_line = re.sub(rf"\b{attribute}(\(.*\))?", f"{replacement}", stripped_line) stripped_line = stripped_line.replace(" ;", ";") stripped_line = stripped_line.replace(" ", " ") if stripped_line: processed_lines.append(stripped_line) return processed_lines def remove_header_guard(lines, keywords): processed_lines = [] for line in lines: stripped_line = line.rstrip() for keyword in keywords: if re.search(rf"#\s*define\s+{keyword}.*_H\b", stripped_line): continue processed_lines.append(stripped_line) return processed_lines def concatenate_c_struct(lines): buffer = [] processed_lines = [] in_struct = False brace_count = 0 for line in lines: stripped_line = line.strip() if stripped_line.startswith("struct") or re.match(r"typedef\s+struct", stripped_line) or in_struct: in_struct = True brace_count += stripped_line.count("{") - stripped_line.count("}") buffer.append(stripped_line) if brace_count == 0: # End of struct block processed_lines.append(" ".join(buffer).strip()) buffer = [] # Reset the buffer for the next definition in_struct = False continue # Skip the rest of the loop to avoid adding the line again processed_lines.append(stripped_line) return processed_lines def make_header_cffi_compliant(src_header_dir, src_header, cffi_dir): with open(os.path.join(src_header_dir, src_header), encoding="utf-8") as f: text = remove_c_comments_emptylines(f.read()) lines = text.split("\n") lines = remove_c_includes(lines) lines = remove_c_ifdef(lines) lines = concatenate_c_defines(lines) lines = remove_deprecated_functions(lines, ["DEPRECATED"]) lines = remove_header_guard(lines, ["SECP256K1"]) lines = remove_function_attributes( lines, { "SECP256K1_API": "extern", "SECP256K1_WARN_UNUSED_RESULT": "", "SECP256K1_DEPRECATED": "", "SECP256K1_ARG_NONNULL": "", }, ) lines = remove_special_defines( lines, [ # Deprecated flags "SECP256K1_CONTEXT_VERIFY", "SECP256K1_CONTEXT_SIGN", "SECP256K1_FLAGS_BIT_CONTEXT_VERIFY", "SECP256K1_FLAGS_BIT_CONTEXT_SIGN", # Testing flags "SECP256K1_CONTEXT_DECLASSIFY", "SECP256K1_FLAGS_BIT_CONTEXT_DECLASSIFY", # Not for direct use - That may not mean to remove them! # 'SECP256K1_FLAGS_TYPE_MASK', # 'SECP256K1_FLAGS_TYPE_CONTEXT', # 'SECP256K1_FLAGS_TYPE_COMPRESSION', # 'SECP256K1_FLAGS_BIT_COMPRESSION', # Not supported "SECP256K1_SCHNORRSIG_EXTRAPARAMS_MAGIC", "SECP256K1_SCHNORRSIG_EXTRAPARAMS", ], ) lines = apply_cffi_defines_syntax(lines) logging.info(" Writing: %s in %s", src_header, cffi_dir) output_filename = os.path.join(cffi_dir, src_header) with open(output_filename, "w", encoding="utf-8") as f_out: f_out.write("\n".join(lines)) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Process a header file.") parser.add_argument("src_header_dir", type=str, help="The path to the header file to be processed.") parser.add_argument("cffi_header", type=str, help="The path where the compliant header will be written.") parser.add_argument("cffi_dir", type=str, help="The path where the compliant header will be written.", default=".") args = parser.parse_args() # Verify args are valid if not os.path.isdir(args.src_header_dir): logging.error("Error: Directory: %s not found.", args.src_header_dir) sys.exit(1) if not os.path.isdir(args.cffi_dir): logging.error("Error: Directory: %s not found.", args.cffi_dir) sys.exit(1) if not os.path.isfile(os.path.join(args.src_header_dir, args.cffi_header)): logging.error("Error: %s not found in %s.", args.cffi_header, args.src_header_dir) sys.exit(1) make_header_cffi_compliant(args.src_header_dir, args.cffi_header, args.cffi_dir) ================================================ FILE: cm_python_module/CMakeLists.txt ================================================ # Create the shared library from the CFFI binding and the static library from ${CFFI_INPUT_LIBRARY} if (CMAKE_SYSTEM_NAME STREQUAL "Windows") Python_add_library(${CFFI_OUTPUT_LIBRARY} MODULE USE_SABI 3.8 "${CFFI_C_CODE_DIR}/${CFFI_C_CODE}") else() set(Python_SOABI ${SKBUILD_SOABI}) Python_add_library(${CFFI_OUTPUT_LIBRARY} MODULE WITH_SOABI "${CFFI_C_CODE_DIR}/${CFFI_C_CODE}") target_compile_definitions(${CFFI_OUTPUT_LIBRARY} PRIVATE Py_LIMITED_API) endif() set_source_files_properties("${CFFI_C_CODE_DIR}/${CFFI_C_CODE}" PROPERTIES GENERATED 1) # Detect whether the vendored library is a system library or not if (PROJECT_IGNORE_SYSTEM_LIB OR NOT VENDORED_AS_SYSTEM_LIB_FOUND) # The build-type seems to be defined as 'MODULE', which creates issues with missing variables # for CMake: (This only happens on Windows though ...) set(CMAKE_MODULE_LINKER_FLAGS_COVERAGE "") add_dependencies(${CFFI_OUTPUT_LIBRARY} ${CFFI_INPUT_LIBRARY}) add_dependencies(${CFFI_OUTPUT_LIBRARY} cffi-c-binding) target_include_directories(${CFFI_OUTPUT_LIBRARY} PUBLIC ${VENDORED_HEADERS_DIR}) # Link the vendored library to the output library # https://docs.python.org/3/c-api/stable.html#limited-c-api target_link_libraries(${CFFI_OUTPUT_LIBRARY} PRIVATE ${CFFI_INPUT_LIBRARY}) elseif(VENDORED_AS_SYSTEM_LIB_FOUND) message(STATUS "Vendored system library found: ${VENDORED_AS_SYSTEM_LIB_LIBRARIES}") target_include_directories(${CFFI_OUTPUT_LIBRARY} PRIVATE ${VENDORED_AS_SYSTEM_LIB_INCLUDE_DIRS}) add_dependencies(${CFFI_OUTPUT_LIBRARY} cffi-c-binding) # On windows, using the LDFLAGS field creates /libpath... secp256k1.lib (correct), but at a later stage # /libpath is converted to \libpath and fails to be interpreted as a flag by the linker # This may be an issue with libsecp256k1.pc, i.e. wrong slash used that triggers the slash conversion target_link_libraries(${CFFI_OUTPUT_LIBRARY} PRIVATE ${VENDORED_AS_SYSTEM_LIB_LIBRARIES}) string(REPLACE "/libpath:" "" VENDORED_AS_SYSTEM_LIB_LIBRARY_DIRS ${VENDORED_AS_SYSTEM_LIB_LIBRARY_DIRS}) string(REPLACE "/LIBPATH:" "" VENDORED_AS_SYSTEM_LIB_LIBRARY_DIRS ${VENDORED_AS_SYSTEM_LIB_LIBRARY_DIRS}) target_link_directories(${CFFI_OUTPUT_LIBRARY} PRIVATE ${VENDORED_AS_SYSTEM_LIB_LIBRARY_DIRS}) else() message(FATAL_ERROR "Vendored library not found.") endif() # Add platform-specific definitions if(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_compile_definitions(${CFFI_OUTPUT_LIBRARY} PUBLIC "IS_LINUX") endif() if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") target_compile_definitions(${CFFI_OUTPUT_LIBRARY} PUBLIC "IS_MACOS") endif() if(CMAKE_SYSTEM_NAME STREQUAL "Windows") target_compile_definitions(${CFFI_OUTPUT_LIBRARY} PUBLIC "IS_WINDOWS") endif() ================================================ FILE: cm_vendored_library/CMakeLists.txt ================================================ if (PROJECT_IGNORE_SYSTEM_LIB OR NOT VENDORED_AS_SYSTEM_LIB_FOUND) # Note that this could also be handled by: ExternalProject_Add # However, FetchContent is a more flexible way to handle this # https://cmake.org/cmake/help/latest/module/ExternalProject.html # https://cmake.org/cmake/help/latest/module/FetchContent.html include(GNUInstallDirs) include(FetchContent) # Set ULR based upon CMake definitions if (NOT VENDORED_UPSTREAM_URL) set(VENDORED_UPSTREAM_URL "https://github.com/bitcoin-core/secp256k1/archive") endif() if (NOT VENDORED_UPSTREAM_REF) message(STATUS "VENDORED_UPSTREAM_REF not set, using default value.") set(VENDORED_UPSTREAM_REF "1ad5185cd42c0636104129fcc9f6a4bf9c67cc40") endif() if (NOT VENDORED_UPSTREAM_SHA) message(STATUS "VENDORED_UPSTREAM_SHA not set, using default value.") set(VENDORED_UPSTREAM_SHA "ba34be4319f505c5766aa80b99cfa696cbb2993bfecf7d7eb8696106c493cb8c") endif() if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() # -fPIC is needed since we will link it from a shared object if (VENDORED_LIBRARY_STATIC_BUILD) set(CMAKE_POSITION_INDEPENDENT_CODE ON) endif() include(UpdateVendoredLibraryOptions) UpdateVendoredLibraryOptions("VENDORED_OPTION" "${VENDORED_LIBRARY_OPTION_PREFIX}") FetchContent_Declare( vendored_library URL "${VENDORED_UPSTREAM_URL}/${VENDORED_UPSTREAM_REF}.tar.gz" URL_HASH "SHA256=${VENDORED_UPSTREAM_SHA}" ) FetchContent_MakeAvailable(vendored_library) if (NOT IS_DIRECTORY ${vendored_library_SOURCE_DIR}/include) message(FATAL_ERROR "The system library: ${VENDORED_LIBRARY_PKG_CONFIG} was not found OR") message(FATAL_ERROR "The IGNORE_SYSTEM_LIB flag was not set (${PROJECT_IGNORE_SYSTEM_LIB}) OR") message(FATAL_ERROR "The vendored library was not installed correctly (/include does not exists). Exiting") else() set(VENDORED_HEADERS_DIR "${vendored_library_SOURCE_DIR}/include" CACHE PATH "Path to the vendored headers") # Avoid spurious warnings when building the vendored library unset(VENDORED_UPSTREAM_URL PARENT_SCOPE) unset(VENDORED_UPSTREAM_REF PARENT_SCOPE) unset(VENDORED_UPSTREAM_SHA PARENT_SCOPE) endif() else() include(UnsetVendoredLibraryOptions) UnsetVendoredLibraryOptions("VENDORED_OPTION") unset(VENDORED_LIBRARY_OPTION_PREFIX) unset(VENDORED_LIBRARY_STATIC_BUILD) unset(VENDORED_UPSTREAM_URL) unset(VENDORED_UPSTREAM_REF) unset(VENDORED_UPSTREAM_SHA) endif() ================================================ FILE: cmake/SetCrossCompilerGithubActions.cmake ================================================ function(SetCrossCompilerGithubActions) # Cross-compilation options: This is setup for Github/Actions runners # For Linux, we use cibuildwheel to build the wheels, which uses Docker if (PROJECT_CROSS_COMPILE_TARGET AND CMAKE_SYSTEM_NAME STREQUAL "Darwin") set(AUTOMATIC_OSX_TARGETS "armv7" "armv7s" "arm64" "arm64e" "x86_64") if ("${PROJECT_CROSS_COMPILE_TARGET}" IN_LIST AUTOMATIC_OSX_TARGETS) set(CMAKE_OSX_ARCHITECTURES ${PROJECT_CROSS_COMPILE_TARGET}) else() message(FATAL_ERROR "Cross-compilation target not supported: >${PROJECT_CROSS_COMPILE_TARGET}< (${AUTOMATIC_OSX_TARGETS})") endif() elseif (PROJECT_CROSS_COMPILE_TARGET AND CMAKE_SYSTEM_NAME STREQUAL "Windows") set(AUTOMATIC_WINDOWS_TARGETS "AMD64" "x86" "arm64") if ("${PROJECT_CROSS_COMPILE_TARGET}" IN_LIST AUTOMATIC_WINDOWS_TARGETS) # Cross-compilation for Windows host system: set(CMAKE_SYSTEM_PROCESSOR ${PROJECT_CROSS_COMPILE_TARGET}) set(CMAKE_LIBRARY_ARCHITECTURE ${PROJECT_CROSS_COMPILE_TARGET}) else() message(FATAL_ERROR "Cross-compilation target not supported: >${PROJECT_CROSS_COMPILE_TARGET}< (${AUTOMATIC_WINDOWS_TARGETS})") endif() endif() endfunction() ================================================ FILE: cmake/SetDefaultVendoredLibrary.cmake ================================================ function (SetDefaultVendoredLibrary) if (DEFINED VENDORED_LIBRARY_CMAKE_TARGET) set(VENDORED_LIBRARY_CMAKE_TARGET ${VENDORED_LIBRARY_CMAKE_TARGET}) else() set(VENDORED_LIBRARY_CMAKE_TARGET "secp256k1") endif() set(VENDORED_LIBRARY_CMAKE_TARGET ${VENDORED_LIBRARY_CMAKE_TARGET} PARENT_SCOPE) if (DEFINED VENDORED_LIBRARY_PKG_CONFIG) set(VENDORED_LIBRARY_PKG_CONFIG ${VENDORED_LIBRARY_PKG_CONFIG}) else() set(VENDORED_LIBRARY_PKG_CONFIG "lib${VENDORED_LIBRARY_CMAKE_TARGET}") endif() set(VENDORED_LIBRARY_PKG_CONFIG ${VENDORED_LIBRARY_PKG_CONFIG} PARENT_SCOPE) if (DEFINED VENDORED_LIBRARY_PKG_CONFIG_VERSION) set(VENDORED_LIBRARY_PKG_CONFIG_VERSION ${VENDORED_LIBRARY_PKG_CONFIG_VERSION}) else() set(VENDORED_LIBRARY_PKG_CONFIG_VERSION "0.4.1") endif() set(VENDORED_LIBRARY_PKG_CONFIG_VERSION ${VENDORED_LIBRARY_PKG_CONFIG_VERSION} PARENT_SCOPE) endfunction() ================================================ FILE: cmake/SetSystemLibIfExists.cmake ================================================ function (SetSystemLibIfExists) set(ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH};$ENV{CONDA_PREFIX}/Library/lib/pkgconfig;$ENV{CONDA_PREFIX}/lib/pkgconfig") if(CMAKE_SYSTEM_NAME STREQUAL "Windows") set(PKG_CONFIG_EXECUTABLE "${PKG_CONFIG_EXECUTABLE};--msvc-syntax;--dont-define-prefix") endif() pkg_check_modules(VENDORED_AS_SYSTEM_LIB IMPORTED_TARGET GLOBAL ${VENDORED_LIBRARY_PKG_CONFIG}>=${VENDORED_LIBRARY_PKG_CONFIG_VERSION}) endfunction() ================================================ FILE: cmake/UnsetVendoredLibraryOptions.cmake ================================================ function (UnsetVendoredLibraryOptions _prefix) get_cmake_property(_vars VARIABLES) string (REGEX MATCHALL "(^|;)${_prefix}[A-Za-z0-9_]*" _matchedVars "${_vars}") foreach (_var ${_matchedVars}) unset (${_var} PARENT_SCOPE) endforeach() endfunction() ================================================ FILE: cmake/UpdateVendoredLibraryOptions.cmake ================================================ function (UpdateVendoredLibraryOptions _prefix _newPrefix) get_cmake_property(_vars VARIABLES) string (REGEX MATCHALL "(^|;)${_prefix}[A-Za-z0-9_]*" _matchedVars "${_vars}") foreach (_var ${_matchedVars}) string (REGEX REPLACE "^${_prefix}" "${_newPrefix}" _newVar ${_var}) set(${_newVar} ${${_var}} PARENT_SCOPE) unset (${_var} PARENT_SCOPE) endforeach() endfunction() ================================================ FILE: cmake/VerifyPythonModule.cmake ================================================ # Verify CFFI python module is available function(VerifyPythonModule module python_executable) find_package(Python3 REQUIRED COMPONENTS Interpreter) execute_process( COMMAND ${python_executable} -c "import ${module}" ERROR_VARIABLE _error OUTPUT_QUIET ERROR_STRIP_TRAILING_WHITESPACE ) if(_error) message(FATAL_ERROR "${module} is required to build coincurve") endif() endfunction() ================================================ FILE: docs/.snippets/abbrs.txt ================================================ *[ECDH]: Elliptic-curve Diffie–Hellman *[PyPI]: Python Package Index ================================================ FILE: docs/.snippets/links.txt ================================================ [Bitcoin Core]: https://github.com/bitcoin/bitcoin [ECDH]: https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman [RFC 6979]: https://tools.ietf.org/html/rfc6979 [libsecp256k1]: https://github.com/bitcoin-core/secp256k1 [secp256k1]: https://en.bitcoin.it/wiki/Secp256k1 ================================================ FILE: docs/api.md ================================================ # Developer Interface ----- All objects are available directly under the root namespace `coincurve`. ::: coincurve.verify_signature ::: coincurve.PrivateKey options: members: - __init__ - sign - sign_recoverable - sign_schnorr - ecdh - add - multiply - to_int - to_hex - to_pem - to_der - from_int - from_hex - from_pem - from_der ::: coincurve.PublicKey options: members: - __init__ - verify - format - point - combine - add - multiply - combine_keys - from_signature_and_message - from_secret - from_valid_secret - from_point ::: coincurve.PublicKeyXOnly options: members: - __init__ - verify - format - tweak_add - from_secret - from_valid_secret ================================================ FILE: docs/assets/css/custom.css ================================================ /* Brighter links for dark mode */ [data-md-color-scheme=slate] { /* https://github.com/squidfunk/mkdocs-material/blob/9.1.2/src/assets/stylesheets/main/_colors.scss#L91-L92 */ --md-typeset-a-color: var(--md-primary-fg-color--light); } /* FiraCode https://github.com/tonsky/FiraCode */ code { font-family: 'Fira Code', monospace; } @supports (font-variation-settings: normal) { code { font-family: 'Fira Code VF', monospace; } } /* https://github.com/squidfunk/mkdocs-material/issues/1522 */ .md-typeset h5 { color: var(--md-default-fg-color); text-transform: none; } /* Indentation. */ div.doc-contents:not(.first) { padding-left: 25px; border-left: .05rem solid var(--md-typeset-table-color); } /* Mark external links as such. */ a.external::after, a.autorefs-external::after { /* https://primer.style/octicons/arrow-up-right-24 */ mask-image: url('data:image/svg+xml,'); -webkit-mask-image: url('data:image/svg+xml,'); content: ' '; display: inline-block; vertical-align: middle; position: relative; height: 1em; width: 1em; background-color: currentColor; } a.external:hover::after, a.autorefs-external:hover::after { background-color: var(--md-accent-fg-color); } ================================================ FILE: docs/benchmarks.md ================================================ # Benchmarks ----- ## Setup Download [Hatch](https://hatch.pypa.io/latest/install/) or [UV](https://docs.astral.sh/uv/getting-started/installation/) in order to run the benchmarks as follows: ``` [hatch|uv] run scripts/bench.py ``` ## Results | Library | Key generation | Signing | Verification | Key export | Key import | | --- | --- | --- | --- | --- | --- | | coincurve v21.0.0 | 33.4 | 52.8 | 59.0 | 12.6 | 39.4 | | [fastecdsa](https://github.com/AntonKueltz/fastecdsa) v3.0.1 | 1319.6 | 1449.5 | 1160.4 | 1402.9 | 15.5 | !!! note - the timings are in microseconds - signing and verification use a 16 KiB message - the Python version used for the benchmarks is 3.13.x ================================================ FILE: docs/history.md ================================================ # History ----- Important changes are emphasized. ## Unreleased ## 21.0.0 - **Breaking:** Drop support for Python 3.8 - Add support for Python 3.13 - Remove all runtime dependencies (`cffi` & `asn1crypto`) - Add `COINCURVE_VENDOR_CFFI` environment variable to control vendoring of the `_cffi_backend` module - Minor performance improvement by removing use of formatted string constants - Upgrade [libsecp256k1][] to version 0.6.0 ## 20.0.0 - **Breaking:** CMake is now a build dependency; this is only a breaking change for redistributors as building with standard Python packaging tools will automatically use the CMake that is available on PyPI - **Breaking:** Stop building wheels for Windows 32-bit - Build wheels for Windows ARM64 - Upgrade [libsecp256k1][] to version 0.5.0 ## 19.0.1 - Fix regression in Windows wheels ## 19.0.0 - **Breaking:** Drop support for Python 3.7 - Add support for Python 3.12 - Upgrade [libsecp256k1][] to version 0.4.1 ## 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 - Fix wheels for Apple M1 - Upgrade [libsecp256k1][] to the latest available version ## 16.0.0 - Wheels for Apple Silicon and musl linux (Alpine) - No wheels for PyPy until the build system is fixed ## 15.0.1 - Fix the `combine` method of `PublicKey` ## 15.0.0 - **Breaking:** Drop support for Python 2 - **Breaking:** Binary wheels for CPython require version 19.3 or later of ``pip`` to install - Build AArch64 binary wheels for Linux - Build binary wheels for PyPy3.6 7.3.3 & PyPy3.7 7.3.3 on Linux - Upgrade [libsecp256k1][] to the latest available version - Upgrade libgmp to the latest available version - Introduce `COINCURVE_UPSTREAM_REF` environment variable to select an alternative [libsecp256k1][] version when building from source - Support PEP 561 type hints - Added support for supplying a custom nonce to `PrivateKey.sign_recoverable` ## 14.0.0 **IMPORTANT: This will be the final release that supports Python 2.** - **New:** Binary wheels for Python 3.9 - **Breaking:** Drop support for Python 3.5 - Fetch [libsecp256k1][] source if the system installation lacks ECDH support - Fix innocuous `setuptools` warning when building from source - Switch CI/CD to GitHub Actions ## 13.0.0 - **New:** Binary wheels for Python 3.8 - Support building on OpenBSD - Improve handling of PEM private key deserialization - Improve ECDH documentation - Improvements from [libsecp256k1][] master ## 12.0.0 - **New:** Binary wheels on Linux for PyPy3.6 v7.1.1-beta - **New:** Binary wheels on macOS for Python 3.8.0-alpha.3 - **New:** Binary wheels on Linux are now also built with the new [manylinux2010](https://www.python.org/dev/peps/pep-0571) spec for 64-bit platforms - Improvements from [libsecp256k1][] master ## 11.0.0 - Fix some linking scenarios by placing bundled [libsecp256k1][] dir first in path - Allow override of system [libsecp256k1][] with environment variable - Add benchmarks - Use Codecov to track coverage - Use black for code formatting ## 10.0.0 - Support tox for testing - Compatibility with latest [libsecp256k1][] ECDH API - Make libgmp optional when building from source ## 9.0.0 - Fixed wheels for macOS - **Breaking:** Drop support for 32-bit macOS ## 8.0.2 - No longer package tests ## 8.0.0 - **New:** Binary wheels for Python 3.7 - **Changed:** Binary wheels on macOS for Python 3.5 now use Homebrew Python for compilation due to new security requirements - Make build system support new GitHub & PyPI security requirements - Improvements from [libsecp256k1][] master ## 7.1.0 - Pin version of [libsecp256k1][] - Improve docs ## 7.0.0 - Improvements from [libsecp256k1][] master - Fix build script ## 6.0.0 - Resolved #6. You can choose to use this or remain on `5.2.0`. This will only be a temporary change, see 3e93480b3e38c6b9beb0bc2de83bc3630fc74c46. ## 5.2.0 - Added support for supplying a custom nonce to `PrivateKey.sign` ## 5.1.0 - Added `PublicKey.combine_keys` class method - Improvements to documentation ## 5.0.1 - Fixed an issue where secret validation would occasionally erroneously error on user-provided secrets (secrets not generated by Coincurve itself) if there were not exactly 256 bits of entropy. See #5. ## 5.0.0 - **Breaking:** Coincurve is now dual-licensed under the terms of `MIT` and `Apache-2.0` - Performance improvements from [libsecp256k1][] master - Improvements to documentation. ## 4.5.1 - First public stable release ================================================ FILE: docs/index.md ================================================ # coincurve | | | | --- | --- | | CI/CD | [![CI - Test](https://github.com/ofek/coincurve/actions/workflows/build.yml/badge.svg){ loading=lazy }](https://github.com/ofek/coincurve/actions/workflows/build.yml) [![CI - Coverage](https://img.shields.io/codecov/c/github/ofek/coincurve/master.svg?logo=codecov&logoColor=red){ loading=lazy }](https://codecov.io/github/ofek/coincurve) | | Docs | [![CI - Docs](https://github.com/ofek/coincurve/actions/workflows/docs.yml/badge.svg){ loading=lazy }](https://github.com/ofek/coincurve/actions/workflows/docs.yml) | | Package | [![PyPI - Version](https://img.shields.io/pypi/v/coincurve.svg?logo=pypi&label=PyPI&logoColor=gold){ loading=lazy }](https://pypi.org/project/coincurve/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/coincurve.svg?color=blue&label=Downloads&logo=pypi&logoColor=gold){ loading=lazy }](https://pypi.org/project/coincurve/) [![Required Python Version](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fofek%2Fcoincurve%2FHEAD%2Fpyproject.toml){ loading=lazy }](https://pypi.org/project/coincurve/) | | Meta | [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg){ loading=lazy }](https://github.com/ofek/dep-sync) [![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json){ loading=lazy }](https://github.com/astral-sh/ruff) [![types - Mypy](https://img.shields.io/badge/types-Mypy-blue.svg){ loading=lazy }](https://github.com/python/mypy) [![License - MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-9400d3.svg){ loading=lazy }](https://spdx.org/licenses/) [![GitHub Sponsors](https://img.shields.io/github/sponsors/ofek?logo=GitHub%20Sponsors&style=social){ loading=lazy }](https://github.com/sponsors/ofek) | ----- This library provides well-tested Python bindings for [libsecp256k1][], the heavily optimized C library used by [Bitcoin Core][] for operations on the elliptic curve [secp256k1][]. ## Features - Fastest available implementation (more than 10x faster than OpenSSL) - Clean, easy to use API - Frequent updates from the development version of [libsecp256k1][] - Linux, macOS, and Windows all have binary packages for multiple architectures - Deterministic signatures as specified by [RFC 6979][] - Non-malleable signatures (lower-S form) by default - Secure, non-malleable [ECDH][] implementation ## Users - [Ethereum](https://ethereum.org) - [LBRY](https://lbry.com) - [libp2p](https://libp2p.io) and [many more](users.md)! ## License `coincurve` is distributed under the terms of any of the following licenses: - [MIT](https://spdx.org/licenses/MIT.html) - [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) ## Navigation Desktop readers can use keyboard shortcuts to navigate. | Keys | Action | | --- | --- | | | Navigate to the "previous" page | | | Navigate to the "next" page | | | Display the search modal | ================================================ FILE: docs/install.md ================================================ # Installation ----- `coincurve` is available on PyPI and can be installed with [pip](https://pip.pypa.io): ``` pip install coincurve ``` ## Wheel Binary wheels are available for most platforms and require at least version `19.3` of pip to install. | | | | | | | --- | --- | --- | --- | --- | | | macOS | Windows | Linux (glibc) | Linux (musl) | | CPython 3.9 | | | | | | CPython 3.10 | | | | | | CPython 3.11 | | | | | | CPython 3.12 | | | | | | CPython 3.13 | | | | | ## Source If you are on a platform without support for pre-compiled wheels, you will need certain system packages in order to build from source. A few environment variables influence the build: - `COINCURVE_UPSTREAM_REF` - This is the Git reference of [libsecp256k1][] to use rather than the (frequently updated) default. - `COINCURVE_IGNORE_SYSTEM_LIB` - The presence of this will force fetching of [libsecp256k1][] even if it's already detected at the system level. - `COINCURVE_VENDOR_CFFI` - Setting this to anything other than `1` (the default) prevents vendoring of the `_cffi_backend` module. Re-distributors should make sure to require `cffi` as a runtime dependency when disabling this. !!! tip To avoid installing the binary wheels on compatible distributions, use the `--no-binary` option. ``` pip install coincurve --no-binary coincurve ``` ### Alpine ``` sudo apk add autoconf automake build-base libffi-dev libtool pkgconfig python3-dev ``` ### Debian/Ubuntu ``` sudo apt-get install -y autoconf automake build-essential libffi-dev libtool pkg-config python3-dev ``` ### RHEL/CentOS ``` sudo yum install -y autoconf automake gcc gcc-c++ libffi-devel libtool make pkgconfig python3-devel ``` ### macOS ``` xcode-select --install brew install autoconf automake libffi libtool pkg-config python ``` ================================================ FILE: docs/users.md ================================================ # Users ----- ## Organizations | Name | Projects | | --- | --- | | [Anyl](https://github.com/Anylsite) | | | [ARK](https://ark.io) | | | [Augur](https://www.augur.net) | | | [Blockcerts](https://www.blockcerts.org) | | | [ECIES](https://ecies.org) | | | [Elements](https://elementsproject.org) | | | [Ethereum](https://ethereum.org) | | | [Gnosis](https://gnosis.io) | | | [Golem Network](https://golem.network) | | | [ICON Foundation](https://icon.foundation) | | | [LBRY](https://lbry.com) | | | [libp2p](https://libp2p.io) | | | [Microsoft](https://www.microsoft.com) | | | [NuCypher](https://www.nucypher.com) | | | [Quantstamp](https://quantstamp.com) | | | [QuarkChain](https://www.quarkchain.io) | | | [Raiden Network](https://raiden.network) | | | [SKALE Network](https://skale.network) | | ## Projects - [bit](https://github.com/ofek/bit/blob/776f97ae7f9b3f05157113abc913eb141b2817ee/setup.py#L44) - [btcrecover](https://github.com/gurnec/btcrecover/commit/f113867fa22d2f5b22175cc2b5b3892351bc1109) - [crankycoin](https://github.com/cranklin/crankycoin/blob/7663a1c5429b3ddd11997b6a2e3488018789bf3b/requirements.txt#L2) - [ForkDelta](https://github.com/forkdelta/backend-replacement/blob/97ccd1a19544f26d242a8412113086f0c0dd5760/requirements.txt#L46) - [Heimdall](https://github.com/maddevsio/heimdall/blob/21f16880030cfdb1c1c97969158bec02ca6c0336/requirements.txt#L14) - [HoneyBadgerBFT](https://github.com/initc3/HoneyBadgerBFT-Python/blob/e8bcbc081dfb5d1e7298039d47bbebf7048b8e62/setup.py#L30) - [JoinMarket](https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/dc581e9c99d7db6436ed9f8913b6ce614bcef8d8/jmbitcoin/setup.py#L13) - [minichain](https://github.com/kigawas/minichain/blob/0ae0437fdc4aa05e73c4d31a8df91d371542c8fe/pyproject.toml#L13) - [Nekoyume](https://github.com/nekoyume/nekoyume/blob/0dec2d6f1002091f3f727bd645ce67fadd85faeb/setup.cfg#L45) - [NuCypher](https://github.com/nucypher/nucypher/blob/24a57e1c810aa6408ecfc24942956925146aa024/requirements.txt#L16) - [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/zeronet-conservancy/zeronet-conservancy/blob/b6e18fd3738b4725726c5e170040deb3048c9048/requirements.txt#L12) ================================================ FILE: hatch.toml ================================================ [envs.default] installer = "uv" dev-mode = false [envs.hatch-static-analysis] config-path = "ruff_defaults.toml" dependencies = ["ruff==0.13.0"] [envs.hatch-test] dev-mode = false [envs.types] dependencies = [ "mypy", "pytest", ] [envs.types.scripts] check = "mypy --install-types --non-interactive {args:src/coincurve tests}" [envs.docs] dependencies = [ "mkdocs~=1.6.1", "mkdocs-material~=9.5.40", # Plugins "mkdocs-minify-plugin~=0.8.0", "mkdocs-git-revision-date-localized-plugin~=1.2.9", "mkdocs-glightbox~=0.4.0", "mkdocs-redirects~=1.2.1", "mkdocstrings-python~=1.16.2", "mike~=2.1.3", # Extensions "pymdown-extensions~=10.11.2", # Necessary for syntax highlighting in code blocks "pygments~=2.18.0", # Validation "linkchecker~=10.5.0", ] [envs.docs.env-vars] SOURCE_DATE_EPOCH = "1580601600" PYTHONUNBUFFERED = "1" [envs.docs.scripts] build = "mkdocs build --clean --strict {args}" serve = "mkdocs serve --dev-addr localhost:8000 {args}" ci-build = "mike deploy --update-aliases {args}" validate = "linkchecker --config .linkcheckerrc site" # https://github.com/linkchecker/linkchecker/issues/678 build-check = [ "build --no-directory-urls", "validate", ] ================================================ FILE: hatch_build.py ================================================ from __future__ import annotations import os import shutil from functools import cached_property from importlib.metadata import PackagePath, distribution from typing import Any import _cffi_backend # noqa: PLC2701 from hatchling.builders.hooks.plugin.interface import BuildHookInterface class CustomBuildHook(BuildHookInterface): """ A build hook that copies the `_cffi_backend` extension module into the wheel so that the `cffi` package is not required as a runtime dependency. """ LICENSE_NAME = "LICENSE-cffi" @cached_property def local_cffi_license(self) -> str: return os.path.join(self.root, self.LICENSE_NAME) @staticmethod def get_cffi_distribution_license_files() -> list[PackagePath]: license_files = [] dist_files = distribution("cffi").files or [] for f in dist_files: if f.name == "LICENSE" and f.parts[0].endswith(".dist-info"): license_files.append(f) break return license_files def initialize(self, version: str, build_data: dict[str, Any]) -> None: # noqa: ARG002 cffi_shared_lib = _cffi_backend.__file__ relative_path = f"coincurve/{os.path.basename(cffi_shared_lib)}" build_data["force_include"][cffi_shared_lib] = relative_path license_files = self.get_cffi_distribution_license_files() if len(license_files) != 1: message = f"Expected exactly one LICENSE file in cffi distribution, got {len(license_files)}" raise RuntimeError(message) license_file = license_files[0] shutil.copy2(license_file.locate(), self.local_cffi_license) self.metadata.core.license_files.append(self.LICENSE_NAME) def finalize(self, version: str, build_data: dict[str, Any], artifact: str) -> None: # noqa: ARG002 os.remove(self.local_cffi_license) ================================================ FILE: mkdocs.yml ================================================ site_name: coincurve site_description: Cross-platform Python bindings for libsecp256k1 site_author: Ofek Lev site_url: https://ofek.dev/coincurve/ repo_name: ofek/coincurve repo_url: https://github.com/ofek/coincurve edit_uri: blob/master/docs copyright: 'Copyright © Ofek Lev 2017-present' docs_dir: docs site_dir: site theme: name: material language: en font: text: Roboto code: Roboto Mono icon: logo: material/circle-multiple repo: fontawesome/brands/github-alt favicon: assets/images/favicon.ico palette: - media: "(prefers-color-scheme: dark)" scheme: slate primary: teal accent: teal toggle: icon: material/weather-night name: Switch to light mode - media: "(prefers-color-scheme: light)" scheme: default primary: teal accent: teal toggle: icon: material/weather-sunny name: Switch to dark mode features: - content.action.edit - content.code.copy - content.tabs.link - content.tooltips - navigation.expand - navigation.footer - navigation.instant - navigation.sections nav: - About: index.md - Install: install.md - API Reference: api.md - Benchmarks: benchmarks.md - Meta: - Users: users.md - History: history.md plugins: # Built-in search: {} # Extra glightbox: {} minify: minify_html: true git-revision-date-localized: type: date strict: false mkdocstrings: default_handler: python handlers: python: paths: - src options: # Rendering show_root_full_path: false # Headings show_root_heading: true show_source: false show_symbol_type_toc: true # Docstrings show_if_no_docstring: true # Signatures/annotations show_signature_annotations: true signature_crossrefs: true # Other show_bases: false import: - https://docs.python.org/3/objects.inv markdown_extensions: # Built-in - markdown.extensions.abbr: - markdown.extensions.admonition: - markdown.extensions.attr_list: - markdown.extensions.footnotes: - markdown.extensions.md_in_html: - markdown.extensions.meta: - markdown.extensions.tables: - markdown.extensions.toc: permalink: true # Extra - pymdownx.arithmatex: - pymdownx.betterem: smart_enable: all - pymdownx.caret: - pymdownx.critic: - pymdownx.details: - pymdownx.emoji: # https://github.com/twitter/twemoji # https://raw.githubusercontent.com/facelessuser/pymdown-extensions/master/pymdownx/twemoji_db.py emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.highlight: guess_lang: false linenums_style: pymdownx-inline use_pygments: true - pymdownx.inlinehilite: - pymdownx.keys: - pymdownx.magiclink: repo_url_shortener: true repo_url_shorthand: true social_url_shortener: true social_url_shorthand: true normalize_issue_symbols: true provider: github user: ofek repo: coincurve - pymdownx.mark: - pymdownx.progressbar: - pymdownx.saneheaders: - pymdownx.smartsymbols: - pymdownx.snippets: check_paths: true base_path: - docs/.snippets auto_append: - links.txt - abbrs.txt - pymdownx.superfences: - pymdownx.tabbed: alternate_style: true slugify: !!python/object/apply:pymdownx.slugs.slugify kwds: case: lower - pymdownx.tasklist: custom_checkbox: true - pymdownx.tilde: extra: social: - icon: fontawesome/brands/github-alt link: https://github.com/ofek - icon: fontawesome/solid/blog link: https://ofek.dev/words/ - icon: fontawesome/brands/x-twitter link: https://x.com/Ofekmeister - icon: fontawesome/brands/linkedin link: https://www.linkedin.com/in/ofeklev/ extra_css: - assets/css/custom.css - https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css ================================================ FILE: pyproject.toml ================================================ [build-system] build-backend = "hatchling.build" requires = [ "hatchling>=1.27.0", "cffi", "scikit-build-core>=0.9.0", "pkgconf; sys_platform == 'win32'", ] [project] name = "coincurve" authors = [ { name = "Ofek Lev", email = "oss@ofek.dev" }, ] description = "Safest and fastest Python library for secp256k1 elliptic curve operations" keywords = [ "bitcoin", "crypto", "cryptocurrency", "ecdh", "ecdsa", "elliptic curves", "ethereum", "libsecp256k1", "secp256k1", "schnorr", ] readme = "README.md" license = "MIT OR Apache-2.0" license-files = [ "LICENSE-APACHE", "LICENSE-MIT", "NOTICE", ] requires-python = ">=3.9" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries", "Topic :: Security :: Cryptography", ] dynamic = ["version"] [project.urls] Homepage = "https://ofek.dev/coincurve/" Sponsor = "https://github.com/sponsors/ofek" History = "https://ofek.dev/coincurve/history/" Tracker = "https://github.com/ofek/coincurve/issues" Source = "https://github.com/ofek/coincurve" # --- hatch --- [tool.hatch.version] path = "src/coincurve/__init__.py" [tool.hatch.build.targets.wheel.hooks.custom] [tool.hatch.build.targets.wheel.hooks.scikit-build] experimental = true build.verbose = true cmake.build-type = "Release" cmake.source-dir = "." wheel.py-api = "" wheel.packages = [] # --- scikit-build-core --- [tool.scikit-build.cmake.define] CMAKE_BUILD_TYPE = "Release" # Coincurve build options - This may be better extracted from the ENV by the CMake directly? PROJECT_IGNORE_SYSTEM_LIB = { env = "COINCURVE_IGNORE_SYSTEM_LIB", default = "ON" } PROJECT_CROSS_COMPILE_TARGET = { env = "COINCURVE_CROSS_HOST", default = "" } # Vendored library: SECP256K1 VENDORED_LIBRARY_CMAKE_TARGET = "secp256k1" VENDORED_LIBRARY_PKG_CONFIG = "libsecp256k1" VENDORED_LIBRARY_PKG_CONFIG_VERSION = "0.6.0" VENDORED_UPSTREAM_URL = "https://github.com/bitcoin-core/secp256k1/archive/" VENDORED_UPSTREAM_REF = { env = "COINCURVE_UPSTREAM_REF", default = "0cdc758a56360bf58a851fe91085a327ec97685a" } VENDORED_UPSTREAM_SHA = { env = "COINCURVE_UPSTREAM_SHA", default = "385c115a21ee1ff31d0b0320acc2b278c92f7bde971f510566ad481a38835be0" } # SECP256K1 library specific build options # `VENDORED_OPTION` is reserved prefix for vendored library build options VENDORED_LIBRARY_OPTION_PREFIX = "SECP256K1" VENDORED_OPTION_DISABLE_SHARED = { env = "COINCURVE_SECP256K1_STATIC", default = "ON" } VENDORED_OPTION_BUILD_BENCHMARK = "OFF" VENDORED_OPTION_BUILD_TESTS = "OFF" VENDORED_OPTION_BUILD_CTIME_TESTS = "OFF" VENDORED_OPTION_BUILD_EXHAUSTIVE_TESTS = "OFF" VENDORED_OPTION_BUILD_EXAMPLES = "OFF" VENDORED_OPTION_ENABLE_MODULE_ECDH = "ON" VENDORED_OPTION_ENABLE_MODULE_RECOVERY = "ON" VENDORED_OPTION_ENABLE_MODULE_SCHNORRSIG = "ON" VENDORED_OPTION_ENABLE_MODULE_EXTRAKEYS = "ON" VENDORED_OPTION_EXPERIMENTAL = "ON" # Vendored library build options (cmake, compiler, linker, etc.) # VENDORED_CMAKE is reserved prefix for vendored library cmake options # VENDORED_CMAKE__ = VENDORED_LIBRARY_STATIC_BUILD = { env = "COINCURVE_SECP256K1_STATIC", default = "ON" } # --- Coverage --- [tool.coverage.run] source_pkgs = ["coincurve", "tests"] branch = true parallel = true omit = [ "src/coincurve/__init__.py", "tests/test_bench.py", ] [tool.coverage.report] exclude_lines =[ "no cov", # Ignore missing debug-only code "def __repr__", "if self\\.debug", # Ignore non-runnable code "if __name__ == .__main__.:", ] [tool.coverage.paths] coincurve = ["src/coincurve", "*/coincurve/src/coincurve"] tests = ["tests", "*/coincurve/tests"] # --- Pytest --- [tool.pytest.ini_options] addopts = [ "--import-mode=importlib", ] # --- Mypy --- [tool.mypy] disallow_untyped_defs = false follow_imports = "normal" ignore_missing_imports = true pretty = true show_column_numbers = true warn_no_return = false warn_unused_ignores = true ================================================ FILE: ruff.toml ================================================ extend = "ruff_defaults.toml" [format] preview = true [lint] preview = true [lint.extend-per-file-ignores] # Implicit namespace packages "cm_library_c_binding/build.py" = ["INP001"] "cm_library_cffi_headers/compose_cffi_headers.py" = ["INP001"] ================================================ FILE: ruff_defaults.toml ================================================ line-length = 120 [format] docstring-code-format = true docstring-code-line-length = 80 [lint] select = [ "A001", "A002", "A003", "ARG001", "ARG002", "ARG003", "ARG004", "ARG005", "ASYNC100", "ASYNC105", "ASYNC109", "ASYNC110", "ASYNC115", "B002", "B003", "B004", "B005", "B006", "B007", "B008", "B009", "B010", "B011", "B012", "B013", "B014", "B015", "B016", "B017", "B018", "B019", "B020", "B021", "B022", "B023", "B024", "B025", "B026", "B028", "B029", "B030", "B031", "B032", "B033", "B034", "B035", "B904", "B905", "B909", "BLE001", "C400", "C401", "C402", "C403", "C404", "C405", "C406", "C408", "C409", "C410", "C411", "C413", "C414", "C415", "C416", "C417", "C418", "C419", "C420", "COM818", "DTZ001", "DTZ002", "DTZ003", "DTZ004", "DTZ005", "DTZ006", "DTZ007", "DTZ011", "DTZ012", "E101", "E112", "E113", "E115", "E116", "E201", "E202", "E203", "E211", "E221", "E222", "E223", "E224", "E225", "E226", "E227", "E228", "E231", "E241", "E242", "E251", "E252", "E261", "E262", "E265", "E266", "E271", "E272", "E273", "E274", "E275", "E401", "E402", "E502", "E701", "E702", "E703", "E711", "E712", "E713", "E714", "E721", "E722", "E731", "E741", "E742", "E743", "E902", "EM101", "EM102", "EM103", "EXE001", "EXE002", "EXE003", "EXE004", "EXE005", "F401", "F402", "F403", "F404", "F405", "F406", "F407", "F501", "F502", "F503", "F504", "F505", "F506", "F507", "F508", "F509", "F521", "F522", "F523", "F524", "F525", "F541", "F601", "F602", "F621", "F622", "F631", "F632", "F633", "F634", "F701", "F702", "F704", "F706", "F707", "F722", "F811", "F821", "F822", "F823", "F841", "F842", "F901", "FA100", "FA102", "FBT001", "FBT002", "FLY002", "FURB105", "FURB110", "FURB113", "FURB116", "FURB118", "FURB129", "FURB131", "FURB132", "FURB136", "FURB142", "FURB145", "FURB148", "FURB152", "FURB157", "FURB161", "FURB163", "FURB164", "FURB166", "FURB167", "FURB168", "FURB169", "FURB171", "FURB177", "FURB180", "FURB181", "FURB187", "FURB192", "G001", "G002", "G003", "G004", "G010", "G101", "G201", "G202", "I001", "I002", "ICN001", "ICN002", "ICN003", "INP001", "INT001", "INT002", "INT003", "ISC003", "LOG001", "LOG002", "LOG007", "LOG009", "N801", "N802", "N803", "N804", "N805", "N806", "N807", "N811", "N812", "N813", "N814", "N815", "N816", "N817", "N818", "N999", "PERF101", "PERF102", "PERF401", "PERF402", "PERF403", "PGH005", "PIE790", "PIE794", "PIE796", "PIE800", "PIE804", "PIE807", "PIE808", "PIE810", "PLC0105", "PLC0131", "PLC0132", "PLC0205", "PLC0208", "PLC0414", "PLC0415", "PLC1901", "PLC2401", "PLC2403", "PLC2701", "PLC2801", "PLC3002", "PLE0100", "PLE0101", "PLE0115", "PLE0116", "PLE0117", "PLE0118", "PLE0237", "PLE0241", "PLE0302", "PLE0303", "PLE0304", "PLE0305", "PLE0307", "PLE0308", "PLE0309", "PLE0604", "PLE0605", "PLE0643", "PLE0704", "PLE1132", "PLE1141", "PLE1142", "PLE1205", "PLE1206", "PLE1300", "PLE1307", "PLE1310", "PLE1507", "PLE1519", "PLE1520", "PLE1700", "PLE2502", "PLE2510", "PLE2512", "PLE2513", "PLE2514", "PLE2515", "PLE4703", "PLR0124", "PLR0133", "PLR0202", "PLR0203", "PLR0206", "PLR0402", "PLR1704", "PLR1711", "PLR1714", "PLR1722", "PLR1730", "PLR1733", "PLR1736", "PLR2004", "PLR2044", "PLR5501", "PLR6104", "PLR6201", "PLR6301", "PLW0108", "PLW0120", "PLW0127", "PLW0128", "PLW0129", "PLW0131", "PLW0133", "PLW0177", "PLW0211", "PLW0245", "PLW0406", "PLW0602", "PLW0603", "PLW0604", "PLW0642", "PLW0711", "PLW1501", "PLW1508", "PLW1509", "PLW1510", "PLW1514", "PLW1641", "PLW2101", "PLW2901", "PLW3201", "PLW3301", "PT001", "PT002", "PT003", "PT006", "PT007", "PT008", "PT009", "PT010", "PT011", "PT012", "PT013", "PT014", "PT015", "PT016", "PT017", "PT018", "PT019", "PT020", "PT021", "PT022", "PT023", "PT024", "PT025", "PT026", "PT027", "PYI001", "PYI002", "PYI003", "PYI004", "PYI005", "PYI006", "PYI007", "PYI008", "PYI009", "PYI010", "PYI011", "PYI012", "PYI013", "PYI014", "PYI015", "PYI016", "PYI017", "PYI018", "PYI019", "PYI020", "PYI021", "PYI024", "PYI025", "PYI026", "PYI029", "PYI030", "PYI032", "PYI033", "PYI034", "PYI035", "PYI036", "PYI041", "PYI042", "PYI043", "PYI044", "PYI045", "PYI046", "PYI047", "PYI048", "PYI049", "PYI050", "PYI051", "PYI052", "PYI053", "PYI054", "PYI055", "PYI056", "PYI058", "PYI059", "PYI062", "RET503", "RET504", "RET505", "RET506", "RET507", "RET508", "RSE102", "RUF001", "RUF002", "RUF003", "RUF005", "RUF006", "RUF007", "RUF008", "RUF009", "RUF010", "RUF012", "RUF013", "RUF015", "RUF016", "RUF017", "RUF018", "RUF019", "RUF020", "RUF021", "RUF022", "RUF023", "RUF024", "RUF026", "RUF027", "RUF028", "RUF029", "RUF100", "RUF101", "S101", "S102", "S103", "S104", "S105", "S106", "S107", "S108", "S110", "S112", "S113", "S201", "S202", "S301", "S302", "S303", "S304", "S305", "S306", "S307", "S308", "S310", "S311", "S312", "S313", "S314", "S315", "S316", "S317", "S318", "S319", "S321", "S323", "S324", "S401", "S402", "S403", "S405", "S406", "S407", "S408", "S409", "S411", "S412", "S413", "S415", "S501", "S502", "S503", "S504", "S505", "S506", "S507", "S508", "S509", "S601", "S602", "S604", "S605", "S606", "S607", "S608", "S609", "S610", "S611", "S612", "S701", "S702", "SIM101", "SIM102", "SIM103", "SIM105", "SIM107", "SIM108", "SIM109", "SIM110", "SIM112", "SIM113", "SIM114", "SIM115", "SIM116", "SIM117", "SIM118", "SIM201", "SIM202", "SIM208", "SIM210", "SIM211", "SIM212", "SIM220", "SIM221", "SIM222", "SIM223", "SIM300", "SIM910", "SIM911", "SLF001", "SLOT000", "SLOT001", "SLOT002", "T100", "T201", "T203", "TC001", "TC002", "TC003", "TC004", "TC005", "TC010", "TD004", "TD005", "TD006", "TD007", "TID251", "TID252", "TID253", "TRY002", "TRY003", "TRY004", "TRY201", "TRY203", "TRY300", "TRY301", "TRY400", "TRY401", "UP001", "UP003", "UP004", "UP005", "UP006", "UP007", "UP008", "UP009", "UP010", "UP011", "UP012", "UP013", "UP014", "UP015", "UP017", "UP018", "UP019", "UP020", "UP021", "UP022", "UP023", "UP024", "UP025", "UP026", "UP028", "UP029", "UP030", "UP031", "UP032", "UP033", "UP034", "UP035", "UP036", "UP037", "UP039", "UP040", "UP041", "UP042", "W291", "W292", "W293", "W391", "W505", "W605", "YTT101", "YTT102", "YTT103", "YTT201", "YTT202", "YTT203", "YTT204", "YTT301", "YTT302", "YTT303", ] [lint.per-file-ignores] "**/scripts/*" = [ "INP001", "T201", ] "**/tests/**/*" = [ "PLC1901", "PLR2004", "PLR6301", "S", "TID252", ] [lint.flake8-tidy-imports] ban-relative-imports = "all" [lint.isort] known-first-party = ["coincurve"] [lint.flake8-pytest-style] fixture-parentheses = false mark-parentheses = false ================================================ FILE: scripts/README.md ================================================ # Scripts ----- This directory contains scripts that are used to develop the project. ================================================ FILE: scripts/bench.py ================================================ # /// script # dependencies = [ # "coincurve", # "fastecdsa==3.0.1; sys_platform != 'win32'", # "rich", # ] # [tool.uv.sources] # coincurve = { path = ".." } # /// import os import sys from abc import ABC, abstractmethod from decimal import Decimal from textwrap import dedent from time import perf_counter_ns from timeit import Timer from rich.live import Live from rich.table import Table MESSAGE = os.urandom(8192).hex() class BenchmarkSpec: __slots__ = ("setup", "statement") def __init__(self, setup: str, statement: str): self.setup = dedent(setup[1:]) self.statement = dedent(statement[1:]) class Benchmark(ABC): @staticmethod @abstractmethod def name() -> str: pass @staticmethod @abstractmethod def generate_key_pair() -> BenchmarkSpec: pass @staticmethod @abstractmethod def sign() -> BenchmarkSpec: pass @staticmethod @abstractmethod def verify() -> BenchmarkSpec: pass @staticmethod @abstractmethod def key_export() -> BenchmarkSpec: pass @staticmethod @abstractmethod def key_import() -> BenchmarkSpec: pass class CoincurveBenchmark(Benchmark): @staticmethod def name() -> str: return "coincurve" @staticmethod def generate_key_pair() -> BenchmarkSpec: return BenchmarkSpec( """ from coincurve import PrivateKey """, """ PrivateKey() """, ) @staticmethod def sign() -> BenchmarkSpec: return BenchmarkSpec( f""" from coincurve import PrivateKey message = {MESSAGE!r}.encode() private_key = PrivateKey() """, """ private_key.sign(message) """, ) @staticmethod def verify() -> BenchmarkSpec: return BenchmarkSpec( f""" from coincurve import PrivateKey, verify_signature message = {MESSAGE!r}.encode() private_key = PrivateKey() signature = private_key.sign(message) public_key = private_key.public_key.format(compressed=False) """, """ assert verify_signature(signature, message, public_key) """, ) @staticmethod def key_export() -> BenchmarkSpec: return BenchmarkSpec( """ from coincurve import PrivateKey private_key = PrivateKey() """, """ private_key.to_pem() """, ) @staticmethod def key_import() -> BenchmarkSpec: return BenchmarkSpec( """ from coincurve import PrivateKey private_key = PrivateKey() private_key_pem = private_key.to_pem() """, """ PrivateKey.from_pem(private_key_pem) """, ) class FastecdsaBenchmark(Benchmark): @staticmethod def name() -> str: return "fastecdsa" @staticmethod def generate_key_pair() -> BenchmarkSpec: return BenchmarkSpec( """ from fastecdsa import curve, keys """, """ keys.gen_keypair(curve.secp256k1) """, ) @staticmethod def sign() -> BenchmarkSpec: return BenchmarkSpec( f""" from fastecdsa import curve, ecdsa, keys message = {MESSAGE!r} private_key, _ = keys.gen_keypair(curve.secp256k1) """, """ r, s = ecdsa.sign(message, private_key, curve=curve.secp256k1) """, ) @staticmethod def verify() -> BenchmarkSpec: return BenchmarkSpec( f""" from fastecdsa import curve, ecdsa, keys message = {MESSAGE!r} private_key, public_key = keys.gen_keypair(curve.secp256k1) r, s = ecdsa.sign(message, private_key, curve=curve.secp256k1) """, """ assert ecdsa.verify((r, s), message, public_key, curve=curve.secp256k1) """, ) @staticmethod def key_export() -> BenchmarkSpec: return BenchmarkSpec( """ from fastecdsa import curve, keys from fastecdsa.encoding.pem import PEMEncoder private_key, _ = keys.gen_keypair(curve.secp256k1) encoder = PEMEncoder() """, """ encoder.encode_private_key(private_key, curve=curve.secp256k1) """, ) @staticmethod def key_import() -> BenchmarkSpec: return BenchmarkSpec( """ from fastecdsa import curve, keys from fastecdsa.encoding.pem import PEMEncoder private_key, _ = keys.gen_keypair(curve.secp256k1) encoder = PEMEncoder() private_key_pem = encoder.encode_private_key(private_key, curve=curve.secp256k1) """, """ encoder.decode_private_key(private_key_pem) """, ) def generate_table(rows: list[list[str]]): table = Table() table.add_column("Library") table.add_column("Key generation") table.add_column("Signing") table.add_column("Verification") table.add_column("Key export") table.add_column("Key import") for row in rows: table.add_row(*row) return table def main(): print(sys.version) rows = [] table = generate_table(rows) with Live(table, auto_refresh=False) as live: for benchmark in [CoincurveBenchmark, FastecdsaBenchmark]: row = [benchmark.name()] rows.append(row) live.update(generate_table(rows), refresh=True) for method in [ benchmark.generate_key_pair, benchmark.sign, benchmark.verify, benchmark.key_export, benchmark.key_import, ]: spec = method() timer = Timer(stmt=spec.statement, setup=spec.setup, timer=perf_counter_ns) try: loops, _ = timer.autorange() times = timer.repeat(number=loops, repeat=1000) except Exception as e: # noqa: BLE001 row.append(str(e)) live.update(generate_table(rows), refresh=True) continue best = Decimal(min(times)) # Convert nanoseconds to microseconds and round to 1 decimal place best /= 1_000 best = best.quantize(Decimal("0.1")) row.append(str(best)) live.update(generate_table(rows), refresh=True) if __name__ == "__main__": main() ================================================ FILE: src/coincurve/__init__.py ================================================ from coincurve.context import GLOBAL_CONTEXT, Context from coincurve.keys import PrivateKey, PublicKey, PublicKeyXOnly from coincurve.utils import verify_signature __version__ = "21.0.0" __all__ = [ "GLOBAL_CONTEXT", "Context", "PrivateKey", "PublicKey", "PublicKeyXOnly", "verify_signature", ] ================================================ FILE: src/coincurve/context.py ================================================ from __future__ import annotations from os import urandom from threading import Lock from coincurve._libsecp256k1 import ffi, lib from coincurve.flags import CONTEXT_FLAGS, CONTEXT_NONE class Context: def __init__(self, seed: bytes | None = None, flag=CONTEXT_NONE, name: str = ""): if flag not in CONTEXT_FLAGS: msg = f"{flag} is an invalid context flag." raise ValueError(msg) self._lock = Lock() self.ctx = ffi.gc(lib.secp256k1_context_create(flag), lib.secp256k1_context_destroy) self.reseed(seed) self.name = name def reseed(self, seed: bytes | None = None): """ Protects against certain possible future side-channel timing attacks. """ with self._lock: seed = urandom(32) if not seed or len(seed) != 32 else seed # noqa: PLR2004 res = lib.secp256k1_context_randomize(self.ctx, ffi.new("unsigned char [32]", seed)) if not res: msg = "secp256k1_context_randomize" raise ValueError(msg) def __repr__(self): return self.name or super().__repr__() GLOBAL_CONTEXT = Context(name="GLOBAL_CONTEXT") ================================================ FILE: src/coincurve/der.py ================================================ """ Minimal, dependency-free ASN.1/DER encoder & decoder for secp256k1 EC private keys. This module implements just enough DER encoding/decoding to support: 1. Outputting a DER-encoded PKCS#8 EC private key (with an embedded ECPrivateKey per RFC 5915) 2. Reading such a DER-encoded EC private key Only the following ASN.1 types are supported: - INTEGER - BIT STRING - OCTET STRING - OBJECT IDENTIFIER - SEQUENCE - Context-specific EXPLICIT tags (for the optional public key) The expected DER structure is as follows: PrivateKeyInfo ::= SEQUENCE { version INTEGER, -- must be 0 privateKeyAlgorithm SEQUENCE { algorithm OBJECT IDENTIFIER, -- id-ecPublicKey (1.2.840.10045.2.1) parameters OBJECT IDENTIFIER -- secp256k1 (1.3.132.0.10) }, privateKey OCTET STRING -- DER encoding of ECPrivateKey } ECPrivateKey ::= SEQUENCE { version INTEGER, -- must be 1 privateKey OCTET STRING, -- the secret bytes publicKey [1] EXPLICIT BIT STRING OPTIONAL -- uncompressed public key } """ from __future__ import annotations from coincurve.utils import int_to_bytes # ASN.1 DER tag bytes INTEGER_TAG = 0x02 BIT_STRING_TAG = 0x03 OCTET_STRING_TAG = 0x04 OBJECT_IDENTIFIER_TAG = 0x06 SEQUENCE_TAG = 0x30 # OIDs EC_PUBKEY_OID = bytes([0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01]) # 1.2.840.10045.2.1 (ecPublicKey) SECP256K1_OID = bytes([0x2B, 0x81, 0x04, 0x00, 0x0A]) # 1.3.132.0.10 (secp256k1) # Pre-computed structures VERSION_INTEGER_ZERO = bytes([INTEGER_TAG, 0x01, 0x00]) # INTEGER 0 VERSION_INTEGER_ONE = bytes([INTEGER_TAG, 0x01, 0x01]) # INTEGER 1 EC_ALGORITHM_IDENTIFIER = bytes([ SEQUENCE_TAG, 16, OBJECT_IDENTIFIER_TAG, len(EC_PUBKEY_OID), *EC_PUBKEY_OID, OBJECT_IDENTIFIER_TAG, len(SECP256K1_OID), *SECP256K1_OID, ]) def encode_length(length: int) -> bytes: """Encode a length in DER format.""" # Short form if length < 128: # noqa: PLR2004 return bytes([length]) # Long form length_bytes = int_to_bytes(length) return bytes([0x80 | len(length_bytes)]) + length_bytes def encode_octet_string(value: bytes) -> bytes: """Encode an OCTET STRING in DER format.""" length_bytes = encode_length(len(value)) length_bytes_len = len(length_bytes) result = bytearray(1 + length_bytes_len + len(value)) result[0] = OCTET_STRING_TAG result[1 : 1 + length_bytes_len] = length_bytes result[1 + length_bytes_len :] = value return bytes(result) def encode_bit_string(value: bytes, unused_bits: int = 0) -> bytes: """Encode a BIT STRING in DER format.""" length_bytes = encode_length(len(value) + 1) length_bytes_len = len(length_bytes) result = bytearray(1 + length_bytes_len + 1 + len(value)) result[0] = BIT_STRING_TAG result[1 : 1 + length_bytes_len] = length_bytes result[1 + length_bytes_len] = unused_bits result[1 + length_bytes_len + 1 :] = value return bytes(result) def encode_der(private_key: bytes, public_key: bytes | None = None) -> bytes: """ Encode an EC private key in DER format (PKCS#8/RFC 5208). Optimized for secp256k1 keys. Parameters: private_key: The private key as bytes (32 bytes for secp256k1) public_key: The public key as bytes (65 bytes uncompressed for secp256k1, starting with 0x04) Returns: The DER-encoded private key """ # EC private key contains version(1) + octet string + optional pubkey ec_key_buffer = bytearray(VERSION_INTEGER_ONE) # Add private key as octet string private_key_os = encode_octet_string(private_key) ec_key_buffer.extend(private_key_os) # Add public key if provided (optional) if public_key is not None: public_key_bs = encode_bit_string(public_key) pubkey_len = len(public_key_bs) ec_key_buffer.append(0xA1) # context-specific [1] constructed ec_key_buffer.extend(encode_length(pubkey_len)) ec_key_buffer.extend(public_key_bs) # Wrap EC private key in sequence ec_key_seq = bytearray([SEQUENCE_TAG]) ec_key_seq.extend(encode_length(len(ec_key_buffer))) ec_key_seq.extend(ec_key_buffer) # Wrap in octet string for outer structure ec_key_os = encode_octet_string(ec_key_seq) # Build the outer PKCS#8 structure result = bytearray([SEQUENCE_TAG]) # Calculate total length: version(3) + alg_id(18) + octet_string(len) outer_len = 3 + len(EC_ALGORITHM_IDENTIFIER) + len(ec_key_os) result.extend(encode_length(outer_len)) # Version 0 result.extend(VERSION_INTEGER_ZERO) # Algorithm identifier (pre-computed) result.extend(EC_ALGORITHM_IDENTIFIER) # EC key wrapped in octet string result.extend(ec_key_os) return bytes(result) def decode_length(data: bytes, offset: int) -> tuple[int, int]: """ Decode a DER length field. Parameters: data: The DER-encoded data offset: The current offset in the data Returns: Tuple of (length, new_offset) """ length_byte = data[offset] offset += 1 # Short form if length_byte < 128: # noqa: PLR2004 return length_byte, offset # Long form num_length_bytes = length_byte & 0x7F length = 0 for _ in range(num_length_bytes): length = (length << 8) | data[offset] offset += 1 return length, offset def decode_der(der_data: bytes) -> bytes: """ Decode a DER-encoded EC private key to extract the private key secret. Optimized for secp256k1 keys. Parameters: der_data: The DER-encoded private key in PKCS#8 format Returns: The private key secret as bytes """ # Quick validation for performance if len(der_data) < 34 or der_data[0] != SEQUENCE_TAG: # noqa: PLR2004 msg = "Invalid DER: not a valid PKCS#8 structure" raise ValueError(msg) # Skip outer sequence tag and length offset = 1 _, offset = decode_length(der_data, offset) # Skip version INTEGER (should be 0) if der_data[offset] != INTEGER_TAG: msg = "Invalid DER: expected INTEGER tag for version" raise ValueError(msg) offset += 1 version_len, offset = decode_length(der_data, offset) offset += version_len # Skip version value # Validate algorithm identifier is for EC if der_data[offset] != SEQUENCE_TAG: msg = "Invalid DER: expected SEQUENCE tag for algorithm" raise ValueError(msg) offset += 1 alg_len, offset = decode_length(der_data, offset) alg_end = offset + alg_len # Store the end position of algorithm identifier # Check if first OID is EC if der_data[offset] != OBJECT_IDENTIFIER_TAG: msg = "Invalid DER: expected OBJECT IDENTIFIER tag" raise ValueError(msg) offset += 1 oid_len, offset = decode_length(der_data, offset) algorithm_oid = der_data[offset : offset + oid_len] # Check if it's an EC key if oid_len != len(EC_PUBKEY_OID) or algorithm_oid != EC_PUBKEY_OID: msg = "Not an EC private key" raise ValueError(msg) # Skip to the end of algorithm identifier section offset = alg_end # Extract private key octet string if der_data[offset] != OCTET_STRING_TAG: msg = "Invalid DER: expected OCTET STRING for private key" raise ValueError(msg) offset += 1 priv_len, offset = decode_length(der_data, offset) # Parse EC private key structure ec_data = der_data[offset : offset + priv_len] # Verify EC structure starts with sequence if len(ec_data) < 2 or ec_data[0] != SEQUENCE_TAG: # noqa: PLR2004 msg = "Invalid EC key format: missing sequence" raise ValueError(msg) # Skip sequence tag and length ec_offset = 1 _, ec_offset = decode_length(ec_data, ec_offset) # Skip version INTEGER (should be 1) if ec_data[ec_offset] != INTEGER_TAG: msg = "Invalid EC key format: missing version" raise ValueError(msg) ec_offset += 1 ec_ver_len, ec_offset = decode_length(ec_data, ec_offset) ec_offset += ec_ver_len # Skip version value # Get private key octet string if ec_data[ec_offset] != OCTET_STRING_TAG: msg = "Invalid DER: expected OCTET STRING for EC private key" raise ValueError(msg) ec_offset += 1 key_len, ec_offset = decode_length(ec_data, ec_offset) # Extract private key return ec_data[ec_offset : ec_offset + key_len] ================================================ FILE: src/coincurve/ecdsa.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from coincurve._libsecp256k1 import ffi, lib from coincurve.context import GLOBAL_CONTEXT, Context from coincurve.utils import bytes_to_int, int_to_bytes, sha256 if TYPE_CHECKING: from coincurve.types import Hasher MAX_SIG_LENGTH = 72 CDATA_SIG_LENGTH = 64 def cdata_to_der(cdata, context: Context = GLOBAL_CONTEXT) -> bytes: der = ffi.new("unsigned char[72]") der_length = ffi.new("size_t *", MAX_SIG_LENGTH) lib.secp256k1_ecdsa_signature_serialize_der(context.ctx, der, der_length, cdata) return bytes(ffi.buffer(der, der_length[0])) def der_to_cdata(der: bytes, context: Context = GLOBAL_CONTEXT): cdata = ffi.new("secp256k1_ecdsa_signature *") parsed = lib.secp256k1_ecdsa_signature_parse_der(context.ctx, cdata, der, len(der)) if not parsed: msg = "The DER-encoded signature could not be parsed." raise ValueError(msg) return cdata def recover(message: bytes, recover_sig, hasher: Hasher = sha256, context: Context = GLOBAL_CONTEXT): msg_hash = hasher(message) if hasher is not None else message if len(msg_hash) != 32: # noqa: PLR2004 msg = "Message hash must be 32 bytes long." raise ValueError(msg) pubkey = ffi.new("secp256k1_pubkey *") recovered = lib.secp256k1_ecdsa_recover(context.ctx, pubkey, recover_sig, msg_hash) if recovered: return pubkey msg = "failed to recover ECDSA public key" raise ValueError(msg) def serialize_recoverable(recover_sig, context: Context = GLOBAL_CONTEXT) -> bytes: output = ffi.new("unsigned char[64]") recid = ffi.new("int *") lib.secp256k1_ecdsa_recoverable_signature_serialize_compact(context.ctx, output, recid, recover_sig) return bytes(ffi.buffer(output, CDATA_SIG_LENGTH)) + int_to_bytes(recid[0]) def deserialize_recoverable(serialized: bytes, context: Context = GLOBAL_CONTEXT): if len(serialized) != 65: # noqa: PLR2004 msg = "Serialized signature must be 65 bytes long." raise ValueError(msg) ser_sig, rec_id = serialized[:64], bytes_to_int(serialized[64:]) if not 0 <= rec_id <= 3: # noqa: PLR2004 msg = "Invalid recovery id." raise ValueError(msg) recover_sig = ffi.new("secp256k1_ecdsa_recoverable_signature *") parsed = lib.secp256k1_ecdsa_recoverable_signature_parse_compact(context.ctx, recover_sig, ser_sig, rec_id) if not parsed: msg = "Failed to parse recoverable signature." raise ValueError(msg) return recover_sig """ Warning: The functions below may change and are not tested! """ def serialize_compact(raw_sig, context: Context = GLOBAL_CONTEXT): # no cov output = ffi.new("unsigned char[64]") res = lib.secp256k1_ecdsa_signature_serialize_compact(context.ctx, output, raw_sig) if not res: msg = "secp256k1_ecdsa_signature_serialize_compact" raise ValueError(msg) return bytes(ffi.buffer(output, CDATA_SIG_LENGTH)) def deserialize_compact(ser_sig: bytes, context: Context = GLOBAL_CONTEXT): # no cov if len(ser_sig) != 64: # noqa: PLR2004 msg = "invalid signature length" raise ValueError(msg) raw_sig = ffi.new("secp256k1_ecdsa_signature *") res = lib.secp256k1_ecdsa_signature_parse_compact(context.ctx, raw_sig, ser_sig) if not res: msg = "secp256k1_ecdsa_signature_parse_compact" raise ValueError(msg) return raw_sig def signature_normalize(raw_sig, context: Context = GLOBAL_CONTEXT): # no cov """ Check and optionally convert a signature to a normalized lower-S form. This function always return a tuple containing a boolean (True if not previously normalized or False if signature was already normalized), and the normalized signature. """ sigout = ffi.new("secp256k1_ecdsa_signature *") res = lib.secp256k1_ecdsa_signature_normalize(context.ctx, sigout, raw_sig) return not not res, sigout # noqa: SIM208 def recoverable_convert(recover_sig, context: Context = GLOBAL_CONTEXT): # no cov normal_sig = ffi.new("secp256k1_ecdsa_signature *") lib.secp256k1_ecdsa_recoverable_signature_convert(context.ctx, normal_sig, recover_sig) return normal_sig ================================================ FILE: src/coincurve/flags.py ================================================ from __future__ import annotations from coincurve._libsecp256k1 import lib CONTEXT_NONE = lib.SECP256K1_CONTEXT_NONE CONTEXT_FLAGS = { CONTEXT_NONE, } EC_COMPRESSED = lib.SECP256K1_EC_COMPRESSED EC_UNCOMPRESSED = lib.SECP256K1_EC_UNCOMPRESSED # Additional flags available from libsecp256k1 # lib.SECP256K1_TAG_PUBKEY_EVEN # lib.SECP256K1_TAG_PUBKEY_ODD # lib.SECP256K1_TAG_PUBKEY_UNCOMPRESSED # lib.SECP256K1_TAG_PUBKEY_HYBRID_EVEN # lib.SECP256K1_TAG_PUBKEY_HYBRID_ODD ================================================ FILE: src/coincurve/keys.py ================================================ from __future__ import annotations import os from typing import TYPE_CHECKING from coincurve._libsecp256k1 import ffi, lib from coincurve.context import GLOBAL_CONTEXT, Context from coincurve.der import decode_der, encode_der from coincurve.ecdsa import cdata_to_der, der_to_cdata, deserialize_recoverable, recover, serialize_recoverable from coincurve.flags import EC_COMPRESSED, EC_UNCOMPRESSED from coincurve.utils import ( DEFAULT_NONCE, bytes_to_int, der_to_pem, get_valid_secret, hex_to_bytes, int_to_bytes_padded, pad_scalar, pem_to_der, sha256, validate_secret, ) if TYPE_CHECKING: from coincurve.types import Hasher, Nonce class PrivateKey: def __init__(self, secret: bytes | None = None, context: Context = GLOBAL_CONTEXT): """ Initializes a private key. Parameters: secret: The secret used to initialize the private key. If not provided, a new key will be generated. context: The context to use. """ 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: """ Creates an ECDSA signature. Parameters: message: The message to sign. hasher (collections.abc.Callable[[bytes], bytes] | None): The hash function to use, which must return 32 bytes. By default, the `sha256` algorithm is used. If `None`, no hashing occurs. custom_nonce (tuple[ffi.CData, ffi.CData]): Custom nonce data in the form `(nonce_function, input_data)`. For more information, refer to the `libsecp256k1` documentation [here](https://github.com/bitcoin-core/secp256k1/blob/v0.6.0/include/secp256k1.h#L637-L642). Returns: The ECDSA signature. Raises: ValueError: If the message hash was not 32 bytes long, the nonce generation function failed, or the private key was invalid. """ msg_hash = hasher(message) if hasher is not None else message if len(msg_hash) != 32: # noqa: PLR2004 msg = "Message hash must be 32 bytes long." raise ValueError(msg) signature = ffi.new("secp256k1_ecdsa_signature *") nonce_fn, nonce_data = custom_nonce signed = lib.secp256k1_ecdsa_sign(self.context.ctx, signature, msg_hash, self.secret, nonce_fn, nonce_data) if not signed: msg = "The nonce generation function failed, or the private key was invalid." raise ValueError(msg) return cdata_to_der(signature, self.context) def sign_schnorr(self, message: bytes, aux_randomness: bytes = b"") -> bytes: """ Creates a Schnorr signature. Parameters: message: The message to sign. aux_randomness: 32 bytes of fresh randomness, empty bytestring (auto-generated), or None (no randomness). Returns: 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: # noqa: PLR2004 msg = "Message must be 32 bytes long." raise ValueError(msg) if aux_randomness == b"": aux_randomness = os.urandom(32) elif aux_randomness is None: aux_randomness = ffi.NULL elif len(aux_randomness) != 32: # noqa: PLR2004 msg = "Auxiliary random data must be 32 bytes long." raise ValueError(msg) keypair = ffi.new("secp256k1_keypair *") res = lib.secp256k1_keypair_create(self.context.ctx, keypair, self.secret) if not res: msg = "Secret was invalid" raise ValueError(msg) signature = ffi.new("unsigned char[64]") res = lib.secp256k1_schnorrsig_sign32(self.context.ctx, signature, message, keypair, aux_randomness) if not res: msg = "Signing failed" raise ValueError(msg) res = lib.secp256k1_schnorrsig_verify( self.context.ctx, signature, message, len(message), self.public_key_xonly.public_key ) if not res: msg = "Invalid signature" raise ValueError(msg) return bytes(ffi.buffer(signature)) def sign_recoverable(self, message: bytes, hasher: Hasher = sha256, custom_nonce: Nonce = DEFAULT_NONCE) -> bytes: """ Creates a recoverable ECDSA signature. Parameters: message: The message to sign. hasher (collections.abc.Callable[[bytes], bytes] | None): The hash function to use, which must return 32 bytes. By default, the `sha256` algorithm is used. If `None`, no hashing occurs. custom_nonce (tuple[ffi.CData, ffi.CData]): Custom nonce data in the form `(nonce_function, input_data)`. For more information, refer to the `libsecp256k1` documentation [here](https://github.com/bitcoin-core/secp256k1/blob/v0.6.0/include/secp256k1.h#L637-L642). Returns: The recoverable ECDSA signature. Raises: ValueError: If the message hash was not 32 bytes long, the nonce generation function failed, or the private key was invalid. """ msg_hash = hasher(message) if hasher is not None else message if len(msg_hash) != 32: # noqa: PLR2004 msg = "Message hash must be 32 bytes long." raise ValueError(msg) signature = ffi.new("secp256k1_ecdsa_recoverable_signature *") nonce_fn, nonce_data = custom_nonce signed = lib.secp256k1_ecdsa_sign_recoverable( self.context.ctx, signature, msg_hash, self.secret, nonce_fn, nonce_data ) if not signed: msg = "The nonce generation function failed, or the private key was invalid." raise ValueError(msg) return serialize_recoverable(signature, self.context) def ecdh(self, public_key: bytes) -> bytes: """ Computes an EC Diffie-Hellman secret in constant time. !!! note This prevents malleability by returning `sha256(compressed_public_key)` instead of the `x` coordinate directly. Parameters: public_key: The formatted public key. Returns: The 32-byte shared secret. Raises: ValueError: If the public key could not be parsed or was invalid. """ secret = ffi.new("unsigned char [32]") lib.secp256k1_ecdh(self.context.ctx, secret, PublicKey(public_key).public_key, self.secret, ffi.NULL, ffi.NULL) return bytes(ffi.buffer(secret, 32)) def add(self, scalar: bytes, update: bool = False) -> PrivateKey: # noqa: FBT001, FBT002 """ Adds a scalar to the private key. Parameters: scalar: The scalar with which to add. update: Whether to update the private key in-place. Returns: The new private key, or the modified private key if `update` is `True`. Raises: ValueError: If the tweak was out of range or the resulting private key was invalid. """ scalar = pad_scalar(scalar) secret = ffi.new("unsigned char [32]", self.secret) success = lib.secp256k1_ec_seckey_tweak_add(self.context.ctx, secret, scalar) if not success: msg = "The tweak was out of range, or the resulting private key is invalid." raise ValueError(msg) secret = bytes(ffi.buffer(secret, 32)) if update: self.secret = secret self._update_public_key() return self return PrivateKey(secret, self.context) def multiply(self, scalar: bytes, update: bool = False) -> PrivateKey: # noqa: FBT001, FBT002 """ Multiplies the private key by a scalar. Parameters: scalar: The scalar with which to multiply. update: Whether to update the private key in-place. Returns: The new private key, or the modified private key if `update` is `True`. """ scalar = validate_secret(scalar) secret = ffi.new("unsigned char [32]", self.secret) lib.secp256k1_ec_seckey_tweak_mul(self.context.ctx, secret, scalar) secret = bytes(ffi.buffer(secret, 32)) if update: self.secret = secret self._update_public_key() return self return PrivateKey(secret, self.context) def to_hex(self) -> str: """ Returns the private key encoded as a hex string. """ return self.secret.hex() def to_int(self) -> int: """ Returns the private key as an integer. """ return bytes_to_int(self.secret) def to_pem(self) -> bytes: """ Returns the private key encoded in PEM format. """ return der_to_pem(self.to_der()) def to_der(self) -> bytes: """ Returns the private key encoded in DER format. """ return encode_der(self.secret, self.public_key.format(compressed=False)) @classmethod def from_hex(cls, hexed: str, context: Context = GLOBAL_CONTEXT) -> PrivateKey: """ Creates a private key from a hex string. Parameters: hexed: The private key encoded as a hex string. context: The context to use. Returns: The private key. """ return PrivateKey(hex_to_bytes(hexed), context) @classmethod def from_int(cls, num: int, context: Context = GLOBAL_CONTEXT) -> PrivateKey: """ Creates a private key from an integer. Parameters: num: The private key as an integer. context: The context to use. Returns: The private key. """ return PrivateKey(int_to_bytes_padded(num), context) @classmethod def from_pem(cls, pem: bytes, context: Context = GLOBAL_CONTEXT) -> PrivateKey: """ Creates a private key from PEM format. Parameters: pem: The private key encoded in PEM format. context: The context to use. Returns: The private key. """ return PrivateKey(decode_der(pem_to_der(pem)), context) @classmethod def from_der(cls, der: bytes, context: Context = GLOBAL_CONTEXT) -> PrivateKey: """ Creates a private key from DER format. Parameters: der: The private key encoded in DER format. context: The context to use. Returns: The private key. """ return PrivateKey(decode_der(der), context) def _update_public_key(self): created = lib.secp256k1_ec_pubkey_create(self.context.ctx, self.public_key.public_key, self.secret) if not created: msg = "Invalid secret." raise ValueError(msg) def __eq__(self, other) -> bool: return self.secret == other.secret def __hash__(self) -> int: return hash(self.secret) class PublicKey: def __init__(self, data: bytes | ffi.CData, context: Context = GLOBAL_CONTEXT): """ Initializes a public key. Parameters: data (bytes): The formatted public key. This class supports parsing compressed (33 bytes, header byte `0x02` or `0x03`), uncompressed (65 bytes, header byte `0x04`), or hybrid (65 bytes, header byte `0x06` or `0x07`) format public keys. context: The context to use. Raises: ValueError: If the public key could not be parsed or was invalid. """ if not isinstance(data, bytes): self.public_key = data else: public_key = ffi.new("secp256k1_pubkey *") parsed = lib.secp256k1_ec_pubkey_parse(context.ctx, public_key, data, len(data)) if not parsed: msg = "The public key could not be parsed or is invalid." raise ValueError(msg) self.public_key = public_key self.context = context @classmethod def from_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT) -> PublicKey: """ Derives a public key from a private key secret. Parameters: secret: The private key secret. context: The context to use. Returns: The public key. Raises: ValueError: If an invalid secret was used. """ public_key = ffi.new("secp256k1_pubkey *") created = lib.secp256k1_ec_pubkey_create(context.ctx, public_key, validate_secret(secret)) if not created: # no cov msg = ( "Somehow an invalid secret was used. Please " "submit this as an issue here: " "https://github.com/ofek/coincurve/issues/new" ) raise ValueError(msg) return PublicKey(public_key, context) @classmethod def from_valid_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT) -> PublicKey: """ Derives a public key from a valid private key secret, avoiding input checks. Parameters: secret: The private key secret. context: The context to use. Returns: The public key. Raises: ValueError: If the secret was invalid. """ public_key = ffi.new("secp256k1_pubkey *") created = lib.secp256k1_ec_pubkey_create(context.ctx, public_key, secret) if not created: msg = "Invalid secret." raise ValueError(msg) return PublicKey(public_key, context) @classmethod def from_point(cls, x: int, y: int, context: Context = GLOBAL_CONTEXT) -> PublicKey: """ Derives a public key from a coordinate point. Parameters: x: The x coordinate. y: The y coordinate. context: The context to use. Returns: The public key. """ return PublicKey(b"\x04" + int_to_bytes_padded(x) + int_to_bytes_padded(y), context) @classmethod def from_signature_and_message( cls, signature: bytes, message: bytes, hasher: Hasher = sha256, context: Context = GLOBAL_CONTEXT ) -> PublicKey: """ Recovers an ECDSA public key from a recoverable signature. Parameters: signature: The recoverable ECDSA signature. message: The message that was supposedly signed. hasher (collections.abc.Callable[[bytes], bytes] | None): The hash function to use, which must return 32 bytes. By default, the `sha256` algorithm is used. If `None`, no hashing occurs. context: The context to use. Returns: The public key that signed the message. Raises: ValueError: If the message hash was not 32 bytes long or recovery of the ECDSA public key failed. """ return PublicKey( recover(message, deserialize_recoverable(signature, context=context), hasher=hasher, context=context) ) @classmethod def combine_keys(cls, public_keys: list[PublicKey], context: Context = GLOBAL_CONTEXT) -> PublicKey: """ Adds a number of public keys together. Parameters: public_keys: A sequence of public keys. context: The context to use. Returns: The combined public key. Raises: ValueError: If the sum of the public keys was invalid. """ public_key = ffi.new("secp256k1_pubkey *") combined = lib.secp256k1_ec_pubkey_combine( context.ctx, public_key, [pk.public_key for pk in public_keys], len(public_keys) ) if not combined: msg = "The sum of the public keys is invalid." raise ValueError(msg) return PublicKey(public_key, context) def format(self, compressed: bool = True) -> bytes: # noqa: FBT001, FBT002 """ Formats the public key. Parameters: compressed: Whether to use the compressed format. Returns: The 33 byte formatted public key, or the 65 byte formatted public key if `compressed` is `False`. """ length = 33 if compressed else 65 serialized = ffi.new("unsigned char [%d]" % length) # noqa: UP031 output_len = ffi.new("size_t *", length) lib.secp256k1_ec_pubkey_serialize( self.context.ctx, serialized, output_len, self.public_key, EC_COMPRESSED if compressed else EC_UNCOMPRESSED ) return bytes(ffi.buffer(serialized, length)) def point(self) -> tuple[int, int]: """ Returns the public key as a coordinate point. """ public_key = self.format(compressed=False) return bytes_to_int(public_key[1:33]), bytes_to_int(public_key[33:]) def verify(self, signature: bytes, message: bytes, hasher: Hasher = sha256) -> bool: """ Verifies an ECDSA signature. Parameters: signature: The ECDSA signature. message: The message that was supposedly signed. hasher (collections.abc.Callable[[bytes], bytes] | None): The hash function to use, which must return 32 bytes. By default, the `sha256` algorithm is used. If `None`, no hashing occurs. Returns: A boolean indicating whether the signature is correct. Raises: ValueError: If the message hash was not 32 bytes long or the DER-encoded signature could not be parsed. """ msg_hash = hasher(message) if hasher is not None else message if len(msg_hash) != 32: # noqa: PLR2004 msg = "Message hash must be 32 bytes long." raise ValueError(msg) verified = lib.secp256k1_ecdsa_verify(self.context.ctx, der_to_cdata(signature), msg_hash, self.public_key) # A performance hack to avoid global bool() lookup. return not not verified # noqa: SIM208 def add(self, scalar: bytes, update: bool = False) -> PublicKey: # noqa: FBT001, FBT002 """ Adds a scalar to the public key. Parameters: scalar: The scalar with which to add. update: Whether to update the public key in-place. Returns: The new public key, or the modified public key if `update` is `True`. Raises: ValueError: If the tweak was out of range or the resulting public key was invalid. """ scalar = pad_scalar(scalar) new_key = ffi.new("secp256k1_pubkey *", self.public_key[0]) success = lib.secp256k1_ec_pubkey_tweak_add(self.context.ctx, new_key, scalar) if not success: msg = "The tweak was out of range, or the resulting public key is invalid." raise ValueError(msg) if update: self.public_key = new_key return self return PublicKey(new_key, self.context) def multiply(self, scalar: bytes, update: bool = False) -> PublicKey: # noqa: FBT001, FBT002 """ Multiplies the public key by a scalar. Parameters: scalar: The scalar with which to multiply. update: Whether to update the public key in-place. Returns: The new public key, or the modified public key if `update` is `True`. """ scalar = validate_secret(scalar) new_key = ffi.new("secp256k1_pubkey *", self.public_key[0]) lib.secp256k1_ec_pubkey_tweak_mul(self.context.ctx, new_key, scalar) if update: self.public_key = new_key return self return PublicKey(new_key, self.context) def combine(self, public_keys: list[PublicKey], update: bool = False) -> PublicKey: # noqa: FBT001, FBT002 """ Adds a number of public keys together. Parameters: public_keys: A sequence of public keys. update: Whether to update the public key in-place. Returns: The combined public key, or the modified public key if `update` is `True`. Raises: ValueError: If the sum of the public keys was invalid. """ new_key = ffi.new("secp256k1_pubkey *") combined = lib.secp256k1_ec_pubkey_combine( self.context.ctx, new_key, [pk.public_key for pk in [self, *public_keys]], len(public_keys) + 1 ) if not combined: msg = "The sum of the public keys is invalid." raise ValueError(msg) if update: self.public_key = new_key return self return PublicKey(new_key, self.context) def __eq__(self, other) -> bool: return self.format(compressed=False) == other.format(compressed=False) def __hash__(self) -> int: return hash(self.format(compressed=False)) class PublicKeyXOnly: def __init__(self, data: bytes | ffi.CData, parity: bool = False, context: Context = GLOBAL_CONTEXT): # noqa: FBT001, FBT002 """ Initializes a BIP340 `x-only` public key. Parameters: data (bytes): The formatted public key. parity: Whether the encoded point is the negation of the public key. context: The context to use. Raises: ValueError: If the public key could not be parsed or is invalid. """ 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: msg = "The public key could not be parsed or is invalid." raise ValueError(msg) self.public_key = public_key self.parity = parity self.context = context @classmethod def from_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT) -> PublicKeyXOnly: """ Derives an x-only public key from a private key secret. Parameters: secret: The private key secret. context: The context to use. Returns: The x-only public key. Raises: ValueError: If the secret was invalid. """ keypair = ffi.new("secp256k1_keypair *") res = lib.secp256k1_keypair_create(context.ctx, keypair, validate_secret(secret)) if not res: msg = "Secret was invalid" raise ValueError(msg) 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) # noqa: SIM208 @classmethod def from_valid_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT) -> PublicKeyXOnly: """ Derives an x-only public key from a valid private key secret, avoiding input checks. Parameters: secret: The private key secret. context: The context to use. Returns: The x-only public key. Raises: ValueError: If the secret was invalid. """ keypair = ffi.new("secp256k1_keypair *") res = lib.secp256k1_keypair_create(context.ctx, keypair, secret) if not res: msg = "Secret was invalid" raise ValueError(msg) 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) # noqa: SIM208 def format(self) -> bytes: """ Serializes the public key. Returns: The public key serialized as 32 bytes. Raises: ValueError: If the public key in `self.public_key` is invalid. """ output32 = ffi.new("unsigned char [32]") res = lib.secp256k1_xonly_pubkey_serialize(self.context.ctx, output32, self.public_key) if not res: msg = "Public key in self.public_key must be valid" raise ValueError(msg) return bytes(ffi.buffer(output32, 32)) def verify(self, signature: bytes, message: bytes) -> bool: """ Verifies a Schnorr signature over a given message. Parameters: signature: The 64-byte Schnorr signature to verify. message: The message to be verified. Returns: A boolean indicating whether the signature is correct. Raises: ValueError: If the signature is not 64 bytes long. """ if len(signature) != 64: # noqa: PLR2004 msg = "Signature must be 64 bytes long." raise ValueError(msg) return not not lib.secp256k1_schnorrsig_verify( # noqa: SIM208 self.context.ctx, signature, message, len(message), self.public_key ) def tweak_add(self, scalar: bytes) -> None: """ Adds a scalar to the public key. Parameters: scalar: The scalar with which to add. Returns: The modified public key. Raises: ValueError: If the tweak was out of range or the resulting public key would be 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: msg = "The tweak was out of range, or the resulting public key would be invalid" raise ValueError(msg) 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] # noqa: SIM208 def __eq__(self, other) -> bool: res = lib.secp256k1_xonly_pubkey_cmp(self.context.ctx, self.public_key, other.public_key) return res == 0 def __hash__(self) -> int: return hash(self.format()) ================================================ FILE: src/coincurve/py.typed ================================================ ================================================ FILE: src/coincurve/types.py ================================================ from __future__ import annotations from collections.abc import Callable from coincurve._libsecp256k1 import ffi Hasher = Callable[[bytes], bytes] | None Nonce = tuple[ffi.CData, ffi.CData] ================================================ FILE: src/coincurve/utils.py ================================================ from __future__ import annotations from base64 import b64decode, b64encode from hashlib import sha256 as _sha256 from os import environ, urandom from typing import TYPE_CHECKING from coincurve._libsecp256k1 import ffi, lib from coincurve.context import GLOBAL_CONTEXT, Context if TYPE_CHECKING: from collections.abc import Generator from coincurve.types import Hasher GROUP_ORDER = ( b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xba\xae\xdc\xe6\xafH\xa0;\xbf\xd2^\x8c\xd06AA" ) GROUP_ORDER_INT = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 KEY_SIZE = 32 MSG_HASH_SIZE = 32 ZERO = b"\x00" PEM_HEADER = b"-----BEGIN PRIVATE KEY-----\n" PEM_FOOTER = b"-----END PRIVATE KEY-----\n" if environ.get("COINCURVE_BUILDING_DOCS") != "true": DEFAULT_NONCE = (ffi.NULL, ffi.NULL) def sha256(bytestr: bytes) -> bytes: return _sha256(bytestr).digest() else: # no cov class __Nonce(tuple): # noqa: SLOT001 def __repr__(self) -> str: return "(ffi.NULL, ffi.NULL)" class __HasherSHA256: def __call__(self, bytestr: bytes) -> bytes: return _sha256(bytestr).digest() def __repr__(self) -> str: return "sha256" DEFAULT_NONCE = __Nonce((ffi.NULL, ffi.NULL)) sha256 = __HasherSHA256() def pad_hex(hexed: str) -> str: # Pad odd-length hex strings. return hexed if not len(hexed) & 1 else f"0{hexed}" def bytes_to_int(bytestr: bytes) -> int: return int.from_bytes(bytestr, "big") def int_to_bytes(num: int) -> bytes: return num.to_bytes((num.bit_length() + 7) // 8 or 1, "big") def int_to_bytes_padded(num: int) -> bytes: return pad_scalar(num.to_bytes((num.bit_length() + 7) // 8 or 1, "big")) def hex_to_bytes(hexed: str) -> bytes: return pad_scalar(bytes.fromhex(pad_hex(hexed))) def chunk_data(data: bytes, size: int) -> Generator[bytes, None, None]: return (data[i : i + size] for i in range(0, len(data), size)) def der_to_pem(der: bytes) -> bytes: return b"".join([PEM_HEADER, b"\n".join(chunk_data(b64encode(der), 64)), b"\n", PEM_FOOTER]) def pem_to_der(pem: bytes) -> bytes: return b64decode(b"".join(pem.strip().splitlines()[1:-1])) def get_valid_secret() -> bytes: while True: secret = urandom(KEY_SIZE) if ZERO < secret < GROUP_ORDER: return secret def pad_scalar(scalar: bytes) -> bytes: return (ZERO * (KEY_SIZE - len(scalar))) + scalar def validate_secret(secret: bytes) -> bytes: if not 0 < bytes_to_int(secret) < GROUP_ORDER_INT: msg = f"Secret scalar must be greater than 0 and less than {GROUP_ORDER_INT}." raise ValueError(msg) return pad_scalar(secret) def verify_signature( signature: bytes, message: bytes, public_key: bytes, hasher: Hasher = sha256, context: Context = GLOBAL_CONTEXT ) -> bool: """ Verify an ECDSA signature. Parameters: signature: The ECDSA signature. message: The message that was supposedly signed. public_key: The formatted public key. hasher (collections.abc.Callable[[bytes], bytes] | None): The hash function to use, which must return 32 bytes. By default, the `sha256` algorithm is used. If `None`, no hashing occurs. context: The secp256k1 context. Returns: A boolean indicating whether or not the signature is correct. Raises: ValueError: If the public key could not be parsed or was invalid, the message hash was not 32 bytes long, or the DER-encoded signature could not be parsed. """ pubkey = ffi.new("secp256k1_pubkey *") pubkey_parsed = lib.secp256k1_ec_pubkey_parse(context.ctx, pubkey, public_key, len(public_key)) if not pubkey_parsed: msg = "The public key could not be parsed or is invalid." raise ValueError(msg) msg_hash = hasher(message) if hasher is not None else message if len(msg_hash) != MSG_HASH_SIZE: msg = "Message hash must be 32 bytes long." raise ValueError(msg) sig = ffi.new("secp256k1_ecdsa_signature *") sig_parsed = lib.secp256k1_ecdsa_signature_parse_der(context.ctx, sig, signature, len(signature)) if not sig_parsed: msg = "The DER-encoded signature could not be parsed." raise ValueError(msg) verified = lib.secp256k1_ecdsa_verify(context.ctx, sig, msg_hash, pubkey) # A performance hack to avoid global bool() lookup. return not not verified # noqa: SIM208 ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/conftest.py ================================================ import pytest PRIVATE_KEY_BYTES = b"\xc2\x8a\x9f\x80s\x8fw\rRx\x03\xa5f\xcfo\xc3\xed\xf6\xce\xa5\x86\xc4\xfcJR#\xa5\xady~\x1a\xc3" PRIVATE_KEY_DER = ( b"0\x81\x84\x02\x01\x000\x10\x06\x07*\x86H\xce=\x02\x01\x06" b"\x05+\x81\x04\x00\n\x04m0k\x02\x01\x01\x04 \xc2\x8a\x9f" b"\x80s\x8fw\rRx\x03\xa5f\xcfo\xc3\xed\xf6\xce\xa5\x86\xc4" b"\xfcJR#\xa5\xady~\x1a\xc3\xa1D\x03B\x00\x04=\\(u\xc9\xbd" b"\x11hu\xa7\x1a]\xb6L\xff\xcb\x139k\x16=\x03\x9b\x1d\x93'" b"\x82H\x91\x80C4v\xa45**\xdd\x00\xeb\xb0\xd5\xc9LQ[r\xeb" b"\x10\xf1\xfd\x8f?\x03\xb4/J+%[\xfc\x9a\xa9\xe3" ) PRIVATE_KEY_HEX = "c28a9f80738f770d527803a566cf6fc3edf6cea586c4fc4a5223a5ad797e1ac3" PRIVATE_KEY_NUM = 87993618360805341115891506172036624893404292644470266399436498750715784469187 PRIVATE_KEY_PEM = ( b"-----BEGIN PRIVATE KEY-----\n" b"MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgwoqfgHOPdw1SeAOlZs9v\n" b"w+32zqWGxPxKUiOlrXl+GsOhRANCAAQ9XCh1yb0RaHWnGl22TP/LEzlrFj0Dmx2T\n" b"J4JIkYBDNHakNSoq3QDrsNXJTFFbcusQ8f2PPwO0L0orJVv8mqnj\n" b"-----END PRIVATE KEY-----\n" ) PUBLIC_KEY_COMPRESSED = b"\x03=\\(u\xc9\xbd\x11hu\xa7\x1a]\xb6L\xff\xcb\x139k\x16=\x03\x9b\x1d\x93'\x82H\x91\x80C4" PUBLIC_KEY_UNCOMPRESSED = ( b"\x04=\\(u\xc9\xbd\x11hu\xa7\x1a]\xb6L\xff\xcb\x139k\x16=\x03" b"\x9b\x1d\x93'\x82H\x91\x80C4v\xa45**\xdd\x00\xeb\xb0\xd5\xc9" b"LQ[r\xeb\x10\xf1\xfd\x8f?\x03\xb4/J+%[\xfc\x9a\xa9\xe3" ) PUBLIC_KEY_X = 27753912938952041417634381842191885283234814940840273460372041880794577257268 PUBLIC_KEY_Y = 53663045980837260634637807506183816949039230809110041985901491152185762425315 MESSAGE = ( b"\xdfw\xeb)\t2R8\xda5\x02\xadE\xdd\xce\xd2\xe0\xb4\xf1\x81\xe7\xdf" b":\xce\x82m\xcf\x99\xf3o\x9d\xe6\xfb\xe4\x98O\x88\xcfh\xbe\xfd\xc2" b"{\xafm\xb3\xff\xb4QR\xffPu$\xfc>A'\x03t\xc5\xf9\xd8\xf3I,\xaa\"*" b"\xd7q\xfe\xb7]\x11\xa9uB'd\x89\x03'3\xb8/\x80\xa2#\x00\xa2\xfe" b"\xff\xae\xb0\x86\xc1/ o\xc8]?\xa05L\xff8\x8az\x92\xc9\xab\x9fg0|" b'\\5\x98\xfaG\x9b#\xec\x1a\xc5\x10\xd6\x08\x9c:\x01"\x0c\x812O/i' b'\xc4WI\x0c\r\xd8\x81-m1_\x14]$\xf8\x16\xef\x1e\x1d\xb0"Q\x1a\xcf' b'`R\xae\x0c"r2\x9a\xa3\xdb\xc4W} GROUP_ORDER secret = validate_secret(secret) assert len(secret) == 32 assert ZERO < secret < GROUP_ORDER def test_out_of_range(self): with pytest.raises(ValueError, match=f"Secret scalar must be greater than 0 and less than {GROUP_ORDER_INT}"): validate_secret(ZERO) with pytest.raises(ValueError, match=f"Secret scalar must be greater than 0 and less than {GROUP_ORDER_INT}"): validate_secret(GROUP_ORDER) def test_bytes_int_conversion(): bytestr = b"\x00" + urandom(31) assert pad_scalar(int_to_bytes(bytes_to_int(bytestr))) == bytestr def test_bytes_int_conversion_padded(): bytestr = b"\x00" + urandom(31) assert int_to_bytes_padded(bytes_to_int(bytestr)) == bytestr def test_der_conversion(samples): assert pem_to_der(der_to_pem(samples["PRIVATE_KEY_DER"])) == samples["PRIVATE_KEY_DER"] def test_verify_signature(samples): assert verify_signature(samples["SIGNATURE"], samples["MESSAGE"], samples["PUBLIC_KEY_COMPRESSED"]) assert verify_signature(samples["SIGNATURE"], samples["MESSAGE"], samples["PUBLIC_KEY_UNCOMPRESSED"]) def test_chunk_data(): assert list(chunk_data("4fadd1977328c11efc1c1d8a781aa6b9677984d3e0b", 2)) == [ "4f", "ad", "d1", "97", "73", "28", "c1", "1e", "fc", "1c", "1d", "8a", "78", "1a", "a6", "b9", "67", "79", "84", "d3", "e0", "b", ] if __name__ == "__main__": pytest.main(["-v", __file__])