-
Notifications
You must be signed in to change notification settings - Fork 696
Create script + docs to assist with forks #1903
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| Implementing VM forks | ||
| ===================== | ||
|
|
||
| The Ethereum protocol follows specified rules which continue to be improved through so called | ||
| `Ethereum Improvement Proposals (EIPs) <https://eips.ethereum.org/>`_. Every now and then the | ||
| community agrees on a few EIPs to become part of the next protocol upgrade. These upgrades happen | ||
| through so called `Hardforks <https://en.wikipedia.org/wiki/Fork_(blockchain)>`_ which define: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/so called/so-called/ |
||
|
|
||
| 1. A name for the set of rule changes (e.g. the Istanbul hardfork) | ||
| 2. A block number from which on blocks are processed according to these new rules (e.g. ``9069000``) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/on blocks are/blocks start being/ |
||
|
|
||
| Every client that wants to support the official Ethereum protocol needs to implement these changes | ||
| to remain functional. | ||
|
|
||
|
|
||
| This guide covers how to implement new hardforks in Py-EVM. The specifics and impact of each rule | ||
| change many vary a lot between different hardforks and it is out of the scope of this guide to | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/many vary/may vary/ |
||
| cover these in depth. This is mainly a reference guide for developers to ensure the process of | ||
| implementing hardforks in Py-EVM is as smooth and safe as possible. | ||
|
|
||
|
|
||
| Creating the fork module | ||
| ------------------------ | ||
|
|
||
| Every fork is encapsulated in its own module under ``eth.vm.forks.<fork-name>``. To create the | ||
| scaffolding for a new fork run ``python scripts/forking/create_fork.py`` and follow the assistent. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/assistent/assistant/ |
||
|
|
||
| .. code:: sh | ||
|
|
||
| $ python scripts/forking/create_fork.py | ||
| Specify the name of the fork (e.g Muir Glacier): | ||
| -->ancient tavira | ||
| Specify the fork base (e.g Istanbul): | ||
| -->istanbul | ||
| Check your inputs: | ||
| New fork: | ||
| Writing(pascal_case='AncientTavira', lower_dash_case='ancient-tavira', lower_snake_case='ancient_tavira', upper_snake_case='ANCIENT_TAVIRA') | ||
| Base fork: | ||
| Writing(pascal_case='Istanbul', lower_dash_case='istanbul', lower_snake_case='istanbul', upper_snake_case='ISTANBUL') | ||
| Proceed (y/n)? | ||
| -->y | ||
| Your fork is ready! | ||
|
|
||
|
|
||
| Configuring new opcodes | ||
| ----------------------- | ||
|
|
||
| Configuring new precompiles | ||
| --------------------------- | ||
|
|
||
| Activating the fork | ||
| ------------------- | ||
|
|
||
| Ethereum is a protocol that powers different networks. Most notably, the ethereum mainnet but there | ||
| are also other networks such as testnetworks (e.g. Görli) or xDai. If and when a specific network | ||
| will activate a concrete fork remains to be configured on a per network basis. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion for the entire paragraph: Ethereum is a protocol that powers different networks - most notably, the Ethereum mainnet; but there BTW, I think xDai is the project name, and they're using the POA side-chain. |
||
|
|
||
| At the time of writing, Py-EVM has supports the following three networks: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/has supports/supports/ |
||
|
|
||
| - Mainnet | ||
| - Ropsten | ||
| - Goerli | ||
|
|
||
| For each network that wants to activate the fork, we have to create a new constant in | ||
| ``eth/chains/<network>/constants.py`` that describes the block number at which the fork becomes | ||
| active as seen in the following example: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/active/active,/ |
||
|
|
||
| .. literalinclude:: ../../eth/chains/mainnet/constants.py | ||
| :language: python | ||
| :start-after: BYZANTIUM_MAINNET_BLOCK | ||
| :end-before: # Istanbul Block | ||
|
|
||
| Then, | ||
|
|
||
|
|
||
| Wiring up the tests | ||
| ------------------- | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import glob | ||
| from typing import NamedTuple | ||
| import pathlib | ||
| import shutil | ||
|
|
||
| SCRIPT_BASE_PATH = pathlib.Path(__file__).parent | ||
| SCRIPT_TEMPLATE_PATH = SCRIPT_BASE_PATH / 'template' / 'whitelabel' | ||
| ETH_BASE_PATH = SCRIPT_BASE_PATH.parent.parent / 'eth' | ||
| FORKS_BASE_PATH = ETH_BASE_PATH / 'vm' / 'forks' | ||
|
|
||
| INPUT_PROMPT = '-->' | ||
| YES = 'y' | ||
|
|
||
| # Given a fork name of Muir Glacier we need to derive: | ||
| # pascal case: MuirGlacier | ||
| # lower_dash_case: muir-glacier | ||
| # lower_snake_case: muir_glacier | ||
| # upper_snake_case: MUIR_GLACIER | ||
|
|
||
|
|
||
| class Writing(NamedTuple): | ||
| pascal_case: str | ||
| lower_dash_case: str | ||
| lower_snake_case: str | ||
| upper_snake_case: str | ||
|
|
||
|
|
||
| WHITELABEL_FORK = Writing( | ||
| pascal_case="Istanbul", | ||
| lower_dash_case="istanbul", | ||
| lower_snake_case="istanbul", | ||
| upper_snake_case="ISTANBUL", | ||
| ) | ||
|
|
||
| WHITELABEL_PARENT = Writing( | ||
| pascal_case="Petersburg", | ||
| lower_dash_case="petersburg", | ||
| lower_snake_case="petersburg", | ||
| upper_snake_case="PETERSBURG", | ||
| ) | ||
|
|
||
|
|
||
| def bootstrap() -> None: | ||
| print("Specify the name of the fork (e.g Muir Glacier):") | ||
| fork_name = input(INPUT_PROMPT) | ||
|
|
||
| if not all(x.isalpha() or x.isspace() for x in fork_name): | ||
| print(f"Can't use {fork_name} as fork name, must be alphabetical") | ||
| return | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should probably return non-zero so |
||
|
|
||
| print("Specify the fork base (e.g Istanbul):") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This strikes me as something we can pre-populate a default for by looking at the latest mainnet fork. |
||
| fork_base = input(INPUT_PROMPT) | ||
|
|
||
| writing_new_fork = create_writing(fork_name) | ||
| writing_parent_fork = create_writing(fork_base) | ||
|
|
||
| fork_base_path = FORKS_BASE_PATH / writing_parent_fork.lower_snake_case | ||
| if not fork_base_path.exists(): | ||
| print(f"No fork exists at {fork_base_path}") | ||
| return | ||
|
|
||
| print("Check your inputs:") | ||
| print("New fork:") | ||
| print(writing_new_fork) | ||
|
|
||
| print("Base fork:") | ||
| print(writing_parent_fork) | ||
|
|
||
| print("Proceed (y/n)?") | ||
| proceed = input(INPUT_PROMPT) | ||
|
|
||
| if proceed.lower() == YES: | ||
| create_fork(writing_new_fork, writing_parent_fork) | ||
| print("Your fork is ready!") | ||
|
|
||
|
|
||
| def create_writing(fork_name: str): | ||
| # Remove extra spaces | ||
| normalized = " ".join(fork_name.split()) | ||
|
|
||
| snake_case = normalized.replace(' ', '_') | ||
| dash_case = normalized.replace(' ', '-') | ||
| pascal_case = normalized.title().replace(' ', '') | ||
|
|
||
| return Writing( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be ideal for this to output something like: So that it's being very explicit about what the script is going to do. |
||
| pascal_case=pascal_case, | ||
| lower_dash_case=dash_case.lower(), | ||
| lower_snake_case=snake_case.lower(), | ||
| upper_snake_case=snake_case.upper(), | ||
| ) | ||
|
|
||
|
|
||
| def create_fork(writing_new_fork: Writing, writing_parent_fork: Writing) -> None: | ||
| fork_path = FORKS_BASE_PATH / writing_new_fork.lower_snake_case | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest writing all of this to a temporary directory and then moving the temporary directory into place once it's all done. |
||
| shutil.copytree(SCRIPT_TEMPLATE_PATH, fork_path) | ||
| replace_in(fork_path, WHITELABEL_FORK.pascal_case, writing_new_fork.pascal_case) | ||
| replace_in(fork_path, WHITELABEL_FORK.lower_snake_case, writing_new_fork.lower_snake_case) | ||
| replace_in(fork_path, WHITELABEL_FORK.lower_dash_case, writing_new_fork.lower_dash_case) | ||
| replace_in(fork_path, WHITELABEL_FORK.upper_snake_case, writing_new_fork.upper_snake_case) | ||
|
|
||
| replace_in(fork_path, WHITELABEL_PARENT.pascal_case, writing_parent_fork.pascal_case) | ||
| replace_in(fork_path, WHITELABEL_PARENT.lower_snake_case, writing_parent_fork.lower_snake_case) | ||
| replace_in(fork_path, WHITELABEL_PARENT.lower_dash_case, writing_parent_fork.lower_dash_case) | ||
| replace_in(fork_path, WHITELABEL_PARENT.upper_snake_case, writing_parent_fork.upper_snake_case) | ||
|
|
||
|
|
||
| def replace_in(base_path: pathlib.Path, find_text: str, replace_txt: str) -> None: | ||
| for filepath in glob.iglob(f'{base_path}/**/*.py', recursive=True): | ||
| with open(filepath) as file: | ||
| s = file.read() | ||
| s = s.replace(find_text, replace_txt) | ||
| with open(filepath, "w") as file: | ||
| file.write(s) | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| bootstrap() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| from typing import ( | ||
| Type, | ||
| ) | ||
|
|
||
| from eth.rlp.blocks import BaseBlock | ||
| from eth.vm.forks.constantinople import ( | ||
| ConstantinopleVM, | ||
| ) | ||
| from eth.vm.state import BaseState | ||
|
|
||
| from .blocks import IstanbulBlock | ||
| from .headers import ( | ||
| compute_istanbul_difficulty, | ||
| configure_istanbul_header, | ||
| create_istanbul_header_from_parent, | ||
| ) | ||
| from .state import IstanbulState | ||
|
|
||
|
|
||
| class IstanbulVM(ConstantinopleVM): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm thinking these files should be templated rather than "white labeled" from eth.vm.forks.<% parent_fork_snake_case %> import (
<% parent_fork_name %>VM,
)
class <% fork_name %>VM(<% parent_fork_name %>VM):
...This makes it future proof, otherwise we will likely run into a scenario where the find-replace regexing replaces text somewhere like a comment or something.... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, I also thought about that. The one downside is that you'd lose the ability to run simple flake8/mypy checks against it. You'd have to actually create a fork and run flake8/mypy checks against the result. Not necessarily bad though. Anyway, this PR is just a very early draft anyway. But thanks for reviewing anyway (also to @veox 👍 ) |
||
| # fork name | ||
| fork = 'istanbul' | ||
|
|
||
| # classes | ||
| block_class: Type[BaseBlock] = IstanbulBlock | ||
| _state_class: Type[BaseState] = IstanbulState | ||
|
|
||
| # Methods | ||
| create_header_from_parent = staticmethod(create_istanbul_header_from_parent) # type: ignore | ||
| compute_difficulty = staticmethod(compute_istanbul_difficulty) # type: ignore | ||
| configure_header = configure_istanbul_header | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| from rlp.sedes import ( | ||
| CountableList, | ||
| ) | ||
| from eth.rlp.headers import ( | ||
| BlockHeader, | ||
| ) | ||
| from eth.vm.forks.petersburg.blocks import ( | ||
| PetersburgBlock, | ||
| ) | ||
|
|
||
| from .transactions import ( | ||
| IstanbulTransaction, | ||
| ) | ||
|
|
||
|
|
||
| class IstanbulBlock(PetersburgBlock): | ||
| transaction_class = IstanbulTransaction | ||
| fields = [ | ||
| ('header', BlockHeader), | ||
| ('transactions', CountableList(transaction_class)), | ||
| ('uncles', CountableList(BlockHeader)) | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| from eth.vm.forks.petersburg.computation import ( | ||
| PETERSBURG_PRECOMPILES | ||
| ) | ||
| from eth.vm.forks.petersburg.computation import ( | ||
| PetersburgComputation, | ||
| ) | ||
|
|
||
| from .opcodes import ISTANBUL_OPCODES | ||
|
|
||
| ISTANBUL_PRECOMPILES = PETERSBURG_PRECOMPILES | ||
|
|
||
|
|
||
| class IstanbulComputation(PetersburgComputation): | ||
| """ | ||
| A class for all execution computations in the ``Istanbul`` fork. | ||
| Inherits from :class:`~eth.vm.forks.petersburg.PetersburgComputation` | ||
| """ | ||
| # Override | ||
| opcodes = ISTANBUL_OPCODES | ||
| _precompiles = ISTANBUL_PRECOMPILES |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| from eth.vm.forks.petersburg.headers import ( | ||
| configure_header, | ||
| create_header_from_parent, | ||
| compute_petersburg_difficulty, | ||
| ) | ||
|
|
||
|
|
||
| compute_istanbul_difficulty = compute_petersburg_difficulty | ||
|
|
||
| create_istanbul_header_from_parent = create_header_from_parent( | ||
| compute_istanbul_difficulty | ||
| ) | ||
| configure_istanbul_header = configure_header(compute_istanbul_difficulty) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import copy | ||
|
|
||
| from eth_utils.toolz import merge | ||
|
|
||
|
|
||
| from eth.vm.forks.petersburg.opcodes import ( | ||
| PETERSBURG_OPCODES, | ||
| ) | ||
|
|
||
|
|
||
| UPDATED_OPCODES = { | ||
cburgdorf marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # New opcodes | ||
| } | ||
|
|
||
| ISTANBUL_OPCODES = merge( | ||
| copy.deepcopy(PETERSBURG_OPCODES), | ||
| UPDATED_OPCODES, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| from eth.vm.forks.petersburg.state import ( | ||
| PetersburgState | ||
| ) | ||
|
|
||
| from .computation import IstanbulComputation | ||
|
|
||
|
|
||
| class IstanbulState(PetersburgState): | ||
| computation_class = IstanbulComputation |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| from eth_keys.datatypes import PrivateKey | ||
| from eth_typing import Address | ||
|
|
||
| from eth.vm.forks.petersburg.transactions import ( | ||
| PetersburgTransaction, | ||
| PetersburgUnsignedTransaction, | ||
| ) | ||
|
|
||
| from eth._utils.transactions import ( | ||
| create_transaction_signature, | ||
| ) | ||
|
|
||
|
|
||
| class IstanbulTransaction(PetersburgTransaction): | ||
| @classmethod | ||
| def create_unsigned_transaction(cls, | ||
| *, | ||
| nonce: int, | ||
| gas_price: int, | ||
| gas: int, | ||
| to: Address, | ||
| value: int, | ||
| data: bytes) -> 'IstanbulUnsignedTransaction': | ||
| return IstanbulUnsignedTransaction(nonce, gas_price, gas, to, value, data) | ||
|
|
||
|
|
||
| class IstanbulUnsignedTransaction(PetersburgUnsignedTransaction): | ||
| def as_signed_transaction(self, | ||
| private_key: PrivateKey, | ||
| chain_id: int=None) -> IstanbulTransaction: | ||
cburgdorf marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| v, r, s = create_transaction_signature(self, private_key, chain_id=chain_id) | ||
| return IstanbulTransaction( | ||
| nonce=self.nonce, | ||
| gas_price=self.gas_price, | ||
| gas=self.gas, | ||
| to=self.to, | ||
| value=self.value, | ||
| data=self.data, | ||
| v=v, | ||
| r=r, | ||
| s=s, | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/so called/so-called/