Skip to content

Commit

Permalink
Adjust how MainnetTesterChain configures VMs to not require updates f…
Browse files Browse the repository at this point in the history
…or every new fork rule (ethereum#521)
  • Loading branch information
pipermerriam committed Apr 2, 2018
1 parent a6cc035 commit 271d392
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 112 deletions.
212 changes: 114 additions & 98 deletions evm/chains/tester/__init__.py
@@ -1,31 +1,37 @@
import collections
import operator
from typing import (
Any
Any,
Generator,
Sequence,
Tuple,
Type,
Union,
)

from cytoolz import (
assoc,
last,
)

from eth_utils import (
reversed_return,
to_tuple,
)

from evm.chains.base import Chain

from evm.vm.forks import (
FrontierVM as BaseFrontierVM,
HomesteadVM as BaseHomesteadVM,
TangerineWhistleVM as BaseTangerineWhistleVM,
SpuriousDragonVM as BaseSpuriousDragonVM,
from evm.chains.mainnet import MainnetChain
from evm.exceptions import ValidationError
from evm.rlp.headers import (
BlockHeader
)

from evm.utils.chain import (
generate_vms_by_range,
)

from evm.rlp.headers import (
BlockHeader
from evm.validation import (
validate_gte,
)
from evm.vm.base import BaseVM
from evm.vm.forks.homestead import HomesteadVM


class MaintainGasLimitMixin(object):
Expand All @@ -42,91 +48,92 @@ def create_header_from_parent(cls,
**assoc(header_params, 'gas_limit', parent_header.gas_limit)
)

# Suppressing invalid base class error for several classes.
# Check evm.datatypes.Configurable for a longer explanation


class FrontierTesterVM(MaintainGasLimitMixin, BaseFrontierVM): # type: ignore
pass


class BaseHomesteadTesterVM(MaintainGasLimitMixin, BaseHomesteadVM): # type: ignore
pass


class TangerineWhistleTesterVM(MaintainGasLimitMixin, BaseTangerineWhistleVM): # type: ignore
pass


class SpuriousDragonTesterVM(MaintainGasLimitMixin, BaseSpuriousDragonVM): # type: ignore
pass


INVALID_FORK_ACTIVATION_MSG = (
"The {0}-fork activation block may not be null if the {1}-fork block "
"is non null"
MAINNET_VMS = collections.OrderedDict(
(vm.fork, type(vm.__name__, (MaintainGasLimitMixin, vm), {}))
for _, vm
in MainnetChain.vms_by_range.items()
)


@reversed_return
def _generate_vm_configuration(homestead_start_block=None,
dao_start_block=None,
tangerine_whistle_start_block=None,
spurious_dragon_block=None):
# If no explicit configuration has been passed, configure the vm to start
# with the latest fork rules at block 0
no_declared_blocks = (
spurious_dragon_block is None and
tangerine_whistle_start_block is None and
homestead_start_block is None
ForkStartBlocks = Sequence[Tuple[int, Union[str, Type[BaseVM]]]]
VMStartBlock = Tuple[int, Type[BaseVM]]


@to_tuple
def _generate_vm_configuration(*fork_start_blocks: ForkStartBlocks,
dao_start_block: Union[int, bool]=None) -> Generator[VMStartBlock, None, None]: # noqa: E501
"""
fork_start_blocks should be 2-tuples of (start_block, fork_name_or_vm_class)
dao_start_block determines whether the Homestead fork will support the DAO
fork and if so, at what block.
- dao_start_block = None: perform the DAO fork at the same block as the
Homestead start block.
- dao_start_block = False: do not perform the DAO fork.
- dao_start_block = <int>: perform the DAO fork at the given block number.
"""
# if no configuration was passed in, initialize the chain with the *latest*
# Mainnet VM rules active at block 0.
if not fork_start_blocks:
yield (0, last(MAINNET_VMS.values()))
return

# Validate that there are no fork names which are not represented in the
# mainnet chain.
fork_names = set(
fork_name for
_, fork_name
in fork_start_blocks
if isinstance(fork_name, str)
)
if no_declared_blocks:
yield (0, SpuriousDragonTesterVM)

if spurious_dragon_block is not None:
yield (spurious_dragon_block, SpuriousDragonTesterVM)
unknown_forks = sorted(fork_names.difference(
MAINNET_VMS.keys()
))
if unknown_forks:
raise ValidationError("Configuration contains unknown forks: {0}".format(unknown_forks))

# Validate that *if* an explicit value was passed in for dao_start_block
# that the Homestead fork rules are part of the VM configuration.
if dao_start_block is not None and 'homestead' not in fork_names:
raise ValidationError(
"The `dao_start_block` parameter is only valid for the 'homestead' "
"fork rules. The 'homestead' VM was not included in the provided "
"fork configuration"
)

remaining_blocks_not_declared = (
homestead_start_block is None and
tangerine_whistle_start_block is None
# Validate that there is at least one start block for block 0
start_blocks = set(start_block for start_block, _ in fork_start_blocks)
if 0 not in start_blocks:
raise ValidationError(
"At least one VM must start at block 0. Got start blocks: "
"{0}".format(sorted(start_blocks))
)
if spurious_dragon_block > 0 and remaining_blocks_not_declared:
yield (0, TangerineWhistleTesterVM)

if tangerine_whistle_start_block is not None:
yield (tangerine_whistle_start_block, TangerineWhistleTesterVM)

# If the EIP150 rules do not start at block 0 and homestead_start_block has not
# been configured for a specific block, configure homestead_start_block to start at
# block 0.
if tangerine_whistle_start_block > 0 and homestead_start_block is None:
HomesteadTesterVM = BaseHomesteadTesterVM.configure(
dao_fork_block_number=0,
)
yield (0, HomesteadTesterVM)

if homestead_start_block is not None:
if dao_start_block is False:
# If dao_start_block support has explicitely been configured as `False` then
# mark the HomesteadTesterVM as not supporting the fork.
HomesteadTesterVM = BaseHomesteadTesterVM.configure(support_dao_fork=False)
elif dao_start_block is not None:
# Otherwise, if a specific dao_start_block fork block has been set, use it.
HomesteadTesterVM = BaseHomesteadTesterVM.configure(
dao_fork_block_number=dao_start_block,
)
else:
# Otherwise, default to the homestead_start_block block as the
# start of the dao_start_block fork.
HomesteadTesterVM = BaseHomesteadTesterVM.configure(
dao_fork_block_number=homestead_start_block,
)
yield (homestead_start_block, HomesteadTesterVM)

# If the homestead_start_block block is configured to start after block 0, set the
# frontier rules to start at block 0.
if homestead_start_block > 0:
yield (0, FrontierTesterVM)
ordered_fork_start_blocks = sorted(fork_start_blocks, key=operator.itemgetter(0))

# Iterate over the parameters, generating a tuple of 2-tuples in the form:
# (start_block, vm_class)
for start_block, fork in ordered_fork_start_blocks:
if isinstance(fork, type) and issubclass(fork, BaseVM):
vm_class = fork
elif isinstance(fork, str):
vm_class = MAINNET_VMS[fork]
else:
raise Exception("Invariant: unreachable code path")

if issubclass(vm_class, HomesteadVM):
if dao_start_block is False:
yield (start_block, vm_class.configure(support_dao_fork=False))
elif dao_start_block is None:
yield (start_block, vm_class.configure(dao_fork_block_number=start_block))
elif isinstance(dao_start_block, int):
validate_gte(dao_start_block, start_block)
yield (start_block, vm_class.configure(dao_fork_block_number=dao_start_block))
else:
raise Exception("Invariant: unreachable code path")
else:
yield (start_block, vm_class)


BaseMainnetTesterChain = Chain.configure(
Expand All @@ -136,24 +143,33 @@ def _generate_vm_configuration(homestead_start_block=None,


class MainnetTesterChain(BaseMainnetTesterChain): # type: ignore
"""
This class is intended to be used for in-memory test chains. It
explicitely bypasses the proof of work validation to allow for instant
block mining.
It exposes one additional API `configure_forks` to allow for in-flight
configuration of fork rules.
"""
def validate_seal(self, block):
"""
We don't validate the proof of work seal on the tester chain.
"""
pass

def configure_forks(self,
homestead_start_block=None,
dao_start_block=None,
tangerine_whistle_start_block=None,
spurious_dragon_block=None):
*fork_start_blocks: ForkStartBlocks,
dao_start_block: Union[int, bool]=None) -> None:
"""
TODO: add support for state_cleanup
On demand configuration of fork rules. This is a foot gun that if used
incorrectly could cause weird VM errors.
It should generally only be used on a genesis chain (head block == 0).
Modifying the fork rules, especially if the modification effects
existing blocks could result in a broken chain.
"""
vm_configuration = _generate_vm_configuration(
homestead_start_block=homestead_start_block,
*fork_start_blocks,
dao_start_block=dao_start_block,
tangerine_whistle_start_block=tangerine_whistle_start_block,
spurious_dragon_block=spurious_dragon_block,
)
self.vms_by_range = generate_vms_by_range(vm_configuration)
1 change: 1 addition & 0 deletions evm/vm/base.py
Expand Up @@ -51,6 +51,7 @@ class BaseVM(Configurable, metaclass=ABCMeta):
the individual VM classes for each fork of the protocol rules within that
network.
"""
fork = None
chaindb = None
_state_class = None

Expand Down
3 changes: 3 additions & 0 deletions evm/vm/forks/byzantium/__init__.py
Expand Up @@ -9,7 +9,10 @@


ByzantiumVM = SpuriousDragonVM.configure(
# class name
__name__='ByzantiumVM',
# fork name
fork='byzantium',
# classes
_state_class=ByzantiumState,
# Methods
Expand Down
3 changes: 3 additions & 0 deletions evm/vm/forks/frontier/__init__.py
Expand Up @@ -11,7 +11,10 @@


FrontierVM = VM.configure(
# class name
__name__='FrontierVM',
# fork name
fork='frontier',
# classes
_state_class=FrontierState,
# helpers
Expand Down
3 changes: 3 additions & 0 deletions evm/vm/forks/homestead/__init__.py
Expand Up @@ -17,7 +17,10 @@ class MetaHomesteadVM(FrontierVM): # type: ignore


HomesteadVM = MetaHomesteadVM.configure(
# class name
__name__='HomesteadVM',
# fork name
fork='homestead',
# classes
_state_class=HomesteadState,
# method overrides
Expand Down
3 changes: 3 additions & 0 deletions evm/vm/forks/spurious_dragon/__init__.py
Expand Up @@ -3,7 +3,10 @@
from .state import SpuriousDragonState

SpuriousDragonVM = HomesteadVM.configure(
# class name
__name__='SpuriousDragonVM',
# fork name
fork='spurious-dragon',
# classes
_state_class=SpuriousDragonState,
)
4 changes: 4 additions & 0 deletions evm/vm/forks/tangerine_whistle/__init__.py
Expand Up @@ -3,6 +3,10 @@
from .state import TangerineWhistleState

TangerineWhistleVM = HomesteadVM.configure(
# class name
__name__='TangerineWhistleVM',
# fork name
fork='tangerine-whistle',
# classes
_state_class=TangerineWhistleState,
)
1 change: 1 addition & 0 deletions pytest.ini
@@ -1,3 +1,4 @@
[pytest]
addopts= --showlocals
python_paths= .
xfail_strict=true

0 comments on commit 271d392

Please sign in to comment.