-
Notifications
You must be signed in to change notification settings - Fork 693
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/