diff --git a/.github/ISSUE_TEMPLATE/blank-issue.md b/.github/ISSUE_TEMPLATE/blank-issue.md new file mode 100644 index 0000000000..9aef3ebe63 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/blank-issue.md @@ -0,0 +1,4 @@ +--- +name: Blank Issue +about: Create a blank issue. +--- diff --git a/.github/ISSUE_TEMPLATE/incorrect-documentation.md b/.github/ISSUE_TEMPLATE/incorrect-documentation.md new file mode 100644 index 0000000000..9affd93113 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/incorrect-documentation.md @@ -0,0 +1,34 @@ +--- +name: Incorrect/Incomplete Documentation +about: Report that the documentation is wrong or incomplete +title: '' +labels: C-bug, A-doc +assignees: '' + +--- + +### Metadata + +* **Hardfork**: hardfork-name + +### What was wrong? + + + +### Sources + + + +### Additional Context + + diff --git a/.github/ISSUE_TEMPLATE/incorrect-specification.md b/.github/ISSUE_TEMPLATE/incorrect-specification.md new file mode 100644 index 0000000000..bffc78ff12 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/incorrect-specification.md @@ -0,0 +1,33 @@ +--- +name: Incorrect Specification +about: Create a report showing the specification is wrong +title: '' +labels: C-bug, A-spec +assignees: '' + +--- + +### Metadata + +* **Hardfork**: hardfork-name + +### What was wrong? + + + +### Sources + + + +### Additional Context + + diff --git a/.github/ISSUE_TEMPLATE/tooling-problem.md b/.github/ISSUE_TEMPLATE/tooling-problem.md new file mode 100644 index 0000000000..044736d1f7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/tooling-problem.md @@ -0,0 +1,46 @@ +--- +name: Tooling Problem +about: Create a report of a problem with the project tooling +title: '' +labels: C-bug, A-tool +assignees: '' + +--- + +### Metadata + + * Python: + * Operating System: + +#### `pip freeze` + + + +``` + +``` + +### What was wrong? + + + + +### How can it be fixed? + + + +### Additional Context + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..dc3cde9939 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +### What was wrong? + +Related to Issue # + +### How was it fixed? + + +#### Cute Animal Picture + +![Put a link to a cute animal picture inside the parenthesis-->]() diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml new file mode 100644 index 0000000000..48e7dc344e --- /dev/null +++ b/.github/workflows/gh-pages.yaml @@ -0,0 +1,31 @@ +name: GitHub Pages + +on: + push: + branches: + - master + - pyspec + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: "3.7" + + - name: Checkout + uses: actions/checkout@v2.3.1 + + - name: Install Tox and any other packages + run: pip install tox + + - name: Sphinx + run: tox -e doc + + - name: Deploy + uses: JamesIves/github-pages-deploy-action@4.1.4 + with: + branch: gh-pages + folder: .tox/docs_out diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000000..74bdd209f6 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,37 @@ +name: Python Specification + +on: [push, pull_request, workflow_dispatch] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.7, pypy-3.7] + + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Tox and any other packages + run: pip install tox + - name: Run Tox (PyPy) + if: ${{ matrix.python == 'pypy-3.7' }} + run: tox -e pypy3 + - name: Run Tox (CPython) + if: ${{ matrix.python != 'pypy-3.7' }} + run: tox -e py3 + - name: Upload coverage to Codecov + if: ${{ matrix.python != 'pypy-3.7' }} + uses: codecov/codecov-action@v1 + with: + files: .tox/coverage.xml + flags: unittests + - name: Build docs + if: ${{ matrix.python != 'pypy-3.7' }} + run: tox -e doc diff --git a/.gitignore b/.gitignore index e43b0f9889..7a1c64b6c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,42 @@ .DS_Store +__pycache__ +.AppleDouble +.LSOverride + +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +*.manifest +*.spec + +pip-log.txt +pip-delete-this-directory.txt + +.tox/ + +/doc/_autosummary + +.coverage diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..6ffbb269cb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "tests/fixtures"] + path = tests/fixtures + url = https://github.com/ethereum/tests + branch = develop diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000..ea14511312 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,35 @@ +# Creative Commons Legal Code + +## CC0 1.0 Universal + +> CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: + + 1. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; + 2. moral rights retained by the original author(s) and/or performer(s); + 3. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; + 4. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; + 5. rights protecting the extraction, dissemination, use and reuse of data in a Work; + 6. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and + 7. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + 1. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. + 2. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. + 3. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. + 4. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. + diff --git a/README.md b/README.md index 98e0d76f39..357be75216 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ ## Description -This repository contains various specification related to the Ethereum 1.0 chain, specifically the specifications for [network upgrades](/network-upgrades) and the [JSON RPC API](/json-rpc). +This repository contains various specification related to the Ethereum 1.0 chain, specifically the [pyspec](/src/eth1spec/spec.py), specifications for [network upgrades](/network-upgrades), and the [JSON RPC API](/json-rpc). -## Ethereum Protocol Releases +### Ethereum Protocol Releases | Version and Code Name | Block No. | Released | Incl EIPs | Specs | Blog | |-----------------------|-----------|----------|-----------|-------|-------| @@ -23,4 +23,82 @@ This repository contains various specification related to the Ethereum 1.0 chain | Frontier Thawing | 200000 | 09/07/2015 | | | [Blog](https://blog.ethereum.org/2015/08/04/the-thawing-frontier/) | | Frontier | 1 | 07/30/2015 | | | [Blog](https://blog.ethereum.org/2015/07/22/frontier-is-coming-what-to-expect-and-how-to-prepare/) | +## Consensus Specification (work-in-progress) +The consensus specification is a python implementation of Ethereum that prioritizes readability and simplicity. It [will] accompanied by both narrative and API level documentation of the various components written in restructured text and rendered using Sphinx.... + + * [Rendered specification](https://quilt.github.io/eth1.0-specs/) + +## Usage + +The Ethereum specification is maintained as a Python library, for better integration with tooling and testing. + +Requires Python 3.7+ + +### Building + +Building the documentation is most easily done through [`tox`](https://tox.readthedocs.io/en/latest/): + +```bash +$ tox -e doc +``` + +The path to the generated HTML will be printed to the console. + +#### Live Preview + +A live preview of the documentation can be viewed locally on port `8000` with the following command: + +```bash +$ tox -e doc-autobuild +``` + +### Development + +Running the tests necessary to merge into the repository requires: + + * Python 3.7.x (not 3.8 or later), and + * [PyPy 7.3.x](https://www.pypy.org/). + +These version ranges are necessary because, at the time of writing, PyPy is only compatible with Python 3.7. + +`eth1.0-specs` depends on a submodule that contains common tests that are run across all clients, so we need to clone the repo with the --recursive flag. Example: +```bash +$ git clone --recursive https://github.com/quilt/eth1.0-specs.git +``` + +Or, if you've already cloned the repository, you can fetch the submodules with: + +```bash +$ git submodule update --init --recursive +``` + +The tests can be run with: +```bash +$ tox +``` + +The development tools can also be run outside of `tox`, and can automatically reformat the code: + +```bash +$ pip install -e .[doc,lint,test] # Installs eth1spec, and development tools. +$ isort src # Organizes imports. +$ black src # Formats code. +$ flake8 # Reports style/spelling/documentation errors. +$ mypy src # Verifies type annotations. +$ pytest # Runs tests. +``` + +It is recommended to use a [virtual environment](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/#creating-a-virtual-environment) to keep your system Python installation clean. + +## Contribution Guidelines + +This specification aims to be: + +1. **Correct** - Describe the _intended_ behavior of the Ethereum blockchain, and any deviation from that is a bug. +2. **Complete** - Capture the entirety of _consensus critical_ parts of Ethereum. +3. **Accessible** - Prioritize readability, clarity, and plain language over performance and brevity. + +### Spelling + +Attempt to use descriptive English words (or _very common_ abbreviations) in documentation and identifiers. If necessary, there is a custom dictionary `whitelist.txt`. diff --git a/b2t.sh b/b2t.sh new file mode 100755 index 0000000000..4e22455d39 --- /dev/null +++ b/b2t.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# Translate BlockchainTests into t8n format. +# +# Note: t8n requires that hex values not have leading 0s, so you'll need to +# trim the manually (for now) :(. + +set -euf + +if [ "$#" -ne 2 ]; then + echo "Usage: debug.sh [test_path] [test_name]" + exit +fi + +jq --arg testname "${2}" '.[$testname] | + .alloc = .pre | del(.pre) | + .txs = .blocks[0].transactions | + .env = .genesisBlockHeader | + .env = { + currentCoinbase: .env.coinbase, + currentDifficulty: .env.difficulty, + currentGasLimit: .env.gasLimit, + currentNumber: .env.number, + currentTimestamp: .env.timestamp + } | + {alloc: .alloc, env: .env, txs: .txs }' ${1} + +# evm t8n --input.alloc=stdin --input.env=stdin --input.txs=stdin --output.result=stdout --output.alloc=stdout + diff --git a/doc/_static/css/custom.css b/doc/_static/css/custom.css new file mode 100644 index 0000000000..858edef59d --- /dev/null +++ b/doc/_static/css/custom.css @@ -0,0 +1,47 @@ +/* Headings */ +#package-details h4, #module-details h4 { + border-bottom: 1px solid #888; +} + +/* Code Blocks */ +pre, .class, .function { + font-size: 13px; +} + +pre { + padding: 1.5em; +} + +/* Classes */ +.class { + background: #d5d5ee; + padding: 1.5em; +} + +/* Functions */ +.function .sig { + background: #d0eed0; + padding: 1.5em; + margin: 1em 0; +} + +/* Improve function signatures with type hints */ + +.sig-param:first-of-type()::before { + content: ''; +} + +.sig-param::before { + content: '\A '; + white-space: pre; +} + +/* Prefer increasing width in field lists over wrapping */ +dl.field-list > dt { + word-break: normal; +} + +.sig-param + .sig-paren::before { + content: '\A'; + white-space: pre; +} diff --git a/doc/_templates/autoapi/python/class.rst b/doc/_templates/autoapi/python/class.rst new file mode 100644 index 0000000000..cf894066b9 --- /dev/null +++ b/doc/_templates/autoapi/python/class.rst @@ -0,0 +1,60 @@ +{# + Modified from: + https://github.com/readthedocs/sphinx-autoapi/blob/83b1260e67a69b5c844bcce412e448889223dea8/autoapi/templates/python/class.rst +#} +{% if obj.display %} + +{% if obj.docstring %} +{{ obj.docstring|prepare_docstring }} +{% endif %} + +.. {{ obj.type }}:: {{ obj.short_name }}{% if obj.args %}({{ obj.args }}){% endif %} +{% if obj.constructor %} + +{% for (args, return_annotation) in obj.constructor.overloads %} + {% if args and args.startswith("self, ") %}{% set args = args[6:] %}{% endif %} + {{ " " * (obj.type | length) }} {{ obj.short_name }}{% if args %}({{ args }}){% endif %} +{% endfor %} +{% endif %} + + + {% if obj.bases %} + {% if "show-inheritance" in autoapi_options %} + Bases: {% for base in obj.bases %}{{ base|link_objs }}{% if not loop.last %}, {% endif %}{% endfor %} + {% endif %} + + + {% if "show-inheritance-diagram" in autoapi_options and obj.bases != ["object"] %} + .. autoapi-inheritance-diagram:: {{ obj.obj["full_name"] }} + :parts: 1 + {% if "private-members" in autoapi_options %} + :private-bases: + {% endif %} + + {% endif %} + {% endif %} + {% if "inherited-members" in autoapi_options %} + {% set visible_classes = obj.classes|selectattr("display")|list %} + {% else %} + {% set visible_classes = obj.classes|rejectattr("inherited")|selectattr("display")|list %} + {% endif %} + {% for klass in visible_classes %} + {{ klass.render()|indent(3) }} + {% endfor %} + {% if "inherited-members" in autoapi_options %} + {% set visible_attributes = obj.attributes|selectattr("display")|list %} + {% else %} + {% set visible_attributes = obj.attributes|rejectattr("inherited")|selectattr("display")|list %} + {% endif %} + {% for attribute in visible_attributes %} + {{ attribute.render()|indent(3) }} + {% endfor %} + {% if "inherited-members" in autoapi_options %} + {% set visible_methods = obj.methods|selectattr("display")|list %} + {% else %} + {% set visible_methods = obj.methods|rejectattr("inherited")|selectattr("display")|list %} + {% endif %} + {% for method in visible_methods %} + {{ method.render()|indent(3) }} + {% endfor %} +{% endif %} diff --git a/doc/_templates/autoapi/python/function.rst b/doc/_templates/autoapi/python/function.rst new file mode 100644 index 0000000000..1a860e4a17 --- /dev/null +++ b/doc/_templates/autoapi/python/function.rst @@ -0,0 +1,33 @@ +{# + Modified from: + https://github.com/readthedocs/sphinx-autoapi/blob/83b1260e67a69b5c844bcce412e448889223dea8/autoapi/templates/python/function.rst +#} +{% if obj.display %} + +.. function:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %} + + :noindexentry: +{% for (args, return_annotation) in obj.overloads %} + {{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %} + +{% endfor %} + {% if sphinx_version >= (2, 1) %} + {% for property in obj.properties %} + :{{ property }}: + {% endfor %} + {% endif %} + + {% if obj.docstring %} + {{ obj.docstring|prepare_docstring|indent(3) }} + {% else %} + {% endif %} +{% endif %} + + +{% set suffix = 1 + obj.obj.name|length %} +{% set module_name = obj.obj.full_name[:-suffix] %} +{% set module = obj.app.env.autoapi_objects[module_name] %} + +.. undocinclude:: /../src/{{ module.obj.relative_path }} + :language: {{ module.language }} + :lines: {{ obj.obj.from_line_no }}-{{ obj.obj.to_line_no }} diff --git a/doc/_templates/autoapi/python/module.rst b/doc/_templates/autoapi/python/module.rst new file mode 100644 index 0000000000..2119780c56 --- /dev/null +++ b/doc/_templates/autoapi/python/module.rst @@ -0,0 +1,124 @@ +{# + Modified from: + https://github.com/readthedocs/sphinx-autoapi/blob/83b1260e67a69b5c844bcce412e448889223dea8/autoapi/templates/python/module.rst +#} +{% if not obj.display %} +:orphan: + +{% endif %} +:mod:`{{ obj.name }}` +======={{ "=" * obj.name|length }} + +.. py:module:: {{ obj.name }} + +{% if obj.docstring %} +{{ obj.docstring|prepare_docstring }} +{% endif %} + +{% block subpackages %} +{% set visible_subpackages = obj.subpackages|selectattr("display")|list %} +{% if visible_subpackages %} +Subpackages +----------- +.. toctree:: + :titlesonly: + :maxdepth: 3 + +{% for subpackage in visible_subpackages %} + {{ subpackage.short_name }}/index.rst +{% endfor %} + + +{% endif %} +{% endblock %} +{% block submodules %} +{% set visible_submodules = obj.submodules|selectattr("display")|list %} +{% if visible_submodules %} +Submodules +---------- +.. toctree:: + :titlesonly: + :maxdepth: 1 + +{% for submodule in visible_submodules %} + {{ submodule.short_name }}/index.rst +{% endfor %} + + +{% endif %} +{% endblock %} +{% block content %} +{% if obj.all is not none %} +{% set visible_children = obj.children|selectattr("short_name", "in", obj.all)|list %} +{% elif obj.type is equalto("package") %} +{% set visible_children = obj.children|selectattr("display")|list %} +{% else %} +{% set visible_children = obj.children|selectattr("display")|rejectattr("imported")|list %} +{% endif %} +{% if visible_children %} +{{ obj.type|title }} Contents +{{ "-" * obj.type|length }}--------- + +{% set visible_classes = visible_children|selectattr("type", "equalto", "class")|list %} +{% set visible_functions = visible_children|selectattr("type", "equalto", "function")|list %} +{% set visible_attributes = visible_children|selectattr("type", "equalto", "data")|list %} +{% if "show-module-summary" in autoapi_options and (visible_classes or visible_functions) %} +{% block classes scoped %} +{% if visible_classes %} +Classes +~~~~~~~ + +.. autoapisummary:: + +{% for klass in visible_classes %} + {{ klass.id }} +{% endfor %} + + +{% endif %} +{% endblock %} + +{% block functions scoped %} +{% if visible_functions %} +Functions +~~~~~~~~~ + +.. autoapisummary:: + :nosignatures: + +{% for function in visible_functions %} + {{ function.id }} +{% endfor %} + + +{% endif %} +{% endblock %} + +{% block attributes scoped %} +{% if visible_attributes %} +Attributes +~~~~~~~~~~ + +.. autoapisummary:: + +{% for attribute in visible_attributes %} + {{ attribute.id }} +{% endfor %} + + +{% endif %} +{% endblock %} +{{ obj.type|title }} Details +{{ "-" * obj.type|length }}--------- +{% endif %} +{% for obj_item in visible_children %} +{% if obj_item.display %} + +{{ obj_item.short_name }} +{{ '~' * obj_item.short_name|length }} + +{% endif %} +{{ obj_item.render()|indent(0) }} +{% endfor %} +{% endif %} +{% endblock %} diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000000..249ead970f --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,66 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('../src')) + + +# -- Project information ----------------------------------------------------- + +project = 'Ethereum Specification' +copyright = '2021, Ethereum' +author = 'Ethereum' + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.coverage', + 'sphinx.ext.napoleon', + 'sphinx.ext.autodoc', + 'autoapi.extension', + 'undocinclude.extension', +] + +autoapi_type = 'python' +autoapi_dirs = ['../src/ethereum'] +autoapi_template_dir = '_templates/autoapi' + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +autodoc_typehints = "signature" + +html_css_files = [ + 'css/custom.css', +] diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000000..34cd6fbe17 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,15 @@ +Ethereum Specification +====================== + +.. toctree:: + :maxdepth: 4 + :caption: Contents: + + autoapi/index + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/main.py b/main.py new file mode 100644 index 0000000000..e627c7c60d --- /dev/null +++ b/main.py @@ -0,0 +1,52 @@ +# flake8: noqa + +import argparse +import json + +from . import spec, trie +from .eth_types import Address, Uint + + +def main() -> None: + parser = argparse.ArgumentParser(description="Run a state transition.") + + parser.add_argument("--input.alloc", dest="alloc", action="store") + parser.add_argument("--input.env", dest="env", action="store") + parser.add_argument("--input.txs", dest="txs", action="store") + + args = parser.parse_args() + + with open(args.alloc) as f: + alloc = json.load(f) + with open(args.env) as f: + env = json.load(f) + + state = {} + for (addr, vals) in alloc.items(): + account = spec.Account( + nonce=vals.get("nonce", Uint(0)), + balance=Uint(int(vals.get("balance", Uint(0)))), + code=bytes.fromhex(vals.get("code", "")), + storage={}, # TODO: support storage + ) + addr = bytes.fromhex(addr) + state[addr] = account + + gas_used, receipts, state = spec.apply_body( + state, + Address(bytes.fromhex(env["currentCoinbase"][2:])), + Uint(int(env["currentNumber"], 16)), + Uint(int(env["currentGasLimit"], 16)), + Uint(int(env["currentTimestamp"], 16)), + Uint(int(env["currentDifficulty"], 16)), + [], + [], + ) + + print(gas_used) + print(receipts.hex()) + print(trie.TRIE(trie.y(state)).hex()) + + +if __name__ == "__main__": + main() diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000000..f077396bc4 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,10 @@ +[mypy] +strict_optional = True +disallow_incomplete_defs = True +disallow_untyped_defs = True + +warn_unused_ignores = True +warn_unused_configs = True +warn_redundant_casts = True + +ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..2d6912ad54 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.isort] +profile = "black" +multi_line_output = 3 +line_length = 79 + +[tool.black] +line-length = 79 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000..42c54560e2 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,70 @@ +[metadata] +name = ethereum +description = Ethereum specification, provided as a Python package for tooling + and testing +long_description = file: README.md +long_description_content_type = text/markdown +version = 0.1.0 +url = https://github.com/ethereum/eth1.0-specs +license_files = + LICENSE.md +classifiers = + License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication + +[options] +packages = ethereum, ethereum/vm +package_dir = + =src + +python_requires = >=3.7 +install_requires = + pysha3>=1,<2 + coincurve>=15,<16 + +[options.extras_require] +test = + pytest>=6.2,<7 + pytest-cov>=2.12,<3 + +lint = + isort>=5.8,<6 + mypy==0.812; implementation_name == "cpython" + black==21.5b2; implementation_name == "cpython" + flake8-spellcheck>=0.24,<0.25 + flake8-docstrings>=1.6,<2 + flake8>=3.9,<4 + +doc = + sphinx-autoapi>=1.8.1,<2 + undocinclude>=0.1.1,<0.2 + sphinx==4.0.2 + +doc-autobuild = + sphinx-autobuild==2021.3.14 + +[flake8] +dictionaries=en_US,python,technical +docstring-convention = all +extend-ignore = + E203 # Ignore: Whitespace before ':' + D107 # Ignore: Missing docstring in __init__ + D200 # Ignore: One-line docstring should fit on one line with quotes + D203 # Ignore: 1 blank line required before class docstring + D205 # Ignore: blank line required between summary line and description + D212 # Ignore: Multi-line docstring summary should start at the first line + D400 # Ignore: First line should end with a period + D401 # Ignore: First line should be in imperative mood + D410 # Ignore: Missing blank line after section + D411 # Ignore: Missing blank line before section + D412 # Ignore: No blank lines allowed between a section header and its content + D413 # Ignore: Missing blank line after last section + D414 # Ignore: Section has no content + D415 # Ignore: First line should end with a period, question mark, or exclamation point + D416 # Ignore: Section name should end with a colon + +extend-exclude = + setup.py + tests/ + doc/ + +# vim: set ft=dosini: diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000..b908cbe55c --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +import setuptools + +setuptools.setup() diff --git a/src/ethereum/__init__.py b/src/ethereum/__init__.py new file mode 100644 index 0000000000..bb1c9411d1 --- /dev/null +++ b/src/ethereum/__init__.py @@ -0,0 +1,6 @@ +""" +Ethereum Specification +^^^^^^^^^^^^^^^^^^^^^^ + +Core specifications for Ethereum clients. +""" diff --git a/src/ethereum/base_types.py b/src/ethereum/base_types.py new file mode 100644 index 0000000000..6bd7fea550 --- /dev/null +++ b/src/ethereum/base_types.py @@ -0,0 +1,553 @@ +""" +Numeric & Array Types +^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Integer and array types which are used by—but not unique to—Ethereum. +""" + +# flake8: noqa + +from __future__ import annotations + +from typing import Optional, Tuple, Type + + +class Uint(int): + """ + Unsigned positive integer. + """ + + __slots__ = () + + @classmethod + def from_be_bytes(cls: Type, buffer: "Bytes") -> "Uint": + """ + Converts a sequence of bytes into an arbitrarily sized unsigned integer + from its big endian representation. + + Parameters + ---------- + buffer : + Bytes to decode. + + Returns + ------- + self : `Uint` + Unsigned integer decoded from `buffer`. + """ + return cls(int.from_bytes(buffer, "big")) + + def __new__(cls: Type, value: int) -> "Uint": + if not isinstance(value, int): + raise TypeError() + + if value < 0: + raise ValueError() + + return super(cls, cls).__new__(cls, value) + + def __radd__(self, left: int) -> "Uint": + return self.__add__(left) + + def __add__(self, right: int) -> "Uint": + if not isinstance(right, int): + return NotImplemented + + if right < 0: + raise ValueError() + + result = super(Uint, self).__add__(right) + return self.__class__(result) + + def __iadd__(self, right: int) -> "Uint": + return self.__add__(right) + + def __sub__(self, right: int) -> "Uint": + if not isinstance(right, int): + return NotImplemented + + if right < 0: + raise ValueError() + + if super(Uint, self).__lt__(right): + raise ValueError() + + result = super(Uint, self).__sub__(right) + return self.__class__(result) + + def __rsub__(self, left: int) -> "Uint": + if not isinstance(left, int): + return NotImplemented + + if left < 0: + raise ValueError() + + if super(Uint, self).__gt__(left): + raise ValueError() + + result = super(Uint, self).__rsub__(left) + return self.__class__(result) + + def __isub__(self, right: int) -> "Uint": + return self.__sub__(right) + + def __mul__(self, right: int) -> "Uint": + if not isinstance(right, int): + return NotImplemented + + if right < 0: + raise ValueError() + + result = super(Uint, self).__mul__(right) + return self.__class__(result) + + def __rmul__(self, left: int) -> "Uint": + return self.__mul__(left) + + def __imul__(self, right: int) -> "Uint": + return self.__mul__(right) + + # Explicitly don't override __truediv__, __rtruediv__, and __itruediv__ + # since they return floats anyway. + + def __floordiv__(self, right: int) -> "Uint": + if not isinstance(right, int): + return NotImplemented + + if right < 0: + raise ValueError() + + result = super(Uint, self).__floordiv__(right) + return self.__class__(result) + + def __rfloordiv__(self, left: int) -> "Uint": + if not isinstance(left, int): + return NotImplemented + + if left < 0: + raise ValueError() + + result = super(Uint, self).__rfloordiv__(left) + return self.__class__(result) + + def __ifloordiv__(self, right: int) -> "Uint": + return self.__floordiv__(right) + + def __mod__(self, right: int) -> "Uint": + if not isinstance(right, int): + return NotImplemented + + if right < 0: + raise ValueError() + + result = super(Uint, self).__mod__(right) + return self.__class__(result) + + def __rmod__(self, left: int) -> "Uint": + if not isinstance(left, int): + return NotImplemented + + if left < 0: + raise ValueError() + + result = super(Uint, self).__rmod__(left) + return self.__class__(result) + + def __imod__(self, right: int) -> "Uint": + return self.__mod__(right) + + def __divmod__(self, right: int) -> Tuple["Uint", "Uint"]: + if not isinstance(right, int): + return NotImplemented + + if right < 0: + raise ValueError() + + result = super(Uint, self).__divmod__(right) + return (self.__class__(result[0]), self.__class__(result[1])) + + def __rdivmod__(self, left: int) -> Tuple["Uint", "Uint"]: + if not isinstance(left, int): + return NotImplemented + + if left < 0: + raise ValueError() + + result = super(Uint, self).__rdivmod__(left) + return (self.__class__(result[0]), self.__class__(result[1])) + + def __pow__(self, right: int, modulo: Optional[int] = None) -> "Uint": + if modulo is not None: + if not isinstance(modulo, int): + return NotImplemented + + if modulo < 0: + raise ValueError() + + if not isinstance(right, int): + return NotImplemented + + if right < 0: + raise ValueError() + + result = super(Uint, self).__pow__(right, modulo) + return self.__class__(result) + + def __rpow__(self, left: int, modulo: Optional[int] = None) -> "Uint": + if modulo is not None: + if not isinstance(modulo, int): + return NotImplemented + + if modulo < 0: + raise ValueError() + + if not isinstance(left, int): + return NotImplemented + + if left < 0: + raise ValueError() + + result = super(Uint, self).__rpow__(left, modulo) + return self.__class__(result) + + def __ipow__(self, right: int, modulo: Optional[int] = None) -> "Uint": + return self.__pow__(right, modulo) + + # TODO: Implement and, or, xor, neg, pos, abs, invert, ... + + def to_be_bytes32(self) -> "Bytes32": + """ + Converts this arbitrarily sized unsigned integer into its big endian + representation with exactly 32 bytes. + + Returns + ------- + big_endian : `Bytes32` + Big endian (most significant bits first) representation. + """ + return self.to_bytes(32, "big") + + def to_be_bytes(self) -> "Bytes": + """ + Converts this arbitrarily sized unsigned integer into its big endian + representation. + + Returns + ------- + big_endian : `Bytes` + Big endian (most significant bits first) representation. + """ + bit_length = self.bit_length() + byte_length = (bit_length + 7) // 8 + return self.to_bytes(byte_length, "big") + + +class U256(int): + """ + Unsigned positive integer, which can represent `0` to `2 ** 256 - 1`, + inclusive. + """ + + MAX_VALUE: "U256" + + __slots__ = () + + @classmethod + def from_be_bytes(cls: Type, buffer: "Bytes") -> "U256": + """ + Converts a sequence of bytes into an arbitrarily sized unsigned integer + from its big endian representation. + + Parameters + ---------- + buffer : + Bytes to decode. + + Returns + ------- + self : `U256` + Unsigned integer decoded from `buffer`. + """ + if len(buffer) > 32: + raise ValueError() + + return cls(int.from_bytes(buffer, "big")) + + def __new__(cls: Type, value: int) -> "U256": + if not isinstance(value, int): + raise TypeError() + + if value < 0 or value >= 2 ** 256: + raise ValueError() + + return super(cls, cls).__new__(cls, value) + + def __radd__(self, left: int) -> "U256": + return self.__add__(left) + + def __add__(self, right: int) -> "U256": + result = self.unchecked_add(right) + + if result == NotImplemented: + return NotImplemented + + return self.__class__(result) + + def unchecked_add(self, right: int) -> int: + if not isinstance(right, int): + return NotImplemented + + if right < 0 or right > self.MAX_VALUE: + raise ValueError() + + return super(U256, self).__add__(right) + + def wrapping_add(self, right: int) -> "U256": + result = self.unchecked_add(right) + + if result == NotImplemented: + return NotImplemented + + result %= 2 ** 256 + return self.__class__(result) + + def __iadd__(self, right: int) -> "U256": + return self.__add__(right) + + def __sub__(self, right: int) -> "U256": + result = self.unchecked_sub(right) + + if result == NotImplemented: + return NotImplemented + + return self.__class__(result) + + def unchecked_sub(self, right: int) -> int: + if not isinstance(right, int): + return NotImplemented + + if right < 0 or right > self.MAX_VALUE: + raise ValueError() + + return super(U256, self).__sub__(right) + + def wrapping_sub(self, right: int) -> "U256": + result = self.unchecked_sub(right) + + if result == NotImplemented: + return NotImplemented + + result %= 2 ** 256 + return self.__class__(result) + + def __rsub__(self, left: int) -> "U256": + if not isinstance(left, int): + return NotImplemented + + if left < 0 or left > self.MAX_VALUE: + raise ValueError() + + result = super(U256, self).__rsub__(left) + return self.__class__(result) + + def __isub__(self, right: int) -> "U256": + return self.__sub__(right) + + def unchecked_mul(self, right: int) -> int: + if not isinstance(right, int): + return NotImplemented + + if right < 0 or right > self.MAX_VALUE: + raise ValueError() + + return super(U256, self).__mul__(right) + + def wrapping_mul(self, right: int) -> "U256": + result = self.unchecked_mul(right) + + if result == NotImplemented: + return NotImplemented + + result %= 2 ** 256 + return self.__class__(result) + + def __mul__(self, right: int) -> "U256": + result = self.unchecked_mul(right) + + if result == NotImplemented: + return NotImplemented + + return self.__class__(result) + + def __rmul__(self, left: int) -> "U256": + return self.__mul__(left) + + def __imul__(self, right: int) -> "U256": + return self.__mul__(right) + + # Explicitly don't override __truediv__, __rtruediv__, and __itruediv__ + # since they return floats anyway. + + def __floordiv__(self, right: int) -> "U256": + if not isinstance(right, int): + return NotImplemented + + if right < 0 or right > self.MAX_VALUE: + raise ValueError() + + result = super(U256, self).__floordiv__(right) + return self.__class__(result) + + def __rfloordiv__(self, left: int) -> "U256": + if not isinstance(left, int): + return NotImplemented + + if left < 0 or left > self.MAX_VALUE: + raise ValueError() + + result = super(U256, self).__rfloordiv__(left) + return self.__class__(result) + + def __ifloordiv__(self, right: int) -> "U256": + return self.__floordiv__(right) + + def __mod__(self, right: int) -> "U256": + if not isinstance(right, int): + return NotImplemented + + if right < 0 or right > self.MAX_VALUE: + raise ValueError() + + result = super(U256, self).__mod__(right) + return self.__class__(result) + + def __rmod__(self, left: int) -> "U256": + if not isinstance(left, int): + return NotImplemented + + if left < 0 or left > self.MAX_VALUE: + raise ValueError() + + result = super(U256, self).__rmod__(left) + return self.__class__(result) + + def __imod__(self, right: int) -> "U256": + return self.__mod__(right) + + def __divmod__(self, right: int) -> Tuple["U256", "U256"]: + if not isinstance(right, int): + return NotImplemented + + if right < 0 or right > self.MAX_VALUE: + raise ValueError() + + result = super(U256, self).__divmod__(right) + return (self.__class__(result[0]), self.__class__(result[1])) + + def __rdivmod__(self, left: int) -> Tuple["U256", "U256"]: + if not isinstance(left, int): + return NotImplemented + + if left < 0 or left > self.MAX_VALUE: + raise ValueError() + + result = super(U256, self).__rdivmod__(left) + return (self.__class__(result[0]), self.__class__(result[1])) + + def unchecked_pow(self, right: int, modulo: Optional[int] = None) -> int: + if modulo is not None: + if not isinstance(modulo, int): + return NotImplemented + + if modulo < 0 or modulo > self.MAX_VALUE: + raise ValueError() + + if not isinstance(right, int): + return NotImplemented + + if right < 0 or right > self.MAX_VALUE: + raise ValueError() + + return super(U256, self).__pow__(right, modulo) + + def wrapping_pow(self, right: int, modulo: Optional[int] = None) -> "U256": + result = self.unchecked_pow(right, modulo) + + if result == NotImplemented: + return NotImplemented + + result %= 2 ** 256 + return self.__class__(result) + + def __pow__(self, right: int, modulo: Optional[int] = None) -> "U256": + result = self.unchecked_pow(right, modulo) + + if result == NotImplemented: + return NotImplemented + + return self.__class__(result) + + def __rpow__(self, left: int, modulo: Optional[int] = None) -> "U256": + if modulo is not None: + if not isinstance(modulo, int): + return NotImplemented + + if modulo < 0 or modulo > self.MAX_VALUE: + raise ValueError() + + if not isinstance(left, int): + return NotImplemented + + if left < 0 or left > self.MAX_VALUE: + raise ValueError() + + result = super(U256, self).__rpow__(left, modulo) + return self.__class__(result) + + def __ipow__(self, right: int, modulo: Optional[int] = None) -> "U256": + return self.__pow__(right, modulo) + + # TODO: Implement and, or, xor, neg, pos, abs, invert, ... + + def to_be_bytes32(self) -> "Bytes32": + """ + Converts this 256-bit unsigned integer into its big endian + representation with exactly 32 bytes. + + Returns + ------- + big_endian : `Bytes32` + Big endian (most significant bits first) representation. + """ + return self.to_bytes(32, "big") + + def to_be_bytes(self) -> "Bytes": + """ + Converts this 256-bit unsigned integer into its big endian + representation, omitting leading zero bytes. + + Returns + ------- + big_endian : `Bytes` + Big endian (most significant bits first) representation. + """ + bit_length = self.bit_length() + byte_length = (bit_length + 7) // 8 + return self.to_bytes(byte_length, "big") + + +U256.MAX_VALUE = U256(2 ** 256 - 1) + + +Bytes = bytes +Bytes64 = Bytes +Bytes32 = Bytes +Bytes20 = Bytes +Bytes8 = Bytes diff --git a/src/ethereum/crypto.py b/src/ethereum/crypto.py new file mode 100644 index 0000000000..5a4a3cb6ca --- /dev/null +++ b/src/ethereum/crypto.py @@ -0,0 +1,87 @@ +""" +Cryptographic Functions +^^^^^^^^^^^^^^^^^^^^^^^ + +..contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Cryptographic primatives used in—but not defined by—the Ethereum specification. +""" + +import coincurve +import sha3 + +from .base_types import U256, Bytes +from .eth_types import Hash32, Hash64 + + +def keccak256(buffer: Bytes) -> Hash32: + """ + Computes the keccak256 hash of the input `buffer`. + + Parameters + ---------- + buffer : + Input for the hashing function. + + Returns + ------- + hash : `eth1spec.eth_types.Hash32` + Output of the hash function. + """ + return sha3.keccak_256(buffer).digest() + + +def keccak512(buffer: Bytes) -> Hash64: + """ + Computes the keccak512 hash of the input `buffer`. + + Parameters + ---------- + buffer : + Input for the hashing function. + + Returns + ------- + hash : `eth1spec.eth_types.Hash32` + Output of the hash function. + """ + return sha3.keccak_512(buffer).digest() + + +def secp256k1_recover(r: U256, s: U256, v: U256, msg_hash: Hash32) -> Bytes: + """ + Recovers the public key from a given signature. + + Parameters + ---------- + r : + TODO + s : + TODO + v : + TODO + msg_hash : + Hash of the message being recovered. + + Returns + ------- + public_key : `eth1spec.base_types.Bytes` + Recovered public key. + """ + r_bytes = r.to_be_bytes32() + s_bytes = s.to_be_bytes32() + + signature = bytearray([0] * 65) + signature[32 - len(r_bytes) : 32] = r_bytes + signature[64 - len(s_bytes) : 64] = s_bytes + signature[64] = v + public_key = coincurve.PublicKey.from_signature_and_message( + bytes(signature), msg_hash, hasher=None + ) + public_key = public_key.format(compressed=False)[1:] + return public_key diff --git a/src/ethereum/eth_types.py b/src/ethereum/eth_types.py new file mode 100644 index 0000000000..22a66c5afa --- /dev/null +++ b/src/ethereum/eth_types.py @@ -0,0 +1,127 @@ +""" +Ethereum Types +^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Types re-used throughout the specification, which are specific to Ethereum. +""" + +from dataclasses import dataclass +from typing import Dict, List, Optional + +from .base_types import U256, Bytes, Bytes8, Bytes20, Bytes32, Bytes64, Uint + +Address = Bytes20 +Root = Bytes +Hash32 = Bytes32 +Hash64 = Bytes64 + +Storage = Dict[Bytes32, U256] +Bloom = Bytes32 + +TX_BASE_COST = 21000 +TX_DATA_COST_PER_NON_ZERO = 68 +TX_DATA_COST_PER_ZERO = 4 + + +@dataclass +class Transaction: + """ + Atomic operation performed on the block chain. + """ + + nonce: U256 + gas_price: U256 + gas: U256 + to: Optional[Address] + value: U256 + data: Bytes + v: U256 + r: U256 + s: U256 + + +@dataclass +class Account: + """ + State associated with an address. + """ + + nonce: Uint + balance: Uint + code: bytes + storage: Storage + + +EMPTY_ACCOUNT = Account( + nonce=Uint(0), + balance=Uint(0), + code=bytearray(), + storage={}, +) + + +@dataclass +class Header: + """ + Header portion of a block on the chain. + """ + + parent: Hash32 + ommers: Hash32 + coinbase: Address + state_root: Root + transactions_root: Root + receipt_root: Root + bloom: Bloom + difficulty: Uint + number: Uint + gas_limit: Uint + gas_used: Uint + time: U256 + extra: Bytes + mix_digest: Bytes32 + nonce: Bytes8 + + +@dataclass +class Block: + """ + A complete block. + """ + + header: Header + transactions: List[Transaction] + ommers: List[Header] + + +@dataclass +class Log: + """ + Data record produced during the execution of a transaction. + """ + + address: Address + topics: List[Hash32] + data: bytes + + +@dataclass +class Receipt: + """ + Result of a transaction. + """ + + post_state: Root + cumulative_gas_used: Uint + bloom: Bloom + logs: List[Log] + + +State = Dict[Address, Account] diff --git a/src/ethereum/rlp.py b/src/ethereum/rlp.py new file mode 100644 index 0000000000..08128b2446 --- /dev/null +++ b/src/ethereum/rlp.py @@ -0,0 +1,487 @@ +""" +Recursive Length Prefix (RLP) Encoding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Defines the serialization and deserialization format used throughout Ethereum. +""" + +from __future__ import annotations + +from typing import List, Sequence, Union, cast + +from .base_types import U256, Bytes, Uint +from .crypto import keccak256 +from .eth_types import Account, Block, Header, Log, Receipt, Transaction + +RLP = Union[ # type: ignore + Bytes, + Uint, + U256, + Block, + Header, + Account, + Transaction, + Receipt, + Log, + Sequence["RLP"], # type: ignore +] + + +# +# RLP Encode +# + + +def encode(raw_data: RLP) -> Bytes: + """ + Encodes `raw_data` into a sequence of bytes using RLP. + + Parameters + ---------- + raw_data : + A `Bytes`, `Uint`, `Uint256` or sequence of `RLP` encodable + objects. + + Returns + ------- + encoded : `eth1spec.base_types.Bytes` + The RLP encoded bytes representing `raw_data`. + """ + if isinstance(raw_data, (bytearray, bytes)): + return encode_bytes(raw_data) + elif isinstance(raw_data, (Uint, U256)): + return encode(raw_data.to_be_bytes()) + elif isinstance(raw_data, str): + return encode_bytes(raw_data.encode()) + elif isinstance(raw_data, Sequence): + return encode_sequence(cast(Sequence[RLP], raw_data)) + elif isinstance(raw_data, Block): + return encode_block(raw_data) + elif isinstance(raw_data, Header): + return encode_header(raw_data) + elif isinstance(raw_data, Account): + return encode_account(raw_data) + elif isinstance(raw_data, Transaction): + return encode_transaction(raw_data) + elif isinstance(raw_data, Receipt): + return encode_receipt(raw_data) + elif isinstance(raw_data, Log): + return encode_log(raw_data) + else: + raise TypeError( + "RLP Encoding of type {} is not supported".format(type(raw_data)) + ) + + +def encode_bytes(raw_bytes: Bytes) -> Bytes: + """ + Encodes `raw_bytes`, a sequence of bytes, using RLP. + + Parameters + ---------- + raw_bytes : + Bytes to encode with RLP. + + Returns + ------- + encoded : `eth1spec.base_types.Bytes` + The RLP encoded bytes representing `raw_bytes`. + """ + len_raw_data = Uint(len(raw_bytes)) + + if len_raw_data == 1 and raw_bytes[0] < 0x80: + return raw_bytes + elif len_raw_data < 0x38: + return bytearray([0x80 + len_raw_data]) + raw_bytes + else: + # length of raw data represented as big endian bytes + len_raw_data_as_be = len_raw_data.to_be_bytes() + return ( + bytearray([0xB7 + len(len_raw_data_as_be)]) + + len_raw_data_as_be + + raw_bytes + ) + + +def encode_sequence(raw_sequence: Sequence[RLP]) -> Bytes: + """ + Encodes a list of RLP encodable objects (`raw_sequence`) using RLP. + + Parameters + ---------- + raw_sequence : + Sequence of RLP encodable objects. + + Returns + ------- + encoded : `eth1spec.base_types.Bytes` + The RLP encoded bytes representing `raw_sequence`. + """ + joined_encodings = get_joined_encodings(raw_sequence) + len_joined_encodings = Uint(len(joined_encodings)) + + if len_joined_encodings < 0x38: + return bytearray([0xC0 + len_joined_encodings]) + joined_encodings + else: + len_joined_encodings_as_be = len_joined_encodings.to_be_bytes() + return ( + bytearray([0xF7 + len(len_joined_encodings_as_be)]) + + len_joined_encodings_as_be + + joined_encodings + ) + + +def get_joined_encodings(raw_sequence: Sequence[RLP]) -> Bytes: + """ + Obtain concatenation of rlp encoding for each item in the sequence + raw_sequence. + + Parameters + ---------- + raw_sequence : + Sequence to encode with RLP. + + Returns + ------- + joined_encodings : `eth1spec.base_types.Bytes` + The concatenated RLP encoded bytes for each item in sequence + raw_sequence. + """ + joined_encodings = bytearray() + for item in raw_sequence: + joined_encodings += encode(item) + + return joined_encodings + + +# +# RLP Decode +# + + +def decode(encoded_data: Bytes) -> RLP: + """ + Decodes an integer, byte sequence, or list of RLP encodable objects + from the byte sequence `encoded_data`, using RLP. + + Parameters + ---------- + encoded_data : + A sequence of bytes, in RLP form. + + Returns + ------- + decoded_data : `RLP` + Object decoded from `encoded_data`. + """ + # Raising error as there can never be empty encoded data for any + # given raw data (including empty raw data) + # RLP Encoding(b'') -> [0x80] # noqa: SC100 + # RLP Encoding([]) -> [0xc0] # noqa: SC100 + assert len(encoded_data) > 0 + + if encoded_data[0] <= 0xBF: + # This means that the raw data is of type bytes + return decode_to_bytes(encoded_data) + else: + # This means that the raw data is of type sequence + return decode_to_sequence(encoded_data) + + +def decode_to_bytes(encoded_bytes: Bytes) -> Bytes: + """ + Decodes a rlp encoded byte stream assuming that the decoded data + should be of type `bytes`. + + Parameters + ---------- + encoded_bytes : + RLP encoded byte stream. + + Returns + ------- + decoded : `eth1spec.base_types.Bytes` + RLP decoded Bytes data + """ + if len(encoded_bytes) == 1 and encoded_bytes[0] < 0x80: + return encoded_bytes + elif encoded_bytes[0] <= 0xB7: + len_raw_data = encoded_bytes[0] - 0x80 + assert len_raw_data < len(encoded_bytes) + raw_data = encoded_bytes[1 : 1 + len_raw_data] + assert not (len_raw_data == 1 and raw_data[0] < 0x80) + return raw_data + else: + # This is the index in the encoded data at which decoded data + # starts from. + decoded_data_start_idx = 1 + encoded_bytes[0] - 0xB7 + assert decoded_data_start_idx - 1 < len(encoded_bytes) + # Expectation is that the big endian bytes shouldn't start with 0 + # while trying to decode using RLP, in which case is an error. + assert encoded_bytes[1] != 0 + len_decoded_data = Uint.from_be_bytes( + encoded_bytes[1:decoded_data_start_idx] + ) + assert len_decoded_data >= 0x38 + decoded_data_end_idx = decoded_data_start_idx + len_decoded_data + assert decoded_data_end_idx - 1 < len(encoded_bytes) + return encoded_bytes[decoded_data_start_idx:decoded_data_end_idx] + + +def decode_to_sequence(encoded_sequence: Bytes) -> List[RLP]: + """ + Decodes a rlp encoded byte stream assuming that the decoded data + should be of type `Sequence` of objects. + + Parameters + ---------- + encoded_sequence : + An RLP encoded Sequence. + + Returns + ------- + decoded : `Sequence[RLP]` + Sequence of objects decoded from `encoded_sequence`. + """ + if encoded_sequence[0] <= 0xF7: + len_joined_encodings = encoded_sequence[0] - 0xC0 + assert len_joined_encodings < len(encoded_sequence) + joined_encodings = encoded_sequence[1 : 1 + len_joined_encodings] + else: + joined_encodings_start_idx = 1 + encoded_sequence[0] - 0xF7 + assert joined_encodings_start_idx - 1 < len(encoded_sequence) + # Expectation is that the big endian bytes shouldn't start with 0 + # while trying to decode using RLP, in which case is an error. + assert encoded_sequence[1] != 0 + len_joined_encodings = Uint.from_be_bytes( + encoded_sequence[1:joined_encodings_start_idx] + ) + assert len_joined_encodings >= 0x38 + joined_encodings_end_idx = ( + joined_encodings_start_idx + len_joined_encodings + ) + assert joined_encodings_end_idx - 1 < len(encoded_sequence) + joined_encodings = encoded_sequence[ + joined_encodings_start_idx:joined_encodings_end_idx + ] + + return decode_joined_encodings(joined_encodings) + + +def decode_joined_encodings(joined_encodings: Bytes) -> List[RLP]: + """ + Decodes `joined_encodings`, which is a concatenation of RLP encoded + objects. + + Parameters + ---------- + joined_encodings : + concatenation of RLP encoded objects + + Returns + ------- + decoded : `List[RLP]` + A list of objects decoded from `joined_encodings`. + """ + decoded_sequence = [] + + item_start_idx = 0 + while item_start_idx < len(joined_encodings): + encoded_item_length = decode_item_length( + joined_encodings[item_start_idx:] + ) + assert item_start_idx + encoded_item_length - 1 < len(joined_encodings) + encoded_item = joined_encodings[ + item_start_idx : item_start_idx + encoded_item_length + ] + decoded_sequence.append(decode(encoded_item)) + item_start_idx += encoded_item_length + + return decoded_sequence + + +def decode_item_length(encoded_data: Bytes) -> int: + """ + Find the length of the rlp encoding for the first object in the + encoded sequence. + Here `encoded_data` refers to concatenation of rlp encoding for each + item in a sequence. + + NOTE - This is a helper function not described in the spec. It was + introduced as the spec doesn't discuss about decoding the RLP encoded + data. + + Parameters + ---------- + encoded_data : + RLP encoded data for a sequence of objects. + + Returns + ------- + rlp_length : `int` + """ + # Can't decode item length for empty encoding + assert len(encoded_data) > 0 + + first_rlp_byte = Uint(encoded_data[0]) + + # This is the length of the big endian representation of the length of + # rlp encoded object byte stream. + length_length = Uint(0) + decoded_data_length = 0 + + # This occurs only when the raw_data is a single byte whose value < 128 + if first_rlp_byte < 0x80: + # We return 1 here, as the end formula + # 1 + length_length + decoded_data_length would be invalid for + # this case. + return 1 + # This occurs only when the raw_data is a byte stream with length < 56 + # and doesn't fall into the above cases + elif first_rlp_byte <= 0xB7: + decoded_data_length = first_rlp_byte - 0x80 + # This occurs only when the raw_data is a byte stream and doesn't fall + # into the above cases + elif first_rlp_byte <= 0xBF: + length_length = first_rlp_byte - 0xB7 + assert length_length < len(encoded_data) + # Expectation is that the big endian bytes shouldn't start with 0 + # while trying to decode using RLP, in which case is an error. + assert encoded_data[1] != 0 + decoded_data_length = Uint.from_be_bytes( + encoded_data[1 : 1 + length_length] + ) + # This occurs only when the raw_data is a sequence of objects with + # length(concatenation of encoding of each object) < 56 + elif first_rlp_byte <= 0xF7: + decoded_data_length = first_rlp_byte - 0xC0 + # This occurs only when the raw_data is a sequence of objects and + # doesn't fall into the above cases. + elif first_rlp_byte <= 0xFF: + length_length = first_rlp_byte - 0xF7 + assert length_length < len(encoded_data) + # Expectation is that the big endian bytes shouldn't start with 0 + # while trying to decode using RLP, in which case is an error. + assert encoded_data[1] != 0 + decoded_data_length = Uint.from_be_bytes( + encoded_data[1 : 1 + length_length] + ) + + return 1 + length_length + decoded_data_length + + +# +# Encoding and decoding custom dataclasses like Account, Transaction, +# Receipt etc. +# + + +def encode_block(raw_block_data: Block) -> Bytes: + """ + Encode `Block` dataclass + """ + return encode( + ( + raw_block_data.header, + raw_block_data.transactions, + raw_block_data.ommers, + ) + ) + + +def encode_header(raw_header_data: Header) -> Bytes: + """ + Encode `Header` dataclass + """ + return encode( + ( + raw_header_data.parent, + raw_header_data.ommers, + raw_header_data.coinbase, + raw_header_data.state_root, + raw_header_data.transactions_root, + raw_header_data.receipt_root, + raw_header_data.bloom, + raw_header_data.difficulty, + raw_header_data.number, + raw_header_data.gas_limit, + raw_header_data.gas_used, + raw_header_data.time, + raw_header_data.extra, + raw_header_data.mix_digest, + raw_header_data.nonce, + ) + ) + + +def encode_account(raw_account_data: Account) -> Bytes: + """ + Encode `Account` dataclass + """ + # TODO: This function should be split into 2 functions. One to + # patricialize the storage root and hashing code. Another for rlp + # encoding the previously obtained data. + # Imported here to prevent circular dependency + from .trie import map_keys, root + + return encode( + ( + raw_account_data.nonce, + raw_account_data.balance, + root(map_keys(raw_account_data.storage)), + keccak256(raw_account_data.code), + ) + ) + + +def encode_transaction(raw_tx_data: Transaction) -> Bytes: + """ + Encode `Transaction` dataclass + """ + return encode( + ( + raw_tx_data.nonce, + raw_tx_data.gas_price, + raw_tx_data.gas, + raw_tx_data.to, + raw_tx_data.value, + raw_tx_data.data, + raw_tx_data.v, + raw_tx_data.r, + raw_tx_data.s, + ) + ) + + +def encode_receipt(raw_receipt_data: Receipt) -> Bytes: + """ + Encode `Receipt` dataclass + """ + return encode( + ( + raw_receipt_data.post_state, + raw_receipt_data.cumulative_gas_used, + raw_receipt_data.bloom, + raw_receipt_data.logs, + ) + ) + + +def encode_log(raw_log_data: Log) -> Bytes: + """ + Encode `Log` dataclass + """ + return encode( + ( + raw_log_data.address, + raw_log_data.topics, + raw_log_data.data, + ) + ) diff --git a/src/ethereum/spec.py b/src/ethereum/spec.py new file mode 100644 index 0000000000..f33ee24f0d --- /dev/null +++ b/src/ethereum/spec.py @@ -0,0 +1,378 @@ +""" +Ethereum Specification +^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Entry point for the Ethereum specification. +""" + +from dataclasses import dataclass +from typing import List, Tuple + +from . import crypto, rlp, trie, vm +from .base_types import U256, Uint +from .eth_types import ( + EMPTY_ACCOUNT, + TX_BASE_COST, + TX_DATA_COST_PER_NON_ZERO, + TX_DATA_COST_PER_ZERO, + Address, + Block, + Hash32, + Header, + Log, + Receipt, + Root, + State, + Transaction, +) +from .vm.interpreter import process_call + +BLOCK_REWARD = 5 * 10 ** 18 + + +@dataclass +class BlockChain: + """ + History and current state of the block chain. + """ + + blocks: List[Block] + state: State + + +def state_transition(chain: BlockChain, block: Block) -> None: + """ + Attempts to apply a block to an existing block chain. + + Parameters + ---------- + chain : + History and current state. + block : + Block to apply to `chain`. + """ + # assert verify_header(block.header) + gas_used, transactions_root, receipt_root, state = apply_body( + chain.state, + block.header.coinbase, + block.header.number, + block.header.gas_limit, + block.header.time, + block.header.difficulty, + block.transactions, + block.ommers, + ) + + assert gas_used == block.header.gas_used + assert compute_ommers_hash(block) == block.header.ommers + # TODO: Also need to verify that these ommers are indeed valid as per the + # Nth generation + assert transactions_root == block.header.transactions_root + assert receipt_root == block.header.receipt_root + assert trie.root(trie.map_keys(state)) == block.header.state_root + + chain.blocks.append(block) + + +def verify_header(header: Header) -> bool: + """ + Verifies a block header. + + Parameters + ---------- + header : + Header to check for correctness. + + Returns + ------- + verified : `bool` + True if the header is correct, False otherwise. + """ + raise NotImplementedError() # TODO + + +def apply_body( + state: State, + coinbase: Address, + block_number: Uint, + block_gas_limit: Uint, + block_time: U256, + block_difficulty: Uint, + transactions: List[Transaction], + ommers: List[Header], +) -> Tuple[Uint, Root, Root, State]: + """ + Executes a block. + + Parameters + ---------- + state : + Current account state. + coinbase : + Address of account which receives block reward and transaction fees. + block_number : + Position of the block within the chain. + block_gas_limit : + Initial amount of gas available for execution in this block. + block_time : + Time the block was produced, measured in seconds since the epoch. + block_difficulty : + Difficulty of the block. + transactions : + Transactions included in the block. + ommers : + Headers of ancestor blocks which are not direct parents (formerly + uncles.) + + Returns + ------- + gas_available : `eth1spec.base_types.Uint` + Remaining gas after all transactions have been executed. + root : `eth1spec.eth_types.Root` + State root after all transactions have been executed. + state : `eth1spec.eth_types.State` + State after all transactions have been executed. + """ + gas_available = block_gas_limit + receipts = [] + + if coinbase not in state: + state[coinbase] = EMPTY_ACCOUNT + + for tx in transactions: + assert tx.gas <= gas_available + sender_address = recover_sender(tx) + + env = vm.Environment( + caller=sender_address, + origin=sender_address, + block_hashes=[], + coinbase=coinbase, + number=block_number, + gas_limit=block_gas_limit, + gas_price=tx.gas_price, + time=block_time, + difficulty=block_difficulty, + state=state, + ) + + gas_used, logs = process_transaction(env, tx) + gas_available -= gas_used + + receipts.append( + Receipt( + post_state=Root(trie.root(trie.map_keys(state))), + cumulative_gas_used=(block_gas_limit - gas_available), + bloom=b"\x00" * 256, + logs=logs, + ) + ) + + state[coinbase].balance += BLOCK_REWARD + + gas_remaining = block_gas_limit - gas_available + + receipts_map = { + bytes(rlp.encode(Uint(k))): v for (k, v) in enumerate(receipts) + } + receipt_root = trie.root(trie.map_keys(receipts_map, secured=False)) + + transactions_map = { + bytes(rlp.encode(Uint(idx))): tx + for (idx, tx) in enumerate(transactions) + } + transactions_root = trie.root( + trie.map_keys(transactions_map, secured=False) + ) + + return (gas_remaining, transactions_root, receipt_root, state) + + +def compute_ommers_hash(block: Block) -> Hash32: + """ + Compute hash of ommers list for a block + """ + return crypto.keccak256(rlp.encode(block.ommers)) + + +def process_transaction( + env: vm.Environment, tx: Transaction +) -> Tuple[U256, List[Log]]: + """ + Execute a transaction against the provided environment. + + Parameters + ---------- + env : + Environment for the Ethereum Virtual Machine. + tx : + Transaction to execute. + + Returns + ------- + gas_left : `eth1spec.base_types.U256` + Remaining gas after execution. + logs : `List[eth1spec.eth_types.Log]` + Logs generated during execution. + """ + assert validate_transaction(tx) + + sender_address = env.origin + sender = env.state[sender_address] + + assert sender.nonce == tx.nonce + sender.nonce += 1 + + cost = tx.gas * tx.gas_price + assert cost <= sender.balance + sender.balance -= cost + + gas = tx.gas - calculate_intrinsic_cost(tx) + + if tx.to is None: + raise NotImplementedError() # TODO + + gas_left, logs = process_call( + sender_address, tx.to, tx.data, tx.value, gas, Uint(0), env + ) + + sender.balance += gas_left * tx.gas_price + gas_used = tx.gas - gas_left + env.state[env.coinbase].balance += gas_used * tx.gas_price + + return (gas_used, logs) + + +def validate_transaction(tx: Transaction) -> bool: + """ + Verifies a transaction. + + Parameters + ---------- + tx : + Transaction to validate. + + Returns + ------- + verified : `bool` + True if the transaction can be executed, or False otherwise. + """ + return calculate_intrinsic_cost(tx) <= tx.gas + + +def calculate_intrinsic_cost(tx: Transaction) -> Uint: + """ + Calculates the intrinsic cost of the transaction that is charged before + execution is instantiated. + + Parameters + ---------- + tx : + Transaction to compute the intrinsic cost of. + + Returns + ------- + verified : `eth1spec.base_types.Uint` + The intrinsic cost of the transaction. + """ + data_cost = 0 + + for byte in tx.data: + if byte == 0: + data_cost += TX_DATA_COST_PER_ZERO + else: + data_cost += TX_DATA_COST_PER_NON_ZERO + + return Uint(TX_BASE_COST + data_cost) + + +def recover_sender(tx: Transaction) -> Address: + """ + Extracts the sender address from a transaction. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + sender : `eth1spec.eth_types.Address` + The address of the account that signed the transaction. + """ + v, r, s = tx.v, tx.r, tx.s + + # if v > 28: + # v = v - (chain_id*2+8) + + secp256k1n = ( + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + ) + + assert v == 27 or v == 28 + assert 0 < r and r < secp256k1n + + # TODO: this causes error starting in block 46169 (or 46170?) + # assert 0 Hash32: + """ + Compute the hash of a transaction used in the signature. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `eth1spec.eth_types.Hash32` + Hash of the transaction. + """ + return crypto.keccak256( + rlp.encode( + ( + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + ) + ) + ) + + +def print_state(state: State) -> None: + """ + Pretty prints the state. + + Parameters + ---------- + state : + Ethereum state. + """ + nice = {} + for (address, account) in state.items(): + nice[address.hex()] = { + "nonce": account.nonce, + "balance": account.balance, + "code": account.code.hex(), + "storage": {}, + } + + for (k, v) in account.storage.items(): + nice[address.hex()]["storage"][k.hex()] = hex(v) # type: ignore + + print(nice) diff --git a/src/ethereum/trie.py b/src/ethereum/trie.py new file mode 100644 index 0000000000..773d6a5c9a --- /dev/null +++ b/src/ethereum/trie.py @@ -0,0 +1,270 @@ +""" +State Trie +^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The state trie is the structure responsible for storing +`eth1spec.eth_types.Account` objects. +""" + +from copy import copy +from typing import Mapping, MutableMapping, Set, Union, cast + +from . import crypto, rlp +from .base_types import U256, Bytes, Uint +from .eth_types import Account, Receipt, Root, Transaction + +debug = False +verbose = False + +Node = Union[Account, Bytes, Transaction, Receipt, Uint, U256] + + +def nibble_list_to_compact(x: Bytes, terminal: bool) -> bytearray: + """ + Compresses nibble-list into a standard byte array with a flag. + + A nibble-list is a list of byte values no greater than `15`. The flag is + encoded in high nibble of the highest byte. The flag nibble can be broken + down into two two-bit flags. + + Highest nibble:: + + +---+---+----------+--------+ + | _ | _ | terminal | parity | + +---+---+----------+--------+ + 3 2 1 0 + + + The lowest bit of the nibble encodes the parity of the length of the + remaining nibbles -- `0` when even and `1` when odd. The second lowest bit + encodes whether the key maps to a terminal node. The other two bits are not + used. + + Parameters + ---------- + x : + Array of nibbles. + terminal : + Flag denoting if the key points to a terminal (leaf) node. + + Returns + ------- + compressed : `bytearray` + Compact byte array. + """ + compact = bytearray() + + if len(x) % 2 == 0: # ie even length + compact.append(16 * (2 * terminal)) + for i in range(0, len(x), 2): + compact.append(16 * x[i] + x[i + 1]) + else: + compact.append(16 * ((2 * terminal) + 1) + x[0]) + for i in range(1, len(x), 2): + compact.append(16 * x[i] + x[i + 1]) + + return compact + + +def map_keys( + obj: Mapping[Bytes, Node], secured: bool = True +) -> Mapping[Bytes, Node]: + """ + Maps all compact keys to nibble-list format. Optionally hashes the keys. + + Parameters + ---------- + obj : + Underlying trie key-value pairs. + secured : + Denotes whether the keys should be hashed. Defaults to `true`. + + Returns + ------- + out : `Mapping[eth1spec.base_types.Bytes, Node]` + Object with keys mapped to nibble-byte form. + """ + mapped: MutableMapping[Bytes, Node] = {} + + # skip empty values, these are defined to be omitted from the trie + skip: Set[Bytes] = set() + for (k, v) in obj.items(): + if v == b"": + skip.add(k) + + for (preimage, value) in obj.items(): + if preimage in skip: + continue + + # "secure" tries hash keys once before construction + key = crypto.keccak256(preimage) if secured else preimage + + nibble_list = bytearray(2 * len(key)) + for i in range(2 * len(key)): + byte_idx = i // 2 + if i % 2 == 0: + # get upper nibble + nibble_list[i] = (key[byte_idx] & 0xF0) >> 4 + else: + # get lower nibble + nibble_list[i] = key[byte_idx] & 0x0F + + mapped[Bytes(nibble_list)] = value + + return mapped + + +def encode_leaf(leaf: Node) -> rlp.RLP: + """ + RLP encode leaf nodes of the Trie. + Currently leaf nodes can be `Account`, `Transaction`, `Receipt` + dataclasses. + """ + if isinstance(leaf, (Account, Transaction, Receipt)): + return rlp.encode(cast(rlp.RLP, leaf)) + + return leaf + + +def root(obj: Mapping[Bytes, Node]) -> Root: + """ + Computes the root of a modified merkle patricia trie (MPT). + + Parameters + ---------- + obj : + Underlying trie key-value pairs. + + Returns + ------- + root : `eth1spec.eth_types.Root` + MPT root of the underlying key-value pairs. + """ + root_node = patricialize(obj, Uint(0)) + return crypto.keccak256(rlp.encode(root_node)) + + +def node_cap(obj: Mapping[Bytes, Node], i: Uint) -> rlp.RLP: + """ + Internal nodes less than 32 bytes in length are represented by themselves + directly. Larger nodes are hashed once to cap their size to 32 bytes. + + Parameters + ---------- + obj : + Underlying trie key-value pairs. + i : + Current trie level. + + Returns + ------- + hash : `eth1spec.eth_types.Hash32` + Internal node commitment. + """ + if len(obj) == 0: + return b"" + node = patricialize(obj, i) + encoded = rlp.encode(node) + if len(encoded) < 32: + return node + + return crypto.keccak256(encoded) + + +def patricialize(obj: Mapping[Bytes, Node], i: Uint) -> rlp.RLP: + """ + Structural composition function. + + Used to recursively patricialize and merkleize a dictionary. Includes + memoization of the tree structure and hashes. + + Parameters + ---------- + obj : + Underlying trie key-value pairs. + i : + Current trie level. + + Returns + ------- + node : `eth1spec.base_types.Bytes` + Root node of `obj`. + """ + if len(obj) == 0: + # note: empty storage tree has merkle root: + # + # crypto.keccak256(RLP(b'')) + # == + # 56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421 # noqa: E501,SC100 + # + # also: + # + # crypto.keccak256(RLP(())) + # == + # 1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347 # noqa: E501,SC100 + # + # which is the sha3Uncles hash in block header for no uncles + + return b"" + + key = next(iter(obj)) # get first key, will reuse below + + # if leaf node + if len(obj) == 1: + leaf = obj[key] + node: rlp.RLP = encode_leaf(leaf) + return (nibble_list_to_compact(key[i:], True), node) + + # prepare for extension node check by finding max j such that all keys in + # obj have the same key[i:j] + substring = copy(key) + j = Uint(len(substring)) + for key in obj: + j = min(j, Uint(len(key))) + substring = substring[:j] + for x in range(i, j): + # mismatch -- reduce j to best previous value + if key[x] != substring[x]: + j = Uint(x) + substring = substring[:j] + break + # finished searching, found another key at the current level + if i == j: + break + + # if extension node + if i != j: + child = node_cap(obj, j) + return (nibble_list_to_compact(key[i:j], False), child) + + # otherwise branch node + def build_branch(j: int) -> rlp.RLP: + branch = {} + skip = {} + for (k, v) in obj.items(): + if len(k) <= i: + skip[k] = True + if k in skip: + continue + if k[i] == j: + branch[k] = v + + return node_cap(branch, i + 1) + + value: Bytes = b"" + for key in obj: + if len(key) == i: + # shouldn't ever have an account or receipt in an internal node + if isinstance(obj[key], (Account, Receipt, Uint)): + raise TypeError() + value = cast(Bytes, obj[key]) + break + + return [build_branch(k) for k in range(16)] + [value] diff --git a/src/ethereum/vm/__init__.py b/src/ethereum/vm/__init__.py new file mode 100644 index 0000000000..487a2867a7 --- /dev/null +++ b/src/ethereum/vm/__init__.py @@ -0,0 +1,57 @@ +""" +Ethereum Virtual Machine (EVM) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The abstract computer which runs the code stored in an +`eth1spec.eth_types.Account`. +""" + +from dataclasses import dataclass +from typing import List + +from ..eth_types import U256, Address, Hash32, State, Uint + +__all__ = ("Environment", "Evm") + + +@dataclass +class Environment: + """ + Items external to the virtual machine itself, provided by the environment. + """ + + caller: Address + block_hashes: List[Hash32] + origin: Address + coinbase: Address + number: Uint + gas_limit: Uint + gas_price: U256 + time: U256 + difficulty: Uint + state: State + + +@dataclass +class Evm: + """The internal state of the virtual machine.""" + + pc: Uint + stack: List[U256] + memory: bytearray + code: bytes + gas_left: U256 + current: Address + caller: Address + data: bytes + value: U256 + depth: Uint + env: Environment + refund_counter: Uint diff --git a/src/ethereum/vm/error.py b/src/ethereum/vm/error.py new file mode 100644 index 0000000000..8a01daaf6d --- /dev/null +++ b/src/ethereum/vm/error.py @@ -0,0 +1,38 @@ +""" +Ethereum Virtual Machine (EVM) Errors +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Errors which cause the EVM to halt exceptionally. +""" + + +class StackUnderflowError(Exception): + """ + Occurs when a pop is executed on an empty stack. + """ + + pass + + +class StackOverflowError(Exception): + """ + Occurs when a push is executed on a stack at max capacity. + """ + + pass + + +class OutOfGasError(Exception): + """ + Occurs when an operation costs more than the amount of gas left in the + frame. + """ + + pass diff --git a/src/ethereum/vm/gas.py b/src/ethereum/vm/gas.py new file mode 100644 index 0000000000..488df51999 --- /dev/null +++ b/src/ethereum/vm/gas.py @@ -0,0 +1,43 @@ +""" +Ethereum Virtual Machine (EVM) Gas +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +EVM gas constants and calculators. +""" + +from ..base_types import U256 +from .error import OutOfGasError + +GAS_VERY_LOW = U256(3) +GAS_STORAGE_SET = U256(20000) +GAS_STORAGE_UPDATE = U256(5000) +GAS_STORAGE_CLEAR_REFUND = U256(15000) + + +def subtract_gas(gas_left: U256, amount: U256) -> U256: + """ + Subtracts `amount` from `gas_left`. + + Parameters + ---------- + gas_left : + The amount of gas left in the current frame. + amount : + The amount of gas the current operation requires. + + Raises + ------ + OutOfGasError + If `gas_left` is less than `amount`. + """ + if gas_left < amount: + raise OutOfGasError + + return gas_left - amount diff --git a/src/ethereum/vm/instructions.py b/src/ethereum/vm/instructions.py new file mode 100644 index 0000000000..994364f3aa --- /dev/null +++ b/src/ethereum/vm/instructions.py @@ -0,0 +1,114 @@ +""" +Ethereum Virtual Machine (EVM) Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the instructions understood by the EVM. +""" + + +from ..base_types import U256 +from . import Evm +from .gas import ( + GAS_STORAGE_CLEAR_REFUND, + GAS_STORAGE_SET, + GAS_STORAGE_UPDATE, + GAS_VERY_LOW, + subtract_gas, +) +from .stack import pop, push + + +def add(evm: Evm) -> None: + """ + Adds the top two elements of the stack together, and pushes the result back + on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + StackUnderflowError + If `len(stack)` is less than `2`. + OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + x = pop(evm.stack) + y = pop(evm.stack) + + val = x.wrapping_add(y) + + push(evm.stack, val) + + +def sstore(evm: Evm) -> None: + """ + Stores a value at a certain key in the current context's storage. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + StackUnderflowError + If `len(stack)` is less than `2`. + OutOfGasError + If `evm.gas_left` is less than `20000`. + """ + key = pop(evm.stack).to_be_bytes32() + new_value = pop(evm.stack) + current_value = evm.env.state[evm.current].storage.get(key, U256(0)) + + # TODO: SSTORE gas usage hasn't been tested yet. Testing this needs + # other opcodes to be implemented. + # Calculating the gas needed for the storage + if new_value != 0 and current_value == 0: + gas_cost = GAS_STORAGE_SET + else: + gas_cost = GAS_STORAGE_UPDATE + + evm.gas_left = subtract_gas(evm.gas_left, gas_cost) + + # TODO: Refund counter hasn't been tested yet. Testing this needs other + # Opcodes to be implemented + if new_value == 0 and current_value != 0: + evm.refund_counter += GAS_STORAGE_CLEAR_REFUND + + if new_value == 0: + del evm.env.state[evm.current].storage[key] + else: + evm.env.state[evm.current].storage[key] = new_value + + +def push1(evm: Evm) -> None: + """ + Pushes a one-byte immediate onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + StackOverflowError + If `len(stack)` is equals `1024`. + OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + push(evm.stack, U256(evm.code[evm.pc + 1])) + evm.pc += 1 diff --git a/src/ethereum/vm/interpreter.py b/src/ethereum/vm/interpreter.py new file mode 100644 index 0000000000..8b051555a1 --- /dev/null +++ b/src/ethereum/vm/interpreter.py @@ -0,0 +1,94 @@ +""" +Ethereum Virtual Machine (EVM) Interpreter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +A straightforward interpreter that executes EVM code. +""" + +from typing import List, Tuple + +from ..base_types import U256, Uint +from ..eth_types import Address, Log +from . import Environment, Evm +from .ops import op_implementation + + +def process_call( + caller: Address, + target: Address, + data: bytes, + value: U256, + gas: U256, + depth: Uint, + env: Environment, +) -> Tuple[U256, List[Log]]: + """ + Executes a call from the `caller` to the `target` in a new EVM instance. + + Parameters + ---------- + caller : + Account which initiated this call. + + target : + Account whose code will be executed. + + data : + Array of bytes provided to the code in `target`. + + value : + Value to be transferred. + + gas : + Gas provided for the code in `target`. + + depth : + Number of call/contract creation environments on the call stack. + + env : + External items required for EVM execution. + + Returns + ------- + output : `Tuple[U256, List[eth1spec.eth_types.Log]]` + The tuple `(gas_left, logs)`, where `gas_left` is the remaining gas + after execution, and logs is the list of `eth1spec.eth_types.Log` + generated during execution. + """ + evm = Evm( + pc=Uint(0), + stack=[], + memory=bytearray(), + code=env.state[target].code, + gas_left=gas, + current=target, + caller=caller, + data=data, + value=value, + depth=depth, + env=env, + refund_counter=Uint(0), + ) + + logs: List[Log] = [] + + if evm.value != 0: + evm.env.state[evm.caller].balance -= evm.value + evm.env.state[evm.current].balance += evm.value + + while evm.pc < len(evm.code): + op = evm.code[evm.pc] + op_implementation[op](evm) + evm.pc += 1 + + gas_used = gas - evm.gas_left + refund = min(gas_used // 2, evm.refund_counter) + + return evm.gas_left + refund, logs diff --git a/src/ethereum/vm/ops.py b/src/ethereum/vm/ops.py new file mode 100644 index 0000000000..0c0a83584c --- /dev/null +++ b/src/ethereum/vm/ops.py @@ -0,0 +1,26 @@ +""" +Instruction Encoding (Opcodes) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Machine readable representations of EVM instructions, and a mapping to their +implementations. +""" + +from .instructions import add, push1, sstore + +ADD = 0x01 +PUSH1 = 0x60 +SSTORE = 0x55 + +op_implementation = { + ADD: add, + SSTORE: sstore, + PUSH1: push1, +} diff --git a/src/ethereum/vm/stack.py b/src/ethereum/vm/stack.py new file mode 100644 index 0000000000..87e6f37581 --- /dev/null +++ b/src/ethereum/vm/stack.py @@ -0,0 +1,66 @@ +""" +Ethereum Virtual Machine (EVM) Stack +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the stack operators for the EVM. +""" + +from typing import List + +from ..base_types import U256 +from .error import StackOverflowError, StackUnderflowError + + +def pop(stack: List[U256]) -> U256: + """ + Pops the top item off of `stack`. + + Parameters + ---------- + stack : + EVM stack. + + Returns + ------- + value : `U256` + The top element on the stack. + + Raises + ------ + StackUnderflowError + If `stack` is empty. + """ + if len(stack) == 0: + raise StackUnderflowError + + return stack.pop() + + +def push(stack: List[U256], value: U256) -> None: + """ + Pushes `value` onto `stack`. + + Parameters + ---------- + stack : + EVM stack. + + value : + Item to be pushed onto `stack`. + + Raises + ------ + StackOverflowError + If `len(stack)` is `1024`. + """ + if len(stack) == 1024: + raise StackOverflowError + + return stack.append(value) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/fixtures b/tests/fixtures new file mode 160000 index 0000000000..82f6a5a736 --- /dev/null +++ b/tests/fixtures @@ -0,0 +1 @@ +Subproject commit 82f6a5a7362e703a64dce303dd0c30f264b8195f diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000000..c45cc2a6d2 --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,67 @@ +from typing import Any + +from ethereum import crypto, rlp +from ethereum.base_types import Uint +from ethereum.eth_types import ( + U256, + Address, + Bytes, + Bytes8, + Bytes32, + Hash32, + Root, +) + + +def to_bytes(x: str) -> Bytes: + if x is None: + return b"" + if has_hex_prefix(x): + return hex2bytes(x) + return x.encode() + + +def hex2bytes(x: str) -> Bytes: + return bytes.fromhex(remove_hex_prefix(x)) + + +def hex2bytes8(x: str) -> Bytes8: + return Bytes8(bytes.fromhex(remove_hex_prefix(x).rjust(16, "0"))) + + +def hex2bytes32(x: str) -> Bytes32: + return Bytes32(bytes.fromhex(remove_hex_prefix(x).rjust(64, "0"))) + + +def hex2hash(x: str) -> Hash32: + return Hash32(bytes.fromhex(remove_hex_prefix(x))) + + +def hex2root(x: str) -> Root: + return Root(bytes.fromhex(remove_hex_prefix(x))) + + +def hex2address(x: str) -> Address: + return Address(bytes.fromhex(remove_hex_prefix(x).rjust(40, "0"))) + + +def hex2uint(x: str) -> Uint: + return Uint(int(x, 16)) + + +def hex2u256(x: str) -> U256: + return U256(int(x, 16)) + + +def has_hex_prefix(x: str) -> bool: + return x.startswith("0x") + + +def remove_hex_prefix(x: str) -> str: + if has_hex_prefix(x): + return x[len("0x") :] + return x + + +def rlp_hash(x: Any) -> Bytes: + return crypto.keccak256(rlp.encode(x)) diff --git a/tests/test_base_types.py b/tests/test_base_types.py new file mode 100644 index 0000000000..244ab3732a --- /dev/null +++ b/tests/test_base_types.py @@ -0,0 +1,1184 @@ +import pytest + +from ethereum.base_types import U256, Uint + + +def test_uint_new() -> None: + value = Uint(5) + assert isinstance(value, int) + assert isinstance(value, Uint) + assert value == 5 + + +def test_uint_new_negative() -> None: + with pytest.raises(ValueError): + Uint(-5) + + +def test_uint_new_float() -> None: + with pytest.raises(TypeError): + Uint(0.1) # type: ignore + + +def test_uint_radd() -> None: + value = 4 + Uint(5) + assert isinstance(value, Uint) + assert value == 9 + + +def test_uint_radd_negative() -> None: + with pytest.raises(ValueError): + (-4) + Uint(5) + + +def test_uint_radd_float() -> None: + value = (1.0) + Uint(5) + assert not isinstance(value, int) + assert value == 6.0 + + +def test_uint_add() -> None: + value = Uint(5) + 4 + assert isinstance(value, Uint) + assert value == 9 + + +def test_uint_add_negative() -> None: + with pytest.raises(ValueError): + Uint(5) + (-4) + + +def test_uint_add_float() -> None: + value = Uint(5) + (1.0) + assert not isinstance(value, int) + assert value == 6.0 + + +def test_uint_iadd() -> None: + value = Uint(5) + value += 4 + assert isinstance(value, Uint) + assert value == 9 + + +def test_uint_iadd_negative() -> None: + value = Uint(5) + with pytest.raises(ValueError): + value += -4 + + +def test_uint_iadd_float() -> None: + value = Uint(5) + value += 1.0 # type: ignore + assert not isinstance(value, int) + assert value == 6.0 + + +def test_uint_rsub() -> None: + value = 6 - Uint(5) + assert isinstance(value, Uint) + assert value == 1 + + +def test_uint_rsub_too_big() -> None: + with pytest.raises(ValueError): + 6 - Uint(7) + + +def test_uint_rsub_negative() -> None: + with pytest.raises(ValueError): + (-4) - Uint(5) + + +def test_uint_rsub_float() -> None: + value = (6.0) - Uint(5) + assert not isinstance(value, int) + assert value == 1.0 + + +def test_uint_sub() -> None: + value = Uint(5) - 4 + assert isinstance(value, Uint) + assert value == 1 + + +def test_uint_sub_too_big() -> None: + with pytest.raises(ValueError): + Uint(5) - 6 + + +def test_uint_sub_negative() -> None: + with pytest.raises(ValueError): + Uint(5) - (-4) + + +def test_uint_sub_float() -> None: + value = Uint(5) - (1.0) + assert not isinstance(value, int) + assert value == 4.0 + + +def test_uint_isub() -> None: + value = Uint(5) + value -= 4 + assert isinstance(value, Uint) + assert value == 1 + + +def test_uint_isub_too_big() -> None: + value = Uint(5) + with pytest.raises(ValueError): + value -= 6 + + +def test_uint_isub_negative() -> None: + value = Uint(5) + with pytest.raises(ValueError): + value -= -4 + + +def test_uint_isub_float() -> None: + value = Uint(5) + value -= 1.0 # type: ignore + assert not isinstance(value, int) + assert value == 4.0 + + +def test_uint_rmul() -> None: + value = 4 * Uint(5) + assert isinstance(value, Uint) + assert value == 20 + + +def test_uint_rmul_negative() -> None: + with pytest.raises(ValueError): + (-4) * Uint(5) + + +def test_uint_rmul_float() -> None: + value = (1.0) * Uint(5) + assert not isinstance(value, int) + assert value == 5.0 + + +def test_uint_mul() -> None: + value = Uint(5) * 4 + assert isinstance(value, Uint) + assert value == 20 + + +def test_uint_mul_negative() -> None: + with pytest.raises(ValueError): + Uint(5) * (-4) + + +def test_uint_mul_float() -> None: + value = Uint(5) * (1.0) + assert not isinstance(value, int) + assert value == 5.0 + + +def test_uint_imul() -> None: + value = Uint(5) + value *= 4 + assert isinstance(value, Uint) + assert value == 20 + + +def test_uint_imul_negative() -> None: + value = Uint(5) + with pytest.raises(ValueError): + value *= -4 + + +def test_uint_imul_float() -> None: + value = Uint(5) + value *= 1.0 # type: ignore + assert not isinstance(value, int) + assert value == 5.0 + + +def test_uint_floordiv() -> None: + value = Uint(5) // 2 + assert isinstance(value, Uint) + assert value == 2 + + +def test_uint_floordiv_negative() -> None: + with pytest.raises(ValueError): + Uint(5) // -2 + + +def test_uint_floordiv_float() -> None: + value = Uint(5) // 2.0 + assert not isinstance(value, Uint) + assert value == 2 + + +def test_uint_rfloordiv() -> None: + value = 5 // Uint(2) + assert isinstance(value, Uint) + assert value == 2 + + +def test_uint_rfloordiv_negative() -> None: + with pytest.raises(ValueError): + (-2) // Uint(5) + + +def test_uint_rfloordiv_float() -> None: + value = 5.0 // Uint(2) + assert not isinstance(value, Uint) + assert value == 2 + + +def test_uint_ifloordiv() -> None: + value = Uint(5) + value //= 2 + assert isinstance(value, Uint) + assert value == 2 + + +def test_uint_ifloordiv_negative() -> None: + value = Uint(5) + with pytest.raises(ValueError): + value //= -2 + + +def test_uint_rmod() -> None: + value = 6 % Uint(5) + assert isinstance(value, Uint) + assert value == 1 + + +def test_uint_rmod_negative() -> None: + with pytest.raises(ValueError): + (-4) % Uint(5) + + +def test_uint_rmod_float() -> None: + value = (6.0) % Uint(5) + assert not isinstance(value, int) + assert value == 1.0 + + +def test_uint_mod() -> None: + value = Uint(5) % 4 + assert isinstance(value, Uint) + assert value == 1 + + +def test_uint_mod_negative() -> None: + with pytest.raises(ValueError): + Uint(5) % (-4) + + +def test_uint_mod_float() -> None: + value = Uint(5) % (1.0) + assert not isinstance(value, int) + assert value == 0.0 + + +def test_uint_imod() -> None: + value = Uint(5) + value %= 4 + assert isinstance(value, Uint) + assert value == 1 + + +def test_uint_imod_negative() -> None: + value = Uint(5) + with pytest.raises(ValueError): + value %= -4 + + +def test_uint_imod_float() -> None: + value = Uint(5) + value %= 1.0 # type: ignore + assert not isinstance(value, int) + assert value == 0.0 + + +def test_uint_divmod() -> None: + quotient, remainder = divmod(Uint(5), 2) + assert isinstance(quotient, Uint) + assert isinstance(remainder, Uint) + assert quotient == 2 + assert remainder == 1 + + +def test_uint_divmod_negative() -> None: + with pytest.raises(ValueError): + divmod(Uint(5), -2) + + +def test_uint_divmod_float() -> None: + quotient, remainder = divmod(Uint(5), 2.0) + assert not isinstance(quotient, Uint) + assert not isinstance(remainder, Uint) + assert quotient == 2 + assert remainder == 1 + + +def test_uint_rdivmod() -> None: + quotient, remainder = divmod(5, Uint(2)) + assert isinstance(quotient, Uint) + assert isinstance(remainder, Uint) + assert quotient == 2 + assert remainder == 1 + + +def test_uint_rdivmod_negative() -> None: + with pytest.raises(ValueError): + divmod(-5, Uint(2)) + + +def test_uint_rdivmod_float() -> None: + quotient, remainder = divmod(5.0, Uint(2)) + assert not isinstance(quotient, Uint) + assert not isinstance(remainder, Uint) + assert quotient == 2 + assert remainder == 1 + + +def test_uint_pow() -> None: + value = Uint(3) ** 2 + assert isinstance(value, Uint) + assert value == 9 + + +def test_uint_pow_negative() -> None: + with pytest.raises(ValueError): + Uint(3) ** -2 + + +def test_uint_pow_modulo() -> None: + value = pow(Uint(4), 2, 3) + assert isinstance(value, Uint) + assert value == 1 + + +def test_uint_pow_modulo_negative() -> None: + with pytest.raises(ValueError): + pow(Uint(4), 2, -3) + + +def test_uint_rpow() -> None: + value = 3 ** Uint(2) + assert isinstance(value, Uint) + assert value == 9 + + +def test_uint_rpow_negative() -> None: + with pytest.raises(ValueError): + (-3) ** Uint(2) + + +def test_uint_rpow_modulo() -> None: + value = Uint.__rpow__(Uint(2), 4, 3) + assert isinstance(value, int) + assert value == 1 + + +def test_uint_rpow_modulo_negative() -> None: + with pytest.raises(ValueError): + Uint.__rpow__(Uint(2), 4, -3) + + +def test_uint_ipow() -> None: + value = Uint(3) + value **= 2 + assert isinstance(value, Uint) + assert value == 9 + + +def test_uint_ipow_negative() -> None: + value = Uint(3) + with pytest.raises(ValueError): + value **= -2 + + +def test_uint_ipow_modulo() -> None: + value = Uint(4).__ipow__(2, 3) + assert isinstance(value, Uint) + assert value == 1 + + +def test_uint_ipow_modulo_negative() -> None: + with pytest.raises(ValueError): + Uint(4).__ipow__(2, -3) + + +def test_uint_to_be_bytes_zero() -> None: + encoded = Uint(0).to_be_bytes() + assert encoded == bytes([]) + + +def test_uint_to_be_bytes_one() -> None: + encoded = Uint(1).to_be_bytes() + assert encoded == bytes([1]) + + +def test_uint_to_be_bytes_is_big_endian() -> None: + encoded = Uint(0xABCD).to_be_bytes() + assert encoded == bytes([0xAB, 0xCD]) + + +def test_uint_to_be_bytes32_zero() -> None: + encoded = Uint(0).to_be_bytes32() + assert encoded == bytes([0] * 32) + + +def test_uint_to_be_bytes32_one() -> None: + encoded = Uint(1).to_be_bytes32() + assert encoded == bytes([0] * 31 + [1]) + + +def test_uint_to_be_bytes32_max_value() -> None: + encoded = Uint(2 ** 256 - 1).to_be_bytes32() + assert encoded == bytes( + [ + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + ] + ) + + +def test_uint_from_be_bytes_empty() -> None: + value = Uint.from_be_bytes(b"") + assert value == 0 + + +def test_uint_from_be_bytes_one() -> None: + value = Uint.from_be_bytes(bytes([1])) + assert value == 1 + + +def test_uint_from_be_bytes_is_big_endian() -> None: + value = Uint.from_be_bytes(bytes([0xAB, 0xCD])) + assert value == 0xABCD + + +def test_u256_new() -> None: + value = U256(5) + assert isinstance(value, int) + assert isinstance(value, U256) + assert value == 5 + + +def test_u256_new_negative() -> None: + with pytest.raises(ValueError): + U256(-5) + + +def test_u256_new_float() -> None: + with pytest.raises(TypeError): + U256(0.1) # type: ignore + + +def test_u256_new_max_value() -> None: + value = U256(2 ** 256 - 1) + assert isinstance(value, U256) + assert value == 2 ** 256 - 1 + + +def test_u256_new_too_large() -> None: + with pytest.raises(ValueError): + U256(2 ** 256) + + +def test_u256_radd() -> None: + value = 4 + U256(5) + assert isinstance(value, U256) + assert value == 9 + + +def test_u256_radd_overflow() -> None: + with pytest.raises(ValueError): + (2 ** 256 - 1) + U256(5) + + +def test_u256_radd_negative() -> None: + with pytest.raises(ValueError): + (-4) + U256(5) + + +def test_u256_radd_float() -> None: + value = (1.0) + U256(5) + assert not isinstance(value, int) + assert value == 6.0 + + +def test_u256_add() -> None: + value = U256(5) + 4 + assert isinstance(value, U256) + assert value == 9 + + +def test_u256_add_overflow() -> None: + with pytest.raises(ValueError): + U256(5) + (2 ** 256 - 1) + + +def test_u256_add_negative() -> None: + with pytest.raises(ValueError): + U256(5) + (-4) + + +def test_u256_add_float() -> None: + value = U256(5) + (1.0) + assert not isinstance(value, int) + assert value == 6.0 + + +def test_u256_wrapping_add() -> None: + value = U256(5).wrapping_add(4) + assert isinstance(value, U256) + assert value == 9 + + +def test_u256_wrapping_add_overflow() -> None: + value = U256(5).wrapping_add(2 ** 256 - 1) + assert isinstance(value, U256) + assert value == 4 + + +def test_u256_wrapping_add_negative() -> None: + with pytest.raises(ValueError): + U256(5).wrapping_add(-4) + + +def test_u256_iadd() -> None: + value = U256(5) + value += 4 + assert isinstance(value, U256) + assert value == 9 + + +def test_u256_iadd_negative() -> None: + value = U256(5) + with pytest.raises(ValueError): + value += -4 + + +def test_u256_iadd_float() -> None: + value = U256(5) + value += 1.0 # type: ignore + assert not isinstance(value, int) + assert value == 6.0 + + +def test_u256_iadd_overflow() -> None: + value = U256(5) + with pytest.raises(ValueError): + value += 2 ** 256 - 1 + + +def test_u256_rsub() -> None: + value = 5 - U256(4) + assert isinstance(value, U256) + assert value == 1 + + +def test_u256_rsub_underflow() -> None: + with pytest.raises(ValueError): + (0) - U256(1) + + +def test_u256_rsub_negative() -> None: + with pytest.raises(ValueError): + (-4) - U256(5) + + +def test_u256_rsub_float() -> None: + value = (5.0) - U256(1) + assert not isinstance(value, int) + assert value == 4.0 + + +def test_u256_sub() -> None: + value = U256(5) - 4 + assert isinstance(value, U256) + assert value == 1 + + +def test_u256_sub_underflow() -> None: + with pytest.raises(ValueError): + U256(5) - 6 + + +def test_u256_sub_negative() -> None: + with pytest.raises(ValueError): + U256(5) - (-4) + + +def test_u256_sub_float() -> None: + value = U256(5) - (1.0) + assert not isinstance(value, int) + assert value == 4.0 + + +def test_u256_wrapping_sub() -> None: + value = U256(5).wrapping_sub(4) + assert isinstance(value, U256) + assert value == 1 + + +def test_u256_wrapping_sub_underflow() -> None: + value = U256(5).wrapping_sub(6) + assert isinstance(value, U256) + assert value == 2 ** 256 - 1 + + +def test_u256_wrapping_sub_negative() -> None: + with pytest.raises(ValueError): + U256(5).wrapping_sub(-4) + + +def test_u256_isub() -> None: + value = U256(5) + value -= 4 + assert isinstance(value, U256) + assert value == 1 + + +def test_u256_isub_negative() -> None: + value = U256(5) + with pytest.raises(ValueError): + value -= -4 + + +def test_u256_isub_float() -> None: + value = U256(5) + value -= 1.0 # type: ignore + assert not isinstance(value, int) + assert value == 4.0 + + +def test_u256_isub_underflow() -> None: + value = U256(5) + with pytest.raises(ValueError): + value -= 6 + + +def test_u256_rmul() -> None: + value = 4 * U256(5) + assert isinstance(value, U256) + assert value == 20 + + +def test_u256_rmul_overflow() -> None: + with pytest.raises(ValueError): + (2 ** 256 - 1) * U256(5) + + +def test_u256_rmul_negative() -> None: + with pytest.raises(ValueError): + (-4) * U256(5) + + +def test_u256_rmul_float() -> None: + value = (1.0) * U256(5) + assert not isinstance(value, int) + assert value == 5.0 + + +def test_u256_mul() -> None: + value = U256(5) * 4 + assert isinstance(value, U256) + assert value == 20 + + +def test_u256_mul_overflow() -> None: + with pytest.raises(ValueError): + U256.MAX_VALUE * 4 + + +def test_u256_mul_negative() -> None: + with pytest.raises(ValueError): + U256(5) * (-4) + + +def test_u256_mul_float() -> None: + value = U256(5) * (1.0) + assert not isinstance(value, int) + assert value == 5.0 + + +def test_u256_wrapping_mul() -> None: + value = U256(5).wrapping_mul(4) + assert isinstance(value, U256) + assert value == 20 + + +def test_u256_wrapping_mul_overflow() -> None: + value = U256.MAX_VALUE.wrapping_mul(4) + assert isinstance(value, U256) + assert ( + value + == 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC + ) + + +def test_u256_wrapping_mul_negative() -> None: + with pytest.raises(ValueError): + U256(5).wrapping_mul(-4) + + +def test_u256_imul() -> None: + value = U256(5) + value *= 4 + assert isinstance(value, U256) + assert value == 20 + + +def test_u256_imul_negative() -> None: + value = U256(5) + with pytest.raises(ValueError): + value *= -4 + + +def test_u256_imul_arg_overflow() -> None: + value = U256(5) + with pytest.raises(ValueError): + value *= 2 ** 256 + + +def test_u256_imul_float() -> None: + value = U256(5) + value *= 1.0 # type: ignore + assert not isinstance(value, int) + assert value == 5.0 + + +def test_u256_imul_overflow() -> None: + value = U256(5) + with pytest.raises(ValueError): + value *= 2 ** 256 - 1 + + +def test_u256_floordiv() -> None: + value = U256(5) // 2 + assert isinstance(value, U256) + assert value == 2 + + +def test_u256_floordiv_overflow() -> None: + with pytest.raises(ValueError): + U256(5) // (2 ** 256) + + +def test_u256_floordiv_negative() -> None: + with pytest.raises(ValueError): + U256(5) // -2 + + +def test_u256_floordiv_float() -> None: + value = U256(5) // 2.0 + assert not isinstance(value, U256) + assert value == 2 + + +def test_u256_rfloordiv() -> None: + value = 5 // U256(2) + assert isinstance(value, U256) + assert value == 2 + + +def test_u256_rfloordiv_overflow() -> None: + with pytest.raises(ValueError): + (2 ** 256) // U256(2) + + +def test_u256_rfloordiv_negative() -> None: + with pytest.raises(ValueError): + (-2) // U256(5) + + +def test_u256_rfloordiv_float() -> None: + value = 5.0 // U256(2) + assert not isinstance(value, U256) + assert value == 2 + + +def test_u256_ifloordiv() -> None: + value = U256(5) + value //= 2 + assert isinstance(value, U256) + assert value == 2 + + +def test_u256_ifloordiv_negative() -> None: + value = U256(5) + with pytest.raises(ValueError): + value //= -2 + + +def test_u256_ifloordiv_overflow() -> None: + value = U256(5) + with pytest.raises(ValueError): + value //= 2 ** 256 + + +def test_u256_rmod() -> None: + value = 6 % U256(5) + assert isinstance(value, U256) + assert value == 1 + + +def test_u256_rmod_negative() -> None: + with pytest.raises(ValueError): + (-4) % U256(5) + + +def test_u256_rmod_float() -> None: + value = (6.0) % U256(5) + assert not isinstance(value, int) + assert value == 1.0 + + +def test_u256_mod() -> None: + value = U256(5) % 4 + assert isinstance(value, U256) + assert value == 1 + + +def test_u256_mod_overflow() -> None: + with pytest.raises(ValueError): + U256(5) % (2 ** 256) + + +def test_u256_mod_negative() -> None: + with pytest.raises(ValueError): + U256(5) % (-4) + + +def test_u256_mod_float() -> None: + value = U256(5) % (1.0) + assert not isinstance(value, int) + assert value == 0.0 + + +def test_u256_imod() -> None: + value = U256(5) + value %= 4 + assert isinstance(value, U256) + assert value == 1 + + +def test_u256_imod_overflow() -> None: + value = U256(5) + with pytest.raises(ValueError): + value %= 2 ** 256 + + +def test_u256_imod_negative() -> None: + value = U256(5) + with pytest.raises(ValueError): + value %= -4 + + +def test_u256_imod_float() -> None: + value = U256(5) + value %= 1.0 # type: ignore + assert not isinstance(value, int) + assert value == 0.0 + + +def test_u256_divmod() -> None: + quotient, remainder = divmod(U256(5), 2) + assert isinstance(quotient, U256) + assert isinstance(remainder, U256) + assert quotient == 2 + assert remainder == 1 + + +def test_u256_divmod_overflow() -> None: + with pytest.raises(ValueError): + divmod(U256(5), 2 ** 256) + + +def test_u256_divmod_negative() -> None: + with pytest.raises(ValueError): + divmod(U256(5), -2) + + +def test_u256_divmod_float() -> None: + quotient, remainder = divmod(U256(5), 2.0) + assert not isinstance(quotient, U256) + assert not isinstance(remainder, U256) + assert quotient == 2 + assert remainder == 1 + + +def test_u256_rdivmod() -> None: + quotient, remainder = divmod(5, U256(2)) + assert isinstance(quotient, U256) + assert isinstance(remainder, U256) + assert quotient == 2 + assert remainder == 1 + + +def test_u256_rdivmod_overflow() -> None: + with pytest.raises(ValueError): + divmod(2 ** 256, U256(2)) + + +def test_u256_rdivmod_negative() -> None: + with pytest.raises(ValueError): + divmod(-5, U256(2)) + + +def test_u256_rdivmod_float() -> None: + quotient, remainder = divmod(5.0, U256(2)) + assert not isinstance(quotient, U256) + assert not isinstance(remainder, U256) + assert quotient == 2 + assert remainder == 1 + + +def test_u256_pow() -> None: + value = U256(3) ** 2 + assert isinstance(value, U256) + assert value == 9 + + +def test_u256_pow_overflow() -> None: + with pytest.raises(ValueError): + U256(340282366920938463463374607431768211456) ** 3 + + +def test_u256_pow_negative() -> None: + with pytest.raises(ValueError): + U256(3) ** -2 + + +def test_u256_pow_modulo() -> None: + value = pow(U256(4), 2, 3) + assert isinstance(value, U256) + assert value == 1 + + +def test_u256_pow_modulo_overflow() -> None: + with pytest.raises(ValueError): + pow(U256(4), 2, 2 ** 256) + + +def test_u256_pow_modulo_negative() -> None: + with pytest.raises(ValueError): + pow(U256(4), 2, -3) + + +def test_u256_rpow() -> None: + value = 3 ** U256(2) + assert isinstance(value, U256) + assert value == 9 + + +def test_u256_rpow_overflow() -> None: + with pytest.raises(ValueError): + (2 ** 256) ** U256(2) + + +def test_u256_rpow_negative() -> None: + with pytest.raises(ValueError): + (-3) ** U256(2) + + +def test_u256_rpow_modulo() -> None: + value = U256.__rpow__(U256(2), 4, 3) + assert isinstance(value, int) + assert value == 1 + + +def test_u256_rpow_modulo_overflow() -> None: + with pytest.raises(ValueError): + U256.__rpow__(U256(2), 4, 2 ** 256) + + +def test_u256_rpow_modulo_negative() -> None: + with pytest.raises(ValueError): + U256.__rpow__(U256(2), 4, -3) + + +def test_u256_ipow() -> None: + value = U256(3) + value **= 2 + assert isinstance(value, U256) + assert value == 9 + + +def test_u256_ipow_overflow() -> None: + value = U256(340282366920938463463374607431768211456) + with pytest.raises(ValueError): + value **= 3 + + +def test_u256_ipow_negative() -> None: + value = U256(3) + with pytest.raises(ValueError): + value **= -2 + + +def test_u256_ipow_modulo() -> None: + value = U256(4).__ipow__(2, 3) + assert isinstance(value, U256) + assert value == 1 + + +def test_u256_ipow_modulo_negative() -> None: + with pytest.raises(ValueError): + U256(4).__ipow__(2, -3) + + +def test_u256_ipow_modulo_overflow() -> None: + with pytest.raises(ValueError): + U256(4).__ipow__(2, 2 ** 256) + + +def test_u256_wrapping_pow() -> None: + value = U256(3).wrapping_pow(2) + assert isinstance(value, U256) + assert value == 9 + + +def test_u256_wrapping_pow_overflow() -> None: + value = U256(340282366920938463463374607431768211455).wrapping_pow(3) + assert isinstance(value, U256) + assert value == 0x2FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + + +def test_u256_wrapping_pow_negative() -> None: + with pytest.raises(ValueError): + U256(3).wrapping_pow(-2) + + +def test_u256_wrapping_pow_modulo() -> None: + value = U256(4).wrapping_pow(2, 3) + assert isinstance(value, U256) + assert value == 1 + + +def test_u256_wrapping_pow_modulo_overflow() -> None: + with pytest.raises(ValueError): + U256(4).wrapping_pow(2, 2 ** 256) + + +def test_u256_wrapping_pow_modulo_negative() -> None: + with pytest.raises(ValueError): + U256(4).wrapping_pow(2, -3) + + +def test_u256_to_be_bytes_zero() -> None: + encoded = U256(0).to_be_bytes() + assert encoded == bytes([]) + + +def test_u256_to_be_bytes_one() -> None: + encoded = U256(1).to_be_bytes() + assert encoded == bytes([1]) + + +def test_u256_to_be_bytes_is_big_endian() -> None: + encoded = U256(0xABCD).to_be_bytes() + assert encoded == bytes([0xAB, 0xCD]) + + +def test_u256_to_be_bytes32_zero() -> None: + encoded = U256(0).to_be_bytes32() + assert encoded == bytes([0] * 32) + + +def test_u256_to_be_bytes32_one() -> None: + encoded = U256(1).to_be_bytes32() + assert encoded == bytes([0] * 31 + [1]) + + +def test_u256_to_be_bytes32_max_value() -> None: + encoded = U256(2 ** 256 - 1).to_be_bytes32() + assert encoded == bytes( + [ + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + ] + ) + + +def test_u256_from_be_bytes_empty() -> None: + value = U256.from_be_bytes(b"") + assert value == 0 + + +def test_u256_from_be_bytes_one() -> None: + value = U256.from_be_bytes(bytes([1])) + assert value == 1 + + +def test_u256_from_be_bytes_is_big_endian() -> None: + value = U256.from_be_bytes(bytes([0xAB, 0xCD])) + assert value == 0xABCD + + +def test_u256_from_be_bytes_too_large() -> None: + with pytest.raises(ValueError): + U256.from_be_bytes(bytes([0xFF] * 33)) diff --git a/tests/test_rlp.py b/tests/test_rlp.py new file mode 100644 index 0000000000..da4cccffcc --- /dev/null +++ b/tests/test_rlp.py @@ -0,0 +1,412 @@ +import json +import os +from typing import List, Sequence, Tuple, Union, cast + +import pytest + +from ethereum import rlp +from ethereum.eth_types import U256, Bytes, Uint +from ethereum.rlp import RLP + +from .helpers import hex2bytes + +# +# Tests for RLP encode +# + +# +# Testing bytes encoding +# + + +def test_rlp_encode_empty_bytes() -> None: + assert rlp.encode_bytes(b"") == bytearray([0x80]) + assert rlp.encode_bytes(bytearray()) == bytearray([0x80]) + + +def test_rlp_encode_single_byte_val_less_than_128() -> None: + assert rlp.encode_bytes(b"x") == bytearray([0x78]) + assert rlp.encode_bytes(bytearray(b"x")) == bytearray([0x78]) + + +def test_rlp_encode_single_byte_val_equal_128() -> None: + assert rlp.encode_bytes(b"\x80") == b"\x81\x80" + assert rlp.encode_bytes(bytearray(b"\x80")) == b"\x81\x80" + + +def test_rlp_encode_single_byte_val_greater_than_128() -> None: + assert rlp.encode_bytes(b"\x83") == bytearray([0x81, 0x83]) + assert rlp.encode_bytes(bytearray(b"\x83")) == bytearray([0x81, 0x83]) + + +def test_rlp_encode_55_bytes() -> None: + assert rlp.encode_bytes(b"\x83" * 55) == bytearray([0xB7]) + bytearray( + b"\x83" * 55 + ) + assert rlp.encode_bytes(bytearray(b"\x83") * 55) == bytearray( + [0xB7] + ) + bytearray(b"\x83" * 55) + + +def test_rlp_encode_large_bytes() -> None: + assert rlp.encode_bytes(b"\x83" * 2 ** 20) == ( + bytearray([0xBA]) + + bytearray(b"\x10\x00\x00") + + bytearray(b"\x83" * 2 ** 20) + ) + assert rlp.encode_bytes(bytearray(b"\x83") * 2 ** 20) == ( + bytearray([0xBA]) + + bytearray(b"\x10\x00\x00") + + bytearray(b"\x83" * 2 ** 20) + ) + + +# +# Testing uint and u256 encoding +# + + +def test_rlp_encode_uint_0() -> None: + assert rlp.encode(Uint(0)) == b"\x80" + + +def test_rlp_encode_uint_byte_max() -> None: + assert rlp.encode(Uint(255)) == b"\x81\xff" + + +def test_rlp_encode_uint256_0() -> None: + assert rlp.encode(U256(0)) == b"\x80" + + +def test_rlp_encode_uint256_byte_max() -> None: + assert rlp.encode(U256(255)) == b"\x81\xff" + + +# +# Testing str encoding +# + + +def test_rlp_encode_empty_str() -> None: + assert rlp.encode("") == b"\x80" + + +def test_rlp_encode_one_char_str() -> None: + assert rlp.encode("h") == b"h" + + +def test_rlp_encode_multi_char_str() -> None: + assert rlp.encode("hello") == b"\x85hello" + + +# +# Testing sequence encoding +# + + +def test_rlp_encode_empty_sequence() -> None: + assert rlp.encode_sequence([]) == bytearray([0xC0]) + + +def test_rlp_encode_single_elem_list_byte() -> None: + assert rlp.encode_sequence([b"hello"]) == bytearray([0xC6]) + b"\x85hello" + + +def test_rlp_encode_single_elem_list_uint() -> None: + assert rlp.encode_sequence([Uint(255)]) == bytearray([0xC2]) + b"\x81\xff" + + +def test_rlp_encode_10_elem_byte_uint_combo() -> None: + raw_data = [b"hello"] * 5 + [Uint(35)] * 5 # type: ignore + expected = ( + bytearray([0xE3]) + + b"\x85hello\x85hello\x85hello\x85hello\x85hello#####" + ) + assert rlp.encode_sequence(raw_data) == expected + + +def test_rlp_encode_20_elem_byte_uint_combo() -> None: + raw_data = [Uint(35)] * 10 + [b"hello"] * 10 # type: ignore + expected = ( + bytearray([0xF8]) + + b"F" + + b"##########\x85hello\x85hello\x85hello\x85hello\x85hello\x85hello\x85hello\x85hello\x85hello\x85hello" + ) + assert rlp.encode_sequence(raw_data) == expected + + +def test_rlp_encode_nested_sequence() -> None: + nested_sequence: Sequence["RLP"] = [ + b"hello", + Uint(255), + [b"how", [b"are", b"you", [b"doing"]]], + ] + expected: Bytes = ( + b"\xdd\x85hello\x81\xff\xd4\x83how\xcf\x83are\x83you\xc6\x85doing" + ) + assert rlp.encode_sequence(nested_sequence) == expected + + +def test_rlp_encode_successfully() -> None: + test_cases = [ + (b"", bytearray([0x80])), + (b"\x83" * 55, bytearray([0xB7]) + bytearray(b"\x83" * 55)), + (Uint(0), b"\x80"), + (Uint(255), b"\x81\xff"), + ([], bytearray([0xC0])), + ( + [b"hello"] * 5 + [Uint(35)] * 5, # type: ignore + bytearray([0xE3]) + + bytearray(b"\x85hello\x85hello\x85hello\x85hello\x85hello#####"), + ), + ( + [b"hello", Uint(255), [b"how", [b"are", b"you", [b"doing"]]]], + bytearray( + b"\xdd\x85hello\x81\xff\xd4\x83how\xcf\x83are\x83you\xc6\x85doing" + ), + ), + ] + for (raw_data, expected_encoding) in test_cases: + assert rlp.encode(cast(RLP, raw_data)) == expected_encoding + + +def test_rlp_encode_fails() -> None: + test_cases = [ + 123, + [b"hello", Uint(255), [b"how", [b"are", [b"you", [123]]]]], + ] + for raw_data in test_cases: + with pytest.raises(TypeError): + rlp.encode(cast(RLP, raw_data)) + + +# +# Tests for RLP decode +# + +# +# Testing bytes decoding +# + + +def test_rlp_decode_to_empty_bytes() -> None: + assert rlp.decode_to_bytes(bytearray([0x80])) == b"" + + +def test_rlp_decode_to_single_byte_less_than_128() -> None: + assert rlp.decode_to_bytes(bytearray([0])) == bytearray([0]) + assert rlp.decode_to_bytes(bytearray([0x78])) == bytearray([0x78]) + + +def test_rlp_decode_to_single_byte_gte_128() -> None: + assert rlp.decode_to_bytes(bytearray([0x81, 0x83])) == b"\x83" + assert rlp.decode_to_bytes(b"\x81\x80") == b"\x80" + + +def test_rlp_decode_to_55_bytes() -> None: + encoding = bytearray([0xB7]) + bytearray(b"\x83" * 55) + expected_raw_data = bytearray(b"\x83") * 55 + assert rlp.decode_to_bytes(encoding) == expected_raw_data + + +def test_rlp_decode_to_large_bytes() -> None: + encoding = bytearray([0xBA]) + b"\x10\x00\x00" + b"\x83" * (2 ** 20) + expected_raw_data = b"\x83" * (2 ** 20) + assert rlp.decode_to_bytes(encoding) == expected_raw_data + + +# +# Testing uint decoding +# + + +def test_rlp_decode_to_zero_uint() -> None: + assert rlp.decode(b"\x80") == Uint(0).to_be_bytes() + + +def test_rlp_decode_to_255_uint() -> None: + assert rlp.decode(b"\x81\xff") == Uint(255).to_be_bytes() + + +# +# Testing string decoding +# + + +def test_rlp_decode_empty_str() -> None: + assert rlp.decode(b"\x80") == "".encode() + + +def test_rlp_decode_one_char_str() -> None: + assert rlp.decode(b"h") == "h".encode() + + +def test_rlp_decode_multi_char_str() -> None: + assert rlp.decode(b"\x85hello") == "hello".encode() + + +# +# Testing sequence decoding +# + + +def test_rlp_decode_to_empty_sequence() -> None: + assert rlp.decode_to_sequence(bytearray([0xC0])) == [] + + +def test_rlp_decode_to_1_elem_sequence_of_byte() -> None: + assert rlp.decode_to_sequence(bytearray([0xC6]) + b"\x85hello") == [ + b"hello" + ] + + +def test_rlp_decode_to_1_elem_sequence_of_uint() -> None: + assert rlp.decode_to_sequence(bytearray([0xC2]) + b"\x81\xff") == [ + Uint(255).to_be_bytes() + ] + + +def test_rlp_decode_to_10_elem_sequence_of_bytes_and_uints() -> None: + encoded_data = ( + bytearray([0xE3]) + + b"\x85hello\x85hello\x85hello\x85hello\x85hello#####" + ) + expected_raw_data = [b"hello"] * 5 + [Uint(35).to_be_bytes()] * 5 + assert rlp.decode_to_sequence(encoded_data) == expected_raw_data + + +def test_rlp_decode_to_20_elem_sequence_of_bytes_and_uints() -> None: + encoded_data = ( + bytearray([0xF8]) + + b"F" + + b"\x85hello\x85hello\x85hello\x85hello\x85hello\x85hello\x85hello\x85hello\x85hello\x85hello##########" + ) + expected_raw_data = [b"hello"] * 10 + [Uint(35).to_be_bytes()] * 10 + assert rlp.decode_to_sequence(encoded_data) == expected_raw_data + + +def test_rlp_decode_to_nested_sequence() -> None: + encoded_data = ( + b"\xdf\x85hello\x81\xff\xd6\x83how\xd1\x83are\x83you\xc8\x85doing\xc1#" + ) + expected_raw_data = [ + b"hello", + Uint(255).to_be_bytes(), + [ + b"how", + [b"are", b"you", [b"doing", [Uint(35).to_be_bytes()]]], + ], + ] + assert rlp.decode_to_sequence(encoded_data) == expected_raw_data + + +def test_rlp_decode_successfully() -> None: + test_cases = [ + (bytearray([0x80]), bytearray()), + (bytearray([0xB7]) + bytearray(b"\x83" * 55), bytearray(b"\x83") * 55), + (bytearray([0xC0]), []), + ( + b"\xdb\x85hello\xd4\x83how\xcf\x83are\x83you\xc6\x85doing", + [b"hello", [b"how", [b"are", b"you", [b"doing"]]]], + ), + ] + for (encoding, expected_raw_data) in test_cases: + assert rlp.decode(encoding) == expected_raw_data + + +def test_rlp_decode_failure_empty_bytes() -> None: + with pytest.raises(Exception): + rlp.decode(b"") + + +def test_roundtrip_encoding_and_decoding() -> None: + test_cases = [ + b"", + b"h", + b"hello how are you doing today?", + Uint(35).to_be_bytes(), + Uint(255).to_be_bytes(), + [], + [ + b"hello", + [b"how", [b"are", b"you", [b"doing", [Uint(255).to_be_bytes()]]]], + ], + ] + for raw_data in test_cases: + assert rlp.decode(rlp.encode(cast(RLP, raw_data))) == raw_data + + +# +# Running ethereum/tests for rlp +# + + +def convert_to_rlp_native( + obj: Union[str, int, Sequence[Union[str, int]]] +) -> RLP: + if isinstance(obj, str): + return bytes(obj, "utf-8") + elif isinstance(obj, int): + return Uint(obj) + + # It's a sequence + return [convert_to_rlp_native(element) for element in obj] + + +def ethtest_fixtures_as_pytest_fixtures( + *test_files: str, +) -> List[Tuple[RLP, Bytes]]: + base_path = "tests/fixtures/RLPTests/" + + test_data = dict() + for test_file in test_files: + with open(os.path.join(base_path, test_file), "r") as fp: + test_data.update(json.load(fp)) + + pytest_fixtures = [] + for test_details in test_data.values(): + if isinstance(test_details["in"], str) and test_details[ + "in" + ].startswith("#"): + test_details["in"] = int(test_details["in"][1:]) + + pytest_fixtures.append( + ( + convert_to_rlp_native(test_details["in"]), + hex2bytes(test_details["out"]), + ) + ) + + return pytest_fixtures + + +@pytest.mark.parametrize( + "raw_data, expected_encoded_data", + ethtest_fixtures_as_pytest_fixtures("rlptest.json"), +) +def test_ethtest_fixtures_for_rlp_encoding( + raw_data: RLP, expected_encoded_data: Bytes +) -> None: + assert rlp.encode(raw_data) == expected_encoded_data + + +@pytest.mark.parametrize( + "raw_data, encoded_data", + ethtest_fixtures_as_pytest_fixtures("RandomRLPTests/example.json"), +) +def test_ethtest_fixtures_for_successfull_rlp_decoding( + raw_data: Bytes, encoded_data: Bytes +) -> None: + decoded_data = rlp.decode(encoded_data) + assert rlp.encode(decoded_data) == encoded_data + + +@pytest.mark.parametrize( + "raw_data, encoded_data", + ethtest_fixtures_as_pytest_fixtures("invalidRLPTest.json"), +) +def test_ethtest_fixtures_for_fails_in_rlp_decoding( + raw_data: Bytes, encoded_data: Bytes +) -> None: + with pytest.raises(Exception): + rlp.decode(encoded_data) diff --git a/tests/test_spec.py b/tests/test_spec.py new file mode 100644 index 0000000000..326fc80527 --- /dev/null +++ b/tests/test_spec.py @@ -0,0 +1,146 @@ +import json +import os +from typing import Any, List, cast + +from ethereum import rlp +from ethereum.base_types import U256 +from ethereum.eth_types import Account, Block, Header, State, Transaction +from ethereum.spec import BlockChain, print_state, state_transition + +from .helpers import ( + hex2address, + hex2bytes, + hex2bytes8, + hex2bytes32, + hex2hash, + hex2root, + hex2u256, + hex2uint, + rlp_hash, +) + + +def test_add() -> None: + run_test("stExample/add11_d0g0v0.json") + + +# loads a blockchain test +def load_test(path: str) -> Any: + with open(path) as f: + test = json.load(f) + + name = os.path.splitext(os.path.basename(path))[0] + testname = name + "_Frontier" + + if testname not in test: + print("test not found") + raise NotImplementedError + + return test[testname] + + +def run_test(path: str) -> None: + base = ( + "tests/fixtures/" + "LegacyTests/Constantinople/BlockchainTests/GeneralStateTests/" + ) + + test = load_test(base + path) + + genesis_header = json_to_header(test.get("genesisBlockHeader")) + genesis = Block( + genesis_header, + [], + [], + ) + + assert rlp_hash(genesis_header) == hex2bytes( + test["genesisBlockHeader"]["hash"] + ) + assert rlp.encode(cast(rlp.RLP, genesis)) == hex2bytes( + test.get("genesisRLP") + ) + + pre_state = json_to_state(test.get("pre")) + expected_post_state = json_to_state(test.get("postState")) + + chain = BlockChain( + blocks=[genesis], + state=pre_state, + ) + + block_obj = None + for block in test.get("blocks"): + header = json_to_header(block.get("blockHeader")) + txs: List[Transaction] = [ + json_to_tx(tx_json) for tx_json in block.get("transactions") + ] + ommers: List[Header] = [ + json_to_header(ommer_json) + for ommer_json in block.get("uncleHeaders") + ] + + assert rlp_hash(header) == hex2bytes(block["blockHeader"]["hash"]) + block_obj = Block(header, txs, ommers) + assert rlp.encode(cast(rlp.RLP, block_obj)) == hex2bytes(block["rlp"]) + + state_transition(chain, block_obj) + + last_block_hash = rlp_hash(chain.blocks[-1].header) + assert last_block_hash == hex2bytes(test["lastblockhash"]) + + assert chain.state == expected_post_state + + +def json_to_header(raw: Any) -> Header: + return Header( + hex2hash(raw.get("parentHash")), + hex2hash(raw.get("uncleHash")), + hex2address(raw.get("coinbase")), + hex2root(raw.get("stateRoot")), + hex2root(raw.get("transactionsTrie")), + hex2root(raw.get("receiptTrie")), + hex2bytes(raw.get("bloom")), + hex2uint(raw.get("difficulty")), + hex2uint(raw.get("number")), + hex2uint(raw.get("gasLimit")), + hex2uint(raw.get("gasUsed")), + hex2u256(raw.get("timestamp")), + hex2bytes(raw.get("extraData")), + hex2bytes32(raw.get("mixHash")), + hex2bytes8(raw.get("nonce")), + ) + + +def json_to_tx(raw: Any) -> Transaction: + return Transaction( + hex2u256(raw.get("nonce")), + hex2u256(raw.get("gasPrice")), + hex2u256(raw.get("gasLimit")), + None if raw.get("to") == "" else hex2address(raw.get("to")), + hex2u256(raw.get("value")), + hex2bytes(raw.get("data")), + hex2u256(raw.get("v")), + hex2u256(raw.get("r")), + hex2u256(raw.get("s")), + ) + + +def json_to_state(raw: Any) -> State: + state = {} + for (addr, acc_state) in raw.items(): + account = Account( + nonce=hex2uint(acc_state.get("nonce", "0x0")), + balance=hex2uint(acc_state.get("balance", "0x0")), + code=hex2bytes(acc_state.get("code", "")), + storage={}, + ) + + for (k, v) in acc_state.get("storage", {}).items(): + account.storage[hex2bytes32(k)] = U256.from_be_bytes( + hex2bytes32(v) + ) + + state[hex2address(addr)] = account + + return state diff --git a/tests/test_trie.py b/tests/test_trie.py new file mode 100644 index 0000000000..e6702ff6c9 --- /dev/null +++ b/tests/test_trie.py @@ -0,0 +1,79 @@ +import json +from typing import Any + +from ethereum import rlp +from ethereum.trie import map_keys, root + +from .helpers import remove_hex_prefix, to_bytes + + +def test_trie_secure_hex() -> None: + tests = load_tests("hex_encoded_securetrie_test.json") + + for (name, test) in tests.items(): + normalized = {} + for (k, v) in test.get("in").items(): + normalized[to_bytes(k)] = to_bytes(v) + + result = root(map_keys(normalized)) + expected = remove_hex_prefix(test.get("root")) + assert result.hex() == expected, f"test {name} failed" + + +def test_trie_secure() -> None: + tests = load_tests("trietest_secureTrie.json") + + for (name, test) in tests.items(): + normalized = {} + for t in test.get("in"): + normalized[to_bytes(t[0])] = to_bytes(t[1]) + + result = root(map_keys(normalized)) + expected = remove_hex_prefix(test.get("root")) + assert result.hex() == expected, f"test {name} failed" + + +def test_trie_secure_any_order() -> None: + tests = load_tests("trieanyorder_secureTrie.json") + + for (name, test) in tests.items(): + normalized = {} + for (k, v) in test.get("in").items(): + normalized[to_bytes(k)] = to_bytes(v) + + result = root(map_keys(normalized)) + expected = remove_hex_prefix(test.get("root")) + assert result.hex() == expected, f"test {name} failed" + + +def test_trie() -> None: + tests = load_tests("trietest.json") + + for (name, test) in tests.items(): + normalized = {} + for t in test.get("in"): + normalized[to_bytes(t[0])] = to_bytes(t[1]) + + result = root(map_keys(normalized, secured=False)) + expected = remove_hex_prefix(test.get("root")) + assert result.hex() == expected, f"test {name} failed" + + +def test_trie_any_order() -> None: + tests = load_tests("trieanyorder.json") + + for (name, test) in tests.items(): + normalized = {} + for (k, v) in test.get("in").items(): + normalized[to_bytes(k)] = to_bytes(v) + + result = root(map_keys(normalized, secured=False)) + expected = remove_hex_prefix(test.get("root")) + assert result.hex() == expected, f"test {name} failed" + + +def load_tests(path: str) -> Any: + with open("tests/fixtures/TrieTests/" + path) as f: + tests = json.load(f) + + return tests diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000000..c65e6c7309 --- /dev/null +++ b/tox.ini @@ -0,0 +1,37 @@ +[tox] +envlist = py3,pypy3 + +[testenv] +extras = + test + lint +commands = + isort src tests setup.py --check --diff --skip-glob "tests/fixtures/*" + black src tests setup.py --check --diff --exclude "tests/fixtures/*" + flake8 src tests setup.py + mypy src tests setup.py --exclude "tests/fixtures/*" + pytest --cov=ethereum --cov-report=term --cov-report "xml:{toxworkdir}/coverage.xml" --ignore-glob='tests/fixtures/*' + +[testenv:pypy3] +extras = + test + lint +commands = + isort src tests setup.py --check --diff --skip-glob "tests/fixtures/*" + flake8 src tests setup.py + pytest --ignore-glob='tests/fixtures/*' + +[testenv:doc] +basepython = python3 +extras = doc +commands = + sphinx-build -d "{toxworkdir}/docs_doctree" doc "{toxworkdir}/docs_out" --color -W -bhtml {posargs} + python -c 'import pathlib; print("documentation available under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "docs_out" / "index.html"))' + +[testenv:doc-autobuild] +basepython = python3 +extras = + doc + doc-autobuild +commands = + sphinx-autobuild doc --watch src "{toxworkdir}/docs_out" {posargs} diff --git a/whitelist.txt b/whitelist.txt new file mode 100644 index 0000000000..7f96d3f4dc --- /dev/null +++ b/whitelist.txt @@ -0,0 +1,214 @@ +radd +Hash64 +Bytes20 +Bytes32 +Bytes64 +Bytes8 +coinbase +coincurve +crypto +encodings +endian +eth +ethereum +evm +Hash32 +hasher +idx +keccak +keccak256 +keccak512 +memoization +merkle +merkleize +ommers +patricialize +rlp +trie +U256 +secp256k1 +secp256k1n +iadd +isub +rsub +imul +mul +rmul +truediv +rtruediv +itruediv +rfloordiv +ifloordiv +floordiv +rmod +imod +mod +divmod +rdivmod +rpow +ipow +pos +dest +fromhex +preimage +substring +klass +vm + +sha3 + +stop +add +mul +sub +div +sdiv +mod +smod +addmod +mulmod +exp +signextend + +lt +gt +slt +sgt +eq +iszero +and +or +xor +not +byte +shl +shr +sar + +keccak256 + +address +balance +origin +caller +callvalue +calldataload +calldatasize +calldatacopy +codesize +codecopy +gasprice +extcodesize +extcodecopy +returndatasize +returndatacopy +extcodehash +blockhash +coinbase +timestamp +number +difficulty +gaslimit +chainid +selfbalance +basefee + +pop +mload +mstore +mstore8 +sload +sstore +jump +jumpi +pc +msize +gas +jumpdest + +push +dup +log + +push1 +push2 +push3 +push4 +push5 +push6 +push7 +push8 +push9 +push10 +push11 +push12 +push13 +push14 +push15 +push16 +push17 +push18 +push19 +push20 +push21 +push22 +push23 +push24 +push25 +push26 +push27 +push28 +push29 +push30 +push31 +push32 +dup1 +dup2 +dup3 +dup4 +dup5 +dup6 +dup7 +dup8 +dup9 +dup10 +dup11 +dup12 +dup13 +dup14 +dup15 +dup16 +swap1 +swap2 +swap3 +swap4 +swap5 +swap6 +swap7 +swap8 +swap9 +swap10 +swap11 +swap12 +swap13 +swap14 +swap15 +swap16 +log0 +log1 +log2 +log3 +log4 + +create +call +callcode +return +delegatecall +create2 + +staticcall + +revert +invalid +selfdestruct