-
Notifications
You must be signed in to change notification settings - Fork 655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create script + docs to assist with forks #1903
base: main
Are you sure you want to change the base?
Changes from all 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: Dict[int, eth.vm.opcode.Opcode] = { | ||
# 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: | ||
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/