diff --git a/docs/guides/implementing_vm_forks.rst b/docs/guides/implementing_vm_forks.rst new file mode 100644 index 0000000000..697a67bf73 --- /dev/null +++ b/docs/guides/implementing_vm_forks.rst @@ -0,0 +1,55 @@ +Implementing VM forks +===================== + +The Ethereum protocol follows specified rules which continue to be improved through so called +`Ethereum Improvement Proposals (EIPs) `_. 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 `_ which define: + +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`) + +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 +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.``. To create the +scaffolding for the fork run ``python scripts/forking/create_fork.py`` and follow the assistent. + +.. 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 +--------------------------- + +Set the fork as the recent fork +------------------------------- + +Wiring up the tests +------------------- diff --git a/docs/guides/index.rst b/docs/guides/index.rst index 38cae6e525..fd053891a6 100644 --- a/docs/guides/index.rst +++ b/docs/guides/index.rst @@ -12,3 +12,4 @@ This section aims to provide hands-on guides to demonstrate how to use Py-EVM. I architecture understanding_the_mining_process creating_opcodes + implementing_vm_forks diff --git a/scripts/forking/create_fork.py b/scripts/forking/create_fork.py new file mode 100644 index 0000000000..12eed6fb74 --- /dev/null +++ b/scripts/forking/create_fork.py @@ -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 + + print("Specify the fork base (e.g Istanbul):") + fork_base = input(INPUT_PROMPT) + + fork_base_path = FORKS_BASE_PATH / fork_base + if not fork_base_path.exists(): + print(f"No fork exists at {fork_base_path}") + return + + writing_new_fork = create_writing(fork_name) + writing_parent_fork = create_writing(fork_base) + + 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( + 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 + 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() diff --git a/scripts/forking/template/whitelabel/__init__.py b/scripts/forking/template/whitelabel/__init__.py new file mode 100644 index 0000000000..aa71eb5226 --- /dev/null +++ b/scripts/forking/template/whitelabel/__init__.py @@ -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): + # 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 diff --git a/scripts/forking/template/whitelabel/blocks.py b/scripts/forking/template/whitelabel/blocks.py new file mode 100644 index 0000000000..83eb4b4ca2 --- /dev/null +++ b/scripts/forking/template/whitelabel/blocks.py @@ -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)) + ] diff --git a/scripts/forking/template/whitelabel/computation.py b/scripts/forking/template/whitelabel/computation.py new file mode 100644 index 0000000000..0d4f37f398 --- /dev/null +++ b/scripts/forking/template/whitelabel/computation.py @@ -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 diff --git a/scripts/forking/template/whitelabel/headers.py b/scripts/forking/template/whitelabel/headers.py new file mode 100644 index 0000000000..622990a7db --- /dev/null +++ b/scripts/forking/template/whitelabel/headers.py @@ -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) diff --git a/scripts/forking/template/whitelabel/opcodes.py b/scripts/forking/template/whitelabel/opcodes.py new file mode 100644 index 0000000000..d50da6a4ba --- /dev/null +++ b/scripts/forking/template/whitelabel/opcodes.py @@ -0,0 +1,18 @@ +import copy + +from eth_utils.toolz import merge + + +from eth.vm.forks.petersburg.opcodes import ( + PETERSBURG_OPCODES, +) + + +UPDATED_OPCODES = { + # New opcodes +} + +ISTANBUL_OPCODES = merge( + copy.deepcopy(PETERSBURG_OPCODES), + UPDATED_OPCODES, +) diff --git a/scripts/forking/template/whitelabel/state.py b/scripts/forking/template/whitelabel/state.py new file mode 100644 index 0000000000..a2f2f10c5d --- /dev/null +++ b/scripts/forking/template/whitelabel/state.py @@ -0,0 +1,9 @@ +from eth.vm.forks.petersburg.state import ( + PetersburgState +) + +from .computation import IstanbulComputation + + +class IstanbulState(PetersburgState): + computation_class = IstanbulComputation diff --git a/scripts/forking/template/whitelabel/transactions.py b/scripts/forking/template/whitelabel/transactions.py new file mode 100644 index 0000000000..fe3407b7e5 --- /dev/null +++ b/scripts/forking/template/whitelabel/transactions.py @@ -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: + 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, + )