From 8070f2d9de08b7a7975738589bd15ffd204fe586 Mon Sep 17 00:00:00 2001 From: Francesco Truzzi Date: Thu, 29 Apr 2021 22:47:16 +0200 Subject: [PATCH 01/10] WIP add support for EIP-1559 --- eth/abc.py | 2 + eth/vm/base.py | 1 + eth/vm/forks/__init__.py | 3 + eth/vm/forks/berlin/receipts.py | 4 +- eth/vm/forks/london/__init__.py | 113 +++++++++++++ eth/vm/forks/london/blocks.py | 211 +++++++++++++++++++++++ eth/vm/forks/london/computation.py | 11 ++ eth/vm/forks/london/constants.py | 12 ++ eth/vm/forks/london/receipts.py | 24 +++ eth/vm/forks/london/state.py | 23 +++ eth/vm/forks/london/transactions.py | 248 ++++++++++++++++++++++++++++ 11 files changed, 651 insertions(+), 1 deletion(-) create mode 100644 eth/vm/forks/london/__init__.py create mode 100644 eth/vm/forks/london/blocks.py create mode 100644 eth/vm/forks/london/computation.py create mode 100644 eth/vm/forks/london/constants.py create mode 100644 eth/vm/forks/london/receipts.py create mode 100644 eth/vm/forks/london/state.py create mode 100644 eth/vm/forks/london/transactions.py diff --git a/eth/abc.py b/eth/abc.py index b7ebada2bb..5d117682f1 100644 --- a/eth/abc.py +++ b/eth/abc.py @@ -74,6 +74,8 @@ class MiningHeaderAPI(ABC): gas_used: int timestamp: int extra_data: bytes + gas_target: int # EIP-1559 + base_fee_per_gas: int # EIP-1559 @property @abstractmethod diff --git a/eth/vm/base.py b/eth/vm/base.py index 7fda65c556..ee17488f78 100644 --- a/eth/vm/base.py +++ b/eth/vm/base.py @@ -590,6 +590,7 @@ def validate_header(cls, validate_length_lte( header.extra_data, cls.extra_data_max_bytes, title="BlockHeader.extra_data") + # TODO skip for EIP-1559, or override this whole function? validate_gas_limit(header.gas_limit, parent_header.gas_limit) if header.block_number != parent_header.block_number + 1: 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..8ab3387fb7 100644 --- a/eth/vm/forks/berlin/receipts.py +++ b/eth/vm/forks/berlin/receipts.py @@ -43,6 +43,7 @@ class TypedReceipt(ReceiptAPI, ReceiptDecoderAPI): type_id: int rlp_type = Binary(min_length=1) # must have at least one byte for the type _inner: ReceiptAPI + codecs = TYPED_RECEIPT_BODY_CODECS def __init__(self, type_id: int, proxy_target: ReceiptAPI) -> None: self.type_id = type_id @@ -124,6 +125,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 +133,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/london/__init__.py b/eth/vm/forks/london/__init__.py new file mode 100644 index 0000000000..d3e731a0ad --- /dev/null +++ b/eth/vm/forks/london/__init__.py @@ -0,0 +1,113 @@ +from eth_utils.exceptions import ValidationError +from eth.vm.forks.london.constants import ( + BASE_FEE_MAX_CHANGE_DENOMINATOR, + ELASTICITY_MULTIPLIER +) +from typing import Type + +from eth.abc import BlockAPI, BlockHeaderAPI +from eth.rlp.blocks import BaseBlock +from eth._utils.db import get_parent_header +from eth.vm.forks.berlin import BerlinVM +from eth.vm.state import BaseState + +from .blocks import LondonBlock +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_berlin_header_from_parent) # type: ignore + # compute_difficulty = staticmethod(compute_berlin_difficulty) # type: ignore + # configure_header = configure_berlin_header + + # @staticmethod + # def make_receipt( + # base_header: BlockHeaderAPI, + # transaction: SignedTransactionAPI, + # computation: ComputationAPI, + # state: StateAPI) -> ReceiptAPI: + + # gas_used = base_header.gas_used + finalize_gas_used(transaction, computation) + + # if computation.is_error: + # status_code = EIP658_TRANSACTION_STATUS_CODE_FAILURE + # else: + # status_code = EIP658_TRANSACTION_STATUS_CODE_SUCCESS + + # return transaction.make_receipt(status_code, gas_used, computation.get_log_entries()) + + @staticmethod + def calculate_expected_base_fee_per_gas(parent_header: BlockHeaderAPI) -> int: + parent_base_fee_per_gas = parent_header.base_fee_per_gas + parent_gas_target = parent_header.gas_target + 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_base_fee_per_gas + base_fee_per_gas_delta = max( + ( + parent_base_fee_per_gas * gas_used_delta // \ + 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 + base_fee_per_gas_delta = parent_base_fee_per_gas * gas_used_delta \ + // parent_gas_target // BASE_FEE_MAX_CHANGE_DENOMINATOR + return max(parent_base_fee_per_gas - base_fee_per_gas_delta, 0) + + @classmethod + def validate_header(cls, + header: BlockHeaderAPI, + parent_header: BlockHeaderAPI) -> None: + + parent_gas_target = parent_header.gas_target + + max_usable_gas = header.gas_target * ELASTICITY_MULTIPLIER + if header.gas_used > max_usable_gas: + raise ValidationError( + f"Block used too much gas: {header.gas_used} " + f"(max: {max_usable_gas})" + ) + + if header.gas_target > parent_gas_target + (parent_gas_target // 1024): + raise ValidationError( + f"Gas target increased too much (from {parent_gas_target} " + f"to {header.gas_target})" + ) + + if header.gas_target < parent_gas_target - (parent_gas_target // 1024): + raise ValidationError( + f"Gas target decreased too much (from {parent_gas_target} " + f"to {header.gas_target})" + ) + + expected_base_fee_per_gas = LondonVM.calculate_expected_base_fee_per_gas(parent_header) + if expected_base_fee_per_gas != header.base_fee_per_gas: + raise ValidationError( + f"Incorrect base fee per gas (got {header.base_fee_per_gas}" + f", expected {expected_base_fee_per_gas})" + ) + + # TODO continue validation + + def validate_block(self, block: BlockAPI) -> None: + header = block.header + parent_header = get_parent_header(block.header, self.chaindb) + LondonVM.validate_header(header, parent_header) + + # return super().validate_block(block) diff --git a/eth/vm/forks/london/blocks.py b/eth/vm/forks/london/blocks.py new file mode 100644 index 0000000000..9def3101c2 --- /dev/null +++ b/eth/vm/forks/london/blocks.py @@ -0,0 +1,211 @@ +import time + +from typing import ( + Dict, + Type, +) + +import rlp + +from rlp.sedes import ( + Binary, + CountableList, + big_endian_int, + binary +) + +from eth.abc import ( + BlockHeaderAPI, + 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.sedes import ( + address, + hash32, + trie_root, + uint256, +) +from eth.typing import HeaderParams +from eth.vm.forks.berlin.blocks import ( + BerlinBlock, +) +from eth_hash.auto import keccak + +from eth_typing import ( + BlockNumber, +) +from eth_typing.evm import ( + Address, + Hash32 +) +from eth_utils import ( + encode_hex, +) + +from .receipts import ( + LondonReceiptBuilder, +) +from .transactions import ( + LondonTransactionBuilder, +) + + +class LondonMiningHeader(rlp.Serializable, MiningHeaderAPI): + 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_target', big_endian_int), + ('gas_used', big_endian_int), + ('timestamp', big_endian_int), + ('extra_data', binary), + ('base_fee_per_gas', big_endian_int), + ] + + +class LondonBlockHeader(rlp.Serializable, BlockHeaderAPI): + 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_target', big_endian_int), + ('gas_used', big_endian_int), + ('timestamp', big_endian_int), + ('extra_data', binary), + ('mix_hash', binary), + ('nonce', Binary(8, allow_empty=True)), + ('base_fee_per_gas', big_endian_int), + ] + + def __init__(self, # type: ignore # noqa: F811 + difficulty: int, + block_number: BlockNumber, + gas_target: 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: + timestamp = int(time.time()) + 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_target=gas_target, + 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 self._hash + + @property + def mining_hash(self) -> Hash32: + return keccak(rlp.encode(self[:-2], LondonMiningHeader)) + + @property + def hex_hash(self) -> str: + return encode_hex(self.hash) + + @classmethod + def from_parent(cls, + parent: 'LondonBlockHeader', + gas_target: int, + difficulty: int, + timestamp: int, + coinbase: Address = ZERO_ADDRESS, + base_fee_per_gas: int = 0, + nonce: bytes = None, + extra_data: bytes = None, + transaction_root: bytes = None, + receipt_root: bytes = None) -> 'LondonBlockHeader': + """ + 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_target': gas_target, + 'base_fee_per_gas': base_fee_per_gas, + '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 + + +class LondonBlock(BerlinBlock): + transaction_builder: Type[TransactionBuilderAPI] = LondonTransactionBuilder # type: ignore + receipt_builder: Type[ReceiptBuilderAPI] = LondonReceiptBuilder # type: ignore + fields = [ + ('header', LondonBlockHeader), + ('transactions', CountableList(transaction_builder)), + ('uncles', CountableList(LondonBlockHeader)) + ] 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..d4a33d9ed7 --- /dev/null +++ b/eth/vm/forks/london/constants.py @@ -0,0 +1,12 @@ +from eth.vm.forks.berlin.constants import ( + ACCESS_LIST_ADDRESS_COST_EIP_2930, + ACCESS_LIST_STORAGE_KEY_COST_EIP_2930, +) + +# EIP 1559 +BASE_GAS_FEE_TRANSACTION_TYPE = 2 +BASE_GAS_FEE_ADDRESS_COST = ACCESS_LIST_ADDRESS_COST_EIP_2930 +BASE_GAS_FEE_STORAGE_KEY_COST = ACCESS_LIST_STORAGE_KEY_COST_EIP_2930 + +BASE_FEE_MAX_CHANGE_DENOMINATOR = 8 +ELASTICITY_MULTIPLIER = 2 diff --git a/eth/vm/forks/london/receipts.py b/eth/vm/forks/london/receipts.py new file mode 100644 index 0000000000..61d9eb88ed --- /dev/null +++ b/eth/vm/forks/london/receipts.py @@ -0,0 +1,24 @@ +from typing import ( + Dict, + Type, +) + +from eth.rlp.receipts import ( + Receipt, + ReceiptAPI, +) +from eth.vm.forks.berlin.receipts import ( + BerlinReceiptBuilder +) +from eth.vm.forks.berlin.constants import ( + ACCESS_LIST_TRANSACTION_TYPE, +) + +from .constants import BASE_GAS_FEE_TRANSACTION_TYPE + + +class LondonReceiptBuilder(BerlinReceiptBuilder): + codecs: Dict[int, Type[ReceiptAPI]] = { + ACCESS_LIST_TRANSACTION_TYPE: Receipt, + BASE_GAS_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..14e1d5b2ba --- /dev/null +++ b/eth/vm/forks/london/state.py @@ -0,0 +1,23 @@ +from typing import Type + +from eth.abc import ( + ComputationAPI, + MessageAPI, + SignedTransactionAPI, + TransactionExecutorAPI, +) +from eth.vm.forks.berlin.state import ( + BerlinState, + BerlinTransactionExecutor, +) + +from .computation import LondonComputation + + +class LondonTransactionExecutor(BerlinTransactionExecutor): + pass + + +class LondonState(BerlinState): + computation_class = LondonComputation + transaction_executor_class: Type[TransactionExecutorAPI] = LondonTransactionExecutor diff --git a/eth/vm/forks/london/transactions.py b/eth/vm/forks/london/transactions.py new file mode 100644 index 0000000000..caa6a7e8b8 --- /dev/null +++ b/eth/vm/forks/london/transactions.py @@ -0,0 +1,248 @@ +from functools import cached_property +from typing import ( + Dict, + Sequence, + Tuple, + Type, +) +from eth_keys.datatypes import PrivateKey + +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_TRANSACTION_TYPE +) +from eth.vm.forks.berlin.transactions import ( + AccessListPayloadDecoder, + AccountAccesses, + BerlinLegacyTransaction, + BerlinTransactionBuilder, + TypedTransaction, +) +from eth._utils.transactions import ( + extract_transaction_sender, + validate_transaction_signature, +) + + +import rlp +from eth_typing import ( + Address, + Hash32, +) +from eth_utils import ( + to_bytes, +) +from rlp.sedes import ( + CountableList, + big_endian_int, + binary, +) + +from .constants import BASE_GAS_FEE_TRANSACTION_TYPE + + +class LondonLegacyTransaction(BerlinLegacyTransaction): + pass + + +class UnsignedBaseGasFeeTransaction(rlp.Serializable): + _type_id = BASE_GAS_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 = BaseGasFeeTransaction( + 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 BaseGasFeeTransaction(rlp.Serializable, SignedTransactionMethods, SignedTransactionAPI): + _type_id = BASE_GAS_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), + ] + + def get_sender(self) -> Address: + return extract_transaction_sender(self) + + def get_message_for_signing(self) -> bytes: + unsigned = UnsignedBaseGasFeeTransaction( + 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: + # TODO figure out + pass + + 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 BaseGasFeePayloadDecoder(TransactionDecoderAPI): + @classmethod + def decode(cls, payload: bytes) -> SignedTransactionAPI: + return rlp.decode(payload, sedes=BaseGasFeeTransaction) + + +class LondonTypedTransaction(TypedTransaction): + decoders: Dict[int, Type[TransactionDecoderAPI]] = { + ACCESS_LIST_TRANSACTION_TYPE: AccessListPayloadDecoder, + BASE_GAS_FEE_TRANSACTION_TYPE: BaseGasFeePayloadDecoder, + } + + @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 + + +class LondonTransactionBuilder(BerlinTransactionBuilder): + @classmethod + def new_unsigned_base_gas_price_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 = UnsignedBaseGasFeeTransaction( + chain_id, + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas, + to, + value, + data, + access_list, + ) + return transaction + + @classmethod + def new_base_gas_price_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 = BaseGasFeeTransaction( + chain_id, + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas, + to, + value, + data, + access_list, + y_parity, + r, + s, + ) + return LondonTypedTransaction(BASE_GAS_FEE_TRANSACTION_TYPE, transaction) From c0e67ff51740657c110aa23172684d1d07dee363 Mon Sep 17 00:00:00 2001 From: Francesco Truzzi Date: Wed, 5 May 2021 00:49:38 +0200 Subject: [PATCH 02/10] add initial EIP-1559 transaction validation --- eth/abc.py | 2 +- eth/validation.py | 2 +- eth/vm/forks/__init__.py | 6 +-- eth/vm/forks/london/__init__.py | 29 ++++++---- eth/vm/forks/london/state.py | 44 ++++++++++++++- eth/vm/forks/london/transactions.py | 84 +++++++++++++++++++++++++++-- eth/vm/forks/london/validation.py | 69 ++++++++++++++++++++++++ 7 files changed, 218 insertions(+), 18 deletions(-) create mode 100644 eth/vm/forks/london/validation.py diff --git a/eth/abc.py b/eth/abc.py index 5d117682f1..6d31de2871 100644 --- a/eth/abc.py +++ b/eth/abc.py @@ -335,7 +335,7 @@ def nonce(self) -> int: @property @abstractmethod - def gas_price(self) -> int: + def gas_price(self) -> Optional[int]: ... @property diff --git a/eth/validation.py b/eth/validation.py index 83f5cfdc41..48cfbaee44 100644 --- a/eth/validation.py +++ b/eth/validation.py @@ -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}" ) diff --git a/eth/vm/forks/__init__.py b/eth/vm/forks/__init__.py index 742b3c0d4d..fcbee07060 100644 --- a/eth/vm/forks/__init__.py +++ b/eth/vm/forks/__init__.py @@ -28,6 +28,6 @@ from .berlin import ( # noqa: F401 BerlinVM, ) -from .london import ( # noqa: F401 - LondonVM -) +# from .london import ( # noqa: F401 +# LondonVM +# ) diff --git a/eth/vm/forks/london/__init__.py b/eth/vm/forks/london/__init__.py index d3e731a0ad..856204d62a 100644 --- a/eth/vm/forks/london/__init__.py +++ b/eth/vm/forks/london/__init__.py @@ -1,19 +1,20 @@ -from eth_utils.exceptions import ValidationError -from eth.vm.forks.london.constants import ( - BASE_FEE_MAX_CHANGE_DENOMINATOR, - ELASTICITY_MULTIPLIER -) -from typing import Type +from eth.vm.forks.london.transactions import LondonTypedTransaction +from typing import Tuple, Type -from eth.abc import BlockAPI, BlockHeaderAPI -from eth.rlp.blocks import BaseBlock from eth._utils.db import get_parent_header +from eth.abc import BlockAPI, BlockHeaderAPI, ComputationAPI, ReceiptAPI, SignedTransactionAPI +from eth_utils.exceptions import ValidationError +from eth.rlp.blocks import BaseBlock from eth.vm.forks.berlin import BerlinVM from eth.vm.state import BaseState from .blocks import LondonBlock +from .constants import ( + BASE_FEE_MAX_CHANGE_DENOMINATOR, + ELASTICITY_MULTIPLIER +) from .state import LondonState - +from .validation import validate_london_transaction_against_header class LondonVM(BerlinVM): # fork name @@ -24,6 +25,7 @@ class LondonVM(BerlinVM): _state_class: Type[BaseState] = LondonState # Methods + validate_transaction_against_header = validate_london_transaction_against_header # create_header_from_parent = staticmethod(create_berlin_header_from_parent) # type: ignore # compute_difficulty = staticmethod(compute_berlin_difficulty) # type: ignore # configure_header = configure_berlin_header @@ -44,6 +46,15 @@ class LondonVM(BerlinVM): # return transaction.make_receipt(status_code, gas_used, computation.get_log_entries()) + def apply_transaction( + self, + header: BlockHeaderAPI, + transaction: SignedTransactionAPI, + ) -> Tuple[ReceiptAPI, ComputationAPI]: + + self.state.lock_changes() + computation = self.state.apply_transaction(transaction, header=) + @staticmethod def calculate_expected_base_fee_per_gas(parent_header: BlockHeaderAPI) -> int: parent_base_fee_per_gas = parent_header.base_fee_per_gas diff --git a/eth/vm/forks/london/state.py b/eth/vm/forks/london/state.py index 14e1d5b2ba..ad60bf8a5e 100644 --- a/eth/vm/forks/london/state.py +++ b/eth/vm/forks/london/state.py @@ -1,23 +1,65 @@ +from eth_utils.exceptions import ValidationError +from eth.vm.forks.london.blocks import LondonBlockHeader from typing import Type from eth.abc import ( + AccountDatabaseAPI, ComputationAPI, MessageAPI, SignedTransactionAPI, TransactionExecutorAPI, ) +from eth.constants import ( + SECPK1_N, +) +from eth.db.account import ( + AccountDB +) from eth.vm.forks.berlin.state import ( BerlinState, BerlinTransactionExecutor, ) from .computation import LondonComputation +from .transactions import normalize_transaction +from .validation import validate_london_normalized_transaction class LondonTransactionExecutor(BerlinTransactionExecutor): - pass + def __call__(self, transaction: SignedTransactionAPI) -> ComputationAPI: + # unlike other VMs, don't validate tx here -- we need access to both header and state + message = self.build_evm_message(transaction) + computation = self.build_computation(message, transaction) + finalized_computation = self.finalize_computation(transaction, computation) + return finalized_computation class LondonState(BerlinState): + account_db_class: Type[AccountDatabaseAPI] = AccountDB computation_class = LondonComputation transaction_executor_class: Type[TransactionExecutorAPI] = LondonTransactionExecutor + + def apply_transaction( + self, + transaction: SignedTransactionAPI, + header: LondonBlockHeader + ) -> ComputationAPI: + + self.validate_transaction(transaction, header) + executor = self.get_transaction_executor() + return executor(transaction) + + def validate_transaction( + self, + transaction: SignedTransactionAPI, + header: LondonBlockHeader + ) -> None: + + # homestead validation + if transaction.s > SECPK1_N // 2 or transaction.s == 0: + raise ValidationError("Invalid signature S value") + + normalized_transaction = normalize_transaction(transaction) + validate_london_normalized_transaction( + state=self, transaction=normalized_transaction, header=header + ) diff --git a/eth/vm/forks/london/transactions.py b/eth/vm/forks/london/transactions.py index caa6a7e8b8..36c713e54a 100644 --- a/eth/vm/forks/london/transactions.py +++ b/eth/vm/forks/london/transactions.py @@ -1,13 +1,17 @@ -from functools import cached_property +from eth.exceptions import InsufficientStack +from cached_property import cached_property from typing import ( Dict, Sequence, Tuple, Type, + Union, ) from eth_keys.datatypes import PrivateKey +from eth_utils.exceptions import ValidationError from eth.abc import ( + BaseTransactionAPI, ReceiptAPI, SignedTransactionAPI, TransactionDecoderAPI, @@ -17,13 +21,14 @@ from eth.rlp.transactions import SignedTransactionMethods from eth.rlp.sedes import address from eth.vm.forks.berlin.constants import ( - ACCESS_LIST_TRANSACTION_TYPE + ACCESS_LIST_TRANSACTION_TYPE, ) from eth.vm.forks.berlin.transactions import ( AccessListPayloadDecoder, AccountAccesses, BerlinLegacyTransaction, BerlinTransactionBuilder, + BerlinUnsignedLegacyTransaction, TypedTransaction, ) from eth._utils.transactions import ( @@ -31,6 +36,9 @@ validate_transaction_signature, ) +from .constants import ( + BASE_GAS_FEE_TRANSACTION_TYPE, +) import rlp from eth_typing import ( @@ -53,6 +61,37 @@ class LondonLegacyTransaction(BerlinLegacyTransaction): pass +class LondonUnsignedLegacyTransaction(BerlinUnsignedLegacyTransaction): + pass + + +class LondonNormalizedTransaction(BaseTransactionAPI): + """ + A normalized transaction, used for validation purposes + """ + def __init__(self, + signer_address: Address, + nonce: int, + gas: int, + max_priority_fee_per_gas: int, + max_fee_per_gas: int, + to: Address, + value: int, + data: bytes, + access: Sequence[Tuple[Address, Sequence[int]]]): + self.signer_address = signer_address + self.nonce = nonce + self.gas = gas + self.max_priority_fee_per_gas = max_priority_fee_per_gas + self.max_fee_per_gas = max_fee_per_gas + self.to = to + self.value = value + self.data = data + self.access = access + + # TODO maybe add properties and make the above variables private? + + class UnsignedBaseGasFeeTransaction(rlp.Serializable): _type_id = BASE_GAS_FEE_TRANSACTION_TYPE fields = [ @@ -114,6 +153,11 @@ class BaseGasFeeTransaction(rlp.Serializable, SignedTransactionMethods, SignedTr ('s', big_endian_int), ] + @property + def gas_price(self) -> None: + # maybe add a warning, or raise an exception instead? + return None + def get_sender(self) -> Address: return extract_transaction_sender(self) @@ -145,7 +189,7 @@ def hash(self) -> Hash32: def get_intrinsic_gas(self) -> int: # TODO figure out - pass + return 1 def encode(self) -> bytes: return rlp.encode(self) @@ -181,6 +225,9 @@ class LondonTypedTransaction(TypedTransaction): BASE_GAS_FEE_TRANSACTION_TYPE: BaseGasFeePayloadDecoder, } + def __init__(self, type_id: int, proxy_target: SignedTransactionAPI) -> None: + super().__init__(type_id, proxy_target) + @property def max_priority_fee_per_gas(self) -> int: return self._inner.max_priority_fee_per_gas @@ -246,3 +293,34 @@ def new_base_gas_price_transaction( s, ) return LondonTypedTransaction(BASE_GAS_FEE_TRANSACTION_TYPE, transaction) + + +def normalize_transaction( + transaction: Union[LondonLegacyTransaction, LondonTypedTransaction] + ) -> LondonNormalizedTransaction: + + # fields common to all transactions + fields = { + "signer_address": transaction.sender, + "nonce": transaction.nonce, + "gas": transaction.gas, + "to": transaction.to, + "value": transaction.value, + "data": transaction.data, + "access_list": [], + } + + if isinstance(transaction, (LondonLegacyTransaction, LondonTypedTransaction)): + fields["max_priority_fee_per_gas"] = transaction.gas_price + fields["max_fee_per_gas"] = transaction.gas_price + if isinstance(transaction, LondonTypedTransaction): + fields["access_list"] = transaction.access_list + if transaction.type_id == BASE_GAS_FEE_TRANSACTION_TYPE: + fields["max_priority_fee_per_gas"] = transaction.max_priority_fee_per_gas + fields["max_fee_per_gas"] = transaction.max_fee_per_gas + elif transaction.type_id != ACCESS_LIST_TRANSACTION_TYPE: + raise ValidationError(f"Invalid transaction type_id: {transaction.type_id}") + + return LondonNormalizedTransaction(**fields) + + raise ValidationError(f"Invalid transaction type: {type(transaction)}") diff --git a/eth/vm/forks/london/validation.py b/eth/vm/forks/london/validation.py new file mode 100644 index 0000000000..cb728d9157 --- /dev/null +++ b/eth/vm/forks/london/validation.py @@ -0,0 +1,69 @@ +from eth.vm.forks.london.blocks import LondonBlockHeader +from eth.abc import ( + SignedTransactionAPI, + StateAPI +) +from eth.constants import ( + CREATE_CONTRACT_ADDRESS, +) +from eth.validation import ( + validate_uint256, + validate_is_integer, + validate_is_bytes, + validate_lt_secpk1n, + validate_lt_secpk1n2, + validate_lte, + validate_gte, + validate_canonical_address, +) + +from eth_utils.exceptions import ValidationError + +from .transactions import LondonNormalizedTransaction, LondonTypedTransaction + + +def validate_london_normalized_transaction( + state: StateAPI, + transaction: LondonNormalizedTransaction, + header: LondonBlockHeader +) -> None: + if transaction.max_fee_per_gas < header.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 ({header.base_fee_per_gas})" + ) + + sender_balance = state.get_balance(transaction.sender) + if transaction.value > sender_balance: + raise ValidationError( + f"Sender {transaction.sender!r} cannot afford txn value" + f"{transaction.value} with account balance {sender_balance}" + ) + + priority_fee_per_gas = min( + transaction.max_priority_fee_per_gas, + transaction.max_fee_per_gas - header.base_fee_per_gas + ) + + effective_gas_price = priority_fee_per_gas + header.base_fee_per_gas + total_transaction_cost = transaction.value + effective_gas_price + + if sender_balance - total_transaction_cost < 0: + raise ValidationError( + f"Sender does not have enough balance to cover transaction value and gas " + f" (has {sender_balance}, needs {total_transaction_cost})" + ) + + +# def validate_london_normalized_transaction( +# state: StateAPI, +# transaction: LondonNormalizedTransaction +# ) -> None: +# sender_balance = state.get_balance(transaction.sender) +# if transaction.value > sender_balance: +# raise ValidationError( +# f"Sender {transaction.sender!r} cannot afford txn value" +# f"{transaction.value} with account balance {sender_balance}" +# ) + +# # TODO continue validation From 0b15249187d71bd8c73a25e93c588c85d2e58781 Mon Sep 17 00:00:00 2001 From: Francesco Truzzi Date: Fri, 7 May 2021 00:48:25 +0200 Subject: [PATCH 03/10] WIP tx execution --- eth/vm/forks/london/__init__.py | 6 +- eth/vm/forks/london/state.py | 157 +++++++++++++++++++-- eth/vm/forks/london/transaction_context.py | 6 + eth/vm/forks/london/transactions.py | 45 +++++- eth/vm/forks/london/validation.py | 55 ++++---- 5 files changed, 221 insertions(+), 48 deletions(-) create mode 100644 eth/vm/forks/london/transaction_context.py diff --git a/eth/vm/forks/london/__init__.py b/eth/vm/forks/london/__init__.py index 856204d62a..55e1dbe689 100644 --- a/eth/vm/forks/london/__init__.py +++ b/eth/vm/forks/london/__init__.py @@ -25,7 +25,7 @@ class LondonVM(BerlinVM): _state_class: Type[BaseState] = LondonState # Methods - validate_transaction_against_header = validate_london_transaction_against_header + # validate_transaction_against_header = validate_london_transaction_against_header # create_header_from_parent = staticmethod(create_berlin_header_from_parent) # type: ignore # compute_difficulty = staticmethod(compute_berlin_difficulty) # type: ignore # configure_header = configure_berlin_header @@ -53,7 +53,7 @@ def apply_transaction( ) -> Tuple[ReceiptAPI, ComputationAPI]: self.state.lock_changes() - computation = self.state.apply_transaction(transaction, header=) + computation = self.state.apply_transaction(transaction, header) @staticmethod def calculate_expected_base_fee_per_gas(parent_header: BlockHeaderAPI) -> int: @@ -114,8 +114,6 @@ def validate_header(cls, f", expected {expected_base_fee_per_gas})" ) - # TODO continue validation - def validate_block(self, block: BlockAPI) -> None: header = block.header parent_header = get_parent_header(block.header, self.chaindb) diff --git a/eth/vm/forks/london/state.py b/eth/vm/forks/london/state.py index ad60bf8a5e..945fab5342 100644 --- a/eth/vm/forks/london/state.py +++ b/eth/vm/forks/london/state.py @@ -1,7 +1,12 @@ -from eth_utils.exceptions import ValidationError -from eth.vm.forks.london.blocks import LondonBlockHeader +from eth.vm.forks.frontier.constants import REFUND_SELFDESTRUCT from typing import Type +from eth_hash.auto import keccak +from eth_utils.exceptions import ValidationError +from eth_utils import ( + encode_hex, +) + from eth.abc import ( AccountDatabaseAPI, ComputationAPI, @@ -10,29 +15,154 @@ TransactionExecutorAPI, ) from eth.constants import ( + CREATE_CONTRACT_ADDRESS, SECPK1_N, ) from eth.db.account import ( AccountDB ) +from eth.vm.message import ( + Message, +) from eth.vm.forks.berlin.state import ( BerlinState, BerlinTransactionExecutor, ) +from eth.vm.forks.london.blocks import ( + LondonBlockHeader, +) + +from eth._utils.address import ( + generate_contract_address, +) from .computation import LondonComputation -from .transactions import normalize_transaction -from .validation import validate_london_normalized_transaction +from .transactions import LondonNormalizedTransaction, LondonTypedTransaction, normalize_transaction +from .validation import LondonValidatedTransaction, validate_london_normalized_transaction class LondonTransactionExecutor(BerlinTransactionExecutor): - def __call__(self, transaction: SignedTransactionAPI) -> ComputationAPI: + def __call__( + self, + transaction: SignedTransactionAPI, + effective_gas_price: int + ) -> ComputationAPI: # unlike other VMs, don't validate tx here -- we need access to both header and state - message = self.build_evm_message(transaction) + message = self.build_evm_message(transaction, effective_gas_price) computation = self.build_computation(message, transaction) - finalized_computation = self.finalize_computation(transaction, computation) + finalized_computation = self.finalize_computation( + transaction, computation, effective_gas_price + ) + return finalized_computation + def build_evm_message( + self, + transaction: LondonValidatedTransaction + ) -> MessageAPI: + # Buy Gas + self.vm_state.delta_balance( + transaction.sender, + -1 * transaction.gas * transaction.effective_gas_price + ) + + # 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: sender: %s | to: %s | value: %s | gas: %s | " + "max_priority_fee_per_gas: %s | max_fee_per_gas: %s | s: %s | " + "r: %s | y_parity: %s | data-hash: %s" + ), + encode_hex(transaction.sender), + encode_hex(transaction.to), + transaction.value, + transaction.gas, + transaction.max_priority_fee_per_gas, + transaction.max_fee_per_gas, + transaction.s, + transaction.r, + transaction.y_parity, + 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 + + def finalize_computation( + self, + transaction: LondonValidatedTransaction, + computation: ComputationAPI + ) -> ComputationAPI: + # Self Destruct Refunds + num_deletions = len(computation.get_accounts_for_deletion()) + if num_deletions: + computation.refund_gas(REFUND_SELFDESTRUCT * num_deletions) + + # Gas Refunds + gas_remaining = computation.get_gas_remaining() + 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.effective_gas_price + + if gas_refund_amount: + self.vm_state.logger.debug2( + 'TRANSACTION REFUND: %s -> %s', + gas_refund_amount, + encode_hex(computation.msg.sender), + ) + + self.vm_state.delta_balance(computation.msg.sender, gas_refund_amount) + + # Miner Fees + transaction_fee = \ + (transaction.gas - gas_remaining - gas_refund) * transaction.priority_fee_per_gas + self.vm_state.logger.debug2( + 'TRANSACTION FEE: %s -> %s', + transaction_fee, + encode_hex(self.vm_state.coinbase), + ) + self.vm_state.delta_balance(self.vm_state.coinbase, transaction_fee) + + # Process Self Destructs + for account, _ in computation.get_accounts_for_deletion(): + # TODO: need to figure out how we prevent multiple selfdestructs from + # the same account and if this is the right place to put this. + self.vm_state.logger.debug2('DELETING ACCOUNT: %s', encode_hex(account)) + + # TODO: this balance setting is likely superflous and can be + # removed since `delete_account` does this. + self.vm_state.set_balance(account, 0) + self.vm_state.delete_account(account) + + + + return computation class LondonState(BerlinState): account_db_class: Type[AccountDatabaseAPI] = AccountDB @@ -45,21 +175,26 @@ def apply_transaction( header: LondonBlockHeader ) -> ComputationAPI: - self.validate_transaction(transaction, header) + validated_transaction = self.validate_transaction(transaction, header) executor = self.get_transaction_executor() - return executor(transaction) + return executor(validated_transaction) def validate_transaction( self, transaction: SignedTransactionAPI, header: LondonBlockHeader - ) -> None: + ) -> LondonValidatedTransaction: # homestead validation if transaction.s > SECPK1_N // 2 or transaction.s == 0: raise ValidationError("Invalid signature S value") normalized_transaction = normalize_transaction(transaction) - validate_london_normalized_transaction( + validated_transaction = validate_london_normalized_transaction( state=self, transaction=normalized_transaction, header=header ) + return validated_transaction + + def get_transaction_context(cls, + transaction: LondonNormalizedTransaction) -> TransactionContextAPI: + diff --git a/eth/vm/forks/london/transaction_context.py b/eth/vm/forks/london/transaction_context.py new file mode 100644 index 0000000000..26be6df5f7 --- /dev/null +++ b/eth/vm/forks/london/transaction_context.py @@ -0,0 +1,6 @@ +from eth.vm.forks.frontier.transaction_context import FrontierTransactionContext + + +class LondonTransactionContext(FrontierTransactionContext): + @property + def \ No newline at end of file diff --git a/eth/vm/forks/london/transactions.py b/eth/vm/forks/london/transactions.py index 36c713e54a..3c68a35985 100644 --- a/eth/vm/forks/london/transactions.py +++ b/eth/vm/forks/london/transactions.py @@ -1,4 +1,5 @@ -from eth.exceptions import InsufficientStack +from enum import IntEnum +from eth.vm.forks.london.validation import LondonValidatedTransaction from cached_property import cached_property from typing import ( Dict, @@ -21,6 +22,8 @@ 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 ( @@ -31,7 +34,12 @@ BerlinUnsignedLegacyTransaction, TypedTransaction, ) +from eth.vm.forks.istanbul.transactions import ( + ISTANBUL_TX_GAS_SCHEDULE, +) + from eth._utils.transactions import ( + calculate_intrinsic_gas, extract_transaction_sender, validate_transaction_signature, ) @@ -78,7 +86,7 @@ def __init__(self, to: Address, value: int, data: bytes, - access: Sequence[Tuple[Address, Sequence[int]]]): + access_list: Sequence[Tuple[Address, Sequence[int]]]): self.signer_address = signer_address self.nonce = nonce self.gas = gas @@ -87,10 +95,27 @@ def __init__(self, self.to = to self.value = value self.data = data - self.access = access + self.access_list = access_list # TODO maybe add properties and make the above variables private? - + def as_validated_transaction( + self, + effective_gas_price: int, + priority_fee_per_gas: int + ) -> LondonValidatedTransaction: + return LondonValidatedTransaction( + effective_gas_price, + priority_fee_per_gas, + signer_address=self.signer_address, + nonce=self.nonce, + gas=self.gas, + max_priority_fee_per_gas=self.max_priority_fee_per_gas, + max_fee_per_gas=self.max_fee_per_gas, + to=self.to, + value=self.value, + data=self.data, + access_list=self.access_list + ) class UnsignedBaseGasFeeTransaction(rlp.Serializable): _type_id = BASE_GAS_FEE_TRANSACTION_TYPE @@ -188,8 +213,16 @@ def hash(self) -> Hash32: raise NotImplementedError("Call hash() on the TypedTransaction instead") def get_intrinsic_gas(self) -> int: - # TODO figure out - return 1 + 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) diff --git a/eth/vm/forks/london/validation.py b/eth/vm/forks/london/validation.py index cb728d9157..d5dc37010b 100644 --- a/eth/vm/forks/london/validation.py +++ b/eth/vm/forks/london/validation.py @@ -3,30 +3,41 @@ SignedTransactionAPI, StateAPI ) -from eth.constants import ( - CREATE_CONTRACT_ADDRESS, -) -from eth.validation import ( - validate_uint256, - validate_is_integer, - validate_is_bytes, - validate_lt_secpk1n, - validate_lt_secpk1n2, - validate_lte, - validate_gte, - validate_canonical_address, -) from eth_utils.exceptions import ValidationError from .transactions import LondonNormalizedTransaction, LondonTypedTransaction +class LondonValidatedTransaction(LondonNormalizedTransaction): + """ + A London normalized transaction with additional `effective_gas_price` + and `priority_fee_per_gas` attributes for easier processing. + """ + def __init__( + self, + effective_gas_price: int, + priority_fee_per_gas: int, + **kwargs + ): + self.effective_gas_price = effective_gas_price + self.priority_fee_per_gas = priority_fee_per_gas + super().__init__(**kwargs) + + def validate_london_normalized_transaction( state: StateAPI, transaction: LondonNormalizedTransaction, header: LondonBlockHeader -) -> None: +) -> LondonValidatedTransaction: + """ + Validates a London normalized transaction. + + Raise `eth.exceptions.ValidationError` if the sender cannot + afford to send this transaction. + + Returns a LondonValidatedTransaction. + """ if transaction.max_fee_per_gas < header.base_fee_per_gas: raise ValidationError( f"Sender's max fee per gas ({transaction.max_fee_per_gas}) is " @@ -54,16 +65,6 @@ def validate_london_normalized_transaction( f" (has {sender_balance}, needs {total_transaction_cost})" ) - -# def validate_london_normalized_transaction( -# state: StateAPI, -# transaction: LondonNormalizedTransaction -# ) -> None: -# sender_balance = state.get_balance(transaction.sender) -# if transaction.value > sender_balance: -# raise ValidationError( -# f"Sender {transaction.sender!r} cannot afford txn value" -# f"{transaction.value} with account balance {sender_balance}" -# ) - -# # TODO continue validation + return transaction.as_validated_transaction( + effective_gas_price, priority_fee_per_gas + ) \ No newline at end of file From bb0c3f71199493f22405799d8163bbe1aaf11b39 Mon Sep 17 00:00:00 2001 From: Francesco Truzzi Date: Fri, 7 May 2021 21:35:34 +0200 Subject: [PATCH 04/10] fixes --- eth/abc.py | 36 ++++++++- eth/rlp/transactions.py | 8 ++ eth/vm/base.py | 1 + eth/vm/execution_context.py | 10 ++- eth/vm/forks/berlin/transactions.py | 2 + eth/vm/forks/london/__init__.py | 43 +++------- eth/vm/forks/london/blocks.py | 31 +++++++- eth/vm/forks/london/execution_context.py | 7 ++ eth/vm/forks/london/headers.py | 43 ++++++++++ eth/vm/forks/london/receipts.py | 2 +- eth/vm/forks/london/state.py | 91 ++++++++++------------ eth/vm/forks/london/transaction_context.py | 6 -- eth/vm/forks/london/transactions.py | 40 +++------- eth/vm/forks/london/validation.py | 35 ++------- eth/vm/state.py | 11 ++- 15 files changed, 207 insertions(+), 159 deletions(-) create mode 100644 eth/vm/forks/london/execution_context.py create mode 100644 eth/vm/forks/london/headers.py delete mode 100644 eth/vm/forks/london/transaction_context.py diff --git a/eth/abc.py b/eth/abc.py index 6d31de2871..80bf078442 100644 --- a/eth/abc.py +++ b/eth/abc.py @@ -323,6 +323,22 @@ def access_list(self) -> Sequence[Tuple[Address, Sequence[int]]]: """ ... + @property + @abstractmethod + def max_priority_fee_per_gas(self) -> int: + """ + Get the maximum priority gas fee the sender wants to pay to the miner + """ + ... + + @property + @abstractmethod + def max_fee_per_gas(self) -> int: + """ + Get the maximum total gas fee the sender wants to pay to the miner + """ + ... + class TransactionFieldsAPI(ABC): """ @@ -381,6 +397,12 @@ def hash(self) -> Hash32: def chain_id(self) -> Optional[int]: ... + # TODO is this needed? + # @property + # @abstractmethod + # def max_priority_fee_per_gas(self) -> Optional[int]: + # ... + class LegacyTransactionFieldsAPI(TransactionFieldsAPI): @property @@ -1686,6 +1708,14 @@ def chain_id(self) -> int: """ ... + @property + @abstractmethod + def base_gas_fee(self) -> Optional[int]: + """ + Return the base gas fee of the block + """ + ... + class ComputationAPI(ContextManager['ComputationAPI'], StackManipulationAPI): """ @@ -2867,9 +2897,8 @@ def get_computation(self, # # Transaction context # - @classmethod @abstractmethod - def get_transaction_context_class(cls) -> Type[TransactionContextAPI]: + def get_transaction_context_class(self) -> Type[TransactionContextAPI]: """ Return the :class:`~eth.vm.transaction_context.BaseTransactionContext` class that the state class uses. @@ -2919,9 +2948,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`` diff --git a/eth/rlp/transactions.py b/eth/rlp/transactions.py index 89245c60e0..eea3019a01 100644 --- a/eth/rlp/transactions.py +++ b/eth/rlp/transactions.py @@ -54,6 +54,14 @@ def chain_id(self) -> Optional[int]: def access_list(self) -> Sequence[Tuple[Address, Sequence[int]]]: return [] + @property + def max_priority_fee_per_gas(self) -> Optional[int]: + return None + + @property + def max_fee_per_gas(self) -> Optional[int]: + return None + BASE_TRANSACTION_FIELDS = [ ('nonce', big_endian_int), diff --git a/eth/vm/base.py b/eth/vm/base.py index ee17488f78..cf9be070c3 100644 --- a/eth/vm/base.py +++ b/eth/vm/base.py @@ -184,6 +184,7 @@ def create_execution_context(cls, gas_limit=header.gas_limit, prev_hashes=prev_hashes, chain_id=chain_context.chain_id, + base_gas_fee=header.base_fee_per_gas, ) def execute_bytecode(self, diff --git a/eth/vm/execution_context.py b/eth/vm/execution_context.py index d17284ac9d..b7b7472f28 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_gas_fee = 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_gas_fee: Optional[int]) -> 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_gas_fee = base_gas_fee @property def coinbase(self) -> Address: @@ -66,3 +70,7 @@ def prev_hashes(self) -> Iterable[Hash32]: @property def chain_id(self) -> int: return self._chain_id + + @property + def base_gas_fee(self) -> Optional[int]: + return self._base_gas_fee \ No newline at end of file diff --git a/eth/vm/forks/berlin/transactions.py b/eth/vm/forks/berlin/transactions.py index 5554829198..e0d58c100b 100644 --- a/eth/vm/forks/berlin/transactions.py +++ b/eth/vm/forks/berlin/transactions.py @@ -1,6 +1,7 @@ from typing import ( Any, Dict, + Optional, Sequence, Tuple, Type, @@ -153,6 +154,7 @@ class AccessListTransaction(rlp.Serializable, SignedTransactionMethods, SignedTr ('s', big_endian_int), ] + def get_sender(self) -> Address: return extract_transaction_sender(self) diff --git a/eth/vm/forks/london/__init__.py b/eth/vm/forks/london/__init__.py index 55e1dbe689..9caec377b3 100644 --- a/eth/vm/forks/london/__init__.py +++ b/eth/vm/forks/london/__init__.py @@ -1,8 +1,7 @@ -from eth.vm.forks.london.transactions import LondonTypedTransaction -from typing import Tuple, Type +from typing import Type from eth._utils.db import get_parent_header -from eth.abc import BlockAPI, BlockHeaderAPI, ComputationAPI, ReceiptAPI, SignedTransactionAPI +from eth.abc import BlockAPI, BlockHeaderAPI from eth_utils.exceptions import ValidationError from eth.rlp.blocks import BaseBlock from eth.vm.forks.berlin import BerlinVM @@ -13,8 +12,12 @@ BASE_FEE_MAX_CHANGE_DENOMINATOR, ELASTICITY_MULTIPLIER ) +from .headers import ( + compute_london_difficulty, + create_london_header_from_parent, +) from .state import LondonState -from .validation import validate_london_transaction_against_header + class LondonVM(BerlinVM): # fork name @@ -25,36 +28,12 @@ class LondonVM(BerlinVM): _state_class: Type[BaseState] = LondonState # Methods - # validate_transaction_against_header = validate_london_transaction_against_header - # create_header_from_parent = staticmethod(create_berlin_header_from_parent) # type: ignore - # compute_difficulty = staticmethod(compute_berlin_difficulty) # type: ignore + # skip header validation: validate everything in the executor as we need state access + validate_transaction_against_header = lambda header, transaction: None # type: ignore + create_header_from_parent = staticmethod(create_london_header_from_parent) # type: ignore + compute_difficulty = staticmethod(compute_london_difficulty) # type: ignore # configure_header = configure_berlin_header - # @staticmethod - # def make_receipt( - # base_header: BlockHeaderAPI, - # transaction: SignedTransactionAPI, - # computation: ComputationAPI, - # state: StateAPI) -> ReceiptAPI: - - # gas_used = base_header.gas_used + finalize_gas_used(transaction, computation) - - # if computation.is_error: - # status_code = EIP658_TRANSACTION_STATUS_CODE_FAILURE - # else: - # status_code = EIP658_TRANSACTION_STATUS_CODE_SUCCESS - - # return transaction.make_receipt(status_code, gas_used, computation.get_log_entries()) - - def apply_transaction( - self, - header: BlockHeaderAPI, - transaction: SignedTransactionAPI, - ) -> Tuple[ReceiptAPI, ComputationAPI]: - - self.state.lock_changes() - computation = self.state.apply_transaction(transaction, header) - @staticmethod def calculate_expected_base_fee_per_gas(parent_header: BlockHeaderAPI) -> int: parent_base_fee_per_gas = parent_header.base_fee_per_gas diff --git a/eth/vm/forks/london/blocks.py b/eth/vm/forks/london/blocks.py index 9def3101c2..44935b8d59 100644 --- a/eth/vm/forks/london/blocks.py +++ b/eth/vm/forks/london/blocks.py @@ -3,6 +3,7 @@ from typing import ( Dict, Type, + overload, ) import rlp @@ -97,7 +98,31 @@ class LondonBlockHeader(rlp.Serializable, BlockHeaderAPI): ('nonce', Binary(8, allow_empty=True)), ('base_fee_per_gas', big_endian_int), ] + @overload + def __init__(self, **kwargs: HeaderParams) -> None: + ... + @overload + def __init__(self, # type: ignore # noqa: F811 + difficulty: int, + block_number: BlockNumber, + gas_target: 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: + ... + + @overload def __init__(self, # type: ignore # noqa: F811 difficulty: int, block_number: BlockNumber, @@ -157,16 +182,16 @@ def hex_hash(self) -> str: @classmethod def from_parent(cls, - parent: 'LondonBlockHeader', + parent: 'BlockHeaderAPI', gas_target: int, difficulty: int, timestamp: int, coinbase: Address = ZERO_ADDRESS, - base_fee_per_gas: int = 0, + base_fee_per_gas: int = 0, # TODO validate nonce: bytes = None, extra_data: bytes = None, transaction_root: bytes = None, - receipt_root: bytes = None) -> 'LondonBlockHeader': + receipt_root: bytes = None) -> 'BlockHeaderAPI': """ Initialize a new block header with the `parent` header as the block's parent hash. diff --git a/eth/vm/forks/london/execution_context.py b/eth/vm/forks/london/execution_context.py new file mode 100644 index 0000000000..c8347ec8f4 --- /dev/null +++ b/eth/vm/forks/london/execution_context.py @@ -0,0 +1,7 @@ +from eth.abc import ExecutionContextAPI + + +class LondonExecutionContext(ExecutionContextAPI): + def __init__(self, base_gas_fee: int, **kwargs): # TODO remove kwargs + super().__init__(**kwargs) + self._base_gas_fee = base_gas_fee \ No newline at end of file diff --git a/eth/vm/forks/london/headers.py b/eth/vm/forks/london/headers.py new file mode 100644 index 0000000000..9f1e1f3a69 --- /dev/null +++ b/eth/vm/forks/london/headers.py @@ -0,0 +1,43 @@ +from eth.vm.forks.london.blocks import LondonBlockHeader +from eth.constants import GENESIS_GAS_LIMIT +from eth._utils.headers import compute_gas_limit +from eth.abc import BlockHeaderAPI +from typing import Any, Callable +from toolz.functoolz import curry +from eth.vm.forks.berlin.headers import ( + configure_header, + compute_berlin_difficulty, +) + +@curry +def create_header_from_parent(difficulty_fn: Callable[[BlockHeaderAPI, int], int], + parent_header: BlockHeaderAPI, + **header_params: Any) -> BlockHeaderAPI: + # byzantium + if 'difficulty' not in header_params: + header_params.setdefault('timestamp', parent_header.timestamp + 1) + + header_params['difficulty'] = difficulty_fn( + parent_header, + header_params['timestamp'], + ) + + # frontier + if 'gas_limit' not in header_params: + header_params['gas_limit'] = compute_gas_limit( + parent_header, + gas_limit_floor=GENESIS_GAS_LIMIT, + ) + + header = LondonBlockHeader.from_parent(parent=parent_header, **header_params) + return header + + +compute_london_difficulty = compute_berlin_difficulty + +create_london_header_from_parent = create_header_from_parent( + compute_london_difficulty +) + +# TODO update configure_header +# configure_london_header = configure_header(compute_berlin_difficulty) diff --git a/eth/vm/forks/london/receipts.py b/eth/vm/forks/london/receipts.py index 61d9eb88ed..977d0e862a 100644 --- a/eth/vm/forks/london/receipts.py +++ b/eth/vm/forks/london/receipts.py @@ -18,7 +18,7 @@ class LondonReceiptBuilder(BerlinReceiptBuilder): - codecs: Dict[int, Type[ReceiptAPI]] = { + codecs: Dict[int, Type[Receipt]] = { ACCESS_LIST_TRANSACTION_TYPE: Receipt, BASE_GAS_FEE_TRANSACTION_TYPE: Receipt, } diff --git a/eth/vm/forks/london/state.py b/eth/vm/forks/london/state.py index 945fab5342..53ad6e84f5 100644 --- a/eth/vm/forks/london/state.py +++ b/eth/vm/forks/london/state.py @@ -1,5 +1,7 @@ +from eth.vm.logic.context import origin +from eth.vm.forks.london.transaction_context import LondonTransactionContext from eth.vm.forks.frontier.constants import REFUND_SELFDESTRUCT -from typing import Type +from typing import Type, Union from eth_hash.auto import keccak from eth_utils.exceptions import ValidationError @@ -12,6 +14,8 @@ ComputationAPI, MessageAPI, SignedTransactionAPI, + StateAPI, + TransactionContextAPI, TransactionExecutorAPI, ) from eth.constants import ( @@ -28,43 +32,25 @@ BerlinState, BerlinTransactionExecutor, ) -from eth.vm.forks.london.blocks import ( - LondonBlockHeader, -) - from eth._utils.address import ( generate_contract_address, ) from .computation import LondonComputation -from .transactions import LondonNormalizedTransaction, LondonTypedTransaction, normalize_transaction -from .validation import LondonValidatedTransaction, validate_london_normalized_transaction +from .transactions import LondonLegacyTransaction, LondonTypedTransaction, normalize_transaction +from .validation import validate_london_normalized_transaction class LondonTransactionExecutor(BerlinTransactionExecutor): - def __call__( - self, - transaction: SignedTransactionAPI, - effective_gas_price: int - ) -> ComputationAPI: - # unlike other VMs, don't validate tx here -- we need access to both header and state - message = self.build_evm_message(transaction, effective_gas_price) - computation = self.build_computation(message, transaction) - finalized_computation = self.finalize_computation( - transaction, computation, effective_gas_price - ) - - return finalized_computation - def build_evm_message( self, - transaction: LondonValidatedTransaction + transaction: SignedTransactionAPI, ) -> MessageAPI: + transaction_context = self.vm_state.get_transaction_context(transaction) + gas_fee = transaction.gas * transaction_context.gas_price + # Buy Gas - self.vm_state.delta_balance( - transaction.sender, - -1 * transaction.gas * transaction.effective_gas_price - ) + self.vm_state.delta_balance(transaction.sender, -1 * gas_fee) # Increment Nonce self.vm_state.increment_nonce(transaction.sender) @@ -115,9 +101,11 @@ def build_evm_message( def finalize_computation( self, - transaction: LondonValidatedTransaction, + 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: @@ -128,7 +116,7 @@ def finalize_computation( 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.effective_gas_price + gas_refund_amount = (gas_refund + gas_remaining) * transaction_context.gas_price if gas_refund_amount: self.vm_state.logger.debug2( @@ -141,7 +129,7 @@ def finalize_computation( # Miner Fees transaction_fee = \ - (transaction.gas - gas_remaining - gas_refund) * transaction.priority_fee_per_gas + (transaction.gas - gas_remaining - gas_refund) * transaction.max_priority_fee_per_gas self.vm_state.logger.debug2( 'TRANSACTION FEE: %s -> %s', transaction_fee, @@ -160,41 +148,42 @@ def finalize_computation( self.vm_state.set_balance(account, 0) self.vm_state.delete_account(account) - - return computation class LondonState(BerlinState): account_db_class: Type[AccountDatabaseAPI] = AccountDB computation_class = LondonComputation transaction_executor_class: Type[TransactionExecutorAPI] = LondonTransactionExecutor - - def apply_transaction( - self, - transaction: SignedTransactionAPI, - header: LondonBlockHeader - ) -> ComputationAPI: - - validated_transaction = self.validate_transaction(transaction, header) - executor = self.get_transaction_executor() - return executor(validated_transaction) + transaction_context_class: Type[TransactionContextAPI] = LondonTransactionContext def validate_transaction( self, - transaction: SignedTransactionAPI, - header: LondonBlockHeader - ) -> LondonValidatedTransaction: - + transaction: SignedTransactionAPI + ) -> None: # homestead validation if transaction.s > SECPK1_N // 2 or transaction.s == 0: raise ValidationError("Invalid signature S value") normalized_transaction = normalize_transaction(transaction) - validated_transaction = validate_london_normalized_transaction( - state=self, transaction=normalized_transaction, header=header + validate_london_normalized_transaction( + state=self, + transaction=normalized_transaction, + base_fee_per_gas=self.execution_context.base_gas_fee + ) + + def get_transaction_context(self: StateAPI, + transaction: SignedTransactionAPI) -> TransactionContextAPI: + """ + London-specific transaction context creation, + where gas_price includes the block base fee + """ + priority_fee_per_gas = min( + transaction.max_priority_fee_per_gas, + transaction.max_fee_per_gas - self.execution_context.base_gas_fee ) - return validated_transaction - def get_transaction_context(cls, - transaction: LondonNormalizedTransaction) -> TransactionContextAPI: - + effective_gas_price = self.execution_context.base_gas_fee + priority_fee_per_gas + return self.get_transaction_context_class()( + gas_price=effective_gas_price, + origin=transaction.sender + ) \ No newline at end of file diff --git a/eth/vm/forks/london/transaction_context.py b/eth/vm/forks/london/transaction_context.py deleted file mode 100644 index 26be6df5f7..0000000000 --- a/eth/vm/forks/london/transaction_context.py +++ /dev/null @@ -1,6 +0,0 @@ -from eth.vm.forks.frontier.transaction_context import FrontierTransactionContext - - -class LondonTransactionContext(FrontierTransactionContext): - @property - def \ No newline at end of file diff --git a/eth/vm/forks/london/transactions.py b/eth/vm/forks/london/transactions.py index 3c68a35985..199634f820 100644 --- a/eth/vm/forks/london/transactions.py +++ b/eth/vm/forks/london/transactions.py @@ -1,12 +1,12 @@ -from enum import IntEnum -from eth.vm.forks.london.validation import LondonValidatedTransaction from cached_property import cached_property from typing import ( + Any, Dict, + Optional, Sequence, Tuple, Type, - Union, + overload, ) from eth_keys.datatypes import PrivateKey from eth_utils.exceptions import ValidationError @@ -73,12 +73,12 @@ class LondonUnsignedLegacyTransaction(BerlinUnsignedLegacyTransaction): pass -class LondonNormalizedTransaction(BaseTransactionAPI): +class LondonNormalizedTransaction(): """ A normalized transaction, used for validation purposes """ def __init__(self, - signer_address: Address, + sender: Address, nonce: int, gas: int, max_priority_fee_per_gas: int, @@ -86,8 +86,9 @@ def __init__(self, to: Address, value: int, data: bytes, - access_list: Sequence[Tuple[Address, Sequence[int]]]): - self.signer_address = signer_address + access_list: Sequence[Tuple[Address, Sequence[int]]], + gas_price: Optional[int] = None): # EIP-1559 effective gas price + self.sender = sender self.nonce = nonce self.gas = gas self.max_priority_fee_per_gas = max_priority_fee_per_gas @@ -96,26 +97,9 @@ def __init__(self, self.value = value self.data = data self.access_list = access_list + if gas_price is not None: + self.gas_price = gas_price - # TODO maybe add properties and make the above variables private? - def as_validated_transaction( - self, - effective_gas_price: int, - priority_fee_per_gas: int - ) -> LondonValidatedTransaction: - return LondonValidatedTransaction( - effective_gas_price, - priority_fee_per_gas, - signer_address=self.signer_address, - nonce=self.nonce, - gas=self.gas, - max_priority_fee_per_gas=self.max_priority_fee_per_gas, - max_fee_per_gas=self.max_fee_per_gas, - to=self.to, - value=self.value, - data=self.data, - access_list=self.access_list - ) class UnsignedBaseGasFeeTransaction(rlp.Serializable): _type_id = BASE_GAS_FEE_TRANSACTION_TYPE @@ -329,12 +313,12 @@ def new_base_gas_price_transaction( def normalize_transaction( - transaction: Union[LondonLegacyTransaction, LondonTypedTransaction] + transaction: SignedTransactionAPI ) -> LondonNormalizedTransaction: # fields common to all transactions fields = { - "signer_address": transaction.sender, + "sender": transaction.sender, "nonce": transaction.nonce, "gas": transaction.gas, "to": transaction.to, diff --git a/eth/vm/forks/london/validation.py b/eth/vm/forks/london/validation.py index d5dc37010b..d0a527f415 100644 --- a/eth/vm/forks/london/validation.py +++ b/eth/vm/forks/london/validation.py @@ -6,42 +6,27 @@ from eth_utils.exceptions import ValidationError +from .transaction_context import LondonTransactionContext from .transactions import LondonNormalizedTransaction, LondonTypedTransaction -class LondonValidatedTransaction(LondonNormalizedTransaction): - """ - A London normalized transaction with additional `effective_gas_price` - and `priority_fee_per_gas` attributes for easier processing. - """ - def __init__( - self, - effective_gas_price: int, - priority_fee_per_gas: int, - **kwargs - ): - self.effective_gas_price = effective_gas_price - self.priority_fee_per_gas = priority_fee_per_gas - super().__init__(**kwargs) - - def validate_london_normalized_transaction( state: StateAPI, transaction: LondonNormalizedTransaction, - header: LondonBlockHeader -) -> LondonValidatedTransaction: + base_fee_per_gas: int +) -> None: """ Validates a London normalized transaction. Raise `eth.exceptions.ValidationError` if the sender cannot afford to send this transaction. - Returns a LondonValidatedTransaction. + Returns the effective gas price of the transaction (before refunds). """ - if transaction.max_fee_per_gas < header.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 ({header.base_fee_per_gas})" + f"lower than block's base fee per gas ({base_fee_per_gas})" ) sender_balance = state.get_balance(transaction.sender) @@ -53,10 +38,10 @@ def validate_london_normalized_transaction( priority_fee_per_gas = min( transaction.max_priority_fee_per_gas, - transaction.max_fee_per_gas - header.base_fee_per_gas + transaction.max_fee_per_gas - base_fee_per_gas ) - effective_gas_price = priority_fee_per_gas + header.base_fee_per_gas + effective_gas_price = priority_fee_per_gas + base_fee_per_gas total_transaction_cost = transaction.value + effective_gas_price if sender_balance - total_transaction_cost < 0: @@ -64,7 +49,3 @@ def validate_london_normalized_transaction( f"Sender does not have enough balance to cover transaction value and gas " f" (has {sender_balance}, needs {total_transaction_cost})" ) - - return transaction.as_validated_transaction( - effective_gas_price, priority_fee_per_gas - ) \ No newline at end of file diff --git a/eth/vm/state.py b/eth/vm/state.py index 3bb181250f..cf503942dd 100644 --- a/eth/vm/state.py +++ b/eth/vm/state.py @@ -237,10 +237,10 @@ def get_computation(self, # Transaction context # @classmethod - def get_transaction_context_class(cls) -> Type[TransactionContextAPI]: - if cls.transaction_context_class is None: + def get_transaction_context_class(self) -> Type[TransactionContextAPI]: + if self.transaction_context_class is None: raise AttributeError("No `transaction_context_class` has been set for this State") - return cls.transaction_context_class + return self.transaction_context_class # # Execution @@ -269,10 +269,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, ) From 51d353eaa8c3b6f923e85e95d876a881a0d8a1a9 Mon Sep 17 00:00:00 2001 From: Francesco Truzzi Date: Fri, 7 May 2021 22:15:25 +0200 Subject: [PATCH 05/10] rename gas_target to gas_limit --- eth/vm/forks/london/__init__.py | 15 ++++++------ eth/vm/forks/london/blocks.py | 39 +++++++------------------------ eth/vm/forks/london/state.py | 2 -- eth/vm/forks/london/validation.py | 5 ++-- 4 files changed, 18 insertions(+), 43 deletions(-) diff --git a/eth/vm/forks/london/__init__.py b/eth/vm/forks/london/__init__.py index 9caec377b3..29f30e35b9 100644 --- a/eth/vm/forks/london/__init__.py +++ b/eth/vm/forks/london/__init__.py @@ -37,7 +37,7 @@ class LondonVM(BerlinVM): @staticmethod def calculate_expected_base_fee_per_gas(parent_header: BlockHeaderAPI) -> int: parent_base_fee_per_gas = parent_header.base_fee_per_gas - parent_gas_target = parent_header.gas_target + parent_gas_target = parent_header.gas_limit parent_gas_used = parent_header.gas_used if parent_gas_used == parent_gas_target: @@ -65,25 +65,26 @@ def validate_header(cls, header: BlockHeaderAPI, parent_header: BlockHeaderAPI) -> None: - parent_gas_target = parent_header.gas_target + header_gas_target = header.gas_limit + parent_gas_target = parent_header.gas_limit - max_usable_gas = header.gas_target * ELASTICITY_MULTIPLIER + max_usable_gas = header_gas_target * ELASTICITY_MULTIPLIER if header.gas_used > max_usable_gas: raise ValidationError( f"Block used too much gas: {header.gas_used} " f"(max: {max_usable_gas})" ) - if header.gas_target > parent_gas_target + (parent_gas_target // 1024): + if header_gas_target > parent_gas_target + (parent_gas_target // 1024): raise ValidationError( f"Gas target increased too much (from {parent_gas_target} " - f"to {header.gas_target})" + f"to {header_gas_target})" ) - if header.gas_target < parent_gas_target - (parent_gas_target // 1024): + if header_gas_target < parent_gas_target - (parent_gas_target // 1024): raise ValidationError( f"Gas target decreased too much (from {parent_gas_target} " - f"to {header.gas_target})" + f"to {header_gas_target})" ) expected_base_fee_per_gas = LondonVM.calculate_expected_base_fee_per_gas(parent_header) diff --git a/eth/vm/forks/london/blocks.py b/eth/vm/forks/london/blocks.py index 44935b8d59..2bf6eeb7a7 100644 --- a/eth/vm/forks/london/blocks.py +++ b/eth/vm/forks/london/blocks.py @@ -2,9 +2,11 @@ from typing import ( Dict, + Optional, Type, overload, ) +from eth_utils.exceptions import ValidationError import rlp @@ -71,7 +73,7 @@ class LondonMiningHeader(rlp.Serializable, MiningHeaderAPI): ('bloom', uint256), ('difficulty', big_endian_int), ('block_number', big_endian_int), - ('gas_target', big_endian_int), + ('gas_limit', big_endian_int), ('gas_used', big_endian_int), ('timestamp', big_endian_int), ('extra_data', binary), @@ -90,7 +92,7 @@ class LondonBlockHeader(rlp.Serializable, BlockHeaderAPI): ('bloom', uint256), ('difficulty', big_endian_int), ('block_number', big_endian_int), - ('gas_target', big_endian_int), + ('gas_limit', big_endian_int), ('gas_used', big_endian_int), ('timestamp', big_endian_int), ('extra_data', binary), @@ -98,35 +100,10 @@ class LondonBlockHeader(rlp.Serializable, BlockHeaderAPI): ('nonce', Binary(8, allow_empty=True)), ('base_fee_per_gas', big_endian_int), ] - @overload - def __init__(self, **kwargs: HeaderParams) -> None: - ... - - @overload - def __init__(self, # type: ignore # noqa: F811 - difficulty: int, - block_number: BlockNumber, - gas_target: 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: - ... - - @overload def __init__(self, # type: ignore # noqa: F811 difficulty: int, block_number: BlockNumber, - gas_target: int, + gas_limit: int, timestamp: int = None, coinbase: Address = ZERO_ADDRESS, parent_hash: Hash32 = ZERO_HASH32, @@ -152,7 +129,7 @@ def __init__(self, # type: ignore # noqa: F811 bloom=bloom, difficulty=difficulty, block_number=block_number, - gas_target=gas_target, + gas_limit=gas_limit, gas_used=gas_used, timestamp=timestamp, extra_data=extra_data, @@ -183,9 +160,9 @@ def hex_hash(self) -> str: @classmethod def from_parent(cls, parent: 'BlockHeaderAPI', - gas_target: int, difficulty: int, timestamp: int, + gas_limit: int, # for first London block from legacy parent coinbase: Address = ZERO_ADDRESS, base_fee_per_gas: int = 0, # TODO validate nonce: bytes = None, @@ -200,7 +177,7 @@ def from_parent(cls, 'parent_hash': parent.hash, 'coinbase': coinbase, 'state_root': parent.state_root, - 'gas_target': gas_target, + 'gas_limit': gas_limit, 'base_fee_per_gas': base_fee_per_gas, 'difficulty': difficulty, 'block_number': parent.block_number + 1, diff --git a/eth/vm/forks/london/state.py b/eth/vm/forks/london/state.py index 53ad6e84f5..b7c989eb1b 100644 --- a/eth/vm/forks/london/state.py +++ b/eth/vm/forks/london/state.py @@ -1,5 +1,4 @@ from eth.vm.logic.context import origin -from eth.vm.forks.london.transaction_context import LondonTransactionContext from eth.vm.forks.frontier.constants import REFUND_SELFDESTRUCT from typing import Type, Union @@ -154,7 +153,6 @@ class LondonState(BerlinState): account_db_class: Type[AccountDatabaseAPI] = AccountDB computation_class = LondonComputation transaction_executor_class: Type[TransactionExecutorAPI] = LondonTransactionExecutor - transaction_context_class: Type[TransactionContextAPI] = LondonTransactionContext def validate_transaction( self, diff --git a/eth/vm/forks/london/validation.py b/eth/vm/forks/london/validation.py index d0a527f415..4f21b010f8 100644 --- a/eth/vm/forks/london/validation.py +++ b/eth/vm/forks/london/validation.py @@ -1,4 +1,3 @@ -from eth.vm.forks.london.blocks import LondonBlockHeader from eth.abc import ( SignedTransactionAPI, StateAPI @@ -6,8 +5,8 @@ from eth_utils.exceptions import ValidationError -from .transaction_context import LondonTransactionContext -from .transactions import LondonNormalizedTransaction, LondonTypedTransaction +from .blocks import LondonBlockHeader +from .transactions import LondonNormalizedTransaction def validate_london_normalized_transaction( From 765636f56aea8bc4251e78fad05957216bcaa1c4 Mon Sep 17 00:00:00 2001 From: Francesco Truzzi Date: Fri, 7 May 2021 23:03:43 +0200 Subject: [PATCH 06/10] fixes --- eth/abc.py | 26 ++------------ eth/chains/mainnet/__init__.py | 4 +++ eth/chains/mainnet/constants.py | 6 ++++ eth/rlp/headers.py | 5 +++ eth/rlp/transactions.py | 21 +++++++---- eth/vm/forks/__init__.py | 6 ++-- eth/vm/forks/london/__init__.py | 4 ++- eth/vm/forks/london/blocks.py | 8 ++--- eth/vm/forks/london/state.py | 4 +-- eth/vm/forks/london/transactions.py | 54 ++++++++++++++++++++++++----- eth/vm/forks/london/validation.py | 4 +-- 11 files changed, 92 insertions(+), 50 deletions(-) diff --git a/eth/abc.py b/eth/abc.py index 80bf078442..37dba9099a 100644 --- a/eth/abc.py +++ b/eth/abc.py @@ -74,7 +74,6 @@ class MiningHeaderAPI(ABC): gas_used: int timestamp: int extra_data: bytes - gas_target: int # EIP-1559 base_fee_per_gas: int # EIP-1559 @property @@ -323,27 +322,14 @@ def access_list(self) -> Sequence[Tuple[Address, Sequence[int]]]: """ ... - @property - @abstractmethod - def max_priority_fee_per_gas(self) -> int: - """ - Get the maximum priority gas fee the sender wants to pay to the miner - """ - ... - - @property - @abstractmethod - def max_fee_per_gas(self) -> int: - """ - Get the maximum total gas fee the sender wants to pay to the miner - """ - ... - class TransactionFieldsAPI(ABC): """ A class to define all common transaction fields. """ + max_fee_per_gas: int + max_priority_fee_per_gas: int + @property @abstractmethod def nonce(self) -> int: @@ -397,12 +383,6 @@ def hash(self) -> Hash32: def chain_id(self) -> Optional[int]: ... - # TODO is this needed? - # @property - # @abstractmethod - # def max_priority_fee_per_gas(self) -> Optional[int]: - # ... - class LegacyTransactionFieldsAPI(TransactionFieldsAPI): @property 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..9cc4d2d4c8 100644 --- a/eth/chains/mainnet/constants.py +++ b/eth/chains/mainnet/constants.py @@ -57,3 +57,9 @@ # Berlin Block # BERLIN_MAINNET_BLOCK = BlockNumber(12244000) + + +# +# London Block +# +LONDON_MAINNET_BLOCK = BlockNumber(12244001) # TODO change to actual when known diff --git a/eth/rlp/headers.py b/eth/rlp/headers.py index d50570a1a0..6f078cbf08 100644 --- a/eth/rlp/headers.py +++ b/eth/rlp/headers.py @@ -1,6 +1,7 @@ import time from typing import ( Dict, + Optional, overload, ) @@ -202,3 +203,7 @@ def is_genesis(self) -> bool: # 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) -> Optional[int]: + return None diff --git a/eth/rlp/transactions.py b/eth/rlp/transactions.py index eea3019a01..e6b674dfc9 100644 --- a/eth/rlp/transactions.py +++ b/eth/rlp/transactions.py @@ -54,13 +54,22 @@ def chain_id(self) -> Optional[int]: def access_list(self) -> Sequence[Tuple[Address, Sequence[int]]]: return [] - @property - def max_priority_fee_per_gas(self) -> Optional[int]: - return None + # @property + # def max_priority_fee_per_gas(self) -> Optional[int]: + # return None + + # @max_priority_fee_per_gas.setter + # def max_priority_fee_per_gas(self, value: int) -> None: + # self.max_priority_fee_per_gas = value + + # @property + # def max_fee_per_gas(self) -> Optional[int]: + # return None + + # @max_fee_per_gas.setter + # def max_fee_per_gas(self, value: int) -> None: + # self.max_fee_per_gas = value - @property - def max_fee_per_gas(self) -> Optional[int]: - return None BASE_TRANSACTION_FIELDS = [ diff --git a/eth/vm/forks/__init__.py b/eth/vm/forks/__init__.py index fcbee07060..742b3c0d4d 100644 --- a/eth/vm/forks/__init__.py +++ b/eth/vm/forks/__init__.py @@ -28,6 +28,6 @@ from .berlin import ( # noqa: F401 BerlinVM, ) -# from .london import ( # noqa: F401 -# LondonVM -# ) +from .london import ( # noqa: F401 + LondonVM +) diff --git a/eth/vm/forks/london/__init__.py b/eth/vm/forks/london/__init__.py index 29f30e35b9..b6464def94 100644 --- a/eth/vm/forks/london/__init__.py +++ b/eth/vm/forks/london/__init__.py @@ -29,7 +29,7 @@ class LondonVM(BerlinVM): # Methods # skip header validation: validate everything in the executor as we need state access - validate_transaction_against_header = lambda header, transaction: None # type: ignore + validate_transaction_against_header = lambda *_: None # type: ignore create_header_from_parent = staticmethod(create_london_header_from_parent) # type: ignore compute_difficulty = staticmethod(compute_london_difficulty) # type: ignore # configure_header = configure_berlin_header @@ -37,6 +37,8 @@ class LondonVM(BerlinVM): @staticmethod def calculate_expected_base_fee_per_gas(parent_header: BlockHeaderAPI) -> int: parent_base_fee_per_gas = parent_header.base_fee_per_gas + if parent_base_fee_per_gas is None: # TODO parent header is non-London + parent_base_fee_per_gas = 0 parent_gas_target = parent_header.gas_limit parent_gas_used = parent_header.gas_used diff --git a/eth/vm/forks/london/blocks.py b/eth/vm/forks/london/blocks.py index 2bf6eeb7a7..bd3fc460db 100644 --- a/eth/vm/forks/london/blocks.py +++ b/eth/vm/forks/london/blocks.py @@ -96,9 +96,9 @@ class LondonBlockHeader(rlp.Serializable, BlockHeaderAPI): ('gas_used', big_endian_int), ('timestamp', big_endian_int), ('extra_data', binary), + ('base_fee_per_gas', big_endian_int), ('mix_hash', binary), ('nonce', Binary(8, allow_empty=True)), - ('base_fee_per_gas', big_endian_int), ] def __init__(self, # type: ignore # noqa: F811 difficulty: int, @@ -162,9 +162,9 @@ def from_parent(cls, parent: 'BlockHeaderAPI', difficulty: int, timestamp: int, - gas_limit: int, # for first London block from legacy parent + gas_limit: int, coinbase: Address = ZERO_ADDRESS, - base_fee_per_gas: int = 0, # TODO validate + base_fee_per_gas: int = 0, # TODO is this correct? nonce: bytes = None, extra_data: bytes = None, transaction_root: bytes = None, @@ -192,7 +192,7 @@ def from_parent(cls, if receipt_root is not None: header_kwargs['receipt_root'] = receipt_root - header = cls(**header_kwargs) + header = cls(**header_kwargs) # type: ignore return header @property diff --git a/eth/vm/forks/london/state.py b/eth/vm/forks/london/state.py index b7c989eb1b..6c24e1a0fc 100644 --- a/eth/vm/forks/london/state.py +++ b/eth/vm/forks/london/state.py @@ -36,7 +36,7 @@ ) from .computation import LondonComputation -from .transactions import LondonLegacyTransaction, LondonTypedTransaction, normalize_transaction +from .transactions import LondonLegacyTransaction, LondonTypedTransaction, new_normalize_transaction, normalize_transaction from .validation import validate_london_normalized_transaction @@ -162,7 +162,7 @@ def validate_transaction( if transaction.s > SECPK1_N // 2 or transaction.s == 0: raise ValidationError("Invalid signature S value") - normalized_transaction = normalize_transaction(transaction) + normalized_transaction = new_normalize_transaction(transaction) validate_london_normalized_transaction( state=self, transaction=normalized_transaction, diff --git a/eth/vm/forks/london/transactions.py b/eth/vm/forks/london/transactions.py index 199634f820..f04ea93378 100644 --- a/eth/vm/forks/london/transactions.py +++ b/eth/vm/forks/london/transactions.py @@ -40,6 +40,7 @@ from eth._utils.transactions import ( calculate_intrinsic_gas, + create_transaction_signature, extract_transaction_sender, validate_transaction_signature, ) @@ -70,7 +71,22 @@ class LondonLegacyTransaction(BerlinLegacyTransaction): class LondonUnsignedLegacyTransaction(BerlinUnsignedLegacyTransaction): - pass + 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 LondonNormalizedTransaction(): @@ -244,17 +260,22 @@ class LondonTypedTransaction(TypedTransaction): def __init__(self, type_id: int, proxy_target: SignedTransactionAPI) -> None: super().__init__(type_id, proxy_target) + self.max_priority_fee_per_gas = self._inner.max_priority_fee_per_gas + self.max_fee_per_gas = self._inner.max_fee_per_gas - @property - def max_priority_fee_per_gas(self) -> int: - return self._inner.max_priority_fee_per_gas + # @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 max_fee_per_gas(self) -> int: + # return self._inner.max_fee_per_gas class LondonTransactionBuilder(BerlinTransactionBuilder): + legacy_signed = LondonLegacyTransaction + legacy_unsigned = LondonUnsignedLegacyTransaction + @classmethod def new_unsigned_base_gas_price_transaction( cls, @@ -338,6 +359,23 @@ def normalize_transaction( elif transaction.type_id != ACCESS_LIST_TRANSACTION_TYPE: raise ValidationError(f"Invalid transaction type_id: {transaction.type_id}") - return LondonNormalizedTransaction(**fields) + return LondonNormalizedTransaction(**fields) # type: ignore raise ValidationError(f"Invalid transaction type: {type(transaction)}") + +def new_normalize_transaction( + transaction: SignedTransactionAPI +) -> SignedTransactionAPI: + if isinstance(transaction, LondonLegacyTransaction): + transaction.max_priority_fee_per_gas = transaction.gas_price + transaction.max_fee_per_gas = transaction.gas_price + elif isinstance(transaction, LondonTypedTransaction): + if transaction.type_id == ACCESS_LIST_TRANSACTION_TYPE: + transaction.max_priority_fee_per_gas = transaction.gas_price + transaction.max_fee_per_gas = transaction.gas_price + elif transaction.type_id != BASE_GAS_FEE_TRANSACTION_TYPE: + raise ValidationError(f"Invalid transaction type_id: {transaction.type_id}") + else: + raise ValidationError(f"Invalid transaction type: {type(transaction)}") + + return transaction \ No newline at end of file diff --git a/eth/vm/forks/london/validation.py b/eth/vm/forks/london/validation.py index 4f21b010f8..3c30cea5ed 100644 --- a/eth/vm/forks/london/validation.py +++ b/eth/vm/forks/london/validation.py @@ -11,7 +11,7 @@ def validate_london_normalized_transaction( state: StateAPI, - transaction: LondonNormalizedTransaction, + transaction: SignedTransactionAPI, base_fee_per_gas: int ) -> None: """ @@ -19,8 +19,6 @@ def validate_london_normalized_transaction( Raise `eth.exceptions.ValidationError` if the sender cannot afford to send this transaction. - - Returns the effective gas price of the transaction (before refunds). """ if transaction.max_fee_per_gas < base_fee_per_gas: raise ValidationError( From 9da46a6194e3a1882243603e5d14288a51487af8 Mon Sep 17 00:00:00 2001 From: Francesco Truzzi Date: Mon, 10 May 2021 19:59:49 +0200 Subject: [PATCH 07/10] add london VM in existing tests --- eth/tools/builder/chain/__init__.py | 1 + eth/tools/builder/chain/builders.py | 2 ++ tests/conftest.py | 2 ++ tests/core/builder-tools/test_chain_construction.py | 3 +++ tests/core/chain-object/test_contract_call.py | 11 +++++++++++ tests/core/chain-object/test_gas_estimation.py | 1 + tests/core/opcodes/test_opcodes.py | 4 +++- tests/core/tester/test_generate_vm_configuration.py | 3 +++ tests/core/transaction-utils/test_receipt_encoding.py | 5 +++-- .../transaction-utils/test_transaction_encoding.py | 3 ++- tests/database/test_eth1_chaindb.py | 3 ++- 11 files changed, 33 insertions(+), 5 deletions(-) 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..e54b1433e2 100644 --- a/eth/tools/builder/chain/builders.py +++ b/eth/tools/builder/chain/builders.py @@ -73,6 +73,7 @@ IstanbulVM, MuirGlacierVM, BerlinVM, + LondonVM, ) @@ -237,6 +238,7 @@ 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 diff --git a/tests/conftest.py b/tests/conftest.py index d955f33c21..c3e4aefc07 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,6 +33,7 @@ IstanbulVM, MuirGlacierVM, BerlinVM, + LondonVM, ) # @@ -93,6 +94,7 @@ def _file_logging(request): IstanbulVM, MuirGlacierVM, BerlinVM, + LondonVM, ]) def VM(request): return request.param diff --git a/tests/core/builder-tools/test_chain_construction.py b/tests/core/builder-tools/test_chain_construction.py index eadfb72ebf..6535d9c652 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,6 +90,7 @@ def test_chain_builder_construct_chain_vm_configuration_multiple_forks(): (istanbul_at, IstanbulVM), (muir_glacier_at, MuirGlacierVM), (berlin_at, BerlinVM), + (london_at, LondonVM), (latest_mainnet_at, MuirGlacierVM), # this will change whenever the next upgrade is locked ) ) diff --git a/tests/core/chain-object/test_contract_call.py b/tests/core/chain-object/test_contract_call.py index 94e0e5b370..78f296c81f 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, ) @@ -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..1d50c977d3 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 diff --git a/tests/core/opcodes/test_opcodes.py b/tests/core/opcodes/test_opcodes.py index 71ffc1f336..f93a6220b9 100644 --- a/tests/core/opcodes/test_opcodes.py +++ b/tests/core/opcodes/test_opcodes.py @@ -47,6 +47,7 @@ IstanbulVM, MuirGlacierVM, BerlinVM, + LondonVM, ) from eth.vm.message import ( Message, @@ -1009,6 +1010,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 +1323,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..1c6a96cff3 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): @@ -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..f3cab6b9a9 100644 --- a/tests/core/transaction-utils/test_receipt_encoding.py +++ b/tests/core/transaction-utils/test_receipt_encoding.py @@ -10,6 +10,7 @@ from eth.rlp.receipts import Receipt from eth.vm.forks import ( BerlinVM, + LondonVM, ) from eth.vm.forks.berlin.receipts import ( TypedReceipt, @@ -30,7 +31,7 @@ ) -@pytest.mark.parametrize('vm_class', [BerlinVM]) +@pytest.mark.parametrize('vm_class', [BerlinVM, LondonVM]) @pytest.mark.parametrize( 'encoded, expected', ( @@ -70,7 +71,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', ( diff --git a/tests/core/transaction-utils/test_transaction_encoding.py b/tests/core/transaction-utils/test_transaction_encoding.py index 44226a7cf5..98cfef6a18 100644 --- a/tests/core/transaction-utils/test_transaction_encoding.py +++ b/tests/core/transaction-utils/test_transaction_encoding.py @@ -10,6 +10,7 @@ from eth.exceptions import UnrecognizedTransactionType from eth.vm.forks import ( BerlinVM, + LondonVM, ) RECOGNIZED_TRANSACTION_TYPES = {1} @@ -27,7 +28,7 @@ ) -@pytest.mark.parametrize('vm_class', [BerlinVM]) +@pytest.mark.parametrize('vm_class', [BerlinVM, LondonVM]) @pytest.mark.parametrize( 'encoded, expected', ( diff --git a/tests/database/test_eth1_chaindb.py b/tests/database/test_eth1_chaindb.py index 68ccaafa50..d7ab11e2e6 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, @@ -377,7 +378,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): From cf48c45834d7061f5b54723b43c89baa114e770c Mon Sep 17 00:00:00 2001 From: Francesco Truzzi Date: Tue, 11 May 2021 16:47:00 +0200 Subject: [PATCH 08/10] fixes --- eth/db/header.py | 12 +++++++- eth/rlp/headers.py | 2 +- eth/rlp/transactions.py | 17 ----------- eth/vm/execution_context.py | 5 ++-- eth/vm/forks/london/execution_context.py | 4 +-- eth/vm/forks/london/state.py | 18 ++++++++---- eth/vm/forks/london/transactions.py | 37 +++--------------------- 7 files changed, 33 insertions(+), 62 deletions(-) diff --git a/eth/db/header.py b/eth/db/header.py index 696796a43d..819a21701d 100644 --- a/eth/db/header.py +++ b/eth/db/header.py @@ -56,7 +56,13 @@ validate_block_number, validate_word, ) +from eth.vm.forks.london.blocks import ( + LondonBlockHeader +) +from rlp.exceptions import ( + ObjectDeserializationError +) class HeaderDB(HeaderDatabaseAPI): def __init__(self, db: AtomicDatabaseAPI) -> None: @@ -619,4 +625,8 @@ 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) + try: + return rlp.decode(header_rlp, sedes=BlockHeader) + except ObjectDeserializationError: + # could be a new >=London block header + return rlp.decode(header_rlp, sedes=LondonBlockHeader) diff --git a/eth/rlp/headers.py b/eth/rlp/headers.py index 6f078cbf08..4bdd2f24ff 100644 --- a/eth/rlp/headers.py +++ b/eth/rlp/headers.py @@ -206,4 +206,4 @@ def is_genesis(self) -> bool: @property def base_fee_per_gas(self) -> Optional[int]: - return None + return 0 diff --git a/eth/rlp/transactions.py b/eth/rlp/transactions.py index e6b674dfc9..89245c60e0 100644 --- a/eth/rlp/transactions.py +++ b/eth/rlp/transactions.py @@ -54,23 +54,6 @@ def chain_id(self) -> Optional[int]: def access_list(self) -> Sequence[Tuple[Address, Sequence[int]]]: return [] - # @property - # def max_priority_fee_per_gas(self) -> Optional[int]: - # return None - - # @max_priority_fee_per_gas.setter - # def max_priority_fee_per_gas(self, value: int) -> None: - # self.max_priority_fee_per_gas = value - - # @property - # def max_fee_per_gas(self) -> Optional[int]: - # return None - - # @max_fee_per_gas.setter - # def max_fee_per_gas(self, value: int) -> None: - # self.max_fee_per_gas = value - - BASE_TRANSACTION_FIELDS = [ ('nonce', big_endian_int), diff --git a/eth/vm/execution_context.py b/eth/vm/execution_context.py index b7b7472f28..ed4129bcb6 100644 --- a/eth/vm/execution_context.py +++ b/eth/vm/execution_context.py @@ -22,7 +22,7 @@ class ExecutionContext(ExecutionContextAPI): _gas_limit = None _prev_hashes = None _chain_id = None - _base_gas_fee = None + _base_gas_fee = 0 # TODO check if valid def __init__( self, @@ -41,7 +41,8 @@ def __init__( self._gas_limit = gas_limit self._prev_hashes = CachedIterable(prev_hashes) self._chain_id = chain_id - self._base_gas_fee = base_gas_fee + if base_gas_fee is not None: + self._base_gas_fee = base_gas_fee @property def coinbase(self) -> Address: diff --git a/eth/vm/forks/london/execution_context.py b/eth/vm/forks/london/execution_context.py index c8347ec8f4..70e2369b0b 100644 --- a/eth/vm/forks/london/execution_context.py +++ b/eth/vm/forks/london/execution_context.py @@ -3,5 +3,5 @@ class LondonExecutionContext(ExecutionContextAPI): def __init__(self, base_gas_fee: int, **kwargs): # TODO remove kwargs - super().__init__(**kwargs) - self._base_gas_fee = base_gas_fee \ No newline at end of file + self._base_gas_fee = base_gas_fee + super().__init__(**kwargs) \ No newline at end of file diff --git a/eth/vm/forks/london/state.py b/eth/vm/forks/london/state.py index 6c24e1a0fc..c45dacee84 100644 --- a/eth/vm/forks/london/state.py +++ b/eth/vm/forks/london/state.py @@ -36,7 +36,7 @@ ) from .computation import LondonComputation -from .transactions import LondonLegacyTransaction, LondonTypedTransaction, new_normalize_transaction, normalize_transaction +from .transactions import LondonLegacyTransaction, LondonTypedTransaction, LondonUnsignedLegacyTransaction, normalize_transaction from .validation import validate_london_normalized_transaction @@ -162,7 +162,7 @@ def validate_transaction( if transaction.s > SECPK1_N // 2 or transaction.s == 0: raise ValidationError("Invalid signature S value") - normalized_transaction = new_normalize_transaction(transaction) + normalized_transaction = normalize_transaction(transaction) validate_london_normalized_transaction( state=self, transaction=normalized_transaction, @@ -175,10 +175,16 @@ def get_transaction_context(self: StateAPI, London-specific transaction context creation, where gas_price includes the block base fee """ - priority_fee_per_gas = min( - transaction.max_priority_fee_per_gas, - transaction.max_fee_per_gas - self.execution_context.base_gas_fee - ) + if isinstance(transaction, LondonTypedTransaction): # TODO probably do this somewhere else + priority_fee_per_gas = min( + transaction.max_priority_fee_per_gas, + transaction.max_fee_per_gas - self.execution_context.base_gas_fee + ) + else: + priority_fee_per_gas = min( + transaction.gas_price, + transaction.gas_price - self.execution_context.base_gas_fee + ) effective_gas_price = self.execution_context.base_gas_fee + priority_fee_per_gas return self.get_transaction_context_class()( diff --git a/eth/vm/forks/london/transactions.py b/eth/vm/forks/london/transactions.py index f04ea93378..26e76da564 100644 --- a/eth/vm/forks/london/transactions.py +++ b/eth/vm/forks/london/transactions.py @@ -12,7 +12,6 @@ from eth_utils.exceptions import ValidationError from eth.abc import ( - BaseTransactionAPI, ReceiptAPI, SignedTransactionAPI, TransactionDecoderAPI, @@ -37,6 +36,9 @@ from eth.vm.forks.istanbul.transactions import ( ISTANBUL_TX_GAS_SCHEDULE, ) +from eth.vm.spoof import ( + SpoofTransaction, +) from eth._utils.transactions import ( calculate_intrinsic_gas, @@ -332,41 +334,10 @@ def new_base_gas_price_transaction( ) return LondonTypedTransaction(BASE_GAS_FEE_TRANSACTION_TYPE, transaction) - def normalize_transaction( - transaction: SignedTransactionAPI - ) -> LondonNormalizedTransaction: - - # fields common to all transactions - fields = { - "sender": transaction.sender, - "nonce": transaction.nonce, - "gas": transaction.gas, - "to": transaction.to, - "value": transaction.value, - "data": transaction.data, - "access_list": [], - } - - if isinstance(transaction, (LondonLegacyTransaction, LondonTypedTransaction)): - fields["max_priority_fee_per_gas"] = transaction.gas_price - fields["max_fee_per_gas"] = transaction.gas_price - if isinstance(transaction, LondonTypedTransaction): - fields["access_list"] = transaction.access_list - if transaction.type_id == BASE_GAS_FEE_TRANSACTION_TYPE: - fields["max_priority_fee_per_gas"] = transaction.max_priority_fee_per_gas - fields["max_fee_per_gas"] = transaction.max_fee_per_gas - elif transaction.type_id != ACCESS_LIST_TRANSACTION_TYPE: - raise ValidationError(f"Invalid transaction type_id: {transaction.type_id}") - - return LondonNormalizedTransaction(**fields) # type: ignore - - raise ValidationError(f"Invalid transaction type: {type(transaction)}") - -def new_normalize_transaction( transaction: SignedTransactionAPI ) -> SignedTransactionAPI: - if isinstance(transaction, LondonLegacyTransaction): + if isinstance(transaction, (LondonLegacyTransaction, SpoofTransaction)): transaction.max_priority_fee_per_gas = transaction.gas_price transaction.max_fee_per_gas = transaction.gas_price elif isinstance(transaction, LondonTypedTransaction): From 48ad8fbd8bb4267695225e5eba279ca341ac7461 Mon Sep 17 00:00:00 2001 From: Francesco Truzzi Date: Thu, 20 May 2021 20:05:54 +0200 Subject: [PATCH 09/10] update block according to latest EIP updates --- eth/vm/forks/london/__init__.py | 41 ++++++++++++++++++++----------- eth/vm/forks/london/constants.py | 3 +++ eth/vm/forks/london/validation.py | 4 --- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/eth/vm/forks/london/__init__.py b/eth/vm/forks/london/__init__.py index b6464def94..a43c6e3f17 100644 --- a/eth/vm/forks/london/__init__.py +++ b/eth/vm/forks/london/__init__.py @@ -10,7 +10,10 @@ from .blocks import LondonBlock from .constants import ( BASE_FEE_MAX_CHANGE_DENOMINATOR, - ELASTICITY_MULTIPLIER + ELASTICITY_MULTIPLIER, + INITIAL_BASE_FEE, + INITIAL_FORK_BLOCK_NUMBER, + MINIMUM_GAS_LIMIT ) from .headers import ( compute_london_difficulty, @@ -37,8 +40,9 @@ class LondonVM(BerlinVM): @staticmethod def calculate_expected_base_fee_per_gas(parent_header: BlockHeaderAPI) -> int: parent_base_fee_per_gas = parent_header.base_fee_per_gas - if parent_base_fee_per_gas is None: # TODO parent header is non-London - parent_base_fee_per_gas = 0 + if parent_header.block_number + 1 == INITIAL_FORK_BLOCK_NUMBER: + return INITIAL_BASE_FEE + parent_gas_target = parent_header.gas_limit parent_gas_used = parent_header.gas_used @@ -67,26 +71,35 @@ def validate_header(cls, header: BlockHeaderAPI, parent_header: BlockHeaderAPI) -> None: - header_gas_target = header.gas_limit - parent_gas_target = parent_header.gas_limit + parent_gas_target = parent_header.gas_limit // ELASTICITY_MULTIPLIER + + # On the fork block, don't account for the ELASTICITY_MULTIPLIER + # to avoid unduly halving the gas target. + if INITIAL_FORK_BLOCK_NUMBER == header.block_number: + parent_gas_target = parent_header.gas_limit + parent_gas_limit = parent_header.gas_limit * ELASTICITY_MULTIPLIER - max_usable_gas = header_gas_target * ELASTICITY_MULTIPLIER - if header.gas_used > max_usable_gas: + if header.gas_used > header.gas_limit: raise ValidationError( f"Block used too much gas: {header.gas_used} " - f"(max: {max_usable_gas})" + f"(max: {header.gas_limit})" + ) + + if header.gas_limit > parent_gas_limit + (parent_gas_limit // 1024): + raise ValidationError( + f"Gas limit increased too much (from {parent_gas_limit} " + f"to {header.gas_limit})" ) - if header_gas_target > parent_gas_target + (parent_gas_target // 1024): + if header.gas_limit < parent_gas_target - (parent_gas_target // 1024): raise ValidationError( - f"Gas target increased too much (from {parent_gas_target} " - f"to {header_gas_target})" + f"Gas limit decreased too much (from {parent_gas_target} " + f"to {header.gas_limit})" ) - if header_gas_target < parent_gas_target - (parent_gas_target // 1024): + if header.gas_limit < MINIMUM_GAS_LIMIT: raise ValidationError( - f"Gas target decreased too much (from {parent_gas_target} " - f"to {header_gas_target})" + f"Gas limit is lower than the minimum ({header.gas_limit} < {MINIMUM_GAS_LIMIT})" ) expected_base_fee_per_gas = LondonVM.calculate_expected_base_fee_per_gas(parent_header) diff --git a/eth/vm/forks/london/constants.py b/eth/vm/forks/london/constants.py index d4a33d9ed7..54832e4e43 100644 --- a/eth/vm/forks/london/constants.py +++ b/eth/vm/forks/london/constants.py @@ -9,4 +9,7 @@ BASE_GAS_FEE_STORAGE_KEY_COST = ACCESS_LIST_STORAGE_KEY_COST_EIP_2930 BASE_FEE_MAX_CHANGE_DENOMINATOR = 8 +INITIAL_BASE_FEE = 1000000000 +INITIAL_FORK_BLOCK_NUMBER = -1 +MINIMUM_GAS_LIMIT = 5000 ELASTICITY_MULTIPLIER = 2 diff --git a/eth/vm/forks/london/validation.py b/eth/vm/forks/london/validation.py index 3c30cea5ed..75b2de2fa5 100644 --- a/eth/vm/forks/london/validation.py +++ b/eth/vm/forks/london/validation.py @@ -5,10 +5,6 @@ from eth_utils.exceptions import ValidationError -from .blocks import LondonBlockHeader -from .transactions import LondonNormalizedTransaction - - def validate_london_normalized_transaction( state: StateAPI, transaction: SignedTransactionAPI, From 3b9eb9a33e034c57be40a5715c2101f179e684e0 Mon Sep 17 00:00:00 2001 From: Francesco Truzzi Date: Thu, 20 May 2021 20:30:31 +0200 Subject: [PATCH 10/10] remove unused LondonNormalizedTransaction class --- eth/vm/forks/london/transactions.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/eth/vm/forks/london/transactions.py b/eth/vm/forks/london/transactions.py index 26e76da564..548495e622 100644 --- a/eth/vm/forks/london/transactions.py +++ b/eth/vm/forks/london/transactions.py @@ -91,34 +91,6 @@ def as_signed_transaction(self, -class LondonNormalizedTransaction(): - """ - A normalized transaction, used for validation purposes - """ - def __init__(self, - sender: Address, - nonce: int, - gas: int, - max_priority_fee_per_gas: int, - max_fee_per_gas: int, - to: Address, - value: int, - data: bytes, - access_list: Sequence[Tuple[Address, Sequence[int]]], - gas_price: Optional[int] = None): # EIP-1559 effective gas price - self.sender = sender - self.nonce = nonce - self.gas = gas - self.max_priority_fee_per_gas = max_priority_fee_per_gas - self.max_fee_per_gas = max_fee_per_gas - self.to = to - self.value = value - self.data = data - self.access_list = access_list - if gas_price is not None: - self.gas_price = gas_price - - class UnsignedBaseGasFeeTransaction(rlp.Serializable): _type_id = BASE_GAS_FEE_TRANSACTION_TYPE fields = [