From c03f23893450d6acccbcea5ef0cfacbc589ca525 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Wed, 18 Dec 2024 15:06:41 -0600 Subject: [PATCH] feat(forks,tests): Add EIP-7623 (#1004) * feat(forks): Add EIP-7623 logic to gas calculation * fix(tests): Broken tests due to gas cost change * feat(forks): Add `transaction_data_floor_cost_calculator` * new(tests): EIP-7623: Add transaction validity tests * review comments * new(tests): EIP-7623: Add gas consumption tests * refactor(tests): EIP-7623: minor refactor * Update src/ethereum_test_forks/base_fork.py Co-authored-by: Stuart Reed * Update src/ethereum_test_forks/base_fork.py * Apply suggestions from code review Co-authored-by: danceratopz * fix(forks): tox * docs: changelog --------- Co-authored-by: Stuart Reed Co-authored-by: danceratopz --- docs/CHANGELOG.md | 1 + src/ethereum_test_forks/base_fork.py | 37 +++ src/ethereum_test_forks/forks/forks.py | 84 ++++- src/ethereum_test_forks/gas_costs.py | 2 + .../test_point_evaluation_precompile.py | 17 +- .../test_mcopy_memory_expansion.py | 19 +- .../__init__.py | 4 + .../conftest.py | 302 ++++++++++++++++++ .../eip7623_increase_calldata_cost/helpers.py | 53 +++ .../eip7623_increase_calldata_cost/spec.py | 30 ++ .../test_execution_gas.py | 301 +++++++++++++++++ .../test_transaction_validity.py | 294 +++++++++++++++++ .../eip3860_initcode/test_initcode.py | 28 +- 13 files changed, 1162 insertions(+), 10 deletions(-) create mode 100644 tests/prague/eip7623_increase_calldata_cost/__init__.py create mode 100644 tests/prague/eip7623_increase_calldata_cost/conftest.py create mode 100644 tests/prague/eip7623_increase_calldata_cost/helpers.py create mode 100644 tests/prague/eip7623_increase_calldata_cost/spec.py create mode 100644 tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py create mode 100644 tests/prague/eip7623_increase_calldata_cost/test_transaction_validity.py diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 613796bc116..9663d36915f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -18,6 +18,7 @@ Test fixtures for use by clients are available for each release on the [Github r - ✨ [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) many delegations test ([#923](https://github.com/ethereum/execution-spec-tests/pull/923)) - ✨ [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) set code of non-empty-storage account test ([#948](https://github.com/ethereum/execution-spec-tests/pull/948)) - ✨ [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) Remove delegation behavior of EXTCODE* ([#984](https://github.com/ethereum/execution-spec-tests/pull/984)) +- ✨ [EIP-7623](https://eips.ethereum.org/EIPS/eip-7623) Increase calldata cost ([#1004](https://github.com/ethereum/execution-spec-tests/pull/1004)) ### 🛠️ Framework diff --git a/src/ethereum_test_forks/base_fork.py b/src/ethereum_test_forks/base_fork.py index 7e31a527b27..776af9b237a 100644 --- a/src/ethereum_test_forks/base_fork.py +++ b/src/ethereum_test_forks/base_fork.py @@ -44,6 +44,18 @@ class CalldataGasCalculator(Protocol): A protocol to calculate the transaction gas cost of calldata for a given fork. """ + def __call__(self, *, data: BytesConvertible, floor: bool = False) -> int: + """ + Returns the transaction gas cost of calldata given its contents. + """ + pass + + +class TransactionDataFloorCostCalculator(Protocol): + """ + A protocol to calculate the transaction floor cost due to its calldata for a given fork. + """ + def __call__(self, *, data: BytesConvertible) -> int: """ Returns the transaction gas cost of calldata given its contents. @@ -63,9 +75,24 @@ def __call__( contract_creation: bool = False, access_list: List[AccessList] | None = None, authorization_list_or_count: Sized | int | None = None, + return_cost_deducted_prior_execution: bool = False, ) -> int: """ Returns the intrinsic gas cost of a transaction given its properties. + + Args: + calldata: The data of the transaction. + contract_creation: Whether the transaction creates a contract. + access_list: The list of access lists for the transaction. + authorization_list_or_count: The list of authorizations or the count of authorizations + for the transaction. + return_cost_deducted_prior_execution: If set to False, the returned value is equal to + the minimum gas required for the transaction to be valid. If set to True, the + returned value is equal to the cost that is deducted from the gas limit before + the transaction starts execution. + + Returns: + Gas cost of a transaction """ pass @@ -236,6 +263,16 @@ def calldata_gas_calculator( """ pass + @classmethod + @abstractmethod + def transaction_data_floor_cost_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> TransactionDataFloorCostCalculator: + """ + Returns a callable that calculates the transaction floor cost due to its calldata. + """ + pass + @classmethod @abstractmethod def transaction_intrinsic_cost_calculator( diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index c7385c7555c..523983ce2cc 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -18,6 +18,7 @@ BaseFork, CalldataGasCalculator, MemoryExpansionGasCalculator, + TransactionDataFloorCostCalculator, TransactionIntrinsicCostCalculator, ) from ..gas_costs import GasCosts @@ -140,6 +141,8 @@ def gas_costs(cls, block_number: int = 0, timestamp: int = 0) -> GasCosts: G_MEMORY=3, G_TX_DATA_ZERO=4, G_TX_DATA_NON_ZERO=68, + G_TX_DATA_STANDARD_TOKEN_COST=0, + G_TX_DATA_FLOOR_TOKEN_COST=0, G_TRANSACTION=21_000, G_TRANSACTION_CREATE=32_000, G_LOG=375, @@ -184,7 +187,7 @@ def calldata_gas_calculator( """ gas_costs = cls.gas_costs(block_number, timestamp) - def fn(*, data: BytesConvertible) -> int: + def fn(*, data: BytesConvertible, floor: bool = False) -> int: cost = 0 for b in Bytes(data): if b == 0: @@ -195,6 +198,19 @@ def fn(*, data: BytesConvertible) -> int: return fn + @classmethod + def transaction_data_floor_cost_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> TransactionDataFloorCostCalculator: + """ + At frontier, the transaction data floor cost is a constant zero. + """ + + def fn(*, data: BytesConvertible) -> int: + return 0 + + return fn + @classmethod def transaction_intrinsic_cost_calculator( cls, block_number: int = 0, timestamp: int = 0 @@ -211,6 +227,7 @@ def fn( contract_creation: bool = False, access_list: List[AccessList] | None = None, authorization_list_or_count: Sized | int | None = None, + return_cost_deducted_prior_execution: bool = False, ) -> int: assert access_list is None, f"Access list is not supported in {cls.name()}" assert ( @@ -633,6 +650,7 @@ def fn( contract_creation: bool = False, access_list: List[AccessList] | None = None, authorization_list_or_count: Sized | int | None = None, + return_cost_deducted_prior_execution: bool = False, ) -> int: intrinsic_cost: int = super_fn( calldata=calldata, @@ -764,7 +782,7 @@ def 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. + On Istanbul, the non-zero transaction data byte cost is reduced to 16 due to EIP-2028. """ return replace( super(Istanbul, cls).gas_costs(block_number, timestamp), @@ -818,6 +836,7 @@ def fn( contract_creation: bool = False, access_list: List[AccessList] | None = None, authorization_list_or_count: Sized | int | None = None, + return_cost_deducted_prior_execution: bool = False, ) -> int: intrinsic_cost: int = super_fn( calldata=calldata, @@ -1127,6 +1146,17 @@ def precompiles(cls, block_number: int = 0, timestamp: int = 0) -> List[Address] block_number, timestamp ) + @classmethod + def gas_costs(cls, block_number: int = 0, timestamp: int = 0) -> GasCosts: + """ + On Prague, the standard token cost and the floor token costs are introduced due to EIP-7623 + """ + return replace( + super(Prague, cls).gas_costs(block_number, timestamp), + G_TX_DATA_STANDARD_TOKEN_COST=4, # https://eips.ethereum.org/EIPS/eip-7623 + G_TX_DATA_FLOOR_TOKEN_COST=10, + ) + @classmethod def system_contracts(cls, block_number: int = 0, timestamp: int = 0) -> List[Address]: """ @@ -1146,6 +1176,44 @@ def max_request_type(cls, block_number: int = 0, timestamp: int = 0) -> int: """ return 2 + @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, floor: bool = False) -> int: + tokens = 0 + for b in Bytes(data): + if b == 0: + tokens += 1 + else: + tokens += 4 + if floor: + return tokens * gas_costs.G_TX_DATA_FLOOR_TOKEN_COST + return tokens * gas_costs.G_TX_DATA_STANDARD_TOKEN_COST + + return fn + + @classmethod + def transaction_data_floor_cost_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> TransactionDataFloorCostCalculator: + """ + Starting in Prague, due to EIP-7623, the transaction data floor cost is introduced. + """ + calldata_gas_calculator = cls.calldata_gas_calculator(block_number, timestamp) + gas_costs = cls.gas_costs(block_number, timestamp) + + def fn(*, data: BytesConvertible) -> int: + return calldata_gas_calculator(data=data, floor=True) + gas_costs.G_TRANSACTION + + return fn + @classmethod def transaction_intrinsic_cost_calculator( cls, block_number: int = 0, timestamp: int = 0 @@ -1157,6 +1225,9 @@ def transaction_intrinsic_cost_calculator( block_number, timestamp ) gas_costs = cls.gas_costs(block_number, timestamp) + transaction_data_floor_cost_calculator = cls.transaction_data_floor_cost_calculator( + block_number, timestamp + ) def fn( *, @@ -1164,17 +1235,24 @@ def fn( contract_creation: bool = False, access_list: List[AccessList] | None = None, authorization_list_or_count: Sized | int | None = None, + return_cost_deducted_prior_execution: bool = False, ) -> int: intrinsic_cost: int = super_fn( calldata=calldata, contract_creation=contract_creation, access_list=access_list, + return_cost_deducted_prior_execution=False, ) if authorization_list_or_count is not None: if isinstance(authorization_list_or_count, Sized): authorization_list_or_count = len(authorization_list_or_count) intrinsic_cost += authorization_list_or_count * gas_costs.G_AUTHORIZATION - return intrinsic_cost + + if return_cost_deducted_prior_execution: + return intrinsic_cost + + transaction_floor_data_cost = transaction_data_floor_cost_calculator(data=calldata) + return max(intrinsic_cost, transaction_floor_data_cost) return fn diff --git a/src/ethereum_test_forks/gas_costs.py b/src/ethereum_test_forks/gas_costs.py index 485e8819a3e..4c509badf84 100644 --- a/src/ethereum_test_forks/gas_costs.py +++ b/src/ethereum_test_forks/gas_costs.py @@ -45,6 +45,8 @@ class GasCosts: G_TX_DATA_ZERO: int G_TX_DATA_NON_ZERO: int + G_TX_DATA_STANDARD_TOKEN_COST: int + G_TX_DATA_FLOOR_TOKEN_COST: int G_TRANSACTION: int G_TRANSACTION_CREATE: int diff --git a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py index d82aa8b33ee..0d750c55b23 100644 --- a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py +++ b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py @@ -38,6 +38,7 @@ from ethereum_test_forks import Fork from ethereum_test_tools import ( EOA, + AccessList, Account, Address, Alloc, @@ -559,9 +560,15 @@ def test_tx_entry_point( start_balance = 10**18 sender = pre.fund_eoa(amount=start_balance) + # Starting from EIP-7623, we need to use an access list to raise the intrinsic gas cost to be + # above the floor data cost. + access_list = [AccessList(address=Address(i), storage_keys=[]) for i in range(1, 10)] + # Gas is appended the intrinsic gas cost of the transaction tx_intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator() - intrinsic_gas_cost = tx_intrinsic_gas_cost_calculator(calldata=precompile_input) + intrinsic_gas_cost = tx_intrinsic_gas_cost_calculator( + calldata=precompile_input, access_list=access_list + ) # Consumed gas will only be the precompile gas if the proof is correct and # the call gas is sufficient. @@ -570,13 +577,17 @@ def test_tx_entry_point( Spec.POINT_EVALUATION_PRECOMPILE_GAS if call_gas >= Spec.POINT_EVALUATION_PRECOMPILE_GAS and proof_correct else call_gas - ) + intrinsic_gas_cost - + ) + tx_intrinsic_gas_cost_calculator( + calldata=precompile_input, + access_list=access_list, + return_cost_deducted_prior_execution=True, + ) fee_per_gas = 7 tx = Transaction( sender=sender, data=precompile_input, + access_list=access_list, to=Address(Spec.POINT_EVALUATION_PRECOMPILE_ADDRESS), gas_limit=call_gas + intrinsic_gas_cost, gas_price=fee_per_gas, diff --git a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py index 6d99cff9c38..71c1be01a15 100644 --- a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py +++ b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py @@ -5,12 +5,12 @@ """ # noqa: E501 import itertools -from typing import Mapping +from typing import List, Mapping import pytest from ethereum_test_forks import Fork -from ethereum_test_tools import Account, Address, Alloc, Bytecode, Environment +from ethereum_test_tools import AccessList, Account, Address, Alloc, Bytecode, Environment from ethereum_test_tools import Opcodes as Op from ethereum_test_tools import StateTestFiller, Transaction @@ -51,16 +51,27 @@ def callee_bytecode(dest: int, src: int, length: int) -> Bytecode: return bytecode +@pytest.fixture +def tx_access_list() -> List[AccessList]: + """ + Access list for the transaction. + """ + return [AccessList(address=Address(i), storage_keys=[]) for i in range(1, 10)] + + @pytest.fixture def call_exact_cost( fork: Fork, initial_memory: bytes, dest: int, length: int, + tx_access_list: List[AccessList], ) -> int: """ Returns the exact cost of the subcall, based on the initial memory and the length of the copy. """ + # Starting from EIP-7623, we need to use an access list to raise the intrinsic gas cost to be + # above the floor data cost. cost_memory_bytes = fork.memory_expansion_gas_calculator() gas_costs = fork.gas_costs() tx_intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator() @@ -81,7 +92,7 @@ def call_exact_cost( sstore_cost = 22100 return ( - tx_intrinsic_gas_cost_calculator(calldata=initial_memory) + tx_intrinsic_gas_cost_calculator(calldata=initial_memory, access_list=tx_access_list) + mcopy_cost + calldatacopy_cost + pushes_cost @@ -133,10 +144,12 @@ def tx( # noqa: D103 initial_memory: bytes, tx_max_fee_per_gas: int, tx_gas_limit: int, + tx_access_list: List[AccessList], ) -> Transaction: return Transaction( sender=sender, to=caller_address, + access_list=tx_access_list, data=initial_memory, gas_limit=tx_gas_limit, max_fee_per_gas=tx_max_fee_per_gas, diff --git a/tests/prague/eip7623_increase_calldata_cost/__init__.py b/tests/prague/eip7623_increase_calldata_cost/__init__.py new file mode 100644 index 00000000000..951922c6f92 --- /dev/null +++ b/tests/prague/eip7623_increase_calldata_cost/__init__.py @@ -0,0 +1,4 @@ +""" +abstract: Test [EIP-7623: Increase calldata cost](https://eips.ethereum.org/EIPS/eip-7623) + Tests for [EIP-7623: Increase calldata cost](https://eips.ethereum.org/EIPS/eip-7623). +""" diff --git a/tests/prague/eip7623_increase_calldata_cost/conftest.py b/tests/prague/eip7623_increase_calldata_cost/conftest.py new file mode 100644 index 00000000000..adaaa3b8bfa --- /dev/null +++ b/tests/prague/eip7623_increase_calldata_cost/conftest.py @@ -0,0 +1,302 @@ +""" +Fixtures for the EIP-7623 tests. +""" + +from typing import List, Sequence + +import pytest + +from ethereum_test_forks import Fork +from ethereum_test_tools import ( + EOA, + AccessList, + Address, + Alloc, + AuthorizationTuple, + Bytecode, + Bytes, + Hash, +) +from ethereum_test_tools import Opcodes as Op +from ethereum_test_tools import Transaction, TransactionException + +from .helpers import DataTestType, find_floor_cost_threshold + + +@pytest.fixture +def sender(pre: Alloc) -> EOA: + """ + Create the sender account. + """ + return pre.fund_eoa() + + +@pytest.fixture +def to( + request: pytest.FixtureRequest, + pre: Alloc, +) -> Address | None: + """ + Create the sender account. + """ + if hasattr(request, "param"): + param = request.param + else: + param = Op.STOP + + if param is None: + return None + if isinstance(param, Address): + return param + if isinstance(param, Bytecode): + return pre.deploy_contract(param) + + raise ValueError(f"Invalid value for `to` fixture: {param}") + + +@pytest.fixture +def protected() -> bool: + """ + Whether the transaction is protected or not. Only valid for type-0 transactions. + """ + return True + + +@pytest.fixture +def access_list() -> List[AccessList] | None: + """ + Access list for the transaction. + """ + return None + + +@pytest.fixture +def authorization_existing_authority() -> bool: + """ + Whether the transaction has an existing authority in the authorization list. + """ + return False + + +@pytest.fixture +def authorization_list( + request: pytest.FixtureRequest, + pre: Alloc, + authorization_existing_authority: bool, +) -> List[AuthorizationTuple] | None: + """ + Authorization list for the transaction. + + This fixture needs to be parametrized indirectly in order to generate the authorizations with + valid signers using `pre` in this function, and the parametrized value should be a list of + addresses. + """ + if not hasattr(request, "param"): + return None + if request.param is None: + return None + return [ + AuthorizationTuple( + signer=pre.fund_eoa(1 if authorization_existing_authority else 0), address=address + ) + for address in request.param + ] + + +@pytest.fixture +def blob_versioned_hashes() -> Sequence[Hash] | None: + """ + Versioned hashes for the transaction. + """ + return None + + +@pytest.fixture +def contract_creating_tx(to: Address | None) -> bool: + """ + Whether the transaction creates a contract or not. + """ + return to is None + + +@pytest.fixture +def intrinsic_gas_data_floor_minimum_delta() -> int: + """ + Induce a minimum delta between the transaction intrinsic gas cost and the + floor data gas cost. + """ + return 0 + + +@pytest.fixture +def tx_data( + fork: Fork, + data_test_type: DataTestType, + access_list: List[AccessList] | None, + authorization_list: List[AuthorizationTuple] | None, + contract_creating_tx: bool, + intrinsic_gas_data_floor_minimum_delta: int, +) -> Bytes: + """ + All tests in this file use data that is generated dynamically depending on the case and the + attributes of the transaction in order to reach the edge cases where the floor gas cost is + equal or barely greater than the intrinsic gas cost. + + We have two different types of tests: + - FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS: The floor gas cost is less than or equal + to the intrinsic gas cost, which means that the size of the tokens in the data are not + enough to trigger the floor gas cost. + - FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS: The floor gas cost is greater than the intrinsic + gas cost, which means that the size of the tokens in the data are enough to trigger the + floor gas cost. + + E.g. Given a transaction with a single access list and a single storage key, its intrinsic gas + cost (as of Prague fork) can be calculated as: + - 21,000 gas for the transaction + - 2,400 gas for the access list + - 1,900 gas for the storage key + - 16 gas for each non-zero byte in the data + - 4 gas for each zero byte in the data + + Its floor data gas cost can be calculated as: + - 21,000 gas for the transaction + - 40 gas for each non-zero byte in the data + - 10 gas for each zero byte in the data + + Notice that the data included in the transaction affects both the intrinsic gas cost and the + floor data cost, but at different rates. + + The purpose of this function is to find the exact amount of data where the floor data gas + cost starts exceeding the intrinsic gas cost. + + After a binary search we find that adding 717 tokens of data (179 non-zero bytes + + 1 zero byte) triggers the floor gas cost. + + Therefore, this function will return a Bytes object with 179 non-zero bytes and 1 zero byte + for `FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS` and a Bytes object with 179 non-zero bytes + and no zero bytes for `FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS` + """ + + def tokens_to_data(tokens: int) -> Bytes: + return Bytes(b"\x01" * (tokens // 4) + b"\x00" * (tokens % 4)) + + fork_intrinsic_cost_calculator = fork.transaction_intrinsic_cost_calculator() + + def transaction_intrinsic_cost_calculator(tokens: int) -> int: + return ( + fork_intrinsic_cost_calculator( + calldata=tokens_to_data(tokens), + contract_creation=contract_creating_tx, + access_list=access_list, + authorization_list_or_count=authorization_list, + return_cost_deducted_prior_execution=True, + ) + + intrinsic_gas_data_floor_minimum_delta + ) + + fork_data_floor_cost_calculator = fork.transaction_data_floor_cost_calculator() + + def transaction_data_floor_cost_calculator(tokens: int) -> int: + return fork_data_floor_cost_calculator(data=tokens_to_data(tokens)) + + # Start with zero data and check the difference in the gas calculator between the + # intrinsic gas cost and the floor gas cost. + if transaction_data_floor_cost_calculator(0) >= transaction_intrinsic_cost_calculator(0): + # Special case which is a transaction with no extra intrinsic gas costs other than the + # data cost, any data will trigger the floor gas cost. + if data_test_type == DataTestType.FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS: + return Bytes(b"") + else: + return Bytes(b"\0") + + tokens = find_floor_cost_threshold( + floor_data_gas_cost_calculator=transaction_data_floor_cost_calculator, + intrinsic_gas_cost_calculator=transaction_intrinsic_cost_calculator, + ) + + if data_test_type == DataTestType.FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS: + return tokens_to_data(tokens + 1) + return tokens_to_data(tokens) + + +@pytest.fixture +def tx_gas_delta() -> int: + """ + Gas delta to modify the gas amount included with the transaction. + + If negative, the transaction will be invalid because the intrinsic gas cost is greater than the + gas limit. + + This value operates regardless of whether the floor data gas cost is reached or not. + + If the value is greater than zero, the transaction will also be valid and the test will check + that transaction processing does not consume more gas than it should. + """ + return 0 + + +@pytest.fixture +def tx_gas( + fork: Fork, + tx_data: Bytes, + access_list: List[AccessList] | None, + authorization_list: List[AuthorizationTuple] | None, + contract_creating_tx: bool, + tx_gas_delta: int, +) -> int: + """ + Gas limit for the transaction. + + The calculated value takes into account the normal intrinsic gas cost and the floor data gas + cost. + + The gas delta is added to the intrinsic gas cost to generate different test scenarios. + """ + intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator() + return ( + intrinsic_gas_cost_calculator( + calldata=tx_data, + contract_creation=contract_creating_tx, + access_list=access_list, + authorization_list_or_count=authorization_list, + ) + + tx_gas_delta + ) + + +@pytest.fixture +def tx_error(tx_gas_delta: int) -> TransactionException | None: + """ + Transaction error, only expected if the gas delta is negative. + """ + return TransactionException.INTRINSIC_GAS_TOO_LOW if tx_gas_delta < 0 else None + + +@pytest.fixture +def tx( + sender: EOA, + ty: int, + tx_data: Bytes, + to: Address | None, + protected: bool, + access_list: List[AccessList] | None, + authorization_list: List[AuthorizationTuple] | None, + blob_versioned_hashes: Sequence[Hash] | None, + tx_gas: int, + tx_error: TransactionException | None, +) -> Transaction: + """ + Create the transaction used in each test. + """ + return Transaction( + ty=ty, + sender=sender, + data=tx_data, + to=to, + protected=protected, + access_list=access_list, + authorization_list=authorization_list, + gas_limit=tx_gas, + blob_versioned_hashes=blob_versioned_hashes, + error=tx_error, + ) diff --git a/tests/prague/eip7623_increase_calldata_cost/helpers.py b/tests/prague/eip7623_increase_calldata_cost/helpers.py new file mode 100644 index 00000000000..1bd28a9446c --- /dev/null +++ b/tests/prague/eip7623_increase_calldata_cost/helpers.py @@ -0,0 +1,53 @@ +""" +Helpers for testing EIP-7623. +""" + +from enum import Enum, auto +from typing import Callable + + +class DataTestType(Enum): + """ + Enum for the different types of data tests. + """ + + FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS = auto() + FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS = auto() + + +def find_floor_cost_threshold( + floor_data_gas_cost_calculator: Callable[[int], int], + intrinsic_gas_cost_calculator: Callable[[int], int], +) -> int: + """ + Find the minimum amount of tokens that will trigger the floor gas cost, by using a binary + search and the intrinsic gas cost and floor data calculators. + """ + # Start with 1000 tokens and if the intrinsic gas cost is greater than the floor gas cost, + # multiply the number of tokens by 2 until it's not. + tokens = 1000 + while floor_data_gas_cost_calculator(tokens) < intrinsic_gas_cost_calculator(tokens): + tokens *= 2 + + # Binary search to find the minimum number of tokens that will trigger the floor gas cost. + left = 0 + right = tokens + while left < right: + tokens = (left + right) // 2 + if floor_data_gas_cost_calculator(tokens) < intrinsic_gas_cost_calculator(tokens): + left = tokens + 1 + else: + right = tokens + tokens = left + + if floor_data_gas_cost_calculator(tokens) > intrinsic_gas_cost_calculator(tokens): + tokens -= 1 + + # Verify that increasing the tokens by one would always trigger the floor gas cost. + assert ( + floor_data_gas_cost_calculator(tokens) <= intrinsic_gas_cost_calculator(tokens) + ) and floor_data_gas_cost_calculator(tokens + 1) > intrinsic_gas_cost_calculator( + tokens + 1 + ), "invalid case" + + return tokens diff --git a/tests/prague/eip7623_increase_calldata_cost/spec.py b/tests/prague/eip7623_increase_calldata_cost/spec.py new file mode 100644 index 00000000000..7d5c083abb8 --- /dev/null +++ b/tests/prague/eip7623_increase_calldata_cost/spec.py @@ -0,0 +1,30 @@ +""" +Defines EIP-7623 specification constants and functions. +""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ReferenceSpec: + """ + Defines the reference spec version and git path. + """ + + git_path: str + version: str + + +ref_spec_7623 = ReferenceSpec("EIPS/eip-7623.md", "9104d079c04737b1fec5f7150715f024d8028558") + + +# Constants +@dataclass(frozen=True) +class Spec: + """ + Parameters from the EIP-7623 specifications as defined at + https://eips.ethereum.org/EIPS/eip-7623 + """ + + STANDARD_TOKEN_COST = 4 + TOTAL_COST_FLOOR_PER_TOKEN = 10 diff --git a/tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py b/tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py new file mode 100644 index 00000000000..0b59bca7569 --- /dev/null +++ b/tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py @@ -0,0 +1,301 @@ +""" +abstract: Test [EIP-7623: Increase calldata cost](https://eips.ethereum.org/EIPS/eip-7623) + Test execution gas consumption after [EIP-7623: Increase calldata cost](https://eips.ethereum.org/EIPS/eip-7623). +""" # noqa: E501 + +from typing import List + +import pytest + +from ethereum_test_forks import Fork, Prague +from ethereum_test_tools import AccessList, Address, Alloc, AuthorizationTuple, Bytes, Hash +from ethereum_test_tools import Opcodes as Op +from ethereum_test_tools import StateTestFiller, Transaction, add_kzg_version + +from ...cancun.eip4844_blobs.spec import Spec as EIP_4844_Spec +from .helpers import DataTestType +from .spec import ref_spec_7623 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7623.git_path +REFERENCE_SPEC_VERSION = ref_spec_7623.version + +ENABLE_FORK = Prague +pytestmark = [pytest.mark.valid_from(str(ENABLE_FORK))] + + +@pytest.fixture +def data_test_type() -> DataTestType: + """ + Data test type. + """ + return DataTestType.FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS + + +@pytest.fixture +def authorization_existing_authority() -> bool: + """ + Force the authority of the authorization tuple to be an existing authority in order + to produce a refund. + """ + return True + + +class TestGasRefunds: + """ + Test gas refunds with EIP-7623 active. + """ + + @pytest.fixture + def intrinsic_gas_data_floor_minimum_delta(self) -> int: + """ + In this test we reset a storage key to zero to induce a refund, + but we need to make sure that the floor is higher than the gas + used during execution in order for the refund to be applied to + the floor. + """ + return 50_000 + + @pytest.fixture + def to( + self, + pre: Alloc, + ) -> Address | None: + """ + Return a contract that when executed results in refunds due to storage clearing. + """ + return pre.deploy_contract(Op.SSTORE(0, 0) + Op.STOP, storage={0: 1}) + + @pytest.mark.parametrize( + "ty,protected,blob_versioned_hashes,authorization_list", + [ + pytest.param(0, False, None, None, id="type_0_unprotected"), + pytest.param(0, True, None, None, id="type_0_protected"), + pytest.param(1, True, None, None, id="type_1"), + pytest.param(2, True, None, None, id="type_2"), + pytest.param( + 3, + True, + add_kzg_version( + [Hash(1)], + EIP_4844_Spec.BLOB_COMMITMENT_VERSION_KZG, + ), + None, + id="type_3", + ), + pytest.param( + 4, + True, + None, + [Address(1)], + id="type_4_with_authorization_refund", + ), + ], + indirect=["authorization_list"], + ) + @pytest.mark.parametrize( + "tx_gas_delta", + [ + # Test with exact gas and extra gas, to verify that the refund is correctly applied up + # to the floor data cost. + pytest.param(1, id="extra_gas"), + pytest.param(0, id="exact_gas"), + ], + ) + def test_gas_refunds_from_data_floor( + self, + state_test: StateTestFiller, + pre: Alloc, + tx: Transaction, + ) -> None: + """ + Test gas refunds deducted from the data floor. + + I.e. the used gas by the intrinsic gas cost plus the execution cost is less than the data + floor, hence data floor is used, and then the gas refunds are applied to the data floor. + """ + state_test( + pre=pre, + post={ + tx.to: { + "storage": {0: 0}, # Verify storage was cleared + } + }, + tx=tx, + ) + + +class TestGasConsumption: + """ + Test gas consumption with EIP-7623 active. + """ + + @pytest.fixture + def intrinsic_gas_data_floor_minimum_delta(self) -> int: + """ + Force a minimum delta in order to have some gas to execute the invalid opcode. + """ + return 50_000 + + @pytest.fixture + def to( + self, + pre: Alloc, + ) -> Address | None: + """ + Return a contract that consumes all gas when executed by calling an invalid opcode. + """ + return pre.deploy_contract(Op.INVALID) + + @pytest.mark.parametrize( + "ty,protected,blob_versioned_hashes,authorization_list", + [ + pytest.param(0, False, None, None, id="type_0_unprotected"), + pytest.param(0, True, None, None, id="type_0_protected"), + pytest.param(1, True, None, None, id="type_1"), + pytest.param(2, True, None, None, id="type_2"), + pytest.param( + 3, + True, + add_kzg_version( + [Hash(1)], + EIP_4844_Spec.BLOB_COMMITMENT_VERSION_KZG, + ), + None, + id="type_3", + ), + pytest.param( + 4, + True, + None, + [Address(1)], + id="type_4_with_authorization_refund", + ), + ], + indirect=["authorization_list"], + ) + @pytest.mark.parametrize( + "tx_gas_delta", + [ + # Test with exact gas and extra gas, to verify that the refund is correctly applied + # to the full consumed execution gas. + pytest.param(1, id="extra_gas"), + pytest.param(0, id="exact_gas"), + ], + ) + def test_full_gas_consumption( + self, + state_test: StateTestFiller, + pre: Alloc, + tx: Transaction, + ) -> None: + """ + Test executing a transaction that fully consumes its execution gas allocation. + """ + state_test( + pre=pre, + post={}, + tx=tx, + ) + + +class TestGasConsumptionBelowDataFloor: + """ + Test gas consumption barely below the floor data cost (1 gas below). + """ + + @pytest.fixture + def contract_creating_tx(self) -> bool: + """ + Use a constant in order to avoid circular fixture dependencies. + """ + return False + + @pytest.fixture + def to( + self, + pre: Alloc, + fork: Fork, + tx_data: Bytes, + access_list: List[AccessList] | None, + authorization_list: List[AuthorizationTuple] | None, + ) -> Address | None: + """ + Return a contract that consumes almost all the gas before reaching the floor data cost. + """ + intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator() + data_floor = intrinsic_gas_cost_calculator( + calldata=tx_data, + contract_creation=False, + access_list=access_list, + authorization_list_or_count=authorization_list, + ) + execution_gas = data_floor - intrinsic_gas_cost_calculator( + calldata=tx_data, + contract_creation=False, + access_list=access_list, + authorization_list_or_count=authorization_list, + return_cost_deducted_prior_execution=True, + ) + assert execution_gas > 0 + + return pre.deploy_contract((Op.JUMPDEST * (execution_gas - 1)) + Op.STOP) + + @pytest.mark.parametrize( + "ty,protected,blob_versioned_hashes,authorization_list,authorization_existing_authority", + [ + pytest.param(0, False, None, None, False, id="type_0_unprotected"), + pytest.param(0, True, None, None, False, id="type_0_protected"), + pytest.param(1, True, None, None, False, id="type_1"), + pytest.param(2, True, None, None, False, id="type_2"), + pytest.param( + 3, + True, + add_kzg_version( + [Hash(1)], + EIP_4844_Spec.BLOB_COMMITMENT_VERSION_KZG, + ), + None, + False, + id="type_3", + ), + pytest.param( + 4, + True, + None, + [Address(1)], + False, + id="type_4", + ), + pytest.param( + 4, + True, + None, + [Address(1)], + True, + id="type_4_with_authorization_refund", + ), + ], + indirect=["authorization_list"], + ) + @pytest.mark.parametrize( + "tx_gas_delta", + [ + # Test with exact gas and extra gas, to verify that the refund is correctly applied + # to the full consumed execution gas. + pytest.param(0, id="exact_gas"), + ], + ) + def test_gas_consumption_below_data_floor( + self, + state_test: StateTestFiller, + pre: Alloc, + tx: Transaction, + ) -> None: + """ + Test executing a transaction that almost consumes the floor data cost. + """ + state_test( + pre=pre, + post={}, + tx=tx, + ) diff --git a/tests/prague/eip7623_increase_calldata_cost/test_transaction_validity.py b/tests/prague/eip7623_increase_calldata_cost/test_transaction_validity.py new file mode 100644 index 00000000000..0c55be14f27 --- /dev/null +++ b/tests/prague/eip7623_increase_calldata_cost/test_transaction_validity.py @@ -0,0 +1,294 @@ +""" +abstract: Test [EIP-7623: Increase calldata cost](https://eips.ethereum.org/EIPS/eip-7623) + Test transaction validity on [EIP-7623: Increase calldata cost](https://eips.ethereum.org/EIPS/eip-7623). +""" # noqa: E501 + + +import pytest + +from ethereum_test_forks import Prague +from ethereum_test_tools import AccessList, Address, Alloc, Hash +from ethereum_test_tools import Opcodes as Op +from ethereum_test_tools import StateTestFiller, Transaction, add_kzg_version + +from ...cancun.eip4844_blobs.spec import Spec as EIP_4844_Spec +from .helpers import DataTestType +from .spec import ref_spec_7623 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7623.git_path +REFERENCE_SPEC_VERSION = ref_spec_7623.version + +ENABLE_FORK = Prague +pytestmark = [pytest.mark.valid_from(str(ENABLE_FORK))] + + +# All tests in this file are parametrized with the following parameters: +pytestmark += [ + pytest.mark.parametrize( + "tx_gas_delta", + [ + # Test the case where the included gas is greater than the intrinsic gas to verify that + # the data floor does not consume more gas than it should. + pytest.param(1, id="extra_gas"), + pytest.param(0, id="exact_gas"), + pytest.param(-1, id="insufficient_gas"), + ], + ), + pytest.mark.parametrize( + "data_test_type", + [ + pytest.param( + DataTestType.FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS, + id="floor_gas_less_than_or_equal_to_intrinsic_gas", + ), + pytest.param( + DataTestType.FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS, + id="floor_gas_greater_than_intrinsic_gas", + ), + ], + ), +] + + +@pytest.mark.parametrize( + "protected", + [ + pytest.param(True, id="protected"), + pytest.param(False, id="unprotected"), + ], +) +@pytest.mark.parametrize( + "ty", + [pytest.param(0, id="type_0")], +) +@pytest.mark.parametrize( + "to", + [ + pytest.param(None, id="contract_creating"), + pytest.param(Op.STOP, id=""), + ], + indirect=True, +) +def test_transaction_validity_type_0( + state_test: StateTestFiller, + pre: Alloc, + tx: Transaction, +) -> None: + """ + Test transaction validity for transactions without access lists and contract creation. + """ + state_test( + pre=pre, + post={}, + tx=tx, + ) + + +@pytest.mark.parametrize( + "to", + [ + pytest.param(None, id="contract_creating"), + pytest.param(Op.STOP, id=""), + ], + indirect=True, +) +@pytest.mark.parametrize( + "access_list", + [ + pytest.param( + None, + id="no_access_list", + ), + pytest.param( + [AccessList(address=Address(1), storage_keys=[])], + id="single_access_list_no_storage_keys", + ), + pytest.param( + [AccessList(address=Address(1), storage_keys=[Hash(0)])], + id="single_access_list_single_storage_key", + ), + pytest.param( + [AccessList(address=Address(1), storage_keys=[Hash(k) for k in range(10)])], + id="single_access_list_multiple_storage_keys", + ), + pytest.param( + [AccessList(address=Address(a), storage_keys=[]) for a in range(10)], + id="multiple_access_lists_no_storage_keys", + ), + pytest.param( + [AccessList(address=Address(a), storage_keys=[Hash(0)]) for a in range(10)], + id="multiple_access_lists_single_storage_key", + ), + pytest.param( + [ + AccessList(address=Address(a), storage_keys=[Hash(k) for k in range(10)]) + for a in range(10) + ], + id="multiple_access_lists_multiple_storage_keys", + ), + ], +) +@pytest.mark.parametrize( + "ty", + [pytest.param(1, id="type_1"), pytest.param(2, id="type_2")], +) +def test_transaction_validity_type_1_type_2( + state_test: StateTestFiller, + pre: Alloc, + tx: Transaction, +) -> None: + """ + Test transaction validity for transactions with access lists and contract creation. + """ + state_test( + pre=pre, + post={}, + tx=tx, + ) + + +@pytest.mark.parametrize( + "access_list", + [ + pytest.param( + None, + id="no_access_list", + ), + pytest.param( + [AccessList(address=Address(1), storage_keys=[])], + id="single_access_list_no_storage_keys", + ), + pytest.param( + [AccessList(address=Address(1), storage_keys=[Hash(0)])], + id="single_access_list_single_storage_key", + ), + pytest.param( + [AccessList(address=Address(1), storage_keys=[Hash(k) for k in range(10)])], + id="single_access_list_multiple_storage_keys", + ), + pytest.param( + [AccessList(address=Address(a), storage_keys=[]) for a in range(10)], + id="multiple_access_lists_no_storage_keys", + ), + pytest.param( + [AccessList(address=Address(a), storage_keys=[Hash(0)]) for a in range(10)], + id="multiple_access_lists_single_storage_key", + ), + pytest.param( + [ + AccessList(address=Address(a), storage_keys=[Hash(k) for k in range(10)]) + for a in range(10) + ], + id="multiple_access_lists_multiple_storage_keys", + ), + ], +) +@pytest.mark.parametrize( + # Blobs don't really have an effect because the blob gas does is not considered in the + # intrinsic gas calculation, but we still test it to make sure that the transaction is + # correctly processed. + "blob_versioned_hashes", + [ + pytest.param( + add_kzg_version( + [Hash(x) for x in range(1)], + EIP_4844_Spec.BLOB_COMMITMENT_VERSION_KZG, + ), + id="single_blob", + ), + pytest.param( + add_kzg_version( + [Hash(x) for x in range(6)], + EIP_4844_Spec.BLOB_COMMITMENT_VERSION_KZG, + ), + id="multiple_blobs", + ), + ], +) +@pytest.mark.parametrize( + "ty", + [pytest.param(3, id="type_3")], +) +def test_transaction_validity_type_3( + state_test: StateTestFiller, + pre: Alloc, + tx: Transaction, +) -> None: + """ + Test transaction validity for transactions with access lists, blobs, but no contract creation. + """ + state_test( + pre=pre, + post={}, + tx=tx, + ) + + +@pytest.mark.parametrize( + "access_list", + [ + pytest.param( + None, + id="no_access_list", + ), + pytest.param( + [AccessList(address=Address(1), storage_keys=[])], + id="single_access_list_no_storage_keys", + ), + pytest.param( + [AccessList(address=Address(1), storage_keys=[Hash(0)])], + id="single_access_list_single_storage_key", + ), + pytest.param( + [AccessList(address=Address(1), storage_keys=[Hash(k) for k in range(10)])], + id="single_access_list_multiple_storage_keys", + ), + pytest.param( + [AccessList(address=Address(a), storage_keys=[]) for a in range(10)], + id="multiple_access_lists_no_storage_keys", + ), + pytest.param( + [AccessList(address=Address(a), storage_keys=[Hash(0)]) for a in range(10)], + id="multiple_access_lists_single_storage_key", + ), + pytest.param( + [ + AccessList(address=Address(a), storage_keys=[Hash(k) for k in range(10)]) + for a in range(10) + ], + id="multiple_access_lists_multiple_storage_keys", + ), + ], +) +@pytest.mark.parametrize( + "authorization_list", + [ + pytest.param( + [Address(1)], + id="single_authorization", + ), + pytest.param( + [Address(i + 1) for i in range(10)], + id="multiple_authorizations", + ), + ], + indirect=True, +) +@pytest.mark.parametrize( + "ty", + [pytest.param(4, id="type_4")], +) +def test_transaction_validity_type_4( + state_test: StateTestFiller, + pre: Alloc, + tx: Transaction, +) -> None: + """ + Test transaction validity for transactions with access lists, authorization lists, but no + contract creation. + """ + state_test( + pre=pre, + post={}, + tx=tx, + ) diff --git a/tests/shanghai/eip3860_initcode/test_initcode.py b/tests/shanghai/eip3860_initcode/test_initcode.py index ec9a81d1251..5472bb98e3e 100644 --- a/tests/shanghai/eip3860_initcode/test_initcode.py +++ b/tests/shanghai/eip3860_initcode/test_initcode.py @@ -7,11 +7,14 @@ - [ethereum/tests/pull/1012](https://github.com/ethereum/tests/pull/990) """ +from typing import List + import pytest from ethereum_test_forks import Fork from ethereum_test_tools import ( EOA, + AccessList, Account, Address, Alloc, @@ -208,14 +211,35 @@ class TestContractCreationGasUsage: """ @pytest.fixture - def exact_intrinsic_gas(self, fork: Fork, initcode: Initcode) -> int: + def tx_access_list(self) -> List[AccessList]: + """ + Starting from EIP-7623, we need to use an access list to raise the intrinsic gas cost to + be above the floor data cost. + """ + return [AccessList(address=Address(i), storage_keys=[]) for i in range(1, 478)] + + @pytest.fixture + def exact_intrinsic_gas( + self, fork: Fork, initcode: Initcode, tx_access_list: List[AccessList] + ) -> int: """ Calculates the intrinsic tx gas cost. """ tx_intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator() + assert tx_intrinsic_gas_cost_calculator( + calldata=initcode, + contract_creation=True, + access_list=tx_access_list, + ) == tx_intrinsic_gas_cost_calculator( + calldata=initcode, + contract_creation=True, + access_list=tx_access_list, + return_cost_deducted_prior_execution=True, + ) return tx_intrinsic_gas_cost_calculator( calldata=initcode, contract_creation=True, + access_list=tx_access_list, ) @pytest.fixture @@ -241,6 +265,7 @@ def tx( sender: EOA, initcode: Initcode, gas_test_case: str, + tx_access_list: List[AccessList], tx_error: TransactionException | None, exact_intrinsic_gas: int, exact_execution_gas: int, @@ -264,6 +289,7 @@ def tx( return Transaction( nonce=0, to=None, + access_list=tx_access_list, data=initcode, gas_limit=gas_limit, gas_price=10,