From 7e70d5666f7fb4ecc0e557fda745f88eec95a60e Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 22 Jan 2026 15:22:30 +0100 Subject: [PATCH 1/3] feat(spec-tests): add eip-7778 for calldata checks --- docs/CHANGELOG.md | 1 + .../test_gas_accounting.py | 215 ++++++++++++++++++ 2 files changed, 216 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2069f1710e8..4729c278107 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -82,6 +82,7 @@ Test fixtures for use by clients are available for each release on the [Github r - ✨ Add tests for an old validation rule for gas limit above 5000 ([#1731](https://github.com/ethereum/execution-specs/pull/1731)). - ✨ Add tests for OOG in EXP, LOG and others ([#1686](https://github.com/ethereum/execution-specs/pull/1686)). - ✨ Make EIP-7934 tests more dynamic and able to handle new header fields added in future forks ([#2022](https://github.com/ethereum/execution-specs/pull/2022)). +- ✨ Add EIP-7778 tests to check various values of call data floor cost relative to gas_used ([#2060](https://github.com/ethereum/execution-specs/pull/2060)). ## [v5.3.0](https://github.com/ethereum/execution-spec-tests/releases/tag/v5.3.0) - 2025-10-09 diff --git a/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py index 89ff82ebe12..486225e88b7 100644 --- a/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py +++ b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py @@ -3,6 +3,8 @@ [EIP-7778 Block Gas Accounting without Refunds](https://eips.ethereum.org/EIPS/eip-7778). """ +from enum import Enum + import pytest from execution_testing import ( Account, @@ -15,9 +17,11 @@ Environment, Fork, RefundTypes, + Storage, Transaction, TransactionException, ) +from execution_testing.base_types import HashInt from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "EIPS/eip-7778.md" @@ -417,3 +421,214 @@ def test_multi_transaction_gas_accounting( post=post, genesis_environment=Environment(gas_limit=environment_gas_limit), ) + + +class CallDataTestType(Enum): + """Refund test type.""" + + DATA_FLOOR_LT_TX_GAS_AFTER_REFUND = -1 + """ + calldata_floor < tx_gas_after_refund. + """ + DATA_FLOOR_BETWEEN_TX_GAS_BEFORE_AND_AFTER = 0 + """ + tx_gas_after_refund < calldata_floor < tx_gas_before_refund. + """ + DATA_FLOOR_GT_TX_GAS_BEFORE_REFUND = 1 + """calldata_floor > tx_gas_before_refund.""" + + +@pytest.mark.parametrize( + "refund_tx_reverts", + [ + pytest.param(True, id="refund_tx_reverts"), + pytest.param(False, id=""), + ], +) +@pytest.mark.parametrize( + "calldata_test_type", + [ + CallDataTestType.DATA_FLOOR_LT_TX_GAS_AFTER_REFUND, + CallDataTestType.DATA_FLOOR_BETWEEN_TX_GAS_BEFORE_AND_AFTER, + CallDataTestType.DATA_FLOOR_GT_TX_GAS_BEFORE_REFUND, + ], +) +@pytest.mark.with_all_refund_types() +@pytest.mark.valid_from("Amsterdam") +def test_varying_calldata_costs( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + refund_type: RefundTypes, + refund_tx_reverts: bool, + calldata_test_type: CallDataTestType, +) -> None: + """ + Test by varying the calldata_floor_cost. + + Performs tests for the following 3 scenarios. + + 1. calldata_floor < tx_gas_after_refund + 2. tx_gas_after_refund < calldata_floor < tx_gas_before_refund + 3. calldata_floor > tx_gas_before_refund + """ + fork_gas_costs = fork.gas_costs() + intrinsic_cost_calc = fork.transaction_intrinsic_cost_calculator() + data_floor_calc = fork.transaction_data_floor_cost_calculator() + max_refund_quotient = fork.max_refund_quotient() + + initial_fund = 10**18 + sender = pre.fund_eoa(initial_fund) + + post = {} + match refund_type: + case RefundTypes.STORAGE_CLEAR: + if ( + refund_tx_reverts + and calldata_test_type + == CallDataTestType.DATA_FLOOR_BETWEEN_TX_GAS_BEFORE_AND_AFTER + ): + pytest.skip( + "calldata_cost cannot be between pre and post refund gas" + "since refund is zero when execution reverts" + ) + + bytes_to_add_per_iteration = b"00" * 2 + + pre_storage: Storage = Storage({HashInt(0): HashInt(1)}) + + code = Op.SSTORE(0, 0) + execution_cost = ( + 2 * fork_gas_costs.G_VERY_LOW + + fork_gas_costs.G_COLD_SLOAD + + fork_gas_costs.G_STORAGE_RESET + ) + authorization_list = None + + refund_counter = fork_gas_costs.R_STORAGE_CLEAR + post_storage: Storage = Storage({HashInt(0): HashInt(0)}) + + if refund_tx_reverts: + code += Op.REVERT(0, 0) + execution_cost += 2 * fork_gas_costs.G_VERY_LOW # For Push + post_storage = pre_storage + refund_counter = 0 + + contract_address = pre.deploy_contract( + code=code, + storage=pre_storage, + ) + post[contract_address] = Account(storage=post_storage) + + case RefundTypes.AUTHORIZATION_EXISTING_AUTHORITY: + bytes_to_add_per_iteration = b"00" * 10 + + # Refund is non-zero even if execution reverts + refund_counter = fork_gas_costs.R_AUTHORIZATION_EXISTING_AUTHORITY + + execution_cost = 0 + if refund_tx_reverts: + code = Op.REVERT(0, 0) + execution_cost += 2 * fork_gas_costs.G_VERY_LOW # For Push + else: + code = Op.STOP + + contract_address = pre.deploy_contract(code=code) + authorization_list = [ + AuthorizationTuple( + address=contract_address, + nonce=1, + signer=sender, + ) + ] + case _: + raise ValueError( + f"Unknown refund type: {refund_type} (Test needs update)" + ) + + data = b"" + + # Time to start searching for appropriate call data for each scenario + num_iterations = 200 + # Currently in Amsterdam, the optimal call data is found in about + # 30 iterations for CallDataTestType.DATA_FLOOR_GT_TX_GAS_BEFORE_REFUND. + # Setting this higher just to make it + # a bit more future proof if the gas calc logic changes + found_call_data = False + for _ in range(num_iterations): + gas_used_pre_refund = ( + intrinsic_cost_calc( + calldata=data, + return_cost_deducted_prior_execution=True, + authorization_list_or_count=authorization_list, + ) + + execution_cost + ) + effective_refund = min( + refund_counter, gas_used_pre_refund // max_refund_quotient + ) + gas_used_post_refund = gas_used_pre_refund - effective_refund + + call_data_floor_cost = data_floor_calc(data=data) + + if ( + calldata_test_type + == CallDataTestType.DATA_FLOOR_LT_TX_GAS_AFTER_REFUND + ): + if call_data_floor_cost < gas_used_post_refund: + found_call_data = True + break + elif ( + calldata_test_type + == CallDataTestType.DATA_FLOOR_BETWEEN_TX_GAS_BEFORE_AND_AFTER + ): + if ( + gas_used_post_refund + < call_data_floor_cost + < gas_used_pre_refund + ): + found_call_data = True + break + elif ( + calldata_test_type + == CallDataTestType.DATA_FLOOR_GT_TX_GAS_BEFORE_REFUND + ): + if gas_used_pre_refund < call_data_floor_cost: + found_call_data = True + break + else: + raise ValueError("Invalid calldata test type") + + data += bytes_to_add_per_iteration + + if not found_call_data: + raise ValueError( + f"Could not find the call_data with {num_iterations} iterations." + ) + + gas_used = max(call_data_floor_cost, gas_used_pre_refund) + gas_spent = max(call_data_floor_cost, gas_used_post_refund) + + tx = Transaction( + to=contract_address, + gas_limit=fork.transaction_gas_limit_cap(), + data=data, + sender=sender, + gas_price=7, + authorization_list=authorization_list, + expected_receipt={ + "gas_used": gas_used, + "gas_spent": gas_spent, + }, + ) + + assert tx.gas_price is not None, "tx.gas_price should not be None" + expected_balance = initial_fund - gas_spent * tx.gas_price + + post[sender] = Account(balance=expected_balance) + + blockchain_test( + pre=pre, + blocks=[Block(txs=[tx], expected_gas_used=gas_used)], + post=post, + ) From dc400418014ba79520a1794133d56017a48f66bb Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Thu, 22 Jan 2026 17:59:47 +0100 Subject: [PATCH 2/3] feat(spec-tests): post review - use code gas_cost function Co-authored-by: Louis Tsai <72684086+LouisTsai-Csie@users.noreply.github.com> --- .../test_gas_accounting.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py index 486225e88b7..041fa58600c 100644 --- a/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py +++ b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py @@ -497,7 +497,7 @@ def test_varying_calldata_costs( pre_storage: Storage = Storage({HashInt(0): HashInt(1)}) - code = Op.SSTORE(0, 0) + code = Op.SSTORE(0, 0, original_value=1, new_value=0) execution_cost = ( 2 * fork_gas_costs.G_VERY_LOW + fork_gas_costs.G_COLD_SLOAD @@ -510,10 +510,10 @@ def test_varying_calldata_costs( if refund_tx_reverts: code += Op.REVERT(0, 0) - execution_cost += 2 * fork_gas_costs.G_VERY_LOW # For Push post_storage = pre_storage refund_counter = 0 + execution_cost = code.gas_cost(fork) contract_address = pre.deploy_contract( code=code, storage=pre_storage, @@ -526,13 +526,12 @@ def test_varying_calldata_costs( # Refund is non-zero even if execution reverts refund_counter = fork_gas_costs.R_AUTHORIZATION_EXISTING_AUTHORITY - execution_cost = 0 if refund_tx_reverts: code = Op.REVERT(0, 0) - execution_cost += 2 * fork_gas_costs.G_VERY_LOW # For Push else: code = Op.STOP + execution_cost = code.gas_cost(fork) contract_address = pre.deploy_contract(code=code) authorization_list = [ AuthorizationTuple( From b346d830b06e70a09f16818a3185cde77d0792ef Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Thu, 22 Jan 2026 22:36:36 +0100 Subject: [PATCH 3/3] Apply suggestions from code review --- .../test_gas_accounting.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py index 041fa58600c..a9e9526f625 100644 --- a/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py +++ b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/test_gas_accounting.py @@ -498,14 +498,10 @@ def test_varying_calldata_costs( pre_storage: Storage = Storage({HashInt(0): HashInt(1)}) code = Op.SSTORE(0, 0, original_value=1, new_value=0) - execution_cost = ( - 2 * fork_gas_costs.G_VERY_LOW - + fork_gas_costs.G_COLD_SLOAD - + fork_gas_costs.G_STORAGE_RESET - ) + execution_cost = code.gas_cost(fork) authorization_list = None - refund_counter = fork_gas_costs.R_STORAGE_CLEAR + refund_counter = code.refund(fork) post_storage: Storage = Storage({HashInt(0): HashInt(0)}) if refund_tx_reverts: