From 4ddc4f0d7b853f8e64c4eb6879bc86497015262e Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Mon, 4 Nov 2024 09:56:52 -0600 Subject: [PATCH] feat(forks): Add gas costs functions (#779) * feat(forks): Add memory expansion, calldata calculators to each fork * fix(fw): Remove `copy_opcode_cost`, `cost_memory_bytes` and `eip_2028_transaction_data_cost` * refactor(tests): Use fork calculator methods instead of helpers * refactor(base_types): Move `AccessList` to base types * fix(forks): GasCosts field description * fix(forks): Initcode word cost * feat(forks): Add transaction_intrinsic_cost_calculator * refactor(tests): Use fork gas calculator methods * refactor(forks): Add authorization to intrinsic gas cost calc * refactor(plugins/execute): Use fork gas calc functions * refactor(tests): Use `fork` gas calc functions * docs: changelog --- docs/CHANGELOG.md | 1 + src/ethereum_test_base_types/__init__.py | 3 +- .../composite_types.py | 17 +- .../tests/test_base_types.py | 49 +++- src/ethereum_test_fixtures/state.py | 3 +- .../tests/test_blockchain.py | 2 +- src/ethereum_test_forks/base_fork.py | 90 ++++++- src/ethereum_test_forks/forks/forks.py | 246 +++++++++++++++++- src/ethereum_test_forks/gas_costs.py | 62 +++++ src/ethereum_test_forks/tests/test_forks.py | 67 ++++- src/ethereum_test_tools/__init__.py | 9 +- src/ethereum_test_types/__init__.py | 8 - src/ethereum_test_types/helpers.py | 40 --- src/ethereum_test_types/tests/test_types.py | 19 +- src/ethereum_test_types/types.py | 17 +- src/pytest_plugins/execute/pre_alloc.py | 19 +- tests/cancun/eip4844_blobs/test_blob_txs.py | 14 +- .../eip4844_blobs/test_blob_txs_full.py | 2 +- .../test_point_evaluation_precompile.py | 6 +- .../test_point_evaluation_precompile_gas.py | 20 +- .../test_mcopy_memory_expansion.py | 21 +- .../eip7069_extcall/test_gas.py | 9 +- .../test_returndatacopy_memory_expansion.py | 12 +- .../test_datacopy_memory_expansion.py | 11 +- .../eip7620_eof_create/test_gas.py | 7 +- tests/prague/eip7702_set_code_tx/test_gas.py | 28 +- tests/shanghai/eip3860_initcode/helpers.py | 51 +--- .../eip3860_initcode/test_initcode.py | 87 ++++--- .../eip4895_withdrawals/test_withdrawals.py | 2 +- 29 files changed, 667 insertions(+), 255 deletions(-) create mode 100644 src/ethereum_test_forks/gas_costs.py diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6968108356..003a81c95a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -51,6 +51,7 @@ Test fixtures for use by clients are available for each release on the [Github r - 💥 Rename the `PragueEIP7692` fork to `Osaka` ([#869](https://github.com/ethereum/execution-spec-tests/pull/869)). - ✨ Improve `fill` terminal output to emphasize that filling tests is not actually testing a client ([#807](https://github.com/ethereum/execution-spec-tests/pull/887)). - ✨ Add the `BlockchainTestEngine` test spec type that only generates a fixture in the `EngineFixture` (`blockchain_test_engine`) format ([#888](https://github.com/ethereum/execution-spec-tests/pull/888)). +- 🔀 `ethereum_test_forks` forks now contain gas-calculating functions, which return the appropriate function to calculate the gas used by a transaction or memory function for the given fork ([#779](https://github.com/ethereum/execution-spec-tests/pull/779)). ### 🔧 EVM Tools diff --git a/src/ethereum_test_base_types/__init__.py b/src/ethereum_test_base_types/__init__.py index 8dc5e80395..7348b84044 100644 --- a/src/ethereum_test_base_types/__init__.py +++ b/src/ethereum_test_base_types/__init__.py @@ -18,7 +18,7 @@ Wei, ZeroPaddedHexNumber, ) -from .composite_types import Account, Alloc, Storage, StorageRootType +from .composite_types import AccessList, Account, Alloc, Storage, StorageRootType from .constants import ( AddrAA, AddrBB, @@ -35,6 +35,7 @@ from .reference_spec import ReferenceSpec __all__ = ( + "AccessList", "Account", "AddrAA", "AddrBB", diff --git a/src/ethereum_test_base_types/composite_types.py b/src/ethereum_test_base_types/composite_types.py index 7627c5b107..dbba9896fe 100644 --- a/src/ethereum_test_base_types/composite_types.py +++ b/src/ethereum_test_base_types/composite_types.py @@ -2,7 +2,7 @@ Base composite types for Ethereum test cases. """ from dataclasses import dataclass -from typing import Any, ClassVar, Dict, SupportsBytes, Type, TypeAlias +from typing import Any, ClassVar, Dict, List, SupportsBytes, Type, TypeAlias from pydantic import Field, PrivateAttr, RootModel, TypeAdapter @@ -448,3 +448,18 @@ class Alloc(RootModel[Dict[Address, Account | None]]): """ root: Dict[Address, Account | None] = Field(default_factory=dict, validate_default=True) + + +class AccessList(CamelModel): + """ + Access List for transactions. + """ + + address: Address + storage_keys: List[Hash] + + def to_list(self) -> List[Address | List[Hash]]: + """ + Returns the access list as a list of serializable elements. + """ + return [self.address, self.storage_keys] diff --git a/src/ethereum_test_base_types/tests/test_base_types.py b/src/ethereum_test_base_types/tests/test_base_types.py index 4bc8413e16..e4a8fa5ebb 100644 --- a/src/ethereum_test_base_types/tests/test_base_types.py +++ b/src/ethereum_test_base_types/tests/test_base_types.py @@ -2,11 +2,13 @@ Test suite for `ethereum_test` module base types. """ -from typing import Any +from typing import Any, Dict import pytest from ..base_types import Address, Hash, Wei +from ..composite_types import AccessList +from ..json import to_json @pytest.mark.parametrize( @@ -91,3 +93,48 @@ def test_wei_parsing(s: str, expected: int): Test the parsing of wei values. """ assert Wei(s) == expected + + +@pytest.mark.parametrize( + ["can_be_deserialized", "model_instance", "json"], + [ + pytest.param( + True, + AccessList( + address=0x1234, + storage_keys=[0, 1], + ), + { + "address": "0x0000000000000000000000000000000000001234", + "storageKeys": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001", + ], + }, + id="access_list", + ), + ], +) +class TestPydanticModelConversion: + """ + Test that Pydantic models are converted to and from JSON correctly. + """ + + def test_json_serialization( + self, can_be_deserialized: bool, model_instance: Any, json: str | Dict[str, Any] + ): + """ + Test that to_json returns the expected JSON for the given object. + """ + assert to_json(model_instance) == json + + def test_json_deserialization( + self, can_be_deserialized: bool, model_instance: Any, json: str | Dict[str, Any] + ): + """ + Test that to_json returns the expected JSON for the given object. + """ + if not can_be_deserialized: + pytest.skip(reason="The model instance in this case can not be deserialized") + model_type = type(model_instance) + assert model_type(**json) == model_instance diff --git a/src/ethereum_test_fixtures/state.py b/src/ethereum_test_fixtures/state.py index 7b2cbc57e2..b317601f8a 100644 --- a/src/ethereum_test_fixtures/state.py +++ b/src/ethereum_test_fixtures/state.py @@ -6,10 +6,9 @@ from pydantic import BaseModel, Field -from ethereum_test_base_types import Address, Alloc, Bytes, Hash, ZeroPaddedHexNumber +from ethereum_test_base_types import AccessList, Address, Alloc, Bytes, Hash, ZeroPaddedHexNumber from ethereum_test_exceptions import TransactionExceptionInstanceOrList from ethereum_test_types.types import ( - AccessList, AuthorizationTupleGeneric, CamelModel, EnvironmentGeneric, diff --git a/src/ethereum_test_fixtures/tests/test_blockchain.py b/src/ethereum_test_fixtures/tests/test_blockchain.py index 5276c1835b..b861ed554a 100644 --- a/src/ethereum_test_fixtures/tests/test_blockchain.py +++ b/src/ethereum_test_fixtures/tests/test_blockchain.py @@ -8,6 +8,7 @@ from pydantic import TypeAdapter from ethereum_test_base_types import ( + AccessList, Address, Bloom, BLSPublicKey, @@ -23,7 +24,6 @@ from ethereum_test_forks import Prague from ethereum_test_types import ( EOA, - AccessList, AuthorizationTuple, ConsolidationRequest, DepositRequest, diff --git a/src/ethereum_test_forks/base_fork.py b/src/ethereum_test_forks/base_fork.py index 1f468fba9c..19716201b5 100644 --- a/src/ethereum_test_forks/base_fork.py +++ b/src/ethereum_test_forks/base_fork.py @@ -7,10 +7,12 @@ from semver import Version -from ethereum_test_base_types import Address +from ethereum_test_base_types import AccessList, Address +from ethereum_test_base_types.conversions import BytesConvertible from ethereum_test_vm import EVMCodeType, Opcodes from .base_decorators import prefer_transition_to_method +from .gas_costs import GasCosts class ForkAttribute(Protocol): @@ -25,6 +27,49 @@ def __call__(self, block_number: int = 0, timestamp: int = 0) -> Any: pass +class MemoryExpansionGasCalculator(Protocol): + """ + A protocol to calculate the gas cost of memory expansion for a given fork. + """ + + def __call__(self, *, new_bytes: int, previous_bytes: int = 0) -> int: + """ + Returns the gas cost of expanding the memory by the given length. + """ + pass + + +class CalldataGasCalculator(Protocol): + """ + A protocol to calculate the transaction gas cost of calldata for a given fork. + """ + + def __call__(self, *, data: BytesConvertible) -> int: + """ + Returns the transaction gas cost of calldata given its contents. + """ + pass + + +class TransactionIntrinsicCostCalculator(Protocol): + """ + A protocol to calculate the intrinsic gas cost of a transaction for a given fork. + """ + + def __call__( + self, + *, + calldata: BytesConvertible = b"", + contract_creation: bool = False, + access_list: List[AccessList] | None = None, + authorization_count: int | None = None, + ) -> int: + """ + Returns the intrinsic gas cost of a transaction given its properties. + """ + pass + + class BaseForkMeta(ABCMeta): """ Metaclass for BaseFork @@ -160,6 +205,47 @@ def header_requests_required(cls, block_number: int, timestamp: int) -> bool: """ pass + # Gas related abstract methods + + @classmethod + @abstractmethod + def gas_costs(cls, block_number: int = 0, timestamp: int = 0) -> GasCosts: + """ + Returns a dataclass with the gas costs constants for the fork. + """ + pass + + @classmethod + @abstractmethod + def memory_expansion_gas_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> MemoryExpansionGasCalculator: + """ + Returns a callable that calculates the gas cost of memory expansion for the fork. + """ + pass + + @classmethod + @abstractmethod + def calldata_gas_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> CalldataGasCalculator: + """ + Returns a callable that calculates the transaction gas cost for its calldata + depending on its contents. + """ + pass + + @classmethod + @abstractmethod + def transaction_intrinsic_cost_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> TransactionIntrinsicCostCalculator: + """ + Returns a callable that calculates the intrinsic gas cost of a transaction for the fork. + """ + pass + @classmethod @abstractmethod def blob_gas_per_blob(cls, block_number: int, timestamp: int) -> int: @@ -176,6 +262,8 @@ def get_reward(cls, block_number: int = 0, timestamp: int = 0) -> int: """ pass + # Transaction related abstract methods + @classmethod @abstractmethod def tx_types(cls, block_number: int = 0, timestamp: int = 0) -> List[int]: diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index 6c20c84f67..ac3da65438 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -2,6 +2,7 @@ All Ethereum fork class definitions. """ +from dataclasses import replace from hashlib import sha256 from os.path import realpath from pathlib import Path @@ -9,15 +10,30 @@ from semver import Version -from ethereum_test_base_types import Address +from ethereum_test_base_types import AccessList, Address, Bytes +from ethereum_test_base_types.conversions import BytesConvertible from ethereum_test_vm import EVMCodeType, Opcodes -from ..base_fork import BaseFork +from ..base_fork import ( + BaseFork, + CalldataGasCalculator, + MemoryExpansionGasCalculator, + TransactionIntrinsicCostCalculator, +) +from ..gas_costs import GasCosts CURRENT_FILE = Path(realpath(__file__)) CURRENT_FOLDER = CURRENT_FILE.parent +def ceiling_division(a: int, b: int) -> int: + """ + Calculates the ceil without using floating point. + Used by many of the EVM's formulas + """ + return -(a // -b) + + # All forks must be listed here !!! in the order they were introduced !!! class Frontier(BaseFork, solc_name="homestead"): """ @@ -91,6 +107,124 @@ def header_blob_gas_used_required(cls, block_number: int = 0, timestamp: int = 0 """ return False + @classmethod + def gas_costs(cls, block_number: int = 0, timestamp: int = 0) -> GasCosts: + """ + Returns a dataclass with the defined gas costs constants for genesis. + """ + return GasCosts( + G_JUMPDEST=1, + G_BASE=2, + G_VERY_LOW=3, + G_LOW=5, + G_MID=8, + G_HIGH=10, + G_WARM_ACCOUNT_ACCESS=100, + G_COLD_ACCOUNT_ACCESS=2_600, + G_ACCESS_LIST_ADDRESS=2_400, + G_ACCESS_LIST_STORAGE=1_900, + G_WARM_SLOAD=100, + G_COLD_SLOAD=2_100, + G_STORAGE_SET=20_000, + G_STORAGE_RESET=2_900, + R_STORAGE_CLEAR=4_800, + G_SELF_DESTRUCT=5_000, + G_CREATE=32_000, + G_CODE_DEPOSIT_BYTE=200, + G_INITCODE_WORD=2, + G_CALL_VALUE=9_000, + G_CALL_STIPEND=2_300, + G_NEW_ACCOUNT=25_000, + G_EXP=10, + G_EXP_BYTE=50, + G_MEMORY=3, + G_TX_DATA_ZERO=4, + G_TX_DATA_NON_ZERO=68, + G_TRANSACTION=21_000, + G_TRANSACTION_CREATE=32_000, + G_LOG=375, + G_LOG_DATA=8, + G_LOG_TOPIC=375, + G_KECCAK_256=30, + G_KECCAK_256_WORD=6, + G_COPY=3, + G_BLOCKHASH=20, + G_AUTHORIZATION=25_000, + ) + + @classmethod + def memory_expansion_gas_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> MemoryExpansionGasCalculator: + """ + Returns a callable that calculates the gas cost of memory expansion for the fork. + """ + gas_costs = cls.gas_costs(block_number, timestamp) + + def fn(*, new_bytes: int, previous_bytes: int = 0) -> int: + if new_bytes <= previous_bytes: + return 0 + new_words = ceiling_division(new_bytes, 32) + previous_words = ceiling_division(previous_bytes, 32) + + def c(w: int) -> int: + return (gas_costs.G_MEMORY * w) + ((w * w) // 512) + + return c(new_words) - c(previous_words) + + return fn + + @classmethod + def calldata_gas_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> CalldataGasCalculator: + """ + Returns a callable that calculates the transaction gas cost for its calldata + depending on its contents. + """ + gas_costs = cls.gas_costs(block_number, timestamp) + + def fn(*, data: BytesConvertible) -> int: + cost = 0 + for b in Bytes(data): + if b == 0: + cost += gas_costs.G_TX_DATA_ZERO + else: + cost += gas_costs.G_TX_DATA_NON_ZERO + return cost + + return fn + + @classmethod + def transaction_intrinsic_cost_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> TransactionIntrinsicCostCalculator: + """ + Returns a callable that calculates the intrinsic gas cost of a transaction for the fork. + """ + gas_costs = cls.gas_costs(block_number, timestamp) + calldata_gas_calculator = cls.calldata_gas_calculator(block_number, timestamp) + + def fn( + *, + calldata: BytesConvertible = b"", + contract_creation: bool = False, + access_list: List[AccessList] | None = None, + authorization_count: int | None = None, + ) -> int: + assert access_list is None, f"Access list is not supported in {cls.name()}" + assert authorization_count is None, f"Authorizations are not supported in {cls.name()}" + intrinsic_cost: int = gas_costs.G_TRANSACTION + + if contract_creation: + intrinsic_cost += gas_costs.G_INITCODE_WORD * ceiling_division( + len(Bytes(calldata)), 32 + ) + + return intrinsic_cost + calldata_gas_calculator(data=calldata) + + return fn + @classmethod def blob_gas_per_blob(cls, block_number: int, timestamp: int) -> int: """ @@ -425,6 +559,37 @@ def valid_opcodes( """ return [Opcodes.DELEGATECALL] + super(Homestead, cls).valid_opcodes() + @classmethod + def transaction_intrinsic_cost_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> TransactionIntrinsicCostCalculator: + """ + At Homestead, the transaction intrinsic cost needs to take contract creation into account. + """ + super_fn = super(Homestead, cls).transaction_intrinsic_cost_calculator( + block_number, timestamp + ) + gas_costs = cls.gas_costs(block_number, timestamp) + + def fn( + *, + calldata: BytesConvertible = b"", + contract_creation: bool = False, + access_list: List[AccessList] | None = None, + authorization_count: int | None = None, + ) -> int: + intrinsic_cost: int = super_fn( + calldata=calldata, + contract_creation=contract_creation, + access_list=access_list, + authorization_count=authorization_count, + ) + if contract_creation: + intrinsic_cost += gas_costs.G_TRANSACTION_CREATE + return intrinsic_cost + + return fn + class Byzantium(Homestead): """ @@ -540,6 +705,16 @@ def valid_opcodes( """ return [Opcodes.CHAINID, Opcodes.SELFBALANCE] + super(Istanbul, cls).valid_opcodes() + @classmethod + def gas_costs(cls, block_number: int = 0, timestamp: int = 0) -> GasCosts: + """ + Returns a dataclass with the defined gas costs constants for genesis. + """ + return replace( + super(Istanbul, cls).gas_costs(block_number, timestamp), + G_TX_DATA_NON_ZERO=16, # https://eips.ethereum.org/EIPS/eip-2028 + ) + # Glacier forks skipped, unless explicitly specified class MuirGlacier(Istanbul, solc_name="istanbul", ignore=True): @@ -569,6 +744,39 @@ def contract_creating_tx_types(cls, block_number: int = 0, timestamp: int = 0) - """ return [1] + super(Berlin, cls).contract_creating_tx_types(block_number, timestamp) + @classmethod + def transaction_intrinsic_cost_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> TransactionIntrinsicCostCalculator: + """ + At Berlin, the transaction intrinsic cost needs to take the access list into account + """ + super_fn = super(Berlin, cls).transaction_intrinsic_cost_calculator( + block_number, timestamp + ) + gas_costs = cls.gas_costs(block_number, timestamp) + + def fn( + *, + calldata: BytesConvertible = b"", + contract_creation: bool = False, + access_list: List[AccessList] | None = None, + authorization_count: int | None = None, + ) -> int: + intrinsic_cost: int = super_fn( + calldata=calldata, + contract_creation=contract_creation, + authorization_count=authorization_count, + ) + if access_list is not None: + for access in access_list: + intrinsic_cost += gas_costs.G_ACCESS_LIST_ADDRESS + for _ in access.storage_keys: + intrinsic_cost += gas_costs.G_ACCESS_LIST_STORAGE + return intrinsic_cost + + return fn + class London(Berlin): """ @@ -769,7 +977,7 @@ def pre_allocation_blockchain(cls) -> Mapping: "5ffd5b62001fff42064281555f359062001fff015500", } } - return new_allocation | super(Cancun, cls).pre_allocation_blockchain() + return new_allocation | super(Cancun, cls).pre_allocation_blockchain() # type: ignore @classmethod def engine_new_payload_version( @@ -868,6 +1076,36 @@ def max_request_type(cls, block_number: int = 0, timestamp: int = 0) -> int: """ return 2 + @classmethod + def transaction_intrinsic_cost_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> TransactionIntrinsicCostCalculator: + """ + At Prague, the transaction intrinsic cost needs to take the authorizations into account + """ + super_fn = super(Prague, cls).transaction_intrinsic_cost_calculator( + block_number, timestamp + ) + gas_costs = cls.gas_costs(block_number, timestamp) + + def fn( + *, + calldata: BytesConvertible = b"", + contract_creation: bool = False, + access_list: List[AccessList] | None = None, + authorization_count: int | None = None, + ) -> int: + intrinsic_cost: int = super_fn( + calldata=calldata, + contract_creation=contract_creation, + access_list=access_list, + ) + if authorization_count is not None: + intrinsic_cost += authorization_count * gas_costs.G_AUTHORIZATION + return intrinsic_cost + + return fn + @classmethod def pre_allocation_blockchain(cls) -> Mapping: """ @@ -928,7 +1166,7 @@ def pre_allocation_blockchain(cls) -> Mapping: } ) - return new_allocation | super(Prague, cls).pre_allocation_blockchain() + return new_allocation | super(Prague, cls).pre_allocation_blockchain() # type: ignore @classmethod def header_requests_required(cls, block_number: int, timestamp: int) -> bool: diff --git a/src/ethereum_test_forks/gas_costs.py b/src/ethereum_test_forks/gas_costs.py new file mode 100644 index 0000000000..485e8819a3 --- /dev/null +++ b/src/ethereum_test_forks/gas_costs.py @@ -0,0 +1,62 @@ +""" +Defines the data class that will contain gas cost constants on each fork. +""" + +from dataclasses import dataclass + + +@dataclass(kw_only=True, frozen=True) +class GasCosts: + """ + Class that contains the gas cost constants for any fork. + """ + + G_JUMPDEST: int + G_BASE: int + G_VERY_LOW: int + G_LOW: int + G_MID: int + G_HIGH: int + G_WARM_ACCOUNT_ACCESS: int + G_COLD_ACCOUNT_ACCESS: int + G_ACCESS_LIST_ADDRESS: int + G_ACCESS_LIST_STORAGE: int + G_WARM_SLOAD: int + G_COLD_SLOAD: int + G_STORAGE_SET: int + G_STORAGE_RESET: int + + R_STORAGE_CLEAR: int + + G_SELF_DESTRUCT: int + G_CREATE: int + + G_CODE_DEPOSIT_BYTE: int + G_INITCODE_WORD: int + + G_CALL_VALUE: int + G_CALL_STIPEND: int + G_NEW_ACCOUNT: int + + G_EXP: int + G_EXP_BYTE: int + + G_MEMORY: int + + G_TX_DATA_ZERO: int + G_TX_DATA_NON_ZERO: int + + G_TRANSACTION: int + G_TRANSACTION_CREATE: int + + G_LOG: int + G_LOG_DATA: int + G_LOG_TOPIC: int + + G_KECCAK_256: int + G_KECCAK_256_WORD: int + + G_COPY: int + G_BLOCKHASH: int + + G_AUTHORIZATION: int diff --git a/src/ethereum_test_forks/tests/test_forks.py b/src/ethereum_test_forks/tests/test_forks.py index 36a39c99e1..b723cd6abf 100644 --- a/src/ethereum_test_forks/tests/test_forks.py +++ b/src/ethereum_test_forks/tests/test_forks.py @@ -4,17 +4,27 @@ from typing import Mapping, cast +import pytest from semver import Version from ..base_fork import Fork -from ..forks.forks import Berlin, Cancun, Frontier, London, Paris, Prague, Shanghai +from ..forks.forks import ( + Berlin, + Cancun, + Frontier, + Homestead, + Istanbul, + London, + Paris, + Prague, + Shanghai, +) from ..forks.transition import BerlinToLondonAt5, ParisToShanghaiAtTime15k from ..helpers import ( forks_from, forks_from_until, get_closest_fork_with_solc_support, get_deployed_forks, - get_development_forks, get_forks, get_forks_with_solc_support, transition_fork_from_to, @@ -196,7 +206,7 @@ class PreAllocTransitionFork(PrePreAllocFork): pass -def test_pre_alloc(): +def test_pre_alloc(): # noqa: D103 assert PrePreAllocFork.pre_allocation() == {"test": "test"} assert PreAllocFork.pre_allocation() == {"test": "test", "test2": "test2"} assert PreAllocTransitionFork.pre_allocation() == { @@ -209,21 +219,64 @@ def test_pre_alloc(): } -def test_precompiles(): +def test_precompiles(): # noqa: D103 Cancun.precompiles() == list(range(11))[1:] -def test_tx_types(): +def test_tx_types(): # noqa: D103 Cancun.tx_types() == list(range(4)) -def test_solc_versioning(): +def test_solc_versioning(): # noqa: D103 assert len(get_forks_with_solc_support(Version.parse("0.8.20"))) == 13 assert len(get_forks_with_solc_support(Version.parse("0.8.24"))) > 13 -def test_closest_fork_supported_by_solc(): +def test_closest_fork_supported_by_solc(): # noqa: D103 assert get_closest_fork_with_solc_support(Paris, Version.parse("0.8.20")) == Paris assert get_closest_fork_with_solc_support(Cancun, Version.parse("0.8.20")) == Shanghai assert get_closest_fork_with_solc_support(Cancun, Version.parse("0.8.24")) == Cancun assert get_closest_fork_with_solc_support(Prague, Version.parse("0.8.24")) == Cancun + + +@pytest.mark.parametrize( + "fork", + [ + pytest.param(Berlin, id="Berlin"), + pytest.param(Istanbul, id="Istanbul"), + pytest.param(Homestead, id="Homestead"), + pytest.param(Frontier, id="Frontier"), + ], +) +@pytest.mark.parametrize( + "calldata", + [ + pytest.param(b"\0", id="zero-data"), + pytest.param(b"\1", id="non-zero-data"), + ], +) +@pytest.mark.parametrize( + "create_tx", + [False, True], +) +def test_tx_intrinsic_gas_functions(fork: Fork, calldata: bytes, create_tx: bool): # noqa: D103 + intrinsic_gas = 21_000 + if calldata == b"\0": + intrinsic_gas += 4 + else: + if fork >= Istanbul: + intrinsic_gas += 16 + else: + intrinsic_gas += 68 + + if create_tx: + if fork >= Homestead: + intrinsic_gas += 32000 + intrinsic_gas += 2 + assert ( + fork.transaction_intrinsic_cost_calculator()( + calldata=calldata, + contract_creation=create_tx, + ) + == intrinsic_gas + ) diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index fa32d07980..60854d4d8d 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -4,6 +4,7 @@ """ from ethereum_test_base_types import ( + AccessList, Account, Address, Bytes, @@ -38,7 +39,6 @@ from ethereum_test_specs.blockchain import Block, Header from ethereum_test_types import ( EOA, - AccessList, Alloc, AuthorizationTuple, ConsolidationRequest, @@ -56,9 +56,6 @@ compute_create2_address, compute_create_address, compute_eofcreate_address, - copy_opcode_cost, - cost_memory_bytes, - eip_2028_transaction_data_cost, keccak256, ) from ethereum_test_vm import ( @@ -153,10 +150,6 @@ "compute_create_address", "compute_create2_address", "compute_eofcreate_address", - "copy_opcode_cost", - "cost_memory_bytes", - "eip_2028_transaction_data_cost", - "eip_2028_transaction_data_cost", "extend_with_defaults", "keccak256", "vm", diff --git a/src/ethereum_test_types/__init__.py b/src/ethereum_test_types/__init__.py index 4205ea22b2..c58d6a5c8c 100644 --- a/src/ethereum_test_types/__init__.py +++ b/src/ethereum_test_types/__init__.py @@ -9,13 +9,9 @@ compute_create2_address, compute_create_address, compute_eofcreate_address, - copy_opcode_cost, - cost_memory_bytes, - eip_2028_transaction_data_cost, ) from .types import ( EOA, - AccessList, Account, Alloc, AuthorizationTuple, @@ -34,7 +30,6 @@ ) __all__ = ( - "AccessList", "Account", "Alloc", "AuthorizationTuple", @@ -64,9 +59,6 @@ "compute_create_address", "compute_create2_address", "compute_eofcreate_address", - "copy_opcode_cost", - "cost_memory_bytes", - "eip_2028_transaction_data_cost", "keccak256", "to_json", ) diff --git a/src/ethereum_test_types/helpers.py b/src/ethereum_test_types/helpers.py index 6d479cf3d8..f945ae6011 100644 --- a/src/ethereum_test_types/helpers.py +++ b/src/ethereum_test_types/helpers.py @@ -64,32 +64,6 @@ def compute_create2_address( return Address(hash[-20:]) -def cost_memory_bytes(new_bytes: int, previous_bytes: int) -> int: - """ - Calculates the cost of memory expansion, based on the costs specified in - the yellow paper: https://ethereum.github.io/yellowpaper/paper.pdf - """ - if new_bytes <= previous_bytes: - return 0 - new_words = ceiling_division(new_bytes, 32) - previous_words = ceiling_division(previous_bytes, 32) - - def c(w: int) -> int: - g_memory = 3 - return (g_memory * w) + ((w * w) // 512) - - return c(new_words) - c(previous_words) - - -def copy_opcode_cost(length: int) -> int: - """ - Calculates the cost of the COPY opcodes, assuming memory expansion from - empty memory, based on the costs specified in the yellow paper: - https://ethereum.github.io/yellowpaper/paper.pdf - """ - return 3 + (ceiling_division(length, 32) * 3) + cost_memory_bytes(length, 0) - - def compute_eofcreate_address( address: FixedSizeBytesConvertible, salt: FixedSizeBytesConvertible, @@ -104,20 +78,6 @@ def compute_eofcreate_address( return Address(hash[-20:]) -def eip_2028_transaction_data_cost(data: BytesConvertible) -> int: - """ - Calculates the cost of a given data as part of a transaction, based on the - costs specified in EIP-2028: https://eips.ethereum.org/EIPS/eip-2028 - """ - cost = 0 - for b in Bytes(data): - if b == 0: - cost += 4 - else: - cost += 16 - return cost - - def add_kzg_version( b_hashes: List[bytes | SupportsBytes | int | str], kzg_version: int ) -> List[bytes]: diff --git a/src/ethereum_test_types/tests/test_types.py b/src/ethereum_test_types/tests/test_types.py index 68150fa9e4..08ab049bd4 100644 --- a/src/ethereum_test_types/tests/test_types.py +++ b/src/ethereum_test_types/tests/test_types.py @@ -6,10 +6,10 @@ import pytest -from ethereum_test_base_types import Address, TestPrivateKey, to_json +from ethereum_test_base_types import AccessList, Address, TestPrivateKey, to_json from ethereum_test_base_types.pydantic import CopyValidateModel -from ..types import AccessList, Account, Alloc, Environment, Storage, Transaction, Withdrawal +from ..types import Account, Alloc, Environment, Storage, Transaction, Withdrawal def test_storage(): @@ -438,21 +438,6 @@ def test_account_merge( }, id="account_2", ), - pytest.param( - True, - AccessList( - address=0x1234, - storage_keys=[0, 1], - ), - { - "address": "0x0000000000000000000000000000000000001234", - "storageKeys": [ - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000001", - ], - }, - id="access_list", - ), pytest.param( True, Withdrawal(index=0, validator_index=1, address=0x1234, amount=2), diff --git a/src/ethereum_test_types/types.py b/src/ethereum_test_types/types.py index e7f757523a..f9c32c4a29 100644 --- a/src/ethereum_test_types/types.py +++ b/src/ethereum_test_types/types.py @@ -24,7 +24,7 @@ ) from trie import HexaryTrie -from ethereum_test_base_types import Account, Address +from ethereum_test_base_types import AccessList, Account, Address from ethereum_test_base_types import Alloc as BaseAlloc from ethereum_test_base_types import ( BLSPublicKey, @@ -477,21 +477,6 @@ def set_fork_requirements(self, fork: Fork) -> "Environment": return self.copy(**updated_values) -class AccessList(CamelModel): - """ - Access List for transactions. - """ - - address: Address - storage_keys: List[Hash] - - def to_list(self) -> List[Address | List[Hash]]: - """ - Returns the access list as a list of serializable elements. - """ - return [self.address, self.storage_keys] - - class AuthorizationTupleGeneric(CamelModel, Generic[NumberBoundTypeVar]): """ Authorization tuple for transactions. diff --git a/src/pytest_plugins/execute/pre_alloc.py b/src/pytest_plugins/execute/pre_alloc.py index 999713ff4d..42fbf05733 100644 --- a/src/pytest_plugins/execute/pre_alloc.py +++ b/src/pytest_plugins/execute/pre_alloc.py @@ -15,18 +15,14 @@ FixedSizeBytesConvertible, NumberConvertible, ) +from ethereum_test_forks import Fork from ethereum_test_rpc import EthRPC from ethereum_test_rpc.types import TransactionByHashResponse from ethereum_test_tools import EOA, Account, Address from ethereum_test_tools import Alloc as BaseAlloc from ethereum_test_tools import AuthorizationTuple, Initcode from ethereum_test_tools import Opcodes as Op -from ethereum_test_tools import ( - Storage, - Transaction, - cost_memory_bytes, - eip_2028_transaction_data_cost, -) +from ethereum_test_tools import Storage, Transaction from ethereum_test_types.eof.v1 import Container from ethereum_test_vm import Bytecode, EVMCodeType, Opcodes @@ -96,6 +92,7 @@ class Alloc(BaseAlloc): A custom class that inherits from the original Alloc class. """ + _fork: Fork = PrivateAttr(...) _sender: EOA = PrivateAttr(...) _eth_rpc: EthRPC = PrivateAttr(...) _txs: List[Transaction] = PrivateAttr(default_factory=list) @@ -107,6 +104,7 @@ class Alloc(BaseAlloc): def __init__( self, *args, + fork: Fork, sender: EOA, eth_rpc: EthRPC, eoa_iterator: Iterator[EOA], @@ -116,6 +114,7 @@ def __init__( **kwargs, ): super().__init__(*args, **kwargs) + self._fork = fork self._sender = sender self._eth_rpc = eth_rpc self._eoa_iterator = eoa_iterator @@ -187,13 +186,15 @@ def deploy_contract( initcode = Container.Init(deploy_container=code, initcode_prefix=initcode_prefix) else: initcode = Initcode(deploy_code=code, initcode_prefix=initcode_prefix) - deploy_gas_limit += cost_memory_bytes(len(bytes(initcode)), 0) + memory_expansion_gas_calculator = self._fork.memory_expansion_gas_calculator() + deploy_gas_limit += memory_expansion_gas_calculator(new_bytes=len(bytes(initcode))) assert ( len(initcode) <= MAX_INITCODE_SIZE ), f"initcode too large {len(initcode)} > {MAX_INITCODE_SIZE}" - deploy_gas_limit += eip_2028_transaction_data_cost(bytes(initcode)) + calldata_gas_calculator = self._fork.calldata_gas_calculator() + deploy_gas_limit += calldata_gas_calculator(data=initcode) # Limit the gas limit deploy_gas_limit = min(deploy_gas_limit * 2, 30_000_000) @@ -378,6 +379,7 @@ def eoa_fund_amount_default(request: pytest.FixtureRequest) -> int: @pytest.fixture(autouse=True, scope="function") def pre( + fork: Fork, sender_key: EOA, eoa_iterator: Iterator[EOA], eth_rpc: EthRPC, @@ -389,6 +391,7 @@ def pre( Returns the default pre allocation for all tests (Empty alloc). """ return Alloc( + fork=fork, sender=sender_key, eth_rpc=eth_rpc, eoa_iterator=eoa_iterator, diff --git a/tests/cancun/eip4844_blobs/test_blob_txs.py b/tests/cancun/eip4844_blobs/test_blob_txs.py index 188b87bcb8..1e1047c6e8 100644 --- a/tests/cancun/eip4844_blobs/test_blob_txs.py +++ b/tests/cancun/eip4844_blobs/test_blob_txs.py @@ -44,7 +44,6 @@ Transaction, TransactionException, add_kzg_version, - eip_2028_transaction_data_cost, ) from .spec import Spec, SpecHelpers, ref_spec_4844 @@ -90,20 +89,13 @@ def tx_value() -> int: @pytest.fixture def tx_gas( + fork: Fork, tx_calldata: bytes, tx_access_list: List[AccessList], ) -> int: """Default gas allocated to transactions sent during test.""" - access_list_gas = 0 - if tx_access_list: - ACCESS_LIST_ADDRESS_COST = 2400 - ACCESS_LIST_STORAGE_KEY_COST = 1900 - - for address in tx_access_list: - access_list_gas += ACCESS_LIST_ADDRESS_COST - access_list_gas += len(address.storage_keys) * ACCESS_LIST_STORAGE_KEY_COST - - return 21000 + eip_2028_transaction_data_cost(tx_calldata) + access_list_gas + tx_intrinsic_cost_calculator = fork.transaction_intrinsic_cost_calculator() + return tx_intrinsic_cost_calculator(calldata=tx_calldata, access_list=tx_access_list) @pytest.fixture diff --git a/tests/cancun/eip4844_blobs/test_blob_txs_full.py b/tests/cancun/eip4844_blobs/test_blob_txs_full.py index 6846345150..0f27179c99 100644 --- a/tests/cancun/eip4844_blobs/test_blob_txs_full.py +++ b/tests/cancun/eip4844_blobs/test_blob_txs_full.py @@ -45,7 +45,7 @@ def tx_value() -> int: @pytest.fixture def tx_gas() -> int: """Default gas allocated to transactions sent during test.""" - return 21000 + return 21_000 @pytest.fixture diff --git a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py index 8cfdfc45b6..d82aa8b33e 100644 --- a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py +++ b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py @@ -35,6 +35,7 @@ import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import ( EOA, Account, @@ -48,7 +49,6 @@ Storage, Transaction, call_return_code, - eip_2028_transaction_data_cost, ) from ethereum_test_tools.vm.opcode import Opcodes as Op @@ -542,6 +542,7 @@ def test_call_opcode_types( ) @pytest.mark.valid_from("Cancun") def test_tx_entry_point( + fork: Fork, state_test: StateTestFiller, precompile_input: bytes, call_gas: int, @@ -559,7 +560,8 @@ def test_tx_entry_point( sender = pre.fund_eoa(amount=start_balance) # Gas is appended the intrinsic gas cost of the transaction - intrinsic_gas_cost = 21_000 + eip_2028_transaction_data_cost(precompile_input) + tx_intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator() + intrinsic_gas_cost = tx_intrinsic_gas_cost_calculator(calldata=precompile_input) # Consumed gas will only be the precompile gas if the proof is correct and # the call gas is sufficient. diff --git a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile_gas.py b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile_gas.py index 3e14028304..2fd9491eb1 100644 --- a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile_gas.py +++ b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile_gas.py @@ -7,6 +7,7 @@ import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import ( Account, Address, @@ -16,7 +17,7 @@ Environment, StateTestFiller, Transaction, - copy_opcode_cost, + ceiling_division, ) from ethereum_test_tools.vm.opcode import Opcodes as Op @@ -69,8 +70,23 @@ def call_gas() -> int: return Spec.POINT_EVALUATION_PRECOMPILE_GAS +def copy_opcode_cost(fork: Fork, length: int) -> int: + """ + Calculates the cost of the COPY opcodes, assuming memory expansion from + empty memory, based on the costs specified in the yellow paper: + https://ethereum.github.io/yellowpaper/paper.pdf + """ + cost_memory_bytes = fork.memory_expansion_gas_calculator() + return ( + 3 + + (ceiling_division(length, 32) * 3) + + cost_memory_bytes(new_bytes=length, previous_bytes=0) + ) + + @pytest.fixture def precompile_caller_code( + fork: Fork, call_type: Op, call_gas: int, precompile_input: bytes, @@ -87,7 +103,7 @@ def precompile_caller_code( WARM_STORAGE_READ_COST + (CALLDATASIZE_COST * 1) + (PUSH_OPERATIONS_COST * 2) - + copy_opcode_cost(len(precompile_input)) + + copy_opcode_cost(fork, len(precompile_input)) ) if call_type == Op.CALL or call_type == Op.CALLCODE: precompile_caller_code += call_type( # type: ignore # https://github.com/ethereum/execution-spec-tests/issues/348 # noqa: E501 diff --git a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py index b17a44fda2..6d99cff9c3 100644 --- a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py +++ b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py @@ -9,10 +9,10 @@ import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import Account, Address, Alloc, Bytecode, Environment from ethereum_test_tools import Opcodes as Op -from ethereum_test_tools import StateTestFiller, Transaction, cost_memory_bytes -from ethereum_test_types.helpers import eip_2028_transaction_data_cost +from ethereum_test_tools import StateTestFiller, Transaction from .common import REFERENCE_SPEC_GIT_PATH, REFERENCE_SPEC_VERSION @@ -53,6 +53,7 @@ def callee_bytecode(dest: int, src: int, length: int) -> Bytecode: @pytest.fixture def call_exact_cost( + fork: Fork, initial_memory: bytes, dest: int, length: int, @@ -60,23 +61,27 @@ def call_exact_cost( """ Returns the exact cost of the subcall, based on the initial memory and the length of the copy. """ - intrinsic_cost = 21000 + eip_2028_transaction_data_cost(initial_memory) + cost_memory_bytes = fork.memory_expansion_gas_calculator() + gas_costs = fork.gas_costs() + tx_intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator() mcopy_cost = 3 mcopy_cost += 3 * ((length + 31) // 32) if length > 0 and dest + length > len(initial_memory): - mcopy_cost += cost_memory_bytes(dest + length, len(initial_memory)) + mcopy_cost += cost_memory_bytes( + new_bytes=dest + length, previous_bytes=len(initial_memory) + ) calldatacopy_cost = 3 calldatacopy_cost += 3 * ((len(initial_memory) + 31) // 32) - calldatacopy_cost += cost_memory_bytes(len(initial_memory), 0) + calldatacopy_cost += cost_memory_bytes(new_bytes=len(initial_memory)) - pushes_cost = 3 * 9 - calldatasize_cost = 2 + pushes_cost = gas_costs.G_VERY_LOW * 9 + calldatasize_cost = gas_costs.G_BASE sstore_cost = 22100 return ( - intrinsic_cost + tx_intrinsic_gas_cost_calculator(calldata=initial_memory) + mcopy_cost + calldatacopy_cost + pushes_cost diff --git a/tests/osaka/eip7692_eof_v1/eip7069_extcall/test_gas.py b/tests/osaka/eip7692_eof_v1/eip7069_extcall/test_gas.py index b2417fb711..44c5905449 100644 --- a/tests/osaka/eip7692_eof_v1/eip7069_extcall/test_gas.py +++ b/tests/osaka/eip7692_eof_v1/eip7069_extcall/test_gas.py @@ -5,10 +5,10 @@ import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import Alloc, Environment, StateTestFiller from ethereum_test_tools.eof.v1 import Container from ethereum_test_tools.vm.opcode import Opcodes as Op -from ethereum_test_types.helpers import cost_memory_bytes from .. import EOF_FORK_NAME from ..gas_test import gas_test @@ -114,6 +114,7 @@ def state_env() -> Environment: def test_ext_calls_gas( state_test: StateTestFiller, pre: Alloc, + fork: Fork, state_env: Environment, opcode: Op, pre_setup: Op, @@ -126,7 +127,7 @@ def test_ext_calls_gas( address_target = ( pre.fund_eoa(0) if new_account else pre.deploy_contract(Container.Code(Op.STOP)) ) - + cost_memory_bytes = fork.memory_expansion_gas_calculator() gas_test( state_test, state_env, @@ -137,8 +138,8 @@ def test_ext_calls_gas( + Op.PUSH20(address_target), subject_code=opcode, tear_down_code=Op.STOP, - cold_gas=cold_gas + cost_memory_bytes(mem_expansion_bytes, 0), - warm_gas=warm_gas + cost_memory_bytes(mem_expansion_bytes, 0), + cold_gas=cold_gas + cost_memory_bytes(new_bytes=mem_expansion_bytes), + warm_gas=warm_gas + cost_memory_bytes(new_bytes=mem_expansion_bytes), ) diff --git a/tests/osaka/eip7692_eof_v1/eip7069_extcall/test_returndatacopy_memory_expansion.py b/tests/osaka/eip7692_eof_v1/eip7069_extcall/test_returndatacopy_memory_expansion.py index a951e5475f..fcc9ac9bf1 100644 --- a/tests/osaka/eip7692_eof_v1/eip7069_extcall/test_returndatacopy_memory_expansion.py +++ b/tests/osaka/eip7692_eof_v1/eip7069_extcall/test_returndatacopy_memory_expansion.py @@ -6,9 +6,10 @@ import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import Account, Address, Alloc, Bytecode, Environment from ethereum_test_tools import Opcodes as Op -from ethereum_test_tools import StateTestFiller, Storage, Transaction, cost_memory_bytes +from ethereum_test_tools import StateTestFiller, Storage, Transaction from ethereum_test_tools.eof.v1 import Container from .. import EOF_FORK_NAME @@ -42,6 +43,7 @@ def callee_bytecode(dest: int, src: int, length: int) -> Container: @pytest.fixture def subcall_exact_cost( + fork: Fork, initial_memory: bytes, dest: int, length: int, @@ -49,14 +51,18 @@ def subcall_exact_cost( """ Returns the exact cost of the subcall, based on the initial memory and the length of the copy. """ + cost_memory_bytes = fork.memory_expansion_gas_calculator() + returndatacopy_cost = 3 returndatacopy_cost += 3 * ((length + 31) // 32) if length > 0 and dest + length > len(initial_memory): - returndatacopy_cost += cost_memory_bytes(dest + length, len(initial_memory)) + returndatacopy_cost += cost_memory_bytes( + new_bytes=dest + length, previous_bytes=len(initial_memory) + ) calldatacopy_cost = 3 calldatacopy_cost += 3 * ((len(initial_memory) + 31) // 32) - calldatacopy_cost += cost_memory_bytes(len(initial_memory), 0) + calldatacopy_cost += cost_memory_bytes(new_bytes=len(initial_memory)) pushes_cost = 3 * 7 calldatasize_cost = 2 diff --git a/tests/osaka/eip7692_eof_v1/eip7480_data_section/test_datacopy_memory_expansion.py b/tests/osaka/eip7692_eof_v1/eip7480_data_section/test_datacopy_memory_expansion.py index 49491e90d8..55f4577d22 100644 --- a/tests/osaka/eip7692_eof_v1/eip7480_data_section/test_datacopy_memory_expansion.py +++ b/tests/osaka/eip7692_eof_v1/eip7480_data_section/test_datacopy_memory_expansion.py @@ -5,6 +5,7 @@ import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import ( Account, Address, @@ -14,7 +15,6 @@ StateTestFiller, Storage, Transaction, - cost_memory_bytes, ) from ethereum_test_tools.eof.v1 import Container, Section from ethereum_test_tools.vm.opcode import Opcodes as Op @@ -50,6 +50,7 @@ def callee_bytecode(dest: int, src: int, length: int, data_section: bytes) -> Co @pytest.fixture def subcall_exact_cost( + fork: Fork, initial_memory: bytes, dest: int, length: int, @@ -57,14 +58,18 @@ def subcall_exact_cost( """ Returns the exact cost of the subcall, based on the initial memory and the length of the copy. """ + cost_memory_bytes = fork.memory_expansion_gas_calculator() + datacopy_cost = 3 datacopy_cost += 3 * ((length + 31) // 32) if length > 0 and dest + length > len(initial_memory): - datacopy_cost += cost_memory_bytes(dest + length, len(initial_memory)) + datacopy_cost += cost_memory_bytes( + new_bytes=dest + length, previous_bytes=len(initial_memory) + ) calldatacopy_cost = 3 calldatacopy_cost += 3 * ((len(initial_memory) + 31) // 32) - calldatacopy_cost += cost_memory_bytes(len(initial_memory), 0) + calldatacopy_cost += cost_memory_bytes(new_bytes=len(initial_memory)) pushes_cost = 3 * 7 calldatasize_cost = 2 diff --git a/tests/osaka/eip7692_eof_v1/eip7620_eof_create/test_gas.py b/tests/osaka/eip7692_eof_v1/eip7620_eof_create/test_gas.py index e536414a02..f9d01e82f3 100644 --- a/tests/osaka/eip7692_eof_v1/eip7620_eof_create/test_gas.py +++ b/tests/osaka/eip7692_eof_v1/eip7620_eof_create/test_gas.py @@ -4,10 +4,10 @@ import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import Alloc, Environment, StateTestFiller, compute_eofcreate_address from ethereum_test_tools.eof.v1 import Container, Section from ethereum_test_tools.vm.opcode import Opcodes as Op -from ethereum_test_types.helpers import cost_memory_bytes from .. import EOF_FORK_NAME from ..gas_test import gas_test @@ -115,6 +115,7 @@ def make_factory(initcode: Container): def test_eofcreate_gas( state_test: StateTestFiller, pre: Alloc, + fork: Fork, value: int, new_account: bool, mem_expansion_bytes: int, @@ -139,7 +140,7 @@ def test_eofcreate_gas( code_increment_counter = ( Op.TLOAD(slot_counter) + Op.DUP1 + Op.TSTORE(slot_counter, Op.PUSH1(1) + Op.ADD) ) - + cost_memory_bytes = fork.memory_expansion_gas_calculator() gas_test( state_test, Environment(), @@ -151,7 +152,7 @@ def test_eofcreate_gas( subject_code=Op.EOFCREATE[0], tear_down_code=Op.STOP, cold_gas=EOFCREATE_GAS - + cost_memory_bytes(mem_expansion_bytes, 0) + + cost_memory_bytes(new_bytes=mem_expansion_bytes) + initcode_hashing_cost + initcode_execution_cost + deployed_code_cost, diff --git a/tests/prague/eip7702_set_code_tx/test_gas.py b/tests/prague/eip7702_set_code_tx/test_gas.py index 04d86bc39f..a3b6405895 100644 --- a/tests/prague/eip7702_set_code_tx/test_gas.py +++ b/tests/prague/eip7702_set_code_tx/test_gas.py @@ -10,6 +10,7 @@ import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import ( EOA, AccessList, @@ -28,7 +29,6 @@ Storage, Transaction, TransactionException, - eip_2028_transaction_data_cost, extend_with_defaults, ) @@ -681,6 +681,7 @@ def gas_test_parameter_args( def test_gas_cost( state_test: StateTestFiller, pre: Alloc, + fork: Fork, authorization_list_with_properties: List[AuthorizationWithProperties], authorization_list: List[AuthorizationTuple], data: bytes, @@ -690,15 +691,13 @@ def test_gas_cost( """ Test gas at the execution start of a set-code transaction in multiple scenarios. """ - intrinsic_gas = ( - 21_000 - + eip_2028_transaction_data_cost(data) - + 1900 * sum(len(al.storage_keys) for al in access_list) - + 2400 * len(access_list) - ) # Calculate the intrinsic gas cost of the authorizations, by default the # full empty account cost is charged for each authorization. - intrinsic_gas += Spec.PER_EMPTY_ACCOUNT_COST * len(authorization_list_with_properties) + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=data, + access_list=access_list, + authorization_count=len(authorization_list), + ) discounted_authorizations = 0 seen_authority = set() @@ -934,6 +933,7 @@ def test_account_warming( def test_intrinsic_gas_cost( state_test: StateTestFiller, pre: Alloc, + fork: Fork, authorization_list: List[AuthorizationTuple], data: bytes, access_list: List[AccessList], @@ -944,15 +944,13 @@ def test_intrinsic_gas_cost( Test sending a transaction with the exact intrinsic gas required and also insufficient gas. """ - intrinsic_gas = ( - 21_000 - + eip_2028_transaction_data_cost(data) - + 1900 * sum(len(al.storage_keys) for al in access_list) - + 2400 * len(access_list) - ) # Calculate the intrinsic gas cost of the authorizations, by default the # full empty account cost is charged for each authorization. - intrinsic_gas += Spec.PER_EMPTY_ACCOUNT_COST * len(authorization_list) + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=data, + access_list=access_list, + authorization_count=len(authorization_list), + ) tx_gas = intrinsic_gas if not valid: diff --git a/tests/shanghai/eip3860_initcode/helpers.py b/tests/shanghai/eip3860_initcode/helpers.py index 342431b257..d6e3602793 100644 --- a/tests/shanghai/eip3860_initcode/helpers.py +++ b/tests/shanghai/eip3860_initcode/helpers.py @@ -2,60 +2,11 @@ Helpers for the EIP-3860 initcode tests. """ -from ethereum_test_tools import Initcode, ceiling_division, eip_2028_transaction_data_cost +from ethereum_test_tools import Initcode from ethereum_test_tools.vm.opcode import Opcodes as Op -from .spec import Spec - -KECCAK_WORD_COST = 6 INITCODE_RESULTING_DEPLOYED_CODE = Op.STOP -BASE_TRANSACTION_GAS = 21000 -CREATE_CONTRACT_BASE_GAS = 32000 - - -def calculate_initcode_word_cost(length: int) -> int: - """ - Calculates the added word cost on contract creation added by the - length of the initcode based on the formula: - INITCODE_WORD_COST * ceil(len(initcode) / 32) - """ - return Spec.INITCODE_WORD_COST * ceiling_division(length, 32) - - -def calculate_create2_word_cost(length: int) -> int: - """ - Calculates the added word cost on contract creation added by the - hashing of the initcode during create2 contract creation. - """ - return KECCAK_WORD_COST * ceiling_division(length, 32) - - -def calculate_create_tx_intrinsic_cost(initcode: Initcode) -> int: - """ - Calculates the intrinsic gas cost of a transaction that contains initcode - and creates a contract - """ - return ( - BASE_TRANSACTION_GAS # G_transaction - + CREATE_CONTRACT_BASE_GAS # G_transaction_create - + eip_2028_transaction_data_cost(initcode) # Transaction calldata cost - + calculate_initcode_word_cost(len(initcode)) - ) - - -def calculate_create_tx_execution_cost( - initcode: Initcode, -) -> int: - """ - Calculates the total execution gas cost of a transaction that - contains initcode and creates a contract - """ - cost = calculate_create_tx_intrinsic_cost(initcode) - cost += initcode.deployment_gas - cost += initcode.execution_gas - return cost - def get_initcode_name(val: Initcode): """ diff --git a/tests/shanghai/eip3860_initcode/test_initcode.py b/tests/shanghai/eip3860_initcode/test_initcode.py index 35fd7bf768..ec9a81d125 100644 --- a/tests/shanghai/eip3860_initcode/test_initcode.py +++ b/tests/shanghai/eip3860_initcode/test_initcode.py @@ -9,6 +9,7 @@ import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import ( EOA, Account, @@ -20,20 +21,12 @@ StateTestFiller, Transaction, TransactionException, - compute_create2_address, + ceiling_division, compute_create_address, ) from ethereum_test_tools.vm.opcode import Opcodes as Op -from .helpers import ( - INITCODE_RESULTING_DEPLOYED_CODE, - calculate_create2_word_cost, - calculate_create_tx_execution_cost, - calculate_create_tx_intrinsic_cost, - calculate_initcode_word_cost, - get_create_id, - get_initcode_name, -) +from .helpers import INITCODE_RESULTING_DEPLOYED_CODE, get_create_id, get_initcode_name from .spec import Spec, ref_spec_3860 REFERENCE_SPEC_GIT_PATH = ref_spec_3860.git_path @@ -215,18 +208,22 @@ class TestContractCreationGasUsage: """ @pytest.fixture - def exact_intrinsic_gas(self, initcode: Initcode) -> int: + def exact_intrinsic_gas(self, fork: Fork, initcode: Initcode) -> int: """ Calculates the intrinsic tx gas cost. """ - return calculate_create_tx_intrinsic_cost(initcode) + tx_intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator() + return tx_intrinsic_gas_cost_calculator( + calldata=initcode, + contract_creation=True, + ) @pytest.fixture - def exact_execution_gas(self, initcode: Initcode) -> int: + def exact_execution_gas(self, exact_intrinsic_gas: int, initcode: Initcode) -> int: """ Calculates the total execution gas cost. """ - return calculate_create_tx_execution_cost(initcode) + return exact_intrinsic_gas + initcode.deployment_gas + initcode.execution_gas @pytest.fixture def tx_error(self, gas_test_case: str) -> TransactionException | None: @@ -402,18 +399,13 @@ def created_contract_address( # noqa: D103 """ Calculates the address of the contract created by the creator contract. """ - if opcode == Op.CREATE: - return compute_create_address( - address=creator_contract_address, - nonce=1, - ) - if opcode == Op.CREATE2: - return compute_create2_address( - address=creator_contract_address, - salt=create2_salt, - initcode=initcode, - ) - raise Exception("Invalid opcode for generator") + return compute_create_address( + address=creator_contract_address, + nonce=1, + salt=create2_salt, + initcode=initcode, + opcode=opcode, + ) @pytest.fixture def caller_code(self, creator_contract_address: Address) -> Bytecode: @@ -446,14 +438,16 @@ def tx(self, caller_contract_address: Address, initcode: Initcode, sender: EOA) ) @pytest.fixture - def contract_creation_gas_cost(self, opcode: Op) -> int: + def contract_creation_gas_cost(self, fork: Fork, opcode: Op) -> int: """ Calculates the gas cost of the contract creation operation. """ - CREATE_CONTRACT_BASE_GAS = 32000 - GAS_OPCODE_GAS = 2 - PUSH_DUP_OPCODE_GAS = 3 - CALLDATASIZE_OPCODE_GAS = 2 + gas_costs = fork.gas_costs() + + CREATE_CONTRACT_BASE_GAS = gas_costs.G_CREATE + GAS_OPCODE_GAS = gas_costs.G_BASE + PUSH_DUP_OPCODE_GAS = gas_costs.G_VERY_LOW + CALLDATASIZE_OPCODE_GAS = gas_costs.G_BASE contract_creation_gas_usage = ( CREATE_CONTRACT_BASE_GAS + GAS_OPCODE_GAS @@ -464,6 +458,25 @@ def contract_creation_gas_cost(self, opcode: Op) -> int: contract_creation_gas_usage += PUSH_DUP_OPCODE_GAS return contract_creation_gas_usage + @pytest.fixture + def initcode_word_cost(self, fork: Fork, initcode: Initcode) -> int: + """ + Calculates gas cost charged for the initcode length. + """ + gas_costs = fork.gas_costs() + return ceiling_division(len(initcode), 32) * gas_costs.G_INITCODE_WORD + + @pytest.fixture + def create2_word_cost(self, opcode: Op, fork: Fork, initcode: Initcode) -> int: + """ + Calculates gas cost charged for the initcode length. + """ + if opcode == Op.CREATE: + return 0 + + gas_costs = fork.gas_costs() + return ceiling_division(len(initcode), 32) * gas_costs.G_KECCAK_256_WORD + def test_create_opcode_initcode( self, state_test: StateTestFiller, @@ -471,12 +484,13 @@ def test_create_opcode_initcode( pre: Alloc, post: Alloc, tx: Transaction, - opcode: Op, initcode: Initcode, caller_contract_address: Address, creator_contract_address: Address, created_contract_address: Address, contract_creation_gas_cost: int, + initcode_word_cost: int, + create2_word_cost: int, ): """ Test contract creation via the CREATE/CREATE2 opcodes that have an @@ -508,14 +522,13 @@ def test_create_opcode_initcode( # The code is only deployed if the length check succeeds expected_gas_usage += initcode.deployment_gas - if opcode == Op.CREATE2: - # CREATE2 hashing cost should only be deducted if the initcode - # does not exceed the max length - expected_gas_usage += calculate_create2_word_cost(len(initcode)) + # CREATE2 hashing cost should only be deducted if the initcode + # does not exceed the max length + expected_gas_usage += create2_word_cost # Initcode word cost is only deducted if the length check # succeeds - expected_gas_usage += calculate_initcode_word_cost(len(initcode)) + expected_gas_usage += initcode_word_cost # Call returns 1 as valid initcode length s[0]==1 && s[1]==1 post[caller_contract_address] = Account( diff --git a/tests/shanghai/eip4895_withdrawals/test_withdrawals.py b/tests/shanghai/eip4895_withdrawals/test_withdrawals.py index 7fdc82cb1e..23bd823765 100644 --- a/tests/shanghai/eip4895_withdrawals/test_withdrawals.py +++ b/tests/shanghai/eip4895_withdrawals/test_withdrawals.py @@ -70,7 +70,7 @@ def tx(self, sender: EOA, recipient: EOA): # noqa: D102 # Transaction sent from the `sender`, which has 1 wei balance at start return Transaction( gas_price=ONE_GWEI, - gas_limit=21000, + gas_limit=21_000, to=recipient, sender=sender, )