diff --git a/eth/abc.py b/eth/abc.py index 51699469a2..78cac349ea 100644 --- a/eth/abc.py +++ b/eth/abc.py @@ -43,6 +43,7 @@ JournalDBCheckpoint, AccountState, HeaderParams, + VMConfiguration, ) @@ -1756,6 +1757,31 @@ def get_transaction_context(cls, ... +class ConsensusAPI(ABC): + @abstractmethod + def __init__(self, db: AtomicDatabaseAPI) -> None: + ... + + @abstractmethod + def validate_seal(self, header: BlockHeaderAPI) -> None: + """ + Validate the seal on the given header, even if its parent is missing. + """ + ... + + @abstractmethod + def validate_extension(self, header: BlockHeaderAPI) -> None: + """ + Validate the seal on the given header, after its parent is imported. + """ + ... + + @classmethod + @abstractmethod + def get_fee_recipient(cls, header: BlockHeaderAPI) -> Address: + ... + + class VirtualMachineAPI(ConfigurableAPI): """ The :class:`~eth.abc.VirtualMachineAPI` class represents the Chain rules for a @@ -1772,6 +1798,7 @@ class VirtualMachineAPI(ConfigurableAPI): fork: str # noqa: E701 # flake8 bug that's fixed in 3.6.0+ chaindb: ChainDatabaseAPI extra_data_max_bytes: ClassVar[int] + consensus_class: Type[ConsensusAPI] @abstractmethod def __init__(self, header: BlockHeaderAPI, chaindb: ChainDatabaseAPI) -> None: @@ -2098,9 +2125,8 @@ def validate_block(self, block: BlockAPI) -> None: """ ... - @classmethod @abstractmethod - def validate_header(cls, + def validate_header(self, header: BlockHeaderAPI, parent_header: BlockHeaderAPI, check_seal: bool = True @@ -2124,11 +2150,17 @@ def validate_transaction_against_header(self, """ ... - @classmethod @abstractmethod - def validate_seal(cls, header: BlockHeaderAPI) -> None: + def validate_seal(self, header: BlockHeaderAPI) -> None: + """ + Validate the seal on the given header, even if its parent is missing. + """ + ... + + @abstractmethod + def validate_extension_seal(self, header: BlockHeaderAPI) -> None: """ - Validate the seal on the given header. + Validate the seal on the given header, after its parent is imported. """ ... @@ -2160,6 +2192,21 @@ def state_in_temp_block(self) -> ContextManager[StateAPI]: ... +class VirtualMachineModifierAPI(ABC): + """ + Amend a set of VMs for a chain. This allows modifying a chain for different consensus schemes. + """ + + @abstractmethod + def amend_vm_configuration(self, vm_config: VMConfiguration) -> VMConfiguration: + """ + Make amendments to the ``vm_config`` that are independent of any instance state. These + changes are applied across all instances of the chain where this + ``VirtualMachineModifierAPI`` is applied on. + """ + ... + + class HeaderChainAPI(ABC): header: BlockHeaderAPI chain_id: int @@ -2558,10 +2605,9 @@ def validate_uncles(self, block: BlockAPI) -> None: """ ... - @classmethod @abstractmethod def validate_chain( - cls, + self, root: BlockHeaderAPI, descendants: Tuple[BlockHeaderAPI, ...], seal_check_random_sample_rate: int = 1) -> None: diff --git a/eth/chains/base.py b/eth/chains/base.py index 0809ef3c4d..c09a0e7f72 100644 --- a/eth/chains/base.py +++ b/eth/chains/base.py @@ -133,9 +133,8 @@ def get_vm_class(cls, header: BlockHeaderAPI) -> Type[VirtualMachineAPI]: # # Validation API # - @classmethod def validate_chain( - cls, + self, root: BlockHeaderAPI, descendants: Tuple[BlockHeaderAPI, ...], seal_check_random_sample_rate: int = 1) -> None: @@ -158,14 +157,30 @@ def validate_chain( f" but expected {encode_hex(parent.hash)}" ) should_check_seal = index in indices_to_check_seal - vm_class = cls.get_vm_class_for_block_number(child.block_number) + vm = self.get_vm(child) try: - vm_class.validate_header(child, parent, check_seal=should_check_seal) + vm.validate_header(child, parent, check_seal=should_check_seal) except ValidationError as exc: raise ValidationError( f"{child} is not a valid child of {parent}: {exc}" ) from exc + def validate_extension( + self, + new_header: BlockHeaderAPI, + check_seal: bool = True) -> None: + """ + Run any validations that cannot be executed until the parent header is persisted + to the database. + + This does *not* guarantee to re-run any checks found in :meth:`validate_chain`. + """ + if check_seal: + vm = self.get_vm(new_header) + vm.validate_extension_seal(new_header) + + # non-seal checks can be added here + class Chain(BaseChain): logger = logging.getLogger("eth.chain.chain.Chain") @@ -465,15 +480,16 @@ def validate_receipt(self, receipt: ReceiptAPI, at_header: BlockHeaderAPI) -> No def validate_block(self, block: BlockAPI) -> None: if block.is_genesis: raise ValidationError("Cannot validate genesis block this way") - VM_class = self.get_vm_class_for_block_number(BlockNumber(block.number)) + vm = self.get_vm(block.header) parent_header = self.get_block_header_by_hash(block.header.parent_hash) - VM_class.validate_header(block.header, parent_header, check_seal=True) + vm.validate_header(block.header, parent_header, check_seal=True) + vm.validate_extension_seal(block.header) self.validate_uncles(block) self.validate_gaslimit(block.header) def validate_seal(self, header: BlockHeaderAPI) -> None: - VM_class = self.get_vm_class_for_block_number(BlockNumber(header.block_number)) - VM_class.validate_seal(header) + vm = self.get_vm(header) + vm.validate_seal(header) def validate_gaslimit(self, header: BlockHeaderAPI) -> None: parent_header = self.get_block_header_by_hash(header.parent_hash) diff --git a/eth/chains/mainnet/__init__.py b/eth/chains/mainnet/__init__.py index ad0871502b..9e95b72104 100644 --- a/eth/chains/mainnet/__init__.py +++ b/eth/chains/mainnet/__init__.py @@ -45,8 +45,7 @@ class MainnetDAOValidatorVM(HomesteadVM): """Only on mainnet, TheDAO fork is accompanied by special extra data. Validate those headers""" - @classmethod - def validate_header(cls, + def validate_header(self, header: BlockHeaderAPI, previous_header: BlockHeaderAPI, check_seal: bool=True) -> None: @@ -54,17 +53,17 @@ def validate_header(cls, super().validate_header(header, previous_header, check_seal) # The special extra_data is set on the ten headers starting at the fork - dao_fork_at = cls.get_dao_fork_block_number() + dao_fork_at = self.get_dao_fork_block_number() extra_data_block_nums = range(dao_fork_at, dao_fork_at + 10) if header.block_number in extra_data_block_nums: - if cls.support_dao_fork and header.extra_data != DAO_FORK_MAINNET_EXTRA_DATA: + if self.support_dao_fork and header.extra_data != DAO_FORK_MAINNET_EXTRA_DATA: raise ValidationError( f"Block {header!r} must have extra data " f"{encode_hex(DAO_FORK_MAINNET_EXTRA_DATA)} not " f"{encode_hex(header.extra_data)} when supporting DAO fork" ) - elif not cls.support_dao_fork and header.extra_data == DAO_FORK_MAINNET_EXTRA_DATA: + elif not self.support_dao_fork and header.extra_data == DAO_FORK_MAINNET_EXTRA_DATA: raise ValidationError( f"Block {header!r} must not have extra data " f"{encode_hex(DAO_FORK_MAINNET_EXTRA_DATA)} when declining the DAO fork" diff --git a/eth/chains/tester/__init__.py b/eth/chains/tester/__init__.py index f29be459c6..5740bcf53f 100644 --- a/eth/chains/tester/__init__.py +++ b/eth/chains/tester/__init__.py @@ -146,8 +146,8 @@ class MainnetTesterChain(BaseMainnetTesterChain): It exposes one additional API `configure_forks` to allow for in-flight configuration of fork rules. """ - @classmethod - def validate_seal(cls, block: BlockAPI) -> None: + + def validate_seal(self, block: BlockAPI) -> None: """ We don't validate the proof of work seal on the tester chain. """ diff --git a/eth/consensus/__init__.py b/eth/consensus/__init__.py index e69de29bb2..c05463b215 100644 --- a/eth/consensus/__init__.py +++ b/eth/consensus/__init__.py @@ -0,0 +1,3 @@ +from .clique.clique import CliqueConsensus # noqa: F401 +from .noproof import NoProofConsensus # noqa: F401 +from .pow import PowConsensus # noqa: F401 diff --git a/eth/consensus/applier.py b/eth/consensus/applier.py new file mode 100644 index 0000000000..670127ad48 --- /dev/null +++ b/eth/consensus/applier.py @@ -0,0 +1,37 @@ +from typing import ( + Iterable, + Type, +) + +from eth_utils import ( + to_tuple, +) + +from eth.abc import ( + ConsensusAPI, + VirtualMachineModifierAPI, + VMConfiguration, +) +from eth.typing import ( + VMFork, +) + + +class ConsensusApplier(VirtualMachineModifierAPI): + """ + This class is used to apply simple types of consensus engines to a series of virtual machines. + + Note that this *must not* be used for Clique, which has its own modifier + """ + + def __init__(self, consensus_class: Type[ConsensusAPI]) -> None: + self._consensus_class = consensus_class + + @to_tuple + def amend_vm_configuration(self, config: VMConfiguration) -> Iterable[VMFork]: + """ + Amend the given ``VMConfiguration`` to operate under the rules of the pre-defined consensus + """ + for pair in config: + block_number, vm = pair + yield block_number, vm.configure(consensus_class=self._consensus_class) diff --git a/eth/consensus/clique/__init__.py b/eth/consensus/clique/__init__.py index eeb351b418..8c409d7b65 100644 --- a/eth/consensus/clique/__init__.py +++ b/eth/consensus/clique/__init__.py @@ -1,4 +1,5 @@ from .clique import ( # noqa: F401 + CliqueApplier, CliqueConsensus, ) from .constants import ( # noqa: F401 diff --git a/eth/consensus/clique/clique.py b/eth/consensus/clique/clique.py index 64c3123b6b..b50f957699 100644 --- a/eth/consensus/clique/clique.py +++ b/eth/consensus/clique/clique.py @@ -1,16 +1,19 @@ import logging -from typing import Sequence, Iterable +from typing import ( + Iterable, + Sequence, +) from eth.abc import ( AtomicDatabaseAPI, BlockHeaderAPI, VirtualMachineAPI, + VirtualMachineModifierAPI, ) from eth.db.chain import ChainDB from eth_typing import ( Address, - Hash32, ) from eth_utils import ( encode_hex, @@ -18,15 +21,14 @@ ValidationError, ) +from eth.abc import ( + ConsensusAPI, +) from eth.typing import ( HeaderParams, VMConfiguration, VMFork, ) -from eth.vm.chain_context import ChainContext -from eth.vm.execution_context import ( - ExecutionContext, -) from .constants import ( EPOCH_LENGTH, @@ -65,7 +67,7 @@ def _construct_turn_error_message(expected_difficulty: int, ) -class CliqueConsensus: +class CliqueConsensus(ConsensusAPI): """ This class is the entry point to operate a chain under the rules of Clique consensus which is defined in EIP-225: https://eips.ethereum.org/EIPS/eip-225 @@ -85,43 +87,13 @@ def __init__(self, base_db: AtomicDatabaseAPI, epoch_length: int = EPOCH_LENGTH) self._epoch_length, ) - @to_tuple - def amend_vm_configuration(self, config: VMConfiguration) -> Iterable[VMFork]: - """ - Amend the given ``VMConfiguration`` to operate under the rules of Clique consensus. - """ - for pair in config: - block_number, vm = pair - vm_class = vm.configure( - extra_data_max_bytes=65535, - validate_seal=staticmethod(self.validate_seal), - create_execution_context=staticmethod(self.create_execution_context), - configure_header=configure_header, - _assign_block_rewards=lambda _, __: None, - ) - - yield block_number, vm_class - - @staticmethod - def create_execution_context(header: BlockHeaderAPI, - prev_hashes: Iterable[Hash32], - chain_context: ChainContext) -> ExecutionContext: - + @classmethod + def get_fee_recipient(cls, header: BlockHeaderAPI) -> Address: # In Clique consensus, the tx fee goes to the signer try: - coinbase = get_block_signer(header) + return get_block_signer(header) except ValueError: - coinbase = header.coinbase - - return ExecutionContext( - coinbase=coinbase, - timestamp=header.timestamp, - block_number=header.block_number, - difficulty=header.difficulty, - gas_limit=header.gas_limit, - prev_hashes=prev_hashes, - chain_id=chain_context.chain_id, - ) + return header.coinbase def get_snapshot(self, header: BlockHeaderAPI) -> Snapshot: """ @@ -130,8 +102,13 @@ def get_snapshot(self, header: BlockHeaderAPI) -> Snapshot: return self._snapshot_manager.get_or_create_snapshot(header.block_number, header.hash) def validate_seal(self, header: BlockHeaderAPI) -> None: + validate_header_integrity(header, self._epoch_length) + + def validate_extension(self, header: BlockHeaderAPI) -> None: """ Validate the seal of the given ``header`` according to the Clique consensus rules. + + The caller asserts that the header's parent is in the database. """ if header.block_number == 0: return @@ -165,3 +142,26 @@ def validate_seal(self, header: BlockHeaderAPI) -> None: ) self._header_cache.evict() + + +class CliqueApplier(VirtualMachineModifierAPI): + """ + This class is used to apply a clique consensus engine to a series of virtual machines + """ + + @to_tuple + def amend_vm_configuration(self, config: VMConfiguration) -> Iterable[VMFork]: + """ + Amend the given ``VMConfiguration`` to operate under the rules of Clique consensus. + """ + for pair in config: + block_number, vm = pair + vm_class = vm.configure( + extra_data_max_bytes=65535, + consensus_class=CliqueConsensus, + configure_header=configure_header, + get_block_reward=staticmethod(int), + get_uncle_reward=staticmethod(int), + ) + + yield block_number, vm_class diff --git a/eth/consensus/noproof.py b/eth/consensus/noproof.py new file mode 100644 index 0000000000..a4b9be6b52 --- /dev/null +++ b/eth/consensus/noproof.py @@ -0,0 +1,27 @@ +from eth.abc import ( + AtomicDatabaseAPI, + BlockHeaderAPI, + ConsensusAPI, +) +from eth.typing import ( + Address, +) + + +class NoProofConsensus(ConsensusAPI): + """ + Modify a set of VMs to accept blocks without any validation. + """ + + def __init__(self, base_db: AtomicDatabaseAPI) -> None: + pass + + def validate_seal(self, header: BlockHeaderAPI) -> None: + pass + + def validate_extension(self, header: BlockHeaderAPI) -> None: + pass + + @classmethod + def get_fee_recipient(cls, header: BlockHeaderAPI) -> Address: + return header.coinbase diff --git a/eth/consensus/pow.py b/eth/consensus/pow.py index a2436dd053..23c7993779 100644 --- a/eth/consensus/pow.py +++ b/eth/consensus/pow.py @@ -1,10 +1,11 @@ from collections import OrderedDict from typing import ( - Tuple + Tuple, ) from eth_typing import ( - Hash32 + Address, + Hash32, ) from eth_utils import ( @@ -22,6 +23,11 @@ ) +from eth.abc import ( + AtomicDatabaseAPI, + BlockHeaderAPI, + ConsensusAPI, +) from eth.validation import ( validate_length, validate_lte, @@ -92,3 +98,24 @@ def mine_pow_nonce(block_number: int, mining_hash: Hash32, difficulty: int) -> T return nonce.to_bytes(8, 'big'), mining_output[b'mix digest'] raise Exception("Too many attempts at POW mining, giving up") + + +class PowConsensus(ConsensusAPI): + """ + Modify a set of VMs to validate blocks via Proof of Work (POW) + """ + + def __init__(self, base_db: AtomicDatabaseAPI) -> None: + pass + + def validate_seal(self, header: BlockHeaderAPI) -> None: + check_pow( + header.block_number, header.mining_hash, + header.mix_hash, header.nonce, header.difficulty) + + def validate_extension(self, header: BlockHeaderAPI) -> None: + pass + + @classmethod + def get_fee_recipient(cls, header: BlockHeaderAPI) -> Address: + return header.coinbase diff --git a/eth/tools/builder/chain/builders.py b/eth/tools/builder/chain/builders.py index d4973555ae..cbea3f3d28 100644 --- a/eth/tools/builder/chain/builders.py +++ b/eth/tools/builder/chain/builders.py @@ -33,11 +33,12 @@ from eth.abc import ( AtomicDatabaseAPI, BlockAPI, - BlockHeaderAPI, ChainAPI, MiningChainAPI, VirtualMachineAPI, ) +from eth.consensus.applier import ConsensusApplier +from eth.consensus.noproof import NoProofConsensus from eth.db.atomic import AtomicDB from eth.db.backends.memory import ( MemoryDB, @@ -287,33 +288,6 @@ def enable_pow_mining(chain_class: Type[ChainAPI]) -> Type[ChainAPI]: return chain_class.configure(vm_configuration=vm_configuration) -class NoChainSealValidationMixin: - @classmethod - def validate_seal(cls, block: BlockAPI) -> None: - pass - - -class NoVMSealValidationMixin: - @classmethod - def validate_seal(cls, header: BlockHeaderAPI) -> None: - pass - - -@to_tuple -def _mix_in_disable_seal_validation(vm_configuration: VMConfiguration) -> Iterable[VMFork]: - for fork_block, vm_class in vm_configuration: - if issubclass(vm_class, NoVMSealValidationMixin): - # Seal validation already disabled, hence nothing to change - vm_class_without_seal_validation = vm_class - else: - vm_class_without_seal_validation = type( - vm_class.__name__, - (NoVMSealValidationMixin, vm_class), - {}, - ) - yield fork_block, vm_class_without_seal_validation - - @curry def disable_pow_check(chain_class: Type[ChainAPI]) -> Type[ChainAPI]: """ @@ -325,23 +299,10 @@ def disable_pow_check(chain_class: Type[ChainAPI]) -> Type[ChainAPI]: blocks mined this way will not be importable on any chain that does not have proof of work disabled. """ - if not chain_class.vm_configuration: - raise ValidationError("Chain class has no vm_configuration") + original_vms = chain_class.vm_configuration + no_pow_vms = ConsensusApplier(NoProofConsensus).amend_vm_configuration(original_vms) - if issubclass(chain_class, NoChainSealValidationMixin): - # Seal validation already disabled, hence nothing to change - chain_class_without_seal_validation = chain_class - else: - chain_class_without_seal_validation = type( - chain_class.__name__, - (chain_class, NoChainSealValidationMixin), - {}, - ) - return chain_class_without_seal_validation.configure( # type: ignore - vm_configuration=_mix_in_disable_seal_validation( - chain_class_without_seal_validation.vm_configuration # type: ignore - ), - ) + return chain_class.configure(vm_configuration=no_pow_vms) # diff --git a/eth/vm/base.py b/eth/vm/base.py index 3e4f6625ff..bb4add0abb 100644 --- a/eth/vm/base.py +++ b/eth/vm/base.py @@ -8,13 +8,13 @@ Iterator, Optional, Sequence, + Set, Tuple, Type, Union, ) -from typing import Set - +from cached_property import cached_property from eth_hash.auto import keccak from eth_typing import ( Address, @@ -32,6 +32,7 @@ ChainContextAPI, ChainDatabaseAPI, ComputationAPI, + ConsensusAPI, ExecutionContextAPI, ReceiptAPI, SignedTransactionAPI, @@ -40,7 +41,7 @@ VirtualMachineAPI, ) from eth.consensus.pow import ( - check_pow, + PowConsensus, ) from eth.constants import ( GENESIS_PARENT_HASH, @@ -84,6 +85,7 @@ class VM(Configurable, VirtualMachineAPI): block_class: Type[BlockAPI] = None + consensus_class: Type[ConsensusAPI] = PowConsensus extra_data_max_bytes: ClassVar[int] = 32 fork: str = None # noqa: E701 # flake8 bug that's fixed in 3.6.0+ chaindb: ChainDatabaseAPI = None @@ -133,6 +135,10 @@ def build_state(cls, execution_context = cls.create_execution_context(header, previous_hashes, chain_context) return cls.get_state_class()(db, execution_context, header.state_root) + @cached_property + def _consensus(self) -> ConsensusAPI: + return self.consensus_class(self.chaindb.db) + # # Logging # @@ -154,12 +160,14 @@ def apply_transaction(self, return receipt, computation - @staticmethod - def create_execution_context(header: BlockHeaderAPI, + @classmethod + def create_execution_context(cls, + header: BlockHeaderAPI, prev_hashes: Iterable[Hash32], chain_context: ChainContextAPI) -> ExecutionContextAPI: + fee_recipient = cls.consensus_class.get_fee_recipient(header) return ExecutionContext( - coinbase=header.coinbase, + coinbase=fee_recipient, timestamp=header.timestamp, block_number=header.block_number, difficulty=header.difficulty, @@ -323,21 +331,28 @@ def _assign_block_rewards(self, block: BlockAPI) -> None: len(block.uncles) * self.get_nephew_reward() ) - self.state.delta_balance(block.header.coinbase, block_reward) - self.logger.debug( - "BLOCK REWARD: %s -> %s", - block_reward, - block.header.coinbase, - ) + if block_reward != 0: + self.state.delta_balance(block.header.coinbase, block_reward) + self.logger.debug( + "BLOCK REWARD: %s -> %s", + block_reward, + block.header.coinbase, + ) + else: + self.logger.debug("No block reward given to %s", block.header.coinbase) for uncle in block.uncles: uncle_reward = self.get_uncle_reward(block.number, uncle) - self.state.delta_balance(uncle.coinbase, uncle_reward) - self.logger.debug( - "UNCLE REWARD REWARD: %s -> %s", - uncle_reward, - uncle.coinbase, - ) + + if uncle_reward != 0: + self.state.delta_balance(uncle.coinbase, uncle_reward) + self.logger.debug( + "UNCLE REWARD REWARD: %s -> %s", + uncle_reward, + uncle.coinbase, + ) + else: + self.logger.debug("No uncle reward given to %s", uncle.coinbase) def finalize_block(self, block: BlockAPI) -> BlockAPI: if block.number > 0: @@ -525,8 +540,7 @@ def validate_block(self, block: BlockAPI) -> None: f" - header uncle_hash: {block.header.uncles_hash}" ) - @classmethod - def validate_header(cls, + def validate_header(self, header: BlockHeaderAPI, parent_header: BlockHeaderAPI, check_seal: bool = True) -> None: @@ -535,7 +549,7 @@ def validate_header(cls, raise ValidationError("Must have access to parent header to validate current header") else: validate_length_lte( - header.extra_data, cls.extra_data_max_bytes, title="BlockHeader.extra_data") + header.extra_data, self.extra_data_max_bytes, title="BlockHeader.extra_data") validate_gas_limit(header.gas_limit, parent_header.gas_limit) @@ -557,19 +571,19 @@ def validate_header(cls, if check_seal: try: - cls.validate_seal(header) + self.validate_seal(header) except ValidationError: - cls.cls_logger.warning( + self.cls_logger.warning( "Failed to validate header proof of work on header: %r", header.as_dict() ) raise - @classmethod - def validate_seal(cls, header: BlockHeaderAPI) -> None: - check_pow( - header.block_number, header.mining_hash, - header.mix_hash, header.nonce, header.difficulty) + def validate_seal(self, header: BlockHeaderAPI) -> None: + self._consensus.validate_seal(header) + + def validate_extension_seal(self, header: BlockHeaderAPI) -> None: + self._consensus.validate_extension(header) @classmethod def validate_uncle(cls, block: BlockAPI, uncle: BlockAPI, uncle_parent: BlockAPI) -> None: diff --git a/newsfragments/1874.feature.rst b/newsfragments/1874.feature.rst new file mode 100644 index 0000000000..29477b2dda --- /dev/null +++ b/newsfragments/1874.feature.rst @@ -0,0 +1,7 @@ +Make handling of different consensus mechanisms more flexible and sound. + +Introduce a new property ``consensus_engine_class`` on the ``Chain``. The chain take care +of instantiating the consensus engine and exposes it as ``consensus_engine``. + +Also add ``PowConsensus`` and ``NoProofConsensus`` as engines and refactor ``eth.tools.*`` to +use these. \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 6f07d70a9e..31c9da17a8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,7 @@ Chain, MiningChain, ) +from eth.consensus.noproof import NoProofConsensus from eth.db.atomic import AtomicDB from eth.rlp.headers import BlockHeader from eth.vm.forks import ( @@ -146,7 +147,7 @@ def _chain_with_block_validation(VM, base_db, genesis_state, chain_cls=Chain): klass = chain_cls.configure( __name__='TestChain', vm_configuration=( - (constants.GENESIS_BLOCK_NUMBER, VM), + (constants.GENESIS_BLOCK_NUMBER, VM.configure(consensus_class=NoProofConsensus)), ), chain_id=1337, ) @@ -207,12 +208,11 @@ def _chain_without_block_validation(request, VM, base_db, genesis_state): 'import_block': import_block_without_validation, 'validate_block': lambda self, block: None, } - VMForTesting = VM.configure(validate_seal=lambda block: None) chain_class = request.param klass = chain_class.configure( __name__='TestChainWithoutBlockValidation', vm_configuration=( - (constants.GENESIS_BLOCK_NUMBER, VMForTesting), + (constants.GENESIS_BLOCK_NUMBER, VM.configure(consensus_class=NoProofConsensus)), ), chain_id=1337, **overrides, diff --git a/tests/core/builder-tools/test_chain_builder.py b/tests/core/builder-tools/test_chain_builder.py index 6f1e6e8b4c..7596425853 100644 --- a/tests/core/builder-tools/test_chain_builder.py +++ b/tests/core/builder-tools/test_chain_builder.py @@ -2,11 +2,6 @@ from eth_utils import ValidationError -from eth.chains import ( - MainnetChain, - MainnetTesterChain, - RopstenChain, -) from eth.chains.base import ( Chain, MiningChain, @@ -24,9 +19,6 @@ mine_block, mine_blocks, ) -from eth.tools.builder.chain.builders import ( - NoChainSealValidationMixin, -) MINING_CHAIN_PARAMS = ( @@ -237,18 +229,3 @@ def test_chain_builder_chain_split(mining_chain): head_b = chain_b.get_canonical_head() assert head_b.block_number == 3 - - -@pytest.mark.parametrize( - "chain", - ( - MainnetChain, - MainnetTesterChain, - RopstenChain, - ) -) -def test_disabling_pow_for_already_pow_disabled_chain(chain): - pow_disabled_chain = disable_pow_check(chain) - assert issubclass(pow_disabled_chain, NoChainSealValidationMixin) - again_pow_disabled_chain = disable_pow_check(pow_disabled_chain) - assert issubclass(again_pow_disabled_chain, NoChainSealValidationMixin) diff --git a/tests/core/consensus/test_clique_consensus.py b/tests/core/consensus/test_clique_consensus.py index 758a632388..8ed268b881 100644 --- a/tests/core/consensus/test_clique_consensus.py +++ b/tests/core/consensus/test_clique_consensus.py @@ -14,6 +14,7 @@ GOERLI_GENESIS_HEADER, ) from eth.consensus.clique import ( + CliqueApplier, CliqueConsensus, NONCE_AUTH, NONCE_DROP, @@ -24,6 +25,7 @@ SIGNATURE_LENGTH, ) from eth.consensus.clique._utils import ( + get_block_signer, sign_block_header, ) from eth.constants import ( @@ -179,52 +181,50 @@ def alice_nominates_bob_and_ron_then_they_kick_her(): def validate_seal_and_get_snapshot(clique, header): clique.validate_seal(header) + clique.validate_extension(header) return clique.get_snapshot(header) @pytest.fixture -def clique(paragon_chain_with_clique): - _, clique = paragon_chain_with_clique - return clique - - -@pytest.fixture -def paragon_chain(paragon_chain_with_clique): - chain, _ = paragon_chain_with_clique - return chain - - -@pytest.fixture -def paragon_chain_with_clique(base_db): +def paragon_chain(base_db): vms = ((0, PetersburgVM,),) - - clique = CliqueConsensus(base_db) - - vms = clique.amend_vm_configuration(vms) + clique_vms = CliqueApplier().amend_vm_configuration(vms) chain = MiningChain.configure( - vm_configuration=vms, + vm_configuration=clique_vms, chain_id=5, ).from_genesis(base_db, PARAGON_GENESIS_PARAMS, PARAGON_GENESIS_STATE) - return chain, clique + return chain + +def get_clique(chain, header=None): + if header: + vm = chain.get_vm(header) + else: + vm = chain.get_vm() -def test_can_retrieve_root_snapshot(paragon_chain, clique): + clique = vm._consensus + assert isinstance(clique, CliqueConsensus) + return clique + + +def test_can_retrieve_root_snapshot(paragon_chain): head = paragon_chain.get_canonical_head() - snapshot = clique.get_snapshot(head) + snapshot = get_clique(paragon_chain, head).get_snapshot(head) assert snapshot.get_sorted_signers() == [ALICE] -def test_raises_unknown_ancestor_error(paragon_chain, clique): +def test_raises_unknown_ancestor_error(paragon_chain): head = paragon_chain.get_canonical_head() next_header = make_next_header(head, ALICE_PK, RON, NONCE_AUTH) + clique = get_clique(paragon_chain, head) with pytest.raises(ValidationError, match='Unknown ancestor'): clique.get_snapshot(next_header) -def test_import_block(paragon_chain, clique): +def test_import_block(paragon_chain): vm = paragon_chain.get_vm() tx = new_transaction(vm, ALICE, BOB, 10, ALICE_PK) @@ -247,6 +247,8 @@ def test_import_block(paragon_chain, clique): transactions=[tx] ) + assert get_block_signer(block.header) == ALICE + paragon_chain.import_block(block) # Alice new balance is old balance - 10 + 21000 tx fee (she's the signer) @@ -256,20 +258,22 @@ def test_import_block(paragon_chain, clique): assert paragon_chain.get_vm().state.get_balance(vm.get_block().header.coinbase) == 0 -def test_reapplies_headers_without_snapshots(clique): +def test_reapplies_headers_without_snapshots(paragon_chain): voting_chain = alice_nominates_bob_and_ron_then_they_kick_her() # We save the headers but we do not create intermediate snapshots # to proof that the SnapshotManager re-applies all needed headers # on its own. for i in range(5): - clique._chain_db.persist_header(voting_chain[i]) + paragon_chain.chaindb.persist_header(voting_chain[i]) + clique = get_clique(paragon_chain) snapshot = validate_seal_and_get_snapshot(clique, voting_chain[5]) assert snapshot.signers == {BOB, RON} -def test_can_persist_and_restore_snapshot_from_db(clique): +def test_can_persist_and_restore_snapshot_from_db(paragon_chain): + clique = get_clique(paragon_chain) snapshot = validate_seal_and_get_snapshot(clique, PARAGON_GENESIS_HEADER) clique._snapshot_manager.persist_snapshot(snapshot) @@ -277,8 +281,9 @@ def test_can_persist_and_restore_snapshot_from_db(clique): assert snapshot == revived -def test_revert_previous_nominate(paragon_chain, clique): +def test_revert_previous_nominate(paragon_chain): head = paragon_chain.get_canonical_head() + clique = get_clique(paragon_chain) snapshot = validate_seal_and_get_snapshot(clique, head) assert len(snapshot.tallies) == 0 alice_votes_bob = make_next_header(head, ALICE_PK, coinbase=BOB, nonce=NONCE_AUTH) @@ -298,8 +303,9 @@ def test_revert_previous_nominate(paragon_chain, clique): assert RON not in snapshot.tallies -def test_revert_previous_kick(paragon_chain, clique): +def test_revert_previous_kick(paragon_chain): head = paragon_chain.get_canonical_head() + clique = get_clique(paragon_chain) snapshot = validate_seal_and_get_snapshot(clique, head) assert len(snapshot.tallies) == 0 alice_votes_bob = make_next_header(head, ALICE_PK, coinbase=BOB, nonce=NONCE_AUTH) @@ -320,8 +326,9 @@ def test_revert_previous_kick(paragon_chain, clique): assert BOB not in snapshot.tallies -def test_does_not_count_multiple_kicks(paragon_chain, clique): +def test_does_not_count_multiple_kicks(paragon_chain): head = paragon_chain.get_canonical_head() + clique = get_clique(paragon_chain) snapshot = validate_seal_and_get_snapshot(clique, head) assert len(snapshot.tallies) == 0 alice_votes_bob = make_next_header(head, ALICE_PK, coinbase=BOB, nonce=NONCE_AUTH) @@ -340,8 +347,9 @@ def test_does_not_count_multiple_kicks(paragon_chain, clique): assert snapshot.tallies[BOB].votes == 1 -def test_does_not_count_multiple_nominates(paragon_chain, clique): +def test_does_not_count_multiple_nominates(paragon_chain): head = paragon_chain.get_canonical_head() + clique = get_clique(paragon_chain) snapshot = validate_seal_and_get_snapshot(clique, head) assert len(snapshot.tallies) == 0 alice_votes_bob = make_next_header(head, ALICE_PK, coinbase=BOB, nonce=NONCE_AUTH) @@ -360,7 +368,8 @@ def test_does_not_count_multiple_nominates(paragon_chain, clique): assert snapshot.tallies[RON].votes == 1 -def test_alice_votes_in_bob_and_ron_then_gets_kicked(clique): +def test_alice_votes_in_bob_and_ron_then_gets_kicked(paragon_chain): + clique = get_clique(paragon_chain) voting_chain = alice_nominates_bob_and_ron_then_they_kick_her() @@ -389,7 +398,8 @@ def test_alice_votes_in_bob_and_ron_then_gets_kicked(clique): assert ALICE not in snapshot.tallies -def test_removes_all_pending_votes_after_nomination(clique): +def test_removes_all_pending_votes_after_nomination(paragon_chain): + clique = get_clique(paragon_chain) voting_chain = alice_nominates_bob_and_ron_then_they_kick_her() @@ -414,7 +424,8 @@ def test_removes_all_pending_votes_after_nomination(clique): assert not has_vote_from(ALICE, snapshot.votes) -def test_removes_all_pending_votes_after_kick(clique): +def test_removes_all_pending_votes_after_kick(paragon_chain): + clique = get_clique(paragon_chain) ALICE_FRIEND = PublicKeyFactory().to_canonical_address() diff --git a/tests/core/consensus/test_consensus_engine.py b/tests/core/consensus/test_consensus_engine.py new file mode 100644 index 0000000000..5408e93827 --- /dev/null +++ b/tests/core/consensus/test_consensus_engine.py @@ -0,0 +1,84 @@ +import pytest + +from eth.abc import ConsensusAPI +from eth.chains.base import MiningChain +from eth.tools.builder.chain import ( + genesis, +) +from eth.vm.forks.istanbul import IstanbulVM + +from eth_utils import ( + ValidationError, +) + + +CONSENSUS_DATA_LENGH = 9 + +WHITELISTED_ROOT = b"root" + +ZERO_BYTE = b'\x00' + + +class WhitelistConsensus(ConsensusAPI): + """ + A pseudo consensus engine for testing. Each accepted block puts another block on a whitelist. + """ + + def __init__(self, base_db) -> None: + self.base_db = base_db + + def _get_consensus_data(self, consensus_data): + if len(consensus_data) != CONSENSUS_DATA_LENGH: + raise ValidationError( + f"The `extra_data` field must be of length {CONSENSUS_DATA_LENGH}" + f"but was {len(consensus_data)}" + ) + + return consensus_data[:4], consensus_data[5:] + + def validate_seal(self, header): + pass + + def validate_extension(self, header): + current, following = self._get_consensus_data(header.extra_data) + + if current == WHITELISTED_ROOT or current in self.base_db: + self.base_db[following] = ZERO_BYTE + else: + raise ValidationError(f"Block isn't on whitelist: {current}") + + @classmethod + def get_fee_recipient(cls, header): + return header.coinbase + + +def test_stateful_consensus_isnt_shared_across_chain_instances(): + + class ChainClass(MiningChain): + vm_configuration = ( + (0, IstanbulVM.configure(consensus_class=WhitelistConsensus)), + ) + + chain = genesis(ChainClass) + + chain.mine_block(extra_data=b"root-1000") + chain.mine_block(extra_data=b"1000-1001") + # we could even mine the same block twice + chain.mine_block(extra_data=b"1000-1001") + + # But we can not jump ahead + with pytest.raises(ValidationError, match="Block isn't on whitelist"): + chain.mine_block(extra_data=b"1002-1003") + + # A different chain but same consensus engine class + second_chain = genesis(ChainClass) + + # Should maintain its independent whitelist + with pytest.raises(ValidationError, match="Block isn't on whitelist"): + second_chain.mine_block(extra_data=b"1000-1001") + + second_chain.mine_block(extra_data=b"root-2000") + + # And the second chain's whitelist should also not interfere with the first one's + with pytest.raises(ValidationError, match="Block isn't on whitelist"): + chain.mine_block(extra_data=b"2000-2001") diff --git a/tests/core/vm/test_clique_validation.py b/tests/core/vm/test_clique_validation.py index e7c52c5f39..4df128a4be 100644 --- a/tests/core/vm/test_clique_validation.py +++ b/tests/core/vm/test_clique_validation.py @@ -11,6 +11,7 @@ from eth.rlp.headers import BlockHeader +from eth.vm.chain_context import ChainContext from eth.vm.forks.petersburg import ( PetersburgVM, ) @@ -51,6 +52,8 @@ def clique(base_db): def test_can_validate_header(clique, VM, header, previous_header, valid): CliqueVM = VM.configure( extra_data_max_bytes=128, - validate_seal=lambda header: clique.validate_seal(header), + validate_seal=clique.validate_seal, ) - CliqueVM.validate_header(header, previous_header, check_seal=True) + chain_context = ChainContext(5) + vm = CliqueVM(header=previous_header, chaindb=clique._chain_db, chain_context=chain_context) + vm.validate_header(header, previous_header, check_seal=True) diff --git a/tests/core/vm/test_mainnet_dao_fork.py b/tests/core/vm/test_mainnet_dao_fork.py index 335b05aeb9..2743eced04 100644 --- a/tests/core/vm/test_mainnet_dao_fork.py +++ b/tests/core/vm/test_mainnet_dao_fork.py @@ -11,6 +11,7 @@ MainnetHomesteadVM, ) from eth.rlp.headers import BlockHeader +from eth.vm.chain_context import ChainContext class ETC_VM(MainnetHomesteadVM): @@ -267,6 +268,11 @@ def header_pairs(VM, headers, valid): yield VM, pair[1], pair[0], valid +class FakeChainDB: + def __init__(self, db): + self.db = db + + @pytest.mark.parametrize( 'VM, header, previous_header, valid', header_pairs(MainnetHomesteadVM, ETH_HEADERS_NEAR_FORK, valid=True) + ( @@ -280,11 +286,12 @@ def header_pairs(VM, headers, valid): ), ) def test_mainnet_dao_fork_header_validation(VM, header, previous_header, valid): + vm = VM(header=previous_header, chaindb=FakeChainDB({}), chain_context=ChainContext(1)) if valid: - VM.validate_header(header, previous_header, check_seal=True) + vm.validate_header(header, previous_header, check_seal=True) else: try: - VM.validate_header(header, previous_header, check_seal=True) + vm.validate_header(header, previous_header, check_seal=True) except ValidationError: pass else: