Browse Source

Merge branch 'master' into anonswap

anonswap
tecnovert 2 years ago
parent
commit
2c53babe78
No known key found for this signature in database GPG Key ID: 6C1A887B4701EAE3
  1. 8
      .flake8
  2. 5
      .github/FUNDING.yml
  3. 41
      .github/scripts/build-windows-wheels.sh
  4. 14
      .github/scripts/install-macos-build-deps.sh
  5. 9
      .github/scripts/install-test-deps.sh
  6. 209
      .github/workflows/build.yml
  7. 62
      .github/workflows/docs.yml
  8. 3
      .gitignore
  9. 104
      .travis.yml
  10. 34
      .travis/build-linux-wheels.sh
  11. 51
      .travis/deploy.sh
  12. 91
      .travis/install.sh
  13. 11
      .travis/prepare_windows_build.sh
  14. 14
      .travis/upload_coverage.sh
  15. 50
      DOWNSTREAM.rst
  16. 116
      HISTORY.rst
  17. 1
      MANIFEST.in
  18. 31
      README.md
  19. 349
      README.rst
  20. 2
      _cffi_build/build.py
  21. 1
      coincurve/__init__.py
  22. 12
      coincurve/context.py
  23. 34
      coincurve/ecdsa.py
  24. 276
      coincurve/keys.py
  25. 0
      coincurve/py.typed
  26. 13
      coincurve/types.py
  27. 112
      coincurve/utils.py
  28. 3
      docs/.scripts/49_global_refs.py
  29. 2
      docs/.snippets/abbrs.txt
  30. 5
      docs/.snippets/links.txt
  31. 4
      docs/.snippets/refs.txt
  32. 50
      docs/api.md
  33. 61
      docs/assets/css/custom.css
  34. BIN
      docs/assets/images/favicon.ico
  35. 131
      docs/history.md
  36. 49
      docs/index.md
  37. 62
      docs/install.md
  38. 43
      docs/users.md
  39. 123
      mkdocs.yml
  40. 8
      mypy.ini
  41. 12
      pyproject.toml
  42. 40
      release.py
  43. 2
      setup.cfg
  44. 45
      setup.py
  45. 21
      setup_support.py
  46. 28
      tests/samples.py
  47. 3
      tests/test_bench.py
  48. 1
      tests/test_ecdsa.py
  49. 12
      tests/test_keys.py
  50. 8
      tests/test_utils.py
  51. 80
      tox.ini

8
.flake8

@ -0,0 +1,8 @@
# TODO: move this to pyproject.toml when supported: https://gitlab.com/pycqa/flake8/-/issues/428
[flake8]
select = B,C,E,F,W,B001,B003,B006,B007,B301,B305,B306,B902,Q000,Q001,Q002,Q003
ignore = E203,E722,W503
exclude = .tox,build
max-line-length = 120
per-file-ignores = coincurve/__init__.py:F401

5
.github/FUNDING.yml

@ -0,0 +1,5 @@
github:
- ofek
custom:
- https://ofek.dev/donate/
- https://paypal.me/ofeklev

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

