diff --git a/Makefile b/Makefile index 2d8ab4d4fb..465b57c865 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ clean-pyc: find . -name '__pycache__' -exec rm -rf {} + lint: - tox -epy3{6,5}-lint + tox -epy38-lint test: py.test --tb native tests diff --git a/docs/cookbook/index.rst b/docs/cookbook/index.rst index 987bf09503..c9571aba2a 100644 --- a/docs/cookbook/index.rst +++ b/docs/cookbook/index.rst @@ -79,16 +79,7 @@ state. ... } >>> GENESIS_PARAMS = { - ... 'parent_hash': constants.GENESIS_PARENT_HASH, - ... 'uncles_hash': constants.EMPTY_UNCLE_HASH, - ... 'coinbase': constants.ZERO_ADDRESS, - ... 'transaction_root': constants.BLANK_ROOT_HASH, - ... 'receipt_root': constants.BLANK_ROOT_HASH, ... 'difficulty': constants.GENESIS_DIFFICULTY, - ... 'block_number': constants.GENESIS_BLOCK_NUMBER, - ... 'gas_limit': constants.GENESIS_GAS_LIMIT, - ... 'extra_data': constants.GENESIS_EXTRA_DATA, - ... 'nonce': constants.GENESIS_NONCE ... } >>> chain = MainnetChain.from_genesis(AtomicDB(), GENESIS_PARAMS, GENESIS_STATE) diff --git a/docs/guides/building_an_app_that_uses_pyevm.rst b/docs/guides/building_an_app_that_uses_pyevm.rst index c36d6f77b0..dc58de8d63 100644 --- a/docs/guides/building_an_app_that_uses_pyevm.rst +++ b/docs/guides/building_an_app_that_uses_pyevm.rst @@ -83,16 +83,7 @@ Next, we'll create a new directory ``app`` and create a file ``main.py`` inside. >>> DEFAULT_INITIAL_BALANCE = to_wei(10000, 'ether') >>> GENESIS_PARAMS = { - ... 'parent_hash': constants.GENESIS_PARENT_HASH, - ... 'uncles_hash': constants.EMPTY_UNCLE_HASH, - ... 'coinbase': constants.ZERO_ADDRESS, - ... 'transaction_root': constants.BLANK_ROOT_HASH, - ... 'receipt_root': constants.BLANK_ROOT_HASH, ... 'difficulty': constants.GENESIS_DIFFICULTY, - ... 'block_number': constants.GENESIS_BLOCK_NUMBER, - ... 'gas_limit': constants.GENESIS_GAS_LIMIT, - ... 'extra_data': constants.GENESIS_EXTRA_DATA, - ... 'nonce': constants.GENESIS_NONCE ... } >>> GENESIS_STATE = { diff --git a/docs/guides/understanding_the_mining_process.rst b/docs/guides/understanding_the_mining_process.rst index 5e49e5304e..5e8f7b884a 100644 --- a/docs/guides/understanding_the_mining_process.rst +++ b/docs/guides/understanding_the_mining_process.rst @@ -146,17 +146,9 @@ Let's start off by defining the ``GENESIS_PARAMS``. from eth import constants GENESIS_PARAMS = { - 'parent_hash': constants.GENESIS_PARENT_HASH, - 'uncles_hash': constants.EMPTY_UNCLE_HASH, - 'coinbase': constants.ZERO_ADDRESS, - 'transaction_root': constants.BLANK_ROOT_HASH, - 'receipt_root': constants.BLANK_ROOT_HASH, 'difficulty': 1, - 'block_number': constants.GENESIS_BLOCK_NUMBER, 'gas_limit': 3141592, 'timestamp': 1514764800, - 'extra_data': constants.GENESIS_EXTRA_DATA, - 'nonce': constants.GENESIS_NONCE } Next, we'll create the chain itself using the defined ``GENESIS_PARAMS`` and the latest @@ -327,17 +319,11 @@ zero value transfer transaction. >>> GENESIS_PARAMS = { - ... 'parent_hash': constants.GENESIS_PARENT_HASH, - ... 'uncles_hash': constants.EMPTY_UNCLE_HASH, - ... 'coinbase': constants.ZERO_ADDRESS, - ... 'transaction_root': constants.BLANK_ROOT_HASH, - ... 'receipt_root': constants.BLANK_ROOT_HASH, ... 'difficulty': 1, - ... 'block_number': constants.GENESIS_BLOCK_NUMBER, ... 'gas_limit': 3141592, + ... # We set the timestamp, just to make this documented example reproducible. + ... # In common usage, we remove the field to let py-evm choose a reasonable default. ... 'timestamp': 1514764800, - ... 'extra_data': constants.GENESIS_EXTRA_DATA, - ... 'nonce': constants.GENESIS_NONCE ... } >>> SENDER_PRIVATE_KEY = keys.PrivateKey( @@ -355,6 +341,7 @@ zero value transfer transaction. ... )) >>> chain = klass.from_genesis(AtomicDB(), GENESIS_PARAMS) + >>> genesis = chain.get_canonical_block_header_by_number(0) >>> vm = chain.get_vm() >>> nonce = vm.state.get_nonce(SENDER) @@ -372,6 +359,12 @@ zero value transfer transaction. >>> chain.apply_transaction(signed_tx) (>> # Normally, we can let the timestamp be chosen automatically, but + >>> # for the sake of reproducing exactly the same block every time, + >>> # we will set it manually here: + >>> chain.set_header_timestamp(genesis.timestamp + 1) + >>> # We have to finalize the block first in order to be able read the >>> # attributes that are important for the PoW algorithm >>> block_result = chain.get_vm().finalize_block(chain.get_block()) diff --git a/eth/_utils/headers.py b/eth/_utils/headers.py index 27cef1a5c3..90712d232b 100644 --- a/eth/_utils/headers.py +++ b/eth/_utils/headers.py @@ -1,35 +1,116 @@ -import time -from typing import Callable, Tuple, Optional +import datetime +from typing import ( + Dict, + Tuple, +) from eth_typing import ( - Address + Address, ) from eth.abc import BlockHeaderAPI from eth.constants import ( - GENESIS_GAS_LIMIT, + BLANK_ROOT_HASH, + GENESIS_BLOCK_NUMBER, + GENESIS_PARENT_HASH, GAS_LIMIT_EMA_DENOMINATOR, GAS_LIMIT_ADJUSTMENT_FACTOR, + GAS_LIMIT_MAXIMUM, GAS_LIMIT_MINIMUM, GAS_LIMIT_USAGE_ADJUSTMENT_NUMERATOR, GAS_LIMIT_USAGE_ADJUSTMENT_DENOMINATOR, + ZERO_ADDRESS, ) -from eth.rlp.headers import ( - BlockHeader, +from eth.typing import ( + BlockNumber, + HeaderParams, ) -def compute_gas_limit_bounds(parent: BlockHeaderAPI) -> Tuple[int, int]: +def eth_now() -> int: + """ + The timestamp is in UTC. + """ + return int(datetime.datetime.utcnow().timestamp()) + + +def new_timestamp_from_parent(parent: BlockHeaderAPI) -> int: + """ + Generate a timestamp to use on a new header. + + Generally, attempt to use the current time. If timestamp is too old (equal + or less than parent), return `parent.timestamp + 1`. If parent is None, + then consider this a genesis block. + """ + if parent is None: + return eth_now() + else: + # header timestamps must increment + return max( + parent.timestamp + 1, + eth_now(), + ) + + +def fill_header_params_from_parent( + parent: BlockHeaderAPI, + gas_limit: int, + difficulty: int, + timestamp: int, + coinbase: Address = ZERO_ADDRESS, + nonce: bytes = None, + extra_data: bytes = None, + transaction_root: bytes = None, + state_root: bytes = None, + mix_hash: bytes = None, + receipt_root: bytes = None) -> Dict[str, HeaderParams]: + + if parent is None: + parent_hash = GENESIS_PARENT_HASH + block_number = GENESIS_BLOCK_NUMBER + if state_root is None: + state_root = BLANK_ROOT_HASH + else: + parent_hash = parent.hash + block_number = BlockNumber(parent.block_number + 1) + + if state_root is None: + state_root = parent.state_root + + header_kwargs: Dict[str, HeaderParams] = { + 'parent_hash': parent_hash, + 'coinbase': coinbase, + 'state_root': state_root, + 'gas_limit': gas_limit, + 'difficulty': difficulty, + 'block_number': block_number, + 'timestamp': timestamp, + } + if nonce is not None: + header_kwargs['nonce'] = nonce + if extra_data is not None: + header_kwargs['extra_data'] = extra_data + if transaction_root is not None: + header_kwargs['transaction_root'] = transaction_root + if receipt_root is not None: + header_kwargs['receipt_root'] = receipt_root + if mix_hash is not None: + header_kwargs['mix_hash'] = mix_hash + + return header_kwargs + + +def compute_gas_limit_bounds(previous_limit: int) -> Tuple[int, int]: """ Compute the boundaries for the block gas limit based on the parent block. """ - boundary_range = parent.gas_limit // GAS_LIMIT_ADJUSTMENT_FACTOR - upper_bound = parent.gas_limit + boundary_range - lower_bound = max(GAS_LIMIT_MINIMUM, parent.gas_limit - boundary_range) + boundary_range = previous_limit // GAS_LIMIT_ADJUSTMENT_FACTOR + upper_bound = min(GAS_LIMIT_MAXIMUM, previous_limit + boundary_range) + lower_bound = max(GAS_LIMIT_MINIMUM, previous_limit - boundary_range) return lower_bound, upper_bound -def compute_gas_limit(parent_header: BlockHeaderAPI, gas_limit_floor: int) -> int: +def compute_gas_limit(parent_header: BlockHeaderAPI, genesis_gas_limit: int) -> int: """ A simple strategy for adjusting the gas limit. @@ -38,7 +119,7 @@ def compute_gas_limit(parent_header: BlockHeaderAPI, gas_limit_floor: int) -> in - decrease by 1/1024th of the gas limit from the previous block - increase by 50% of the total gas used by the previous block - If the value is less than the given `gas_limit_floor`: + If the value is less than the given `genesis_gas_limit`: - increase the gas limit by 1/1024th of the gas limit from the previous block. @@ -46,13 +127,16 @@ def compute_gas_limit(parent_header: BlockHeaderAPI, gas_limit_floor: int) -> in - use the GAS_LIMIT_MINIMUM as the new gas limit. """ - if gas_limit_floor < GAS_LIMIT_MINIMUM: + if genesis_gas_limit < GAS_LIMIT_MINIMUM: raise ValueError( - "The `gas_limit_floor` value must be greater than the " - f"GAS_LIMIT_MINIMUM. Got {gas_limit_floor}. Must be greater than " + "The `genesis_gas_limit` value must be greater than the " + f"GAS_LIMIT_MINIMUM. Got {genesis_gas_limit}. Must be greater than " f"{GAS_LIMIT_MINIMUM}" ) + if parent_header is None: + return genesis_gas_limit + decay = parent_header.gas_limit // GAS_LIMIT_EMA_DENOMINATOR if parent_header.gas_used: @@ -73,40 +157,7 @@ def compute_gas_limit(parent_header: BlockHeaderAPI, gas_limit_floor: int) -> in if gas_limit < GAS_LIMIT_MINIMUM: return GAS_LIMIT_MINIMUM - elif gas_limit < gas_limit_floor: + elif gas_limit < genesis_gas_limit: return parent_header.gas_limit + decay else: return gas_limit - - -def generate_header_from_parent_header( - compute_difficulty_fn: Callable[[BlockHeaderAPI, int], int], - parent_header: BlockHeaderAPI, - coinbase: Address, - timestamp: Optional[int] = None, - extra_data: bytes = b'') -> BlockHeader: - """ - Generate BlockHeader from state_root and parent_header - """ - if timestamp is None: - timestamp = max(int(time.time()), parent_header.timestamp + 1) - elif timestamp <= parent_header.timestamp: - raise ValueError( - f"header.timestamp ({timestamp}) should be higher than" - f"parent_header.timestamp ({parent_header.timestamp})" - ) - header = BlockHeader( - difficulty=compute_difficulty_fn(parent_header, timestamp), - block_number=(parent_header.block_number + 1), - gas_limit=compute_gas_limit( - parent_header, - gas_limit_floor=GENESIS_GAS_LIMIT, - ), - timestamp=timestamp, - parent_hash=parent_header.hash, - state_root=parent_header.state_root, - coinbase=coinbase, - extra_data=extra_data, - ) - - return header diff --git a/eth/abc.py b/eth/abc.py index b7ebada2bb..6fb84af167 100644 --- a/eth/abc.py +++ b/eth/abc.py @@ -122,8 +122,47 @@ def as_dict(self) -> Dict[Hashable, Any]: """ ... + @property + @abstractmethod + def base_fee_per_gas(self) -> Optional[int]: + """ + Return the base fee per gas of the block. + + Set to None in pre-EIP-1559 (London) header. + """ + ... + + +class BlockHeaderSedesAPI(ABC): + """ + Serialize and deserialize RLP for a header. + + The header may be one of several definitions, like a London (EIP-1559) or + pre-London header. + """ + + @classmethod + @abstractmethod + def deserialize(cls, encoded: List[bytes]) -> 'BlockHeaderAPI': + """ + Extract a header from an encoded RLP object. + + This method is used by rlp.decode(..., sedes=TransactionBuilderAPI). + """ + ... + + @classmethod + @abstractmethod + def serialize(cls, obj: 'BlockHeaderAPI') -> List[bytes]: + """ + Encode a header to a series of bytes used by RLP. + + This method is used by rlp.encode(obj). + """ + ... + -class BlockHeaderAPI(MiningHeaderAPI): +class BlockHeaderAPI(MiningHeaderAPI, BlockHeaderSedesAPI): """ A class derived from :class:`~eth.abc.MiningHeaderAPI` to define a block header after it is sealed. @@ -334,6 +373,25 @@ def nonce(self) -> int: @property @abstractmethod def gas_price(self) -> int: + """ + Will raise :class:`AttributeError` if get or set on a 1559 transaction. + """ + ... + + @property + @abstractmethod + def max_fee_per_gas(self) -> int: + """ + Will default to gas_price if this is a pre-1559 transaction. + """ + ... + + @property + @abstractmethod + def max_priority_fee_per_gas(self) -> int: + """ + Will default to gas_price if this is a pre-1559 transaction. + """ ... @property @@ -1684,6 +1742,14 @@ def chain_id(self) -> int: """ ... + @property + @abstractmethod + def base_fee_per_gas(self) -> Optional[int]: + """ + Return the base fee per gas of the block + """ + ... + class ComputationAPI(ContextManager['ComputationAPI'], StackManipulationAPI): """ @@ -2604,6 +2670,27 @@ def gas_limit(self) -> int: """ ... + @abstractmethod + def get_gas_price(self, transaction: SignedTransactionAPI) -> int: + """ + Return the gas price of the given transaction. + + Factor in the current block's base gase price, if appropriate. (See EIP-1559) + """ + ... + + @abstractmethod + def get_tip(self, transaction: SignedTransactionAPI) -> int: + """ + Return the gas price that gets allocated to the miner/validator. + + Pre-EIP-1559 that would be the full transaction gas price. After, it + would be the tip price (potentially reduced, if the base fee is so high + that it surpasses the transaction's maximum gas price after adding the + tip). + """ + ... + # # Access to account db # @@ -2917,9 +3004,8 @@ def validate_transaction(self, transaction: SignedTransactionAPI) -> None: """ ... - @classmethod @abstractmethod - def get_transaction_context(cls, + def get_transaction_context(self, transaction: SignedTransactionAPI) -> TransactionContextAPI: """ Return the :class:`~eth.abc.TransactionContextAPI` for the given ``transaction`` @@ -3271,6 +3357,17 @@ def generate_block_from_parent_header_and_coinbase(cls, """ ... + @classmethod + @abstractmethod + def create_genesis_header(cls, **genesis_params: Any) -> BlockHeaderAPI: + """ + Create a genesis header using this VM's rules. + + This is equivalent to calling :meth:`create_header_from_parent` + with ``parent_header`` set to None. + """ + ... + @classmethod @abstractmethod def get_block_class(cls) -> Type[BlockAPI]: @@ -3456,10 +3553,10 @@ def get_state_class(cls) -> Type[StateAPI]: ... @abstractmethod - def state_in_temp_block(self) -> ContextManager[StateAPI]: + def in_costless_state(self) -> ContextManager[StateAPI]: """ Return a :class:`~typing.ContextManager` with the current state wrapped in a temporary - block. + block. In this state, the ability to pay gas costs is ignored. """ ... @@ -3922,13 +4019,6 @@ def validate_seal(self, header: BlockHeaderAPI) -> None: """ ... - @abstractmethod - def validate_gaslimit(self, header: BlockHeaderAPI) -> None: - """ - Validate the gas limit on the given ``header``. - """ - ... - @abstractmethod def validate_uncles(self, block: BlockAPI) -> None: """ @@ -3976,6 +4066,16 @@ def __init__(self, base_db: AtomicDatabaseAPI, header: BlockHeaderAPI = None) -> """ ... + @abstractmethod + def set_header_timestamp(self, timestamp: int) -> None: + """ + Set the timestamp of the pending header to mine. + + This is mostly useful for testing, as the timestamp will be chosen + automatically if this method is not called. + """ + ... + @abstractmethod def mine_all( self, diff --git a/eth/chains/base.py b/eth/chains/base.py index f8cf668e76..095cf3f3a9 100644 --- a/eth/chains/base.py +++ b/eth/chains/base.py @@ -31,9 +31,6 @@ from eth._utils.datatypes import ( Configurable, ) -from eth._utils.headers import ( - compute_gas_limit_bounds, -) from eth._utils.rlp import ( validate_imported_block_unchanged, ) @@ -249,7 +246,7 @@ def from_genesis(cls, f"Expected {genesis_params['state_root']!r}" ) - genesis_header = BlockHeader(**genesis_params) + genesis_header = genesis_vm_class.create_genesis_header(**genesis_params) return cls.from_genesis_header(base_db, genesis_header) @classmethod @@ -442,7 +439,7 @@ def get_transaction_result( transaction: SignedTransactionAPI, at_header: BlockHeaderAPI) -> bytes: - with self.get_vm(at_header).state_in_temp_block() as state: + with self.get_vm(at_header).in_costless_state() as state: computation = state.costless_execute_transaction(transaction) computation.raise_if_error() @@ -454,7 +451,7 @@ def estimate_gas( at_header: BlockHeaderAPI = None) -> int: if at_header is None: at_header = self.get_canonical_head() - with self.get_vm(at_header).state_in_temp_block() as state: + with self.get_vm(at_header).in_costless_state() as state: return self.gas_estimator(state, transaction) def import_block(self, @@ -541,28 +538,11 @@ def validate_block(self, block: BlockAPI) -> None: vm.validate_seal(block.header) vm.validate_seal_extension(block.header, ()) self.validate_uncles(block) - self.validate_gaslimit(block.header) def validate_seal(self, header: BlockHeaderAPI) -> None: 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) - low_bound, high_bound = compute_gas_limit_bounds(parent_header) - if header.gas_limit < low_bound: - raise ValidationError( - f"The gas limit on block {encode_hex(header.hash)} " - f"is too low: {header.gas_limit}. " - f"It must be at least {low_bound}" - ) - elif header.gas_limit > high_bound: - raise ValidationError( - f"The gas limit on block {encode_hex(header.hash)} " - f"is too high: {header.gas_limit}. " - f"It must be at most {high_bound}" - ) - def validate_uncles(self, block: BlockAPI) -> None: has_uncles = len(block.uncles) > 0 should_have_uncles = block.header.uncles_hash != EMPTY_UNCLE_HASH @@ -680,6 +660,15 @@ def import_block(self, self.header = self.ensure_header() return result + def set_header_timestamp(self, timestamp: int) -> None: + self.header = self.header.copy(timestamp=timestamp) + + @staticmethod + def _custom_header(base_header: BlockHeaderAPI, **kwargs: Any) -> BlockHeaderAPI: + header_fields = {'coinbase'} + header_params = {k: v for k, v in kwargs.items() if k in header_fields} + return base_header.copy(**header_params) + def mine_all( self, transactions: Sequence[SignedTransactionAPI], @@ -693,7 +682,8 @@ def mine_all( else: base_header = self.create_header_from_parent(parent_header) - vm = self.get_vm(base_header) + custom_header = self._custom_header(base_header, **kwargs) + vm = self.get_vm(custom_header) new_header, receipts, computations = vm.apply_all_transactions(transactions, base_header) filled_block = vm.set_block_transactions(vm.get_block(), new_header, transactions, receipts) @@ -714,7 +704,8 @@ def mine_block(self, *args: Any, **kwargs: Any) -> BlockAPI: return self.mine_block_extended(*args, **kwargs).block def mine_block_extended(self, *args: Any, **kwargs: Any) -> BlockAndMetaWitness: - vm = self.get_vm(self.header) + custom_header = self._custom_header(self.header, **kwargs) + vm = self.get_vm(custom_header) current_block = vm.get_block() mine_result = vm.mine_block(current_block, *args, **kwargs) mined_block = mine_result.block diff --git a/eth/chains/mainnet/__init__.py b/eth/chains/mainnet/__init__.py index 9f64b29cd7..9cd9ac8040 100644 --- a/eth/chains/mainnet/__init__.py +++ b/eth/chains/mainnet/__init__.py @@ -12,6 +12,7 @@ from .constants import ( MAINNET_CHAIN_ID, + LONDON_MAINNET_BLOCK, BERLIN_MAINNET_BLOCK, BYZANTIUM_MAINNET_BLOCK, PETERSBURG_MAINNET_BLOCK, @@ -39,6 +40,7 @@ FrontierVM, HomesteadVM, IstanbulVM, + LondonVM, MuirGlacierVM, PetersburgVM, SpuriousDragonVM, @@ -97,6 +99,7 @@ class MainnetHomesteadVM(MainnetDAOValidatorVM): ISTANBUL_MAINNET_BLOCK, MUIR_GLACIER_MAINNET_BLOCK, BERLIN_MAINNET_BLOCK, + LONDON_MAINNET_BLOCK, ) MAINNET_VMS = ( FrontierVM, @@ -108,6 +111,7 @@ class MainnetHomesteadVM(MainnetDAOValidatorVM): IstanbulVM, MuirGlacierVM, BerlinVM, + LondonVM, ) MAINNET_VM_CONFIGURATION = tuple(zip(MAINNET_FORK_BLOCKS, MAINNET_VMS)) diff --git a/eth/chains/mainnet/constants.py b/eth/chains/mainnet/constants.py index 1e07c07d51..ecfc6d8265 100644 --- a/eth/chains/mainnet/constants.py +++ b/eth/chains/mainnet/constants.py @@ -57,3 +57,8 @@ # Berlin Block # BERLIN_MAINNET_BLOCK = BlockNumber(12244000) + +# +# London Block +# +LONDON_MAINNET_BLOCK = BlockNumber(12965000) diff --git a/eth/consensus/clique/_utils.py b/eth/consensus/clique/_utils.py index 7d9bc0f81c..4473401187 100644 --- a/eth/consensus/clique/_utils.py +++ b/eth/consensus/clique/_utils.py @@ -1,4 +1,3 @@ -import time from typing import ( Iterable, ) @@ -15,6 +14,7 @@ ValidationError, ) +from eth._utils.headers import eth_now from eth.abc import ( BlockHeaderAPI, ) @@ -127,7 +127,7 @@ def is_checkpoint(block_number: int, epoch_length: int) -> bool: def validate_header_integrity(header: BlockHeaderAPI, epoch_length: int) -> None: - if header.timestamp > int(time.time()): + if header.timestamp > eth_now(): raise ValidationError(f"Invalid future timestamp: {header.timestamp}") at_checkpoint = is_checkpoint(header.block_number, epoch_length) diff --git a/eth/constants.py b/eth/constants.py index da66d29480..f3f4af224c 100644 --- a/eth/constants.py +++ b/eth/constants.py @@ -161,6 +161,9 @@ GENESIS_NONCE = b'\x00\x00\x00\x00\x00\x00\x00B' # 0x42 encoded as big-endian-integer GENESIS_MIX_HASH = ZERO_HASH32 GENESIS_EXTRA_DATA = b'' +GENESIS_BLOOM = 0 +GENESIS_GAS_USED = 0 + # # Sha3 Keccak # diff --git a/eth/db/chain.py b/eth/db/chain.py index e275927913..22b4529a84 100644 --- a/eth/db/chain.py +++ b/eth/db/chain.py @@ -7,6 +7,7 @@ Sequence, Tuple, Type, + cast, ) from eth_typing import ( @@ -53,14 +54,12 @@ ) from eth.db.header import HeaderDB from eth.db.schema import SchemaV1 -from eth.rlp.headers import ( - BlockHeader, -) from eth.rlp.sedes import chain_gaps from eth.typing import ChainGaps from eth.validation import ( validate_word, ) +from eth.vm.header import HeaderSedes from eth._warnings import catch_and_ignore_import_warning with catch_and_ignore_import_warning(): import rlp @@ -165,7 +164,7 @@ def get_block_uncles(self, uncles_hash: Hash32) -> Tuple[BlockHeaderAPI, ...]: f"No uncles found for hash {uncles_hash!r}" ) from exc else: - return tuple(rlp.decode(encoded_uncles, sedes=rlp.sedes.CountableList(BlockHeader))) + return tuple(rlp.decode(encoded_uncles, sedes=rlp.sedes.CountableList(HeaderSedes))) @classmethod def _decanonicalize_old_headers( @@ -275,16 +274,21 @@ def _persist_block( cls._update_chain_gaps(db, block) return new_canonical_hashes, old_canonical_hashes - def persist_uncles(self, uncles: Tuple[BlockHeaderAPI]) -> Hash32: + def persist_uncles( + self, + uncles: Tuple[BlockHeaderAPI]) -> Hash32: return self._persist_uncles(self.db, uncles) @staticmethod - def _persist_uncles(db: DatabaseAPI, uncles: Tuple[BlockHeaderAPI, ...]) -> Hash32: + def _persist_uncles( + db: DatabaseAPI, + uncles: Tuple[BlockHeaderAPI, ...]) -> Hash32: + uncles_hash = keccak(rlp.encode(uncles)) db.set( uncles_hash, - rlp.encode(uncles, sedes=rlp.sedes.CountableList(BlockHeader))) - return uncles_hash + rlp.encode(uncles, sedes=rlp.sedes.CountableList(HeaderSedes))) + return cast(Hash32, uncles_hash) # # Transaction API @@ -328,7 +332,7 @@ def _get_block_transaction_hashes( block_header.transaction_root, ) for encoded_transaction in all_encoded_transactions: - yield keccak(encoded_transaction) + yield cast(Hash32, keccak(encoded_transaction)) @to_tuple def get_receipts(self, diff --git a/eth/db/hash_trie.py b/eth/db/hash_trie.py index 3b6606f93f..7b94c79530 100644 --- a/eth/db/hash_trie.py +++ b/eth/db/hash_trie.py @@ -13,7 +13,7 @@ class HashTrie(KeyMapDB): - keymap = keccak + keymap = keccak # type: ignore # mypy doesn't like that keccak accepts bytearray @contextlib.contextmanager def squash_changes(self) -> Iterator['HashTrie']: diff --git a/eth/db/header.py b/eth/db/header.py index 696796a43d..4f4d697d9f 100644 --- a/eth/db/header.py +++ b/eth/db/header.py @@ -49,13 +49,13 @@ ParentNotFound, ) from eth.db.schema import SchemaV1 -from eth.rlp.headers import BlockHeader from eth.rlp.sedes import chain_gaps from eth.typing import ChainGaps from eth.validation import ( validate_block_number, validate_word, ) +from eth.vm.header import HeaderSedes class HeaderDB(HeaderDatabaseAPI): @@ -619,4 +619,9 @@ def _add_block_number_to_hash_lookup(db: DatabaseAPI, header: BlockHeaderAPI) -> # be looking up recent blocks. @functools.lru_cache(128) def _decode_block_header(header_rlp: bytes) -> BlockHeaderAPI: - return rlp.decode(header_rlp, sedes=BlockHeader) + # Use a deserialization class that can handle any type of header. + # This feels a little hack-y, but we don't know the shape of the header + # at this point. It could be a pre-London header, or a post-London + # header, which includes the base fee. So we use a class that knows how to + # decode both. + return rlp.decode(header_rlp, sedes=HeaderSedes) diff --git a/eth/rlp/headers.py b/eth/rlp/headers.py index d50570a1a0..869956cf97 100644 --- a/eth/rlp/headers.py +++ b/eth/rlp/headers.py @@ -1,6 +1,5 @@ -import time from typing import ( - Dict, + cast, overload, ) @@ -23,6 +22,9 @@ encode_hex, ) +from eth._utils.headers import ( + new_timestamp_from_parent, +) from eth.abc import ( BlockHeaderAPI, MiningHeaderAPI, @@ -122,7 +124,12 @@ def __init__(self, # type: ignore # noqa: F811 mix_hash: Hash32 = ZERO_HASH32, nonce: bytes = GENESIS_NONCE) -> None: if timestamp is None: - timestamp = int(time.time()) + if parent_hash == ZERO_HASH32: + timestamp = new_timestamp_from_parent(None) + else: + # without access to the parent header, we cannot select a new timestamp correctly + raise ValueError("Must set timestamp explicitly if this is not a genesis header") + super().__init__( parent_hash=parent_hash, uncles_hash=uncles_hash, @@ -150,55 +157,24 @@ def __str__(self) -> str: def hash(self) -> Hash32: if self._hash is None: self._hash = keccak(rlp.encode(self)) - return self._hash + return cast(Hash32, self._hash) @property def mining_hash(self) -> Hash32: - return keccak(rlp.encode(self[:-2], MiningHeader)) + result = keccak(rlp.encode(self[:-2], MiningHeader)) + return cast(Hash32, result) @property def hex_hash(self) -> str: return encode_hex(self.hash) - @classmethod - def from_parent(cls, - parent: BlockHeaderAPI, - gas_limit: int, - difficulty: int, - timestamp: int, - coinbase: Address = ZERO_ADDRESS, - nonce: bytes = None, - extra_data: bytes = None, - transaction_root: bytes = None, - receipt_root: bytes = None) -> 'BlockHeader': - """ - Initialize a new block header with the `parent` header as the block's - parent hash. - """ - header_kwargs: Dict[str, HeaderParams] = { - 'parent_hash': parent.hash, - 'coinbase': coinbase, - 'state_root': parent.state_root, - 'gas_limit': gas_limit, - 'difficulty': difficulty, - 'block_number': parent.block_number + 1, - 'timestamp': timestamp, - } - if nonce is not None: - header_kwargs['nonce'] = nonce - if extra_data is not None: - header_kwargs['extra_data'] = extra_data - if transaction_root is not None: - header_kwargs['transaction_root'] = transaction_root - if receipt_root is not None: - header_kwargs['receipt_root'] = receipt_root - - header = cls(**header_kwargs) - return header - @property def is_genesis(self) -> bool: # if removing the block_number == 0 test, consider the validation consequences. # validate_header stops trying to check the current header against a parent header. # Can someone trick us into following a high difficulty header with genesis parent hash? return self.parent_hash == GENESIS_PARENT_HASH and self.block_number == 0 + + @property + def base_fee_per_gas(self) -> int: + raise AttributeError("Base fee per gas not available until London fork") diff --git a/eth/rlp/transactions.py b/eth/rlp/transactions.py index 89245c60e0..7781a7813a 100644 --- a/eth/rlp/transactions.py +++ b/eth/rlp/transactions.py @@ -2,6 +2,7 @@ Optional, Sequence, Tuple, + cast, ) from cached_property import cached_property @@ -73,7 +74,7 @@ class BaseTransactionFields(rlp.Serializable, TransactionFieldsAPI): @property def hash(self) -> Hash32: - return keccak(rlp.encode(self)) + return cast(Hash32, keccak(rlp.encode(self))) class SignedTransactionMethods(BaseTransactionMethods, SignedTransactionAPI): diff --git a/eth/tools/_utils/hashing.py b/eth/tools/_utils/hashing.py index 35b72b1875..1dcacdb20b 100644 --- a/eth/tools/_utils/hashing.py +++ b/eth/tools/_utils/hashing.py @@ -1,15 +1,14 @@ -from eth_hash.auto import keccak - -import rlp - from typing import ( Iterable, Tuple, + cast, ) +from eth_hash.auto import keccak from eth_typing import ( Hash32, ) +import rlp from eth.rlp.logs import Log @@ -22,4 +21,4 @@ def hash_log_entries(log_entries: Iterable[Tuple[bytes, Tuple[int, ...], bytes]] logs = [Log(*entry) for entry in log_entries] encoded_logs = rlp.encode(logs) logs_hash = keccak(encoded_logs) - return logs_hash + return cast(Hash32, logs_hash) diff --git a/eth/tools/_utils/normalization.py b/eth/tools/_utils/normalization.py index 2bcd5facd5..3ab82c4bc4 100644 --- a/eth/tools/_utils/normalization.py +++ b/eth/tools/_utils/normalization.py @@ -84,7 +84,8 @@ def normalize_int(value: IntConvertible) -> int: return cast(int, value) elif is_bytes(value): return big_endian_to_int(cast(bytes, value)) - elif is_hex(value) and is_0x_prefixed(value): + elif is_hex(value) and is_0x_prefixed(value): # type: ignore + # mypy doesn't recognize that is_hex() forces value to be a str value = cast(str, value) if len(value) == 2: return 0 diff --git a/eth/tools/builder/chain/__init__.py b/eth/tools/builder/chain/__init__.py index d220befd66..3bead9ac6b 100644 --- a/eth/tools/builder/chain/__init__.py +++ b/eth/tools/builder/chain/__init__.py @@ -27,6 +27,7 @@ istanbul_at, muir_glacier_at, berlin_at, + london_at, latest_mainnet_at, ) diff --git a/eth/tools/builder/chain/builders.py b/eth/tools/builder/chain/builders.py index 34c2d6bc50..1b878af6e7 100644 --- a/eth/tools/builder/chain/builders.py +++ b/eth/tools/builder/chain/builders.py @@ -73,6 +73,7 @@ IstanbulVM, MuirGlacierVM, BerlinVM, + LondonVM, ) @@ -237,23 +238,25 @@ def dao_fork_at(dao_fork_block_number: BlockNumber, istanbul_at = fork_at(IstanbulVM) muir_glacier_at = fork_at(MuirGlacierVM) berlin_at = fork_at(BerlinVM) +london_at = fork_at(LondonVM) -latest_mainnet_at = muir_glacier_at +latest_mainnet_at = london_at GENESIS_DEFAULTS = cast( Tuple[Tuple[str, Union[BlockNumber, int, None, bytes, Address, Hash32]], ...], + # values that will automatically be default are commented out ( ('difficulty', 1), ('extra_data', constants.GENESIS_EXTRA_DATA), ('gas_limit', constants.GENESIS_GAS_LIMIT), - ('gas_used', 0), - ('bloom', 0), + # ('gas_used', 0), + # ('bloom', 0), ('mix_hash', constants.ZERO_HASH32), ('nonce', constants.GENESIS_NONCE), - ('block_number', constants.GENESIS_BLOCK_NUMBER), - ('parent_hash', constants.GENESIS_PARENT_HASH), + # ('block_number', constants.GENESIS_BLOCK_NUMBER), + # ('parent_hash', constants.GENESIS_PARENT_HASH), ('receipt_root', constants.BLANK_ROOT_HASH), - ('uncles_hash', constants.EMPTY_UNCLE_HASH), + # ('uncles_hash', constants.EMPTY_UNCLE_HASH), ('state_root', constants.BLANK_ROOT_HASH), ('transaction_root', constants.BLANK_ROOT_HASH), ) diff --git a/eth/tools/factories/transaction.py b/eth/tools/factories/transaction.py index 274c99cd36..86651f6962 100644 --- a/eth/tools/factories/transaction.py +++ b/eth/tools/factories/transaction.py @@ -12,7 +12,7 @@ def new_transaction( to, amount=0, private_key=None, - gas_price=10, + gas_price=10**10, # 10 gwei, to easily cover the initial London fee of 1 gwei gas=100000, data=b'', nonce=None, @@ -49,7 +49,7 @@ def new_access_list_transaction( to, private_key, amount=0, - gas_price=10, + gas_price=10**10, gas=100000, data=b'', nonce=None, @@ -77,3 +77,42 @@ def new_access_list_transaction( ) return tx.as_signed_transaction(private_key) + + +@curry +def new_dynamic_fee_transaction( + vm, + from_, + to, + private_key, + amount=0, + max_priority_fee_per_gas=1, + max_fee_per_gas=10**10, + gas=100000, + data=b'', + nonce=None, + chain_id=1, + access_list=None): + """ + Create and return a transaction sending amount from to . + + The transaction will be signed with the given private key. + """ + if nonce is None: + nonce = vm.state.get_nonce(from_) + if access_list is None: + access_list = [] + + tx = vm.get_transaction_builder().new_unsigned_dynamic_fee_transaction( + chain_id=chain_id, + nonce=nonce, + max_priority_fee_per_gas=max_priority_fee_per_gas, + max_fee_per_gas=max_fee_per_gas, + gas=gas, + to=to, + value=amount, + data=data, + access_list=access_list, + ) + + return tx.as_signed_transaction(private_key) diff --git a/eth/tools/fixtures/__init__.py b/eth/tools/fixtures/__init__.py index 30832a91ac..89fb588371 100644 --- a/eth/tools/fixtures/__init__.py +++ b/eth/tools/fixtures/__init__.py @@ -8,6 +8,7 @@ ) from .helpers import ( # noqa: F401 new_chain_from_fixture, + genesis_fields_from_fixture, genesis_params_from_fixture, apply_fixture_block_to_chain, setup_state, diff --git a/eth/tools/fixtures/generation.py b/eth/tools/fixtures/generation.py index 36011fb8a1..a5b10e10a4 100644 --- a/eth/tools/fixtures/generation.py +++ b/eth/tools/fixtures/generation.py @@ -25,7 +25,11 @@ def idfn(fixture_params: Iterable[Any]) -> str: """ Function for pytest to produce uniform names for fixtures. """ - return ":".join(str(item) for item in fixture_params) + try: + return ":".join(str(item) for item in fixture_params) + except TypeError: + # In case params are not iterable for some reason... + return str(fixture_params) def get_fixtures_file_hash(all_fixture_paths: Iterable[str]) -> str: diff --git a/eth/tools/fixtures/helpers.py b/eth/tools/fixtures/helpers.py index ff13cf5d3c..0af67b53f1 100644 --- a/eth/tools/fixtures/helpers.py +++ b/eth/tools/fixtures/helpers.py @@ -24,6 +24,7 @@ StateAPI, VirtualMachineAPI, ) +from eth import constants from eth.db.atomic import AtomicDB from eth.chains.mainnet import ( MainnetDAOValidatorVM, @@ -165,7 +166,11 @@ def chain_vm_configuration(fixture: Dict[str, Any]) -> Iterable[Tuple[int, Type[ raise ValueError(f"Network {network} does not match any known VM rules") -def genesis_params_from_fixture(fixture: Dict[str, Any]) -> Dict[str, Any]: +def genesis_fields_from_fixture(fixture: Dict[str, Any]) -> Dict[str, Any]: + """ + Convert all genesis fields in a fixture to a dictionary of header fields and values. + """ + return { 'parent_hash': fixture['genesisBlockHeader']['parentHash'], 'uncles_hash': fixture['genesisBlockHeader']['uncleHash'], @@ -185,6 +190,34 @@ def genesis_params_from_fixture(fixture: Dict[str, Any]) -> Dict[str, Any]: } +def genesis_params_from_fixture(fixture: Dict[str, Any]) -> Dict[str, Any]: + """ + Convert a genesis fixture into a dict of the configurable header fields and values. + + Some fields cannot be explicitly set when creating a new header, like + parent_hash, which is automatically set to the empty hash. + """ + + params = genesis_fields_from_fixture(fixture) + + # Confirm that (currently) non-configurable defaults are set correctly, + # then remove them because they cannot be configured on the header. + defaults = ( + ('parent_hash', constants.GENESIS_PARENT_HASH), + ('uncles_hash', constants.EMPTY_UNCLE_HASH), + ('bloom', constants.GENESIS_BLOOM), + ('block_number', constants.GENESIS_BLOCK_NUMBER), + ('gas_used', constants.GENESIS_GAS_USED), + ) + + for key, default_val in defaults: + supplied_val = params.pop(key) + if supplied_val != default_val: + raise ValueError(f"Unexpected genesis {key}: {supplied_val}, expected: {default_val}") + + return params + + def new_chain_from_fixture(fixture: Dict[str, Any], chain_cls: Type[ChainAPI] = MainnetChain) -> ChainAPI: base_db = AtomicDB() diff --git a/eth/tools/rlp.py b/eth/tools/rlp.py index d697232f74..2313fdf2ec 100644 --- a/eth/tools/rlp.py +++ b/eth/tools/rlp.py @@ -8,11 +8,6 @@ ) -assert_imported_genesis_header_unchanged = replace_exceptions({ - ValidationError: AssertionError, -})(validate_rlp_equal(obj_a_name='genesis header', obj_b_name='imported header')) - - assert_mined_block_unchanged = replace_exceptions({ ValidationError: AssertionError, })(validate_rlp_equal(obj_a_name='provided block', obj_b_name='executed block')) diff --git a/eth/validation.py b/eth/validation.py index 83f5cfdc41..8594e10da5 100644 --- a/eth/validation.py +++ b/eth/validation.py @@ -24,11 +24,11 @@ from eth_utils.toolz import itertoolz +from eth._utils.headers import ( + compute_gas_limit_bounds, +) from eth.abc import VirtualMachineAPI from eth.constants import ( - GAS_LIMIT_ADJUSTMENT_FACTOR, - GAS_LIMIT_MAXIMUM, - GAS_LIMIT_MINIMUM, SECPK1_N, UINT_256_MAX, UINT_64_MAX, @@ -166,7 +166,7 @@ def validate_uint256(value: int, title: str = "Value") -> None: ) if value > UINT_256_MAX: raise ValidationError( - f"{title} exeeds maximum UINT256 size. Got: {value}" + f"{title} exceeds maximum UINT256 size. Got: {value}" ) @@ -229,14 +229,14 @@ def validate_vm_configuration(vm_configuration: Tuple[Tuple[int, Type[VirtualMac def validate_gas_limit(gas_limit: int, parent_gas_limit: int) -> None: - if gas_limit < GAS_LIMIT_MINIMUM: - raise ValidationError(f"Gas limit {gas_limit} is below minimum {GAS_LIMIT_MINIMUM}") - if gas_limit > GAS_LIMIT_MAXIMUM: - raise ValidationError(f"Gas limit {gas_limit} is above maximum {GAS_LIMIT_MAXIMUM}") - diff = gas_limit - parent_gas_limit - if diff > (parent_gas_limit // GAS_LIMIT_ADJUSTMENT_FACTOR): + low_bound, high_bound = compute_gas_limit_bounds(parent_gas_limit) + if gas_limit < low_bound: + raise ValidationError( + f"The gas limit {gas_limit} is too low. It must be at least {low_bound}" + ) + elif gas_limit > high_bound: raise ValidationError( - f"Gas limit {gas_limit} difference to parent {parent_gas_limit} is too big {diff}" + f"The gas limit {gas_limit} is too high. It must be at most {high_bound}" ) diff --git a/eth/vm/base.py b/eth/vm/base.py index 7fda65c556..a80f5c9f9e 100644 --- a/eth/vm/base.py +++ b/eth/vm/base.py @@ -69,9 +69,6 @@ get_parent_header, get_block_header_by_hash, ) -from eth._utils.headers import ( - generate_header_from_parent_header, -) from eth.validation import ( validate_length_lte, validate_gas_limit, @@ -176,15 +173,29 @@ def create_execution_context(cls, prev_hashes: Iterable[Hash32], chain_context: ChainContextAPI) -> ExecutionContextAPI: fee_recipient = cls.consensus_class.get_fee_recipient(header) - return ExecutionContext( - coinbase=fee_recipient, - 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, - ) + try: + base_fee = header.base_fee_per_gas + except AttributeError: + return ExecutionContext( + coinbase=fee_recipient, + 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, + ) + else: + return ExecutionContext( + coinbase=fee_recipient, + 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, + base_fee_per_gas=base_fee, + ) def execute_bytecode(self, origin: Address, @@ -435,12 +446,7 @@ def pack_block(self, block: BlockAPI, *args: Any, **kwargs: Any) -> BlockAPI: def generate_block_from_parent_header_and_coinbase(cls, parent_header: BlockHeaderAPI, coinbase: Address) -> BlockAPI: - block_header = generate_header_from_parent_header( - cls.compute_difficulty, - parent_header, - coinbase, - timestamp=parent_header.timestamp + 1, - ) + block_header = cls.create_header_from_parent(parent_header, coinbase=coinbase) block = cls.get_block_class()( block_header, transactions=[], @@ -448,6 +454,11 @@ def generate_block_from_parent_header_and_coinbase(cls, ) return block + @classmethod + def create_genesis_header(cls, **genesis_params: Any) -> BlockHeaderAPI: + # Create genesis header by setting the parent to None + return cls.create_header_from_parent(None, **genesis_params) + @classmethod def get_block_class(cls) -> Type[BlockAPI]: if cls.block_class is None: @@ -590,7 +601,7 @@ def validate_header(cls, validate_length_lte( header.extra_data, cls.extra_data_max_bytes, title="BlockHeader.extra_data") - validate_gas_limit(header.gas_limit, parent_header.gas_limit) + cls.validate_gas(header, parent_header) if header.block_number != parent_header.block_number + 1: raise ValidationError( @@ -608,6 +619,13 @@ def validate_header(cls, f"- parent : {parent_header.timestamp}. " ) + @classmethod + def validate_gas( + cls, + header: BlockHeaderAPI, + parent_header: BlockHeaderAPI) -> None: + validate_gas_limit(header.gas_limit, parent_header.gas_limit) + def validate_seal(self, header: BlockHeaderAPI) -> None: try: self._consensus.validate_seal(header) @@ -662,13 +680,19 @@ def get_state_class(cls) -> Type[StateAPI]: return cls._state_class @contextlib.contextmanager - def state_in_temp_block(self) -> Iterator[StateAPI]: + def in_costless_state(self) -> Iterator[StateAPI]: header = self.get_header() + temp_block = self.generate_block_from_parent_header_and_coinbase(header, header.coinbase) prev_hashes = itertools.chain((header.hash,), self.previous_hashes) + if hasattr(temp_block.header, 'base_fee_per_gas'): + free_header = temp_block.header.copy(base_fee_per_gas=0) + else: + free_header = temp_block.header + state = self.build_state(self.chaindb.db, - temp_block.header, + free_header, self.chain_context, prev_hashes) diff --git a/eth/vm/execution_context.py b/eth/vm/execution_context.py index d17284ac9d..b059916fd6 100644 --- a/eth/vm/execution_context.py +++ b/eth/vm/execution_context.py @@ -1,5 +1,6 @@ from typing import ( Iterable, + Optional, ) from eth_typing import ( @@ -21,6 +22,7 @@ class ExecutionContext(ExecutionContextAPI): _gas_limit = None _prev_hashes = None _chain_id = None + _base_fee_per_gas = None def __init__( self, @@ -30,7 +32,8 @@ def __init__( difficulty: int, gas_limit: int, prev_hashes: Iterable[Hash32], - chain_id: int) -> None: + chain_id: int, + base_fee_per_gas: Optional[int] = None) -> None: self._coinbase = coinbase self._timestamp = timestamp self._block_number = block_number @@ -38,6 +41,7 @@ def __init__( self._gas_limit = gas_limit self._prev_hashes = CachedIterable(prev_hashes) self._chain_id = chain_id + self._base_fee_per_gas = base_fee_per_gas @property def coinbase(self) -> Address: @@ -66,3 +70,12 @@ def prev_hashes(self) -> Iterable[Hash32]: @property def chain_id(self) -> int: return self._chain_id + + @property + def base_fee_per_gas(self) -> int: + if self._base_fee_per_gas is None: + raise AttributeError( + f"This header at Block #{self.block_number} does not have a base gas fee" + ) + else: + return self._base_fee_per_gas diff --git a/eth/vm/forks/__init__.py b/eth/vm/forks/__init__.py index ccce67561f..742b3c0d4d 100644 --- a/eth/vm/forks/__init__.py +++ b/eth/vm/forks/__init__.py @@ -28,3 +28,6 @@ from .berlin import ( # noqa: F401 BerlinVM, ) +from .london import ( # noqa: F401 + LondonVM +) diff --git a/eth/vm/forks/berlin/receipts.py b/eth/vm/forks/berlin/receipts.py index fc6dbd51a0..2c73539e33 100644 --- a/eth/vm/forks/berlin/receipts.py +++ b/eth/vm/forks/berlin/receipts.py @@ -124,6 +124,7 @@ def __eq__(self, other: Any) -> bool: class BerlinReceiptBuilder(ReceiptBuilderAPI): legacy_sedes = Receipt + codecs = TYPED_RECEIPT_BODY_CODECS @classmethod def decode(cls, encoded: bytes) -> ReceiptAPI: @@ -131,7 +132,7 @@ def decode(cls, encoded: bytes) -> ReceiptAPI: raise ValidationError("Encoded receipt was empty, which makes it invalid") type_id = to_int(encoded[0]) - if type_id in TYPED_RECEIPT_BODY_CODECS: + if type_id in cls.codecs: return TypedReceipt.decode(encoded) else: return rlp.decode(encoded, sedes=cls.legacy_sedes) diff --git a/eth/vm/forks/berlin/transactions.py b/eth/vm/forks/berlin/transactions.py index 5554829198..62b7dc972e 100644 --- a/eth/vm/forks/berlin/transactions.py +++ b/eth/vm/forks/berlin/transactions.py @@ -4,6 +4,7 @@ Sequence, Tuple, Type, + cast, ) from cached_property import cached_property @@ -136,6 +137,15 @@ def as_signed_transaction(self, private_key: PrivateKey) -> 'TypedTransaction': ) return TypedTransaction(self._type_id, signed_transaction) + # Old transactions are treated as setting both max-fees as the gas price + @property + def max_priority_fee_per_gas(self) -> int: + return self.gas_price + + @property + def max_fee_per_gas(self) -> int: + return self.gas_price + class AccessListTransaction(rlp.Serializable, SignedTransactionMethods, SignedTransactionAPI): _type_id = ACCESS_LIST_TRANSACTION_TYPE @@ -213,6 +223,15 @@ def make_receipt( logs=logs, ) + # Old transactions are treated as setting both max-fees as the gas price + @property + def max_priority_fee_per_gas(self) -> int: + return self.gas_price + + @property + def max_fee_per_gas(self) -> int: + return self.gas_price + class AccessListPayloadDecoder(TransactionDecoderAPI): @classmethod @@ -280,6 +299,14 @@ def nonce(self) -> int: def gas_price(self) -> int: return self._inner.gas_price + @property + def max_priority_fee_per_gas(self) -> int: + return self._inner.max_priority_fee_per_gas + + @property + def max_fee_per_gas(self) -> int: + return self._inner.max_fee_per_gas + @property def gas(self) -> int: return self._inner.gas @@ -323,7 +350,7 @@ def check_signature_validity(self) -> None: @cached_property def hash(self) -> Hash32: - return keccak(self.encode()) + return cast(Hash32, keccak(self.encode())) def get_intrinsic_gas(self) -> int: return self._inner.get_intrinsic_gas() @@ -361,6 +388,7 @@ class BerlinTransactionBuilder(TransactionBuilderAPI): """ legacy_signed = BerlinLegacyTransaction legacy_unsigned = BerlinUnsignedLegacyTransaction + typed_transaction = TypedTransaction @classmethod def decode(cls, encoded: bytes) -> SignedTransactionAPI: @@ -369,21 +397,21 @@ def decode(cls, encoded: bytes) -> SignedTransactionAPI: type_id = to_int(encoded[0]) if type_id in VALID_TRANSACTION_TYPES: - return TypedTransaction.decode(encoded) + return cls.typed_transaction.decode(encoded) else: return rlp.decode(encoded, sedes=cls.legacy_signed) @classmethod def deserialize(cls, encoded: DecodedZeroOrOneLayerRLP) -> SignedTransactionAPI: if isinstance(encoded, bytes): - return TypedTransaction.deserialize(encoded) + return cls.typed_transaction.deserialize(encoded) else: return cls.legacy_signed.deserialize(encoded) @classmethod def serialize(cls, obj: SignedTransactionAPI) -> DecodedZeroOrOneLayerRLP: - if isinstance(obj, TypedTransaction): - return TypedTransaction.serialize(obj) + if isinstance(obj, cls.typed_transaction): + return cls.typed_transaction.serialize(obj) else: return cls.legacy_signed.serialize(obj) @@ -462,4 +490,4 @@ def new_access_list_transaction( r, s, ) - return TypedTransaction(ACCESS_LIST_TRANSACTION_TYPE, transaction) + return cls.typed_transaction(ACCESS_LIST_TRANSACTION_TYPE, transaction) diff --git a/eth/vm/forks/byzantium/headers.py b/eth/vm/forks/byzantium/headers.py index 1c76034e7a..f477854af4 100644 --- a/eth/vm/forks/byzantium/headers.py +++ b/eth/vm/forks/byzantium/headers.py @@ -20,6 +20,9 @@ from eth._utils.db import ( get_parent_header, ) +from eth._utils.headers import ( + new_timestamp_from_parent, +) from eth.validation import ( validate_gt, validate_header_params_for_configuration, @@ -77,9 +80,10 @@ def create_header_from_parent(difficulty_fn: Callable[[BlockHeaderAPI, int], int parent_header: BlockHeaderAPI, **header_params: Any) -> BlockHeaderAPI: - if 'difficulty' not in header_params: - header_params.setdefault('timestamp', parent_header.timestamp + 1) + if 'timestamp' not in header_params: + header_params['timestamp'] = new_timestamp_from_parent(parent_header) + if 'difficulty' not in header_params: header_params['difficulty'] = difficulty_fn( parent_header, header_params['timestamp'], diff --git a/eth/vm/forks/frontier/__init__.py b/eth/vm/forks/frontier/__init__.py index 0db64af528..47a1d36119 100644 --- a/eth/vm/forks/frontier/__init__.py +++ b/eth/vm/forks/frontier/__init__.py @@ -1,4 +1,6 @@ -from typing import Type +from typing import ( + Type, +) from eth_bloom import ( BloomFilter, diff --git a/eth/vm/forks/frontier/headers.py b/eth/vm/forks/frontier/headers.py index 9226247d0a..f7fce87301 100644 --- a/eth/vm/forks/frontier/headers.py +++ b/eth/vm/forks/frontier/headers.py @@ -21,6 +21,8 @@ ) from eth._utils.headers import ( compute_gas_limit, + fill_header_params_from_parent, + new_timestamp_from_parent, ) from eth.rlp.headers import BlockHeader @@ -74,10 +76,12 @@ def compute_frontier_difficulty(parent_header: BlockHeaderAPI, timestamp: int) - def create_frontier_header_from_parent(parent_header: BlockHeaderAPI, **header_params: Any) -> BlockHeader: + if 'timestamp' not in header_params: + header_params['timestamp'] = new_timestamp_from_parent(parent_header) + if 'difficulty' not in header_params: # Use setdefault to ensure the new header has the same timestamp we use to calculate its # difficulty. - header_params.setdefault('timestamp', parent_header.timestamp + 1) header_params['difficulty'] = compute_frontier_difficulty( parent_header, header_params['timestamp'], @@ -85,12 +89,11 @@ def create_frontier_header_from_parent(parent_header: BlockHeaderAPI, if 'gas_limit' not in header_params: header_params['gas_limit'] = compute_gas_limit( parent_header, - gas_limit_floor=GENESIS_GAS_LIMIT, + genesis_gas_limit=GENESIS_GAS_LIMIT, ) - header = BlockHeader.from_parent(parent=parent_header, **header_params) - - return header + all_fields = fill_header_params_from_parent(parent_header, **header_params) + return BlockHeader(**all_fields) def configure_frontier_header(vm: "FrontierVM", **header_params: Any) -> BlockHeader: diff --git a/eth/vm/forks/frontier/state.py b/eth/vm/forks/frontier/state.py index 3f8fdac2b9..f52a46a598 100644 --- a/eth/vm/forks/frontier/state.py +++ b/eth/vm/forks/frontier/state.py @@ -50,8 +50,13 @@ def validate_transaction(self, transaction: SignedTransactionAPI) -> None: self.vm_state.validate_transaction(transaction) def build_evm_message(self, transaction: SignedTransactionAPI) -> MessageAPI: - - gas_fee = transaction.gas * transaction.gas_price + # Use vm_state.get_gas_price instead of transaction_context.gas_price so + # that we can run get_transaction_result (aka~ eth_call) and estimate_gas. + # Both work better if the GASPRICE opcode returns the original real price, + # but the sender's balance doesn't actually deduct the gas. This get_gas_price() + # will return 0 for eth_call, but transaction_context.gas_price will return + # the same value as the GASPRICE opcode. + gas_fee = transaction.gas * self.vm_state.get_gas_price(transaction) # Buy Gas self.vm_state.delta_balance(transaction.sender, -1 * gas_fee) @@ -140,6 +145,8 @@ def build_computation(self, def finalize_computation(self, transaction: SignedTransactionAPI, computation: ComputationAPI) -> ComputationAPI: + transaction_context = self.vm_state.get_transaction_context(transaction) + # Self Destruct Refunds num_deletions = len(computation.get_accounts_for_deletion()) if num_deletions: @@ -150,7 +157,7 @@ def finalize_computation(self, gas_refunded = computation.get_gas_refund() gas_used = transaction.gas - gas_remaining gas_refund = min(gas_refunded, gas_used // 2) - gas_refund_amount = (gas_refund + gas_remaining) * transaction.gas_price + gas_refund_amount = (gas_refund + gas_remaining) * transaction_context.gas_price if gas_refund_amount: self.vm_state.logger.debug2( @@ -162,8 +169,8 @@ def finalize_computation(self, self.vm_state.delta_balance(computation.msg.sender, gas_refund_amount) # Miner Fees - transaction_fee = \ - (transaction.gas - gas_remaining - gas_refund) * transaction.gas_price + gas_used = transaction.gas - gas_remaining - gas_refund + transaction_fee = gas_used * self.vm_state.get_tip(transaction) self.vm_state.logger.debug2( 'TRANSACTION FEE: %s -> %s', transaction_fee, diff --git a/eth/vm/forks/frontier/transactions.py b/eth/vm/forks/frontier/transactions.py index afb35c21a3..d6e0d4bf34 100644 --- a/eth/vm/forks/frontier/transactions.py +++ b/eth/vm/forks/frontier/transactions.py @@ -158,6 +158,15 @@ def make_receipt( logs=logs, ) + # Old transactions are treated as setting both max-fees as the gas price + @property + def max_priority_fee_per_gas(self) -> int: + return self.gas_price + + @property + def max_fee_per_gas(self) -> int: + return self.gas_price + class FrontierUnsignedTransaction(BaseUnsignedTransaction): @@ -187,3 +196,12 @@ def as_signed_transaction(self, private_key: PrivateKey) -> FrontierTransaction: def get_intrinsic_gas(self) -> int: return frontier_get_intrinsic_gas(self) + + # Old transactions are treated as setting both max-fees as the gas price + @property + def max_priority_fee_per_gas(self) -> int: + return self.gas_price + + @property + def max_fee_per_gas(self) -> int: + return self.gas_price diff --git a/eth/vm/forks/frontier/validation.py b/eth/vm/forks/frontier/validation.py index 6de5e7a150..cc87327804 100644 --- a/eth/vm/forks/frontier/validation.py +++ b/eth/vm/forks/frontier/validation.py @@ -12,19 +12,22 @@ def validate_frontier_transaction(state: StateAPI, transaction: SignedTransactionAPI) -> None: - gas_cost = transaction.gas * transaction.gas_price + max_gas_cost = transaction.gas * state.get_gas_price(transaction) sender_balance = state.get_balance(transaction.sender) - if sender_balance < gas_cost: + if sender_balance < max_gas_cost: raise ValidationError( f"Sender {transaction.sender!r} cannot afford txn gas " - f"{gas_cost} with account balance {sender_balance}" + f"{max_gas_cost} with account balance {sender_balance}" ) - total_cost = transaction.value + gas_cost + total_cost = transaction.value + max_gas_cost if sender_balance < total_cost: - raise ValidationError("Sender account balance cannot afford txn") + raise ValidationError( + f"Sender does not have enough balance to cover transaction value and gas " + f" (has {sender_balance}, needs {total_cost})" + ) sender_nonce = state.get_nonce(transaction.sender) if sender_nonce != transaction.nonce: diff --git a/eth/vm/forks/london/__init__.py b/eth/vm/forks/london/__init__.py new file mode 100644 index 0000000000..686a88ba6b --- /dev/null +++ b/eth/vm/forks/london/__init__.py @@ -0,0 +1,61 @@ +from typing import Type + +from eth_utils.exceptions import ValidationError + +from eth.abc import ( + BlockHeaderAPI, +) +from eth.rlp.blocks import BaseBlock +from eth.validation import validate_gas_limit +from eth.vm.forks.berlin import BerlinVM +from eth.vm.state import BaseState + +from .blocks import LondonBlock +from .constants import ( + ELASTICITY_MULTIPLIER, +) +from .headers import ( + calculate_expected_base_fee_per_gas, + compute_london_difficulty, + configure_london_header, + create_london_header_from_parent, +) +from .state import LondonState + + +class LondonVM(BerlinVM): + # fork name + fork = 'london' + + # classes + block_class: Type[BaseBlock] = LondonBlock + _state_class: Type[BaseState] = LondonState + + # Methods + create_header_from_parent = staticmethod(create_london_header_from_parent) # type: ignore + compute_difficulty = staticmethod(compute_london_difficulty) # type: ignore + configure_header = configure_london_header + + @classmethod + def validate_gas( + cls, + header: BlockHeaderAPI, + parent_header: BlockHeaderAPI) -> None: + + if hasattr(parent_header, 'base_fee_per_gas'): + # Follow normal gas limit rules if the previous block had a base fee + parent_gas_limit = parent_header.gas_limit + else: + # On the fork block, double the gas limit. + # That way, the gas target (which is half the London limit) equals the + # previous (pre-London) gas limit. + parent_gas_limit = parent_header.gas_limit * ELASTICITY_MULTIPLIER + + validate_gas_limit(header.gas_limit, parent_gas_limit) + + expected_base_fee_per_gas = calculate_expected_base_fee_per_gas(parent_header) + if expected_base_fee_per_gas != header.base_fee_per_gas: + raise ValidationError( + f"Header has invalid base fee per gas (has {header.base_fee_per_gas}" + f", expected {expected_base_fee_per_gas})" + ) diff --git a/eth/vm/forks/london/blocks.py b/eth/vm/forks/london/blocks.py new file mode 100644 index 0000000000..9cb9a83e17 --- /dev/null +++ b/eth/vm/forks/london/blocks.py @@ -0,0 +1,194 @@ +from typing import ( + List, + Type, + cast, +) + +from eth_hash.auto import keccak +from eth_typing import ( + BlockNumber, +) +from eth_typing.evm import ( + Address, + Hash32 +) +from eth_utils import ( + encode_hex, +) +import rlp +from rlp.sedes import ( + Binary, + CountableList, + big_endian_int, + binary +) + +from eth._utils.headers import ( + new_timestamp_from_parent, +) +from eth.abc import ( + BlockHeaderAPI, + BlockHeaderSedesAPI, + MiningHeaderAPI, + ReceiptBuilderAPI, + TransactionBuilderAPI, +) +from eth.constants import ( + ZERO_ADDRESS, + ZERO_HASH32, + EMPTY_UNCLE_HASH, + GENESIS_NONCE, + GENESIS_PARENT_HASH, + BLANK_ROOT_HASH, +) +from eth.rlp.headers import ( + BlockHeader, +) +from eth.rlp.sedes import ( + address, + hash32, + trie_root, + uint256, +) +from eth.vm.forks.berlin.blocks import ( + BerlinBlock, +) + +from .receipts import ( + LondonReceiptBuilder, +) +from .transactions import ( + LondonTransactionBuilder, +) + +UNMINED_LONDON_HEADER_FIELDS = [ + ('parent_hash', hash32), + ('uncles_hash', hash32), + ('coinbase', address), + ('state_root', trie_root), + ('transaction_root', trie_root), + ('receipt_root', trie_root), + ('bloom', uint256), + ('difficulty', big_endian_int), + ('block_number', big_endian_int), + ('gas_limit', big_endian_int), + ('gas_used', big_endian_int), + ('timestamp', big_endian_int), + ('extra_data', binary), + ('base_fee_per_gas', big_endian_int), +] + + +class LondonMiningHeader(rlp.Serializable, MiningHeaderAPI): + fields = UNMINED_LONDON_HEADER_FIELDS + + +class LondonBlockHeader(rlp.Serializable, BlockHeaderAPI): + fields = UNMINED_LONDON_HEADER_FIELDS + [ + ('mix_hash', binary), + ('nonce', Binary(8, allow_empty=True)), + ] + + def __init__(self, + difficulty: int, + block_number: BlockNumber, + gas_limit: int, + timestamp: int = None, + coinbase: Address = ZERO_ADDRESS, + parent_hash: Hash32 = ZERO_HASH32, + uncles_hash: Hash32 = EMPTY_UNCLE_HASH, + state_root: Hash32 = BLANK_ROOT_HASH, + transaction_root: Hash32 = BLANK_ROOT_HASH, + receipt_root: Hash32 = BLANK_ROOT_HASH, + bloom: int = 0, + gas_used: int = 0, + extra_data: bytes = b'', + mix_hash: Hash32 = ZERO_HASH32, + nonce: bytes = GENESIS_NONCE, + base_fee_per_gas: int = 0) -> None: + if timestamp is None: + if parent_hash == ZERO_HASH32: + timestamp = new_timestamp_from_parent(None) + else: + # without access to the parent header, we cannot select a new timestamp correctly + raise ValueError("Must set timestamp explicitly if this is not a genesis header") + super().__init__( + parent_hash=parent_hash, + uncles_hash=uncles_hash, + coinbase=coinbase, + state_root=state_root, + transaction_root=transaction_root, + receipt_root=receipt_root, + bloom=bloom, + difficulty=difficulty, + block_number=block_number, + gas_limit=gas_limit, + gas_used=gas_used, + timestamp=timestamp, + extra_data=extra_data, + mix_hash=mix_hash, + nonce=nonce, + base_fee_per_gas=base_fee_per_gas, + ) + + def __str__(self) -> str: + return f'' + + _hash = None + + @property + def hash(self) -> Hash32: + if self._hash is None: + self._hash = keccak(rlp.encode(self)) + return cast(Hash32, self._hash) + + @property + def mining_hash(self) -> Hash32: + result = keccak(rlp.encode(self[:-2], LondonMiningHeader)) + return cast(Hash32, result) + + @property + def hex_hash(self) -> str: + return encode_hex(self.hash) + + @property + def is_genesis(self) -> bool: + # if removing the block_number == 0 test, consider the validation consequences. + # validate_header stops trying to check the current header against a parent header. + # Can someone trick us into following a high difficulty header with genesis parent hash? + return self.parent_hash == GENESIS_PARENT_HASH and self.block_number == 0 + + +class LondonBackwardsHeader(BlockHeaderSedesAPI): + """ + An rlp sedes class for block headers. + + It can serialize and deserialize *both* London and pre-London headers. + """ + + @classmethod + def serialize(cls, obj: BlockHeaderAPI) -> List[bytes]: + return obj.serialize(obj) + + @classmethod + def deserialize(cls, encoded: List[bytes]) -> BlockHeaderAPI: + num_fields = len(encoded) + if num_fields == 16: + return LondonBlockHeader.deserialize(encoded) + elif num_fields == 15: + return BlockHeader.deserialize(encoded) + else: + raise ValueError( + "London & earlier can only handle headers of 15 or 16 fields. " + f"Got {num_fields} in {encoded!r}" + ) + + +class LondonBlock(BerlinBlock): + transaction_builder: Type[TransactionBuilderAPI] = LondonTransactionBuilder + receipt_builder: Type[ReceiptBuilderAPI] = LondonReceiptBuilder + fields = [ + ('header', LondonBlockHeader), + ('transactions', CountableList(transaction_builder)), + ('uncles', CountableList(LondonBackwardsHeader)) + ] diff --git a/eth/vm/forks/london/computation.py b/eth/vm/forks/london/computation.py new file mode 100644 index 0000000000..e34983772f --- /dev/null +++ b/eth/vm/forks/london/computation.py @@ -0,0 +1,11 @@ +from eth.vm.forks.berlin.computation import ( + BerlinComputation, +) + + +class LondonComputation(BerlinComputation): + """ + A class for all execution computations in the ``London`` fork. + Inherits from :class:`~eth.vm.forks.berlin.BerlinComputation` + """ + pass diff --git a/eth/vm/forks/london/constants.py b/eth/vm/forks/london/constants.py new file mode 100644 index 0000000000..1eaa79b4e3 --- /dev/null +++ b/eth/vm/forks/london/constants.py @@ -0,0 +1,13 @@ +from eth.vm.forks.berlin.constants import ( + ACCESS_LIST_ADDRESS_COST_EIP_2930, + ACCESS_LIST_STORAGE_KEY_COST_EIP_2930, +) + +# EIP 1559 +DYNAMIC_FEE_TRANSACTION_TYPE = 2 +DYNAMIC_FEE_ADDRESS_COST = ACCESS_LIST_ADDRESS_COST_EIP_2930 +DYNAMIC_FEE_STORAGE_KEY_COST = ACCESS_LIST_STORAGE_KEY_COST_EIP_2930 + +BASE_FEE_MAX_CHANGE_DENOMINATOR = 8 +INITIAL_BASE_FEE = 1000000000 +ELASTICITY_MULTIPLIER = 2 diff --git a/eth/vm/forks/london/headers.py b/eth/vm/forks/london/headers.py new file mode 100644 index 0000000000..b92a0fc8c9 --- /dev/null +++ b/eth/vm/forks/london/headers.py @@ -0,0 +1,158 @@ +from typing import ( + Any, + Callable, + List, + Optional, +) + +from eth_utils import ( + ValidationError, +) +from toolz.functoolz import curry + +from eth._utils.headers import ( + compute_gas_limit, + fill_header_params_from_parent, + new_timestamp_from_parent, +) +from eth.abc import ( + BlockHeaderAPI, + BlockHeaderSedesAPI, +) +from eth.constants import GENESIS_GAS_LIMIT +from eth.rlp.headers import BlockHeader +from eth.vm.forks.berlin.headers import ( + compute_berlin_difficulty, + configure_header, +) + +from .blocks import LondonBlockHeader +from .constants import ( + BASE_FEE_MAX_CHANGE_DENOMINATOR, + ELASTICITY_MULTIPLIER, + INITIAL_BASE_FEE, +) + + +def calculate_expected_base_fee_per_gas(parent_header: BlockHeaderAPI) -> int: + if parent_header is None: + # Parent is empty when making the genesis header + return INITIAL_BASE_FEE + else: + try: + parent_base_fee_per_gas = parent_header.base_fee_per_gas + except AttributeError: + # Parent is a non-London header + return INITIAL_BASE_FEE + + # Parent *is* a London header + parent_gas_target = parent_header.gas_limit // ELASTICITY_MULTIPLIER + parent_gas_used = parent_header.gas_used + + if parent_gas_used == parent_gas_target: + return parent_base_fee_per_gas + + elif parent_gas_used > parent_gas_target: + gas_used_delta = parent_gas_used - parent_gas_target + overburnt_wei = parent_base_fee_per_gas * gas_used_delta + base_fee_per_gas_delta = max( + overburnt_wei // parent_gas_target // BASE_FEE_MAX_CHANGE_DENOMINATOR, + 1, + ) + return parent_base_fee_per_gas + base_fee_per_gas_delta + + else: + gas_used_delta = parent_gas_target - parent_gas_used + underburnt_wei = parent_base_fee_per_gas * gas_used_delta + base_fee_per_gas_delta = ( + underburnt_wei + // parent_gas_target + // BASE_FEE_MAX_CHANGE_DENOMINATOR + ) + return max(parent_base_fee_per_gas - base_fee_per_gas_delta, 0) + + +@curry +def create_header_from_parent(difficulty_fn: Callable[[BlockHeaderAPI, int], int], + parent_header: Optional[BlockHeaderAPI], + **header_params: Any) -> BlockHeaderAPI: + + if 'gas_limit' not in header_params: + if parent_header is not None and not hasattr(parent_header, 'base_fee_per_gas'): + # If the previous block was not a London block, + # double the gas limit, so the new target is the old gas limit + header_params['gas_limit'] = parent_header.gas_limit * ELASTICITY_MULTIPLIER + else: + # frontier rules + header_params['gas_limit'] = compute_gas_limit( + parent_header, + genesis_gas_limit=GENESIS_GAS_LIMIT, + ) + + # byzantium + if 'timestamp' not in header_params: + header_params['timestamp'] = new_timestamp_from_parent(parent_header) + + if 'difficulty' not in header_params: + if parent_header is None: + raise ValueError( + "Must set difficulty when creating a new genesis header (no parent)." + " Consider 1 for easy mining or eth.constants.GENESIS_DIFFICULTY for consistency." + ) + else: + header_params['difficulty'] = difficulty_fn( + parent_header, + header_params['timestamp'], + ) + + all_fields = fill_header_params_from_parent(parent_header, **header_params) + + # must add the new field *after* filling, because the general fill function doesn't recognize it + base_fee_per_gas = calculate_expected_base_fee_per_gas(parent_header) + if 'base_fee_per_gas' in header_params and all_fields['base_fee_per_gas'] != base_fee_per_gas: + raise ValidationError( + f"Cannot select an invalid base_fee_per_gas of:" + f" {all_fields['base_fee_per_gas']!r}, expected: {base_fee_per_gas}" + ) + else: + all_fields['base_fee_per_gas'] = base_fee_per_gas + + new_header = LondonBlockHeader(**all_fields) # type:ignore + return new_header + + +compute_london_difficulty = compute_berlin_difficulty + +create_london_header_from_parent = create_header_from_parent( + compute_london_difficulty +) + +configure_london_header = configure_header(compute_london_difficulty) + + +class LondonBackwardsHeader(BlockHeaderSedesAPI): + """ + An rlp sedes class for block headers. + + It can serialize and deserialize *both* London and pre-London headers. + """ + + @classmethod + def serialize(cls, obj: BlockHeaderAPI) -> List[bytes]: + if isinstance(obj, LondonBlockHeader): + return LondonBlockHeader.serialize(obj) + else: + return BlockHeader.serialize(obj) + + @classmethod + def deserialize(cls, encoded: List[bytes]) -> BlockHeaderAPI: + num_fields = len(encoded) + if num_fields == 16: + return LondonBlockHeader.deserialize(encoded) + elif num_fields == 15: + return BlockHeader.deserialize(encoded) + else: + raise ValueError( + "London & earlier can only handle headers of 15 or 16 fields. " + f"Got {num_fields} in {encoded!r}" + ) diff --git a/eth/vm/forks/london/receipts.py b/eth/vm/forks/london/receipts.py new file mode 100644 index 0000000000..567fa069de --- /dev/null +++ b/eth/vm/forks/london/receipts.py @@ -0,0 +1,23 @@ +from typing import ( + Dict, + Type, +) + +from eth.rlp.receipts import ( + Receipt, +) +from eth.vm.forks.berlin.receipts import ( + BerlinReceiptBuilder +) +from eth.vm.forks.berlin.constants import ( + ACCESS_LIST_TRANSACTION_TYPE, +) + +from .constants import DYNAMIC_FEE_TRANSACTION_TYPE + + +class LondonReceiptBuilder(BerlinReceiptBuilder): + codecs: Dict[int, Type[Receipt]] = { + ACCESS_LIST_TRANSACTION_TYPE: Receipt, + DYNAMIC_FEE_TRANSACTION_TYPE: Receipt, + } diff --git a/eth/vm/forks/london/state.py b/eth/vm/forks/london/state.py new file mode 100644 index 0000000000..3364ab9227 --- /dev/null +++ b/eth/vm/forks/london/state.py @@ -0,0 +1,137 @@ +from typing import Type + +from eth_hash.auto import keccak +from eth_utils import ( + encode_hex, +) + +from eth.abc import ( + AccountDatabaseAPI, + MessageAPI, + SignedTransactionAPI, + StateAPI, + TransactionContextAPI, + TransactionExecutorAPI, +) +from eth.constants import ( + CREATE_CONTRACT_ADDRESS, +) +from eth.db.account import ( + AccountDB +) +from eth.vm.message import ( + Message, +) +from eth.vm.forks.berlin.state import ( + BerlinState, + BerlinTransactionExecutor, +) +from eth._utils.address import ( + generate_contract_address, +) + +from .computation import LondonComputation +from .validation import validate_london_normalized_transaction + + +class LondonTransactionExecutor(BerlinTransactionExecutor): + def build_evm_message(self, transaction: SignedTransactionAPI) -> MessageAPI: + # Use vm_state.get_gas_price instead of transaction_context.gas_price so + # that we can run get_transaction_result (aka~ eth_call) and estimate_gas. + # Both work better if the GASPRICE opcode returns the original real price, + # but the sender's balance doesn't actually deduct the gas. This get_gas_price() + # will return 0 for eth_call, but transaction_context.gas_price will return + # the same value as the GASPRICE opcode. + gas_fee = transaction.gas * self.vm_state.get_gas_price(transaction) + + # Buy Gas + self.vm_state.delta_balance(transaction.sender, -1 * gas_fee) + + # Increment Nonce + self.vm_state.increment_nonce(transaction.sender) + + # Setup VM Message + message_gas = transaction.gas - transaction.intrinsic_gas + + if transaction.to == CREATE_CONTRACT_ADDRESS: + contract_address = generate_contract_address( + transaction.sender, + self.vm_state.get_nonce(transaction.sender) - 1, + ) + data = b'' + code = transaction.data + else: + contract_address = None + data = transaction.data + code = self.vm_state.get_code(transaction.to) + + self.vm_state.logger.debug2( + ( + "TRANSACTION: %r; sender: %s | to: %s | data-hash: %s" + ), + transaction, + encode_hex(transaction.sender), + encode_hex(transaction.to), + encode_hex(keccak(transaction.data)), + ) + + message = Message( + gas=message_gas, + to=transaction.to, + sender=transaction.sender, + value=transaction.value, + data=data, + code=code, + create_address=contract_address, + ) + return message + + +class LondonState(BerlinState): + account_db_class: Type[AccountDatabaseAPI] = AccountDB + computation_class = LondonComputation + transaction_executor_class: Type[TransactionExecutorAPI] = LondonTransactionExecutor + + def get_tip(self, transaction: SignedTransactionAPI) -> int: + return min( + transaction.max_fee_per_gas - self.execution_context.base_fee_per_gas, + transaction.max_priority_fee_per_gas, + ) + + def get_gas_price(self, transaction: SignedTransactionAPI) -> int: + return min( + transaction.max_fee_per_gas, + transaction.max_priority_fee_per_gas + self.execution_context.base_fee_per_gas, + ) + + def validate_transaction( + self, + transaction: SignedTransactionAPI + ) -> None: + validate_london_normalized_transaction( + state=self, + transaction=transaction, + ) + + def get_transaction_context(self: StateAPI, + transaction: SignedTransactionAPI) -> TransactionContextAPI: + """ + London-specific transaction context creation, + where gas_price includes the block base fee + """ + effective_gas_price = min( + transaction.max_priority_fee_per_gas + self.execution_context.base_fee_per_gas, + transaction.max_fee_per_gas, + ) + # See how this reduces in a pre-1559 transaction: + # 1. effective_gas_price = min( + # transaction.gas_price + self.execution_context.base_fee_per_gas, + # transaction.gas_price, + # ) + # base_fee_per_gas is non-negative, so: + # 2. effective_gas_price = transaction.gas_price + + return self.get_transaction_context_class()( + gas_price=effective_gas_price, + origin=transaction.sender + ) diff --git a/eth/vm/forks/london/transactions.py b/eth/vm/forks/london/transactions.py new file mode 100644 index 0000000000..dea368c097 --- /dev/null +++ b/eth/vm/forks/london/transactions.py @@ -0,0 +1,282 @@ +from cached_property import cached_property +from typing import ( + Dict, + Sequence, + Tuple, + Type, +) + +from eth_keys.datatypes import PrivateKey +from eth_typing import ( + Address, + Hash32, +) +from eth_utils import ( + to_bytes, +) +import rlp +from rlp.sedes import ( + CountableList, + big_endian_int, + binary, +) + +from eth._utils.transactions import ( + calculate_intrinsic_gas, + create_transaction_signature, + extract_transaction_sender, + validate_transaction_signature, +) +from eth.abc import ( + ReceiptAPI, + SignedTransactionAPI, + TransactionDecoderAPI, +) +from eth.rlp.logs import Log +from eth.rlp.receipts import Receipt +from eth.rlp.transactions import SignedTransactionMethods +from eth.rlp.sedes import address +from eth.vm.forks.berlin.constants import ( + ACCESS_LIST_ADDRESS_COST_EIP_2930, + ACCESS_LIST_STORAGE_KEY_COST_EIP_2930, + ACCESS_LIST_TRANSACTION_TYPE, +) +from eth.vm.forks.berlin.transactions import ( + AccessListPayloadDecoder, + AccountAccesses, + BerlinLegacyTransaction, + BerlinTransactionBuilder, + BerlinUnsignedLegacyTransaction, + TypedTransaction, +) +from eth.vm.forks.istanbul.transactions import ( + ISTANBUL_TX_GAS_SCHEDULE, +) + +from .constants import DYNAMIC_FEE_TRANSACTION_TYPE + + +class LondonLegacyTransaction(BerlinLegacyTransaction): + pass + + +class LondonUnsignedLegacyTransaction(BerlinUnsignedLegacyTransaction): + def as_signed_transaction(self, + private_key: PrivateKey, + chain_id: int = None) -> LondonLegacyTransaction: + v, r, s = create_transaction_signature(self, private_key, chain_id=chain_id) + return LondonLegacyTransaction( + 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, + ) + + +class UnsignedDynamicFeeTransaction(rlp.Serializable): + _type_id = DYNAMIC_FEE_TRANSACTION_TYPE + fields = [ + ('chain_id', big_endian_int), + ('nonce', big_endian_int), + ('max_priority_fee_per_gas', big_endian_int), + ('max_fee_per_gas', big_endian_int), + ('gas', big_endian_int), + ('to', address), + ('value', big_endian_int), + ('data', binary), + ('access_list', CountableList(AccountAccesses)), + ] + + @cached_property + def _type_byte(self) -> bytes: + return to_bytes(self._type_id) + + def get_message_for_signing(self) -> bytes: + payload = rlp.encode(self) + return self._type_byte + payload + + def as_signed_transaction(self, private_key: PrivateKey) -> 'TypedTransaction': + message = self.get_message_for_signing() + signature = private_key.sign_msg(message) + y_parity, r, s = signature.vrs + + signed_transaction = DynamicFeeTransaction( + self.chain_id, + self.nonce, + self.max_priority_fee_per_gas, + self.max_fee_per_gas, + self.gas, + self.to, + self.value, + self.data, + self.access_list, + y_parity, + r, + s + ) + return LondonTypedTransaction(self._type_id, signed_transaction) + + +class DynamicFeeTransaction(rlp.Serializable, SignedTransactionMethods, SignedTransactionAPI): + _type_id = DYNAMIC_FEE_TRANSACTION_TYPE + fields = [ + ('chain_id', big_endian_int), + ('nonce', big_endian_int), + ('max_priority_fee_per_gas', big_endian_int), + ('max_fee_per_gas', big_endian_int), + ('gas', big_endian_int), + ('to', address), + ('value', big_endian_int), + ('data', binary), + ('access_list', CountableList(AccountAccesses)), + ('y_parity', big_endian_int), + ('r', big_endian_int), + ('s', big_endian_int), + ] + + @property + def gas_price(self) -> None: + raise AttributeError( + "Gas price is no longer available. See max_priority_fee_per_gas or max_fee_per_gas" + ) + + def get_sender(self) -> Address: + return extract_transaction_sender(self) + + def get_message_for_signing(self) -> bytes: + unsigned = UnsignedDynamicFeeTransaction( + self.chain_id, + self.nonce, + self.max_priority_fee_per_gas, + self.max_fee_per_gas, + self.gas, + self.to, + self.value, + self.data, + self.access_list, + ) + payload = rlp.encode(unsigned) + return self._type_byte + payload + + def check_signature_validity(self) -> None: + validate_transaction_signature(self) + + @cached_property + def _type_byte(self) -> bytes: + return to_bytes(self._type_id) + + @cached_property + def hash(self) -> Hash32: + raise NotImplementedError("Call hash() on the TypedTransaction instead") + + def get_intrinsic_gas(self) -> int: + core_gas = calculate_intrinsic_gas(ISTANBUL_TX_GAS_SCHEDULE, self) + + num_addresses = len(self.access_list) + preload_address_costs = ACCESS_LIST_ADDRESS_COST_EIP_2930 * num_addresses + + num_slots = sum(len(slots) for _, slots in self.access_list) + preload_slot_costs = ACCESS_LIST_STORAGE_KEY_COST_EIP_2930 * num_slots + + return core_gas + preload_address_costs + preload_slot_costs + + def encode(self) -> bytes: + return rlp.encode(self) + + def make_receipt( + self, + status: bytes, + gas_used: int, + log_entries: Tuple[Tuple[bytes, Tuple[int, ...], bytes], ...]) -> ReceiptAPI: + + logs = [ + Log(address, topics, data) + for address, topics, data + in log_entries + ] + # TypedTransaction is responsible for wrapping this into a TypedReceipt + return Receipt( + state_root=status, + gas_used=gas_used, + logs=logs, + ) + + +class DynamicFeePayloadDecoder(TransactionDecoderAPI): + @classmethod + def decode(cls, payload: bytes) -> SignedTransactionAPI: + return rlp.decode(payload, sedes=DynamicFeeTransaction) + + +class LondonTypedTransaction(TypedTransaction): + decoders: Dict[int, Type[TransactionDecoderAPI]] = { + ACCESS_LIST_TRANSACTION_TYPE: AccessListPayloadDecoder, + DYNAMIC_FEE_TRANSACTION_TYPE: DynamicFeePayloadDecoder, + } + + +class LondonTransactionBuilder(BerlinTransactionBuilder): + legacy_signed = LondonLegacyTransaction + legacy_unsigned = LondonUnsignedLegacyTransaction + typed_transaction = LondonTypedTransaction + + @classmethod + def new_unsigned_dynamic_fee_transaction( + cls, + chain_id: int, + nonce: int, + max_priority_fee_per_gas: int, + max_fee_per_gas: int, + gas: int, + to: Address, + value: int, + data: bytes, + access_list: Sequence[Tuple[Address, Sequence[int]]],) -> LondonTypedTransaction: + transaction = UnsignedDynamicFeeTransaction( + chain_id, + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas, + to, + value, + data, + access_list, + ) + return transaction + + @classmethod + def new_dynamic_fee_transaction( + cls, + chain_id: int, + nonce: int, + max_priority_fee_per_gas: int, + max_fee_per_gas: int, + gas: int, + to: Address, + value: int, + data: bytes, + access_list: Sequence[Tuple[Address, Sequence[int]]], + y_parity: int, + r: int, + s: int) -> LondonTypedTransaction: + transaction = DynamicFeeTransaction( + chain_id, + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas, + to, + value, + data, + access_list, + y_parity, + r, + s, + ) + return LondonTypedTransaction(DYNAMIC_FEE_TRANSACTION_TYPE, transaction) diff --git a/eth/vm/forks/london/validation.py b/eth/vm/forks/london/validation.py new file mode 100644 index 0000000000..4b4c0e09d2 --- /dev/null +++ b/eth/vm/forks/london/validation.py @@ -0,0 +1,29 @@ +from eth_utils.exceptions import ValidationError + +from eth.abc import ( + SignedTransactionAPI, + StateAPI +) +from eth.vm.forks.homestead.validation import ( + validate_homestead_transaction, +) + + +def validate_london_normalized_transaction( + state: StateAPI, + transaction: SignedTransactionAPI, +) -> None: + """ + Validates a London normalized transaction. + + Raise `eth.exceptions.ValidationError` if the sender cannot + afford to send this transaction. + """ + base_fee_per_gas = state.execution_context.base_fee_per_gas + if transaction.max_fee_per_gas < base_fee_per_gas: + raise ValidationError( + f"Sender's max fee per gas ({transaction.max_fee_per_gas}) is " + f"lower than block's base fee per gas ({base_fee_per_gas})" + ) + + validate_homestead_transaction(state, transaction) diff --git a/eth/vm/header.py b/eth/vm/header.py new file mode 100644 index 0000000000..442f32237f --- /dev/null +++ b/eth/vm/header.py @@ -0,0 +1,12 @@ +from eth.vm.forks.london.blocks import LondonBackwardsHeader + +HeaderSedes = LondonBackwardsHeader +""" +An RLP codec that can decode *all* known header types. + +Unfortunately, we often cannot look up the VM to determine the valid codec. For +example, when looking up a header by hash, we don't have the block number yet, +and so can't load the VM configuration to find out which VM's rules to use to +decode the header. Also, it's useful to have this universal sedes class when +decoding multiple uncles that span a fork block. +""" diff --git a/eth/vm/state.py b/eth/vm/state.py index 3bb181250f..a5b66f7110 100644 --- a/eth/vm/state.py +++ b/eth/vm/state.py @@ -88,6 +88,12 @@ def difficulty(self) -> int: def gas_limit(self) -> int: return self.execution_context.gas_limit + def get_tip(self, transaction: SignedTransactionAPI) -> int: + return transaction.gas_price + + def get_gas_price(self, transaction: SignedTransactionAPI) -> int: + return transaction.gas_price + # # Access to account db # @@ -269,10 +275,9 @@ def get_custom_transaction_context(transaction: SignedTransactionAPI) -> Transac finally: self.get_transaction_context = original_context # type: ignore # Remove ignore if https://github.com/python/mypy/issues/708 is fixed. # noqa: E501 - @classmethod - def get_transaction_context(cls, + def get_transaction_context(self, transaction: SignedTransactionAPI) -> TransactionContextAPI: - return cls.get_transaction_context_class()( + return self.get_transaction_context_class()( gas_price=transaction.gas_price, origin=transaction.sender, ) diff --git a/newsfragments/2013.bugfix.rst b/newsfragments/2013.bugfix.rst new file mode 100644 index 0000000000..bec1924c71 --- /dev/null +++ b/newsfragments/2013.bugfix.rst @@ -0,0 +1,2 @@ +- Use UTC timestamp instead of local time zone, when creating a header. +- Use UTC for clique validation. diff --git a/newsfragments/2013.feature.rst b/newsfragments/2013.feature.rst new file mode 100644 index 0000000000..f0d2b1d000 --- /dev/null +++ b/newsfragments/2013.feature.rst @@ -0,0 +1 @@ +Implement `EIP-1559 `_ for London support. diff --git a/newsfragments/2013.internal.rst b/newsfragments/2013.internal.rst new file mode 100644 index 0000000000..e533b8b325 --- /dev/null +++ b/newsfragments/2013.internal.rst @@ -0,0 +1,5 @@ +- some test_vm fixes: + - use the correctly paired VMs in PoW test + - make sure *only* the block number is invalid in block number validity test +- more robust test fixture name generation +- run a newer version of the lint test from `make lint` diff --git a/newsfragments/2013.removal.rst b/newsfragments/2013.removal.rst new file mode 100644 index 0000000000..032809c3b8 --- /dev/null +++ b/newsfragments/2013.removal.rst @@ -0,0 +1,3 @@ +- Can no longer supply some fields to the genesis, like bloom and parent_hash. +- :meth:`eth.rlp.headers.BlockHeader.from_parent()` is gone, because you should + always use the VM to create a header (to make sure you get the correct type). diff --git a/scripts/benchmark/_utils/chain_plumbing.py b/scripts/benchmark/_utils/chain_plumbing.py index ae6f33aac7..7220d5cd24 100644 --- a/scripts/benchmark/_utils/chain_plumbing.py +++ b/scripts/benchmark/_utils/chain_plumbing.py @@ -60,13 +60,10 @@ SECOND_ADDRESS = Address(SECOND_ADDRESS_PRIVATE_KEY.public_key.to_canonical_address()) GENESIS_PARAMS = { - 'parent_hash': constants.GENESIS_PARENT_HASH, - 'uncles_hash': constants.EMPTY_UNCLE_HASH, 'coinbase': constants.ZERO_ADDRESS, 'transaction_root': constants.BLANK_ROOT_HASH, 'receipt_root': constants.BLANK_ROOT_HASH, 'difficulty': 1, - 'block_number': constants.GENESIS_BLOCK_NUMBER, 'gas_limit': 3141592, 'extra_data': constants.GENESIS_EXTRA_DATA, 'nonce': constants.GENESIS_NONCE diff --git a/tests/conftest.py b/tests/conftest.py index d955f33c21..6c73e95bbc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,6 @@ from eth_utils import ( decode_hex, - to_canonical_address, to_tuple, to_wei, setup_DEBUG2_logging, @@ -33,6 +32,7 @@ IstanbulVM, MuirGlacierVM, BerlinVM, + LondonVM, ) # @@ -93,6 +93,7 @@ def _file_logging(request): IstanbulVM, MuirGlacierVM, BerlinVM, + LondonVM, ]) def VM(request): return request.param @@ -120,6 +121,27 @@ def funded_address_initial_balance(): return to_wei(1000, 'ether') +# wrapped in a method so that different callers aren't using (and modifying) the same dict +def _get_genesis_defaults(): + # values that are not yet customizeable (and will automatically be default) are commented out + return { + 'difficulty': constants.GENESIS_DIFFICULTY, + 'gas_limit': 3141592, + 'coinbase': constants.GENESIS_COINBASE, + 'nonce': constants.GENESIS_NONCE, + 'mix_hash': constants.GENESIS_MIX_HASH, + 'extra_data': constants.GENESIS_EXTRA_DATA, + 'timestamp': 1501851927, + # 'block_number': constants.GENESIS_BLOCK_NUMBER, + # 'parent_hash': constants.GENESIS_PARENT_HASH, + # "bloom": 0, + # "gas_used": 0, + # "uncles_hash": decode_hex("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347") # noqa: E501 + # "receipt_root": decode_hex("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), # noqa: E501 + # "transaction_root": decode_hex("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), # noqa: E501 + } + + def _chain_with_block_validation(VM, base_db, genesis_state, chain_cls=Chain): """ Return a Chain object containing just the genesis block. @@ -132,23 +154,6 @@ def _chain_with_block_validation(VM, base_db, genesis_state, chain_cls=Chain): importing arbitrarily constructe, not finalized blocks, use the chain_without_block_validation fixture instead. """ - genesis_params = { - "bloom": 0, - "coinbase": to_canonical_address("8888f1f195afa192cfee860698584c030f4c9db1"), - "difficulty": 131072, - "extra_data": b"B", - "gas_limit": 3141592, - "gas_used": 0, - "mix_hash": decode_hex("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), # noqa: E501 - "nonce": decode_hex("0102030405060708"), - "block_number": 0, - "parent_hash": decode_hex("0000000000000000000000000000000000000000000000000000000000000000"), # noqa: E501 - "receipt_root": decode_hex("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), # noqa: E501 - "timestamp": 1422494849, - "transaction_root": decode_hex("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), # noqa: E501 - "uncles_hash": decode_hex("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347") # noqa: E501 - } - klass = chain_cls.configure( __name__='TestChain', vm_configuration=( @@ -156,7 +161,7 @@ def _chain_with_block_validation(VM, base_db, genesis_state, chain_cls=Chain): ), chain_id=1337, ) - chain = klass.from_genesis(base_db, genesis_params, genesis_state) + chain = klass.from_genesis(base_db, _get_genesis_defaults(), genesis_state) return chain @@ -222,18 +227,7 @@ def _chain_without_block_validation(request, VM, base_db, genesis_state): chain_id=1337, **overrides, ) - genesis_params = { - 'block_number': constants.GENESIS_BLOCK_NUMBER, - 'difficulty': constants.GENESIS_DIFFICULTY, - 'gas_limit': 3141592, - 'parent_hash': constants.GENESIS_PARENT_HASH, - 'coinbase': constants.GENESIS_COINBASE, - 'nonce': constants.GENESIS_NONCE, - 'mix_hash': constants.GENESIS_MIX_HASH, - 'extra_data': constants.GENESIS_EXTRA_DATA, - 'timestamp': 1501851927, - } - chain = klass.from_genesis(base_db, genesis_params, genesis_state) + chain = klass.from_genesis(base_db, _get_genesis_defaults(), genesis_state) return chain diff --git a/tests/core/builder-tools/test_chain_construction.py b/tests/core/builder-tools/test_chain_construction.py index eadfb72ebf..eda2393e7e 100644 --- a/tests/core/builder-tools/test_chain_construction.py +++ b/tests/core/builder-tools/test_chain_construction.py @@ -18,6 +18,7 @@ homestead_at, istanbul_at, latest_mainnet_at, + london_at, muir_glacier_at, name, petersburg_at, @@ -35,6 +36,7 @@ IstanbulVM, MuirGlacierVM, BerlinVM, + LondonVM, ) @@ -88,7 +90,8 @@ def test_chain_builder_construct_chain_vm_configuration_multiple_forks(): (istanbul_at, IstanbulVM), (muir_glacier_at, MuirGlacierVM), (berlin_at, BerlinVM), - (latest_mainnet_at, MuirGlacierVM), # this will change whenever the next upgrade is locked + (london_at, LondonVM), + (latest_mainnet_at, LondonVM), # this will change whenever the next upgrade is locked ) ) def test_chain_builder_construct_chain_fork_specific_helpers(fork_fn, vm_class): diff --git a/tests/core/chain-object/test_chain.py b/tests/core/chain-object/test_chain.py index 3190d0579c..892c10bfa2 100644 --- a/tests/core/chain-object/test_chain.py +++ b/tests/core/chain-object/test_chain.py @@ -2,11 +2,17 @@ import rlp from eth_utils import decode_hex +from eth_utils.toolz import sliding_window from eth import constants from eth.abc import MiningChainAPI -from eth.chains.mainnet import MAINNET_GENESIS_HEADER +from eth.chains.base import MiningChain +from eth.chains.mainnet import ( + MAINNET_GENESIS_HEADER, + MAINNET_VMS, +) from eth.chains.ropsten import ROPSTEN_GENESIS_HEADER +from eth.consensus.noproof import NoProofConsensus from eth.exceptions import ( TransactionNotFound, ) @@ -25,6 +31,30 @@ def chain(chain_without_block_validation): return chain_without_block_validation +VM_PAIRS = sliding_window(2, MAINNET_VMS) + + +@pytest.fixture(params=VM_PAIRS) +def vm_crossover_chain(request, base_db, genesis_state): + start_vm, end_vm = request.param + klass = MiningChain.configure( + __name__='CrossoverTestChain', + vm_configuration=( + ( + constants.GENESIS_BLOCK_NUMBER, + start_vm.configure(consensus_class=NoProofConsensus), + ), + # Can mine one block of the first VM, then the next block with be the next VM + ( + constants.GENESIS_BLOCK_NUMBER + 2, + end_vm.configure(consensus_class=NoProofConsensus), + ), + ), + chain_id=1337, + ) + return klass.from_genesis(base_db, dict(difficulty=1), genesis_state) + + @pytest.fixture def valid_chain(chain_with_block_validation): return chain_with_block_validation @@ -98,7 +128,7 @@ def test_mine_all(chain, tx, tx2, funded_address): assert chain.get_canonical_transaction(tx.hash) == tx end_balance = chain.get_vm().state.get_balance(funded_address) - expected_spend = 2 * (100 + 21000 * 10) # sent + gas * gasPrice + expected_spend = 2 * (100 + 21000 * 10**10) # sent + gas * gasPrice assert start_balance - end_balance == expected_spend elif isinstance(chain, MiningChainAPI): @@ -200,3 +230,37 @@ def test_get_transaction_receipt(chain, tx): assert chain.get_canonical_transaction_index(tx.hash) == (1, 0) assert chain.get_transaction_receipt_by_index(1, 0) == expected_receipt assert chain.get_transaction_receipt(tx.hash) == expected_receipt + + +def _mine_result_to_header(mine_all_result): + block_import_result, _, _ = mine_all_result + return block_import_result.imported_block.header + + +def test_uncles_across_VMs(vm_crossover_chain): + chain = vm_crossover_chain + + genesis = chain.get_canonical_block_header_by_number(0) + + # Mine in 1st VM + uncle_header1 = chain.mine_block(extra_data=b'uncle1').header + canon_header1 = _mine_result_to_header( + chain.mine_all([], parent_header=genesis) + ) + + # Mine in 2nd VM + uncle_header2 = chain.mine_block(extra_data=b'uncle2').header + canon_header2 = _mine_result_to_header( + chain.mine_all([], parent_header=canon_header1) + ) + + # Mine block with uncles from both VMs + canon_block3 = chain.mine_block(uncles=[uncle_header1, uncle_header2]) + + assert canon_header2.hash == canon_block3.header.parent_hash + + assert canon_block3.uncles == (uncle_header1, uncle_header2) + + deserialized_block3 = chain.get_canonical_block_by_number(3) + + assert deserialized_block3.uncles == (uncle_header1, uncle_header2) diff --git a/tests/core/chain-object/test_contract_call.py b/tests/core/chain-object/test_contract_call.py index 94e0e5b370..7871c4a025 100644 --- a/tests/core/chain-object/test_contract_call.py +++ b/tests/core/chain-object/test_contract_call.py @@ -29,6 +29,7 @@ IstanbulVM, MuirGlacierVM, BerlinVM, + LondonVM, ) @@ -93,23 +94,23 @@ def uint256_to_bytes(uint): ( ( 'getMeaningOfLife()', - 0, + 10 ** 10, # In order to work with >=EIP-1559, the minimum gas should be >1 gwei uint256_to_bytes(42), ), ( 'getGasPrice()', - 0, - uint256_to_bytes(0), + 10 ** 10, + uint256_to_bytes(10 ** 10), ), ( 'getGasPrice()', - 9, - uint256_to_bytes(9), + 10 ** 11, + uint256_to_bytes(10 ** 11), ), ( # make sure that whatever voodoo is used to execute a call, the balance is not inflated 'getBalance()', - 1, + 10 ** 10, uint256_to_bytes(0), ), ), @@ -245,6 +246,16 @@ def test_get_transaction_result( 'useLotsOfGas()', OutOfGas, ), + ( + LondonVM, + 'doRevert()', + Revert, + ), + ( + LondonVM, + 'useLotsOfGas()', + OutOfGas, + ), ), ) def test_get_transaction_result_revert( diff --git a/tests/core/chain-object/test_gas_estimation.py b/tests/core/chain-object/test_gas_estimation.py index c621c349f1..8fe97fde2c 100644 --- a/tests/core/chain-object/test_gas_estimation.py +++ b/tests/core/chain-object/test_gas_estimation.py @@ -15,6 +15,7 @@ IstanbulVM, MuirGlacierVM, BerlinVM, + LondonVM, ) from eth._utils.address import force_bytes_to_address @@ -754,6 +755,78 @@ 22272, id='sha3 precompile 32 bytes 1000_tolerance binary pending for BerlinVM', ), + pytest.param( + b'', + None, + ADDR_1010, + True, + LondonVM, + 21000, + id='simple default pending for LondonVM', + ), + pytest.param( + b'', + None, + ADDR_1010, + False, + LondonVM, + 21000, + id='simple default for LondonVM', + ), + pytest.param( + b'\xff' * 10, + None, + ADDR_1010, + True, + LondonVM, + 21160, + id='10 bytes default pending for LondonVM', + ), + pytest.param( + b'\xff' * 10, + None, + ADDR_1010, + False, + LondonVM, + 21160, + id='10 bytes default for LondonVM', + ), + pytest.param( + b'\xff' * 32, + None, + ADDRESS_2, + True, + LondonVM, + 33675, + id='sha3 precompile 32 bytes default pending for LondonVM', + ), + pytest.param( + b'\xff' * 32, + None, + ADDRESS_2, + False, + LondonVM, + 33687, + id='sha3 precompile 32 bytes default for LondonVM', + ), + pytest.param( + b'\xff' * 320, + None, + ADDRESS_2, + True, + LondonVM, + 38265, + id='sha3 precompile 320 bytes default pending for LondonVM', + ), + pytest.param( + b'\xff' * 32, + binary_gas_search_1000_tolerance, + ADDRESS_2, + True, + LondonVM, + 22272, + id='sha3 precompile 32 bytes 1000_tolerance binary pending for LondonVM', + ), ), ) def test_estimate_gas( @@ -815,6 +888,7 @@ def test_estimate_gas( (IstanbulVM, 186120), (MuirGlacierVM, 186120), (BerlinVM, 186120), + (LondonVM, 186120), ) ) def test_estimate_gas_on_full_block( diff --git a/tests/core/chain-object/test_header_chain.py b/tests/core/chain-object/test_header_chain.py deleted file mode 100644 index 507b71c265..0000000000 --- a/tests/core/chain-object/test_header_chain.py +++ /dev/null @@ -1,137 +0,0 @@ -import random - -import pytest - -from eth_utils import ( - to_tuple, - keccak, -) - -from eth.db.header import HeaderDB -from eth.chains.header import HeaderChain - -from eth.constants import ( - GENESIS_BLOCK_NUMBER, - GENESIS_DIFFICULTY, - GENESIS_GAS_LIMIT, -) -from eth.rlp.headers import ( - BlockHeader, -) -from eth.tools.rlp import ( - assert_headers_eq, -) - - -@to_tuple -def mk_header_chain(base_header, length): - previous_header = base_header - for _ in range(length): - next_header = BlockHeader.from_parent( - parent=previous_header, - timestamp=previous_header.timestamp + 1, - gas_limit=previous_header.gas_limit, - difficulty=previous_header.difficulty, - extra_data=keccak(random.randint(0, 1e18)), - ) - yield next_header - previous_header = next_header - - -@pytest.fixture -def headerdb(base_db): - return HeaderDB(base_db) - - -@pytest.fixture -def genesis_header(): - return BlockHeader( - difficulty=GENESIS_DIFFICULTY, - block_number=GENESIS_BLOCK_NUMBER, - gas_limit=GENESIS_GAS_LIMIT, - ) - - -@pytest.fixture() -def header_chain(base_db, genesis_header): - return HeaderChain.from_genesis_header(base_db, genesis_header) - - -def test_header_chain_initialization_from_genesis_header(base_db, genesis_header): - header_chain = HeaderChain.from_genesis_header(base_db, genesis_header) - - head = header_chain.get_canonical_head() - assert_headers_eq(head, genesis_header) - - -def test_header_chain_initialization_header_already_persisted(base_db, genesis_header): - headerdb = HeaderDB(base_db) - headerdb.persist_header(genesis_header) - - # sanity check that the header is persisted - assert_headers_eq(headerdb.get_canonical_head(), genesis_header) - - header_chain = HeaderChain.from_genesis_header(base_db, genesis_header) - - head = header_chain.get_canonical_head() - assert_headers_eq(head, genesis_header) - - -def test_header_chain_get_canonical_block_hash_passthrough(header_chain, genesis_header): - assert header_chain.get_canonical_block_hash(0) == genesis_header.hash - - -def test_header_chain_get_canonical_block_header_by_number_passthrough( - header_chain, - genesis_header): - assert header_chain.get_canonical_block_header_by_number(0) == genesis_header - - -def test_header_chain_get_canonical_head_passthrough(header_chain): - assert header_chain.get_canonical_head() == header_chain.header - - -def test_header_chain_import_block(header_chain, genesis_header): - chain_a = mk_header_chain(genesis_header, 3) - chain_b = mk_header_chain(genesis_header, 2) - chain_c = mk_header_chain(genesis_header, 5) - - for header in chain_a: - res, _ = header_chain.import_header(header) - assert res == (header,) - assert_headers_eq(header_chain.header, header) - - for header in chain_b: - res, _ = header_chain.import_header(header) - assert res == () - assert_headers_eq(header_chain.header, chain_a[-1]) - - for idx, header in enumerate(chain_c, 1): - res, _ = header_chain.import_header(header) - if idx <= 3: - # prior to passing up `chain_a` each import should not return new - # canonical headers. - assert res == () - assert_headers_eq(header_chain.header, chain_a[-1]) - elif idx == 4: - # at the point where `chain_c` passes `chain_a` we should get the - # headers from `chain_c` up through current. - assert res == chain_c[:idx] - assert_headers_eq(res[-1], header) - assert_headers_eq(header_chain.header, header) - else: - # after `chain_c` has become canonical we should just get each new - # header back. - assert res == (header,) - assert_headers_eq(header_chain.header, header) - - assert_headers_eq(header_chain.header, chain_c[-1]) - - -def test_header_chain_get_block_header_by_hash_passthrough(header_chain, genesis_header): - assert header_chain.get_block_header_by_hash(genesis_header.hash) == genesis_header - - -def test_header_chain_header_exists(header_chain, genesis_header): - assert header_chain.header_exists(genesis_header.hash) is True - assert header_chain.header_exists(b'\x0f' * 32) is False diff --git a/tests/core/consensus/test_clique_consensus.py b/tests/core/consensus/test_clique_consensus.py index a5ea22fc19..036b228cf9 100644 --- a/tests/core/consensus/test_clique_consensus.py +++ b/tests/core/consensus/test_clique_consensus.py @@ -71,14 +71,15 @@ ) # Genesis params are dervived from the genesis header +# values that are not yet customizeable (and will automatically be default) are commented out PARAGON_GENESIS_PARAMS = { - 'parent_hash': PARAGON_GENESIS_HEADER.parent_hash, - 'uncles_hash': PARAGON_GENESIS_HEADER.uncles_hash, + # 'parent_hash': PARAGON_GENESIS_HEADER.parent_hash, + # 'uncles_hash': PARAGON_GENESIS_HEADER.uncles_hash, 'coinbase': PARAGON_GENESIS_HEADER.coinbase, - 'transaction_root': PARAGON_GENESIS_HEADER.transaction_root, - 'receipt_root': PARAGON_GENESIS_HEADER.receipt_root, + # 'transaction_root': PARAGON_GENESIS_HEADER.transaction_root, + # 'receipt_root': PARAGON_GENESIS_HEADER.receipt_root, 'difficulty': PARAGON_GENESIS_HEADER.difficulty, - 'block_number': PARAGON_GENESIS_HEADER.block_number, + # 'block_number': PARAGON_GENESIS_HEADER.block_number, 'timestamp': PARAGON_GENESIS_HEADER.timestamp, 'gas_limit': PARAGON_GENESIS_HEADER.gas_limit, 'extra_data': PARAGON_GENESIS_HEADER.extra_data, @@ -134,50 +135,51 @@ def has_vote_from(signer, votes): return any(vote.signer == signer for vote in votes) -def make_next_header(previous_header, +def make_next_header(chain, + previous_header, signer_private_key, coinbase=ZERO_ADDRESS, nonce=NONCE_DROP, difficulty=2): - next_header = sign_block_header(BlockHeader.from_parent( + unsigned_header = chain.create_header_from_parent( + previous_header, coinbase=coinbase, nonce=nonce, - parent=previous_header, timestamp=previous_header.timestamp + 1, gas_limit=previous_header.gas_limit, difficulty=difficulty, # FIXME: I think our sign_block_header is wrong extra_data=VANITY_LENGTH * b'0' + SIGNATURE_LENGTH * b'0', - ), signer_private_key) - return next_header + ) + return sign_block_header(unsigned_header, signer_private_key) @to_tuple -def alice_nominates_bob_and_ron_then_they_kick_her(): +def alice_nominates_bob_and_ron_then_they_kick_her(chain): header = PARAGON_GENESIS_HEADER - header = make_next_header(header, ALICE_PK) + header = make_next_header(chain, header, ALICE_PK) yield header - header = make_next_header(header, ALICE_PK, BOB, NONCE_AUTH) + header = make_next_header(chain, header, ALICE_PK, BOB, NONCE_AUTH) yield header # At this point, we have a new signer because it just needed a single vote to win at this point - header = make_next_header(header, BOB_PK, RON, NONCE_AUTH) + header = make_next_header(chain, header, BOB_PK, RON, NONCE_AUTH) yield header - header = make_next_header(header, ALICE_PK, RON, NONCE_AUTH) + header = make_next_header(chain, header, ALICE_PK, RON, NONCE_AUTH) yield header # But we needed two votes to get a third signer approvoved (+ 50 % signers) - header = make_next_header(header, BOB_PK, ALICE, NONCE_DROP) + header = make_next_header(chain, header, BOB_PK, ALICE, NONCE_DROP) yield header - header = make_next_header(header, RON_PK, ALICE, NONCE_DROP) + header = make_next_header(chain, header, RON_PK, ALICE, NONCE_DROP) yield header - header = make_next_header(header, BOB_PK) + header = make_next_header(chain, header, BOB_PK) yield header @@ -222,7 +224,7 @@ def test_can_retrieve_root_snapshot(paragon_chain): 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) + next_header = make_next_header(paragon_chain, head, ALICE_PK, RON, NONCE_AUTH) clique = get_clique(paragon_chain, head) with pytest.raises(ValidationError, match='Unknown ancestor'): @@ -230,7 +232,7 @@ def test_raises_unknown_ancestor_error(paragon_chain): def test_validate_chain_works_across_forks(paragon_chain): - voting_chain = alice_nominates_bob_and_ron_then_they_kick_her() + voting_chain = alice_nominates_bob_and_ron_then_they_kick_her(paragon_chain) paragon_chain.validate_chain_extension((PARAGON_GENESIS_HEADER,) + voting_chain) @@ -238,7 +240,7 @@ def test_validate_chain_works_across_forks(paragon_chain): def test_import_block(paragon_chain): vm = paragon_chain.get_vm() - tx = new_transaction(vm, ALICE, BOB, 10, ALICE_PK) + tx = new_transaction(vm, ALICE, BOB, 10, ALICE_PK, gas_price=10) assert vm.state.get_balance(ALICE) == ALICE_INITIAL_BALANCE assert vm.state.get_balance(BOB) == BOB_INITIAL_BALANCE assert vm.state.get_balance(vm.get_block().header.coinbase) == 0 @@ -270,7 +272,7 @@ def test_import_block(paragon_chain): def test_reapplies_headers_without_snapshots(paragon_chain): - voting_chain = alice_nominates_bob_and_ron_then_they_kick_her() + voting_chain = alice_nominates_bob_and_ron_then_they_kick_her(paragon_chain) # We save the headers but we do not create intermediate snapshots # to proof that the SnapshotManager re-applies all needed headers @@ -297,16 +299,18 @@ def test_revert_previous_nominate(paragon_chain): 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) + alice_votes_bob = make_next_header( + paragon_chain, head, ALICE_PK, coinbase=BOB, nonce=NONCE_AUTH) snapshot = validate_seal_and_get_snapshot(clique, alice_votes_bob) assert snapshot.get_sorted_signers() == [ALICE, BOB] - alice_votes_ron = make_next_header(alice_votes_bob, ALICE_PK, coinbase=RON, nonce=NONCE_AUTH) + alice_votes_ron = make_next_header( + paragon_chain, alice_votes_bob, ALICE_PK, coinbase=RON, nonce=NONCE_AUTH) snapshot = validate_seal_and_get_snapshot(clique, alice_votes_ron) assert snapshot.get_sorted_signers() == [ALICE, BOB] assert snapshot.tallies[RON].action == VoteAction.NOMINATE assert snapshot.tallies[RON].votes == 1 alice_votes_against_ron = make_next_header( - alice_votes_ron, ALICE_PK, coinbase=RON, nonce=NONCE_DROP, difficulty=1) + paragon_chain, alice_votes_ron, ALICE_PK, coinbase=RON, nonce=NONCE_DROP, difficulty=1) snapshot = validate_seal_and_get_snapshot(clique, alice_votes_against_ron) assert snapshot.get_sorted_signers() == [ALICE, BOB] # RON doesn't have a Tally anymore because Alice simple voted against her previous nomination @@ -319,16 +323,18 @@ def test_revert_previous_kick(paragon_chain): 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) + alice_votes_bob = make_next_header( + paragon_chain, head, ALICE_PK, coinbase=BOB, nonce=NONCE_AUTH) snapshot = validate_seal_and_get_snapshot(clique, alice_votes_bob) assert snapshot.get_sorted_signers() == [ALICE, BOB] - alice_kicks_bob = make_next_header(alice_votes_bob, ALICE_PK, coinbase=BOB, nonce=NONCE_DROP) + alice_kicks_bob = make_next_header( + paragon_chain, alice_votes_bob, ALICE_PK, coinbase=BOB, nonce=NONCE_DROP) snapshot = validate_seal_and_get_snapshot(clique, alice_kicks_bob) assert snapshot.get_sorted_signers() == [ALICE, BOB] assert snapshot.tallies[BOB].action == VoteAction.KICK assert snapshot.tallies[BOB].votes == 1 alice_votes_bob = make_next_header( - alice_kicks_bob, ALICE_PK, coinbase=BOB, nonce=NONCE_AUTH, difficulty=1) + paragon_chain, alice_kicks_bob, ALICE_PK, coinbase=BOB, nonce=NONCE_AUTH, difficulty=1) snapshot = validate_seal_and_get_snapshot(clique, alice_votes_bob) assert snapshot.get_sorted_signers() == [ALICE, BOB] # RON doesn't have a Tally anymore because Alice simple voted against her previous kick @@ -342,16 +348,18 @@ def test_does_not_count_multiple_kicks(paragon_chain): 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) + alice_votes_bob = make_next_header( + paragon_chain, head, ALICE_PK, coinbase=BOB, nonce=NONCE_AUTH) snapshot = validate_seal_and_get_snapshot(clique, alice_votes_bob) assert snapshot.get_sorted_signers() == [ALICE, BOB] - alice_kicks_bob = make_next_header(alice_votes_bob, ALICE_PK, coinbase=BOB, nonce=NONCE_DROP) + alice_kicks_bob = make_next_header( + paragon_chain, alice_votes_bob, ALICE_PK, coinbase=BOB, nonce=NONCE_DROP) snapshot = validate_seal_and_get_snapshot(clique, alice_kicks_bob) assert snapshot.get_sorted_signers() == [ALICE, BOB] assert snapshot.tallies[BOB].action == VoteAction.KICK assert snapshot.tallies[BOB].votes == 1 alice_kicks_bob_again = make_next_header( - alice_kicks_bob, ALICE_PK, coinbase=BOB, nonce=NONCE_DROP, difficulty=1) + paragon_chain, alice_kicks_bob, ALICE_PK, coinbase=BOB, nonce=NONCE_DROP, difficulty=1) snapshot = validate_seal_and_get_snapshot(clique, alice_kicks_bob_again) assert snapshot.get_sorted_signers() == [ALICE, BOB] assert snapshot.tallies[BOB].action == VoteAction.KICK @@ -363,16 +371,18 @@ def test_does_not_count_multiple_nominates(paragon_chain): 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) + alice_votes_bob = make_next_header( + paragon_chain, head, ALICE_PK, coinbase=BOB, nonce=NONCE_AUTH) snapshot = validate_seal_and_get_snapshot(clique, alice_votes_bob) assert snapshot.get_sorted_signers() == [ALICE, BOB] - alice_votes_ron = make_next_header(alice_votes_bob, ALICE_PK, coinbase=RON, nonce=NONCE_AUTH) + alice_votes_ron = make_next_header( + paragon_chain, alice_votes_bob, ALICE_PK, coinbase=RON, nonce=NONCE_AUTH) snapshot = validate_seal_and_get_snapshot(clique, alice_votes_ron) assert snapshot.get_sorted_signers() == [ALICE, BOB] assert snapshot.tallies[RON].action == VoteAction.NOMINATE assert snapshot.tallies[RON].votes == 1 alice_votes_ron_again = make_next_header( - alice_votes_ron, ALICE_PK, coinbase=RON, nonce=NONCE_AUTH, difficulty=1) + paragon_chain, alice_votes_ron, ALICE_PK, coinbase=RON, nonce=NONCE_AUTH, difficulty=1) snapshot = validate_seal_and_get_snapshot(clique, alice_votes_ron_again) assert snapshot.get_sorted_signers() == [ALICE, BOB] assert snapshot.tallies[RON].action == VoteAction.NOMINATE @@ -382,7 +392,7 @@ def test_does_not_count_multiple_nominates(paragon_chain): 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() + voting_chain = alice_nominates_bob_and_ron_then_they_kick_her(paragon_chain) snapshot = validate_seal_and_get_snapshot(clique, voting_chain[0]) assert snapshot.signers == {ALICE} @@ -412,7 +422,7 @@ def test_alice_votes_in_bob_and_ron_then_gets_kicked(paragon_chain): 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() + voting_chain = alice_nominates_bob_and_ron_then_they_kick_her(paragon_chain) # Fast forward to the point where we have Alice and Bob as signers snapshot = None @@ -440,7 +450,7 @@ def test_removes_all_pending_votes_after_kick(paragon_chain): ALICE_FRIEND = PublicKeyFactory().to_canonical_address() - voting_chain = alice_nominates_bob_and_ron_then_they_kick_her() + voting_chain = alice_nominates_bob_and_ron_then_they_kick_her(paragon_chain) # Fast forward to the point where we have Alice, Bob and Ron as signers snapshot = None @@ -451,6 +461,7 @@ def test_removes_all_pending_votes_after_kick(paragon_chain): # Alice nominates a weird friend that Bob and Ron have never heard of alices_nominates_friend = make_next_header( + paragon_chain, voting_chain[3], ALICE_PK, coinbase=ALICE_FRIEND, nonce=NONCE_AUTH, difficulty=1) snapshot = validate_seal_and_get_snapshot(clique, alices_nominates_friend) @@ -460,11 +471,12 @@ def test_removes_all_pending_votes_after_kick(paragon_chain): # Bob and Ron get upset and kick Alice bob_kicks_alice = make_next_header( + paragon_chain, alices_nominates_friend, BOB_PK, coinbase=ALICE, nonce=NONCE_DROP, difficulty=1) snapshot = validate_seal_and_get_snapshot(clique, bob_kicks_alice) ron_kicks_alice = make_next_header( - bob_kicks_alice, RON_PK, coinbase=ALICE, nonce=NONCE_DROP, difficulty=1) + paragon_chain, bob_kicks_alice, RON_PK, coinbase=ALICE, nonce=NONCE_DROP, difficulty=1) snapshot = validate_seal_and_get_snapshot(clique, ron_kicks_alice) # As Alice was kicked, her pending votes regarding her friend and his tally were removed diff --git a/tests/core/helpers.py b/tests/core/helpers.py index c859bb0768..b4a615b00b 100644 --- a/tests/core/helpers.py +++ b/tests/core/helpers.py @@ -36,5 +36,9 @@ def fill_block(chain, from_, key, gas, data): break else: raise exc + else: + new_header = chain.get_vm().get_block().header + assert new_header.gas_used > 0 + assert new_header.gas_used <= new_header.gas_limit assert chain.get_vm().get_block().header.gas_used > 0 diff --git a/tests/core/opcodes/test_opcodes.py b/tests/core/opcodes/test_opcodes.py index 71ffc1f336..014dcbe07c 100644 --- a/tests/core/opcodes/test_opcodes.py +++ b/tests/core/opcodes/test_opcodes.py @@ -26,9 +26,6 @@ InvalidInstruction, VMError, ) -from eth.rlp.headers import ( - BlockHeader, -) from eth._utils.padding import ( pad32 ) @@ -47,6 +44,7 @@ IstanbulVM, MuirGlacierVM, BerlinVM, + LondonVM, ) from eth.vm.message import ( Message, @@ -64,11 +62,6 @@ CANONICAL_ADDRESS_B = to_canonical_address("0xcd1722f3947def4cf144679da39c4c32bdc35681") CANONICAL_ADDRESS_C = b'\xee' * 20 CANONICAL_ZERO_ADDRESS = b'\0' * 20 -GENESIS_HEADER = BlockHeader( - difficulty=constants.GENESIS_DIFFICULTY, - block_number=constants.GENESIS_BLOCK_NUMBER, - gas_limit=constants.GENESIS_GAS_LIMIT, -) def assemble(*codes): @@ -81,7 +74,11 @@ def assemble(*codes): def setup_vm(vm_class, chain_id=None): db = AtomicDB() chain_context = ChainContext(chain_id) - return vm_class(GENESIS_HEADER, ChainDB(db), chain_context, ConsensusContext(db)) + genesis_header = vm_class.create_genesis_header( + difficulty=constants.GENESIS_DIFFICULTY, + timestamp=0, + ) + return vm_class(genesis_header, ChainDB(db), chain_context, ConsensusContext(db)) def run_computation( @@ -1009,6 +1006,7 @@ def test_sstore_limit_2300(gas_supplied, success, gas_used, refund): IstanbulVM, MuirGlacierVM, BerlinVM, + LondonVM, )) @pytest.mark.parametrize( # Testcases from https://eips.ethereum.org/EIPS/eip-1344 @@ -1321,7 +1319,7 @@ def test_access_list_gas_costs(vm_class, code, expect_gas_used, access_list): # cases from https://gist.github.com/holiman/174548cad102096858583c6fbbb0649a # mentioned in EIP-2929 -@pytest.mark.parametrize('vm_class', (BerlinVM, )) +@pytest.mark.parametrize('vm_class', (BerlinVM, LondonVM, )) @pytest.mark.parametrize( 'bytecode_hex, expect_gas_used', ( diff --git a/tests/core/tester/test_generate_vm_configuration.py b/tests/core/tester/test_generate_vm_configuration.py index ea84feab7a..062f77412e 100644 --- a/tests/core/tester/test_generate_vm_configuration.py +++ b/tests/core/tester/test_generate_vm_configuration.py @@ -20,6 +20,7 @@ class Forks(enum.Enum): Istanbul = 'Istanbul' MuirGlacier = 'MuirGlacier' Berlin = 'Berlin' + London = 'London' class CustomFrontierVM(FrontierVM): @@ -32,7 +33,7 @@ class CustomFrontierVM(FrontierVM): ( (), {}, - ((0, Forks.Berlin),), + ((0, Forks.London),), ), ( ((0, 'tangerine-whistle'), (1, 'spurious-dragon')), @@ -123,6 +124,7 @@ class CustomFrontierVM(FrontierVM): (6, 'istanbul'), (7, 'muir-glacier'), (8, 'berlin'), + (9, 'london'), ), {}, ( @@ -134,6 +136,7 @@ class CustomFrontierVM(FrontierVM): (6, Forks.Istanbul), (7, Forks.MuirGlacier), (8, Forks.Berlin), + (9, Forks.London), ), ), ), diff --git a/tests/core/transaction-utils/test_receipt_encoding.py b/tests/core/transaction-utils/test_receipt_encoding.py index cc5fa6ea37..545dbdfcbd 100644 --- a/tests/core/transaction-utils/test_receipt_encoding.py +++ b/tests/core/transaction-utils/test_receipt_encoding.py @@ -10,12 +10,18 @@ from eth.rlp.receipts import Receipt from eth.vm.forks import ( BerlinVM, + LondonVM, ) from eth.vm.forks.berlin.receipts import ( TypedReceipt, ) -RECOGNIZED_TRANSACTION_TYPES = {1} +# The type of receipt is based on the type of the transaction. So we are +# checking the type of the receipt against known transaction types. +# Add recognized types here if any fork knows about it. Then, +# add manual unrecognized types for older forks. For example, +# (BerlinVM, to_bytes(2), UnrecognizedTransactionType) should be added explicitly. +RECOGNIZED_TRANSACTION_TYPES = {1, 2} UNRECOGNIZED_TRANSACTION_TYPES = tuple( (to_bytes(val), UnrecognizedTransactionType) @@ -30,7 +36,7 @@ ) -@pytest.mark.parametrize('vm_class', [BerlinVM]) +@pytest.mark.parametrize('vm_class', [BerlinVM, LondonVM]) @pytest.mark.parametrize( 'encoded, expected', ( @@ -61,7 +67,7 @@ ), ) ) -def test_transaction_decode(vm_class, encoded, expected): +def test_receipt_decode(vm_class, encoded, expected): expected_encoding = expected.encode() assert encoded == expected_encoding @@ -70,7 +76,7 @@ def test_transaction_decode(vm_class, encoded, expected): assert decoded == expected -@pytest.mark.parametrize('vm_class', [BerlinVM]) +@pytest.mark.parametrize('vm_class', [BerlinVM, LondonVM]) @pytest.mark.parametrize( 'encoded, expected_failure', ( @@ -82,8 +88,24 @@ def test_transaction_decode(vm_class, encoded, expected): + UNRECOGNIZED_TRANSACTION_TYPES + INVALID_TRANSACTION_TYPES ) -def test_transaction_decode_failure(vm_class, encoded, expected_failure): - sedes = vm_class.get_transaction_builder() +def test_receipt_decode_failure(vm_class, encoded, expected_failure): + sedes = vm_class.get_receipt_builder() + with pytest.raises(expected_failure): + rlp.decode(encoded, sedes=sedes) + + +@pytest.mark.parametrize( + 'vm_class, encoded, expected_failure', + ( + ( + BerlinVM, + to_bytes(2), + UnrecognizedTransactionType, + ), + ) +) +def test_receipt_decode_failure_by_vm(vm_class, encoded, expected_failure): + sedes = vm_class.get_receipt_builder() with pytest.raises(expected_failure): rlp.decode(encoded, sedes=sedes) diff --git a/tests/core/transaction-utils/test_transaction_encoding.py b/tests/core/transaction-utils/test_transaction_encoding.py index 44226a7cf5..df1e8d0b4e 100644 --- a/tests/core/transaction-utils/test_transaction_encoding.py +++ b/tests/core/transaction-utils/test_transaction_encoding.py @@ -10,9 +10,13 @@ from eth.exceptions import UnrecognizedTransactionType from eth.vm.forks import ( BerlinVM, + LondonVM, ) -RECOGNIZED_TRANSACTION_TYPES = {1} +# Add recognized types here if any fork knows about it. Then, +# add manual unrecognized types for older forks. For example, +# (BerlinVM, to_bytes(2), UnrecognizedTransactionType) should be added explicitly. +RECOGNIZED_TRANSACTION_TYPES = {1, 2} UNRECOGNIZED_TRANSACTION_TYPES = tuple( (to_bytes(val), UnrecognizedTransactionType) @@ -27,7 +31,7 @@ ) -@pytest.mark.parametrize('vm_class', [BerlinVM]) +@pytest.mark.parametrize('vm_class', [BerlinVM, LondonVM]) @pytest.mark.parametrize( 'encoded, expected', ( @@ -69,6 +73,22 @@ def test_transaction_decode(vm_class, encoded, expected): assert decoded == expected_txn +@pytest.mark.parametrize( + 'vm_class, encoded, expected_failure', + ( + ( + BerlinVM, + to_bytes(2), + UnrecognizedTransactionType, + ), + ) +) +def test_transaction_decode_failure_by_vm(vm_class, encoded, expected_failure): + sedes = vm_class.get_transaction_builder() + with pytest.raises(expected_failure): + rlp.decode(encoded, sedes=sedes) + + @pytest.mark.parametrize('is_rlp_encoded', (True, False)) def test_EIP2930_transaction_decode(typed_txn_fixture, is_rlp_encoded): signed_txn = decode_hex(typed_txn_fixture['signed']) diff --git a/tests/core/vm/test_london.py b/tests/core/vm/test_london.py new file mode 100644 index 0000000000..7f7128961a --- /dev/null +++ b/tests/core/vm/test_london.py @@ -0,0 +1,81 @@ +import pytest + +from eth import constants +from eth.consensus.noproof import NoProofConsensus +from eth.chains.base import MiningChain +from eth.chains.mainnet import ( + MAINNET_VMS, +) +from eth.vm.forks import BerlinVM +from eth.tools.factories.transaction import ( + new_transaction +) + + +# VMs starting at London +@pytest.fixture(params=MAINNET_VMS[9:]) +def london_plus_miner(request, base_db, genesis_state): + klass = MiningChain.configure( + __name__='LondonAt1', + vm_configuration=( + ( + constants.GENESIS_BLOCK_NUMBER, + BerlinVM.configure(consensus_class=NoProofConsensus), + ), + ( + constants.GENESIS_BLOCK_NUMBER + 1, + request.param.configure(consensus_class=NoProofConsensus), + ), + ), + chain_id=1337, + ) + header_fields = dict( + difficulty=1, + gas_limit=21000 * 2, # block limit is hit with two transactions + ) + # On the first London+ block, it will double the block limit so that it + # can precisely hold 4 transactions. + return klass.from_genesis(base_db, header_fields, genesis_state) + + +@pytest.mark.parametrize( + 'num_txns, expected_base_fee', + ( + (0, 875000000), + (1, 937500000), + # base fee should stay stable at 1 gwei when block is exactly half full + (2, 1000000000), + (3, 1062500000), + (4, 1125000000), + ), +) +def test_base_fee_evolution( + london_plus_miner, funded_address, funded_address_private_key, num_txns, expected_base_fee): + chain = london_plus_miner + FOUR_TXN_GAS_LIMIT = 21000 * 4 + assert chain.header.gas_limit == FOUR_TXN_GAS_LIMIT + + vm = chain.get_vm() + txns = [ + new_transaction( + vm, + funded_address, + b'\x00' * 20, + private_key=funded_address_private_key, + gas=21000, + nonce=nonce, + ) + for nonce in range(num_txns) + ] + block_import, _, _ = chain.mine_all(txns, gas_limit=FOUR_TXN_GAS_LIMIT) + mined_header = block_import.imported_block.header + assert mined_header.gas_limit == FOUR_TXN_GAS_LIMIT + assert mined_header.gas_used == 21000 * num_txns + assert mined_header.base_fee_per_gas == 10 ** 9 # Initialize at 1 gwei + + block_import, _, _ = chain.mine_all([], gas_limit=FOUR_TXN_GAS_LIMIT) + mined_header = block_import.imported_block.header + assert mined_header.gas_limit == FOUR_TXN_GAS_LIMIT + assert mined_header.gas_used == 0 + # Check that the base fee evolved correctly, depending on how much gas was used in the parent + assert mined_header.base_fee_per_gas == expected_base_fee diff --git a/tests/core/vm/test_rewards.py b/tests/core/vm/test_rewards.py index bb3d006ea3..45128b56b9 100644 --- a/tests/core/vm/test_rewards.py +++ b/tests/core/vm/test_rewards.py @@ -21,8 +21,11 @@ tangerine_whistle_at, constantinople_at, petersburg_at, + berlin_at, + london_at, genesis, ) +from eth.tools.factories.transaction import new_dynamic_fee_transaction @pytest.mark.parametrize( @@ -311,3 +314,53 @@ def test_rewards_nephew_uncle_different_vm( # But we also ensure the balance matches the numbers that we calculated on paper assert coinbase_balance == to_wei(miner_1_balance, 'ether') assert other_miner_balance == to_wei(miner_2_balance, 'ether') + + +@pytest.mark.parametrize( + 'max_total_price, max_priority_price, expected_miner_tips', + ( + # none of the tip makes it to the miner when the base price matches the txn max price + (10 ** 9, 1, 0), + # half of this tip makes it to the miner because the base price squeezes the tip + (10 ** 9 + 1, 2, 21000), + # the full tip makes it to the miner because the txn max price is exactly big enough + (10 ** 9 + 1, 1, 21000), + # the full tip makes it to the miner, and no more, because the txn max + # price is larger than the sum of the base burn fee and the max tip + (10 ** 9 + 2, 1, 21000), + ), +) +def test_eip1559_txn_rewards( + max_total_price, + max_priority_price, + expected_miner_tips, + funded_address, + funded_address_private_key): + + chain = build( + MiningChain, + berlin_at(0), + london_at(1), # Start London at block one to get easy 1gwei base fee + disable_pow_check(), + genesis( + params=dict(gas_limit=10**7), + state={funded_address: dict(balance=10**20)}, + ), + ) + vm = chain.get_vm() + txn = new_dynamic_fee_transaction( + vm, + from_=funded_address, + to=funded_address, + private_key=funded_address_private_key, + max_priority_fee_per_gas=max_priority_price, + max_fee_per_gas=max_total_price, + ) + + MINER = b'\x0f' * 20 + original_balance = vm.state.get_balance(MINER) + chain.mine_all([txn], coinbase=MINER) + new_balance = chain.get_vm().state.get_balance(MINER) + + BLOCK_REWARD = 2 * (10 ** 18) + assert original_balance + BLOCK_REWARD + expected_miner_tips == new_balance diff --git a/tests/core/vm/test_validate_transaction.py b/tests/core/vm/test_validate_transaction.py new file mode 100644 index 0000000000..2571127bc9 --- /dev/null +++ b/tests/core/vm/test_validate_transaction.py @@ -0,0 +1,90 @@ +from eth_utils import ValidationError +import pytest + +from eth._utils.address import force_bytes_to_address +from eth.chains.base import MiningChain +from eth.constants import GAS_TX +from eth.tools.factories.transaction import new_dynamic_fee_transaction +from eth.vm.forks import LondonVM + + +@pytest.fixture +def london_plus_miner(chain_without_block_validation): + if not isinstance(chain_without_block_validation, MiningChain): + pytest.skip("This test is only meant to run with mining capability") + return + + valid_vms = ( + LondonVM, + ) + vm = chain_without_block_validation.get_vm() + if isinstance(vm, valid_vms): + return chain_without_block_validation + else: + pytest.skip("This test is not meant to run on pre-London VMs") + + +ADDRESS_A = force_bytes_to_address(b'\x10\x10') + + +def test_transaction_cost_valid(london_plus_miner, funded_address, funded_address_private_key): + chain = london_plus_miner + vm = chain.get_vm() + base_fee_per_gas = vm.get_header().base_fee_per_gas + # Make sure we're testing an interesting case + assert base_fee_per_gas > 0 + + account_balance = vm.state.get_balance(funded_address) + + tx = new_dynamic_fee_transaction( + vm, + from_=funded_address, + to=ADDRESS_A, + private_key=funded_address_private_key, + gas=GAS_TX, + amount=account_balance - base_fee_per_gas * GAS_TX, + max_priority_fee_per_gas=1, + max_fee_per_gas=base_fee_per_gas, + ) + + # sanity check + assert vm.get_header().gas_used == 0 + + # There should be no validation failure when applying the transaction + chain.apply_transaction(tx) + + # sanity check: make sure the transaction actually got applied + assert chain.get_vm().get_header().gas_used > 0 + + +def test_transaction_cost_invalid(london_plus_miner, funded_address, funded_address_private_key): + chain = london_plus_miner + vm = chain.get_vm() + base_fee_per_gas = vm.get_header().base_fee_per_gas + # Make sure we're testing an interesting case + assert base_fee_per_gas > 0 + + account_balance = vm.state.get_balance(funded_address) + + tx = new_dynamic_fee_transaction( + vm, + from_=funded_address, + to=ADDRESS_A, + private_key=funded_address_private_key, + gas=GAS_TX, + amount=account_balance - base_fee_per_gas * GAS_TX + 1, + max_priority_fee_per_gas=1, + max_fee_per_gas=base_fee_per_gas, + ) + + # sanity check + assert vm.get_header().gas_used == 0 + + # The *validation* step should catch that the sender does not have enough funds. If validation + # misses the problem, then we might see an InsufficientFunds, because the VM will think the + # transaction is fine, then attempt to execute it, then then run out of funds. + with pytest.raises(ValidationError): + chain.apply_transaction(tx) + + # sanity check: make sure the transaction does not get applied + assert chain.get_vm().get_header().gas_used == 0 diff --git a/tests/core/vm/test_vm.py b/tests/core/vm/test_vm.py index 00a0f59cf8..8be14c4f9e 100644 --- a/tests/core/vm/test_vm.py +++ b/tests/core/vm/test_vm.py @@ -17,21 +17,27 @@ @pytest.fixture(params=MAINNET_VMS) -def pow_consensus_chain(request): +def vm_class(request): + return request.param + + +@pytest.fixture +def pow_consensus_chain(vm_class): return api.build( MiningChain, - api.fork_at(request.param, 0), + api.fork_at(vm_class, 0), api.genesis(), ) -@pytest.fixture(params=MAINNET_VMS) -def noproof_consensus_chain(request): +@pytest.fixture +def noproof_consensus_chain(vm_class): + # This will always have the same vm configuration as the POW chain return api.build( MiningChain, - api.fork_at(request.param, 0), + api.fork_at(vm_class, 0), api.disable_pow_check(), - api.genesis(), + api.genesis(params=dict(gas_limit=100000)), ) @@ -100,7 +106,7 @@ def test_import_block(chain, funded_address, funded_address_private_key): def test_validate_header_succeeds_but_pow_fails(pow_consensus_chain, noproof_consensus_chain): - # Create to "structurally valid" blocks that are not backed by PoW + # Create two "structurally valid" blocks that are not backed by PoW block1 = noproof_consensus_chain.mine_block() block2 = noproof_consensus_chain.mine_block() @@ -115,10 +121,63 @@ def test_validate_header_succeeds_but_pow_fails(pow_consensus_chain, noproof_con def test_validate_header_fails_on_invalid_parent(noproof_consensus_chain): block1 = noproof_consensus_chain.mine_block() - noproof_consensus_chain.mine_block() - block3 = noproof_consensus_chain.mine_block() + block2 = noproof_consensus_chain.mine_block() - vm = noproof_consensus_chain.get_vm(block3.header) + vm = noproof_consensus_chain.get_vm(block2.header) with pytest.raises(ValidationError, match="Blocks must be numbered consecutively"): - vm.validate_header(block3.header, block1.header) + vm.validate_header(block2.header.copy(block_number=3), block1.header) + + +def test_validate_gas_limit_almost_too_low(noproof_consensus_chain): + block1 = noproof_consensus_chain.mine_block() + block2 = noproof_consensus_chain.mine_block() + + max_reduction = block1.header.gas_limit // constants.GAS_LIMIT_ADJUSTMENT_FACTOR + barely_valid_low_gas_limit = block1.header.gas_limit - max_reduction + barely_valid_header = block2.header.copy(gas_limit=barely_valid_low_gas_limit) + + vm = noproof_consensus_chain.get_vm(block2.header) + + vm.validate_header(barely_valid_header, block1.header) + + +def test_validate_gas_limit_too_low(noproof_consensus_chain): + block1 = noproof_consensus_chain.mine_block() + block2 = noproof_consensus_chain.mine_block() + + max_reduction = block1.header.gas_limit // constants.GAS_LIMIT_ADJUSTMENT_FACTOR + invalid_low_gas_limit = block1.header.gas_limit - max_reduction - 1 + invalid_header = block2.header.copy(gas_limit=invalid_low_gas_limit) + + vm = noproof_consensus_chain.get_vm(block2.header) + + with pytest.raises(ValidationError, match="[Gg]as limit"): + vm.validate_header(invalid_header, block1.header) + + +def test_validate_gas_limit_almost_too_high(noproof_consensus_chain): + block1 = noproof_consensus_chain.mine_block() + block2 = noproof_consensus_chain.mine_block() + + max_increase = block1.header.gas_limit // constants.GAS_LIMIT_ADJUSTMENT_FACTOR + barely_valid_high_gas_limit = block1.header.gas_limit + max_increase + barely_valid_header = block2.header.copy(gas_limit=barely_valid_high_gas_limit) + + vm = noproof_consensus_chain.get_vm(block2.header) + + vm.validate_header(barely_valid_header, block1.header) + + +def test_validate_gas_limit_too_high(noproof_consensus_chain): + block1 = noproof_consensus_chain.mine_block() + block2 = noproof_consensus_chain.mine_block() + + max_increase = block1.header.gas_limit // constants.GAS_LIMIT_ADJUSTMENT_FACTOR + invalid_high_gas_limit = block1.header.gas_limit + max_increase + 1 + invalid_header = block2.header.copy(gas_limit=invalid_high_gas_limit) + + vm = noproof_consensus_chain.get_vm(block2.header) + + with pytest.raises(ValidationError, match="[Gg]as limit"): + vm.validate_header(invalid_header, block1.header) diff --git a/tests/database/test_eth1_chaindb.py b/tests/database/test_eth1_chaindb.py index 68ccaafa50..641f7405d1 100644 --- a/tests/database/test_eth1_chaindb.py +++ b/tests/database/test_eth1_chaindb.py @@ -41,6 +41,7 @@ ) from eth.vm.forks import ( BerlinVM, + LondonVM, ) from eth.vm.forks.frontier.blocks import ( FrontierBlock, @@ -274,7 +275,13 @@ def test_chaindb_get_score(chaindb): assert genesis_score == 1 assert chaindb.get_score(genesis.hash) == 1 - block1 = BlockHeader(difficulty=10, block_number=1, gas_limit=0, parent_hash=genesis.hash) + block1 = BlockHeader( + difficulty=10, + block_number=1, + gas_limit=0, + parent_hash=genesis.hash, + timestamp=genesis.timestamp + 1, + ) chaindb.persist_header(block1) block1_score_key = SchemaV1.make_block_hash_to_score_lookup_key(block1.hash) @@ -377,7 +384,7 @@ def mine_blocks_with_access_list_receipts( funded_address_private_key): current_vm = chain.get_vm() - if not isinstance(current_vm, BerlinVM): + if not isinstance(current_vm, (BerlinVM, LondonVM)): pytest.skip("{current_vm} does not support typed transactions") for _ in range(num_blocks): diff --git a/tests/database/test_header_db.py b/tests/database/test_header_db.py index 877c73465a..75068b3656 100644 --- a/tests/database/test_header_db.py +++ b/tests/database/test_header_db.py @@ -45,8 +45,11 @@ ParentNotFound, ) from eth.db.header import HeaderDB -from eth.rlp.headers import ( - BlockHeader, +from eth.vm.forks.london import ( + LondonVM, +) +from eth.vm.forks.london.blocks import ( + LondonBlockHeader as BlockHeader, ) from eth.tools.rlp import ( assert_headers_eq, @@ -71,8 +74,10 @@ def genesis_header(): def mk_header_chain(base_header, length): previous_header = base_header for _ in range(length): - next_header = BlockHeader.from_parent( - parent=previous_header, + # TODO test a variety of chain configs, where transitions to london + # happen during "interesting" times of the tests + next_header = LondonVM.create_header_from_parent( + previous_header, timestamp=previous_header.timestamp + 1, gas_limit=previous_header.gas_limit, difficulty=previous_header.difficulty, @@ -788,6 +793,7 @@ def test_headerdb_persist_header_disallows_unknown_parent(headerdb): block_number=GENESIS_BLOCK_NUMBER, gas_limit=GENESIS_GAS_LIMIT, parent_hash=b'\x0f' * 32, + timestamp=0, ) with pytest.raises(ParentNotFound, match="unknown parent"): headerdb.persist_header(header) diff --git a/tests/json-fixtures/blockchain/test_blockchain.py b/tests/json-fixtures/blockchain/test_blockchain.py index 107d764716..150a5a77c2 100644 --- a/tests/json-fixtures/blockchain/test_blockchain.py +++ b/tests/json-fixtures/blockchain/test_blockchain.py @@ -8,12 +8,7 @@ ValidationError, ) -from eth.rlp.headers import ( - BlockHeader, -) - from eth.tools.rlp import ( - assert_imported_genesis_header_unchanged, assert_mined_block_unchanged, ) from eth.tools._utils.normalization import ( @@ -23,7 +18,7 @@ apply_fixture_block_to_chain, filter_fixtures, generate_fixture_tests, - genesis_params_from_fixture, + genesis_fields_from_fixture, load_fixture, new_chain_from_fixture, should_run_slow_tests, @@ -315,23 +310,31 @@ def fixture(fixture_data): return fixture +def assert_imported_genesis_header_unchanged(genesis_fields, genesis_header): + for field, expected_val in genesis_fields.items(): + actual_val = getattr(genesis_header, field) + if actual_val != expected_val: + raise ValidationError( + f"Genesis header field {field} doesn't match {expected_val}, was {actual_val}" + ) + + def test_blockchain_fixtures(fixture_data, fixture): try: chain = new_chain_from_fixture(fixture) except ValueError as e: raise AssertionError(f"could not load chain for {fixture_data}") from e - genesis_params = genesis_params_from_fixture(fixture) - expected_genesis_header = BlockHeader(**genesis_params) - # TODO: find out if this is supposed to pass? # if 'genesisRLP' in fixture: # assert rlp.encode(genesis_header) == fixture['genesisRLP'] + genesis_fields = genesis_fields_from_fixture(fixture) + genesis_block = chain.get_canonical_block_by_number(0) genesis_header = genesis_block.header - assert_imported_genesis_header_unchanged(expected_genesis_header, genesis_header) + assert_imported_genesis_header_unchanged(genesis_fields, genesis_header) # 1 - mine the genesis block # 2 - loop over blocks: