diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2d9b2dc850c..cad3bc21861 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -23,7 +23,7 @@ Release tarball changes: - ✨ [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-7702](https://eips.ethereum.org/EIPS/eip-7702) implement 7702 test ideas ([#981](https://github.com/ethereum/execution-spec-tests/pull/981)) -- ✨ [EIP-7623](https://eips.ethereum.org/EIPS/eip-7623) Increase calldata cost ([#1004](https://github.com/ethereum/execution-spec-tests/pull/1004)) +- ✨ [EIP-7623](https://eips.ethereum.org/EIPS/eip-7623) Increase calldata cost ([#1004](https://github.com/ethereum/execution-spec-tests/pull/1004), [#1071](https://github.com/ethereum/execution-spec-tests/pull/1071)) - ✨ Add generic precompile-absence test ([#1036](https://github.com/ethereum/execution-spec-tests/pull/1036)) - ✨ Add test for [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537) which uses the full discount table of G2 MSM ([#1038](https://github.com/ethereum/execution-spec-tests/pull/1038)) - 🔀 Update EIP-7251 according to [spec updates](https://github.com/ethereum/EIPs/pull/9127) ([#1024](https://github.com/ethereum/execution-spec-tests/pull/1024)). diff --git a/tests/prague/eip7623_increase_calldata_cost/conftest.py b/tests/prague/eip7623_increase_calldata_cost/conftest.py index dd1d25c5327..7da764cfec2 100644 --- a/tests/prague/eip7623_increase_calldata_cost/conftest.py +++ b/tests/prague/eip7623_increase_calldata_cost/conftest.py @@ -231,7 +231,30 @@ def tx_gas_delta() -> int: @pytest.fixture -def tx_intrinsic_gas_cost( +def tx_intrinsic_gas_cost_before_execution( + fork: Fork, + tx_data: Bytes, + access_list: List[AccessList] | None, + authorization_list: List[AuthorizationTuple] | None, + contract_creating_tx: bool, +) -> int: + """ + Return the intrinsic gas cost that is applied before the execution start. + + This value never includes the floor data gas cost. + """ + 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, + return_cost_deducted_prior_execution=True, + ) + + +@pytest.fixture +def tx_intrinsic_gas_cost_including_floor_data_cost( fork: Fork, tx_data: Bytes, access_list: List[AccessList] | None, @@ -242,7 +265,9 @@ def tx_intrinsic_gas_cost( Transaction intrinsic gas cost. The calculated value takes into account the normal intrinsic gas cost and the floor data gas - cost. + cost if it is greater than the intrinsic gas cost. + + In other words, this is the value that is required for the transaction to be valid. """ intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator() return intrinsic_gas_cost_calculator( @@ -265,7 +290,7 @@ def tx_floor_data_cost( @pytest.fixture def tx_gas_limit( - tx_intrinsic_gas_cost: int, + tx_intrinsic_gas_cost_including_floor_data_cost: int, tx_gas_delta: int, ) -> int: """ @@ -273,7 +298,7 @@ def tx_gas_limit( The gas delta is added to the intrinsic gas cost to generate different test scenarios. """ - return tx_intrinsic_gas_cost + tx_gas_delta + return tx_intrinsic_gas_cost_including_floor_data_cost + tx_gas_delta @pytest.fixture diff --git a/tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py b/tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py index c14fea638bd..f7880cacd25 100644 --- a/tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py +++ b/tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py @@ -38,98 +38,8 @@ def data_test_type() -> DataTestType: @pytest.fixture def authorization_refund() -> 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.fixture - def refund(self, fork: Fork, ty: int) -> int: - """Return the refund gas of the transaction.""" - gas_costs = fork.gas_costs() - refund = gas_costs.R_STORAGE_CLEAR - if ty == 4: - refund += gas_costs.R_AUTHORIZATION_EXISTING_AUTHORITY - return refund - - @pytest.mark.parametrize( - "ty,protected,authorization_list", - [ - pytest.param(0, False, None, id="type_0_unprotected"), - pytest.param(0, True, None, id="type_0_protected"), - pytest.param(1, True, None, id="type_1"), - pytest.param(2, True, None, id="type_2"), - pytest.param( - 3, - True, - None, - id="type_3", - ), - pytest.param( - 4, - True, - [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, - tx_floor_data_cost: int, - refund: int, - ) -> 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. - """ - tx.expected_receipt = TransactionReceipt(gas_used=tx_floor_data_cost - refund) - state_test( - pre=pre, - post={ - tx.to: { - "storage": {0: 0}, # Verify storage was cleared - } - }, - tx=tx, - ) + """Disable the refunds on these tests (see ./test_refunds.py).""" + return False class TestGasConsumption: @@ -148,20 +58,6 @@ def to( """Return a contract that consumes all gas when executed by calling an invalid opcode.""" return pre.deploy_contract(Op.INVALID) - @pytest.fixture - def refund( - self, - fork: Fork, - ty: int, - authorization_refund: bool, - ) -> int: - """Return the refund gas of the transaction.""" - gas_costs = fork.gas_costs() - refund = 0 - if ty == 4 and authorization_refund: - refund += gas_costs.R_AUTHORIZATION_EXISTING_AUTHORITY - return refund - @pytest.mark.parametrize( "ty,protected,authorization_list", [ @@ -169,26 +65,15 @@ def refund( pytest.param(0, True, None, id="type_0_protected"), pytest.param(1, True, None, id="type_1"), pytest.param(2, True, None, id="type_2"), - pytest.param( - 3, - True, - None, - id="type_3", - ), - pytest.param( - 4, - True, - [Address(1)], - id="type_4_with_authorization_refund", - ), + pytest.param(3, True, None, id="type_3"), + pytest.param(4, True, [Address(1)], id="type_4"), ], 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. + # Test with exact gas and extra gas. pytest.param(1, id="extra_gas"), pytest.param(0, id="exact_gas"), ], @@ -198,10 +83,9 @@ def test_full_gas_consumption( state_test: StateTestFiller, pre: Alloc, tx: Transaction, - refund: int, ) -> None: """Test executing a transaction that fully consumes its execution gas allocation.""" - tx.expected_receipt = TransactionReceipt(gas_used=tx.gas_limit - refund) + tx.expected_receipt = TransactionReceipt(gas_used=tx.gas_limit) state_test( pre=pre, post={}, @@ -217,20 +101,6 @@ def contract_creating_tx(self) -> bool: """Use a constant in order to avoid circular fixture dependencies.""" return False - @pytest.fixture - def refund( - self, - fork: Fork, - ty: int, - authorization_refund: bool, - ) -> int: - """Return the refund gas of the transaction.""" - gas_costs = fork.gas_costs() - refund = 0 - if ty == 4 and authorization_refund: - refund += gas_costs.R_AUTHORIZATION_EXISTING_AUTHORITY - return refund - @pytest.fixture def to( self, @@ -258,33 +128,14 @@ def to( return pre.deploy_contract((Op.JUMPDEST * (execution_gas - 1)) + Op.STOP) @pytest.mark.parametrize( - "ty,protected,authorization_list,authorization_refund", + "ty,protected,authorization_list", [ - pytest.param(0, False, None, False, id="type_0_unprotected"), - pytest.param(0, True, None, False, id="type_0_protected"), - pytest.param(1, True, None, False, id="type_1"), - pytest.param(2, True, None, False, id="type_2"), - pytest.param( - 3, - True, - None, - False, - id="type_3", - ), - pytest.param( - 4, - True, - [Address(1)], - False, - id="type_4", - ), - pytest.param( - 4, - True, - [Address(1)], - True, - id="type_4_with_authorization_refund", - ), + pytest.param(0, False, None, id="type_0_unprotected"), + pytest.param(0, True, None, id="type_0_protected"), + pytest.param(1, True, None, id="type_1"), + pytest.param(2, True, None, id="type_2"), + pytest.param(3, True, None, id="type_3"), + pytest.param(4, True, [Address(1)], id="type_4"), ], indirect=["authorization_list"], ) @@ -302,10 +153,9 @@ def test_gas_consumption_below_data_floor( pre: Alloc, tx: Transaction, tx_floor_data_cost: int, - refund: int, ) -> None: """Test executing a transaction that almost consumes the floor data cost.""" - tx.expected_receipt = TransactionReceipt(gas_used=tx_floor_data_cost - refund) + tx.expected_receipt = TransactionReceipt(gas_used=tx_floor_data_cost) state_test( pre=pre, post={}, diff --git a/tests/prague/eip7623_increase_calldata_cost/test_refunds.py b/tests/prague/eip7623_increase_calldata_cost/test_refunds.py new file mode 100644 index 00000000000..a5829bbb105 --- /dev/null +++ b/tests/prague/eip7623_increase_calldata_cost/test_refunds.py @@ -0,0 +1,296 @@ +""" +abstract: Test [EIP-7623: Increase calldata cost](https://eips.ethereum.org/EIPS/eip-7623) + Test applied refunds for [EIP-7623: Increase calldata cost](https://eips.ethereum.org/EIPS/eip-7623). +""" # noqa: E501 + +from enum import Enum, Flag, auto +from typing import Dict, List + +import pytest + +from ethereum_test_forks import Fork, Prague +from ethereum_test_tools import ( + Address, + Alloc, + AuthorizationTuple, + Bytecode, + StateTestFiller, + Transaction, + TransactionReceipt, +) +from ethereum_test_tools import Opcodes as Op + +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))] + + +class RefundTestType(Enum): + """Refund test type.""" + + EXECUTION_GAS_MINUS_REFUND_GREATER_THAN_DATA_FLOOR = 0 + """ + The execution gas minus the refund is greater than the data floor, hence the execution gas cost + is charged. + """ + EXECUTION_GAS_MINUS_REFUND_LESS_THAN_DATA_FLOOR = 1 + """ + The execution gas minus the refund is less than the data floor, hence the data floor cost is + charged. + """ + EXECUTION_GAS_MINUS_REFUND_EQUAL_TO_DATA_FLOOR = 2 + """ + The execution gas minus the refund is equal to the data floor. + """ + + +class RefundType(Flag): + """Refund type.""" + + STORAGE_CLEAR = auto() + """The storage is cleared from a non-zero value.""" + + AUTHORIZATION_EXISTING_AUTHORITY = auto() + """The authorization list contains an authorization where the authority exists in the state.""" + + +@pytest.fixture +def data_test_type() -> DataTestType: + """Return data test type.""" + return DataTestType.FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS + + +@pytest.fixture +def authorization_list(pre: Alloc, refund_type: RefundType) -> List[AuthorizationTuple] | None: + """Modify fixture from conftest to automatically read the refund_type information.""" + if RefundType.AUTHORIZATION_EXISTING_AUTHORITY not in refund_type: + return None + return [AuthorizationTuple(signer=pre.fund_eoa(1), address=Address(1))] + + +@pytest.fixture +def ty(refund_type: RefundType) -> int: + """Modify fixture from conftest to automatically read the refund_type information.""" + if RefundType.AUTHORIZATION_EXISTING_AUTHORITY in refund_type: + return 4 + return 2 + + +@pytest.fixture +def max_refund(fork: Fork, refund_type: RefundType) -> int: + """Return the max refund gas of the transaction.""" + gas_costs = fork.gas_costs() + max_refund = gas_costs.R_STORAGE_CLEAR if RefundType.STORAGE_CLEAR in refund_type else 0 + max_refund += ( + gas_costs.R_AUTHORIZATION_EXISTING_AUTHORITY + if RefundType.AUTHORIZATION_EXISTING_AUTHORITY in refund_type + else 0 + ) + return max_refund + + +@pytest.fixture +def prefix_code_gas(fork: Fork, refund_type: RefundType) -> int: + """Return the minimum execution gas cost due to the refund type.""" + if RefundType.STORAGE_CLEAR in refund_type: + # Minimum code to generate a storage clear is Op.SSTORE(0, 0). + gas_costs = fork.gas_costs() + return gas_costs.G_COLD_SLOAD + gas_costs.G_STORAGE_RESET + (gas_costs.G_VERY_LOW * 2) + return 0 + + +@pytest.fixture +def prefix_code(refund_type: RefundType) -> Bytecode: + """Return the minimum execution gas cost due to the refund type.""" + if RefundType.STORAGE_CLEAR in refund_type: + # Clear the storage to trigger a refund. + return Op.SSTORE(0, 0) + return Bytecode() + + +@pytest.fixture +def code_storage(refund_type: RefundType) -> Dict: + """Return the minimum execution gas cost due to the refund type.""" + if RefundType.STORAGE_CLEAR in refund_type: + # Pre-set the storage to be cleared. + return {0: 1} + return {} + + +@pytest.fixture +def contract_creating_tx() -> bool: + """ + Override fixture in order to avoid a circular fixture dependency since + none of theses tests are contract creating transactions. + """ + return False + + +@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. + + Since at least one of the cases requires some execution gas expenditure (SSTORE clearing), + we need to introduce an increment of the floor data cost above the transaction intrinsic gas + cost, otherwise the floor data cost would always be the below the execution gas cost even + after the refund is applied. + + This value has been set as of Prague and should be adjusted if the gas costs change. + """ + return 250 + + +@pytest.fixture +def execution_gas_used( + tx_intrinsic_gas_cost_before_execution: int, + tx_floor_data_cost: int, + max_refund: int, + prefix_code_gas: int, + refund_test_type: RefundTestType, +) -> int: + """ + Return the amount of gas that needs to be consumed by the execution. + + This gas amount is on top of the transaction intrinsic gas cost. + + If this value were zero it would result in the refund being applied to the execution gas cost + and the resulting amount being always below the floor data cost, hence we need to find a + higher value in this function to ensure we get both scenarios where the refund drives + the execution cost below the floor data cost and above the floor data cost. + """ + + def execution_gas_cost(execution_gas: int) -> int: + total_gas_used = tx_intrinsic_gas_cost_before_execution + execution_gas + return total_gas_used - min(max_refund, total_gas_used // 5) + + execution_gas = prefix_code_gas + + assert execution_gas_cost(execution_gas) < tx_floor_data_cost, ( + "tx_floor_data_cost is too low, there might have been a gas cost change that caused this " + "test to fail. Try increasing the intrinsic_gas_data_floor_minimum_delta fixture." + ) + + # Dumb for-loop to find the execution gas cost that will result in the expected refund. + while execution_gas_cost(execution_gas) < tx_floor_data_cost: + execution_gas += 1 + if refund_test_type == RefundTestType.EXECUTION_GAS_MINUS_REFUND_EQUAL_TO_DATA_FLOOR: + return execution_gas + elif refund_test_type == RefundTestType.EXECUTION_GAS_MINUS_REFUND_GREATER_THAN_DATA_FLOOR: + return execution_gas + 1 + elif refund_test_type == RefundTestType.EXECUTION_GAS_MINUS_REFUND_LESS_THAN_DATA_FLOOR: + return execution_gas - 1 + + raise ValueError("Invalid refund test type") + + +@pytest.fixture +def refund( + tx_intrinsic_gas_cost_before_execution: int, + execution_gas_used: int, + max_refund: int, +) -> int: + """Return the refund gas of the transaction.""" + total_gas_used = tx_intrinsic_gas_cost_before_execution + execution_gas_used + return min(max_refund, total_gas_used // 5) + + +@pytest.fixture +def to( + pre: Alloc, + execution_gas_used: int, + prefix_code: Bytecode, + prefix_code_gas: int, + code_storage: Dict, +) -> Address | None: + """ + Return a contract that consumes the expected execution gas. + + At the moment we naively use JUMPDEST to consume the gas, which can yield very big contracts. + + Ideally, we can use memory expansion to consume gas. + """ + extra_gas = execution_gas_used - prefix_code_gas + return pre.deploy_contract( + prefix_code + (Op.JUMPDEST * extra_gas) + Op.STOP, + storage=code_storage, + ) + + +@pytest.fixture +def tx_gas_limit( + tx_intrinsic_gas_cost_including_floor_data_cost: int, + tx_intrinsic_gas_cost_before_execution: int, + execution_gas_used: int, +) -> int: + """ + Gas limit for the transaction. + + The gas delta is added to the intrinsic gas cost to generate different test scenarios. + """ + tx_gas_limit = tx_intrinsic_gas_cost_before_execution + execution_gas_used + assert tx_gas_limit >= tx_intrinsic_gas_cost_including_floor_data_cost + return tx_gas_limit + + +@pytest.mark.parametrize( + "refund_test_type", + [ + RefundTestType.EXECUTION_GAS_MINUS_REFUND_GREATER_THAN_DATA_FLOOR, + RefundTestType.EXECUTION_GAS_MINUS_REFUND_LESS_THAN_DATA_FLOOR, + RefundTestType.EXECUTION_GAS_MINUS_REFUND_EQUAL_TO_DATA_FLOOR, + ], +) +@pytest.mark.parametrize( + "refund_type", + [ + RefundType.STORAGE_CLEAR, + RefundType.STORAGE_CLEAR | RefundType.AUTHORIZATION_EXISTING_AUTHORITY, + RefundType.AUTHORIZATION_EXISTING_AUTHORITY, + ], +) +def test_gas_refunds_from_data_floor( + state_test: StateTestFiller, + pre: Alloc, + tx: Transaction, + tx_floor_data_cost: int, + tx_intrinsic_gas_cost_before_execution: int, + execution_gas_used: int, + refund: int, + refund_test_type: RefundTestType, +) -> None: + """Test gas refunds deducted from the execution gas cost and not the data floor.""" + gas_used = tx_intrinsic_gas_cost_before_execution + execution_gas_used - refund + if refund_test_type == RefundTestType.EXECUTION_GAS_MINUS_REFUND_LESS_THAN_DATA_FLOOR: + assert gas_used < tx_floor_data_cost + elif refund_test_type == RefundTestType.EXECUTION_GAS_MINUS_REFUND_GREATER_THAN_DATA_FLOOR: + assert gas_used > tx_floor_data_cost + elif refund_test_type == RefundTestType.EXECUTION_GAS_MINUS_REFUND_EQUAL_TO_DATA_FLOOR: + assert gas_used == tx_floor_data_cost + else: + raise ValueError("Invalid refund test type") + if gas_used < tx_floor_data_cost: + gas_used = tx_floor_data_cost + # This is the actual test verification: + # - During test filling, the receipt returned by the transition tool (t8n) is verified against + # the expected receipt. + # - During test consumption, this is reflected in the balance difference and the state + # root. + tx.expected_receipt = TransactionReceipt(gas_used=gas_used) + state_test( + pre=pre, + post={ + tx.to: { + # Verify that the storage was cleared (for storage clear refund). + # See `code_storage` fixture for more details. + "storage": {0: 0}, + } + }, + tx=tx, + )