@ -0,0 +1,41 @@
#!/bin/bash
set -ex
build_dll() {
./autogen.sh
./configure --host=$1 --enable-module-recovery --enable-experimental --enable-module-ecdh --enable-benchmark=no --enable-tests=no --enable-openssl-tests=no --enable-exhaustive-tests=no --enable-static --disable-dependency-tracking --with-pic
make
}
sudo apt-get install -y mingw-w64
sudo apt-get -f install
mkdir .hidden
cp * .hidden -R
mv .hidden/coincurve/_windows_libsecp256k1.py .hidden/coincurve/_libsecp256k1.py
mv .hidden ../clean
cd ..
curl -sLO "https://github.com/bitcoin-core/secp256k1/archive/$COINCURVE_UPSTREAM_REF.tar.gz"
tar -xzf "$COINCURVE_UPSTREAM_REF.tar.gz"
mv "secp256k1-$COINCURVE_UPSTREAM_REF" secp256k1
mv secp256k1 64bit
cp 64bit 32bit -R
cd 64bit
build_dll x86_64-w64-mingw32
mv .libs/libsecp256k1-0.dll ../clean/coincurve/libsecp256k1.dll
cd ../clean
python setup.py bdist_wheel --plat-name=win_amd64
rm coincurve/libsecp256k1.dll
cd ../32bit
build_dll i686-w64-mingw32
mv .libs/libsecp256k1-0.dll ../clean/coincurve/libsecp256k1.dll
cd ../clean
python setup.py bdist_wheel --plat-name=win32
mv dist/* ../coincurve/dist/
cd ../coincurve

14
.github/scripts/install-macos-build-deps.sh

@ -0,0 +1,14 @@
#!/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 automake libtool pkg-config; do
brew list $pkg > /dev/null || brew install $pkg
brew outdated --quiet $pkg || brew upgrade $pkg
done

9
.github/scripts/install-test-deps.sh

@ -0,0 +1,9 @@
#!/bin/bash
set -ex
if [ "$RUNNER_OS" == "macOS" ]; then
./.github/scripts/install-macos-build-deps.sh
fi
python -m pip install --upgrade cffi
python -m pip install --upgrade tox codecov

209
.github/workflows/build.yml

@ -0,0 +1,209 @@
name: build
on:
push:
tags:
- v*
branches:
- master
pull_request:
branches:
- master
concurrency:
group: build-${{ github.head_ref }}
cancel-in-progress: true
env:
COINCURVE_UPSTREAM_REF: d8a246324650c3df8d54d133a8ac3c1b857a7a4e
COINCURVE_IGNORE_SYSTEM_LIB: '1'
CIBW_BEFORE_ALL_MACOS: ./.github/scripts/install-macos-build-deps.sh
CIBW_ENVIRONMENT_PASS_LINUX: >
COINCURVE_UPSTREAM_REF
COINCURVE_IGNORE_SYSTEM_LIB
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())
"
CIBW_SKIP: >
pp*
jobs:
test:
name: Test latest Python
runs-on: ubuntu-latest
env:
PYTHON_VERSION: '3.10'
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v2
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Upgrade Python packaging tools
run: pip install --upgrade pip setuptools wheel
- name: Show runner information
run: |
python --version
pip --version
- name: Install dependencies
run: ./.github/scripts/install-test-deps.sh
- name: Check style and typing
run: tox -e lint,typing
- name: Run tests
run: tox -e ${PYTHON_VERSION}
- name: Run benchmarks
run: tox -e bench
- name: Upload coverage
run: codecov -X gcov
linux-wheels-standard:
name: Build Linux wheels
needs:
- test
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Build wheels
uses: pypa/cibuildwheel@v2.3.1
- uses: actions/upload-artifact@v2
with:
name: artifacts
path: wheelhouse/*.whl
if-no-files-found: error
macos-wheels-x86-64:
name: Build macOS wheels
needs:
- test
runs-on: macos-10.15
steps:
- uses: actions/checkout@v2
- name: Build wheels
uses: pypa/cibuildwheel@v2.3.1
env:
CIBW_ARCHS_MACOS: x86_64
- uses: actions/upload-artifact@v2
with:
name: artifacts
path: wheelhouse/*.whl
if-no-files-found: error
macos-wheels-arm:
name: Build macOS wheels for ARM
needs:
- test
runs-on: macos-10.15
steps:
- uses: actions/checkout@v2
- name: Build wheels
uses: pypa/cibuildwheel@v2.3.1
env:
CIBW_ARCHS_MACOS: arm64
COINCURVE_CROSS_HOST: aarch64-apple-darwin
CFLAGS: -target arm64-apple-macos11
- uses: actions/upload-artifact@v2
with:
name: artifacts
path: wheelhouse/*.whl
if-no-files-found: error
windows-wheels-and-sdist:
name: Build Windows wheels and source distribution
needs:
- test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install build dependencies
run: python -m pip install --upgrade cffi
- name: Build source distribution
run: python setup.py sdist
- name: Build Windows wheels
run: ./.github/scripts/build-windows-wheels.sh
- uses: actions/upload-artifact@v2
with:
name: artifacts
path: dist/*
if-no-files-found: error
linux-wheels-arm:
name: Build Linux wheels for ARM
needs:
- test
runs-on: ubuntu-20.04
if: >
github.event_name == 'push'
&&
(github.ref == 'refs/heads/master' || startsWith(github.event.ref, 'refs/tags'))
steps:
- uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: arm64
- name: Build wheels
uses: pypa/cibuildwheel@v2.3.1
env:
CIBW_ARCHS_LINUX: aarch64
- uses: actions/upload-artifact@v2
with:
name: artifacts
path: wheelhouse/*.whl
if-no-files-found: error
publish:
name: Publish release
needs:
- linux-wheels-standard
- macos-wheels-x86-64
- macos-wheels-arm
- windows-wheels-and-sdist
- linux-wheels-arm
runs-on: ubuntu-latest
if: >
github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
steps:
- uses: actions/download-artifact@v2
with:
name: artifacts
path: dist
- name: Push build artifacts to PyPI
uses: pypa/gh-action-pypi-publish@v1.4.2
with:
skip_existing: true
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}

62
.github/workflows/docs.yml

@ -0,0 +1,62 @@
name: docs
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
# Fetch all history for applying timestamps to every page
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Upgrade Python packaging tools
run: pip install --disable-pip-version-check --upgrade pip setuptools wheel
- name: Install dependencies
run: python -m pip install --upgrade tox
- name: Build documentation
run: tox -e docs-ci build
- uses: actions/upload-artifact@v2
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@v2
with:
name: documentation
path: site
- uses: peaceiris/actions-gh-pages@v3
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

3
.gitignore

@ -4,11 +4,12 @@
/.coverage
/.eggs
/.idea
/.mypy_cache
/.tox
/coincurve.egg-info
/build
/dist
/docs/build
/site
/wheelhouse
/pip-wheel-metadata

104
.travis.yml

@ -1,104 +0,0 @@
sudo: true
dist: xenial
language: python
cache:
directories:
- $HOME/.cache/pip
# Only build master, release branches, and version tags (which are considered
# branches by Travis, see https://github.com/travis-ci/travis-ci/issues/8518)
branches:
only:
- master
- /^release.+$/
- /^[0-9]+\.[0-9]+\.[0-9]+.*$/
env:
global:
- LD_LIBRARY_PATH=./libsecp256k1_ext/.libs
- DYLD_FALLBACK_LIBRARY_PATH=./libsecp256k1_ext/.libs
- LIB_DIR=./libsecp256k1_ext/.libs
- INCLUDE_DIR=./libsecp256k1_ext/include
- PYPI_USERNAME=Ofekmeister
addons:
apt:
packages:
- autoconf
- automake
- git
- libffi-dev
- libgmp-dev
- libtool
- pkg-config
matrix:
include:
- python: 3.6
env: TOXENV=style BUILD_LINUX_WHEELS=1
services: docker
- python: 2.7
env: TOXENV=py27,bench
- python: 3.5
env: TOXENV=py35,bench
- python: 3.6
env: TOXENV=py36,bench
- python: 3.7
env: TOXENV=py37,bench
- python: 3.8
env: TOXENV=py38,bench
- python: pypy3
env: TOXENV=pypy3k,bench
- os: osx
language: generic
python: 2.7
osx_image: xcode10.1
env: TOXENV=py27,bench TRAVIS_PYTHON_VERSION=2.7
- os: osx
language: generic
python: 3.5
osx_image: xcode10.1
env: TOXENV=py35,bench TRAVIS_PYTHON_VERSION=3.5
- os: osx
language: generic
python: 3.6
osx_image: xcode10.1
env: TOXENV=py36,bench TRAVIS_PYTHON_VERSION=3.6 NEED_SSL_FIX=true
- os: osx
language: generic
python: 3.7
osx_image: xcode10.1
env: TOXENV=py37,bench TRAVIS_PYTHON_VERSION=3.7
- os: osx
language: generic
python: 3.8
osx_image: xcode10.1
env: TOXENV=py38,bench TRAVIS_PYTHON_VERSION=3.8
before_install:
- chmod +x .travis/build-linux-wheels.sh
- chmod +x .travis/build_windows_wheels.sh
- chmod +x .travis/deploy.sh
- chmod +x .travis/prepare_windows_build.sh
- chmod +x .travis/upload_coverage.sh
install:
- .travis/prepare_windows_build.sh
- source .travis/install.sh
script:
# Reuse test environments for benchmarks
- export TESTENV=$(python -c "import os;print(os.getenv('TOXENV', 'bench').split(',')[0])")
- tox
after_success:
- .travis/upload_coverage.sh
deploy:
- provider: script
skip_cleanup: true
script: .travis/deploy.sh
on:
repo: ofek/coincurve
tags: true

34
.travis/build-linux-wheels.sh

@ -1,34 +0,0 @@
#!/bin/bash
set -e
set -x
# Install a system package required by our library
yum install -y pkg-config libffi libffi-devel
# Use updated GMP
curl -O https://ftp.gnu.org/gnu/gmp/gmp-6.1.2.tar.bz2 && tar -xjpf gmp-*.tar.bz2 && cd gmp* && ./configure --build=${BUILD_GMP_CPU}-pc-linux-gnu > /dev/null && make > /dev/null && make check > /dev/null && make install > /dev/null && cd ..
mkdir out
# PyPy
if [[ "$PLAT" == "manylinux2010_x86_64" ]]; then
mkdir /opt/python/pypy3
curl -LO https://bitbucket.org/squeaky/portable-pypy/downloads/pypy3.6-7.1.1-beta-linux_x86_64-portable.tar.bz2
tar -xjpf pypy3.6-7.1.1-beta-linux_x86_64-portable.tar.bz2 -C /opt/python/pypy3 --strip-components=1
curl -sSL https://raw.githubusercontent.com/pypa/get-pip/master/get-pip.py | /opt/python/pypy3/bin/pypy
fi
# Compile wheels
for PYBIN in /opt/python/*/bin; do
if [[ ${PYBIN} =~ (cp27|cp35|cp36|cp37|cp38|pypy) ]]; then
${PYBIN}/pip wheel /io/ -w wheelhouse/
fi
done
# Adjust wheel tags
for whl in wheelhouse/coincurve*.whl; do
auditwheel repair "$whl" --plat $PLAT -w out
done
cp out/*.whl /io/dist

51
.travis/deploy.sh

@ -1,51 +0,0 @@
#!/bin/bash
set -e -x
echo "deploy"
python setup.py install
# remove any left over files from previous steps
rm -rf build dist
mkdir dist
python setup.py sdist
if [[ "$TRAVIS_OS_NAME" == "linux" && ${BUILD_LINUX_WHEELS} -eq 1 ]]; then
docker run --rm -e PLAT="manylinux2010_x86_64" -e BUILD_GMP_CPU="amd64" -v $(pwd):/io quay.io/pypa/manylinux2010_x86_64 /io/.travis/build-linux-wheels.sh
docker run --rm -e PLAT="manylinux1_x86_64" -e BUILD_GMP_CPU="amd64" -v $(pwd):/io quay.io/pypa/manylinux1_x86_64 /io/.travis/build-linux-wheels.sh
linux32 docker run --rm -e PLAT="manylinux1_i686" -e BUILD_GMP_CPU="i686" -v $(pwd):/io quay.io/pypa/manylinux1_i686 /io/.travis/build-linux-wheels.sh
.travis/build_windows_wheels.sh
else
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
# Make sure we can build and "fix" the wheel.
python -m pip install delocate wheel
# Create directories for the built and fixed wheels.
mkdir dist_wheels/ fixed_wheels/
# Build the wheel for the local OS.
python -m pip wheel . --wheel-dir dist_wheels/
# Make the wheel relocatable to another OS.
delocate-wheel \
--check-archs \
--wheel-dir fixed_wheels/ \
--verbose \
dist_wheels/coincurve*.whl
# Move the fixed wheel into dist/.
[ -d dist/ ] || mkdir dist/
mv fixed_wheels/coincurve*.whl dist/
# Clean up build directories.
rm -fr dist_wheels/ fixed_wheels/
fi
fi
ls -l dist
python -m pip install twine
# Ignore non-existing files in globs
shopt -s nullglob
twine upload --skip-existing dist/coincurve*.{whl,gz} -u "${PYPI_USERNAME}"
set +e +x

91
.travis/install.sh

@ -1,91 +0,0 @@
#!/bin/bash
set -e
set -x
# On osx we need to bring our own Python.
# See: https://github.com/travis-ci/travis-ci/issues/2312
if [[ $TRAVIS_OS_NAME == "osx" ]]; then
# We use the official python.org installers to make sure our wheels are
# going to be as widely compatible as possible
PYTHON_PKG_27="https://www.python.org/ftp/python/2.7.16/python-2.7.16-macosx10.9.pkg"
PYTHON_PKG_36="https://www.python.org/ftp/python/3.6.8/python-3.6.8-macosx10.9.pkg"
PYTHON_PKG_37="https://www.python.org/ftp/python/3.7.3/python-3.7.3-macosx10.9.pkg"
PYTHON_PKG_38="https://www.python.org/ftp/python/3.8.0/python-3.8.0-macosx10.9.pkg"
GET_PIP="https://bootstrap.pypa.io/get-pip.py"
# update brew
brew update || brew update
# Update openssl if necessary
brew outdated openssl || brew upgrade openssl
# Install packages needed to build lib-secp256k1
for pkg in automake libtool pkg-config libffi; do
brew list $pkg > /dev/null || brew install $pkg
brew outdated --quiet $pkg || brew upgrade $pkg
done
mkdir -p ~/.cache/python-dl
if [[ "${TRAVIS_PYTHON_VERSION}" == "3.5" ]]; then
PYVERSION="${TRAVIS_PYTHON_VERSION}.5"
brew outdated pyenv || brew upgrade pyenv
pyenv install ${PYVERSION}
pyenv global ${PYVERSION}
else
builtin pushd ~/.cache/python-dl
ls -l
py_pkg=PYTHON_PKG_${TRAVIS_PYTHON_VERSION//./}
py_pkg=${!py_pkg}
installer_pkg=$(basename ${py_pkg})
# The package might have been cached from a previous run
if [[ ! -f ${installer_pkg} ]]; then
curl -LO ${py_pkg}
fi
sudo installer -pkg ${installer_pkg} -target /
builtin popd
fi
case "${TRAVIS_PYTHON_VERSION}" in
2.7)
python=/Library/Frameworks/Python.framework/Versions/${TRAVIS_PYTHON_VERSION}/bin/python
virtualenv=virtualenv
;;
3.6|3.7|3.8)
python=/Library/Frameworks/Python.framework/Versions/${TRAVIS_PYTHON_VERSION}/bin/python3
virtualenv=venv
;;
3.5)
python="$(pyenv root)/versions/${PYVERSION}/bin/python"
virtualenv=venv
;;
esac
if [[ "${TRAVIS_PYTHON_VERSION}" == "2.7" ]]; then
builtin pushd ~
curl -LO ${GET_PIP}
${python} get-pip.py
${python} -m pip install --user virtualenv
builtin popd
fi
# https://bugs.python.org/issue28150
if [[ "${NEED_SSL_FIX}" == "true" ]]; then
"/Applications/Python ${TRAVIS_PYTHON_VERSION}/Install Certificates.command"
fi
mkdir ~/virtualenv
${python} -m ${virtualenv} ~/virtualenv/python${TRAVIS_PYTHON_VERSION}
source ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/activate
fi
# Install necessary packages
python -m pip install -U cffi tox codecov
set +x +e

11
.travis/prepare_windows_build.sh

@ -1,11 +0,0 @@
#!/bin/bash
if [[ "$TRAVIS_OS_NAME" == "linux" && ${BUILD_LINUX_WHEELS} -eq 1 ]]; then
mkdir .hidden
cp * .hidden -R
mkdir .hidden/dist
mv .hidden/coincurve/_windows_libsecp256k1.py .hidden/coincurve/_libsecp256k1.py
mv .hidden ../clean
sudo apt-get install -y mingw-w64
sudo apt-get -f install
fi

14
.travis/upload_coverage.sh

@ -1,14 +0,0 @@
#!/bin/bash
set -e
set -x
if [ -n "${TOXENV}" ]; then
case "${TOXENV}" in
style);;
^bench$);;
*)
codecov -X gcov
;;
esac
fi

50
DOWNSTREAM.rst

@ -1,50 +0,0 @@
Users
-----
- `AnyLedger <https://github.com/AnyLedger/anyledger-backend/blob/cb9e277ef4ba775384a1eb80ff1577418f88684e/requirements.in#L5>`_
- `Ark Ecosystem <https://github.com/ArkEcosystem/python-crypto/blob/a7c739c070ce17f8aa64155b95b698e7465ab373/setup.py#L9>`_
- `Augur Project <https://github.com/AugurProject/augur/blob/95177dfaee7d978608543523f180609c582c1ff9/packages/augur-core/requirements.txt#L9>`_
- `bit <https://github.com/ofek/bit>`_
- `Blockcerts <https://github.com/blockchain-certificates/cert-issuer/blob/b8f7af75c403f62e8dc1e50bd139d8ed2dc00742/ethereum_requirements.txt#L1>`_
- `btcrecover <https://github.com/gurnec/btcrecover/commit/f113867fa22d2f5b22175cc2b5b3892351bc1109>`_
- `Confidential Consortium Framework <https://github.com/microsoft/CCF/pull/213>`_
- `crankycoin <https://github.com/cranklin/crankycoin/blob/3d2b3482698227397a8daf30e0b80b2f2c030aec/requirements.txt#L8>`_
- `ecies/py <https://github.com/ecies/py/blob/8a2144ade9b61d0f47d32933a4480b8ad7332aec/requirements.txt#L3>`_
- `ElementsProject <https://github.com/ElementsProject/lightning/pull/2803>`_
- `Enigma <https://github.com/enigmampc/surface/blob/40ca2056bce32d0d479e4809ac8cd5ded102b3f0/etc/requirements.txt#L8>`_
- `ethereum/eth-keys <https://github.com/ethereum/eth-keys/commit/81755dfda714d77c1f8a092810ca31e570d84425>`_
- `ethereum/eth-tester <https://github.com/ethereum/eth-tester/blob/96e4e69de46acca387f2a946920e4c3e3f35605f/tox.ini#L17>`_
- `ethereum/py-evm <https://github.com/ethereum/py-evm/pull/31>`_
- `ethereum/pydevp2p <https://github.com/ethereum/pydevp2p/pull/80>`_
- `ethereum/pyethereum <https://github.com/ethereum/pyethereum/pull/777>`_
- `ethereum/trinity <https://github.com/ethereum/trinity/blob/07d9692a36663c69b6f00331654501b4d928b001/setup.py#L33>`_
- `ethereum/vyper <https://github.com/ethereum/vyper/blob/9491bcde0f87fd04d19d0a40a6c901b1bc0a718b/Dockerfile#L15>`_
- `EtherollApp <https://github.com/AndreMiras/EtherollApp/commit/2966c0850156364e46412da2331cee146b490e57>`_
- `ForkDelta <https://github.com/forkdelta/backend-replacement/blob/45517f48579f3270dc47da2075d8e0efc2e9ecb8/requirements.txt#L46>`_
- `golemfactory/golem <https://github.com/golemfactory/golem/pull/1527>`_
- `golemfactory/golem-messages <https://github.com/golemfactory/golem-messages/blob/1f72b6a6757036218cdf471c0295b8895b963266/setup.py#L39>`_
- `Heimdall <https://github.com/maddevsio/heimdall/blob/40248cd7e3ca6d0a39e3d71388792fc557ebc5ec/requirements.txt#L14>`_
- `HoneyBadgerBFT <https://github.com/initc3/HoneyBadgerBFT-Python/blob/048d6afb3c7184db670b96119aa99a6a5b0dafa6/setup.py#L31>`_
- `ICON Foundation <https://github.com/icon-project/icon-sdk-python/pull/48>`_
- `JoinMarket <https://github.com/JoinMarket-Org/joinmarket-clientserver/pull/223>`_
- `lbryio/lbry <https://github.com/lbryio/lbry/blob/d64916a06115920aaa9eaab67704a0b2d34aae20/CHANGELOG.md#security-1>`_
- `lbryio/torba <https://github.com/lbryio/torba/pull/13>`_
- `libp2p <https://github.com/libp2p/py-libp2p/pull/240>`_
- `minichain <https://github.com/kigawas/minichain/blob/8c1fd9499954bcdbc7e0f77f6fa6d9af3328f64c/requirements.txt#L3>`_
- `Nekoyume <https://github.com/nekoyume/nekoyume/pull/67>`_
- `NuCypher <https://github.com/nucypher/nucypher/pull/592>`_
- `Ocean Protocol <https://github.com/oceanprotocol/squid-py/blob/45e1ceada45934a593022ca96bec552e8beb6051/Pipfile#L14>`_
- `OmiseGO <https://github.com/omisego/fee-burner/blob/984f75362ca193680ecb4dc43c7d2e13f3be68bd/contracts/requirements.txt#L9>`_
- `PeerAssets <https://github.com/PeerAssets/pypeerassets/commit/113c9a234c94499c7e591b8a93928be0a77298fa>`_
- `Planetarium <https://github.com/planetarium/coincurve-stubs>`_
- `python-idex <https://github.com/sammchardy/python-idex/blob/3b698533e290a0fe884961ce69c4b2e699378b8d/requirements.txt#L2>`_
- `PyWallet <https://github.com/AndreMiras/PyWallet/commit/69f2f240b39f332123d347c72bc75f0b199813c1>`_
- `Quantstamp <https://github.com/quantstamp/qsp-protocol-node/blob/6abadfeef4c80d8a246e3676ed7238e290edf050/requirements.txt#L32>`_
- `QuarkChain <https://github.com/QuarkChain/pyquarkchain/blob/4c002d4b535174704ce39f3954e4026f23d520bb/requirements.txt#L4>`_
- `raiden-network/microraiden <https://github.com/raiden-network/microraiden/blob/8d5f1d86818f01c8cafe9366da1cecdef0e8b0f4/requirements.txt#L5>`_
- `raiden-network/raiden <https://github.com/raiden-network/raiden/pull/534>`_
- `raiden-network/raiden-contracts <https://github.com/raiden-network/raiden-contracts/blob/f251c01015564a2b91401692234aa5ed1ea67ebc/requirements.txt#L3>`_
- `raiden-network/raiden-libs <https://github.com/raiden-network/raiden-libs/blob/e88586e6d40e2b49d19efbdffafdaa2a86f84c86/requirements.txt#L1>`_
- `raiden-network/raiden-services <https://github.com/raiden-network/raiden-services/blob/cf69ebc29bfcdf946d6429bc9c1bb63779a19599/requirements.txt#L9>`_
- `Rotkehlchen <https://github.com/rotkehlchenio/rotkehlchen/blob/599bd9c1a780940926b4baabcd4164368b75a120/requirements.txt#L8>`_
- `ZeroNet <https://github.com/HelloZeroNet/ZeroNet/blob/fa7013fdf7eeb7bb01c1c113b4529b7233bf52ce/requirements.txt#L11>`_

116
HISTORY.rst

@ -1,116 +0,0 @@
History
-------
Important changes are emphasized.
master
^^^^^^
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 <https://github.com/ofek/coincurve/issues/6>`_. You can choose
to use this or remain on 5.2.0. This will only be a temporary change. See
`<https://github.com/ofek/coincurve/commit/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 ``validate_secret`` 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 <https://github.com/ofek/coincurve/issues/5>`_
5.0.0
^^^^^
- **Breaking:** Coincurve is now dual-licensed under the terms of MIT and Apache v2.0.
- Performance improvements from libsecp256k1 master:
`1 <https://github.com/bitcoin-core/secp256k1/commit/cf12fa13cb96797d6ce356a5023051f99f915fe6>`_
`2 <https://github.com/bitcoin-core/secp256k1/commit/aa8499080e2a657113781921096b59a74d7bc0e7>`_
`3 <https://github.com/bitcoin-core/secp256k1/commit/8b7680a826498a786eca5737e0e97ee4d2e63713>`_
`4 <https://github.com/bitcoin-core/secp256k1/commit/465159c278cecc2cf8d934e78f640f345243eb72>`_
`5 <https://github.com/bitcoin-core/secp256k1/commit/4cc8f52505b2922390a115c77eeb3b251bc9af88>`_
`6 <https://github.com/bitcoin-core/secp256k1/commit/cbc20b8c34d44c2ef175420f3cdfe054f82e8e2c>`_
- Improvements to documentation.
4.5.1
^^^^^
- First public stable release

1
MANIFEST.in

@ -1,3 +1,4 @@
include coincurve/py.typed
include setup_support.py
recursive-include _cffi_build *.py *.h
graft libsecp256k1

31
README.md

@ -0,0 +1,31 @@
# 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/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/coincurve.svg?logo=python&label=Python&logoColor=gold)](https://pypi.org/project/coincurve/) |
| Meta | [![code style - black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![types - Mypy](https://img.shields.io/badge/types-Mypy-blue.svg)](https://github.com/ambv/black) [![imports - isort](https://img.shields.io/badge/imports-isort-ef8336.svg)](https://github.com/pycqa/isort) [![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)
- [ZeroNet](https://zeronet.io)
- [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)

349
README.rst

@ -1,349 +0,0 @@
Coincurve
=========
.. image:: https://travis-ci.org/ofek/coincurve.svg?branch=master
:target: https://travis-ci.org/ofek/coincurve
:alt: Travis CI
.. image:: https://codecov.io/github/ofek/coincurve/coverage.svg?branch=master
:target: https://codecov.io/github/ofek/coincurve?branch=master
:alt: Codecov
.. image:: https://img.shields.io/pypi/status/coverage.svg
:target: https://pypi.org/project/coincurve
:alt: PyPI - Status
.. image:: https://img.shields.io/pypi/v/coincurve.svg
:target: https://pypi.org/project/coincurve
:alt: PyPI - Version
.. image:: https://pepy.tech/badge/coincurve
:target: https://pepy.tech/project/coincurve
:alt: PyPI - Downloads
.. image:: https://img.shields.io/badge/license-MIT%2FApache--2.0-9400d3.svg
:target: https://choosealicense.com/licenses
:alt: License: MIT/Apache-2.0
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black
:alt: Code style: black
-----
This library provides well-tested Python CFFI 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 elliptic curve secp256k1.
Table of Contents
~~~~~~~~~~~~~~~~~
.. contents::
:backlinks: top
:local:
Features
--------
- Fastest available implementation (more than 10x faster than OpenSSL)
- Clean, easy to use API
- Frequent updates from `libsecp256k1 <https://github.com/bitcoin-core/secp256k1>`_ master
- Linux, macOS, and Windows all have binary packages for both 64 and 32-bit architectures
- Linux & macOS use GMP for faster computation
- Deterministic signatures via `RFC 6979 <https://tools.ietf.org/html/rfc6979>`_
- Non-malleable signatures (lower-S form) by default
- Secure, non-malleable `ECDH implementation <https://github.com/ofek/coincurve/issues/9#issuecomment-329235214>`_
- Implements a fix for `<https://bugs.python.org/issue28150>`_ to support Python 3.6+ on macOS
Users
-----
- `<https://www.ethereum.org>`_
- `<https://lbry.io>`_
- `<https://libp2p.io>`_
- `<https://zeronet.io>`_
- `<https://ark.io>`_
- `<https://www.augur.net>`_
- `<https://www.nucypher.com>`_
- `<https://raiden.network>`_
- `<https://golem.network>`_
- `<https://omisego.network>`_
and `many more <https://github.com/ofek/coincurve/blob/master/DOWNSTREAM.rst>`_
Installation
------------
Coincurve is distributed on PyPI and is available on Linux/macOS and Windows and
supports Python 2.7/3.5+ and PyPy3.5-v5.8.1+.
.. code-block:: bash
$ pip install coincurve
If you are on a system that doesn't have a precompiled binary wheel (e.g. FreeBSD)
then pip will fetch source to build yourself. You must have the necessary packages.
On Debian/Ubuntu the necessary system packages are:
- ``build-essential``
- ``automake``
- ``pkg-config``
- ``libtool``
- ``libffi-dev``
- ``python3-dev`` (or ``python-dev`` for Python 2)
- ``libgmp-dev`` (optional)
On macOS the necessary Homebrew packages are:
- ``automake``
- ``pkg-config``
- ``libtool``
- ``libffi``
- ``gmp`` (optional)
API
---
Coincurve provides a simple API.
coincurve.verify_signature
^^^^^^^^^^^^^^^^^^^^^^^^^^
``verify_signature(signature, message, public_key, hasher=sha256, context=GLOBAL_CONTEXT)``
Verifies some message was signed by the owner of a public key.
* Parameters:
- **signature** (``bytes``) - The signature to verify.
- **message** (``bytes``) - The message that was supposedly signed.
- **public_key** (``bytes``) - A public key in compressed or uncompressed form.
- **hasher** - The hash function to use, can be ``None``. hasher(message) must return 32 bytes.
- **context** (``coincurve.Context``)
* Returns: ``bool``
coincurve.PrivateKey
^^^^^^^^^^^^^^^^^^^^
All instances have a ``public_key`` of type ``coincurve.PublicKey``
``PrivateKey(secret=None, context=GLOBAL_CONTEXT)``
* Parameters:
- **secret** (``bytes``) - The secret to use.
- **context** (``coincurve.Context``)
**Methods:**
*classmethod* ``from_hex(hexed, context=GLOBAL_CONTEXT)``
*classmethod* ``from_int(num, context=GLOBAL_CONTEXT)``
*classmethod* ``from_pem(pem, context=GLOBAL_CONTEXT)``
*classmethod* ``from_der(der, context=GLOBAL_CONTEXT)``
``sign(message, hasher=sha256, custom_nonce=None)``
* Parameters:
- **message** (``bytes``) - The message to sign.
- **hasher** - The hash function to use, can be ``None``. hasher(message) must return 32 bytes.
- **custom_nonce** - A tuple of arity 2 in the form of ``(nonce_fn, nonce_data)``. Refer to:
`secp256k1.h <https://github.com/bitcoin-core/secp256k1/blob/b8c26a39903de7bf1d789232e030319116b011ac/include/secp256k1.h#L449-L450>`_
* Returns: ``bytes``. 68 <= len(signature) <= 71
``sign_recoverable(message, hasher=sha256)``
* Parameters:
- **message** (``bytes``) - The message to sign.
- **hasher** - The hash function to use, can be ``None``. hasher(message) must return 32 bytes.
* Returns: ``bytes``
``ecdh(public_key)``
Computes a Diffie-Hellman secret in constant time. **Note:** This prevents malleability by returning
``sha256(compressed_public_key)`` instead of the ``x`` coordinate directly. See `<https://github.com/ofek/coincurve/issues/9>`_.
* Parameters:
- **public_key** (``bytes``) - Another party's public key in compressed or uncompressed form.
* Returns: ``bytes``
``add(scalar, update=False)``
* Parameters:
- **scalar** (``bytes``) - The scalar to add.
- **update** (``bool``) - If ``True``, will update and return ``self``.
* Returns: ``coincurve.PrivateKey``
``multiply(scalar, update=False)``
* Parameters:
- **scalar** (``bytes``) - The scalar to multiply.
- **update** (``bool``) - If ``True``, will update and return ``self``.
* Returns: ``coincurve.PrivateKey``
``to_hex()``
``to_int()``
``to_pem()``
``to_der()``
coincurve.PublicKey
^^^^^^^^^^^^^^^^^^^
``PublicKey(data, context=GLOBAL_CONTEXT)``
* Parameters:
- **data** (``bytes``) - The public key in compressed or uncompressed form.
- **context** (``coincurve.Context``)
**Methods:**
*classmethod* ``from_secret(secret, context=GLOBAL_CONTEXT)``
*classmethod* ``from_valid_secret(secret, context=GLOBAL_CONTEXT)``
*classmethod* ``from_point(x, y, context=GLOBAL_CONTEXT)``
*classmethod* ``from_signature_and_message(serialized_sig, message, hasher=sha256, context=GLOBAL_CONTEXT)``
*classmethod* ``combine_keys(public_keys, context=GLOBAL_CONTEXT)``
* Parameters:
- **public_keys** (``list``) - A ``list`` of ``coincurve.PublicKey`` to add.
- **context** (``coincurve.Context``)
* Returns: ``coincurve.PublicKey``
``format(compressed=True)``
* Parameters:
- **compressed** (``bool``)
* Returns: The public key serialized to ``bytes``.
``point()``
* Returns: (x, y)
``verify(signature, message, hasher=sha256)``
Verifies some message was signed by the owner of this public key.
* Parameters:
- **signature** (``bytes``) - The signature to verify.
- **message** (``bytes``) - The message that was supposedly signed.
- **hasher** - The hash function to use, can be ``None``. hasher(message) must return 32 bytes.
* Returns: ``bool``
``add(scalar, update=False)``
* Parameters:
- **scalar** (``bytes``) - The scalar to add.
- **update** (``bool``) - If ``True``, will update and return ``self``.
* Returns: ``coincurve.PublicKey``
``multiply(scalar, update=False)``
* Parameters:
- **scalar** (``bytes``) - The scalar to multiply.
- **update** (``bool``) - If ``True``, will update and return ``self``.
* Returns: ``coincurve.PublicKey``
``combine(public_keys, update=False)``
* Parameters:
- **public_keys** (``list``) - A ``list`` of ``coincurve.PublicKey`` to add.
- **update** (``bool``) - If ``True``, will update and return ``self``.
* Returns: ``coincurve.PublicKey``
License
-------
Coincurve is distributed under the terms of both
- `Apache License, Version 2.0 <https://choosealicense.com/licenses/apache-2.0>`_
- `MIT License <https://choosealicense.com/licenses/mit>`_
at your option.
Credits
-------
- Contributors of `libsecp256k1 <https://github.com/bitcoin-core/secp256k1>`_.
- Contributors of `secp256k1-py <https://github.com/ludbb/secp256k1-py>`_.
While Coincurve is nearly a complete rewrite, much of the build system
provided by `ulope <https://github.com/ulope>`_ remains.
History
-------
Important changes are emphasized.
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
View `all history <https://github.com/ofek/coincurve/blob/master/HISTORY.rst>`_

2
_cffi_build/build.py

@ -3,7 +3,6 @@ from collections import namedtuple
from cffi import FFI
here = os.path.dirname(os.path.abspath(__file__))
Source = namedtuple('Source', ('h', 'include'))
@ -23,6 +22,7 @@ def _mk_ffi(sources, name='_libsecp256k1', **kwargs):
return _ffi
modules = [
Source('secp256k1.h', '#include <secp256k1.h>'),
Source('secp256k1_ecdh.h', '#include <secp256k1_ecdh.h>'),

1
coincurve/__init__.py

@ -1,3 +1,4 @@
from coincurve.context import GLOBAL_CONTEXT, Context
from coincurve.keys import PrivateKey, PublicKey
from coincurve.utils import verify_signature

12
coincurve/context.py

@ -2,11 +2,12 @@ from os import urandom
from threading import Lock
from coincurve.flags import CONTEXT_ALL, CONTEXT_FLAGS
from ._libsecp256k1 import ffi, lib
class Context:
def __init__(self, seed=None, flag=CONTEXT_ALL):
def __init__(self, seed: bytes = None, flag=CONTEXT_ALL, name: str = ''):
if flag not in CONTEXT_FLAGS:
raise ValueError('{} is an invalid context flag.'.format(flag))
self._lock = Lock()
@ -14,7 +15,9 @@ class Context:
self.ctx = ffi.gc(lib.secp256k1_context_create(flag), lib.secp256k1_context_destroy)
self.reseed(seed)
def reseed(self, seed=None):
self.name = name
def reseed(self, seed: bytes = None):
"""
Protects against certain possible future side-channel timing attacks.
"""
@ -23,5 +26,8 @@ class Context:
res = lib.secp256k1_context_randomize(self.ctx, ffi.new('unsigned char [32]', seed))
assert res == 1
def __repr__(self):
return self.name or super().__repr__()
GLOBAL_CONTEXT = Context()
GLOBAL_CONTEXT = Context(name='GLOBAL_CONTEXT')

34
coincurve/ecdsa.py

@ -1,12 +1,14 @@
from coincurve.context import GLOBAL_CONTEXT
from coincurve.context import GLOBAL_CONTEXT, Context
from coincurve.types import Hasher
from coincurve.utils import bytes_to_int, int_to_bytes, sha256
from ._libsecp256k1 import ffi, lib
MAX_SIG_LENGTH = 72
CDATA_SIG_LENGTH = 64
def cdata_to_der(cdata, context=GLOBAL_CONTEXT):
def cdata_to_der(cdata, context: Context = GLOBAL_CONTEXT) -> bytes:
der = ffi.new('unsigned char[%d]' % MAX_SIG_LENGTH)
der_length = ffi.new('size_t *', MAX_SIG_LENGTH)
@ -15,7 +17,7 @@ def cdata_to_der(cdata, context=GLOBAL_CONTEXT):
return bytes(ffi.buffer(der, der_length[0]))
def der_to_cdata(der, context=GLOBAL_CONTEXT):
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))
@ -36,7 +38,7 @@ def parse_compact(data, context=GLOBAL_CONTEXT):
return cdata
def recover(message, recover_sig, hasher=sha256, context=GLOBAL_CONTEXT):
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:
raise ValueError('Message hash must be 32 bytes long.')
@ -45,10 +47,10 @@ def recover(message, recover_sig, hasher=sha256, context=GLOBAL_CONTEXT):
recovered = lib.secp256k1_ecdsa_recover(context.ctx, pubkey, recover_sig, msg_hash)
if recovered:
return pubkey
raise Exception('failed to recover ECDSA public key')
raise ValueError('failed to recover ECDSA public key')
def serialize_recoverable(recover_sig, context=GLOBAL_CONTEXT):
def serialize_recoverable(recover_sig, context: Context = GLOBAL_CONTEXT) -> bytes:
output = ffi.new('unsigned char[%d]' % CDATA_SIG_LENGTH)
recid = ffi.new('int *')
@ -57,14 +59,14 @@ def serialize_recoverable(recover_sig, context=GLOBAL_CONTEXT):
return bytes(ffi.buffer(output, CDATA_SIG_LENGTH)) + int_to_bytes(recid[0])
def deserialize_recoverable(serialized, context=GLOBAL_CONTEXT):
def deserialize_recoverable(serialized: bytes, context: Context = GLOBAL_CONTEXT):
if len(serialized) != 65:
raise ValueError("Serialized signature must be 65 bytes long.")
raise ValueError('Serialized signature must be 65 bytes long.')
ser_sig, rec_id = serialized[:64], bytes_to_int(serialized[64:])
if not 0 <= rec_id <= 3:
raise ValueError("Invalid recovery id.")
raise ValueError('Invalid recovery id.')
recover_sig = ffi.new('secp256k1_ecdsa_recoverable_signature *')
@ -81,7 +83,7 @@ Warning:
"""
def serialize_compact(raw_sig, context=GLOBAL_CONTEXT): # no cov
def serialize_compact(raw_sig, context: Context = GLOBAL_CONTEXT): # no cov
output = ffi.new('unsigned char[%d]' % CDATA_SIG_LENGTH)
res = lib.secp256k1_ecdsa_signature_serialize_compact(context.ctx, output, raw_sig)
@ -90,9 +92,9 @@ def serialize_compact(raw_sig, context=GLOBAL_CONTEXT): # no cov
return bytes(ffi.buffer(output, CDATA_SIG_LENGTH))
def deserialize_compact(ser_sig, context=GLOBAL_CONTEXT): # no cov
def deserialize_compact(ser_sig: bytes, context: Context = GLOBAL_CONTEXT): # no cov
if len(ser_sig) != 64:
raise Exception("invalid signature length")
raise Exception('invalid signature length')
raw_sig = ffi.new('secp256k1_ecdsa_signature *')
res = lib.secp256k1_ecdsa_signature_parse_compact(context.ctx, raw_sig, ser_sig)
@ -101,15 +103,13 @@ def deserialize_compact(ser_sig, context=GLOBAL_CONTEXT): # no cov
return raw_sig
def signature_normalize(raw_sig, context=GLOBAL_CONTEXT): # no cov
def signature_normalize(raw_sig, context: Context = GLOBAL_CONTEXT): # no cov
"""
Check and optionally convert a signature to a normalized lower-S form.
If check_only is True then the normalized signature is not returned.
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. When check_only is True,
the normalized signature returned is always None.
normalized), and the normalized signature.
"""
sigout = ffi.new('secp256k1_ecdsa_signature *')
@ -118,7 +118,7 @@ def signature_normalize(raw_sig, context=GLOBAL_CONTEXT): # no cov
return not not res, sigout
def recoverable_convert(recover_sig, context=GLOBAL_CONTEXT): # no cov
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)

276
coincurve/keys.py

@ -1,13 +1,15 @@
from typing import Tuple
from asn1crypto.keys import ECDomainParameters, ECPointBitString, ECPrivateKey, PrivateKeyAlgorithm, PrivateKeyInfo
from coincurve.context import GLOBAL_CONTEXT
from coincurve.context import GLOBAL_CONTEXT, Context
from coincurve.ecdsa import parse_compact, cdata_to_der, der_to_cdata, deserialize_recoverable, recover, serialize_recoverable
from coincurve.flags import EC_COMPRESSED, EC_UNCOMPRESSED
from coincurve.types import Hasher, Nonce
from coincurve.utils import (
bytes_to_hex,
DEFAULT_NONCE,
bytes_to_int,
der_to_pem,
ensure_unicode,
get_valid_secret,
hex_to_bytes,
int_to_bytes_padded,
@ -16,24 +18,39 @@ from coincurve.utils import (
sha256,
validate_secret,
)
from ._libsecp256k1 import ffi, lib
DEFAULT_NONCE = (ffi.NULL, ffi.NULL)
from ._libsecp256k1 import ffi, lib
class PrivateKey:
def __init__(self, secret=None, context=GLOBAL_CONTEXT):
self.secret = validate_secret(secret) if secret is not None else get_valid_secret()
def __init__(self, secret: bytes = None, context: Context = GLOBAL_CONTEXT):
"""
:param secret: The secret used to initialize the private key.
If not provided or `None`, a new key will be generated.
"""
self.secret: bytes = validate_secret(secret) if secret is not None else get_valid_secret()
self.context = context
self.public_key = PublicKey.from_valid_secret(self.secret, self.context)
def sign(self, message, hasher=sha256, custom_nonce=None):
self.public_key: PublicKey = PublicKey.from_valid_secret(self.secret, self.context)
def sign(self, message: bytes, hasher: Hasher = sha256, custom_nonce: Nonce = DEFAULT_NONCE) -> bytes:
"""
Create an ECDSA signature.
:param message: The message to sign.
:param hasher: The hash function to use, which must return 32 bytes. By default,
the `sha256` algorithm is used. If `None`, no hashing occurs.
:param custom_nonce: Custom nonce data in the form `(nonce_function, input_data)`. Refer to
[secp256k1.h](https://github.com/bitcoin-core/secp256k1/blob/f8c0b57e6ba202b1ce7c5357688de97c9c067697/include/secp256k1.h#L546-L547).
:return: 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:
raise ValueError('Message hash must be 32 bytes long.')
signature = ffi.new('secp256k1_ecdsa_signature *')
nonce_fn, nonce_data = custom_nonce or DEFAULT_NONCE
nonce_fn, nonce_data = custom_nonce
signed = lib.secp256k1_ecdsa_sign(self.context.ctx, signature, msg_hash, self.secret, nonce_fn, nonce_data)
@ -42,15 +59,28 @@ class PrivateKey:
return cdata_to_der(signature, self.context)
def sign_recoverable(self, message, hasher=sha256):
def sign_recoverable(self, message: bytes, hasher: Hasher = sha256, custom_nonce: Nonce = DEFAULT_NONCE) -> bytes:
"""
Create a recoverable ECDSA signature.
:param message: The message to sign.
:param hasher: The hash function to use, which must return 32 bytes. By default,
the `sha256` algorithm is used. If `None`, no hashing occurs.
:param custom_nonce: Custom nonce data in the form `(nonce_function, input_data)`. Refer to
[secp256k1_recovery.h](https://github.com/bitcoin-core/secp256k1/blob/f8c0b57e6ba202b1ce7c5357688de97c9c067697/include/secp256k1_recovery.h#L78-L79).
:return: 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:
raise ValueError('Message hash must be 32 bytes long.')
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, ffi.NULL, ffi.NULL
self.context.ctx, signature, msg_hash, self.secret, nonce_fn, nonce_data
)
if not signed:
@ -58,14 +88,34 @@ class PrivateKey:
return serialize_recoverable(signature, self.context)
def ecdh(self, public_key):
def ecdh(self, public_key: bytes) -> bytes:
"""
Compute an EC Diffie-Hellman secret in constant time.
!!! note
This prevents malleability by returning `sha256(compressed_public_key)` instead of the `x` coordinate
directly. See #9.
:param public_key: The formatted public key.
:return: 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, update=False):
def add(self, scalar: bytes, update: bool = False):
"""
Add a scalar to the private key.
:param scalar: The scalar with which to add.
:param update: Whether or not to update and return the private key in-place.
:return: The new private key, or the modified private key if `update` is `True`.
:rtype: PrivateKey
: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)
@ -84,7 +134,15 @@ class PrivateKey:
return PrivateKey(secret, self.context)
def multiply(self, scalar, update=False):
def multiply(self, scalar: bytes, update: bool = False):
"""
Multiply the private key by a scalar.
:param scalar: The scalar with which to multiply.
:param update: Whether or not to update and return the private key in-place.
:return: The new private key, or the modified private key if `update` is `True`.
:rtype: PrivateKey
"""
scalar = validate_secret(scalar)
secret = ffi.new('unsigned char [32]', self.secret)
@ -100,19 +158,31 @@ class PrivateKey:
return PrivateKey(secret, self.context)
def to_hex(self):
return bytes_to_hex(self.secret)
def to_hex(self) -> str:
"""
:return: The private key encoded as a hex string.
"""
return self.secret.hex()
def to_int(self):
def to_int(self) -> int:
"""
:return: The private key as an integer.
"""
return bytes_to_int(self.secret)
def to_pem(self):
def to_pem(self) -> bytes:
"""
:return: The private key encoded in PEM format.
"""
return der_to_pem(self.to_der())
def to_der(self):
def to_der(self) -> bytes:
"""
:return: The private key encoded in DER format.
"""
pk = ECPrivateKey(
{
'version': ensure_unicode('ecPrivkeyVer1'),
'version': 'ecPrivkeyVer1',
'private_key': self.to_int(),
'public_key': ECPointBitString(self.public_key.format(compressed=False)),
}
@ -123,8 +193,8 @@ class PrivateKey:
'version': 0,
'private_key_algorithm': PrivateKeyAlgorithm(
{
'algorithm': ensure_unicode('ec'),
'parameters': ECDomainParameters(name='named', value=ensure_unicode('1.3.132.0.10')),
'algorithm': 'ec',
'parameters': ECDomainParameters(name='named', value='1.3.132.0.10'),
}
),
'private_key': pk,
@ -132,21 +202,45 @@ class PrivateKey:
).dump()
@classmethod
def from_hex(cls, hexed, context=GLOBAL_CONTEXT):
def from_hex(cls, hexed: str, context: Context = GLOBAL_CONTEXT):
"""
:param hexed: The private key encoded as a hex string.
:param context:
:return: The private key.
:rtype: PrivateKey
"""
return PrivateKey(hex_to_bytes(hexed), context)
@classmethod
def from_int(cls, num, context=GLOBAL_CONTEXT):
def from_int(cls, num: int, context: Context = GLOBAL_CONTEXT):
"""
:param num: The private key as an integer.
:param context:
:return: The private key.
:rtype: PrivateKey
"""
return PrivateKey(int_to_bytes_padded(num), context)
@classmethod
def from_pem(cls, pem, context=GLOBAL_CONTEXT):
def from_pem(cls, pem: bytes, context: Context = GLOBAL_CONTEXT):
"""
:param pem: The private key encoded in PEM format.
:param context:
:return: The private key.
:rtype: PrivateKey
"""
return PrivateKey(
int_to_bytes_padded(PrivateKeyInfo.load(pem_to_der(pem)).native['private_key']['private_key']), context
)
@classmethod
def from_der(cls, der, context=GLOBAL_CONTEXT):
def from_der(cls, der: bytes, context: Context = GLOBAL_CONTEXT):
"""
:param der: The private key encoded in DER format.
:param context:
:return: The private key.
:rtype: PrivateKey
"""
return PrivateKey(int_to_bytes_padded(PrivateKeyInfo.load(der).native['private_key']['private_key']), context)
def _update_public_key(self):
@ -155,12 +249,21 @@ class PrivateKey:
if not created:
raise ValueError('Invalid secret.')
def __eq__(self, other):
def __eq__(self, other) -> bool:
return self.secret == other.secret
class PublicKey:
def __init__(self, data, context=GLOBAL_CONTEXT):
def __init__(self, data, context: Context = GLOBAL_CONTEXT):
"""
:param data: 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.
:type data: bytes
:param context:
:raises ValueError: If the public key could not be parsed or was invalid.
"""
if not isinstance(data, bytes):
self.public_key = data
else:
@ -176,7 +279,15 @@ class PublicKey:
self.context = context
@classmethod
def from_secret(cls, secret, context=GLOBAL_CONTEXT):
def from_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT):
"""
Derive a public key from a private key secret.
:param secret: The private key secret.
:param context:
:return: The public key.
:rtype: PublicKey
"""
public_key = ffi.new('secp256k1_pubkey *')
created = lib.secp256k1_ec_pubkey_create(context.ctx, public_key, validate_secret(secret))
@ -191,7 +302,7 @@ class PublicKey:
return PublicKey(public_key, context)
@classmethod
def from_valid_secret(cls, secret, context=GLOBAL_CONTEXT):
def from_valid_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT):
public_key = ffi.new('secp256k1_pubkey *')
created = lib.secp256k1_ec_pubkey_create(context.ctx, public_key, secret)
@ -202,17 +313,50 @@ class PublicKey:
return PublicKey(public_key, context)
@classmethod
def from_point(cls, x, y, context=GLOBAL_CONTEXT):
def from_point(cls, x: int, y: int, context: Context = GLOBAL_CONTEXT):
"""
Derive a public key from a coordinate point in the form `(x, y)`.
:param x:
:param y:
:param context:
:return: The public key.
:rtype: PublicKey
"""
return PublicKey(b'\x04' + int_to_bytes_padded(x) + int_to_bytes_padded(y), context)
@classmethod
def from_signature_and_message(cls, serialized_sig, message, hasher=sha256, context=GLOBAL_CONTEXT):
def from_signature_and_message(
cls, signature: bytes, message: bytes, hasher: Hasher = sha256, context: Context = GLOBAL_CONTEXT
):
"""
Recover an ECDSA public key from a recoverable signature.
:param signature: The recoverable ECDSA signature.
:param message: The message that was supposedly signed.
:param hasher: The hash function to use, which must return 32 bytes. By default,
the `sha256` algorithm is used. If `None`, no hashing occurs.
:param context:
:return: The public key that signed the message.
:rtype: PublicKey
: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(serialized_sig, context=context), hasher=hasher, context=context)
recover(message, deserialize_recoverable(signature, context=context), hasher=hasher, context=context)
)
@classmethod
def combine_keys(cls, public_keys, context=GLOBAL_CONTEXT):
def combine_keys(cls, public_keys, context: Context = GLOBAL_CONTEXT):
"""
Add a number of public keys together.
:param public_keys: A sequence of public keys.
:type public_keys: List[PublicKey]
:param context:
:return: The combined public key.
:rtype: PublicKey
:raises ValueError: If the sum of the public keys was invalid.
"""
public_key = ffi.new('secp256k1_pubkey *')
combined = lib.secp256k1_ec_pubkey_combine(
@ -224,7 +368,13 @@ class PublicKey:
return PublicKey(public_key, context)
def format(self, compressed=True):
def format(self, compressed: bool = True) -> bytes:
"""
Format the public key.
:param compressed: Whether or to use the compressed format.
:return: 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)
output_len = ffi.new('size_t *', length)
@ -235,11 +385,22 @@ class PublicKey:
return bytes(ffi.buffer(serialized, length))
def point(self):
def point(self) -> Tuple[int, int]:
"""
:return: 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, message, hasher=sha256):
def verify(self, signature: bytes, message: bytes, hasher: Hasher = sha256) -> bool:
"""
:param signature: The ECDSA signature.
:param message: The message that was supposedly signed.
:param hasher: The hash function to use, which must return 32 bytes. By default,
the `sha256` algorithm is used. If `None`, no hashing occurs.
:return: A boolean indicating whether or not 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:
raise ValueError('Message hash must be 32 bytes long.')
@ -259,7 +420,16 @@ class PublicKey:
# A performance hack to avoid global bool() lookup.
return not not verified
def add(self, scalar, update=False):
def add(self, scalar: bytes, update: bool = False):
"""
Add a scalar to the public key.
:param scalar: The scalar with which to add.
:param update: Whether or not to update and return the public key in-place.
:return: The new public key, or the modified public key if `update` is `True`.
:rtype: PublicKey
: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])
@ -275,7 +445,15 @@ class PublicKey:
return PublicKey(new_key, self.context)
def multiply(self, scalar, update=False):
def multiply(self, scalar: bytes, update: bool = False):
"""
Multiply the public key by a scalar.
:param scalar: The scalar with which to multiply.
:param update: Whether or not to update and return the public key in-place.
:return: The new public key, or the modified public key if `update` is `True`.
:rtype: PublicKey
"""
scalar = validate_secret(scalar)
new_key = ffi.new('secp256k1_pubkey *', self.public_key[0])
@ -288,11 +466,21 @@ class PublicKey:
return PublicKey(new_key, self.context)
def combine(self, public_keys, update=False):
def combine(self, public_keys, update: bool = False):
"""
Add a number of public keys together.
:param public_keys: A sequence of public keys.
:type public_keys: List[PublicKey]
:param update: Whether or not to update and return the public key in-place.
:return: The combined public key, or the modified public key if `update` is `True`.
:rtype: PublicKey
: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 public_keys], len(public_keys)
self.context.ctx, new_key, [pk.public_key for pk in [self, *public_keys]], len(public_keys) + 1
)
if not combined:
@ -304,5 +492,5 @@ class PublicKey:
return PublicKey(new_key, self.context)
def __eq__(self, other):
def __eq__(self, other) -> bool:
return self.format(compressed=False) == other.format(compressed=False)

0
coincurve/py.typed

13
coincurve/types.py

@ -0,0 +1,13 @@
import sys
from typing import Optional, Tuple
from ._libsecp256k1 import ffi
# https://bugs.python.org/issue42965
if sys.version_info >= (3, 9, 2):
from collections.abc import Callable
else:
from typing import Callable
Hasher = Optional[Callable[[bytes], bytes]]
Nonce = Tuple[ffi.CData, ffi.CData]

112
coincurve/utils.py

@ -1,9 +1,11 @@
from base64 import b64decode, b64encode
from binascii import hexlify, unhexlify
from hashlib import sha256 as _sha256
from os import urandom
from os import environ, urandom
from typing import Generator
from coincurve.context import GLOBAL_CONTEXT, Context
from coincurve.types import Hasher
from coincurve.context import GLOBAL_CONTEXT
from ._libsecp256k1 import ffi, lib
GROUP_ORDER = (
@ -17,105 +19,93 @@ PEM_HEADER = b'-----BEGIN PRIVATE KEY-----\n'
PEM_FOOTER = b'-----END PRIVATE KEY-----\n'
def ensure_unicode(s):
if isinstance(s, bytes):
s = s.decode('utf-8')
return s
def pad_hex(hexed):
# Pad odd-length hex strings.
return hexed if not len(hexed) & 1 else '0' + hexed
if hasattr(int, "from_bytes"):
def bytes_to_int(bytestr):
return int.from_bytes(bytestr, 'big')
else:
if environ.get('COINCURVE_BUILDING_DOCS') != 'true':
DEFAULT_NONCE = (ffi.NULL, ffi.NULL)
def bytes_to_int(bytestr):
return int(bytestr.encode('hex'), 16)
def sha256(bytestr: bytes) -> bytes:
return _sha256(bytestr).digest()
else: # no cov
if hasattr(int, "to_bytes"):
class __Nonce(tuple):
def __repr__(self):
return '(ffi.NULL, ffi.NULL)'
def int_to_bytes(num):
return num.to_bytes((num.bit_length() + 7) // 8 or 1, 'big')
class __HasherSHA256:
def __call__(self, bytestr: bytes) -> bytes:
return _sha256(bytestr).digest()
def int_to_bytes_padded(num):
return pad_scalar(num.to_bytes((num.bit_length() + 7) // 8 or 1, 'big'))
def __repr__(self):
return 'sha256'
DEFAULT_NONCE = __Nonce((ffi.NULL, ffi.NULL)) # type: ignore
sha256 = __HasherSHA256()
else:
def int_to_bytes(num):
return unhexlify(pad_hex('%x' % num))
def int_to_bytes_padded(num):
return pad_scalar(unhexlify(pad_hex('%x' % num)))
if hasattr(bytes, "hex"):
def bytes_to_hex(bytestr):
return bytestr.hex()
else:
def bytes_to_hex(bytestr):
return ensure_unicode(hexlify(bytestr))
def pad_hex(hexed: str) -> str:
# Pad odd-length hex strings.
return hexed if not len(hexed) & 1 else f'0{hexed}'
if hasattr(bytes, "fromhex"):
def bytes_to_int(bytestr: bytes) -> int:
return int.from_bytes(bytestr, 'big')
def hex_to_bytes(hexed):
return pad_scalar(bytes.fromhex(pad_hex(hexed)))
def int_to_bytes(num: int) -> bytes:
return num.to_bytes((num.bit_length() + 7) // 8 or 1, 'big')
else:
def hex_to_bytes(hexed):
return pad_scalar(unhexlify(pad_hex(hexed)))
def int_to_bytes_padded(num: int) -> bytes:
return pad_scalar(num.to_bytes((num.bit_length() + 7) // 8 or 1, 'big'))
def sha256(bytestr):
return _sha256(bytestr).digest()
def hex_to_bytes(hexed: str) -> bytes:
return pad_scalar(bytes.fromhex(pad_hex(hexed)))
def chunk_data(data, size):
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):
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):
def pem_to_der(pem: bytes) -> bytes:
return b64decode(b''.join(pem.strip().splitlines()[1:-1]))
def get_valid_secret():
def get_valid_secret() -> bytes:
while True:
secret = urandom(KEY_SIZE)
if ZERO < secret < GROUP_ORDER:
return secret
def pad_scalar(scalar):
def pad_scalar(scalar: bytes) -> bytes:
return (ZERO * (KEY_SIZE - len(scalar))) + scalar
def validate_secret(secret):
def validate_secret(secret: bytes) -> bytes:
if not 0 < bytes_to_int(secret) < GROUP_ORDER_INT:
raise ValueError('Secret scalar must be greater than 0 and less than {}.'.format(GROUP_ORDER_INT))
return pad_scalar(secret)
def verify_signature(signature, message, public_key, hasher=sha256, context=GLOBAL_CONTEXT):
def verify_signature(
signature: bytes, message: bytes, public_key: bytes, hasher: Hasher = sha256, context: Context = GLOBAL_CONTEXT
) -> bool:
"""
:param signature: The ECDSA signature.
:param message: The message that was supposedly signed.
:param public_key: The formatted public key.
:param hasher: The hash function to use, which must return 32 bytes. By default,
the `sha256` algorithm is used. If `None`, no hashing occurs.
:param context:
:return: 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))

3
docs/.scripts/49_global_refs.py

@ -0,0 +1,3 @@
def patch(lines):
"""This ensures that links and abbreviations are always available."""
lines.extend(('', '--8<-- "refs.txt"', ''))

2
docs/.snippets/abbrs.txt

@ -0,0 +1,2 @@
*[ECDH]: Elliptic-curve Diffie–Hellman
*[PyPI]: Python Package Index

5
docs/.snippets/links.txt

@ -0,0 +1,5 @@
[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

4
docs/.snippets/refs.txt

@ -0,0 +1,4 @@
--8<--
links.txt
abbrs.txt
--8<--

50
docs/api.md

@ -0,0 +1,50 @@
# Developer Interface
-----
All objects are available directly under the root namespace `coincurve`.
::: coincurve.verify_signature
rendering:
show_root_full_path: false
selection:
docstring_style: restructured-text
::: coincurve.PrivateKey
rendering:
show_root_full_path: false
selection:
docstring_style: restructured-text
members:
- __init__
- sign
- sign_recoverable
- ecdh
- add
- multiply
- to_hex
- to_pem
- to_der
- to_int
- from_hex
- from_pem
- from_der
- from_int
::: coincurve.PublicKey
rendering:
show_root_full_path: false
selection:
docstring_style: restructured-text
members:
- __init__
- verify
- format
- point
- combine
- add
- multiply
- combine_keys
- from_signature_and_message
- from_secret
- from_point

61
docs/assets/css/custom.css

@ -0,0 +1,61 @@
/* https://github.com/squidfunk/mkdocs-material/issues/1522 */
.md-typeset h5 {
color: var(--md-default-fg-color);
text-transform: none;
}
/* https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/ */
.md-typeset .tabbed-set {
border: 1px solid #eee;
}
.md-typeset .tabbed-content {
padding: 0.5em 1em;
}
/* Brighter links for dark mode */
[data-md-color-scheme=slate] {
--md-text-link-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; }
}
/* Everything below is from https://pawamoy.github.io/mkdocstrings/handlers/python/#recommended-style-material */
/* Indentation. */
div.doc-contents:not(.first) {
padding-left: 25px;
border-left: 4px solid rgba(230, 230, 230);
margin-bottom: 80px;
}
/* Don't capitalize names. */
h5.doc-heading {
text-transform: none !important;
}
/* Don't use vertical space on hidden ToC entries. */
.hidden-toc::before {
margin-top: 0 !important;
padding-top: 0 !important;
}
/* Don't show permalink of hidden ToC entries. */
.hidden-toc a.headerlink {
display: none;
}
/* Avoid breaking parameters name, etc. in table cells. */
td code {
word-break: normal !important;
}
/* For pieces of Markdown rendered in table cells. */
td p {
margin-top: 0 !important;
margin-bottom: 0 !important;
}

