From 80177d2eeb58d24e8a6a1ecc299d3f633ba7af7f Mon Sep 17 00:00:00 2001 From: spencer Date: Thu, 26 Mar 2026 14:00:18 +0000 Subject: [PATCH 1/5] chore(test-client-clis): update besu exception mapper (#2565) --- .../testing/src/execution_testing/client_clis/clis/besu.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/testing/src/execution_testing/client_clis/clis/besu.py b/packages/testing/src/execution_testing/client_clis/clis/besu.py index dbe965c64eb..a48ec82f50e 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/besu.py +++ b/packages/testing/src/execution_testing/client_clis/clis/besu.py @@ -424,11 +424,13 @@ class BesuExceptionMapper(ExceptionMapper): r"exceeds transaction sender account balance 0x[0-9a-f]+" ), TransactionException.INTRINSIC_GAS_TOO_LOW: ( - r"transaction invalid intrinsic gas cost \d+ " + r"transaction invalid intrinsic gas cost \d+" + r"(?: \(regular \d+ \+ state \d+\))? " r"exceeds gas limit \d+" ), TransactionException.INTRINSIC_GAS_BELOW_FLOOR_GAS_COST: ( - r"transaction invalid intrinsic gas cost \d+ " + r"transaction invalid intrinsic gas cost \d+" + r"(?: \(regular \d+ \+ state \d+\))? " r"exceeds gas limit \d+" ), TransactionException.SENDER_NOT_EOA: ( From f2b2039b3458a8cb51841af9053a2ce24747c1b3 Mon Sep 17 00:00:00 2001 From: spencer Date: Thu, 26 Mar 2026 14:19:36 +0000 Subject: [PATCH 2/5] chore(test-client-clis): update erigon exception mapper (#2564) Add BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED regex mapping for erigon's "block access list too large" validation error. Update GAS_LIMIT_EXCEEDS_MAXIMUM regex to match erigon's current error format ("gas limit too high") instead of the old txnIdx-prefixed format. --- .../testing/src/execution_testing/client_clis/clis/erigon.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/testing/src/execution_testing/client_clis/clis/erigon.py b/packages/testing/src/execution_testing/client_clis/clis/erigon.py index a4331640fc0..699fd4f5744 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/erigon.py +++ b/packages/testing/src/execution_testing/client_clis/clis/erigon.py @@ -112,8 +112,11 @@ class ErigonExceptionMapper(ExceptionMapper): r"invalid block access list" ), BlockException.INCORRECT_BLOCK_FORMAT: (r"invalid block access list"), + BlockException.BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED: ( + r"block access list too large" + ), TransactionException.GAS_LIMIT_EXCEEDS_MAXIMUM: ( - r"invalid block, txnIdx=\d+,.*gas limit too high" + r"gas limit too high" ), BlockException.INCORRECT_BLOB_GAS_USED: ( r"blobGasUsed by execution: \d+, in header: \d+" From 0ada2de695d7519cc8df6972865d43853191d47f Mon Sep 17 00:00:00 2001 From: felix Date: Thu, 26 Mar 2026 15:42:06 +0100 Subject: [PATCH 3/5] fix(test-tests,ci): fix flakiness due to mutated `EnvironmentDefaults.gas_limit` with xdist (#2566) * fix: resolves worker-state leak problem where EnvironmentDefaults.gas_limit is being mutated in one nested pytest session and then reused by later tests on the same worker, Date: 2026-03-26, 14:26:54.0336062192 * fix: remove unnecessary line, Date: 2026-03-26, 15:18:49.0327541876 --- .../plugins/execute/execute.py | 6 +- .../plugins/execute/tests/test_execute.py | 57 +++++++++++++++++++ .../pytest_commands/plugins/filler/filler.py | 3 +- .../plugins/filler/tests/conftest.py | 17 ++++-- .../plugins/filler/tests/test_filler.py | 1 - .../pytest_commands/plugins/shared/helpers.py | 12 ++++ 6 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/tests/test_execute.py diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/execute.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/execute.py index 59941c04857..f5c567627bb 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/execute.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/execute.py @@ -26,6 +26,7 @@ get_spec_format_for_item, is_help_or_collectonly_mode, labeled_format_parameter_set, + option_was_explicitly_set, ) from ..spec_version_checker.spec_version_checker import EIPSpecTestItem from .pre_alloc import Alloc @@ -200,8 +201,9 @@ def pytest_configure(config: pytest.Config) -> None: called before the pytest-html plugin's pytest_configure to ensure that it uses the modified `htmlpath` option. """ - # Modify the block gas limit if specified. - if config.getoption("transaction_gas_limit"): + # Keep execute-mode overrides working, but avoid rewriting the global + # default when this plugin is merely imported into nested pytest sessions. + if option_was_explicitly_set(config, "--transaction-gas-limit"): EnvironmentDefaults.gas_limit = config.getoption( "transaction_gas_limit" ) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/tests/test_execute.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/tests/test_execute.py new file mode 100644 index 00000000000..2f606ad21c8 --- /dev/null +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/tests/test_execute.py @@ -0,0 +1,57 @@ +"""Regression tests for execute plugin configuration.""" + +from types import SimpleNamespace +from typing import Any + +from execution_testing.test_types.block_types import EnvironmentDefaults + +from ..execute import pytest_configure + + +class FakeConfig: + """Minimal config object for exercising execute plugin setup.""" + + engine_rpc_supported: bool + + def __init__(self, *args: str) -> None: + self.invocation_params = SimpleNamespace(args=args) + self.option = SimpleNamespace(help=True) + self.pluginmanager = SimpleNamespace(has_plugin=lambda _name: False) + + def getoption(self, name: str, default: Any = None) -> Any: + """Return configured option values used by pytest_configure.""" + options = { + "transaction_gas_limit": 7, + "disable_html": False, + "htmlpath": None, + "markers": False, + "collectonly": False, + "show_ported_from": False, + "links_as_filled": False, + "help": True, + } + return options.get(name, default) + + +def test_pytest_configure_ignores_default_transaction_gas_limit() -> None: + """Default execute options must not rewrite the global block gas limit.""" + original_gas_limit = EnvironmentDefaults.gas_limit + + config = FakeConfig() + pytest_configure(config) # type: ignore[arg-type] + + assert EnvironmentDefaults.gas_limit == original_gas_limit + assert config.engine_rpc_supported is False + + +def test_pytest_configure_applies_explicit_transaction_gas_limit() -> None: + """An explicit execute gas-limit override still updates the default.""" + original_gas_limit = EnvironmentDefaults.gas_limit + + config = FakeConfig("--transaction-gas-limit=7") + pytest_configure(config) # type: ignore[arg-type] + + assert EnvironmentDefaults.gas_limit == 7 + assert config.engine_rpc_supported is False + + EnvironmentDefaults.gas_limit = original_gas_limit diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py index 012022631e0..3ba89261097 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py @@ -89,6 +89,7 @@ get_spec_format_for_item, is_help_or_collectonly_mode, labeled_format_parameter_set, + option_was_explicitly_set, ) from ..spec_version_checker.spec_version_checker import ( get_ref_spec_from_module, @@ -827,7 +828,7 @@ def pytest_configure(config: pytest.Config) -> None: """ # Register custom markers # Modify the block gas limit if specified. - if config.getoption("block_gas_limit"): + if option_was_explicitly_set(config, "--block-gas-limit"): EnvironmentDefaults.gas_limit = config.getoption("block_gas_limit") # Initialize fixture output configuration diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/conftest.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/conftest.py index 4c7920f5f6c..ea3bfada648 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/conftest.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/conftest.py @@ -5,15 +5,20 @@ import pytest -@pytest.fixture +@pytest.fixture(autouse=True) def restore_environment_defaults() -> Generator[None, None, None]: """ - Restore EnvironmentDefaults.gas_limit after tests. + Reset EnvironmentDefaults.gas_limit around each test. - Restore the gas limit after the test run to prevent side effects. + Reset the gas limit to DEFAULT_BLOCK_GAS_LIMIT before and after each test + run to prevent side effects from nested in-process pytest sessions leaking + into later tests on the same worker. """ - from execution_testing.test_types.block_types import EnvironmentDefaults + from execution_testing.test_types.block_types import ( + DEFAULT_BLOCK_GAS_LIMIT, + EnvironmentDefaults, + ) - original_gas_limit = EnvironmentDefaults.gas_limit + EnvironmentDefaults.gas_limit = DEFAULT_BLOCK_GAS_LIMIT yield - EnvironmentDefaults.gas_limit = original_gas_limit + EnvironmentDefaults.gas_limit = DEFAULT_BLOCK_GAS_LIMIT diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_filler.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_filler.py index a9c5cdae630..73e97d70f62 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_filler.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_filler.py @@ -872,7 +872,6 @@ def test_max_gas_limit(state_test, pre, block_gas_limit) -> None: ), ], ) -@pytest.mark.usefixtures("restore_environment_defaults") def test_fill_variables( testdir: pytest.Testdir, args: list[str], diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/helpers.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/helpers.py index 8b37e019096..07c2b9474ea 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/helpers.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/helpers.py @@ -16,6 +16,18 @@ from execution_testing.specs import BaseTest +def option_was_explicitly_set(config: pytest.Config, option_name: str) -> bool: + """Return whether a long CLI option was passed explicitly.""" + normalized_option = option_name.strip() + if not normalized_option.startswith("--"): + normalized_option = f"--{normalized_option}" + + for arg in config.invocation_params.args: + if arg == normalized_option or arg.startswith(f"{normalized_option}="): + return True + return False + + def is_help_or_collectonly_mode(config: pytest.Config) -> bool: """Check if pytest is running in a help or collectonly mode.""" return ( From 5c198a343beb0d2e06a31294ecac6b4b9dd0bb25 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Thu, 26 Mar 2026 18:25:27 +0100 Subject: [PATCH 4/5] refactor(test-benchmark): generic erc20 benchmark (#2569) --- .../stateful/bloatnet/test_single_opcode.py | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/tests/benchmark/stateful/bloatnet/test_single_opcode.py b/tests/benchmark/stateful/bloatnet/test_single_opcode.py index 69fdc7ad7b1..2cc30d13ad9 100644 --- a/tests/benchmark/stateful/bloatnet/test_single_opcode.py +++ b/tests/benchmark/stateful/bloatnet/test_single_opcode.py @@ -60,6 +60,93 @@ % (2**160) ) + +@pytest.mark.parametrize("token_name", SLOAD_TOKENS) +def test_sload_erc20_generic( + benchmark_test: BenchmarkTestFiller, + pre: Alloc, + fork: Fork, + gas_benchmark_value: int, + tx_gas_limit: int, + token_name: str, +) -> None: + """Benchmark SLOAD using ERC20 balanceOf on bloatnet.""" + # Stub Account + erc20_address = pre.deploy_contract( + code=Bytecode(), + stub=f"test_sload_empty_erc20_balanceof_{token_name}", + ) + threshold = 100000 + + # MEM[0] = function selector + # MEM[32] = starting address offset + setup = Op.MSTORE( + 0, + BALANCEOF_SELECTOR, + # gas accounting + old_memory_size=0, + new_memory_size=32, + ) + Op.MSTORE( + 32, + Op.SLOAD(0), # Address Offset + # gas accounting + old_memory_size=32, + new_memory_size=64, + ) + + call_balance_of = Op.POP( + Op.CALL( + address=erc20_address, + args_offset=32 - 4, + args_size=32 + 4, + ) + ) + + loop = While( + body=call_balance_of + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)), + condition=Op.GT(Op.GAS, threshold), + ) + + teardown = Op.SSTORE(0, Op.MLOAD(32)) + + # Contract Deployment + code = setup + loop + teardown + attack_contract_address = pre.deploy_contract(code=code) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() + + # Transaction Loops + txs = [] + gas_remaining = gas_benchmark_value + + sender = pre.fund_eoa() + + while gas_remaining > intrinsic_gas: + gas_available = min(gas_remaining, tx_gas_limit) + + if gas_available < intrinsic_gas: + break + + with TestPhaseManager.execution(): + txs.append( + Transaction( + gas_limit=gas_available, + to=attack_contract_address, + sender=sender, + ) + ) + + gas_remaining -= gas_available + + blocks = [Block(txs=txs)] + benchmark_test( + pre=pre, + blocks=blocks, + skip_gas_used_validation=True, + expected_receipt_status=True, + ) + + # SLOAD BENCHMARK ARCHITECTURE: # # [Pre-deployed ERC20 Contract] ──── Storage slots for balances @@ -273,6 +360,97 @@ def test_sload_erc20_balanceof( benchmark_test(pre=pre, blocks=blocks, skip_gas_used_validation=True) +@pytest.mark.parametrize("token_name", SSTORE_TOKENS) +def test_sstore_erc20_generic( + benchmark_test: BenchmarkTestFiller, + pre: Alloc, + fork: Fork, + gas_benchmark_value: int, + tx_gas_limit: int, + token_name: str, +) -> None: + """Benchmark SSTORE using ERC20 approve.""" + sender = pre.fund_eoa() + + threshold = 100_000 + + # Stub Account + erc20_address = pre.deploy_contract( + code=Bytecode(), + stub=f"test_sstore_erc20_approve_{token_name}", + ) + + # MEM[0] = function selector + # MEM[32] = starting address offset + setup = Op.MSTORE( + 0, + APPROVE_SELECTOR, + ) + Op.MSTORE( + 32, + Op.SLOAD(0), # Address Offset + ) + + call_approve = Op.MSTORE( + 64, + Op.ADD(1, Op.MLOAD(32)), + ) + Op.POP( + Op.CALL( + address=erc20_address, + args_offset=28, + args_size=68, + ) + ) + + loop = While( + body=call_approve + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)), + condition=Op.GT(Op.GAS, threshold), + ) + + teardown = Op.SSTORE(0, Op.MLOAD(32)) + + # Contract Deployment + code = setup + loop + teardown + attack_contract_address = pre.deploy_contract(code=code) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() + + # Transaction Loops + gas_remaining = gas_benchmark_value + + # Collect tx params first, then build Transaction objects + # so that nonces are allocated contiguously per block. + tx_gas: list[int] = [] + while gas_remaining > intrinsic_gas: + gas_available = min(gas_remaining, tx_gas_limit) + + if gas_available < intrinsic_gas: + break + + tx_gas.append(gas_available) + + gas_remaining -= gas_available + + txs = [] + with TestPhaseManager.execution(): + for gas_available in tx_gas: + txs.append( + Transaction( + gas_limit=gas_available, + to=attack_contract_address, + sender=sender, + ) + ) + + blocks = [Block(txs=txs)] + + benchmark_test( + pre=pre, + blocks=blocks, + skip_gas_used_validation=True, + expected_receipt_status=True, + ) + + @pytest.mark.repricing @pytest.mark.parametrize("cache_strategy", list(CacheStrategy)) @pytest.mark.parametrize("token_name", SSTORE_TOKENS) From 6e98103b8ac378c44a02c81e2734898c782ac717 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 26 Mar 2026 21:05:35 +0300 Subject: [PATCH 5/5] chore(test-client-clis): update BAL exceptions for nethermind (#2550) * Refactor BAL exception patterns in nethermind.py * Update comments for BAL exceptions handling Clarify comments regarding BAL exceptions and their patterns. * Add handling for BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED * Fix ws --- .../client_clis/clis/nethermind.py | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/testing/src/execution_testing/client_clis/clis/nethermind.py b/packages/testing/src/execution_testing/client_clis/clis/nethermind.py index 8ef9411886c..90745b6ac6b 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/nethermind.py +++ b/packages/testing/src/execution_testing/client_clis/clis/nethermind.py @@ -397,6 +397,9 @@ class NethermindExceptionMapper(ExceptionMapper): "InvalidStateRoot: State root in header does not match" ), BlockException.GAS_USED_OVERFLOW: ("Block gas limit exceeded"), + BlockException.BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED: ( + "BlockAccessListGasLimitExceeded:" + ), } mapping_regex = { TransactionException.INSUFFICIENT_ACCOUNT_FUNDS: ( @@ -431,21 +434,29 @@ class NethermindExceptionMapper(ExceptionMapper): BlockException.SYSTEM_CONTRACT_CALL_FAILED: ( r"(Withdrawals|Consolidations)Failed: Contract execution failed\." ), - # BAL Exceptions: TODO - review once all clients completed. - BlockException.INVALID_BAL_EXTRA_ACCOUNT: ( - r"could not be parsed as a block: " - r"Could not decode block access list." - ), - BlockException.INVALID_BAL_HASH: (r"InvalidBlockLevelAccessListRoot:"), + # BAL Exceptions — specific exceptions have unique patterns, but + # INVALID_BLOCK_ACCESS_LIST and INCORRECT_BLOCK_FORMAT intentionally + # overlap because the test framework requires `want in got` matching. + BlockException.INVALID_BAL_HASH: (r"InvalidBlockLevelAccessListHash:"), BlockException.INVALID_BAL_MISSING_ACCOUNT: ( - r"InvalidBlockLevelAccessListRoot:" + r"InvalidBlockLevelAccessList:.*missing account" + ), + BlockException.INVALID_BAL_EXTRA_ACCOUNT: ( + r"InvalidBlockLevelAccessList:.*surplus changes" + r"|could not be parsed as a block: " + r"Error decoding block access list:" ), BlockException.INVALID_BLOCK_ACCESS_LIST: ( - r"InvalidBlockLevelAccessListRoot:|could not be parsed as a " - r"block: Could not decode block access list." + r"InvalidBlockLevelAccessListHash:" + r"|InvalidBlockLevelAccessList:" + r"|could not be parsed as a block: " + r"Error decoding block access list:" ), BlockException.INCORRECT_BLOCK_FORMAT: ( r"could not be parsed as a block: " - r"Could not decode block access list." + r"Error decoding block access list:" + ), + TransactionException.GAS_ALLOWANCE_EXCEEDED: ( + r"TxGasLimitCapExceeded:" ), }