From 25cb8766462096e091e7a9f97f8815278bc6f0cf Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Mon, 3 Aug 2026 17:03:21 +0800 Subject: [PATCH 1/5] refactor: split setup transactions into blocks --- .../src/execution_testing/specs/blockchain.py | 47 ++++++++ .../tests/test_split_setup_blocks_by_gas.py | 110 ++++++++++++++++++ .../scenario/test_transaction_types.py | 2 +- 3 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 packages/testing/src/execution_testing/specs/tests/test_split_setup_blocks_by_gas.py diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index a96b02df60..b04ff6790b 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -720,6 +720,49 @@ def _split_blocks_by_phase(blocks: List[Block]) -> List[Block]: return out +_SETUP_BLOCK_GAS_HEADROOM = 0.99 + + +def _split_setup_blocks_by_gas( + blocks: List[Block], *, block_gas_limit: int +) -> List[Block]: + """ + Split each SETUP block into runs that fit one block's gas limit. + + A live client inherits its gas limit from the snapshot chain and + cannot raise it, so setup work beyond one block's worth has to span + several or the client rejects the payload with ``gas limit reached``. + Execution blocks pass through at any size: one is the unit a + benchmark measures, so splitting it would change the result. + + Requires single-phase blocks; run ``_split_blocks_by_phase`` first. + """ + budget = int(block_gas_limit * _SETUP_BLOCK_GAS_HEADROOM) + out: List[Block] = [] + for block in blocks: + if not block.txs or block.phase is not TestPhase.SETUP: + out.append(block) + continue + + runs: List[List[Transaction]] = [] + current_run: List[Transaction] = [] + current_gas = 0 + for tx in block.txs: + if current_run and current_gas + tx.gas_limit > budget: + runs.append(current_run) + current_run, current_gas = [], 0 + current_run.append(tx) + current_gas += tx.gas_limit + runs.append(current_run) + + if len(runs) == 1: + out.append(block) + continue + + out.extend(block.model_copy(update={"txs": run}) for run in runs) + return out + + class BlockchainTest(BaseTest): """Filler type that tests multiple blocks (valid or invalid) in a chain.""" @@ -1483,6 +1526,10 @@ def make_stateful_fixture( # split into contiguous phase runs so benchmark gas isn't # swallowed into ``setupEngineNewPayloads``. blocks_to_process.extend(_split_blocks_by_phase(self.blocks)) + blocks_to_process = _split_setup_blocks_by_gas( + blocks_to_process, + block_gas_limit=int(HexNumber(start_block["gasLimit"])), + ) # Chain off the session start_block. We pull parent_* from a # FixtureHeader-validated copy of the client's block dict, but diff --git a/packages/testing/src/execution_testing/specs/tests/test_split_setup_blocks_by_gas.py b/packages/testing/src/execution_testing/specs/tests/test_split_setup_blocks_by_gas.py new file mode 100644 index 0000000000..e082503d04 --- /dev/null +++ b/packages/testing/src/execution_testing/specs/tests/test_split_setup_blocks_by_gas.py @@ -0,0 +1,110 @@ +"""Test suite for ``_split_setup_blocks_by_gas`` in stateful filling.""" + +import pytest + +from execution_testing.test_types import TestPhase, Transaction + +from ..blockchain import ( + _SETUP_BLOCK_GAS_HEADROOM, + Block, + _split_setup_blocks_by_gas, +) + +BLOCK_GAS_LIMIT = 1_000_000 +"""Stand-in for the snapshot chain's block gas limit.""" + +BUDGET = int(BLOCK_GAS_LIMIT * _SETUP_BLOCK_GAS_HEADROOM) + + +def _tx( + gas_limit: int, phase: TestPhase | None = TestPhase.SETUP +) -> Transaction: + """Build a phase-tagged Transaction of the given gas limit.""" + tx = Transaction(gas_limit=gas_limit) + tx.test_phase = phase + return tx + + +def _split(block: Block) -> list[Block]: + """Split *block* against the stand-in block gas limit.""" + return _split_setup_blocks_by_gas([block], block_gas_limit=BLOCK_GAS_LIMIT) + + +def test_passthrough_within_budget() -> None: + """A setup block that already fits is returned unchanged.""" + block = Block(txs=[_tx(100_000) for _ in range(5)]) + out = _split(block) + assert len(out) == 1 + assert out[0] is block + + +def test_passthrough_empty() -> None: + """Empty-txs block is returned unchanged (identity).""" + block = Block(txs=[]) + out = _split(block) + assert len(out) == 1 + assert out[0] is block + + +def test_passthrough_all_untagged() -> None: + """Block with no phase-tagged txs is returned unchanged.""" + block = Block(txs=[_tx(BLOCK_GAS_LIMIT, phase=None) for _ in range(4)]) + out = _split(block) + assert len(out) == 1 + assert out[0] is block + + +def test_execution_block_never_split() -> None: + """Execution block survives intact far past the block gas limit.""" + block = Block( + txs=[ + _tx(BLOCK_GAS_LIMIT, phase=TestPhase.EXECUTION) for _ in range(10) + ] + ) + out = _split(block) + assert len(out) == 1 + assert out[0] is block + + +def test_every_sub_block_fits_the_budget() -> None: + """Over-budget setup work spans blocks that each fit.""" + out = _split(Block(txs=[_tx(200_000) for _ in range(25)])) + assert len(out) > 1 + for block in out: + assert sum(tx.gas_limit for tx in block.txs) <= BUDGET + + +def test_split_preserves_every_tx_in_order() -> None: + """Splitting neither drops, duplicates nor reorders transactions.""" + txs = [_tx(200_000) for _ in range(25)] + out = _split(Block(txs=txs)) + assert [tx for block in out for tx in block.txs] == txs + + +def test_oversized_tx_gets_its_own_block() -> None: + """A tx past the budget on its own is isolated, not dropped.""" + small, huge = _tx(21_000), _tx(BLOCK_GAS_LIMIT * 3) + out = _split(Block(txs=[small, huge, small])) + assert [len(block.txs) for block in out] == [1, 1, 1] + assert out[1].txs[0] is huge + + +@pytest.mark.parametrize( + "gas_limits, expected_sizes", + [ + ([500_000, 400_000], [2]), + ([500_000, 600_000], [1, 1]), + ([900_000, 90_000], [2]), + ([900_000, 100_000], [1, 1]), + ([250_000] * 9, [3, 3, 3]), + ([300_000] * 7, [3, 3, 1]), + ([BLOCK_GAS_LIMIT // 2] * 2, [1, 1]), + ], +) +def test_sub_block_sizes( + gas_limits: list[int], expected_sizes: list[int] +) -> None: + """Sub-block sizes follow a greedy pack of the gas budget.""" + block = Block(txs=[_tx(gas) for gas in gas_limits]) + out = _split(block) + assert [len(block.txs) for block in out] == expected_sizes diff --git a/tests/benchmark/compute/scenario/test_transaction_types.py b/tests/benchmark/compute/scenario/test_transaction_types.py index 78746fd188..07a4d14966 100644 --- a/tests/benchmark/compute/scenario/test_transaction_types.py +++ b/tests/benchmark/compute/scenario/test_transaction_types.py @@ -406,7 +406,7 @@ def test_block_full_data( txs.append( Transaction( - to=pre.fund_eoa(), + to=pre.fund_eoa(amount=1), data=data, gas_limit=gas_available + intrinsic_cost, sender=pre.fund_eoa(), From 50b9420424997b4cf91173d2fce1152d03d05fb4 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Mon, 3 Aug 2026 17:29:05 +0800 Subject: [PATCH 2/5] refactor: use single sender to avoid state creation --- tests/benchmark/compute/instruction/test_account_query.py | 6 ++++-- tests/benchmark/compute/scenario/test_transaction_types.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/benchmark/compute/instruction/test_account_query.py b/tests/benchmark/compute/instruction/test_account_query.py index 43e7614065..35fe811887 100644 --- a/tests/benchmark/compute/instruction/test_account_query.py +++ b/tests/benchmark/compute/instruction/test_account_query.py @@ -247,6 +247,7 @@ def test_ext_account_query_cold( creation_txs = [] with TestPhaseManager.setup(): + creation_sender = pre.fund_eoa() num_creation_txs = math.ceil( num_target_accounts / max_creations_per_tx ) @@ -257,7 +258,7 @@ def test_ext_account_query_cold( to=factory_address, data=Hash(addr_start), gas_limit=tx_gas_limit, - sender=pre.fund_eoa(), + sender=creation_sender, ) ) blocks.append(Block(txs=creation_txs)) @@ -285,6 +286,7 @@ def test_ext_account_query_cold( execution_txs = [] with TestPhaseManager.execution(): + execution_sender = pre.fund_eoa() max_target_per_tx = ( tx_gas_limit - intrinsic_gas_cost_calc() ) // cold_account_access_gas @@ -304,7 +306,7 @@ def test_ext_account_query_cold( to=op_address, data=calldata, gas_limit=gas_limit, - sender=pre.fund_eoa(), + sender=execution_sender, ) ) gas_used += gas_limit diff --git a/tests/benchmark/compute/scenario/test_transaction_types.py b/tests/benchmark/compute/scenario/test_transaction_types.py index 07a4d14966..3da0684f7f 100644 --- a/tests/benchmark/compute/scenario/test_transaction_types.py +++ b/tests/benchmark/compute/scenario/test_transaction_types.py @@ -295,6 +295,7 @@ def test_ether_transfers_to_precompile( recipient_type=RecipientType.PRECOMPILE, ) iteration_count = gas_benchmark_value // iteration_cost + sender = pre.fund_eoa() txs = [] for _ in range(iteration_count): txs.append( @@ -302,7 +303,7 @@ def test_ether_transfers_to_precompile( to=Address(precompile), value=transfer_amount, gas_limit=iteration_cost, - sender=pre.fund_eoa(), + sender=sender, ) ) From 0468269bef2f4696024291e1fe43db2192af8570 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Tue, 4 Aug 2026 17:40:14 +0800 Subject: [PATCH 3/5] refactor: limit tx batch size --- .../pytest_commands/plugins/shared/live_client_flags.py | 2 +- packages/testing/src/execution_testing/rpc/rpc.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/live_client_flags.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/live_client_flags.py index bd89330f73..dbb7ae13f3 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/live_client_flags.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/live_client_flags.py @@ -28,7 +28,7 @@ # Multiplier applied to a one-shot live fee-market query to absorb the gap # between query timing and tx submission (basefee can climb a few blocks # between the two; the bump keeps txs landing without per-tx requeries). -FEE_BUMP_MULTIPLIER = 1.5 +FEE_BUMP_MULTIPLIER = 3.0 def pytest_addoption(parser: pytest.Parser) -> None: diff --git a/packages/testing/src/execution_testing/rpc/rpc.py b/packages/testing/src/execution_testing/rpc/rpc.py index 739c2c2696..05cf9a6de9 100644 --- a/packages/testing/src/execution_testing/rpc/rpc.py +++ b/packages/testing/src/execution_testing/rpc/rpc.py @@ -1119,7 +1119,7 @@ def get_alloc( ) all_calls: List[RPCCall] = [] - # (address, per-account call_info list, call count) + # (address, per-account call_info list) address_info: List[tuple[Address, List[tuple[str, Any]]]] = [] for address, account in alloc.root.items(): @@ -1129,7 +1129,12 @@ def get_alloc( all_calls.extend(calls) address_info.append((address, call_info)) - responses = self.post_batch_request(calls=all_calls) + responses: List[JSONRPCResponse] = [] + batch_size = self.max_transactions_per_batch + for i in range(0, len(all_calls), batch_size): + responses.extend( + self.post_batch_request(calls=all_calls[i : i + batch_size]) + ) result_alloc: Dict[Address, Account | None] = {} offset = 0 From 4afd3eea4377c8563f2f9b112be7aeadaa025256 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Tue, 4 Aug 2026 17:44:46 +0800 Subject: [PATCH 4/5] refactor: apply suggestion --- .../src/execution_testing/specs/blockchain.py | 25 ++++++++++++++++++- .../tests/test_split_setup_blocks_by_gas.py | 13 +++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index b04ff6790b..205dd9f6d6 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -748,6 +748,12 @@ def _split_setup_blocks_by_gas( current_run: List[Transaction] = [] current_gas = 0 for tx in block.txs: + if tx.gas_limit > block_gas_limit: + raise ValueError( + f"Setup tx gas_limit ({int(tx.gas_limit)}) exceeds " + f"the snapshot chain's block gas limit " + f"({block_gas_limit}); no split can make it fit." + ) if current_run and current_gas + tx.gas_limit > budget: runs.append(current_run) current_run, current_gas = [], 0 @@ -759,7 +765,24 @@ def _split_setup_blocks_by_gas( out.append(block) continue - out.extend(block.model_copy(update={"txs": run}) for run in runs) + out.extend( + block.model_copy( + update={ + "txs": run_txs, + "header_verify": None, + "rlp_modifier": None, + "expected_block_access_list": None, + "expected_post_state": None, + "expected_gas_used": None, + "exception": None, + "skip_exception_verification": False, + "engine_api_error_code": None, + } + ) + for run_txs in runs[:-1] + ) + out.append(block.model_copy(update={"txs": runs[-1]})) + return out diff --git a/packages/testing/src/execution_testing/specs/tests/test_split_setup_blocks_by_gas.py b/packages/testing/src/execution_testing/specs/tests/test_split_setup_blocks_by_gas.py index e082503d04..2e92f5c9eb 100644 --- a/packages/testing/src/execution_testing/specs/tests/test_split_setup_blocks_by_gas.py +++ b/packages/testing/src/execution_testing/specs/tests/test_split_setup_blocks_by_gas.py @@ -81,14 +81,21 @@ def test_split_preserves_every_tx_in_order() -> None: assert [tx for block in out for tx in block.txs] == txs -def test_oversized_tx_gets_its_own_block() -> None: - """A tx past the budget on its own is isolated, not dropped.""" - small, huge = _tx(21_000), _tx(BLOCK_GAS_LIMIT * 3) +def test_tx_at_the_limit_gets_its_own_block() -> None: + """A tx past the budget but within the limit is isolated, not dropped.""" + small, huge = _tx(21_000), _tx(BLOCK_GAS_LIMIT) out = _split(Block(txs=[small, huge, small])) assert [len(block.txs) for block in out] == [1, 1, 1] assert out[1].txs[0] is huge +def test_tx_past_the_limit_raises() -> None: + """A tx no block can hold fails loudly instead of being emitted.""" + block = Block(txs=[_tx(21_000), _tx(BLOCK_GAS_LIMIT * 3)]) + with pytest.raises(ValueError, match="no split can make it fit"): + _split(block) + + @pytest.mark.parametrize( "gas_limits, expected_sizes", [ From 3af71ea86cd3776d490a305ea4c4233296be4cee Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Tue, 4 Aug 2026 18:25:48 +0800 Subject: [PATCH 5/5] refactor: enhance test implementation --- .../stateful/bloatnet/test_account_query.py | 1 + .../stateful/bloatnet/test_transaction_types.py | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/benchmark/stateful/bloatnet/test_account_query.py b/tests/benchmark/stateful/bloatnet/test_account_query.py index bec412631d..83e07cf2d9 100644 --- a/tests/benchmark/stateful/bloatnet/test_account_query.py +++ b/tests/benchmark/stateful/bloatnet/test_account_query.py @@ -209,6 +209,7 @@ def test_account_access( opcode( address=address_source.address_op(), value=value_sent, + gas=Op.GAS, # Gas accounting address_warm=access_warm, value_transfer=value_sent > 0, diff --git a/tests/benchmark/stateful/bloatnet/test_transaction_types.py b/tests/benchmark/stateful/bloatnet/test_transaction_types.py index 77f08358e0..01ee979b03 100644 --- a/tests/benchmark/stateful/bloatnet/test_transaction_types.py +++ b/tests/benchmark/stateful/bloatnet/test_transaction_types.py @@ -155,7 +155,7 @@ def test_ether_transfers_onchain_receivers( raise ValueError(f"Unknown case: {case_id}") sends_value = transfer_amount > 0 - iteration_cost = ( + regular_cost = ( fork.transaction_intrinsic_cost_calculator()( sends_value=sends_value, recipient_type=recipient_type, @@ -164,13 +164,14 @@ def test_ether_transfers_onchain_receivers( sends_value=sends_value, recipient_type=recipient_type, ) - + fork.transaction_top_frame_state_gas( - sends_value=sends_value, - recipient_type=recipient_type, - ) + receiver_execution_gas ) - iteration_count = gas_benchmark_value // iteration_cost + state_cost = fork.transaction_top_frame_state_gas( + sends_value=sends_value, + recipient_type=recipient_type, + ) + iteration_cost = regular_cost + state_cost + iteration_count = gas_benchmark_value // max(regular_cost, state_cost) txs = [] for _ in range(iteration_count):