BIN
docs/assets/images/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

131
docs/history.md

@ -0,0 +1,131 @@
# History
-----
Important changes are emphasized.
## master
## Unreleased
## 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

49
docs/index.md

@ -0,0 +1,49 @@
# 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/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/coincurve.svg?logo=python&label=Python&logoColor=gold)](https://pypi.org/project/coincurve/) |
| Meta | [![code style - black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![types - Mypy](https://img.shields.io/badge/types-Mypy-blue.svg)](https://github.com/ambv/black) [![imports - isort](https://img.shields.io/badge/imports-isort-ef8336.svg)](https://github.com/pycqa/isort) [![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][], 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)
- [ZeroNet](https://zeronet.io)
- [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 |
| --- | --- |
| <ul><li><kbd>,</kbd> (comma)</li><li><kbd>p</kbd></li></ul> | Navigate to the "previous" page |
| <ul><li><kbd>.</kbd> (period)</li><li><kbd>n</kbd></li></ul> | Navigate to the "next" page |
| <ul><li><kbd>/</kbd></li><li><kbd>s</kbd></li></ul> | Display the search modal |

62
docs/install.md

@ -0,0 +1,62 @@
# 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.7 | <ul><li>x86_64</li></ul> | <ul><li>x86_64</li><li>x86</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> |
| CPython 3.8 | <ul><li>x86_64</li><li>ARM64</li></ul> | <ul><li>x86_64</li><li>x86</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> |
| CPython 3.9 | <ul><li>x86_64</li><li>ARM64</li></ul> | <ul><li>x86_64</li><li>x86</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> |
| CPython 3.10 | <ul><li>x86_64</li><li>ARM64</li></ul> | <ul><li>x86_64</li><li>x86</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> |
## 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.
!!! 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
```

43
docs/users.md

@ -0,0 +1,43 @@
# Users
-----
## Organizations
| Name | Projects |
| --- | --- |
| [Anyl](https://github.com/Anylsite) | <ul><li><a href=https://github.com/Anylsite/anyledger-backend/blob/cb9e277ef4ba775384a1eb80ff1577418f88684e/requirements.in#L5> anyledger-backend </a></li></ul> |
| [ARK](https://ark.io) | <ul><li><a href=https://github.com/ArkEcosystem/python-crypto/blob/1bd016f76b41eba9711be748c1caf20d8042f590/setup.py#L9> python-crypto </a></li></ul> |
| [Augur](https://www.augur.net) | <ul><li><a href=https://github.com/AugurProject/augur-core/blob/5388e00120d3e2328b5ccc70960bedff1c8a03dd/requirements.txt#L9> augur-core </a></li></ul> |
| [Blockcerts](https://www.blockcerts.org) | <ul><li><a href=https://github.com/blockchain-certificates/cert-issuer/blob/9b0ed451ef686018a507acd8bb7d217263a6fbf4/ethereum_requirements.txt#L2> cert-issuer </a></li></ul> |
| [ECIES](https://ecies.org) | <ul><li><a href=https://github.com/ecies/py/blob/7d4bd252129410eb1472d9e3cf0fd9f1fcfaf174/pyproject.toml#L38> py </a></li></ul> |
| [Elements](https://elementsproject.org) | <ul><li><a href=https://github.com/ElementsProject/lightning/blob/d134580419b90f2299cfa3646906b8b8b96c356e/requirements.txt#L33> lightning </a></li></ul> |
| [Ethereum](https://ethereum.org) | <ul><li><a href=https://github.com/ethereum/alexandria/blob/adba4114fbd5f707181da602abd977e008e463c9/setup.py#L67> alexandria </a></li><li><a href=https://github.com/ethereum/ddht/blob/341e84e9163338556cd48dd2fcfda9eedec3eb45/setup.py#L73> ddht </a></li><li><a href=https://github.com/ethereum/eth-keys/blob/dd4f00a5d2f2b394665ccecc9817f753e58cc7bc/setup.py#L10> eth-keys </a></li><li><a href=https://github.com/ethereum/eth-tester/blob/1e37e846915374914bdc2950fbb2f9ea6ca2f3ec/tox.ini#L16> eth-tester </a></li><li><a href=https://github.com/ethereum/py-evm/blob/5e949a457fbe6692dcd9e9e2f141a1848000a0c2/setup.py#L26> py-evm </a></li><li><a href=https://github.com/ethereum/pydevp2p/blob/b09b8a06a152f34cd7dc7950b14b04e3f01511af/requirements.txt#L8> pydevp2p </a></li><li><a href=https://github.com/ethereum/trinity/blob/65609f8fda7d880c0efe859ea84a7d0935c02edb/setup.py#L39> trinity </a></li></ul> |
| [Gnosis](https://gnosis.io) | <ul><li><a href=https://github.com/gnosis/gnosis-py/blob/7bad62eb83b50cf952227f5ae1019c95b8b4d9cd/README.rst#quick-start> gnosis-py </a></li></ul> |
| [Golem Network](https://golem.network) | <ul><li><a href=https://github.com/golemfactory/concent/blob/23ffa9464c995a628aa570bc9a0a05d29f48b044/concent_api/requirements.lock#L18> concent </a></li><li><a href=https://github.com/golemfactory/golem/blob/6280b5d946640e81c475c66c5de4a9e53cbfcc69/requirements.txt#L25> golem </a></li><li><a href=https://github.com/golemfactory/golem-messages/blob/aae2a60a88c938f5ed7012b838cf2a29934b58b2/setup.py#L41> golem-messages </a></li></ul> |
| [ICON Foundation](https://icon.foundation) | <ul><li><a href=https://github.com/icon-project/goloop/blob/6c8341e0c29fc7f9136221f315e8a76af9b5450c/pyee/requirements.txt#L2> goloop </a></li><li><a href=https://github.com/icon-project/icon-sdk-python/blob/cb293688c154349cabeb9b3f50a1cab29e91859d/requirements.txt#L2> icon-sdk-python </a></li><li><a href=https://github.com/icon-project/icon-service/blob/73c8d4521374207ef77e63b789fd511fc6d028b8/requirements.txt#L3> icon-service </a></li><li><a href=https://github.com/icon-project/loopchain/blob/9f29e8914918e12d683f2e1318c9b3c52dbee08d/requirements.txt#L11> loopchain </a></li><li><a href=https://github.com/icon-project/t-bears/pull/77> t-bears </a></li></ul> |
| [LBRY](https://lbry.com) | <ul><li><a href=https://github.com/lbryio/lbry-android-sdk/blob/b0331248cdb7b1ca4a866e4aaa166dd820549e56/recipes/coincurve/__init__.py> lbry-android-sdk </a></li><li><a href=https://github.com/lbryio/lbry-sdk/blob/7486ee95371c238d51e1c2552113ad25bfaf0426/setup.py#L53> lbry-sdk </a></li></ul> |
| [libp2p](https://libp2p.io) | <ul><li><a href=https://github.com/libp2p/py-libp2p/blob/12786f4e26783b530279a6f89089cf69af8e3922/setup.py#L73> py-libp2p </a></li></ul> |
| [Microsoft](https://www.microsoft.com) | <ul><li><a href=https://github.com/microsoft/CCF/blob/f6670587ec1cb6c0faf8efcc6e4d08b8f4c1fd60/tests/requirements.txt#L4> CCF </a></li></ul> |
| [NuCypher](https://www.nucypher.com) | <ul><li><a href=https://github.com/nucypher/nucypher/blob/24a57e1c810aa6408ecfc24942956925146aa024/requirements.txt#L16> nucypher </a></li><li><a href=https://github.com/nucypher/nucypher-monitor/blob/f8df51a37d0299c36541b26ef13d72fa390c294e/requirements.txt#L17> nucypher-monitor </a></li></ul> |
| [Quantstamp](https://quantstamp.com) | <ul><li><a href=https://github.com/quantstamp/qsp-protocol-node/blob/6f776b01c91a3b1c306ab74932324ea367fa6157/requirements.txt#L32> qsp-protocol-node </a></li></ul> |
| [QuarkChain](https://www.quarkchain.io) | <ul><li><a href=https://github.com/QuarkChain/pyquarkchain/blob/1f858f46efe31c3fb3bac20cdbd44eb94d89fb63/requirements.txt#L4> pyquarkchain </a></li></ul> |
| [Raiden Network](https://raiden.network) | <ul><li><a href=https://github.com/raiden-network/demo-train/blob/66d187bd4f9e83a7d8d0cc43fc5d31c8444745f5/requirements.txt#L16> demo-train </a></li><li><a href=https://github.com/raiden-network/light-client/blob/8da39f8f164a3d93674889db39875003e1bc6e93/e2e-environment/synapse/auth/eth_auth_provider.py#L20> light-client </a></li><li><a href=https://github.com/raiden-network/microraiden/blob/2d51e78afaf3c0a8ddab87e59a5260c0064cdbdd/requirements.txt#L5> microraiden </a></li><li><a href=https://github.com/raiden-network/raiden/blob/e4c0f6d22788eddf51da551f11ea988a8dd5fd0d/requirements/requirements.in#L4> raiden </a></li><li><a href=https://github.com/raiden-network/raiden-contracts/blob/93230caa554f6f29e55b4521aafd0af20b710b1f/requirements.txt#L3> raiden-contracts </a></li><li><a href=https://github.com/raiden-network/raiden-service-bundle/blob/1ba6a265016eca4ade0ed4f2a198cebc570c11d3/build/synapse/Dockerfile#L18> raiden-service-bundle </a></li><li><a href=https://github.com/raiden-network/raiden-services/blob/b5d0f81447fbe476ed8185d825560b2b9327d455/src/raiden_libs/utils.py#L4> raiden-services </a></li><li><a href=https://github.com/raiden-network/raiden-wizard/blob/bf2fe8662be4db2c36bcc920d8e31cec888a496e/requirements.txt#L17> raiden-wizard </a></li><li><a href=https://github.com/raiden-network/raidex/blob/master/requirements.txt#L12> raidex </a></li></ul> |
| [SKALE Network](https://skale.network) | <ul><li><a href=https://github.com/skalenetwork/libBLS/blob/785b7ab11f78512f6466b6dc996c4db44825696a/.travis.yml#L46> libBLS </a></li><li><a href=https://github.com/skalenetwork/sgx.py/blob/efff7d1a09abbea2c703cc21b25f9a6d9e7fcb79/setup.py#L11> sgx.py </a></li></ul> |
## 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/HelloZeroNet/ZeroNet/blob/454c0b2e7e000fda7000cba49027541fbf327b96/requirements.txt#L12)

123
mkdocs.yml

@ -0,0 +1,123 @@
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 &copy; Ofek Lev 2017-present'
docs_dir: docs
site_dir: site
theme:
name: material
language: en
features:
- navigation.instant
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
font:
text: Roboto
code: Roboto Mono
icon:
logo: material/circle-multiple
repo: fontawesome/brands/github-alt
favicon: assets/images/favicon.ico
nav:
- About: index.md
- Install: install.md
- API Reference: api.md
- Users: users.md
- History: history.md
plugins:
# Built-in
- search:
# Extra
- minify:
minify_html: true
- git-revision-date-localized:
type: date
- mkdocstrings:
default_handler: python
handlers:
python:
rendering:
show_if_no_docstring: true
show_root_heading: true
show_source: true
markdown_extensions:
# Built-in
- markdown.extensions.abbr:
- markdown.extensions.admonition:
- markdown.extensions.footnotes:
- markdown.extensions.meta:
- markdown.extensions.tables:
- markdown.extensions.toc:
permalink: true
# Extra
- mkpatcher:
location: docs/.scripts
- 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:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.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_shorthand: true
provider: github
user: ofek
repo: coincurve
- pymdownx.mark:
- pymdownx.progressbar:
- pymdownx.smartsymbols:
- pymdownx.snippets:
base_path: docs/.snippets
- pymdownx.superfences:
- pymdownx.tabbed:
- 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/twitter
link: https://twitter.com/Ofekmeister
- icon: fontawesome/brands/linkedin
link: https://www.linkedin.com/in/ofeklev/
extra_css:
- assets/css/custom.css
- https://cdn.jsdelivr.net/gh/tonsky/FiraCode@5.2/distr/fira_code.css

8
mypy.ini

@ -0,0 +1,8 @@
[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

12
pyproject.toml

@ -1,6 +1,6 @@
[tool.black]
line-length = 120
py36 = false
py36 = true
skip-string-normalization = true
include = '\.pyi?$'
@ -23,3 +23,13 @@ exclude = '''
| _libsecp256k1\.py$
)
'''
[tool.isort]
default_section = 'THIRDPARTY'
force_grid_wrap = 0
include_trailing_comma = true
known_first_party = 'coincurve'
line_length = 120
multi_line_output = 3
skip_glob = 'setup.py'
use_parentheses = true

40
release.py

@ -1,40 +0,0 @@
import os
import subprocess
import sys
import appdirs
def main():
here = os.path.dirname(os.path.abspath(__file__))
os.chdir(here)
version = sys.argv[-1]
delete = not not sys.argv[-2] == '-d'
delete_only = not not sys.argv[-2] == '--d'
try:
subprocess.call('git -h')
git_path = 'git'
except:
git_path = None
if git_path is None:
raise OSError('Unable to find git executable.')
cmds = ['{0} tag -a {1} -m "Version {1}"'.format(git_path, version), '{} push origin {}'.format(git_path, version)]
delete_cmd = '{} tag -d {}'.format(git_path, version)
if delete:
cmds.insert(0, delete_cmd)
elif delete_only:
cmds = [delete_cmd]
for cmd in cmds:
subprocess.call(cmd)
print(cmds)
if __name__ == '__main__':
main()

2
setup.cfg

@ -1,2 +0,0 @@
[wheel]
universal = 0

45
setup.py

@ -26,15 +26,19 @@ except ImportError:
sys.path.append(os.path.abspath(os.path.dirname(__file__)))
from setup_support import absolute, build_flags, detect_dll, has_system_lib
from setup_support import absolute, build_flags, detect_dll, has_system_lib # noqa: E402
BUILDING_FOR_WINDOWS = detect_dll()
MAKE = 'gmake' if platform.system() in ['FreeBSD', 'OpenBSD'] else 'make'
# IMPORTANT: keep in sync with .github/workflows/build.yml
#
# Version of libsecp256k1 to download if none exists in the `libsecp256k1` directory
LIB_TARBALL_URL = 'https://github.com/bitcoin-core/secp256k1/archive/0d9540b13ffcd7cd44cc361b8744b93d88aa76ba.tar.gz'
UPSTREAM_REF = os.getenv('COINCURVE_UPSTREAM_REF') or 'd8a246324650c3df8d54d133a8ac3c1b857a7a4e'
LIB_TARBALL_URL = f'https://github.com/bitcoin-core/secp256k1/archive/{UPSTREAM_REF}.tar.gz'
# We require setuptools >= 3.3
@ -191,14 +195,16 @@ class build_clib(_build_clib):
os.path.abspath(self.build_clib),
'--enable-experimental',
'--enable-module-ecdh',
#'--enable-benchmark=no',
'--enable-benchmark=yes',
'--enable-benchmark=no',
'--enable-tests=no',
'--enable-module-ed25519',
'--enable-module-generator',
'--enable-module-dleag',
'--enable-module-ecdsaotves',
'--with-valgrind=no'
]
if 'COINCURVE_CROSS_HOST' in os.environ:
cmd.append('--host={}'.format(os.environ['COINCURVE_CROSS_HOST']))
log.debug('Running configure: {}'.format(' '.join(cmd)))
subprocess.check_call(cmd, cwd=build_temp)
@ -239,13 +245,16 @@ class develop(_develop):
_develop.run(self)
package_data = {'coincurve': ['py.typed']}
if BUILDING_FOR_WINDOWS:
class Distribution(_Distribution):
def is_pure(self):
return False
setup_kwargs = dict(package_data={'coincurve': ['*.dll']}, include_package_data=True)
package_data['coincurve'].append('libsecp256k1.dll')
setup_kwargs = dict()
else:
class Distribution(_Distribution):
@ -269,25 +278,28 @@ else:
setup(
name='coincurve',
version='13.0.2',
version='17.0.2',
description='Cross-platform Python CFFI bindings for libsecp256k1',
long_description=open('README.rst', 'r').read(),
author='Ofek Lev',
author_email='ofekmeister@gmail.com',
maintainer='Ofek Lev',
maintainer_email='ofekmeister@gmail.com',
url='https://github.com/ofek/coincurve',
download_url='https://github.com/ofek/coincurve',
license='MIT or Apache-2.0',
long_description=open('README.md', 'r').read(),
long_description_content_type='text/markdown',
author_email='Ofek Lev <oss@ofek.dev>',
license='MIT OR Apache-2.0',
python_requires='>=3.7',
install_requires=['asn1crypto', 'cffi>=1.3.0'],
packages=find_packages(exclude=('_cffi_build', '_cffi_build.*', 'libsecp256k1', 'tests')),
package_data=package_data,
distclass=Distribution,
zip_safe=False,
project_urls={
'Documentation': 'https://ofek.dev/coincurve/',
'Issues': 'https://github.com/ofek/coincurve/issues',
'Source': 'https://github.com/ofek/coincurve',
},
keywords=[
'secp256k1',
'crypto',
@ -303,12 +315,11 @@ setup(
'License :: OSI Approved :: Apache Software License',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Software Development :: Libraries',

21
setup_support.py

@ -1,11 +1,10 @@
import glob
import os
import shutil
import subprocess
from contextlib import contextmanager
from tempfile import mkdtemp
import subprocess
@contextmanager
def workdir():
@ -42,20 +41,20 @@ def build_flags(library, type_, path):
"""Return separated build flags from pkg-config output"""
pkg_config_path = [path]
if "PKG_CONFIG_PATH" in os.environ:
if 'PKG_CONFIG_PATH' in os.environ:
pkg_config_path.append(os.environ['PKG_CONFIG_PATH'])
if "LIB_DIR" in os.environ:
if 'LIB_DIR' in os.environ:
pkg_config_path.append(os.environ['LIB_DIR'])
pkg_config_path.append(os.path.join(os.environ['LIB_DIR'], "pkgconfig"))
pkg_config_path.append(os.path.join(os.environ['LIB_DIR'], 'pkgconfig'))
options = ["--static", {'I': "--cflags-only-I", 'L': "--libs-only-L", 'l': "--libs-only-l"}[type_]]
options = ['--static', {'I': '--cflags-only-I', 'L': '--libs-only-L', 'l': '--libs-only-l'}[type_]]
return [
flag.strip("-{}".format(type_))
flag.strip('-{}'.format(type_))
for flag in subprocess.check_output(
["pkg-config"] + options + [library], env=dict(os.environ, PKG_CONFIG_PATH=":".join(pkg_config_path))
['pkg-config'] + options + [library], env=dict(os.environ, PKG_CONFIG_PATH=':'.join(pkg_config_path))
)
.decode("UTF-8")
.decode('UTF-8')
.split()
]
@ -68,7 +67,7 @@ def _find_lib():
ffi = FFI()
try:
ffi.dlopen("secp256k1")
ffi.dlopen('secp256k1')
if os.path.exists('/usr/include/secp256k1_ecdh.h'):
return True
else:
@ -76,7 +75,7 @@ def _find_lib():
return False
except OSError:
if 'LIB_DIR' in os.environ:
for path in glob.glob(os.path.join(os.environ['LIB_DIR'], "*secp256k1*")):
for path in glob.glob(os.path.join(os.environ['LIB_DIR'], '*secp256k1*')):
try:
FFI().dlopen(path)
return True

28
tests/samples.py

@ -1,12 +1,12 @@
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'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"
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
@ -19,9 +19,9 @@ PRIVATE_KEY_PEM = (
)
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'\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"
b'LQ[r\xeb\x10\xf1\xfd\x8f?\x03\xb4/J+%[\xfc\x9a\xa9\xe3'
)
PUBLIC_KEY_X = 27753912938952041417634381842191885283234814940840273460372041880794577257268
PUBLIC_KEY_Y = 53663045980837260634637807506183816949039230809110041985901491152185762425315
@ -30,7 +30,7 @@ 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"\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'
@ -38,13 +38,13 @@ MESSAGE = (
b'\x84\x1a?No\x10T\xf8\xb8\xd3\x18\xa4\xaf'
)
SIGNATURE = (
b"0E\x02!\x00\xee$\x1b\x0e@fa\xd4<\x17)\xa7\n\xd0\xd7\xef\x90\xcd\x13"
b'0E\x02!\x00\xee$\x1b\x0e@fa\xd4<\x17)\xa7\n\xd0\xd7\xef\x90\xcd\x13'
b"\xad`\xc1\x06[\xe0\x821\x96\xe29\x80'\x02 \r\x02\x13\xd2\xaf?\x92G"
b"\x80&8\x1cVz%2\xb0\x8a\xd0l\x0b4\x9c~\x93\x18\xad\xe4J\x9c-\n"
b'\x80&8\x1cVz%2\xb0\x8a\xd0l\x0b4\x9c~\x93\x18\xad\xe4J\x9c-\n'
)
RECOVERABLE_SIGNATURE = (
b"\xee$\x1b\x0e@fa\xd4<\x17)\xa7\n\xd0\xd7\xef\x90\xcd\x13"
b'\xee$\x1b\x0e@fa\xd4<\x17)\xa7\n\xd0\xd7\xef\x90\xcd\x13'
b"\xad`\xc1\x06[\xe0\x821\x96\xe29\x80'\r\x02\x13\xd2\xaf?"
b"\x92G\x80&8\x1cVz%2\xb0\x8a\xd0l\x0b4\x9c~\x93\x18\xad"
b"\xe4J\x9c-\n\x00"
b'\x92G\x80&8\x1cVz%2\xb0\x8a\xd0l\x0b4\x9c~\x93\x18\xad'
b'\xe4J\x9c-\n\x00'
)

3
tests/test_bench.py

@ -1,5 +1,6 @@
from coincurve import PrivateKey, PublicKey, verify_signature
from .samples import PRIVATE_KEY_BYTES, PUBLIC_KEY_COMPRESSED, MESSAGE, SIGNATURE
from .samples import MESSAGE, PRIVATE_KEY_BYTES, PUBLIC_KEY_COMPRESSED, SIGNATURE
def test_verify_signature_util(benchmark):

1
tests/test_ecdsa.py

@ -1,4 +1,5 @@
from coincurve.ecdsa import cdata_to_der, der_to_cdata
from .samples import SIGNATURE

12
tests/test_keys.py

@ -6,7 +6,9 @@ import pytest
from coincurve.ecdsa import deserialize_recoverable, recover
from coincurve.keys import PrivateKey, PublicKey
from coincurve.utils import bytes_to_int, int_to_bytes_padded, verify_signature
from .samples import (
MESSAGE,
PRIVATE_KEY_BYTES,
PRIVATE_KEY_DER,
PRIVATE_KEY_HEX,
@ -16,12 +18,10 @@ from .samples import (
PUBLIC_KEY_UNCOMPRESSED,
PUBLIC_KEY_X,
PUBLIC_KEY_Y,
MESSAGE,
SIGNATURE,
RECOVERABLE_SIGNATURE,
SIGNATURE,
)
G = PublicKey(
b'\x04y\xbef~\xf9\xdc\xbb\xacU\xa0b\x95\xce\x87\x0b\x07\x02\x9b'
b'\xfc\xdb-\xce(\xd9Y\xf2\x81[\x16\xf8\x17\x98H:\xdaw&\xa3\xc4e'
@ -140,3 +140,9 @@ class TestPublicKey:
point = G.multiply(x)
assert point.add(k) == G.multiply(int_to_bytes_padded((bytes_to_int(x) + bytes_to_int(k)) % n))
def test_combine(self):
a = PrivateKey().public_key
b = PrivateKey().public_key
assert PublicKey.combine_keys([a, b]) == a.combine([b])

8
tests/test_utils.py

@ -5,12 +5,10 @@ import pytest
from coincurve.utils import (
GROUP_ORDER,
ZERO,
bytes_to_hex,
bytes_to_int,
chunk_data,
der_to_pem,
get_valid_secret,
hex_to_bytes,
int_to_bytes,
int_to_bytes_padded,
pad_scalar,
@ -18,6 +16,7 @@ from coincurve.utils import (
validate_secret,
verify_signature,
)
from .samples import MESSAGE, PRIVATE_KEY_DER, PUBLIC_KEY_COMPRESSED, PUBLIC_KEY_UNCOMPRESSED, SIGNATURE
@ -61,11 +60,6 @@ class TestValidateSecret:
validate_secret(GROUP_ORDER)
def test_bytes_hex_conversion():
bytestr = b'\x00' + urandom(31)
assert hex_to_bytes(bytes_to_hex(bytestr)) == bytestr
def test_bytes_int_conversion():
bytestr = b'\x00' + urandom(31)
assert pad_scalar(int_to_bytes(bytes_to_int(bytestr))) == bytestr

80
tox.ini

@ -1,10 +1,16 @@
[tox]
skip_missing_interpreters = true
envlist =
py{27,35,36,37,38},
pypy3k,
bench,
style
3.7
3.8
3.9
3.10
pypy3
bench
lint
fmt
typing
docs
[testenv]
passenv = *
@ -19,13 +25,71 @@ commands =
[testenv:bench]
skip_install = true
envdir = {toxworkdir}/{env:TESTENV:bench}
envdir = {toxworkdir}/{env:PYTHON_VERSION:bench}
commands =
python -c "import shutil; shutil.move('coincurve', '_coincurve')"
pytest -v --benchmark-only --benchmark-sort=name --benchmark-cprofile=tottime
python -c "import shutil; shutil.move('_coincurve', 'coincurve')"
[testenv:style]
[testenv:lint]
envdir = {toxworkdir}/lint
skip_install = true
deps = black
commands = black --check --diff .
deps =
flake8>=3.9
flake8-bugbear>=20.1.4
flake8-quotes>=3.2.0
black>=21.12b0
isort[pyproject]>=5
commands =
flake8 .
black --check --diff .
isort --check-only --diff .
[testenv:fmt]
envdir = {[testenv:lint]envdir}
skip_install = true
deps = {[testenv:lint]deps}
commands =
isort .
black .
{[testenv:lint]commands}
[testenv:typing]
skip_install = true
deps =
mypy==0.790
commands =
mypy coincurve
[testenv:docs]
usedevelop = true
setenv =
; Pretty __repr__ for defaults of complex types
COINCURVE_BUILDING_DOCS=true
; Use a set timestamp for reproducible builds.
; See https://reproducible-builds.org/specs/source-date-epoch/
SOURCE_DATE_EPOCH=1580601600
deps =
mkdocs~=1.2.2
; theme
mkdocs-material~=7.3.1
; plugins
mkdocs-minify-plugin~=0.4.1
mkdocs-git-revision-date-localized-plugin~=0.10.0
mkdocstrings~=0.16.2
; Extensions
pymdown-extensions~=9.0
mkdocs-material-extensions~=1.0.3
mkpatcher~=1.0.2
; Necessary for syntax highlighting in code blocks
Pygments~=2.10.0
commands =
python -m mkdocs {posargs}
[testenv:docs-ci]
setenv = {[testenv:docs]setenv}
deps = {[testenv:docs]deps}
commands =
python -c "import shutil; shutil.move('coincurve', '_coincurve')"
{[testenv:docs]commands}
python -c "import shutil; shutil.move('_coincurve', 'coincurve')"

Loading…
Cancel
Save