diff --git a/eth/beacon/chains/__init__.py b/eth/beacon/chains/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/eth/beacon/chains/base.py b/eth/beacon/chains/base.py new file mode 100644 index 0000000000..2be24cbb14 --- /dev/null +++ b/eth/beacon/chains/base.py @@ -0,0 +1,360 @@ +from __future__ import absolute_import + +from abc import ( + ABC, + abstractmethod +) +from typing import ( + Tuple, + Type, +) + +import logging + +from eth_typing import ( + Hash32, +) +from eth_utils import ( + ValidationError, +) + +from eth.db.backends.base import BaseAtomicDB +from eth.exceptions import ( + BlockNotFound, +) +from eth.validation import ( + validate_word, +) + +from eth.utils.datatypes import ( + Configurable, +) +from eth.utils.hexadecimal import ( + encode_hex, +) +from eth.utils.rlp import ( + validate_imported_block_unchanged, +) + +from eth.beacon.db.chain import ( # noqa: F401 + BaseBeaconChainDB, + BeaconChainDB, +) +from eth.beacon.exceptions import ( + SMNotFound, +) +from eth.beacon.state_machines.base import BaseBeaconStateMachine # noqa: F401 +from eth.beacon.types.active_states import ActiveState +from eth.beacon.types.blocks import BaseBeaconBlock +from eth.beacon.types.crystallized_states import CrystallizedState +from eth.beacon.validation import ( + validate_slot, +) + + +class BaseBeaconChain(Configurable, ABC): + """ + The base class for all BeaconChain objects + """ + chaindb = None # type: BaseBeaconChainDB + chaindb_class = None # type: Type[BaseBeaconChainDB] + sm_configuration = None # type: Tuple[Tuple[int, Type[BaseBeaconStateMachine]], ...] + chain_id = None # type: int + + # + # Helpers + # + @classmethod + @abstractmethod + def get_chaindb_class(cls) -> Type[BaseBeaconChainDB]: + pass + + @classmethod + def get_sm_configuration(cls) -> Tuple[Tuple[int, Type['BaseBeaconStateMachine']], ...]: + return cls.sm_configuration + + # + # Chain API + # + @classmethod + @abstractmethod + def from_genesis(cls, + base_db: BaseAtomicDB, + genesis_block: BaseBeaconBlock, + genesis_crystallized_state: CrystallizedState, + genesis_active_state: ActiveState) -> 'BaseBeaconChain': + pass + + @classmethod + @abstractmethod + def from_genesis_block(cls, + base_db: BaseAtomicDB, + genesis_block: BaseBeaconBlock) -> 'BaseBeaconChain': + pass + + # + # State Machine API + # + @classmethod + def get_sm_class(cls, block: BaseBeaconBlock) -> Type['BaseBeaconStateMachine']: + """ + Returns the StateMachine instance for the given block slot number. + """ + return cls.get_sm_class_for_block_slot(block.slot_number) + + @abstractmethod + def get_sm(self, block: BaseBeaconBlock) -> 'BaseBeaconStateMachine': + raise NotImplementedError("Chain classes must implement this method") + + @classmethod + def get_sm_class_for_block_slot(cls, slot: int) -> Type['BaseBeaconStateMachine']: + """ + Return the StateMachine class for the given block slot number. + """ + if cls.sm_configuration is None: + raise AttributeError("Chain classes must define the StateMachines in sm_configuration") + + validate_slot(slot) + for start_slot, sm_class in reversed(cls.sm_configuration): + if slot >= start_slot: + return sm_class + raise SMNotFound("No StateMachine available for block slot: #{0}".format(slot)) + + # + # Block API + # + @abstractmethod + def create_block_from_parent(self, parent_block, **block_params): + pass + + @abstractmethod + def get_block_by_hash(self, block_hash: Hash32) -> BaseBeaconBlock: + pass + + @abstractmethod + def get_canonical_head(self) -> BaseBeaconBlock: + pass + + @abstractmethod + def get_score(self, block_hash: Hash32) -> int: + pass + + @abstractmethod + def ensure_block(self, block: BaseBeaconBlock=None) -> BaseBeaconBlock: + pass + + @abstractmethod + def get_block(self) -> BaseBeaconBlock: + pass + + @abstractmethod + def get_canonical_block_by_slot(self, slot: int) -> BaseBeaconBlock: + pass + + @abstractmethod + def get_canonical_block_hash(self, slot: int) -> Hash32: + pass + + @abstractmethod + def import_block( + self, + block: BaseBeaconBlock, + perform_validation: bool=True + ) -> Tuple[BaseBeaconBlock, Tuple[BaseBeaconBlock, ...], Tuple[BaseBeaconBlock, ...]]: + pass + + +class BeaconChain(BaseBeaconChain): + """ + A Chain is a combination of one or more StateMachine classes. Each StateMachine is associated + with a range of blocks. The Chain class acts as a wrapper around these other + StateMachine classes, delegating operations to the appropriate StateMachine depending on the + current block slot number. + """ + logger = logging.getLogger("eth.beacon.chains.chain.BeaconChain") + + chaindb_class = BeaconChainDB # type: Type[BaseBeaconChainDB] + + def __init__(self, base_db: BaseAtomicDB) -> None: + if not self.sm_configuration: + raise ValueError( + "The Chain class cannot be instantiated with an empty `sm_configuration`" + ) + else: + # TODO implment validate_sm_configuration(self.sm_configuration) + # validate_sm_configuration(self.sm_configuration) + pass + + self.chaindb = self.get_chaindb_class()(base_db) + + # + # Helpers + # + @classmethod + def get_chaindb_class(cls) -> Type['BaseBeaconChainDB']: + if cls.chaindb_class is None: + raise AttributeError("`chaindb_class` not set") + return cls.chaindb_class + + # + # Chain API + # + @classmethod + def from_genesis(cls, + base_db: BaseAtomicDB, + genesis_block: BaseBeaconBlock, + genesis_crystallized_state: CrystallizedState, + genesis_active_state: ActiveState) -> 'BaseBeaconChain': + """ + Initialize the Chain from a genesis state. + """ + # mutation + chaindb = cls.get_chaindb_class()(base_db) + chaindb.persist_crystallized_state(genesis_crystallized_state) + chaindb.persist_active_state(genesis_active_state, genesis_crystallized_state.hash) + + return cls.from_genesis_block(base_db, genesis_block) + + @classmethod + def from_genesis_block(cls, + base_db: BaseAtomicDB, + genesis_block: BaseBeaconBlock) -> 'BaseBeaconChain': + """ + Initialize the chain from the genesis block. + """ + chaindb = cls.get_chaindb_class()(base_db) + chaindb.persist_block(genesis_block) + return cls(base_db) + + # + # StateMachine API + # + def get_sm(self, at_block: BaseBeaconBlock=None) -> 'BaseBeaconStateMachine': + """ + Return the StateMachine instance for the given block number. + """ + block = self.ensure_block(at_block) + sm_class = self.get_sm_class_for_block_slot(block.slot_number) + return sm_class(block=block, chaindb=self.chaindb) + + # + # Block API + # + def create_block_from_parent(self, parent_block, **block_params): + """ + Passthrough helper to the StateMachine class of the block descending from the + given block. + """ + + return self.get_sm_class_for_block_slot( + slot=parent_block.slot_number + 1, + ).create_block_from_parent(parent_block, **block_params) + + def get_block_by_hash(self, block_hash: Hash32) -> BaseBeaconBlock: + """ + Return the requested block as specified by block hash. + + Raise BlockNotFound if there's no block with the given hash in the db. + """ + validate_word(block_hash, title="Block Hash") + return self.chaindb.get_block_by_hash(block_hash) + + def get_canonical_head(self) -> BaseBeaconBlock: + """ + Return the block at the canonical chain head. + + Raise CanonicalHeadNotFound if there's no head defined for the canonical chain. + """ + return self.chaindb.get_canonical_head() + + def get_score(self, block_hash: Hash32) -> int: + """ + Return the score of the block with the given hash. + + Raises BlockNotFound if there is no matching black hash. + """ + return self.chaindb.get_score(block_hash) + + def ensure_block(self, block: BaseBeaconBlock=None) -> BaseBeaconBlock: + """ + Return ``block`` if it is not ``None``, otherwise return the block + of the canonical head. + """ + if block is None: + head = self.get_canonical_head() + return self.create_block_from_parent(head) + else: + return block + + def get_block(self) -> BaseBeaconBlock: + """ + Return the current TIP block. + """ + return self.get_sm().block + + def get_canonical_block_by_slot(self, slot: int) -> BaseBeaconBlock: + """ + Return the block with the given number in the canonical chain. + + Raise BlockNotFound if there's no block with the given number in the + canonical chain. + """ + validate_slot(slot) + return self.get_block_by_hash(self.chaindb.get_canonical_block_hash(slot)) + + def get_canonical_block_hash(self, slot: int) -> Hash32: + """ + Return the block hash with the given number in the canonical chain. + + Raise BlockNotFound if there's no block with the given number in the + canonical chain. + """ + return self.chaindb.get_canonical_block_hash(slot) + + def import_block( + self, + block: BaseBeaconBlock, + perform_validation: bool=True + ) -> Tuple[BaseBeaconBlock, Tuple[BaseBeaconBlock, ...], Tuple[BaseBeaconBlock, ...]]: + """ + Import a complete block and returns a 3-tuple + + - the imported block + - a tuple of blocks which are now part of the canonical chain. + - a tuple of blocks which are were canonical and now are no longer canonical. + """ + + try: + parent_block = self.get_block_by_hash(block.parent_hash) + except BlockNotFound: + raise ValidationError( + "Attempt to import block #{}. Cannot import block {} before importing " + "its parent block at {}".format( + block.slot_number, + block.hash, + block.parent_hash, + ) + ) + base_block_for_import = self.create_block_from_parent(parent_block) + imported_block, crystallized_state, active_state = self.get_sm( + base_block_for_import + ).import_block(block) + + # TODO: deal with crystallized_state, active_state + + # Validate the imported block. + if perform_validation: + validate_imported_block_unchanged(imported_block, block) + + ( + new_canonical_blocks, + old_canonical_blocks, + ) = self.chaindb.persist_block(imported_block) + + self.logger.debug( + 'IMPORTED_BLOCK: slot %s | hash %s', + imported_block.slot_number, + encode_hex(imported_block.hash), + ) + + return imported_block, new_canonical_blocks, old_canonical_blocks diff --git a/eth/beacon/db/chain.py b/eth/beacon/db/chain.py index 9bf76581d5..f9806816df 100644 --- a/eth/beacon/db/chain.py +++ b/eth/beacon/db/chain.py @@ -56,12 +56,18 @@ class BaseBeaconChainDB(ABC): db = None # type: BaseAtomicDB + @abstractmethod + def __init__(self, db: BaseAtomicDB) -> None: + pass + # # Block API # @abstractmethod - def persist_block(self, - block: BaseBeaconBlock) -> Tuple[Tuple[bytes, ...], Tuple[bytes, ...]]: + def persist_block( + self, + block: BaseBeaconBlock + ) -> Tuple[Tuple[BaseBeaconBlock, ...], Tuple[BaseBeaconBlock, ...]]: pass @abstractmethod @@ -94,8 +100,8 @@ def block_exists(self, block_hash: Hash32) -> bool: @abstractmethod def persist_block_chain( - self, - blocks: Iterable[BaseBeaconBlock] + self, + blocks: Iterable[BaseBeaconBlock] ) -> Tuple[Tuple[BaseBeaconBlock, ...], Tuple[BaseBeaconBlock, ...]]: pass @@ -148,8 +154,10 @@ class BeaconChainDB(BaseBeaconChainDB): def __init__(self, db: BaseAtomicDB) -> None: self.db = db - def persist_block(self, - block: BaseBeaconBlock) -> Tuple[Tuple[bytes, ...], Tuple[bytes, ...]]: + def persist_block( + self, + block: BaseBeaconBlock + ) -> Tuple[Tuple[BaseBeaconBlock, ...], Tuple[BaseBeaconBlock, ...]]: """ Persist the given block. """ @@ -160,11 +168,11 @@ def persist_block(self, def _persist_block( cls, db: 'BaseDB', - block: BaseBeaconBlock) -> Tuple[Tuple[bytes, ...], Tuple[bytes, ...]]: + block: BaseBeaconBlock + ) -> Tuple[Tuple[BaseBeaconBlock, ...], Tuple[BaseBeaconBlock, ...]]: block_chain = (block, ) - new_canonical_blocks, old_canonical_blocks = cls._persist_block_chain(db, block_chain) - return new_canonical_blocks, old_canonical_blocks + return cls._persist_block_chain(db, block_chain) # # diff --git a/eth/beacon/exceptions.py b/eth/beacon/exceptions.py new file mode 100644 index 0000000000..3f294e3f78 --- /dev/null +++ b/eth/beacon/exceptions.py @@ -0,0 +1,8 @@ +from eth.exceptions import PyEVMError + + +class SMNotFound(PyEVMError): + """ + Raise when no StateMachine is available for the provided block slot number. + """ + pass diff --git a/eth/beacon/state_machines/base.py b/eth/beacon/state_machines/base.py index 5400f89e8a..1df4ce34d4 100644 --- a/eth/beacon/state_machines/base.py +++ b/eth/beacon/state_machines/base.py @@ -1,5 +1,6 @@ from abc import ( ABC, + abstractmethod, ) import logging from typing import ( @@ -68,6 +69,19 @@ class BaseBeaconStateMachine(Configurable, ABC): # TODO: Add abstractmethods + @abstractmethod + def __init__(self, chaindb: BaseBeaconChainDB, block: BaseBeaconBlock=None) -> None: + pass + + # + # Import block API + # + @abstractmethod + def import_block( + self, + block: BaseBeaconBlock) -> Tuple[BaseBeaconBlock, CrystallizedState, ActiveState]: + pass + class BeaconStateMachine(BaseBeaconStateMachine): """ @@ -82,13 +96,13 @@ def __init__(self, chaindb: BaseBeaconChainDB, block: BaseBeaconBlock=None) -> N self.chaindb = chaindb if block is None: # Build a child block of current head - head_block = self.chaindb.get_canonical_head() - self.block = self.get_block_class()(*head_block).copy( - slot_number=head_block.slot_number + 1, - parent_hash=head_block.hash, + block = self.chaindb.get_canonical_head() + self.block = self.get_block_class()(**block.as_dict()).copy( + slot_number=block.slot_number + 1, + parent_hash=block.hash, ) else: - self.block = self.get_block_class()(*block) + self.block = self.get_block_class()(**block.as_dict()) # # Logging diff --git a/eth/beacon/state_machines/forks/serenity/__init__.py b/eth/beacon/state_machines/forks/serenity/__init__.py index e7fc4eba3f..2e05cd9d7f 100644 --- a/eth/beacon/state_machines/forks/serenity/__init__.py +++ b/eth/beacon/state_machines/forks/serenity/__init__.py @@ -10,7 +10,10 @@ from .active_states import SerenityActiveState from .attestation_records import SerenityAttestationRecord -from .blocks import SerenityBeaconBlock +from .blocks import ( + create_serenity_block_from_parent, + SerenityBeaconBlock, +) from .crystallized_states import SerenityCrystallizedState from .configs import SERENITY_CONFIG @@ -25,3 +28,6 @@ class SerenityStateMachine(BeaconStateMachine): active_state_class = SerenityActiveState # type: Type[ActiveState] attestation_record_class = SerenityAttestationRecord # type: Type[AttestationRecord] config = SERENITY_CONFIG + + # methods + create_block_from_parent = staticmethod(create_serenity_block_from_parent) diff --git a/eth/beacon/state_machines/forks/serenity/blocks.py b/eth/beacon/state_machines/forks/serenity/blocks.py index 1a5b776af9..c208712c7d 100644 --- a/eth/beacon/state_machines/forks/serenity/blocks.py +++ b/eth/beacon/state_machines/forks/serenity/blocks.py @@ -1,5 +1,18 @@ -from eth.beacon.types.blocks import BaseBeaconBlock +from typing import ( + Any, +) + +from eth.beacon.types.blocks import ( + BaseBeaconBlock, +) class SerenityBeaconBlock(BaseBeaconBlock): pass + + +def create_serenity_block_from_parent(parent_block: BaseBeaconBlock, + **block_params: Any) -> BaseBeaconBlock: + block = BaseBeaconBlock.from_parent(parent_block=parent_block, **block_params) + + return block diff --git a/eth/beacon/types/blocks.py b/eth/beacon/types/blocks.py index 3eaee398a4..216aa7e4ed 100644 --- a/eth/beacon/types/blocks.py +++ b/eth/beacon/types/blocks.py @@ -1,5 +1,12 @@ +from abc import ( + ABC, +) + from typing import ( Iterable, + Optional, + Union, + overload, ) from eth_typing import ( @@ -10,8 +17,6 @@ CountableList, ) - -from eth.utils.blake import blake from eth.constants import ( ZERO_HASH32, ) @@ -19,6 +24,10 @@ int64, hash32, ) +from eth.utils.blake import blake +from eth.utils.datatypes import ( + Configurable, +) from eth.utils.hexadecimal import ( encode_hex, ) @@ -26,7 +35,14 @@ from .attestation_records import AttestationRecord -class BaseBeaconBlock(rlp.Serializable): +BlockParams = Union[ + Optional[int], + Optional[Iterable[AttestationRecord]], + Optional[Hash32], +] + + +class BaseBeaconBlock(rlp.Serializable, Configurable, ABC): fields = [ # Hash of the parent block ('parent_hash', hash32), @@ -44,23 +60,37 @@ class BaseBeaconBlock(rlp.Serializable): ('crystallized_state_root', hash32), ] - def __init__(self, + @overload + def __init__(self, **kwargs: BlockParams) -> None: + ... + + @overload # noqa: F811 + def __init__(self, # noqa: F811 parent_hash: Hash32, slot_number: int, randao_reveal: Hash32, - attestations: Iterable[AttestationRecord], pow_chain_ref: Hash32, + attestations: Iterable[AttestationRecord]=None, active_state_root: Hash32=ZERO_HASH32, crystallized_state_root: Hash32=ZERO_HASH32) -> None: + ... + + def __init__(self, # noqa: F811 + parent_hash, + slot_number, + randao_reveal, + pow_chain_ref, + attestations=None, + active_state_root=ZERO_HASH32, + crystallized_state_root=ZERO_HASH32): if attestations is None: attestations = [] - super().__init__( parent_hash=parent_hash, slot_number=slot_number, randao_reveal=randao_reveal, - attestations=attestations, pow_chain_ref=pow_chain_ref, + attestations=attestations, active_state_root=active_state_root, crystallized_state_root=crystallized_state_root, ) @@ -82,3 +112,42 @@ def hash(self) -> Hash32: @property def num_attestations(self) -> int: return len(self.attestations) + + @property + def is_genesis(self) -> bool: + return self.parent_hash == ZERO_HASH32 and self.slot_number == 0 + + @classmethod + def from_parent(cls, + parent_block: 'BaseBeaconBlock', + slot_number: int=None, + randao_reveal: Hash32=None, + attestations: Iterable[AttestationRecord]=None, + pow_chain_ref: Hash32=None, + active_state_root: Hash32=ZERO_HASH32, + crystallized_state_root: Hash32=ZERO_HASH32) -> 'BaseBeaconBlock': + """ + Initialize a new block with the `parent` block as the block's + parent hash. + """ + if slot_number is None: + slot_number = parent_block.slot_number + 1 + if randao_reveal is None: + randao_reveal = parent_block.randao_reveal + if attestations is None: + attestations = () + if pow_chain_ref is None: + pow_chain_ref = parent_block.pow_chain_ref + + block_kwargs = { + 'parent_hash': parent_block.hash, + 'slot_number': slot_number, + 'randao_reveal': randao_reveal, + 'attestations': attestations, + 'pow_chain_ref': pow_chain_ref, + 'active_state_root': active_state_root, + 'crystallized_state_root': crystallized_state_root, + } + + block = cls(**block_kwargs) + return block diff --git a/tests/beacon/chains/conftest.py b/tests/beacon/chains/conftest.py new file mode 100644 index 0000000000..83ff63113a --- /dev/null +++ b/tests/beacon/chains/conftest.py @@ -0,0 +1,99 @@ +import pytest + +from eth.beacon.chains.base import ( + BeaconChain, +) + + +def _beacon_chain_with_block_validation( + base_db, + genesis_block, + genesis_crystallized_state, + genesis_active_state, + fixture_sm_class, + chain_cls=BeaconChain): + """ + Return a Chain object containing just the genesis block. + + The Chain's state includes one funded account, which can be found in the + funded_address in the chain itself. + + This Chain will perform all validations when importing new blocks, so only + valid and finalized blocks can be used with it. If you want to test + importing arbitrarily constructe, not finalized blocks, use the + chain_without_block_validation fixture instead. + """ + + klass = chain_cls.configure( + __name__='TestChain', + sm_configuration=( + (0, fixture_sm_class), + ), + chain_id=5566, + ) + chain = klass.from_genesis( + base_db, + genesis_block, + genesis_crystallized_state, + genesis_active_state, + ) + return chain + + +@pytest.fixture +def beacon_chain_with_block_validation(base_db, + genesis_block, + genesis_crystallized_state, + genesis_active_state, + fixture_sm_class): + return _beacon_chain_with_block_validation( + base_db, + genesis_block, + genesis_crystallized_state, + genesis_active_state, + fixture_sm_class, + ) + + +def import_block_without_validation(chain, block): + return super(type(chain), chain).import_block(block, perform_validation=False) + + +@pytest.fixture(params=[BeaconChain]) +def beacon_chain_without_block_validation( + request, + base_db, + genesis_crystallized_state, + genesis_active_state, + genesis_block, + fixture_sm_class): + """ + Return a Chain object containing just the genesis block. + + This Chain does not perform any validation when importing new blocks. + + The Chain's state includes one funded account and a private key for it, + which can be found in the funded_address and private_keys variables in the + chain itself. + """ + # Disable block validation so that we don't need to construct finalized blocks. + overrides = { + 'import_block': import_block_without_validation, + } + chain_class = request.param + klass = chain_class.configure( + __name__='TestChainWithoutBlockValidation', + sm_configuration=( + (0, fixture_sm_class), + ), + chain_id=5566, + **overrides, + ) + + chain = klass.from_genesis( + base_db, + genesis_block, + genesis_crystallized_state, + genesis_active_state, + ) + return chain diff --git a/tests/beacon/chains/test_chain.py b/tests/beacon/chains/test_chain.py new file mode 100644 index 0000000000..599fe0328c --- /dev/null +++ b/tests/beacon/chains/test_chain.py @@ -0,0 +1,73 @@ +import pytest +import rlp + +from eth_utils import decode_hex + +from eth.beacon.state_machines.forks.serenity.blocks import SerenityBeaconBlock + + +@pytest.fixture +def chain(beacon_chain_without_block_validation): + return beacon_chain_without_block_validation + + +@pytest.fixture +def valid_chain(beacon_chain_with_block_validation): + return beacon_chain_with_block_validation + + +# The valid block RLP data under parameter: +# num_validators=1000 +# cycle_length=20 +# min_committee_size=10 +# shard_count=100 +valid_block_rlp = decode_hex( + "0x" + "f8e8a0f6ccfe9efcfdc7915ae1c81bf19a353886bd06a749e3ccba4177d8619c" + "48a27fb840000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000" + "0000000001a00000000000000000000000000000000000000000000000000000" + "000000000000c0a0000000000000000000000000000000000000000000000000" + "0000000000000000a00610b46e7e3ffcb11843b7831e439954edf7a8ee6a1e70" + "c1cea6a86c4808d522a0f8d6d603f8d7757411700348390fcb5ff2ca9f0b6af7" + "89c30bb23a4e8482445a" +) + + +@pytest.mark.parametrize( + ( + 'num_validators,cycle_length,min_committee_size,shard_count' + ), + [ + (1000, 20, 10, 100), + ] +) +def test_import_block_validation(valid_chain): + block = rlp.decode(valid_block_rlp, sedes=SerenityBeaconBlock) + imported_block, _, _ = valid_chain.import_block(block) + assert imported_block == block + + +@pytest.mark.parametrize( + ( + 'num_validators,cycle_length,min_committee_size,shard_count' + ), + [ + (1000, 20, 10, 100), + ] +) +def test_canonical_chain(valid_chain): + genesis_block = valid_chain.chaindb.get_canonical_block_by_slot(0) + + # Our chain fixture is created with only the genesis header, so initially that's the head of + # the canonical chain. + assert valid_chain.get_canonical_head() == genesis_block + + block = rlp.decode(valid_block_rlp, sedes=SerenityBeaconBlock) + valid_chain.chaindb.persist_block(block) + + assert valid_chain.get_canonical_head() == block + + canonical_block_1 = valid_chain.chaindb.get_canonical_block_by_slot( + genesis_block.slot_number + 1) + assert canonical_block_1 == block diff --git a/tests/beacon/chains/test_chain_retrieval_of_sm_class.py b/tests/beacon/chains/test_chain_retrieval_of_sm_class.py new file mode 100644 index 0000000000..43bc969750 --- /dev/null +++ b/tests/beacon/chains/test_chain_retrieval_of_sm_class.py @@ -0,0 +1,56 @@ +# Imitate tests/core/chain-object/test_chain_retrieval_of_vm_class.py + +import pytest + +from eth.beacon.chains.base import ( + BeaconChain, +) +from eth.beacon.exceptions import ( + SMNotFound, +) +from eth.beacon.state_machines.base import ( + BeaconStateMachine, +) + + +class BaseSMForTesting(BeaconStateMachine): + @classmethod + def create_block_from_parent(cls, parent_block, **block_params): + pass + + +class SM_A(BaseSMForTesting): + pass + + +class SM_B(SM_A): + pass + + +class ChainForTesting(BeaconChain): + sm_configuration = ( + (0, SM_A), + (10, SM_B), + ) + + +def test_simple_chain(base_db, genesis_block): + chain = ChainForTesting.from_genesis_block(base_db, genesis_block) + + assert chain.get_sm_class_for_block_slot(0) is SM_A + + for num in range(1, 10): + assert chain.get_sm_class_for_block_slot(num) is SM_A + + assert chain.get_sm_class_for_block_slot(10) is SM_B + + for num in range(11, 100, 5): + assert chain.get_sm_class_for_block_slot(num) is SM_B + + +def test_vm_not_found_if_no_matching_block_number(genesis_block): + chain_class = BeaconChain.configure('ChainStartsAtBlock10', sm_configuration=( + (10, SM_A), + )) + with pytest.raises(SMNotFound): + chain_class.get_sm_class_for_block_slot(9) diff --git a/tests/beacon/conftest.py b/tests/beacon/conftest.py index 73ec9307c9..5b25f2ac7d 100644 --- a/tests/beacon/conftest.py +++ b/tests/beacon/conftest.py @@ -4,12 +4,16 @@ to_tuple, ) +from eth.beacon.db.chain import BeaconChainDB from eth.beacon.genesis_helpers import ( get_genesis_active_state, get_genesis_block, get_genesis_crystallized_state, ) +from eth.beacon.state_machines.configs import BeaconConfig +from eth.beacon.state_machines.forks.serenity import SerenityStateMachine from eth.beacon.state_machines.forks.serenity.configs import SERENITY_CONFIG + from eth.beacon.types.validator_records import ValidatorRecord import eth.utils.bls as bls from eth.utils.blake import blake @@ -253,3 +257,49 @@ def genesis_block(genesis_active_state, genesis_crystallized_state): active_state_root=active_state_root, crystallized_state_root=crystallized_state_root, ) + + +# +# State Machine +# +@pytest.fixture +def config(base_reward_quotient, + default_end_dynasty, + deposit_size, + cycle_length, + min_committee_size, + min_dynasty_length, + shard_count, + slot_duration, + sqrt_e_drop_time): + return BeaconConfig( + BASE_REWARD_QUOTIENT=base_reward_quotient, + DEFAULT_END_DYNASTY=default_end_dynasty, + DEPOSIT_SIZE=deposit_size, + CYCLE_LENGTH=cycle_length, + MIN_COMMITTEE_SIZE=min_committee_size, + MIN_DYNASTY_LENGTH=min_dynasty_length, + SHARD_COUNT=shard_count, + SLOT_DURATION=slot_duration, + SQRT_E_DROP_TIME=sqrt_e_drop_time, + ) + + +@pytest.fixture +def fixture_sm_class(config): + return SerenityStateMachine.configure( + __name__='SerenityStateMachineForTesting', + config=config, + ) + + +@pytest.fixture +def initial_chaindb(base_db, + genesis_block, + genesis_crystallized_state, + genesis_active_state): + chaindb = BeaconChainDB(base_db) + chaindb.persist_block(genesis_block) + chaindb.persist_crystallized_state(genesis_crystallized_state) + chaindb.persist_active_state(genesis_active_state, genesis_crystallized_state.hash) + return chaindb diff --git a/tests/beacon/state_machines/conftest.py b/tests/beacon/state_machines/conftest.py deleted file mode 100644 index 46b5b5cbb2..0000000000 --- a/tests/beacon/state_machines/conftest.py +++ /dev/null @@ -1,50 +0,0 @@ -import pytest - -from eth.beacon.db.chain import BeaconChainDB -from eth.beacon.state_machines.configs import BeaconConfig -from eth.beacon.state_machines.forks.serenity import ( - SerenityStateMachine, -) - - -@pytest.fixture -def config(base_reward_quotient, - default_end_dynasty, - deposit_size, - cycle_length, - min_committee_size, - min_dynasty_length, - shard_count, - slot_duration, - sqrt_e_drop_time): - return BeaconConfig( - BASE_REWARD_QUOTIENT=base_reward_quotient, - DEFAULT_END_DYNASTY=default_end_dynasty, - DEPOSIT_SIZE=deposit_size, - CYCLE_LENGTH=cycle_length, - MIN_COMMITTEE_SIZE=min_committee_size, - MIN_DYNASTY_LENGTH=min_dynasty_length, - SHARD_COUNT=shard_count, - SLOT_DURATION=slot_duration, - SQRT_E_DROP_TIME=sqrt_e_drop_time, - ) - - -@pytest.fixture -def fixture_sm_class(config): - return SerenityStateMachine.configure( - __name__='SerenityStateMachineForTesting', - config=config, - ) - - -@pytest.fixture -def initial_chaindb(base_db, - genesis_block, - genesis_crystallized_state, - genesis_active_state): - chaindb = BeaconChainDB(base_db) - chaindb.persist_block(genesis_block) - chaindb.persist_crystallized_state(genesis_crystallized_state) - chaindb.persist_active_state(genesis_active_state, genesis_crystallized_state.hash) - return chaindb diff --git a/tests/beacon/state_machines/test_state_machines.py b/tests/beacon/state_machines/test_state_machines.py index 8d2d28201c..ccb6e904e8 100644 --- a/tests/beacon/state_machines/test_state_machines.py +++ b/tests/beacon/state_machines/test_state_machines.py @@ -15,10 +15,10 @@ def test_state_machine_canonical(initial_chaindb, genesis_active_state): chaindb = initial_chaindb sm = SerenityStateMachine(chaindb) - assert sm.block == genesis_block.copy( + assert sm.block.hash == genesis_block.copy( slot_number=genesis_block.slot_number + 1, parent_hash=genesis_block.hash - ) + ).hash assert sm.crystallized_state == genesis_crystallized_state assert sm.active_state == genesis_active_state @@ -49,6 +49,8 @@ def test_state_machine(initial_chaindb, slot_number=3, active_state_root=b'\x33' * 32, ) + # canonical head is block_3 + chaindb.persist_block(block_3) sm = SerenityStateMachine(chaindb, block_3)