Skip to content
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

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions docs/guides/implementing_vm_forks.rst
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
Copy link
Contributor

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/

`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:
Copy link
Contributor

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/


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``)
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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.
Copy link
Contributor

@veox veox Dec 19, 2019

Choose a reason for hiding this comment

The 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
are also other value-bearing networks (e.g. xDai), or test networks (e.g. Görli). Whether and when a specific network
will activate a fork remains to be configured on a per-network basis.


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:
Copy link
Contributor

Choose a reason for hiding this comment

The 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:
Copy link
Contributor

Choose a reason for hiding this comment

The 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
-------------------
1 change: 1 addition & 0 deletions docs/guides/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
117 changes: 117 additions & 0 deletions scripts/forking/create_fork.py
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably return non-zero so sys.exit(1)?


print("Specify the fork base (e.g Istanbul):")
Copy link
Member

Choose a reason for hiding this comment

The 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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be ideal for this to output something like:

For name: "Istanbul"
python module: ./eth/forks/istanbul/
block number variable name: "ISTANBUL_BLOCK_NUMBER"
...

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
Copy link
Member

Choose a reason for hiding this comment

The 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()
31 changes: 31 additions & 0 deletions scripts/forking/template/whitelabel/__init__.py
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):
Copy link
Member

Choose a reason for hiding this comment

The 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....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking these files should be templated rather than "white labeled"

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
22 changes: 22 additions & 0 deletions scripts/forking/template/whitelabel/blocks.py
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))
]
20 changes: 20 additions & 0 deletions scripts/forking/template/whitelabel/computation.py
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
13 changes: 13 additions & 0 deletions scripts/forking/template/whitelabel/headers.py
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)
18 changes: 18 additions & 0 deletions scripts/forking/template/whitelabel/opcodes.py
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.
Show resolved Hide resolved
# New opcodes
}

ISTANBUL_OPCODES = merge(
copy.deepcopy(PETERSBURG_OPCODES),
UPDATED_OPCODES,
)
9 changes: 9 additions & 0 deletions scripts/forking/template/whitelabel/state.py
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
42 changes: 42 additions & 0 deletions scripts/forking/template/whitelabel/transactions.py
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.
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,
)