diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 3db5f1c..95fb46f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -18,6 +18,5 @@ values = [bumpversion:part:devnum] [bumpversion:file:setup.py] -search = version='{current_version}', -replace = version='{new_version}', - +search = version="{current_version}", +replace = version="{new_version}", diff --git a/.circleci/config.yml b/.circleci/config.yml index ad12cfa..35d67e9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,4 @@ -version: 2.0 +version: 2.1 # heavily inspired by https://raw.githubusercontent.com/pinax/pinax-wiki/6bd2a99ab6f702e300d708532a6d1d9aa638b9f8/.circleci/config.yml @@ -6,389 +6,277 @@ common: &common working_directory: ~/repo steps: - checkout + - run: + name: merge pull request base + command: ./.circleci/merge_pr.sh + - run: + name: merge pull request base (2nd try) + command: ./.circleci/merge_pr.sh + when: on_fail + - run: + name: merge pull request base (3rd try) + command: ./.circleci/merge_pr.sh + when: on_fail - restore_cache: keys: - cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} - run: name: install dependencies - command: pip install --user tox + command: | + python -m pip install --upgrade pip + python -m pip install tox + - run: + name: install pre-commit + command: python -m pip install --progress-bar=off pre-commit - run: name: run tox - command: ~/.local/bin/tox -r + command: python -m tox run -r - save_cache: paths: + - .hypothesis - .tox - ~/.cache/pip - ~/.local - - ./eggs + key: cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} + +orbs: + win: circleci/windows@5.0.0 + +windows_steps: &windows_steps + executor: + name: win/default + shell: bash.exe + working_directory: C:\Users\circleci\project\eth-keys + steps: + - checkout + - restore_cache: + keys: + - cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} + - run: + name: install dependencies + command: | + python -m pip install --upgrade pip + python -m pip install tox + - run: + name: run tox + command: python -m tox run -r + - save_cache: + paths: + - .tox key: cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} jobs: - lint: - <<: *common - docker: - - image: circleci/python:3.6 - environment: - TOXENV: lint - py36-core: + docs: <<: *common docker: - - image: circleci/python:3.6 + - image: cimg/python:3.8 environment: - TOXENV: py36-core - py37-core: - <<: *common - docker: - - image: circleci/python:3.7 - environment: - TOXENV: py37-core + TOXENV: docs + py38-core: <<: *common docker: - - image: circleci/python:3.8 + - image: cimg/python:3.8 environment: TOXENV: py38-core py39-core: <<: *common docker: - - image: circleci/python:3.9 + - image: cimg/python:3.9 environment: TOXENV: py39-core py310-core: <<: *common docker: - - image: circleci/python:3.10 + - image: cimg/python:3.10 environment: TOXENV: py310-core - pypy3-core: - <<: *common - docker: - - image: pypy - environment: - TOXENV: pypy3-core - py36-backends-coincurve7: - <<: *common - docker: - - image: circleci/python:3.6 - environment: - TOXENV: py36-backends-coincurve7 - py37-backends-coincurve7: - <<: *common - docker: - - image: circleci/python:3.7 - environment: - TOXENV: py37-backends-coincurve7 - py38-backends-coincurve7: - <<: *common - docker: - - image: circleci/python:3.8 - environment: - TOXENV: py38-backends-coincurve7 - py39-backends-coincurve7: - <<: *common - docker: - - image: circleci/python:3.9 - environment: - TOXENV: py39-backends-coincurve7 - py310-backends-coincurve7: - <<: *common - docker: - - image: circleci/python:3.10 - environment: - TOXENV: py310-backends-coincurve7 - py36-backends-coincurve8: - <<: *common - docker: - - image: circleci/python:3.6 - environment: - TOXENV: py36-backends-coincurve8 - py37-backends-coincurve8: - <<: *common - docker: - - image: circleci/python:3.7 - environment: - TOXENV: py37-backends-coincurve8 - py38-backends-coincurve8: - <<: *common - docker: - - image: circleci/python:3.8 - environment: - TOXENV: py38-backends-coincurve8 - py39-backends-coincurve8: - <<: *common - docker: - - image: circleci/python:3.9 - environment: - TOXENV: py39-backends-coincurve8 - py310-backends-coincurve8: - <<: *common - docker: - - image: circleci/python:3.10 - environment: - TOXENV: py310-backends-coincurve8 - py36-backends-coincurve9: - <<: *common - docker: - - image: circleci/python:3.6 - environment: - TOXENV: py36-backends-coincurve9 - py37-backends-coincurve9: + py311-core: <<: *common docker: - - image: circleci/python:3.7 + - image: cimg/python:3.11 environment: - TOXENV: py37-backends-coincurve9 - py38-backends-coincurve9: - <<: *common - docker: - - image: circleci/python:3.8 - environment: - TOXENV: py38-backends-coincurve9 - py39-backends-coincurve9: - <<: *common - docker: - - image: circleci/python:3.9 - environment: - TOXENV: py39-backends-coincurve9 - py310-backends-coincurve9: - <<: *common - docker: - - image: circleci/python:3.10 - environment: - TOXENV: py310-backends-coincurve9 - py36-backends-coincurve10: - <<: *common - docker: - - image: circleci/python:3.6 - environment: - TOXENV: py36-backends-coincurve10 - py37-backends-coincurve10: - <<: *common - docker: - - image: circleci/python:3.7 - environment: - TOXENV: py37-backends-coincurve10 - py38-backends-coincurve10: - <<: *common - docker: - - image: circleci/python:3.8 - environment: - TOXENV: py38-backends-coincurve10 - py39-backends-coincurve10: - <<: *common - docker: - - image: circleci/python:3.9 - environment: - TOXENV: py39-backends-coincurve10 - py310-backends-coincurve10: + TOXENV: py311-core + + py38-lint: <<: *common docker: - - image: circleci/python:3.10 + - image: cimg/python:3.8 environment: - TOXENV: py310-backends-coincurve10 - py36-backends-coincurve11: + TOXENV: py38-lint + py39-lint: <<: *common docker: - - image: circleci/python:3.6 + - image: cimg/python:3.9 environment: - TOXENV: py36-backends-coincurve11 - py37-backends-coincurve11: + TOXENV: py39-lint + py310-lint: <<: *common docker: - - image: circleci/python:3.7 + - image: cimg/python:3.10 environment: - TOXENV: py37-backends-coincurve11 - py38-backends-coincurve11: + TOXENV: py310-lint + py311-lint: <<: *common docker: - - image: circleci/python:3.8 + - image: cimg/python:3.11 environment: - TOXENV: py38-backends-coincurve11 - py39-backends-coincurve11: + TOXENV: py311-lint + + py38-wheel: <<: *common docker: - - image: circleci/python:3.9 + - image: cimg/python:3.8 environment: - TOXENV: py39-backends-coincurve11 - py310-backends-coincurve11: + TOXENV: py38-wheel + py39-wheel: <<: *common docker: - - image: circleci/python:3.10 + - image: cimg/python:3.9 environment: - TOXENV: py310-backends-coincurve11 - py36-backends-coincurve12: + TOXENV: py39-wheel + py310-wheel: <<: *common docker: - - image: circleci/python:3.6 + - image: cimg/python:3.10 environment: - TOXENV: py36-backends-coincurve12 - py37-backends-coincurve12: + TOXENV: py310-wheel + py311-wheel: <<: *common docker: - - image: circleci/python:3.7 + - image: cimg/python:3.11 environment: - TOXENV: py37-backends-coincurve12 + TOXENV: py311-wheel + + py311-wheel-windows: + <<: *windows_steps + environment: + TOXENV: py311-wheel-windows + py38-backends-coincurve12: <<: *common docker: - - image: circleci/python:3.8 + - image: cimg/python:3.8 environment: TOXENV: py38-backends-coincurve12 - py39-backends-coincurve12: - <<: *common - docker: - - image: circleci/python:3.9 - environment: - TOXENV: py39-backends-coincurve12 - py310-backends-coincurve12: - <<: *common - docker: - - image: circleci/python:3.10 - environment: - TOXENV: py310-backends-coincurve12 - py36-backends-coincurve13: - <<: *common - docker: - - image: circleci/python:3.6 - environment: - TOXENV: py36-backends-coincurve13 - py37-backends-coincurve13: + py38-backends-coincurve13: <<: *common docker: - - image: circleci/python:3.7 + - image: cimg/python:3.8 environment: - TOXENV: py37-backends-coincurve13 - py38-backends-coincurve13: + TOXENV: py38-backends-coincurve13 + py38-backends-coincurve14: <<: *common docker: - - image: circleci/python:3.8 + - image: cimg/python:3.8 environment: - TOXENV: py38-backends-coincurve13 - py39-backends-coincurve13: + TOXENV: py38-backends-coincurve14 + py38-backends-coincurve15: <<: *common docker: - - image: circleci/python:3.9 + - image: cimg/python:3.8 environment: - TOXENV: py39-backends-coincurve13 - py310-backends-coincurve13: + TOXENV: py38-backends-coincurve15 + py39-backends-coincurve15: <<: *common docker: - - image: circleci/python:3.10 + - image: cimg/python:3.9 environment: - TOXENV: py310-backends-coincurve13 - py36-backends-coincurve14: + TOXENV: py39-backends-coincurve15 + + py38-backends-coincurve16: <<: *common docker: - - image: circleci/python:3.6 + - image: cimg/python:3.8 environment: - TOXENV: py36-backends-coincurve14 - py37-backends-coincurve14: + TOXENV: py38-backends-coincurve16 + py39-backends-coincurve16: <<: *common docker: - - image: circleci/python:3.7 + - image: cimg/python:3.9 environment: - TOXENV: py37-backends-coincurve14 - py38-backends-coincurve14: + TOXENV: py39-backends-coincurve16 + py310-backends-coincurve16: <<: *common docker: - - image: circleci/python:3.8 + - image: cimg/python:3.10 environment: - TOXENV: py38-backends-coincurve14 - py39-backends-coincurve14: + TOXENV: py310-backends-coincurve16 + + py38-backends-coincurve17: <<: *common docker: - - image: circleci/python:3.9 + - image: cimg/python:3.8 environment: - TOXENV: py39-backends-coincurve14 - py310-backends-coincurve14: + TOXENV: py38-backends-coincurve17 + py39-backends-coincurve17: <<: *common docker: - - image: circleci/python:3.10 + - image: cimg/python:3.9 environment: - TOXENV: py310-backends-coincurve14 - py36-backends-coincurve15: + TOXENV: py39-backends-coincurve17 + py310-backends-coincurve17: <<: *common docker: - - image: circleci/python:3.6 + - image: cimg/python:3.10 environment: - TOXENV: py36-backends-coincurve15 - py37-backends-coincurve15: + TOXENV: py310-backends-coincurve17 + + py38-backends-coincurve18: <<: *common docker: - - image: circleci/python:3.7 + - image: cimg/python:3.8 environment: - TOXENV: py37-backends-coincurve15 - py38-backends-coincurve15: + TOXENV: py38-backends-coincurve18 + py39-backends-coincurve18: <<: *common docker: - - image: circleci/python:3.8 + - image: cimg/python:3.9 environment: - TOXENV: py38-backends-coincurve15 - py39-backends-coincurve15: + TOXENV: py39-backends-coincurve18 + py310-backends-coincurve18: <<: *common docker: - - image: circleci/python:3.9 + - image: cimg/python:3.10 environment: - TOXENV: py39-backends-coincurve15 - py310-backends-coincurve15: + TOXENV: py310-backends-coincurve18 + py311-backends-coincurve18: <<: *common docker: - - image: circleci/python:3.10 + - image: cimg/python:3.11 environment: - TOXENV: py310-backends-coincurve15 + TOXENV: py311-backends-coincurve18 + workflows: version: 2 test: jobs: - - lint - - py36-core - - py37-core + - docs - py38-core - py39-core - py310-core - - pypy3-core - - py36-backends-coincurve7 - - py37-backends-coincurve7 - - py38-backends-coincurve7 - - py39-backends-coincurve7 - - py310-backends-coincurve7 - - py36-backends-coincurve8 - - py37-backends-coincurve8 - - py38-backends-coincurve8 - - py39-backends-coincurve8 - - py310-backends-coincurve8 - - py36-backends-coincurve9 - - py37-backends-coincurve9 - - py38-backends-coincurve9 - - py39-backends-coincurve9 - - py310-backends-coincurve9 - - py36-backends-coincurve10 - - py37-backends-coincurve10 - - py38-backends-coincurve10 - - py39-backends-coincurve10 - - py310-backends-coincurve10 - - py36-backends-coincurve11 - - py37-backends-coincurve11 - - py38-backends-coincurve11 - - py39-backends-coincurve11 - - py310-backends-coincurve11 - - py36-backends-coincurve12 - - py37-backends-coincurve12 + - py311-core + - py38-lint + - py39-lint + - py310-lint + - py311-lint + - py38-wheel + - py39-wheel + - py310-wheel + - py311-wheel + - py311-wheel-windows - py38-backends-coincurve12 - - py39-backends-coincurve12 - - py310-backends-coincurve12 - - py36-backends-coincurve13 - - py37-backends-coincurve13 - py38-backends-coincurve13 - - py39-backends-coincurve13 - - py310-backends-coincurve13 - - py36-backends-coincurve14 - - py37-backends-coincurve14 - py38-backends-coincurve14 - - py39-backends-coincurve14 - - py310-backends-coincurve14 - - py36-backends-coincurve15 - - py37-backends-coincurve15 - py38-backends-coincurve15 - py39-backends-coincurve15 - - py310-backends-coincurve15 + - py38-backends-coincurve16 + - py39-backends-coincurve16 + - py310-backends-coincurve16 + - py38-backends-coincurve17 + - py39-backends-coincurve17 + - py310-backends-coincurve17 + - py38-backends-coincurve18 + - py39-backends-coincurve18 + - py310-backends-coincurve18 + - py311-backends-coincurve18 diff --git a/.circleci/merge_pr.sh b/.circleci/merge_pr.sh new file mode 100755 index 0000000..91eb47c --- /dev/null +++ b/.circleci/merge_pr.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +if [[ -n "${CIRCLE_PR_NUMBER}" ]]; then + PR_INFO_URL=https://api.github.com/repos/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pulls/$CIRCLE_PR_NUMBER + PR_BASE_BRANCH=$(curl -L "$PR_INFO_URL" | python -c 'import json, sys; obj = json.load(sys.stdin); sys.stdout.write(obj["base"]["ref"])') + git fetch origin +"$PR_BASE_BRANCH":circleci/pr-base + # We need these config values or git complains when creating the + # merge commit + git config --global user.name "Circle CI" + git config --global user.email "circleci@example.com" + git merge --no-edit circleci/pr-base +fi diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 9b05d23..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,16 +0,0 @@ -* Version: x.x.x -* Python: 2.7/3.4/3.5 -* OS: osx/linux/win - - -### What was wrong? - -Please include any of the following that are applicable: - -* The code which produced the error -* The full output of the error - - -### How can it be fixed? - -Fill this section in if you know how this could or should be fixed. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000..f15082f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,72 @@ +name: Bug Report +description: File a bug report +labels: ["bug"] +body: + - type: markdown + attributes: + value: "## What was wrong" + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us what you expected to happen + validations: + required: true + - type: textarea + id: code-that-caused + attributes: + label: Code that produced the error + description: Formats to Python, no backticks needed + render: python + validations: + required: false + - type: textarea + id: error-output + attributes: + label: Full error output + description: Formats to shell, no backticks needed + render: shell + validations: + required: false + - type: markdown + attributes: + value: "## Potential Solutions" + - type: textarea + id: how-to-fix + attributes: + label: Fill this section in if you know how this could or should be fixed + description: Include any relevant examples or reference material + validations: + required: false + - type: input + id: lib-version + attributes: + label: eth-keys Version + description: Which version of eth-keys are you using? + placeholder: x.x.x + validations: + required: false + - type: input + id: py-version + attributes: + label: Python Version + description: Which version of Python are you using? + placeholder: x.x.x + validations: + required: false + - type: input + id: os + attributes: + label: Operating System + description: Which operating system are you using? + placeholder: osx/linux/win + validations: + required: false + - type: textarea + id: pip-freeze + attributes: + label: Output from `pip freeze` + description: Run `python -m pip freeze` and paste the output below + render: shell + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 0000000..2987a4e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Questions about using eth-keys? + url: https://discord.gg/GHryRvPB84 + about: You can ask and answer usage questions on the Ethereum Python Community Discord diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 0000000..041c8fd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,11 @@ +name: Feature Request +description: Request a new feature +labels: ["feature_request"] +body: + - type: textarea + id: feature-description + attributes: + label: What feature should we add? + description: Include any relevant examples or reference material + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index a9937bb..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,11 +0,0 @@ -### What was wrong? - - - -### How was it fixed? - - - -#### Cute Animal Picture - -![Cute animal picture]() diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..6f2a8d5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ +### What was wrong? + +Related to Issue # +Closes # + +### How was it fixed? + +### Todo: + +- [ ] Clean up commit history + +- [ ] Add or update documentation related to these changes + +- [ ] Add entry to the [release notes](https://github.com/ethereum/eth-keys/blob/main/newsfragments/README.md) + +#### Cute Animal Picture + +![Put a link to a cute animal picture inside the parenthesis-->](<>) diff --git a/.gitignore b/.gitignore index d8db78e..57eac52 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,9 @@ *.egg-info dist build -.eggs +.build eggs +.eggs parts bin var @@ -18,7 +19,9 @@ develop-eggs .installed.cfg lib lib64 -venv +pip-wheel-metadata +venv* +.venv* # Installer logs pip-log.txt @@ -42,64 +45,65 @@ output/*/index.html # Sphinx docs/_build +docs/modules.rst +docs/*.internal.rst +docs/*.utils.rst +docs/*._utils.* -# Hypothese Property base testing +# Blockchain +chains + +# Hypothesis Property base testing .hypothesis # tox/pytest cache .cache -.mypy_cache/ +.pytest_cache + +# pycache +__pycache__/ # Test output logs logs -### JetBrains template + +# VIM temp files +*.sw[op] + +# mypy +.mypy_cache + +# macOS +.DS_Store + +# pyenv +.python-version + +# vs-code +.vscode + # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff: -.idea/workspace.xml -.idea/tasks.xml -.idea/dictionaries -.idea/vcs.xml -.idea/jsLibraryMappings.xml - -# Sensitive or high-churn files: -.idea/dataSources.ids -.idea/dataSources.xml -.idea/dataSources.local.xml -.idea/sqlDataSources.xml -.idea/dynamic.xml -.idea/uiDesigner.xml - -# Gradle: -.idea/gradle.xml -.idea/libraries - -# Mongo Explorer plugin: -.idea/mongoSettings.xml +# For a more precise, explicit template, see: +# https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +## General +.idea/* +.idea_modules/* ## File-based project format: *.iws -## Plugin-specific files: +## IntelliJ +out/ -# IntelliJ -/out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ +## Plugin-specific files: -# JIRA plugin +### JIRA plugin atlassian-ide-plugin.xml -# Crashlytics plugin (for Android Studio and IntelliJ) +### Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties -# test fixtures -fixtures/* - -# editors -*.swp +# END JetBrains section diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..82b01b7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,50 @@ +exclude: '.project-template|docs/conf.py|.bumpversion.cfg' +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-yaml + - id: check-toml + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 + hooks: + - id: pyupgrade + args: [--py38-plus] +- repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black +- repo: https://github.com/PyCQA/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + additional_dependencies: + - flake8-bugbear==23.9.16 + exclude: setup.py +- repo: https://github.com/PyCQA/autoflake + rev: v2.2.1 + hooks: + - id: autoflake +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort +- repo: https://github.com/pycqa/pydocstyle + rev: 6.3.0 + hooks: + - id: pydocstyle + additional_dependencies: + - tomli # required until >= python311 +- repo: https://github.com/executablebooks/mdformat + rev: 0.7.17 + hooks: + - id: mdformat + additional_dependencies: + - mdformat-gfm +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.5.1 + hooks: + - id: mypy + exclude: tests/ diff --git a/.project-template/fill_template_vars.py b/.project-template/fill_template_vars.py new file mode 100644 index 0000000..52ceb02 --- /dev/null +++ b/.project-template/fill_template_vars.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +import os +import sys +import re +from pathlib import Path + + +def _find_files(project_root): + path_exclude_pattern = r"\.git($|\/)|venv|_build" + file_exclude_pattern = r"fill_template_vars\.py|\.swp$" + filepaths = [] + for dir_path, _dir_names, file_names in os.walk(project_root): + if not re.search(path_exclude_pattern, dir_path): + for file in file_names: + if not re.search(file_exclude_pattern, file): + filepaths.append(str(Path(dir_path, file))) + + return filepaths + + +def _replace(pattern, replacement, project_root): + print(f"Replacing values: {pattern}") + for file in _find_files(project_root): + try: + with open(file) as f: + content = f.read() + content = re.sub(pattern, replacement, content) + with open(file, "w") as f: + f.write(content) + except UnicodeDecodeError: + pass + + +def main(): + project_root = Path(os.path.realpath(sys.argv[0])).parent.parent + + module_name = input("What is your python module name? ") + + pypi_input = input(f"What is your pypi package name? (default: {module_name}) ") + pypi_name = pypi_input or module_name + + repo_input = input(f"What is your github project name? (default: {pypi_name}) ") + repo_name = repo_input or pypi_name + + rtd_input = input( + f"What is your readthedocs.org project name? (default: {pypi_name}) " + ) + rtd_name = rtd_input or pypi_name + + project_input = input( + f"What is your project name (ex: at the top of the README)? (default: {repo_name}) " + ) + project_name = project_input or repo_name + + short_description = input("What is a one-liner describing the project? ") + + _replace("", module_name, project_root) + _replace("", pypi_name, project_root) + _replace("", repo_name, project_root) + _replace("", rtd_name, project_root) + _replace("", project_name, project_root) + _replace("", short_description, project_root) + + os.makedirs(project_root / module_name, exist_ok=True) + Path(project_root / module_name / "__init__.py").touch() + Path(project_root / module_name / "py.typed").touch() + + +if __name__ == "__main__": + main() diff --git a/.project-template/refill_template_vars.py b/.project-template/refill_template_vars.py new file mode 100644 index 0000000..03ab7c0 --- /dev/null +++ b/.project-template/refill_template_vars.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +import os +import sys +from pathlib import Path +import subprocess + + +def main(): + template_dir = Path(os.path.dirname(sys.argv[0])) + template_vars_file = template_dir / "template_vars.txt" + fill_template_vars_script = template_dir / "fill_template_vars.py" + + with open(template_vars_file, "r") as input_file: + content_lines = input_file.readlines() + + process = subprocess.Popen( + [sys.executable, str(fill_template_vars_script)], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + for line in content_lines: + process.stdin.write(line) + process.stdin.flush() + + stdout, stderr = process.communicate() + + if process.returncode != 0: + print(f"Error occurred: {stderr}") + sys.exit(1) + + print(stdout) + + +if __name__ == "__main__": + main() diff --git a/.project-template/template_vars.txt b/.project-template/template_vars.txt new file mode 100644 index 0000000..3fd6b0f --- /dev/null +++ b/.project-template/template_vars.txt @@ -0,0 +1,6 @@ +eth_keys +eth-keys +eth-keys +eth-keys +eth-keys +Common API for Ethereum key operations diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 91cb816..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.pythonPath": "venv/bin/python" -} \ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG.rst similarity index 100% rename from CHANGELOG rename to CHANGELOG.rst diff --git a/LICENSE b/LICENSE index 5e8bed1..c98bff5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2017 Piper Merriam +Copyright (c) 2017-2023 The Ethereum Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in index 1109e8d..a122460 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,11 @@ include LICENSE -include VERSION include README.md -include requirements.txt + +recursive-include tests * + +global-include *.pyi recursive-exclude * __pycache__ recursive-exclude * *.py[co] +prune .tox +prune venv* diff --git a/Makefile b/Makefile index 5207d70..48faed9 100644 --- a/Makefile +++ b/Makefile @@ -1,45 +1,71 @@ -.PHONY: clean-pyc clean-build +CURRENT_SIGN_SETTING := $(shell git config commit.gpgSign) + +.PHONY: clean-pyc clean-build docs help: @echo "clean-build - remove build artifacts" @echo "clean-pyc - remove Python file artifacts" - @echo "lint - check style with flake8" + @echo "lint - fix linting issues with pre-commit" @echo "test - run tests quickly with the default Python" - @echo "testall - run tests on every Python version with tox" - @echo "release - package and upload a release" - @echo "sdist - package" + @echo "docs - view draft of newsfragments to be added to CHANGELOG" + @echo "notes - consume towncrier newsfragments/ and update CHANGELOG" + @echo "release - package and upload a release (does not run notes target)" + @echo "dist - package" clean: clean-build clean-pyc clean-build: rm -fr build/ rm -fr dist/ - rm -fr *.egg-info clean-pyc: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -rf {} + lint: - tox -e lint + @pre-commit run --all-files --show-diff-on-failure || ( \ + echo "\n\n\n * pre-commit should have fixed the errors above. Running again to make sure everything is good..." \ + && pre-commit run --all-files --show-diff-on-failure \ + ) test: - py.test tests + pytest tests + +docs: + python ./newsfragments/validate_files.py + towncrier build --draft --version preview + +check-bump: +ifndef bump + $(error bump must be set, typically: major, minor, patch, or devnum) +endif -test-all: - tox +notes: check-bump + # Let UPCOMING_VERSION be the version that is used for the current bump + $(eval UPCOMING_VERSION=$(shell bumpversion $(bump) --dry-run --list | grep new_version= | sed 's/new_version=//g')) + # Now generate the release notes to have them included in the release commit + towncrier build --yes --version $(UPCOMING_VERSION) + # Before we bump the version, make sure that the towncrier-generated docs will build + make build-docs + git commit -m "Compile release notes for v$(UPCOMING_VERSION)" -release: clean +release: check-bump clean + # require that upstream is configured for ethereum/eth-keys + @git remote -v | grep -E "upstream\tgit@github.com:ethereum/eth-keys.git \(push\)|upstream\thttps://(www.)?github.com/ethereum/eth-keys \(push\)" + # verify that docs build correctly + ./newsfragments/validate_files.py is-empty + make build-docs CURRENT_SIGN_SETTING=$(git config commit.gpgSign) git config commit.gpgSign true bumpversion $(bump) git push upstream && git push upstream --tags - python setup.py sdist bdist_wheel + python -m build twine upload dist/* git config commit.gpgSign "$(CURRENT_SIGN_SETTING)" -sdist: clean - python setup.py sdist bdist_wheel +dist: clean + python -m build ls -l dist diff --git a/README.md b/README.md index ddda3a1..7e41ff2 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,22 @@ -# Ethereum Keys +# eth-keys +[![Join the conversation on Discord](https://img.shields.io/discord/809793915578089484?color=blue&label=chat&logo=discord&logoColor=white)](https://discord.gg/GHryRvPB84) +[![Build Status](https://circleci.com/gh/ethereum/eth-keys.svg?style=shield)](https://circleci.com/gh/ethereum/eth-keys) +[![PyPI version](https://badge.fury.io/py/eth-keys.svg)](https://badge.fury.io/py/eth-keys) +[![Python versions](https://img.shields.io/pypi/pyversions/eth-keys.svg)](https://pypi.python.org/pypi/eth-keys) -A common API for Ethereum key operations with pluggable backends. - +Common API for Ethereum key operations > This library and repository was previously located at https://github.com/pipermerriam/ethereum-keys. It was transferred to the Ethereum foundation github in November 2017 and renamed to `eth-keys`. The PyPi package was also renamed from `ethereum-keys` to `eth-keys`. -## Installation - -```sh -pip install eth-keys -``` - -## Development - -```sh -pip install -e .[dev] -``` - - -### Running the tests - -You can run the tests with: - -```sh -py.test tests -``` - -Or you can install `tox` to run the full test suite. - - -### Releasing +Read more in the documentation below. [View the change log](https://github.com/ethereum/eth-keys/blob/main/CHANGELOG.rst). -Pandoc is required for transforming the markdown README to the proper format to -render correctly on pypi. - -For Debian-like systems: - -``` -apt install pandoc -``` - -Or on OSX: +## Quickstart ```sh -brew install pandoc +python -m pip install eth-keys ``` -To release a new version: - -```sh -make release bump=$$VERSION_PART_TO_BUMP$$ -``` - - -#### How to bumpversion - -The version format for this repo is `{major}.{minor}.{patch}` for stable, and -`{major}.{minor}.{patch}-{stage}.{devnum}` for unstable (`stage` can be alpha or beta). - -To issue the next version in line, specify which part to bump, -like `make release bump=minor` or `make release bump=devnum`. - -If you are in a beta version, `make release bump=stage` will switch to a stable. - -To issue an unstable version when the current version is stable, specify the -new version explicitly, like `make release bump="--new-version 2.0.0-alpha.1 devnum"` - - - -## QuickStart - ```python >>> from eth_keys import keys >>> pk = keys.PrivateKey(b'\x01' * 32) @@ -89,7 +35,6 @@ True True ``` - ## Documentation ### `KeyAPI(backend=None)` @@ -99,8 +44,8 @@ libary. The object takes a single optional argument in its constructor which designates what backend will be used for eliptical curve cryptography operations. The built-in backends are: -* `eth_keys.backends.NativeECCBackend`: A pure python implementation of the ECC operations. -* `eth_keys.backends.CoinCurveECCBackend`: Uses the [`coincurve`](https://github.com/ofek/coincurve) library for ECC operations. +- `eth_keys.backends.NativeECCBackend`: A pure python implementation of the ECC operations. +- `eth_keys.backends.CoinCurveECCBackend`: Uses the [`coincurve`](https://github.com/ofek/coincurve) library for ECC operations. By default, `eth-keys` will *try* to use the `CoinCurveECCBackend`, falling back to the `NativeECCBackend` if the `coincurve` library is not @@ -110,9 +55,9 @@ available. The `backend` argument can be given in any of the following forms. -* Instance of the backend class -* The backend class -* String with the dot-separated import path for the backend class. +- Instance of the backend class +- The backend class +- String with the dot-separated import path for the backend class. ```python >>> from eth_keys import KeyAPI @@ -134,56 +79,50 @@ to the desired backend. >>> os.environ['ECC_BACKEND_CLASS'] = 'eth_keys.backends.CoinCurveECCBackend' ``` - ### `KeyAPI.ecdsa_sign(message_hash, private_key) -> Signature` This method returns a signature for the given `message_hash`, signed by the provided `private_key`. -* `message_hash`: **must** be a byte string of length 32 -* `private_key`: **must** be an instance of `PrivateKey` - +- `message_hash`: **must** be a byte string of length 32 +- `private_key`: **must** be an instance of `PrivateKey` ### `KeyAPI.ecdsa_verify(message_hash, signature, public_key) -> bool` Returns `True` or `False` based on whether the provided `signature` is a valid signature for the provided `message_hash` and `public_key`. -* `message_hash`: **must** be a byte string of length 32 -* `signature`: **must** be an instance of `Signature` -* `public_key`: **must** be an instance of `PublicKey` - +- `message_hash`: **must** be a byte string of length 32 +- `signature`: **must** be an instance of `Signature` +- `public_key`: **must** be an instance of `PublicKey` ### `KeyAPI.ecdsa_recover(message_hash, signature) -> PublicKey` Returns the `PublicKey` instances recovered from the given `signature` and `message_hash`. -* `message_hash`: **must** be a byte string of length 32 -* `signature`: **must** be an instance of `Signature` - +- `message_hash`: **must** be a byte string of length 32 +- `signature`: **must** be an instance of `Signature` ### `KeyAPI.private_key_to_public_key(private_key) -> PublicKey` Returns the `PublicKey` instances computed from the given `private_key` instance. -* `private_key`: **must** be an instance of `PublicKey` - +- `private_key`: **must** be an instance of `PublicKey` ### Common APIs for `PublicKey`, `PrivateKey` and `Signature` There is a common API for the following objects. -* `PublicKey` -* `PrivateKey` -* `Signature` +- `PublicKey` +- `PrivateKey` +- `Signature` Each of these objects has all of the following APIs. -* `obj.to_bytes()`: Returns the object in it's canonical `bytes` serialization. -* `obj.to_hex()`: Returns a text string of the hex encoded canonical representation. - +- `obj.to_bytes()`: Returns the object in it's canonical `bytes` serialization. +- `obj.to_hex()`: Returns a text string of the hex encoded canonical representation. ### `KeyAPI.PublicKey(public_key_bytes)` @@ -195,101 +134,86 @@ The `PublicKey` class takes a single argument which must be a bytes string with The following methods are available: - #### `PublicKey.from_compressed_bytes(compressed_bytes) -> PublicKey` This `classmethod` returns a new `PublicKey` instance computed from its compressed representation. -* `compressed_bytes` **must** be a byte string of length 33 starting with `\x02` or `\x03`. - +- `compressed_bytes` **must** be a byte string of length 33 starting with `\x02` or `\x03`. #### `PublicKey.from_private(private_key) -> PublicKey` This `classmethod` returns a new `PublicKey` instance computed from the -given `private_key`. - -* `private_key` may either be a byte string of length 32 or an instance of the `KeyAPI.PrivateKey` class. +given `private_key`. +- `private_key` may either be a byte string of length 32 or an instance of the `KeyAPI.PrivateKey` class. #### `PublicKey.recover_from_msg(message, signature) -> PublicKey` This `classmethod` returns a new `PublicKey` instance computed from the provided `message` and `signature`. -* `message` **must** be a byte string -* `signature` **must** be an instance of `KeyAPI.Signature` - +- `message` **must** be a byte string +- `signature` **must** be an instance of `KeyAPI.Signature` #### `PublicKey.recover_from_msg_hash(message_hash, signature) -> PublicKey` Same as `PublicKey.recover_from_msg` except that `message_hash` should be the Keccak hash of the `message`. - #### `PublicKey.verify_msg(message, signature) -> bool` This method returns `True` or `False` based on whether the signature is a valid for the given message. - #### `PublicKey.verify_msg_hash(message_hash, signature) -> bool` Same as `PublicKey.verify_msg` except that `message_hash` should be the Keccak hash of the `message`. - #### `PublicKey.to_compressed_bytes() -> bytes` Returns the compressed representation of this public key. - #### `PublicKey.to_address() -> text` Returns the hex encoded ethereum address for this public key. - #### `PublicKey.to_checksum_address() -> text` Returns the ERC55 checksum formatted ethereum address for this public key. - #### `PublicKey.to_canonical_address() -> bytes` Returns the 20-byte representation of the ethereum address for this public key. - ### `KeyAPI.PrivateKey(private_key_bytes)` The `PrivateKey` class takes a single argument which must be a bytes string with length 32. The following methods and properties are available - #### `PrivateKey.public_key` This *property* holds the `PublicKey` instance coresponding to this private key. - #### `PrivateKey.sign_msg(message) -> Signature` This method returns a signature for the given `message` in the form of a `Signature` instance -* `message` **must** be a byte string. - +- `message` **must** be a byte string. #### `PrivateKey.sign_msg_hash(message_hash) -> Signature` Same as `PrivateKey.sign` except that `message_hash` should be the Keccak hash of the `message`. - ### `KeyAPI.Signature(signature_bytes=None, vrs=None)` The `Signature` class can be instantiated in one of two ways. -* `signature_bytes`: a bytes string with length 65. -* `vrs`: a 3-tuple composed of the integers `v`, `r`, and `s`. +- `signature_bytes`: a bytes string with length 65. +- `vrs`: a 3-tuple composed of the integers `v`, `r`, and `s`. > Note: If using the `signature_bytes` to instantiate, the byte string should be encoded as `r_bytes | s_bytes | v_bytes` where `|` represents concatenation. `r_bytes` and `s_bytes` should be 32 bytes in length. `v_bytes` should be a single byte `\x00` or `\x01`. @@ -297,55 +221,46 @@ Signatures are expected to use `1` or `0` for their `v` value. The following methods and properties are available - #### `Signature.v` This property returns the `v` value from the signature as an integer. - #### `Signature.r` This property returns the `r` value from the signature as an integer. - #### `Signature.s` This property returns the `s` value from the signature as an integer. - #### `Signature.vrs` This property returns a 3-tuple of `(v, r, s)`. - #### `Signature.verify_msg(message, public_key) -> bool` This method returns `True` or `False` based on whether the signature is a valid for the given public key. -* `message`: **must** be a byte string. -* `public_key`: **must** be an instance of `PublicKey` - +- `message`: **must** be a byte string. +- `public_key`: **must** be an instance of `PublicKey` #### `Signature.verify_msg_hash(message_hash, public_key) -> bool` Same as `Signature.verify_msg` except that `message_hash` should be the Keccak hash of the `message`. - #### `Signature.recover_public_key_from_msg(message) -> PublicKey` This method returns a `PublicKey` instance recovered from the signature. -* `message`: **must** be a byte string. - +- `message`: **must** be a byte string. #### `Signature.recover_public_key_from_msg_hash(message_hash) -> PublicKey` Same as `Signature.recover_public_key_from_msg` except that `message_hash` should be the Keccak hash of the `message`. - ### Exceptions #### `eth_api.exceptions.ValidationError` @@ -354,8 +269,58 @@ This error is raised during instantaition of any of the `PublicKey`, `PrivateKey` or `Signature` classes if their constructor parameters are invalid. - #### `eth_api.exceptions.BadSignature` This error is raised from any of the `recover` or `verify` methods involving signatures if the signature is invalid. + +## Developer Setup + +If you would like to hack on eth-keys, please check out the [Snake Charmers +Tactical Manual](https://github.com/ethereum/snake-charmers-tactical-manual) +for information on how we do: + +- Testing +- Pull Requests +- Documentation + +We use [pre-commit](https://pre-commit.com/) to maintain consistent code style. Once +installed, it will run automatically with every commit. You can also run it manually +with `make lint`. If you need to make a commit that skips the `pre-commit` checks, you +can do so with `git commit --no-verify`. + +### Development Environment Setup + +You can set up your dev environment with: + +```sh +git clone git@github.com:ethereum/eth-keys.git +cd eth-keys +virtualenv -p python3 venv +. venv/bin/activate +python -m pip install -e ".[dev]" +pre-commit install +``` + +### Release setup + +To release a new version: + +```sh +make release bump=$$VERSION_PART_TO_BUMP$$ +``` + +#### How to bumpversion + +The version format for this repo is `{major}.{minor}.{patch}` for stable, and +`{major}.{minor}.{patch}-{stage}.{devnum}` for unstable (`stage` can be alpha or beta). + +To issue the next version in line, specify which part to bump, +like `make release bump=minor` or `make release bump=devnum`. This is typically done from the +main branch, except when releasing a beta (in which case the beta is released from main, +and the previous stable branch is released from said branch). + +If you are in a beta version, `make release bump=stage` will switch to a stable. + +To issue an unstable version when the current version is stable, specify the +new version explicitly, like `make release bump="--new-version 4.0.0-alpha.1 devnum"` diff --git a/eth_keys/__init__.py b/eth_keys/__init__.py index e02dc18..8601480 100644 --- a/eth_keys/__init__.py +++ b/eth_keys/__init__.py @@ -1,18 +1,10 @@ -from __future__ import absolute_import - -import sys -import warnings - - -if sys.version_info.major < 3: - warnings.simplefilter('always', DeprecationWarning) - warnings.warn(DeprecationWarning( - "The `eth-keys` library is dropping support for Python 2. Upgrade to Python 3." - )) - warnings.resetwarnings() - +from importlib.metadata import ( + version as __version, +) -from .main import ( # noqa: F401 +from .main import ( KeyAPI, lazy_key_api as keys, ) + +__version__ = __version("eth-keys") diff --git a/eth_keys/backends/__init__.py b/eth_keys/backends/__init__.py index aaa3683..d19af45 100644 --- a/eth_keys/backends/__init__.py +++ b/eth_keys/backends/__init__.py @@ -1,31 +1,35 @@ -from __future__ import absolute_import - import os -from typing import Type +from typing import ( + Type, +) from eth_keys.utils.module_loading import ( import_string, ) -from .base import BaseECCBackend # noqa: F401 -from .coincurve import ( # noqa: F401 +from .base import ( + BaseECCBackend, +) +from .coincurve import ( CoinCurveECCBackend, is_coincurve_available, ) -from .native import NativeECCBackend # noqa: F401 +from .native import ( + NativeECCBackend, +) def get_default_backend_class() -> str: if is_coincurve_available(): - return 'eth_keys.backends.CoinCurveECCBackend' + return "eth_keys.backends.CoinCurveECCBackend" else: - return 'eth_keys.backends.NativeECCBackend' + return "eth_keys.backends.NativeECCBackend" def get_backend_class(import_path: str = None) -> Type[BaseECCBackend]: if import_path is None: import_path = os.environ.get( - 'ECC_BACKEND_CLASS', + "ECC_BACKEND_CLASS", get_default_backend_class(), ) return import_string(import_path) diff --git a/eth_keys/backends/base.py b/eth_keys/backends/base.py index 8991f95..0b6356d 100644 --- a/eth_keys/backends/base.py +++ b/eth_keys/backends/base.py @@ -1,5 +1,3 @@ -from typing import Any # noqa: F401 - from eth_keys.datatypes import ( BaseSignature, NonRecoverableSignature, @@ -9,36 +7,28 @@ ) -class BaseECCBackend(object): - def ecdsa_sign(self, - msg_hash: bytes, - private_key: PrivateKey) -> Signature: +class BaseECCBackend: + def ecdsa_sign(self, msg_hash: bytes, private_key: PrivateKey) -> Signature: raise NotImplementedError() - def ecdsa_sign_non_recoverable(self, - msg_hash: bytes, - private_key: PrivateKey) -> NonRecoverableSignature: + def ecdsa_sign_non_recoverable( + self, msg_hash: bytes, private_key: PrivateKey + ) -> NonRecoverableSignature: raise NotImplementedError() - def ecdsa_verify(self, - msg_hash: bytes, - signature: BaseSignature, - public_key: PublicKey) -> bool: + def ecdsa_verify( + self, msg_hash: bytes, signature: BaseSignature, public_key: PublicKey + ) -> bool: raise NotImplementedError() - def ecdsa_recover(self, - msg_hash: bytes, - signature: Signature) -> PublicKey: + def ecdsa_recover(self, msg_hash: bytes, signature: Signature) -> PublicKey: raise NotImplementedError() - def private_key_to_public_key(self, - private_key: PrivateKey) -> PublicKey: + def private_key_to_public_key(self, private_key: PrivateKey) -> PublicKey: raise NotImplementedError() - def decompress_public_key_bytes(self, - compressed_public_key_bytes: bytes) -> bytes: + def decompress_public_key_bytes(self, compressed_public_key_bytes: bytes) -> bytes: raise NotImplementedError() - def compress_public_key_bytes(self, - uncompressed_public_key_bytes: bytes) -> bytes: + def compress_public_key_bytes(self, uncompressed_public_key_bytes: bytes) -> bytes: raise NotImplementedError() diff --git a/eth_keys/backends/coincurve.py b/eth_keys/backends/coincurve.py index 8b65968..d6d5e51 100644 --- a/eth_keys/backends/coincurve.py +++ b/eth_keys/backends/coincurve.py @@ -1,12 +1,8 @@ -from __future__ import absolute_import - -from typing import Optional # noqa: F401 - from eth_utils import ( big_endian_to_int, ) -from eth_keys.datatypes import ( # noqa: F401 +from eth_keys.datatypes import ( BaseSignature, NonRecoverableSignature, PrivateKey, @@ -16,17 +12,19 @@ from eth_keys.exceptions import ( BadSignature, ) -from eth_keys.validation import ( - validate_uncompressed_public_key_bytes, -) from eth_keys.utils import ( der, ) from eth_keys.utils.numeric import ( coerce_low_s, ) +from eth_keys.validation import ( + validate_uncompressed_public_key_bytes, +) -from .base import BaseECCBackend +from .base import ( + BaseECCBackend, +) def is_coincurve_available() -> bool: @@ -43,15 +41,15 @@ def __init__(self) -> None: try: import coincurve except ImportError: - raise ImportError("The CoinCurveECCBackend requires the coincurve \ - library which is not available for import.") + raise ImportError( + "The CoinCurveECCBackend requires the coincurve " + "library which is not available for import." + ) self.keys = coincurve.keys self.ecdsa = coincurve.ecdsa - super(CoinCurveECCBackend, self).__init__() + super().__init__() - def ecdsa_sign(self, - msg_hash: bytes, - private_key: PrivateKey) -> Signature: + def ecdsa_sign(self, msg_hash: bytes, private_key: PrivateKey) -> Signature: private_key_bytes = private_key.to_bytes() signature_bytes = self.keys.PrivateKey(private_key_bytes).sign_recoverable( msg_hash, @@ -60,9 +58,9 @@ def ecdsa_sign(self, signature = Signature(signature_bytes, backend=self) return signature - def ecdsa_sign_non_recoverable(self, - msg_hash: bytes, - private_key: PrivateKey) -> NonRecoverableSignature: + def ecdsa_sign_non_recoverable( + self, msg_hash: bytes, private_key: PrivateKey + ) -> NonRecoverableSignature: private_key_bytes = private_key.to_bytes() der_encoded_signature = self.keys.PrivateKey(private_key_bytes).sign( @@ -74,11 +72,11 @@ def ecdsa_sign_non_recoverable(self, signature = NonRecoverableSignature(rs=rs, backend=self) return signature - def ecdsa_verify(self, - msg_hash: bytes, - signature: BaseSignature, - public_key: PublicKey) -> bool: - # coincurve rejects signatures with a high s, so convert to the equivalent low s form + def ecdsa_verify( + self, msg_hash: bytes, signature: BaseSignature, public_key: PublicKey + ) -> bool: + # coincurve rejects signatures with a high s, + # so convert to the equivalent low s form low_s = coerce_low_s(signature.s) der_encoded_signature = der.two_int_sequence_encoder(signature.r, low_s) coincurve_public_key = self.keys.PublicKey(b"\x04" + public_key.to_bytes()) @@ -88,9 +86,7 @@ def ecdsa_verify(self, hasher=None, ) - def ecdsa_recover(self, - msg_hash: bytes, - signature: Signature) -> PublicKey: + def ecdsa_recover(self, msg_hash: bytes, signature: Signature) -> PublicKey: signature_bytes = signature.to_bytes() try: public_key_bytes = self.keys.PublicKey.from_signature_and_message( @@ -98,26 +94,22 @@ def ecdsa_recover(self, msg_hash, hasher=None, ).format(compressed=False)[1:] - except (ValueError, Exception) as err: - # `coincurve` can raise `ValueError` or `Exception` dependending on - # how the signature is invalid. + except Exception as err: raise BadSignature(str(err)) public_key = PublicKey(public_key_bytes, backend=self) return public_key def private_key_to_public_key(self, private_key: PrivateKey) -> PublicKey: - public_key_bytes = self.keys.PrivateKey(private_key.to_bytes()).public_key.format( - compressed=False, - )[1:] + public_key_bytes = self.keys.PrivateKey( + private_key.to_bytes() + ).public_key.format(compressed=False,)[1:] return PublicKey(public_key_bytes, backend=self) - def decompress_public_key_bytes(self, - compressed_public_key_bytes: bytes) -> bytes: + def decompress_public_key_bytes(self, compressed_public_key_bytes: bytes) -> bytes: public_key = self.keys.PublicKey(compressed_public_key_bytes) return public_key.format(compressed=False)[1:] - def compress_public_key_bytes(self, - uncompressed_public_key_bytes: bytes) -> bytes: + def compress_public_key_bytes(self, uncompressed_public_key_bytes: bytes) -> bytes: validate_uncompressed_public_key_bytes(uncompressed_public_key_bytes) point = ( big_endian_to_int(uncompressed_public_key_bytes[:32]), diff --git a/eth_keys/backends/native/__init__.py b/eth_keys/backends/native/__init__.py index d7a2a73..b9e24f8 100644 --- a/eth_keys/backends/native/__init__.py +++ b/eth_keys/backends/native/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - -from .main import ( # noqa: F401 +from .main import ( NativeECCBackend, ) diff --git a/eth_keys/backends/native/ecdsa.py b/eth_keys/backends/native/ecdsa.py index 6719b7f..72068f3 100644 --- a/eth_keys/backends/native/ecdsa.py +++ b/eth_keys/backends/native/ecdsa.py @@ -3,36 +3,41 @@ """ import hashlib import hmac -from typing import (Any, Callable, Optional, Tuple) # noqa: F401 +from typing import ( + Any, + Callable, + Tuple, +) from eth_utils import ( - int_to_big_endian, big_endian_to_int, + int_to_big_endian, ) from eth_keys.constants import ( - SECPK1_N as N, + SECPK1_A as A, + SECPK1_B as B, SECPK1_G as G, + SECPK1_N as N, + SECPK1_P as P, SECPK1_Gx as Gx, SECPK1_Gy as Gy, - SECPK1_P as P, - SECPK1_A as A, - SECPK1_B as B, ) from eth_keys.exceptions import ( BadSignature, ) - -from eth_keys.utils.padding import pad32 +from eth_keys.utils.padding import ( + pad32, +) from .jacobian import ( - inv, - fast_multiply, fast_add, + fast_multiply, + from_jacobian, + inv, is_identity, jacobian_add, jacobian_multiply, - from_jacobian, ) @@ -44,10 +49,12 @@ def decode_public_key(public_key_bytes: bytes) -> Tuple[int, int]: def encode_raw_public_key(raw_public_key: Tuple[int, int]) -> bytes: left, right = raw_public_key - return b''.join(( - pad32(int_to_big_endian(left)), - pad32(int_to_big_endian(right)), - )) + return b"".join( + ( + pad32(int_to_big_endian(left)), + pad32(int_to_big_endian(right)), + ) + ) def private_key_to_public_key(private_key_bytes: bytes) -> bytes: @@ -90,15 +97,21 @@ def decompress_public_key(compressed_public_key_bytes: bytes) -> bytes: return encode_raw_public_key((x, y)) -def deterministic_generate_k(msg_hash: bytes, - private_key_bytes: bytes, - digest_fn: Callable[[], Any] = hashlib.sha256) -> int: - v_0 = b'\x01' * 32 - k_0 = b'\x00' * 32 +def deterministic_generate_k( + msg_hash: bytes, + private_key_bytes: bytes, + digest_fn: Callable[[], Any] = hashlib.sha256, +) -> int: + v_0 = b"\x01" * 32 + k_0 = b"\x00" * 32 - k_1 = hmac.new(k_0, v_0 + b'\x00' + private_key_bytes + msg_hash, digest_fn).digest() + k_1 = hmac.new( + k_0, v_0 + b"\x00" + private_key_bytes + msg_hash, digest_fn + ).digest() v_1 = hmac.new(k_1, v_0, digest_fn).digest() - k_2 = hmac.new(k_1, v_1 + b'\x01' + private_key_bytes + msg_hash, digest_fn).digest() + k_2 = hmac.new( + k_1, v_1 + b"\x01" + private_key_bytes + msg_hash, digest_fn + ).digest() v_2 = hmac.new(k_2, v_1, digest_fn).digest() kb = hmac.new(k_2, v_2, digest_fn).digest() @@ -106,8 +119,7 @@ def deterministic_generate_k(msg_hash: bytes, return k -def ecdsa_raw_sign(msg_hash: bytes, - private_key_bytes: bytes) -> Tuple[int, int, int]: +def ecdsa_raw_sign(msg_hash: bytes, private_key_bytes: bytes) -> Tuple[int, int, int]: z = big_endian_to_int(msg_hash) k = deterministic_generate_k(msg_hash, private_key_bytes) @@ -120,9 +132,9 @@ def ecdsa_raw_sign(msg_hash: bytes, return v - 27, r, s -def ecdsa_raw_verify(msg_hash: bytes, - rs: Tuple[int, int], - public_key_bytes: bytes) -> bool: +def ecdsa_raw_verify( + msg_hash: bytes, rs: Tuple[int, int], public_key_bytes: bytes +) -> bool: raw_public_key = decode_public_key(public_key_bytes) r, s = rs @@ -138,13 +150,12 @@ def ecdsa_raw_verify(msg_hash: bytes, return bool(r == x and (r % N) and (s % N)) -def ecdsa_raw_recover(msg_hash: bytes, - vrs: Tuple[int, int, int]) -> bytes: +def ecdsa_raw_recover(msg_hash: bytes, vrs: Tuple[int, int, int]) -> bytes: v, r, s = vrs v += 27 if not (27 <= v <= 34): - raise BadSignature("%d must in range 27-31" % v) + raise BadSignature(f"{v} must in range 27-31") x = r diff --git a/eth_keys/backends/native/jacobian.py b/eth_keys/backends/native/jacobian.py index 1ea09fe..383a240 100644 --- a/eth_keys/backends/native/jacobian.py +++ b/eth_keys/backends/native/jacobian.py @@ -1,10 +1,12 @@ -from typing import Tuple # noqa: F401 +from typing import ( + Tuple, +) from eth_keys.constants import ( IDENTITY_POINTS, - SECPK1_P as P, - SECPK1_N as N, SECPK1_A as A, + SECPK1_N as N, + SECPK1_P as P, ) @@ -32,13 +34,14 @@ def jacobian_double(p: Tuple[int, int, int]) -> Tuple[int, int, int]: S = (4 * p[0] * ysq) % P M = (3 * p[0] ** 2 + A * p[2] ** 4) % P nx = (M**2 - 2 * S) % P - ny = (M * (S - nx) - 8 * ysq ** 2) % P + ny = (M * (S - nx) - 8 * ysq**2) % P nz = (2 * p[1] * p[2]) % P return (nx, ny, nz) -def jacobian_add(p: Tuple[int, int, int], - q: Tuple[int, int, int]) -> Tuple[int, int, int]: +def jacobian_add( + p: Tuple[int, int, int], q: Tuple[int, int, int] +) -> Tuple[int, int, int]: if not p[1]: return q if not q[1]: @@ -56,7 +59,7 @@ def jacobian_add(p: Tuple[int, int, int], H2 = (H * H) % P H3 = (H * H2) % P U1H2 = (U1 * H2) % P - nx = (R ** 2 - H3 - 2 * U1H2) % P + nx = (R**2 - H3 - 2 * U1H2) % P ny = (R * (U1H2 - nx) - S1 * H3) % P nz = (H * p[2] * q[2]) % P return (nx, ny, nz) @@ -67,8 +70,7 @@ def from_jacobian(p: Tuple[int, int, int]) -> Tuple[int, int]: return ((p[0] * z**2) % P, (p[1] * z**3) % P) -def jacobian_multiply(a: Tuple[int, int, int], - n: int) -> Tuple[int, int, int]: +def jacobian_multiply(a: Tuple[int, int, int], n: int) -> Tuple[int, int, int]: if a[1] == 0 or n == 0: return (0, 0, 1) if n == 1: @@ -83,13 +85,11 @@ def jacobian_multiply(a: Tuple[int, int, int], raise Exception("Invariant: Unreachable code path") -def fast_multiply(a: Tuple[int, int], - n: int) -> Tuple[int, int]: +def fast_multiply(a: Tuple[int, int], n: int) -> Tuple[int, int]: return from_jacobian(jacobian_multiply(to_jacobian(a), n)) -def fast_add(a: Tuple[int, int], - b: Tuple[int, int]) -> Tuple[int, int]: +def fast_add(a: Tuple[int, int], b: Tuple[int, int]) -> Tuple[int, int]: return from_jacobian(jacobian_add(to_jacobian(a), to_jacobian(b))) diff --git a/eth_keys/backends/native/main.py b/eth_keys/backends/native/main.py index ca7094f..8237ceb 100644 --- a/eth_keys/backends/native/main.py +++ b/eth_keys/backends/native/main.py @@ -1,18 +1,7 @@ -from __future__ import absolute_import - -from typing import Optional # noqa: F401 - -from .ecdsa import ( - ecdsa_raw_recover, - ecdsa_raw_sign, - ecdsa_raw_verify, - private_key_to_public_key, - compress_public_key, - decompress_public_key, +from eth_keys.backends.base import ( + BaseECCBackend, ) - -from eth_keys.backends.base import BaseECCBackend -from eth_keys.datatypes import ( # noqa: F401 +from eth_keys.datatypes import ( BaseSignature, NonRecoverableSignature, PrivateKey, @@ -20,31 +9,35 @@ Signature, ) +from .ecdsa import ( + compress_public_key, + decompress_public_key, + ecdsa_raw_recover, + ecdsa_raw_sign, + ecdsa_raw_verify, + private_key_to_public_key, +) + class NativeECCBackend(BaseECCBackend): - def ecdsa_sign(self, - msg_hash: bytes, - private_key: PrivateKey) -> Signature: + def ecdsa_sign(self, msg_hash: bytes, private_key: PrivateKey) -> Signature: signature_vrs = ecdsa_raw_sign(msg_hash, private_key.to_bytes()) signature = Signature(vrs=signature_vrs, backend=self) return signature - def ecdsa_sign_non_recoverable(self, - msg_hash: bytes, - private_key: PrivateKey) -> NonRecoverableSignature: + def ecdsa_sign_non_recoverable( + self, msg_hash: bytes, private_key: PrivateKey + ) -> NonRecoverableSignature: _, signature_r, signature_s = ecdsa_raw_sign(msg_hash, private_key.to_bytes()) signature = NonRecoverableSignature(rs=(signature_r, signature_s), backend=self) return signature - def ecdsa_verify(self, - msg_hash: bytes, - signature: BaseSignature, - public_key: PublicKey) -> bool: + def ecdsa_verify( + self, msg_hash: bytes, signature: BaseSignature, public_key: PublicKey + ) -> bool: return ecdsa_raw_verify(msg_hash, signature.rs, public_key.to_bytes()) - def ecdsa_recover(self, - msg_hash: bytes, - signature: Signature) -> PublicKey: + def ecdsa_recover(self, msg_hash: bytes, signature: Signature) -> PublicKey: public_key_bytes = ecdsa_raw_recover(msg_hash, signature.vrs) public_key = PublicKey(public_key_bytes, backend=self) return public_key @@ -54,10 +47,8 @@ def private_key_to_public_key(self, private_key: PrivateKey) -> PublicKey: public_key = PublicKey(public_key_bytes, backend=self) return public_key - def decompress_public_key_bytes(self, - compressed_public_key_bytes: bytes) -> bytes: + def decompress_public_key_bytes(self, compressed_public_key_bytes: bytes) -> bytes: return decompress_public_key(compressed_public_key_bytes) - def compress_public_key_bytes(self, - uncompressed_public_key_bytes: bytes) -> bytes: + def compress_public_key_bytes(self, uncompressed_public_key_bytes: bytes) -> bytes: return compress_public_key(uncompressed_public_key_bytes) diff --git a/eth_keys/constants.py b/eth_keys/constants.py index b7757ce..9b62f00 100644 --- a/eth_keys/constants.py +++ b/eth_keys/constants.py @@ -1,16 +1,23 @@ -from typing import Tuple # noqa: F401 - +from typing import ( + Tuple, +) # # SECPK1N # -SECPK1_P = 2**256 - 2**32 - 977 # type: int -SECPK1_N = 115792089237316195423570985008687907852837564279074904382605163141518161494337 # type: int # noqa: E501 -SECPK1_A = 0 # type: int # noqa: E501 -SECPK1_B = 7 # type: int # noqa: E501 -SECPK1_Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240 # type: int # noqa: E501 -SECPK1_Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424 # type: int # noqa: E501 -SECPK1_G = (SECPK1_Gx, SECPK1_Gy) # type: Tuple[int, int] +SECPK1_P: int = 2**256 - 2**32 - 977 +SECPK1_N: int = ( + 115792089237316195423570985008687907852837564279074904382605163141518161494337 +) +SECPK1_A: int = 0 +SECPK1_B: int = 7 +SECPK1_Gx: int = ( + 55066263022277343669578718895168534326250603453777594175500187360389116729240 +) +SECPK1_Gy: int = ( + 32670510020758816978083085130507043184471273380659243275938904335757337482424 +) +SECPK1_G: Tuple[int, int] = (SECPK1_Gx, SECPK1_Gy) # diff --git a/eth_keys/datatypes.py b/eth_keys/datatypes.py index 386d173..b5d4058 100644 --- a/eth_keys/datatypes.py +++ b/eth_keys/datatypes.py @@ -1,24 +1,22 @@ -from __future__ import absolute_import - from abc import ( ABC, abstractmethod, ) import codecs import collections -import sys -from typing import ( # noqa: F401 +from typing import ( + TYPE_CHECKING, Any, Tuple, - Union, Type, - TYPE_CHECKING, + Union, ) from eth_typing import ( ChecksumAddress, ) from eth_utils import ( + ValidationError, big_endian_to_int, encode_hex, int_to_big_endian, @@ -27,10 +25,11 @@ keccak, to_checksum_address, to_normalized_address, - ValidationError, - ) +from eth_keys.exceptions import ( + BadSignature, +) from eth_keys.utils.address import ( public_key_bytes_to_address, ) @@ -40,39 +39,27 @@ from eth_keys.utils.padding import ( pad32, ) - -from eth_keys.exceptions import ( - BadSignature, -) from eth_keys.validation import ( - validate_private_key_bytes, validate_compressed_public_key_bytes, - validate_uncompressed_public_key_bytes, - validate_recoverable_signature_bytes, validate_non_recoverable_signature_bytes, - validate_signature_v, + validate_private_key_bytes, + validate_recoverable_signature_bytes, validate_signature_r_or_s, + validate_signature_v, + validate_uncompressed_public_key_bytes, ) if TYPE_CHECKING: - from eth_keys.backends.base import BaseECCBackend # noqa: F401 - - -# Must compare against version_info[0] and not version_info.major to please mypy. -if sys.version_info[0] == 2: - ByteString = type( - b'BaseString', - (collections.abc.Sequence, basestring), # noqa: F821 - {}, - ) # type: Any -else: - ByteString = collections.abc.ByteString + from eth_keys.backends.base import ( + BaseECCBackend, + ) class LazyBackend: - def __init__(self, - backend: 'Union[BaseECCBackend, Type[BaseECCBackend], str, None]' = None, - ) -> None: + def __init__( + self, + backend: "Union[BaseECCBackend, Type[BaseECCBackend], str, None]" = None, + ) -> None: from eth_keys.backends.base import ( # noqa: F811 BaseECCBackend, ) @@ -94,30 +81,33 @@ def __init__(self, self.backend = backend - _backend = None # type: BaseECCBackend + _backend: "BaseECCBackend" = None @property - def backend(self) -> 'BaseECCBackend': + def backend(self) -> "BaseECCBackend": if self._backend is None: return self.get_backend() else: return self._backend @backend.setter - def backend(self, value: 'BaseECCBackend') -> None: + def backend(self, value: "BaseECCBackend") -> None: self._backend = value @classmethod - def get_backend(cls, *args: Any, **kwargs: Any) -> 'BaseECCBackend': - from eth_keys.backends import get_backend + def get_backend(cls, *args: Any, **kwargs: Any) -> "BaseECCBackend": + from eth_keys.backends import ( + get_backend, + ) + return get_backend(*args, **kwargs) -class BaseKey(ByteString, collections.abc.Hashable): - _raw_key = None # type: bytes +class BaseKey(collections.abc.Hashable): + _raw_key: bytes = None def to_hex(self) -> str: - return '0x' + codecs.decode(codecs.encode(self._raw_key, 'hex'), 'ascii') + return "0x" + codecs.decode(codecs.encode(self._raw_key, "hex"), "ascii") def to_bytes(self) -> bytes: return self._raw_key @@ -135,13 +125,11 @@ def __len__(self) -> int: # TODO: this seems wrong. return 64 - # Must be typed with `ignore` due to - # https://github.com/python/mypy/issues/1237 - def __getitem__(self, index: int) -> int: # type: ignore + def __getitem__(self, index: int) -> int: return self._raw_key[index] def __eq__(self, other: Any) -> bool: - if hasattr(other, 'to_bytes'): + if hasattr(other, "to_bytes"): return self.to_bytes() == other.to_bytes() elif is_bytes(other): return self.to_bytes() == other @@ -149,80 +137,86 @@ def __eq__(self, other: Any) -> bool: return False def __repr__(self) -> str: - return "'{0}'".format(self.to_hex()) + return f"'{self.to_hex()}'" def __index__(self) -> int: return self.__int__() def __hex__(self) -> str: - if sys.version_info[0] == 2: - return codecs.encode(self.to_hex(), 'ascii') - else: - return self.to_hex() + return self.to_hex() class PublicKey(BaseKey, LazyBackend): - def __init__(self, - public_key_bytes: bytes, - backend: 'Union[BaseECCBackend, Type[BaseECCBackend], str, None]' = None, - ) -> None: + def __init__( + self, + public_key_bytes: bytes, + backend: "Union[BaseECCBackend, Type[BaseECCBackend], str, None]" = None, + ) -> None: validate_uncompressed_public_key_bytes(public_key_bytes) self._raw_key = public_key_bytes super().__init__(backend=backend) @classmethod - def from_compressed_bytes(cls, - compressed_public_key_bytes: bytes, - backend: 'BaseECCBackend' = None, - ) -> 'PublicKey': + def from_compressed_bytes( + cls, + compressed_public_key_bytes: bytes, + backend: "BaseECCBackend" = None, + ) -> "PublicKey": validate_compressed_public_key_bytes(compressed_public_key_bytes) if backend is None: backend = cls.get_backend() - uncompressed_key = backend.decompress_public_key_bytes(compressed_public_key_bytes) + uncompressed_key = backend.decompress_public_key_bytes( + compressed_public_key_bytes + ) return cls(uncompressed_key, backend) @classmethod - def from_private(cls, - private_key: 'PrivateKey', - backend: 'BaseECCBackend' = None, - ) -> 'PublicKey': + def from_private( + cls, + private_key: "PrivateKey", + backend: "BaseECCBackend" = None, + ) -> "PublicKey": if backend is None: backend = cls.get_backend() return backend.private_key_to_public_key(private_key) @classmethod - def recover_from_msg(cls, - message: bytes, - signature: 'Signature', - backend: 'BaseECCBackend' = None, - ) -> 'PublicKey': + def recover_from_msg( + cls, + message: bytes, + signature: "Signature", + backend: "BaseECCBackend" = None, + ) -> "PublicKey": message_hash = keccak(message) return cls.recover_from_msg_hash(message_hash, signature, backend) @classmethod - def recover_from_msg_hash(cls, - message_hash: bytes, - signature: 'Signature', - backend: 'BaseECCBackend' = None, - ) -> 'PublicKey': + def recover_from_msg_hash( + cls, + message_hash: bytes, + signature: "Signature", + backend: "BaseECCBackend" = None, + ) -> "PublicKey": if backend is None: backend = cls.get_backend() return backend.ecdsa_recover(message_hash, signature) - def verify_msg(self, - message: bytes, - signature: 'Signature', - ) -> bool: + def verify_msg( + self, + message: bytes, + signature: "Signature", + ) -> bool: message_hash = keccak(message) return self.verify_msg_hash(message_hash, signature) - def verify_msg_hash(self, - message_hash: bytes, - signature: 'Signature', - ) -> bool: + def verify_msg_hash( + self, + message_hash: bytes, + signature: "Signature", + ) -> bool: return self.backend.ecdsa_verify(message_hash, signature, self) def to_compressed_bytes(self) -> bytes: @@ -242,12 +236,13 @@ def to_canonical_address(self) -> bytes: class PrivateKey(BaseKey, LazyBackend): - public_key = None # type: PublicKey + public_key: PublicKey = None - def __init__(self, - private_key_bytes: bytes, - backend: 'Union[BaseECCBackend, Type[BaseECCBackend], str, None]' = None, - ) -> None: + def __init__( + self, + private_key_bytes: bytes, + backend: "Union[BaseECCBackend, Type[BaseECCBackend], str, None]" = None, + ) -> None: validate_private_key_bytes(private_key_bytes) self._raw_key = private_key_bytes @@ -255,29 +250,32 @@ def __init__(self, self.public_key = self.backend.private_key_to_public_key(self) super().__init__(backend=backend) - def sign_msg(self, message: bytes) -> 'Signature': + def sign_msg(self, message: bytes) -> "Signature": message_hash = keccak(message) return self.sign_msg_hash(message_hash) - def sign_msg_hash(self, message_hash: bytes) -> 'Signature': + def sign_msg_hash(self, message_hash: bytes) -> "Signature": return self.backend.ecdsa_sign(message_hash, self) - def sign_msg_non_recoverable(self, message: bytes) -> 'NonRecoverableSignature': + def sign_msg_non_recoverable(self, message: bytes) -> "NonRecoverableSignature": message_hash = keccak(message) return self.sign_msg_hash_non_recoverable(message_hash) - def sign_msg_hash_non_recoverable(self, message_hash: bytes) -> 'NonRecoverableSignature': + def sign_msg_hash_non_recoverable( + self, message_hash: bytes + ) -> "NonRecoverableSignature": return self.backend.ecdsa_sign_non_recoverable(message_hash, self) -class BaseSignature(ByteString, LazyBackend, ABC): - _r = None # type: int - _s = None # type: int +class BaseSignature(LazyBackend, ABC): + _r: int = None + _s: int = None - def __init__(self, - rs: Tuple[int, int], - backend: 'Union[BaseECCBackend, Type[BaseECCBackend], str, None]' = None - ) -> None: + def __init__( + self, + rs: Tuple[int, int], + backend: "Union[BaseECCBackend, Type[BaseECCBackend], str, None]" = None, + ) -> None: for value in rs: try: validate_signature_r_or_s(value) @@ -319,20 +317,18 @@ def __len__(self) -> int: return len(bytes(self)) def __eq__(self, other: Any) -> bool: - if hasattr(other, 'to_bytes'): + if hasattr(other, "to_bytes"): return self.to_bytes() == other.to_bytes() elif is_bytes(other): return self.to_bytes() == other else: return False - # Must be typed with `ignore` due to - # https://github.com/python/mypy/issues/1237 - def __getitem__(self, index: int) -> int: # type: ignore + def __getitem__(self, index: int) -> int: return self.to_bytes()[index] def __repr__(self) -> str: - return "'{0}'".format(self.to_hex()) + return f"'{self.to_hex()}'" def __index__(self) -> int: return self.__int__() @@ -343,26 +339,23 @@ def __hex__(self) -> str: def __int__(self) -> int: return big_endian_to_int(self.to_bytes()) - def verify_msg(self, - message: bytes, - public_key: PublicKey) -> bool: + def verify_msg(self, message: bytes, public_key: PublicKey) -> bool: message_hash = keccak(message) return self.verify_msg_hash(message_hash, public_key) - def verify_msg_hash(self, - message_hash: bytes, - public_key: PublicKey) -> bool: + def verify_msg_hash(self, message_hash: bytes, public_key: PublicKey) -> bool: return self.backend.ecdsa_verify(message_hash, self, public_key) class Signature(BaseSignature): - _v = None # type: int - - def __init__(self, - signature_bytes: bytes = None, - vrs: Tuple[int, int, int] = None, - backend: 'Union[BaseECCBackend, Type[BaseECCBackend], str, None]' = None, - ) -> None: + _v: int = None + + def __init__( + self, + signature_bytes: bytes = None, + vrs: Tuple[int, int, int] = None, + backend: "Union[BaseECCBackend, Type[BaseECCBackend], str, None]" = None, + ) -> None: if bool(signature_bytes) is bool(vrs): raise TypeError("You must provide one of `signature_bytes` or `vrs`") elif signature_bytes: @@ -371,7 +364,11 @@ def __init__(self, s = big_endian_to_int(signature_bytes[32:64]) v = ord(signature_bytes[64:65]) elif vrs: - v, r, s, = vrs + ( + v, + r, + s, + ) = vrs else: raise TypeError("Invariant: unreachable code path") @@ -411,7 +408,7 @@ def to_bytes(self) -> bytes: vb = int_to_byte(self.v) rb = pad32(int_to_big_endian(self.r)) sb = pad32(int_to_big_endian(self.s)) - return b''.join((rb, sb, vb)) + return b"".join((rb, sb, vb)) def recover_public_key_from_msg(self, message: bytes) -> PublicKey: message_hash = keccak(message) @@ -420,17 +417,17 @@ def recover_public_key_from_msg(self, message: bytes) -> PublicKey: def recover_public_key_from_msg_hash(self, message_hash: bytes) -> PublicKey: return self.backend.ecdsa_recover(message_hash, self) - def to_non_recoverable_signature(self) -> 'NonRecoverableSignature': + def to_non_recoverable_signature(self) -> "NonRecoverableSignature": return NonRecoverableSignature(rs=self.rs) class NonRecoverableSignature(BaseSignature): - - def __init__(self, - signature_bytes: bytes = None, - rs: Tuple[int, int] = None, - backend: 'Union[BaseECCBackend, Type[BaseECCBackend], str, None]' = None, - ) -> None: + def __init__( + self, + signature_bytes: bytes = None, + rs: Tuple[int, int] = None, + backend: "Union[BaseECCBackend, Type[BaseECCBackend], str, None]" = None, + ) -> None: if signature_bytes is None and rs is None: raise TypeError("You must provide one of `signature_bytes` or `vr`") elif signature_bytes: @@ -445,7 +442,4 @@ def __init__(self, super().__init__(rs=(r, s), backend=backend) def to_bytes(self) -> bytes: - return b''.join( - pad32(int_to_big_endian(value)) - for value in self.rs - ) + return b"".join(pad32(int_to_big_endian(value)) for value in self.rs) diff --git a/eth_keys/main.py b/eth_keys/main.py index 678a7b8..f6a87e1 100644 --- a/eth_keys/main.py +++ b/eth_keys/main.py @@ -1,5 +1,3 @@ -from typing import (Any, Union, Type) # noqa: F401 - from eth_utils import ( ValidationError, ) @@ -8,15 +6,14 @@ BaseSignature, LazyBackend, NonRecoverableSignature, - PublicKey, PrivateKey, + PublicKey, Signature, ) from eth_keys.validation import ( validate_message_hash, ) - # These must be aliased due to a scoping issue in mypy # https://github.com/python/mypy/issues/1775 _PublicKey = PublicKey @@ -26,24 +23,20 @@ class KeyAPI(LazyBackend): - # - # datatype shortcuts - # - PublicKey = PublicKey # type: Type[_PublicKey] - PrivateKey = PrivateKey # type: Type[_PrivateKey] - Signature = Signature # type: Type[_Signature] - NonRecoverableSignature = NonRecoverableSignature # type: Type[_NonRecoverableSignature] + PublicKey = PublicKey + PrivateKey = PrivateKey + Signature = Signature + NonRecoverableSignature = NonRecoverableSignature # # Proxy method calls to the backends # - def ecdsa_sign(self, - message_hash: bytes, - private_key: _PrivateKey) -> _Signature: + def ecdsa_sign(self, message_hash: bytes, private_key: _PrivateKey) -> _Signature: validate_message_hash(message_hash) if not isinstance(private_key, PrivateKey): raise ValidationError( - "The `private_key` must be an instance of `eth_keys.datatypes.PrivateKey`" + "The `private_key` must be an instance of " + "`eth_keys.datatypes.PrivateKey`" ) signature = self.backend.ecdsa_sign(message_hash, private_key) if not isinstance(signature, Signature): @@ -53,13 +46,14 @@ def ecdsa_sign(self, ) return signature - def ecdsa_sign_non_recoverable(self, - message_hash: bytes, - private_key: _PrivateKey) -> _NonRecoverableSignature: + def ecdsa_sign_non_recoverable( + self, message_hash: bytes, private_key: _PrivateKey + ) -> _NonRecoverableSignature: validate_message_hash(message_hash) if not isinstance(private_key, PrivateKey): raise ValidationError( - "The `private_key` must be an instance of `eth_keys.datatypes.PrivateKey`" + "The `private_key` must be an instance of " + "`eth_keys.datatypes.PrivateKey`" ) signature = self.backend.ecdsa_sign_non_recoverable(message_hash, private_key) if not isinstance(signature, NonRecoverableSignature): @@ -69,10 +63,9 @@ def ecdsa_sign_non_recoverable(self, ) return signature - def ecdsa_verify(self, - message_hash: bytes, - signature: BaseSignature, - public_key: _PublicKey) -> bool: + def ecdsa_verify( + self, message_hash: bytes, signature: BaseSignature, public_key: _PublicKey + ) -> bool: validate_message_hash(message_hash) if not isinstance(public_key, PublicKey): raise ValidationError( @@ -80,13 +73,12 @@ def ecdsa_verify(self, ) if not isinstance(signature, BaseSignature): raise ValidationError( - "The `signature` must be an instance of `eth_keys.datatypes.BaseSignature`" + "The `signature` must be an instance of " + "`eth_keys.datatypes.BaseSignature`" ) return self.backend.ecdsa_verify(message_hash, signature, public_key) - def ecdsa_recover(self, - message_hash: bytes, - signature: _Signature) -> _PublicKey: + def ecdsa_recover(self, message_hash: bytes, signature: _Signature) -> _PublicKey: validate_message_hash(message_hash) if not isinstance(signature, Signature): raise ValidationError( @@ -103,7 +95,8 @@ def ecdsa_recover(self, def private_key_to_public_key(self, private_key: _PrivateKey) -> _PublicKey: if not isinstance(private_key, PrivateKey): raise ValidationError( - "The `private_key` must be an instance of `eth_keys.datatypes.PrivateKey`" + "The `private_key` must be an instance of " + "`eth_keys.datatypes.PrivateKey`" ) public_key = self.backend.private_key_to_public_key(private_key) if not isinstance(public_key, PublicKey): diff --git a/eth_keys/tools/factories.py b/eth_keys/tools/factories.py index ffc01bb..c4a3ebb 100644 --- a/eth_keys/tools/factories.py +++ b/eth_keys/tools/factories.py @@ -6,7 +6,9 @@ "which does not appear to be installed." ) from err -from eth_keys import keys +from eth_keys import ( + keys, +) def _mk_random_bytes(num_bytes: int) -> bytes: @@ -14,6 +16,7 @@ def _mk_random_bytes(num_bytes: int) -> bytes: import secrets except ImportError: import os + return os.urandom(num_bytes) else: return secrets.token_bytes(num_bytes) @@ -30,4 +33,6 @@ class PublicKeyFactory(factory.Factory): # type: ignore class Meta: model = keys.PublicKey - public_key_bytes = factory.LazyFunction(lambda: PrivateKeyFactory().public_key.to_bytes()) + public_key_bytes = factory.LazyFunction( + lambda: PrivateKeyFactory().public_key.to_bytes() + ) diff --git a/eth_keys/utils/der.py b/eth_keys/utils/der.py index ddeee83..c06e591 100644 --- a/eth_keys/utils/der.py +++ b/eth_keys/utils/der.py @@ -1,8 +1,9 @@ # Non-recoverable signatures are encoded using a DER sequence of two integers # We locally implement serialization and deserialization for this specific spec # with constrained inputs. -# This is done locally to avoid importing a 3rd-party library, in this very sensitive project. -# asn1tools and pyasn1 were used as reference APIs, see how in tests/core/test_utils_asn1.py +# This is done locally to avoid importing a 3rd-party library, in this very sensitive +# project. asn1tools and pyasn1 were used as reference APIs, see how in +# tests/core/test_utils_asn1.py # # See more about DER encodings, and ASN.1 in general, here: # http://luca.ntop.org/Teaching/Appunti/asn1.html @@ -73,14 +74,19 @@ def two_int_sequence_decoder(encoded: bytes) -> Tuple[int, int]: See: https://docs.microsoft.com/en-us/windows/desktop/seccertenroll/about-sequence """ if encoded[0] != 0x30: - raise ValueError("Encoded sequence must start with 0x30 byte, but got %s" % encoded[0]) + raise ValueError( + f"Encoded sequence must start with 0x30 byte, but got {encoded[0]}" + ) # skip sequence length int1, rest = _decode_int(encoded[2:]) int2, empty = _decode_int(rest) if len(empty) != 0: - raise ValueError("Encoded sequence must not contain any trailing data, but had %r" % empty) + raise ValueError( + "Encoded sequence must not contain any trailing data, but had " + f"{repr(empty)}" + ) return int1, int2 @@ -94,7 +100,8 @@ def _encode_int(primitive: int) -> Iterator[int]: encoded = int_to_big_endian(primitive) if encoded[0] >= 128: - # Indicate that integer is positive (it always is, but doesn't always need the flag) + # Indicate that integer is positive + # (it always is, but doesn't always need the flag) yield len(encoded) + 1 yield 0x00 else: @@ -108,11 +115,12 @@ def _decode_int(encoded: bytes) -> Tuple[int, bytes]: if encoded[0] != 0x02: raise ValueError( - "Encoded value must be an integer, starting with on 0x02 byte, but got %s" % encoded[0] + "Encoded value must be an integer, starting with on 0x02 byte, but got " + f"{encoded[0]}" ) length = encoded[1] # to_int can handle leading zeros - decoded_int = big_endian_to_int(encoded[2:2 + length]) + decoded_int = big_endian_to_int(encoded[2 : 2 + length]) - return decoded_int, encoded[2 + length:] + return decoded_int, encoded[2 + length :] diff --git a/eth_keys/utils/module_loading.py b/eth_keys/utils/module_loading.py index 03b4a78..308acea 100644 --- a/eth_keys/utils/module_loading.py +++ b/eth_keys/utils/module_loading.py @@ -1,6 +1,11 @@ +from importlib import ( + import_module, +) import operator -from typing import (Any, Tuple) -from importlib import import_module +from typing import ( + Any, + Tuple, +) def import_string(dotted_path: str) -> Any: @@ -10,9 +15,9 @@ def import_string(dotted_path: str) -> Any: last name in the path. Raise ImportError if the import failed. """ try: - module_path, class_name = dotted_path.rsplit('.', 1) + module_path, class_name = dotted_path.rsplit(".", 1) except ValueError: - msg = "%s doesn't look like a module path" % dotted_path + msg = f"{dotted_path} doesn't look like a module path" raise ImportError(msg) module = import_module(module_path) @@ -20,18 +25,17 @@ def import_string(dotted_path: str) -> Any: try: return getattr(module, class_name) except AttributeError: - msg = 'Module "%s" does not define a "%s" attribute/class' % ( - module_path, class_name) + msg = f'Module "{module_path}" does not define a "{class_name}" attribute/class' raise ImportError(msg) def split_at_longest_importable_path(dotted_path: str) -> Tuple[str, str]: - num_path_parts = len(dotted_path.split('.')) + num_path_parts = len(dotted_path.split(".")) for i in range(1, num_path_parts): - path_parts = dotted_path.rsplit('.', i) + path_parts = dotted_path.rsplit(".", i) import_part = path_parts[0] - remainder = '.'.join(path_parts[1:]) + remainder = ".".join(path_parts[1:]) try: module = import_module(import_part) @@ -42,11 +46,9 @@ def split_at_longest_importable_path(dotted_path: str) -> Tuple[str, str]: operator.attrgetter(remainder)(module) except AttributeError: raise ImportError( - "Unable to derive appropriate import path for {0}".format( - dotted_path, - ) + f"Unable to derive appropriate import path for {dotted_path}" ) else: return import_part, remainder else: - return '', dotted_path + return "", dotted_path diff --git a/eth_keys/utils/numeric.py b/eth_keys/utils/numeric.py index 9041e0a..42a86a1 100644 --- a/eth_keys/utils/numeric.py +++ b/eth_keys/utils/numeric.py @@ -8,7 +8,8 @@ def int_to_byte(value: int) -> bytes: def coerce_low_s(value: int) -> int: - """Coerce the s component of an ECDSA signature into its low-s form. + """ + Coerce the s component of an ECDSA signature into its low-s form. See https://bitcoin.stackexchange.com/questions/83408/in-ecdsa-why-is-r-%E2%88%92s-mod-n-complementary-to-r-s # noqa: E501 or https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2.md. diff --git a/eth_keys/utils/padding.py b/eth_keys/utils/padding.py index 58665bf..d03fcbd 100644 --- a/eth_keys/utils/padding.py +++ b/eth_keys/utils/padding.py @@ -1,2 +1,2 @@ def pad32(value: bytes) -> bytes: - return value.rjust(32, b'\x00') + return value.rjust(32, b"\x00") diff --git a/eth_keys/validation.py b/eth_keys/validation.py index 96ae759..878adda 100644 --- a/eth_keys/validation.py +++ b/eth_keys/validation.py @@ -1,12 +1,16 @@ -from typing import Any +from typing import ( + Any, +) from eth_utils import ( + ValidationError, encode_hex, is_bytes, is_integer, - ValidationError, ) -from eth_utils.toolz import curry +from eth_utils.toolz import ( + curry, +) from eth_keys.constants import ( SECPK1_N, @@ -15,12 +19,12 @@ def validate_integer(value: Any) -> None: if not is_integer(value) or isinstance(value, bool): - raise ValidationError("Value must be a an integer. Got: {0}".format(type(value))) + raise ValidationError(f"Value must be a an integer. Got: {type(value)}") def validate_bytes(value: Any) -> None: if not is_bytes(value): - raise ValidationError("Value must be a byte string. Got: {0}".format(type(value))) + raise ValidationError(f"Value must be a byte string. Got: {type(value)}") @curry @@ -28,9 +32,7 @@ def validate_gte(value: Any, minimum: int) -> None: validate_integer(value) if value < minimum: raise ValidationError( - "Value {0} is not greater than or equal to {1}".format( - value, minimum, - ) + f"Value {value} is not greater than or equal to {minimum}" ) @@ -38,11 +40,7 @@ def validate_gte(value: Any, minimum: int) -> None: def validate_lte(value: Any, maximum: int) -> None: validate_integer(value) if value > maximum: - raise ValidationError( - "Value {0} is not less than or equal to {1}".format( - value, maximum, - ) - ) + raise ValidationError(f"Value {value} is not less than or equal to {maximum}") validate_lt_secpk1n = validate_lte(maximum=SECPK1_N - 1) @@ -52,12 +50,8 @@ def validate_bytes_length(value: bytes, expected_length: int, name: str) -> None actual_length = len(value) if actual_length != expected_length: raise ValidationError( - "Unexpected {name} length: Expected {expected_length}, but got {actual_length} " - "bytes".format( - name=name, - expected_length=expected_length, - actual_length=actual_length, - ) + f"Unexpected {name} length: Expected {expected_length}, but got " + f"{actual_length} bytes" ) @@ -77,10 +71,8 @@ def validate_compressed_public_key_bytes(value: Any) -> None: first_byte = value[0:1] if first_byte not in (b"\x02", b"\x03"): raise ValidationError( - "Unexpected compressed public key format: Must start with 0x02 or 0x03, but starts " - "with {first_byte}".format( - first_byte=encode_hex(first_byte), - ) + "Unexpected compressed public key format: Must start with 0x02 or 0x03, " + f"but starts with {encode_hex(first_byte)}" ) diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 457f52c..0000000 --- a/mypy.ini +++ /dev/null @@ -1,15 +0,0 @@ -[mypy] - -check_untyped_defs = True -disallow_incomplete_defs = True -disallow_untyped_defs = True -disallow_any_generics = True -disallow_untyped_calls = True -disallow_subclassing_any = True -ignore_missing_imports = True -strict_optional = False -strict_equality = True -warn_redundant_casts = True -warn_unused_configs = True -warn_unused_ignores = True - diff --git a/newsfragments/96.breaking.rst b/newsfragments/96.breaking.rst new file mode 100644 index 0000000..8c18340 --- /dev/null +++ b/newsfragments/96.breaking.rst @@ -0,0 +1 @@ +Drop python 3.6 and 3.7 support diff --git a/newsfragments/96.feature.rst b/newsfragments/96.feature.rst new file mode 100644 index 0000000..cc8e290 --- /dev/null +++ b/newsfragments/96.feature.rst @@ -0,0 +1 @@ +Add python 3.11 support diff --git a/newsfragments/96.internal.rst b/newsfragments/96.internal.rst new file mode 100644 index 0000000..5138f8f --- /dev/null +++ b/newsfragments/96.internal.rst @@ -0,0 +1 @@ +Merge template updates, notably using ``pre-commit`` for linting and change the name of the ``master`` branch to ``main`` diff --git a/newsfragments/README.md b/newsfragments/README.md new file mode 100644 index 0000000..177d649 --- /dev/null +++ b/newsfragments/README.md @@ -0,0 +1,29 @@ +This directory collects "newsfragments": short files that each contain +a snippet of ReST-formatted text that will be added to the next +release notes. This should be a description of aspects of the change +(if any) that are relevant to users. (This contrasts with the +commit message and PR description, which are a description of the change as +relevant to people working on the code itself.) + +Each file should be named like `..rst`, where +`` is an issue number, and `` is one of: + +- `breaking` +- `bugfix` +- `deprecation` +- `docs` +- `feature` +- `internal` +- `misc` +- `performance` +- `removal` + +So for example: `123.feature.rst`, `456.bugfix.rst` + +If the PR fixes an issue, use that number here. If there is no issue, +then open up the PR first and use the PR number for the newsfragment. + +Note that the `towncrier` tool will automatically +reflow your text, so don't try to do any fancy formatting. Run +`towncrier build --draft` to get a preview of what the release notes entry +will look like in the final release notes. diff --git a/newsfragments/validate_files.py b/newsfragments/validate_files.py new file mode 100755 index 0000000..02b0e3e --- /dev/null +++ b/newsfragments/validate_files.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +# Towncrier silently ignores files that do not match the expected ending. +# We use this script to ensure we catch these as errors in CI. + +import pathlib +import sys + +ALLOWED_EXTENSIONS = { + ".breaking.rst", + ".bugfix.rst", + ".deprecation.rst", + ".docs.rst", + ".feature.rst", + ".internal.rst", + ".misc.rst", + ".performance.rst", + ".removal.rst", +} + +ALLOWED_FILES = { + "validate_files.py", + "README.md", +} + +THIS_DIR = pathlib.Path(__file__).parent + +num_args = len(sys.argv) - 1 +assert num_args in {0, 1} +if num_args == 1: + assert sys.argv[1] in ("is-empty",) + +for fragment_file in THIS_DIR.iterdir(): + if fragment_file.name in ALLOWED_FILES: + continue + elif num_args == 0: + full_extension = "".join(fragment_file.suffixes) + if full_extension not in ALLOWED_EXTENSIONS: + raise Exception(f"Unexpected file: {fragment_file}") + elif sys.argv[1] == "is-empty": + raise Exception(f"Unexpected file: {fragment_file}") + else: + raise RuntimeError( + f"Strange: arguments {sys.argv} were validated, but not found" + ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4497cd5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,122 @@ +[tool.autoflake] +remove_all_unused_imports = true +exclude = "__init__.py" + +[tool.isort] +combine_as_imports = true +extra_standard_library = "pytest" +force_grid_wrap = 1 +force_sort_within_sections = true +known_third_party = "hypothesis,pytest" +known_first_party = "eth_keys" +multi_line_output = 3 +profile = "black" + +[tool.mypy] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_defs = true +disallow_any_generics = true +disallow_untyped_calls = true +disallow_untyped_decorators = false +disallow_subclassing_any = true +ignore_missing_imports = true +strict_optional = false +strict_equality = true +warn_redundant_casts = true +warn_return_any = false +warn_unused_configs = true +warn_unused_ignores = true + + +[tool.pydocstyle] +# All error codes found here: +# http://www.pydocstyle.org/en/3.0.0/error_codes.html +# +# Ignored: +# D1 - Missing docstring error codes +# +# Selected: +# D2 - Whitespace error codes +# D3 - Quote error codes +# D4 - Content related error codes +select = "D2,D3,D4" + +# Extra ignores: +# D200 - One-line docstring should fit on one line with quotes +# D203 - 1 blank line required before class docstring +# D204 - 1 blank line required after class docstring +# D205 - 1 blank line required between summary line and description +# D212 - Multi-line docstring summary should start at the first line +# D302 - Use u""" for Unicode docstrings +# D400 - First line should end with a period +# D401 - First line should be in imperative mood +# D412 - No blank lines allowed between a section header and its content +# D415 - First line should end with a period, question mark, or exclamation point +add-ignore = "D200,D203,D204,D205,D212,D302,D400,D401,D412,D415" + +# Explanation: +# D400 - Enabling this error code seems to make it a requirement that the first +# sentence in a docstring is not split across two lines. It also makes it a +# requirement that no docstring can have a multi-sentence description without a +# summary line. Neither one of those requirements seem appropriate. + +[tool.pytest.ini_options] +addopts = "-v --showlocals --durations 10" +xfail_strict = true +log_format = "%(levelname)8s %(asctime)s %(filename)20s %(message)s" +log_date_format = "%m-%d %H:%M:%S" + +[tool.towncrier] +# Read https://github.com/ethereum/eth-keys/blob/main/newsfragments/README.md for instructions +package = "eth_keys" +filename = "docs/release_notes.rst" +directory = "newsfragments" +underlines = ["-", "~", "^"] +title_format = "eth-keys v{version} ({project_date})" +issue_format = "`#{issue} `__" + +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking Changes" +showcontent = true + +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bugfixes" +showcontent = true + +[[tool.towncrier.type]] +directory = "deprecation" +name = "Deprecations" +showcontent = true + +[[tool.towncrier.type]] +directory = "docs" +name = "Improved Documentation" +showcontent = true + +[[tool.towncrier.type]] +directory = "feature" +name = "Features" +showcontent = true + +[[tool.towncrier.type]] +directory = "internal" +name = "Internal Changes - for eth-keys Contributors" +showcontent = true + +[[tool.towncrier.type]] +directory = "misc" +name = "Miscellaneous Changes" +showcontent = false + +[[tool.towncrier.type]] +directory = "performance" +name = "Performance Improvements" +showcontent = true + +[[tool.towncrier.type]] +directory = "removal" +name = "Removals" +showcontent = true diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index beaabb6..0000000 --- a/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -addopts= -v --showlocals diff --git a/setup.py b/setup.py index 20cea66..879a294 100644 --- a/setup.py +++ b/setup.py @@ -1,78 +1,79 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- from setuptools import ( - setup, find_packages, + setup, ) - -deps = { - 'coincurve': [ - 'coincurve>=7.0.0,<16.0.0', - ], - 'eth-keys': [ - "eth-utils>=2.0.0,<3.0.0", - "eth-typing>=3.0.0,<4", +extras_require = { + "coincurve": [ + "coincurve>=12.0.0", ], - 'test': [ - "asn1tools>=0.146.2,<0.147", - "factory-boy>=3.0.1,<3.1", - "pyasn1>=0.4.5,<0.5", - "pytest==6.2.5", - "hypothesis>=5.10.3, <6.0.0", - "eth-hash[pysha3];implementation_name=='cpython'", - "eth-hash[pycryptodome];implementation_name=='pypy'", + "dev": [ + "build>=0.9.0", + "bumpversion>=0.5.3", + "ipython", + "pre-commit>=3.4.0", + "tox>=4.0.0", + "twine", + "wheel", ], - 'lint': [ - 'flake8==3.0.4', - 'mypy==0.782', + "docs": [ + "towncrier>=21,<22", ], - 'dev': [ - 'tox==3.20.0', - 'bumpversion==0.5.3', - 'twine', + "test": [ + "pytest>=7.0.0", + "asn1tools>=0.146.2", + "factory-boy>=3.0.1", + "pyasn1>=0.4.5", + "hypothesis>=5.10.3,<6", + "eth-hash[pysha3]", ], } -deps['dev'] = ( - deps['dev'] + - deps['eth-keys'] + - deps['lint'] + - deps['test'] +extras_require["dev"] = ( + extras_require["coincurve"] + + extras_require["dev"] + + extras_require["docs"] + + extras_require["test"] ) -with open('./README.md') as readme: + +with open("./README.md") as readme: long_description = readme.read() + setup( - name='eth-keys', - # *IMPORTANT*: Don't manually change the version here. Use the 'bumpversion' utility. - version='0.4.0', - description="""Common API for Ethereum key operations.""", + name="eth-keys", + # *IMPORTANT*: Don't manually change the version here. Use `make bump`, as described in readme + version="0.4.0", + description="""eth-keys: Common API for Ethereum key operations""", long_description=long_description, - long_description_content_type='text/markdown', - author='Piper Merriam', - author_email='pipermerriam@gmail.com', - url='https://github.com/ethereum/eth-keys', + long_description_content_type="text/markdown", + author="The Ethereum Foundation", + author_email="snakecharmers@ethereum.org", + url="https://github.com/ethereum/eth-keys", include_package_data=True, - install_requires=deps['eth-keys'], - py_modules=['eth_keys'], - extras_require=deps, + install_requires=[ + "eth-utils>=2", + "eth-typing>=3", + ], + python_requires=">=3.8, <4", + extras_require=extras_require, + py_modules=["eth_keys"], license="MIT", zip_safe=False, - package_data={'eth_keys': ['py.typed']}, - keywords='ethereum', + keywords="ethereum", packages=find_packages(exclude=["tests", "tests.*"]), + package_data={"eth_keys": ["py.typed"]}, classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Natural Language :: English', - 'Programming Language :: Python :: 3', - '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', + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ], ) diff --git a/tests/backends/conftest.py b/tests/backends/conftest.py index 613f7b8..8aed26e 100644 --- a/tests/backends/conftest.py +++ b/tests/backends/conftest.py @@ -1,18 +1,16 @@ -import pytest - from eth_utils import ( decode_hex, keccak, ) +import pytest - -MSG = b'message' +MSG = b"message" MSGHASH = keccak(MSG) # This is a sample of signatures generated with a known-good implementation of the ECDSA -# algorithm, which we use to test our ECC backends. If necessary, it can be generated from scratch -# with the following code: +# algorithm, which we use to test our ECC backends. If necessary, it can be generated +# from scratch with the following code: """ from devp2p import crypto from eth_utils import encode_hex @@ -35,48 +33,72 @@ SECRETS = { "alice": dict( - privkey=decode_hex('0x9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501'), - pubkey=decode_hex('0x5eed5fa3a67696c334762bb4823e585e2ee579aba3558d9955296d6c04541b426078dbd48d74af1fd0c72aa1a05147cf17be6b60bdbed6ba19b08ec28445b0ca'), # noqa: E501 - compressed_pubkey=decode_hex('0x025eed5fa3a67696c334762bb4823e585e2ee579aba3558d9955296d6c04541b42'), # noqa: E501 - sig=decode_hex('0xb20e2ea5d3cbaa83c1e0372f110cf12535648613b479b64c1a8c1a20c5021f380434d07ec5795e3f789794351658e80b7faf47a46328f41e019d7b853745cdfd01'), # noqa: E501 + privkey=decode_hex( + "0x9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501" + ), + pubkey=decode_hex( + "0x5eed5fa3a67696c334762bb4823e585e2ee579aba3558d9955296d6c04541b426078dbd48d74af1fd0c72aa1a05147cf17be6b60bdbed6ba19b08ec28445b0ca" # noqa: E501 + ), + compressed_pubkey=decode_hex( + "0x025eed5fa3a67696c334762bb4823e585e2ee579aba3558d9955296d6c04541b42" + ), + sig=decode_hex( + "0xb20e2ea5d3cbaa83c1e0372f110cf12535648613b479b64c1a8c1a20c5021f380434d07ec5795e3f789794351658e80b7faf47a46328f41e019d7b853745cdfd01" # noqa: E501 + ), raw_sig=( 1, - 80536744857756143861726945576089915884233437828013729338039544043241440681784, - 1902566422691403459035240420865094128779958320521066670269403689808757640701, - ) + 80536744857756143861726945576089915884233437828013729338039544043241440681784, # noqa: E501 + 1902566422691403459035240420865094128779958320521066670269403689808757640701, # noqa: E501 + ), ), "bob": dict( - privkey=decode_hex('0x38e47a7b719dce63662aeaf43440326f551b8a7ee198cee35cb5d517f2d296a2'), - pubkey=decode_hex('0x347746ccb908e583927285fa4bd202f08e2f82f09c920233d89c47c79e48f937d049130e3d1c14cf7b21afefc057f71da73dec8e8ff74ff47dc6a574ccd5d570'), # noqa: E501 - compressed_pubkey=decode_hex('0x02347746ccb908e583927285fa4bd202f08e2f82f09c920233d89c47c79e48f937'), # noqa: E501 - sig=decode_hex('0x5c48ea4f0f2257fa23bd25e6fcb0b75bbe2ff9bbda0167118dab2bb6e31ba76e691dbdaf2a231fc9958cd8edd99507121f8184042e075cf10f98ba88abff1f3601'), # noqa: E501 + privkey=decode_hex( + "0x38e47a7b719dce63662aeaf43440326f551b8a7ee198cee35cb5d517f2d296a2" + ), + pubkey=decode_hex( + "0x347746ccb908e583927285fa4bd202f08e2f82f09c920233d89c47c79e48f937d049130e3d1c14cf7b21afefc057f71da73dec8e8ff74ff47dc6a574ccd5d570" # noqa: E501 + ), + compressed_pubkey=decode_hex( + "0x02347746ccb908e583927285fa4bd202f08e2f82f09c920233d89c47c79e48f937" + ), + sig=decode_hex( + "0x5c48ea4f0f2257fa23bd25e6fcb0b75bbe2ff9bbda0167118dab2bb6e31ba76e691dbdaf2a231fc9958cd8edd99507121f8184042e075cf10f98ba88abff1f3601" # noqa: E501 + ), raw_sig=( 1, - 41741612198399299636429810387160790514780876799439767175315078161978521003886, - 47545396818609319588074484786899049290652725314938191835667190243225814114102, + 41741612198399299636429810387160790514780876799439767175315078161978521003886, # noqa: E501 + 47545396818609319588074484786899049290652725314938191835667190243225814114102, # noqa: E501 ), ), "eve": dict( - privkey=decode_hex('0x876be0999ed9b7fc26f1b270903ef7b0c35291f89407903270fea611c85f515c'), - pubkey=decode_hex('0xc06641f0d04f64dba13eac9e52999f2d10a1ff0ca68975716b6583dee0318d91e7c2aed363ed22edeba2215b03f6237184833fd7d4ad65f75c2c1d5ea0abecc0'), # noqa: E501 - compressed_pubkey=decode_hex('0x02c06641f0d04f64dba13eac9e52999f2d10a1ff0ca68975716b6583dee0318d91'), # noqa: E501 - sig=decode_hex('0xbabeefc5082d3ca2e0bc80532ab38f9cfb196fb9977401b2f6a98061f15ed603603d0af084bf906b2cdf6cdde8b2e1c3e51a41af5e9adec7f3643b3f1aa2aadf00'), # noqa: E501 + privkey=decode_hex( + "0x876be0999ed9b7fc26f1b270903ef7b0c35291f89407903270fea611c85f515c" + ), + pubkey=decode_hex( + "0xc06641f0d04f64dba13eac9e52999f2d10a1ff0ca68975716b6583dee0318d91e7c2aed363ed22edeba2215b03f6237184833fd7d4ad65f75c2c1d5ea0abecc0" # noqa: E501 + ), + compressed_pubkey=decode_hex( + "0x02c06641f0d04f64dba13eac9e52999f2d10a1ff0ca68975716b6583dee0318d91" + ), + sig=decode_hex( + "0xbabeefc5082d3ca2e0bc80532ab38f9cfb196fb9977401b2f6a98061f15ed603603d0af084bf906b2cdf6cdde8b2e1c3e51a41af5e9adec7f3643b3f1aa2aadf00" # noqa: E501 + ), raw_sig=( 0, - 84467545608142925331782333363288012579669270632210954476013542647119929595395, - 43529886636775750164425297556346136250671451061152161143648812009114516499167, + 84467545608142925331782333363288012579669270632210954476013542647119929595395, # noqa: E501 + 43529886636775750164425297556346136250671451061152161143648812009114516499167, # noqa: E501 ), ), } -@pytest.fixture(params=['alice', 'bob', 'eve']) +@pytest.fixture(params=["alice", "bob", "eve"]) def key_fixture(request): - if request.param == 'alice': - return SECRETS['alice'] - elif request.param == 'bob': - return SECRETS['bob'] - elif request.param == 'eve': - return SECRETS['eve'] + if request.param == "alice": + return SECRETS["alice"] + elif request.param == "bob": + return SECRETS["bob"] + elif request.param == "eve": + return SECRETS["eve"] else: - assert False, "Should be unreachable" + raise AssertionError("Should be unreachable") diff --git a/tests/backends/strategies.py b/tests/backends/strategies.py index 6e896b7..f7b32cb 100644 --- a/tests/backends/strategies.py +++ b/tests/backends/strategies.py @@ -1,10 +1,9 @@ -from hypothesis import ( - strategies as st, -) - from eth_utils import ( int_to_big_endian, ) +from hypothesis import ( + strategies as st, +) from eth_keys.constants import ( SECPK1_N, @@ -13,10 +12,13 @@ pad32, ) - -private_key_st = st.integers(min_value=1, max_value=SECPK1_N).map( - int_to_big_endian, -).map(pad32) +private_key_st = ( + st.integers(min_value=1, max_value=SECPK1_N) + .map( + int_to_big_endian, + ) + .map(pad32) +) message_hash_st = st.binary(min_size=32, max_size=32) diff --git a/tests/backends/test_backends.py b/tests/backends/test_backends.py index 2ba8c19..b2e1e3e 100644 --- a/tests/backends/test_backends.py +++ b/tests/backends/test_backends.py @@ -1,33 +1,35 @@ import os -import pytest - +from eth_utils import ( + keccak, +) from hypothesis import ( given, ) +import pytest +from strategies import ( + message_hash_st, + private_key_st, +) -from eth_keys import KeyAPI -from eth_keys.backends import CoinCurveECCBackend -from eth_keys.backends import NativeECCBackend +from eth_keys import ( + KeyAPI, +) +from eth_keys.backends import ( + CoinCurveECCBackend, + NativeECCBackend, +) from eth_keys.constants import ( SECPK1_N, ) -from eth_keys.exceptions import BadSignature +from eth_keys.exceptions import ( + BadSignature, +) from eth_keys.utils.numeric import ( coerce_low_s, ) -from eth_utils import ( - keccak, -) - -from strategies import ( - private_key_st, - message_hash_st, -) - - -MSG = b'message' +MSG = b"message" MSGHASH = keccak(MSG) @@ -36,10 +38,11 @@ ] try: - import coincurve + pass + backends.append(CoinCurveECCBackend()) except ImportError: - if 'REQUIRE_COINCURVE' in os.environ: + if "REQUIRE_COINCURVE" in os.environ: raise @@ -47,20 +50,20 @@ def backend_id_fn(backend): return type(backend).__name__ -@pytest.fixture(params=backends, ids=backend_id_fn, scope='module') +@pytest.fixture(params=backends, ids=backend_id_fn, scope="module") def key_api(request): return KeyAPI(backend=request.param) def test_ecdsa_sign(key_api, key_fixture): - private_key = key_api.PrivateKey(key_fixture['privkey']) + private_key = key_api.PrivateKey(key_fixture["privkey"]) signature = key_api.ecdsa_sign(MSGHASH, private_key) assert key_api.ecdsa_verify(MSGHASH, signature, private_key.public_key) def test_ecdsa_sign_non_recoverable(key_api, key_fixture): - private_key = key_api.PrivateKey(key_fixture['privkey']) + private_key = key_api.PrivateKey(key_fixture["privkey"]) signature = key_api.ecdsa_sign_non_recoverable(MSGHASH, private_key) non_recoverable_signature = key_api.ecdsa_sign_non_recoverable(MSGHASH, private_key) assert non_recoverable_signature.r == signature.r @@ -70,35 +73,43 @@ def test_ecdsa_sign_non_recoverable(key_api, key_fixture): def test_ecdsa_verify(key_api, key_fixture): - signature = key_api.Signature(vrs=key_fixture['raw_sig']) - public_key = key_api.PublicKey(key_fixture['pubkey']) + signature = key_api.Signature(vrs=key_fixture["raw_sig"]) + public_key = key_api.PublicKey(key_fixture["pubkey"]) assert key_api.ecdsa_verify(MSGHASH, signature, public_key) def test_ecdsa_recover(key_api, key_fixture): - signature = key_api.Signature(vrs=key_fixture['raw_sig']) - public_key = key_api.PublicKey(key_fixture['pubkey']) + signature = key_api.Signature(vrs=key_fixture["raw_sig"]) + public_key = key_api.PublicKey(key_fixture["pubkey"]) assert key_api.ecdsa_recover(MSGHASH, signature) == public_key @pytest.mark.parametrize( - 'v,r,s,msghash', + "v,r,s,msghash", ( ( # Test params from the ethereum/tests repo 0, - int("0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", 16), - int("0x6b8d2c81b11b2d699528dde488dbdf2f94293d0d33c32e347f255fa4a6c1f0a9", 16), - bytes.fromhex("6b8d2c81b11b2d699528dde488dbdf2f94293d0d33c32e347f255fa4a6c1f0a9") + int( + "0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", 16 + ), + int( + "0x6b8d2c81b11b2d699528dde488dbdf2f94293d0d33c32e347f255fa4a6c1f0a9", 16 + ), + bytes.fromhex( + "6b8d2c81b11b2d699528dde488dbdf2f94293d0d33c32e347f255fa4a6c1f0a9" + ), ), ( # Test params from signing a message using the private key: '0x' + '00' * 32 1, - 29836180350949573232951565573845061551093497675544587026406064656720118638890, - 36811185137926304485684021052401800813557229270017380070056792973957337676018, - bytes.fromhex('1476abb745d423bf09273f1afd887d951181d25adc66c4834a70491911b7f750'), + 29836180350949573232951565573845061551093497675544587026406064656720118638890, # noqa: E501 + 36811185137926304485684021052401800813557229270017380070056792973957337676018, # noqa: E501 + bytes.fromhex( + "1476abb745d423bf09273f1afd887d951181d25adc66c4834a70491911b7f750" + ), ), ), ) @@ -106,20 +117,20 @@ def test_ecdsa_recover_identity_point(key_api, key_fixture, v, r, s, msghash): signature = key_api.Signature(vrs=(v, r, s)) with pytest.raises(BadSignature): - key_api.ecdsa_recover(msghash, signature) + key_api.ecdsa_recover(msghash, signature) def test_decompress_public_key_bytes(key_api, key_fixture): - compressed = key_fixture['compressed_pubkey'] - uncompressed = key_fixture['pubkey'] + compressed = key_fixture["compressed_pubkey"] + uncompressed = key_fixture["pubkey"] key_from_compressed = key_api.PublicKey.from_compressed_bytes(compressed) assert key_from_compressed.to_bytes() == uncompressed def test_compress_public_key_bytes(key_api, key_fixture): - uncompressed = key_fixture['pubkey'] - compressed = key_fixture['compressed_pubkey'] + uncompressed = key_fixture["pubkey"] + compressed = key_fixture["compressed_pubkey"] key_from_uncompressed = key_api.PublicKey(uncompressed) assert key_from_uncompressed.to_compressed_bytes() == compressed @@ -147,5 +158,7 @@ def test_signatures_with_high_s(key_api, private_key_bytes, message_hash): assert coerce_low_s(low_s_signature.s) == low_s_signature.s high_s = -low_s_signature.s % SECPK1_N assert coerce_low_s(high_s) == low_s_signature.s - high_s_signature = key_api.Signature(vrs=(low_s_signature.v, low_s_signature.r, high_s)) + high_s_signature = key_api.Signature( + vrs=(low_s_signature.v, low_s_signature.r, high_s) + ) assert key_api.ecdsa_verify(message_hash, high_s_signature, private_key.public_key) diff --git a/tests/backends/test_native_backend_against_coincurve.py b/tests/backends/test_native_backend_against_coincurve.py index 2980386..e89ead0 100644 --- a/tests/backends/test_native_backend_against_coincurve.py +++ b/tests/backends/test_native_backend_against_coincurve.py @@ -1,42 +1,41 @@ -import pytest - +from eth_utils import ( + keccak, +) from hypothesis import ( given, settings, strategies as st, ) - -from eth_utils import ( - keccak, -) - -from eth_keys.exceptions import ( - BadSignature, -) - -from eth_keys import KeyAPI -from eth_keys.backends import CoinCurveECCBackend -from eth_keys.backends import NativeECCBackend - +import pytest from strategies import ( - private_key_st, message_hash_st, + private_key_st, signature_st, ) +from eth_keys import ( + KeyAPI, +) +from eth_keys.backends import ( + CoinCurveECCBackend, + NativeECCBackend, +) +from eth_keys.exceptions import ( + BadSignature, +) -MSG = b'message' +MSG = b"message" MSGHASH = keccak(MSG) MAX_EXAMPLES = 200 -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def native_key_api(): return KeyAPI(backend=NativeECCBackend()) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def coincurve_key_api(): return KeyAPI(backend=CoinCurveECCBackend()) @@ -45,9 +44,9 @@ def coincurve_key_api(): private_key_bytes=private_key_st, ) @settings(max_examples=MAX_EXAMPLES) -def test_public_key_generation_is_equal(private_key_bytes, - native_key_api, - coincurve_key_api): +def test_public_key_generation_is_equal( + private_key_bytes, native_key_api, coincurve_key_api +): native_public_key = native_key_api.PrivateKey(private_key_bytes).public_key coincurve_public_key = coincurve_key_api.PrivateKey(private_key_bytes).public_key @@ -59,10 +58,9 @@ def test_public_key_generation_is_equal(private_key_bytes, message_hash=message_hash_st, ) @settings(max_examples=MAX_EXAMPLES) -def test_signing_is_equal(private_key_bytes, - message_hash, - native_key_api, - coincurve_key_api): +def test_signing_is_equal( + private_key_bytes, message_hash, native_key_api, coincurve_key_api +): native_private_key = native_key_api.PrivateKey(private_key_bytes) native_signature = native_key_api.ecdsa_sign(message_hash, native_private_key) native_non_recoverable_signature = native_key_api.ecdsa_sign_non_recoverable( @@ -71,7 +69,9 @@ def test_signing_is_equal(private_key_bytes, ) coincurve_private_key = coincurve_key_api.PrivateKey(private_key_bytes) - coincurve_signature = coincurve_key_api.ecdsa_sign(message_hash, coincurve_private_key) + coincurve_signature = coincurve_key_api.ecdsa_sign( + message_hash, coincurve_private_key + ) coincurve_non_recoverable_signature = coincurve_key_api.ecdsa_sign_non_recoverable( message_hash, coincurve_private_key, @@ -85,24 +85,22 @@ def test_signing_is_equal(private_key_bytes, private_key_bytes=private_key_st, message_hash=message_hash_st, direction=st.one_of( - st.just('coincurve-to-native'), - st.just('native-to-coincurve'), + st.just("coincurve-to-native"), + st.just("native-to-coincurve"), ), ) @settings(max_examples=MAX_EXAMPLES) -def test_native_to_coincurve_recover(private_key_bytes, - message_hash, - direction, - native_key_api, - coincurve_key_api): - if direction == 'coincurve-to-native': +def test_native_to_coincurve_recover( + private_key_bytes, message_hash, direction, native_key_api, coincurve_key_api +): + if direction == "coincurve-to-native": backend_a = coincurve_key_api backend_b = native_key_api - elif direction == 'native-to-coincurve': + elif direction == "native-to-coincurve": backend_b = coincurve_key_api backend_a = native_key_api else: - assert False, "invariant" + raise AssertionError("invariant") private_key_a = backend_a.PrivateKey(private_key_bytes) public_key_a = private_key_a.public_key @@ -117,24 +115,22 @@ def test_native_to_coincurve_recover(private_key_bytes, message_hash=message_hash_st, signature_bytes=signature_st, direction=st.one_of( - st.just('coincurve-to-native'), - st.just('native-to-coincurve'), + st.just("coincurve-to-native"), + st.just("native-to-coincurve"), ), ) @settings(max_examples=MAX_EXAMPLES) -def test_coincurve_to_native_invalid_signatures(message_hash, - signature_bytes, - direction, - native_key_api, - coincurve_key_api): - if direction == 'coincurve-to-native': +def test_coincurve_to_native_invalid_signatures( + message_hash, signature_bytes, direction, native_key_api, coincurve_key_api +): + if direction == "coincurve-to-native": backend_a = coincurve_key_api backend_b = native_key_api - elif direction == 'native-to-coincurve': + elif direction == "native-to-coincurve": backend_b = coincurve_key_api backend_a = native_key_api else: - assert False, "invariant" + raise AssertionError("invariant") try: signature_a = backend_a.Signature(signature_bytes) @@ -167,9 +163,9 @@ def test_coincurve_to_native_invalid_signatures(message_hash, @given( private_key_bytes=private_key_st, ) -def test_public_key_compression_is_equal(private_key_bytes, - native_key_api, - coincurve_key_api): +def test_public_key_compression_is_equal( + private_key_bytes, native_key_api, coincurve_key_api +): native_public_key = native_key_api.PrivateKey(private_key_bytes).public_key coincurve_public_key = coincurve_key_api.PrivateKey(private_key_bytes).public_key @@ -182,13 +178,17 @@ def test_public_key_compression_is_equal(private_key_bytes, @given( private_key_bytes=private_key_st, ) -def test_public_key_decompression_is_equal(private_key_bytes, - native_key_api, - coincurve_key_api): +def test_public_key_decompression_is_equal( + private_key_bytes, native_key_api, coincurve_key_api +): public_key_template = coincurve_key_api.PrivateKey(private_key_bytes).public_key compressed_public_key = public_key_template.to_compressed_bytes() - native_public_key = native_key_api.PublicKey.from_compressed_bytes(compressed_public_key) - coincurve_public_key = coincurve_key_api.PublicKey.from_compressed_bytes(compressed_public_key) + native_public_key = native_key_api.PublicKey.from_compressed_bytes( + compressed_public_key + ) + coincurve_public_key = coincurve_key_api.PublicKey.from_compressed_bytes( + compressed_public_key + ) assert native_public_key == coincurve_public_key diff --git a/tests/conftest.py b/tests/conftest.py index 0e9fd82..e167fb4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,13 @@ import pytest - -# Change COLLECT_TYPE_INFO to True here, and run the tests with python2.7 to get a type info dump -# that can later be fed to pyannotate to generate type annotation comments. +# Change COLLECT_TYPE_INFO to True here, and run the tests with python2.7 to get a type +# info dump that can later be fed to pyannotate to generate type annotation comments. COLLECT_TYPE_INFO = False if COLLECT_TYPE_INFO: - from pyannotate_runtime import collect_types + from pyannotate_runtime import ( + collect_types, + ) @pytest.fixture(autouse=True) def collect_types_fixture(): @@ -14,10 +15,8 @@ def collect_types_fixture(): yield collect_types.pause() - def pytest_sessionstart(session): collect_types.init_types_collection() - def pytest_sessionfinish(session, exitstatus): collect_types.dump_stats("type_info.json") diff --git a/tests/core/test_factories.py b/tests/core/test_factories.py index b74f2d2..1855665 100644 --- a/tests/core/test_factories.py +++ b/tests/core/test_factories.py @@ -1,4 +1,6 @@ -from eth_keys import keys +from eth_keys import ( + keys, +) from eth_keys.tools.factories import ( PrivateKeyFactory, PublicKeyFactory, diff --git a/tests/core/test_import.py b/tests/core/test_import.py new file mode 100644 index 0000000..4370cd1 --- /dev/null +++ b/tests/core/test_import.py @@ -0,0 +1,2 @@ +def test_import(): + import eth_keys # noqa: F401 diff --git a/tests/core/test_key_and_signature_datastructures.py b/tests/core/test_key_and_signature_datastructures.py index bfc7dc0..9976a38 100644 --- a/tests/core/test_key_and_signature_datastructures.py +++ b/tests/core/test_key_and_signature_datastructures.py @@ -1,24 +1,26 @@ -from __future__ import unicode_literals - -import pytest - from eth_utils import ( + ValidationError, decode_hex, encode_hex, - keccak, - is_same_address, - is_normalized_address, - is_checksum_address, is_canonical_address, - ValidationError, + is_checksum_address, + is_normalized_address, + is_same_address, + keccak, ) +import pytest -from eth_keys import KeyAPI -from eth_keys.backends import NativeECCBackend -from eth_keys.exceptions import ValidationError as EthKeysValidationErrorCopy - +from eth_keys import ( + KeyAPI, +) +from eth_keys.backends import ( + NativeECCBackend, +) +from eth_keys.exceptions import ( + ValidationError as EthKeysValidationErrorCopy, +) -MSG = b'message' +MSG = b"message" MSGHASH = keccak(MSG) @@ -28,9 +30,9 @@ def key_api(): PK_BYTES = decode_hex( - '0x58d23b55bc9cdce1f18c2500f40ff4ab7245df9a89505e9b1fa4851f623d241d' + "0x58d23b55bc9cdce1f18c2500f40ff4ab7245df9a89505e9b1fa4851f623d241d" ) -ADDRESS = '0xdc544d1aa88ff8bbd2f2aec754b1f1e99e1812fd' +ADDRESS = "0xdc544d1aa88ff8bbd2f2aec754b1f1e99e1812fd" @pytest.fixture @@ -129,7 +131,7 @@ def test_to_canonical_address_from_public_key(private_key): def test_hex_conversion(private_key): public_key = private_key.public_key - signature = private_key.sign_msg(b'message') + signature = private_key.sign_msg(b"message") assert hex(public_key) == encode_hex(public_key.to_bytes()) assert hex(private_key) == encode_hex(private_key.to_bytes()) @@ -142,7 +144,7 @@ def test_hex_conversion(private_key): def test_bytes_conversion(key_api, private_key): public_key = private_key.public_key - signature = private_key.sign_msg(b'message') + signature = private_key.sign_msg(b"message") assert public_key.to_bytes() == public_key._raw_key assert private_key.to_bytes() == private_key._raw_key @@ -156,7 +158,9 @@ def test_compressed_bytes_conversion(key_api, private_key): assert key_api.PublicKey.from_compressed_bytes(compressed_bytes) == public_key -@pytest.mark.parametrize('validation_error', (ValidationError, EthKeysValidationErrorCopy)) +@pytest.mark.parametrize( + "validation_error", (ValidationError, EthKeysValidationErrorCopy) +) def test_compressed_bytes_validation(key_api, private_key, validation_error): valid_key = private_key.public_key.to_compressed_bytes() diff --git a/tests/core/test_key_api_proxy_methods.py b/tests/core/test_key_api_proxy_methods.py index 3f9dfd6..dc4cffd 100644 --- a/tests/core/test_key_api_proxy_methods.py +++ b/tests/core/test_key_api_proxy_methods.py @@ -1,17 +1,19 @@ -import pytest - from eth_utils import ( - keccak, ValidationError, + keccak, ) +import pytest -from eth_keys import KeyAPI -from eth_keys.backends import NativeECCBackend - +from eth_keys import ( + KeyAPI, +) +from eth_keys.backends import ( + NativeECCBackend, +) -MSG = b'message' +MSG = b"message" MSGHASH = keccak(MSG) -PK_BYTES = b'\x01' * 32 +PK_BYTES = b"\x01" * 32 @pytest.fixture diff --git a/tests/core/test_keyapi_backend_formats.py b/tests/core/test_keyapi_backend_formats.py index 60f0350..c0ec81e 100644 --- a/tests/core/test_keyapi_backend_formats.py +++ b/tests/core/test_keyapi_backend_formats.py @@ -1,22 +1,26 @@ import pytest -from eth_keys import KeyAPI -from eth_keys.backends import NativeECCBackend +from eth_keys import ( + KeyAPI, +) +from eth_keys.backends import ( + NativeECCBackend, +) @pytest.fixture(autouse=True) def native_backend_env_var(monkeypatch): - monkeypatch.setenv('ECC_BACKEND_CLASS', 'eth_keys.backends.native.NativeECCBackend') + monkeypatch.setenv("ECC_BACKEND_CLASS", "eth_keys.backends.native.NativeECCBackend") @pytest.mark.parametrize( - 'backend', + "backend", ( None, NativeECCBackend(), NativeECCBackend, - 'eth_keys.backends.NativeECCBackend', - 'eth_keys.backends.native.NativeECCBackend', + "eth_keys.backends.NativeECCBackend", + "eth_keys.backends.native.NativeECCBackend", ), ) def test_supported_backend_formats(backend): diff --git a/tests/core/test_utils_der.py b/tests/core/test_utils_der.py index 297925b..0afe8ee 100644 --- a/tests/core/test_utils_der.py +++ b/tests/core/test_utils_der.py @@ -1,21 +1,19 @@ -import itertools -import pytest - import asn1tools from hypothesis import ( example, + given, settings, strategies as st, - given, ) from pyasn1.codec.der import ( - encoder as pyasn1_encoder, decoder as pyasn1_decoder, + encoder as pyasn1_encoder, ) from pyasn1.type import ( - univ, namedtype, + univ, ) +import pytest from eth_keys.utils.der import ( two_int_sequence_decoder, @@ -44,8 +42,8 @@ def asn1tools_decode(encoded): class TwoInts(univ.Sequence): componentType = namedtype.NamedTypes( - namedtype.NamedType('r', univ.Integer()), - namedtype.NamedType('s', univ.Integer()), + namedtype.NamedType("r", univ.Integer()), + namedtype.NamedType("s", univ.Integer()), ) @@ -61,11 +59,12 @@ def pyasn1_decode(encoded): return decoded[0]["r"], decoded[0]["s"] -MAX_32_BYTE_INT = 256 ** 32 - 1 +MAX_32_BYTE_INT = 256**32 - 1 uint32strategy = st.integers(min_value=0, max_value=MAX_32_BYTE_INT) + @pytest.mark.parametrize( - 'encoder, decoder', + "encoder, decoder", ( (two_int_sequence_encoder, asn1tools_decode), (two_int_sequence_encoder, pyasn1_decode), @@ -74,11 +73,11 @@ def pyasn1_decode(encoded): (pyasn1_encode, two_int_sequence_decoder), ), ids=( - 'local_encode=>asn1tools_decode', - 'local_encode=>pyasn1_decode', - 'local_encode=>local_decode', - 'asn1tools_encode=>local_decode', - 'pyasn1_encode=>local_decode', + "local_encode=>asn1tools_decode", + "local_encode=>pyasn1_decode", + "local_encode=>local_decode", + "asn1tools_encode=>local_decode", + "pyasn1_encode=>local_decode", ), ) @given( diff --git a/tox.ini b/tox.ini index 8898877..6c528ae 100644 --- a/tox.ini +++ b/tox.ini @@ -1,42 +1,78 @@ [tox] envlist= - py{36,37,38,39,310}-core - py{36,37,38,39,310}-backends-coincurve{7,8,9,10,11,12,13,14,15} - pypy3-core - lint + py38-backends-coincurve{12,13,14} + py{38,39}-backends-coincurve15 + py{38,39,10}-backends-coincurve{16,17} + py{38,39,10,11}-backends-coincurve18 + py{38,39,310,311}-core + py{38,39,310,311}-lint + py{38,39,310,311}-wheel + py311-wheel-windows + docs [flake8] -max-line-length= 100 -exclude= tests/* +exclude=venv*,.tox,docs,build +extend-ignore=E203 +max-line-length=88 +per-file-ignores=__init__.py:F401 [testenv] usedevelop=True commands= - core: py.test {posargs:tests/core} - backends: py.test {posargs:tests/backends} -deps = .[test] - coincurve7: coincurve>=7.0.0,<8.0.0 - coincurve8: coincurve>=8.0.0,<9.0.0 - coincurve9: coincurve>=9.0.0,<10.0.0 - coincurve10: coincurve>=10.0.0,<11.0.0 - coincurve11: coincurve>=11.0.0,<12.0.0 + core: pytest {posargs:tests/core} + backends: pytest {posargs:tests/backends} + docs: make docs +basepython= + docs: python + py38: python3.8 + py39: python3.9 + py310: python3.10 + py311: python3.11 +deps= .[test] coincurve12: coincurve>=12.0.0,<13.0.0 coincurve13: coincurve>=13.0.0,<14.0.0 coincurve14: coincurve>=14.0.0,<15.0.0 coincurve15: coincurve>=15.0.0,<16.0.0 -setenv = + coincurve16: coincurve>=16.0.0,<17.0.0 + coincurve17: coincurve>=17.0.0,<18.0.0 + coincurve18: coincurve>=18.0.0,<19.0.0 +setenv= backends: REQUIRE_COINCURVE=True -basepython = - py36: python3.6 - py37: python3.7 - py38: python3.8 - py39: python3.9 - py310: python3.10 - pypy3: pypy3 +extras= + test + docs +allowlist_externals=make,pre-commit + +[testenv:py{38,39,310,311}-lint] +deps=pre-commit +commands= + pre-commit run --all-files --show-diff-on-failure + +[testenv:py{38,39,310,311}-wheel] +deps= + wheel + build[virtualenv] +allowlist_externals= + /bin/rm + /bin/bash +commands= + python -m pip install --upgrade pip + /bin/rm -rf build dist + python -m build + /bin/bash -c 'python -m pip install --upgrade "$(ls dist/eth_keys-*-py3-none-any.whl)" --progress-bar off' + python -c "import eth_keys" +skip_install=true -[testenv:lint] -basepython = python3.6 -deps=.[lint] +[testenv:py311-wheel-windows] +deps= + wheel + build[virtualenv] +allowlist_externals= + bash.exe commands= - flake8 {toxinidir}/eth_keys {toxinidir}/setup.py - mypy -p eth_keys --config-file {toxinidir}/mypy.ini + python -m pip install --upgrade pip + bash.exe -c "rm -rf build dist" + python -m build + bash.exe -c 'python -m pip install --upgrade "$(ls dist/eth_keys-*-py3-none-any.whl)" --progress-bar off' + python -c "import eth_keys" +skip_install=true