From 59cdbf91cdcca420c5b45663b668219774d84fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E4=BD=B3=E8=AA=A0=20Louis=20Tsai?= <72684086+LouisTsai-Csie@users.noreply.github.com> Date: Thu, 7 May 2026 16:22:22 +0200 Subject: [PATCH 1/5] refactor(test-benchmark): support 8037 for `SSTORE` benchmark (#2787) --- .../tools/tests/test_iterating_bytecode.py | 3 +- .../tools/tools_code/generators.py | 104 +++++++---- .../stateful/bloatnet/test_single_opcode.py | 170 +++++++++++------- 3 files changed, 177 insertions(+), 100 deletions(-) diff --git a/packages/testing/src/execution_testing/tools/tests/test_iterating_bytecode.py b/packages/testing/src/execution_testing/tools/tests/test_iterating_bytecode.py index 4c5309bfbbf..6e82db00bad 100644 --- a/packages/testing/src/execution_testing/tools/tests/test_iterating_bytecode.py +++ b/packages/testing/src/execution_testing/tools/tests/test_iterating_bytecode.py @@ -325,7 +325,8 @@ def test_tx_iterations_by_total_iteration_count_raises_on_impossible() -> None: with pytest.raises( ValueError, - match="Single iteration gas cost is greater than gas limit.", + match="Single iteration gas cost exceeds gas_limit " + "or compute_gas_limit.", ): list( bytecode.tx_iterations_by_total_iteration_count( diff --git a/packages/testing/src/execution_testing/tools/tools_code/generators.py b/packages/testing/src/execution_testing/tools/tools_code/generators.py index 3e82c18bd0f..9d15d425e59 100644 --- a/packages/testing/src/execution_testing/tools/tools_code/generators.py +++ b/packages/testing/src/execution_testing/tools/tools_code/generators.py @@ -806,6 +806,10 @@ class IteratingBytecode(Bytecode): """ cleanup: Bytecode """Bytecode executed once at the end after all iterations complete.""" + iterating_state_gas: int + """ + State-gas portion (EIP-8037) charged per loop iteration. + """ def __new__( cls, @@ -815,6 +819,7 @@ def __new__( cleanup: Bytecode | None = None, warm_iterating: Bytecode | None = None, iterating_subcall: Bytecode | int | None = None, + iterating_state_gas: int = 0, ) -> Self: """ Create a new iterating bytecode. @@ -833,6 +838,8 @@ def __new__( calculation. The value can also be an integer, in which case it represents the gas cost of the subcall (e.g. the subcall is a precompiled contract). + iterating_state_gas: EIP-8037 state-gas portion charged + per iteration, defaults to 0. Returns: A new IteratingBytecode instance. @@ -860,6 +867,7 @@ def __new__( if cleanup is None: cleanup = Bytecode() instance.cleanup = cleanup + instance.iterating_state_gas = iterating_state_gas return instance def iterating_subcall_gas_cost( @@ -985,60 +993,85 @@ def tx_gas_limit_by_iteration_count( **intrinsic_cost_kwargs, ) + self.iterating_subcall_reserve(fork=fork) + def _iterations_fit_within_gas_limits( + self, + *, + fork: Fork, + iteration_count: int, + start_iteration: int, + gas_limit: int, + compute_gas_limit: int | None = None, + **intrinsic_cost_kwargs: Any, + ) -> bool: + """ + Check whether iteration_count iterations fit within the gas limits. + + Returns True when both: + - The combined regular+state gas (i.e. tx.gas) is <= + gas_limit (block-budget constraint). + - The regular gas, computed as + combined - iteration_count * iterating_state_gas, + respects the compute_gas_limit. + """ + if iteration_count <= 0: + return True + combined = self.tx_gas_limit_by_iteration_count( + fork=fork, + iteration_count=iteration_count, + start_iteration=start_iteration, + **intrinsic_cost_kwargs, + ) + if combined > gas_limit: + return False + if compute_gas_limit is not None: + compute = combined - iteration_count * self.iterating_state_gas + if compute > compute_gas_limit: + return False + return True + def _binary_search_iterations( self, *, fork: Fork, gas_limit: int, start_iteration: int, + compute_gas_limit: int | None = None, **intrinsic_cost_kwargs: Any, ) -> Tuple[int, int]: """ Binary search for the maximum iterations that fit within a gas limit. """ - single_iteration_gas = self.tx_gas_limit_by_iteration_count( - fork=fork, - iteration_count=1, - start_iteration=start_iteration, + fits_kwargs: Dict[str, Any] = { + "fork": fork, + "start_iteration": start_iteration, + "gas_limit": gas_limit, + "compute_gas_limit": compute_gas_limit, **intrinsic_cost_kwargs, - ) - if single_iteration_gas > gas_limit: + } + + if not self._iterations_fit_within_gas_limits( + iteration_count=1, **fits_kwargs + ): raise ValueError( - "Single iteration gas cost is greater than gas limit." + "Single iteration gas cost exceeds gas_limit " + "or compute_gas_limit." ) + low = 1 high = 2 # Exponential search to find upper bound - high_gas_cost = self.tx_gas_limit_by_iteration_count( - fork=fork, - iteration_count=high, - start_iteration=start_iteration, - **intrinsic_cost_kwargs, - ) - while high_gas_cost < gas_limit: + while self._iterations_fit_within_gas_limits( + iteration_count=high, **fits_kwargs + ): low = high high *= 2 - high_gas_cost = self.tx_gas_limit_by_iteration_count( - fork=fork, - iteration_count=high, - start_iteration=start_iteration, - **intrinsic_cost_kwargs, - ) # Binary search for exact fit - best_iterations = 0 while low < high: mid = (low + high) // 2 - - if ( - self.tx_gas_limit_by_iteration_count( - fork=fork, - iteration_count=mid, - start_iteration=start_iteration, - **intrinsic_cost_kwargs, - ) - > gas_limit + if not self._iterations_fit_within_gas_limits( + iteration_count=mid, **fits_kwargs ): high = mid else: @@ -1082,17 +1115,11 @@ def tx_iterations_by_gas_limit( start_iteration=start_iteration, **intrinsic_cost_kwargs, ): - # Binary search for the maximum number of iterations that fits - # within remaining_gas - max_gas_limit = ( - min(remaining_gas, gas_limit_cap) - if gas_limit_cap is not None - else remaining_gas - ) best_iterations, best_iterations_gas = ( self._binary_search_iterations( fork=fork, - gas_limit=max_gas_limit, + gas_limit=remaining_gas, + compute_gas_limit=gas_limit_cap, start_iteration=start_iteration, **intrinsic_cost_kwargs, ) @@ -1142,6 +1169,7 @@ def tx_iterations_by_total_iteration_count( best_iterations, _ = self._binary_search_iterations( fork=fork, gas_limit=gas_limit_cap, + compute_gas_limit=gas_limit_cap, start_iteration=start_iteration, **intrinsic_cost_kwargs, ) diff --git a/tests/benchmark/stateful/bloatnet/test_single_opcode.py b/tests/benchmark/stateful/bloatnet/test_single_opcode.py index 6f97d3ff8ec..1403c95b704 100644 --- a/tests/benchmark/stateful/bloatnet/test_single_opcode.py +++ b/tests/benchmark/stateful/bloatnet/test_single_opcode.py @@ -9,7 +9,7 @@ from enum import Enum, auto from functools import partial -from typing import Generator, List +from typing import Any, Callable, Generator, List import pytest from execution_testing import ( @@ -133,12 +133,10 @@ def run_bloated_eoa_benchmark( existing_slots: bool, runtime_code: Bytecode, cache_strategy: CacheStrategy, + tx_generator: Callable[[EOA], list[Transaction]] | None = None, ) -> None: """ Run a bloated-EOA benchmark with the given runtime delegation code. - - Handles authority setup, slot 0 initialization, delegation to - runtime code, benchmark tx generation, and test invocation. """ slot_0_value = Hash(1) if existing_slots else Hash(START_SLOT) @@ -154,22 +152,25 @@ def run_bloated_eoa_benchmark( blocks: list[Block] = [Block(txs=[init_tx, runtime_tx])] - gas_available = gas_benchmark_value - intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() sender = pre.fund_eoa() txs: list[Transaction] = [] with TestPhaseManager.execution(): - while gas_available >= intrinsic_gas: - tx_gas = min(gas_available, tx_gas_limit) - txs.append( - Transaction( - gas_limit=tx_gas, - to=authority, - sender=sender, + if tx_generator is not None: + txs = tx_generator(sender) + else: + gas_available = gas_benchmark_value + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()() + while gas_available >= intrinsic_gas: + tx_gas = min(gas_available, tx_gas_limit) + txs.append( + Transaction( + gas_limit=tx_gas, + to=authority, + sender=sender, + ) ) - ) - gas_available -= tx_gas + gas_available -= tx_gas cache_txs: list[Transaction] = [] if cache_strategy == CacheStrategy.CACHE_PREVIOUS_BLOCK: @@ -179,6 +180,7 @@ def run_bloated_eoa_benchmark( cache_txs.append( Transaction( gas_limit=tx.gas_limit, + data=tx.data, to=authority, sender=cache_sender, ) @@ -592,73 +594,119 @@ def test_sstore_bloated( ) -> None: """ Benchmark SSTORE opcodes targeting an EOA with storage bloated. - - The storage is assumed to be filled from 0-N linearly, where - each slot has the value of the key. Except slot 0, this is the - pointer to the next free (empty) storage slot. - - For this test to work correctly under all parameters then above - has to be true. If this is not the case then some tests will not - test what they claim to do. For instance, for `write_new_value` - set to False we need to know the current value of the slots. """ + sstore_metadata: dict[str, Any] = {} + # If CACHE_TX, there would be one cold SLOAD before SSTORE + sstore_metadata["key_warm"] = cache_strategy == CacheStrategy.CACHE_TX + + # SSTORE metadata matrix: + # + # existing_slots | write_new_value | original | current | new + # ---------------+-----------------+----------+---------+----- + # True | True | 1 | 1 | 2 + # True | False | 1 | 1 | 1 + # False | True | 0 | 0 | 1 + # False | False | 0 | 0 | 0 + + initial_value = int(existing_slots) + + # When existing_slots is False, the initial value is always 0 + # Otherwise, the initial value starts at 1 instead. + sstore_metadata["original_value"] = initial_value + sstore_metadata["current_value"] = initial_value + + # If not writing a new value, the new value is the same as the current one + # If writing a new value, the new value is current value + 1 + sstore_metadata["new_value"] = ( + initial_value if not write_new_value else initial_value + 1 + ) + setup = ( - Op.PUSH0 # [0] - + Op.SLOAD # [key], s[0] = key - + Op.DUP1 # [key, key] + Op.CALLDATALOAD(32) # [end_slot] + + Op.CALLDATALOAD(0) # [counter, end_slot] ) - if write_new_value: - setup += ( - Op.PUSH1(1) # [1, key, key] - + Op.ADD # [key+1, key] - + Op.SWAP1 # [key, key+1] - ) + # stack element: [counter, end_slot] - # After setup phase, the stack element represents - # [slot, value], slot to write and value to write + loop = Bytecode() + loop += Op.JUMPDEST # jump target - cache_op = Bytecode() + # If CACHE_TX, warm the slot with a cold SLOAD before the SSTORE loop if cache_strategy == CacheStrategy.CACHE_TX: - cache_op = ( - Op.DUP1 # [slot, slot, value] - + Op.SLOAD # [s[slot], slot, value] - + Op.POP # [slot, value] + loop += Op.POP(Op.SLOAD(Op.DUP1, key_warm=False)) + + sstore_op: Bytecode = Bytecode() + if write_new_value: + # s[counter] = counter + 1 + sstore_op = ( + Op.DUP1 # [counter, counter, end_slot] + + Op.DUP1 # [counter, counter, counter, end_slot] + + Op.PUSH1(1) # [1, counter, counter, counter, end_slot] + + Op.ADD # [counter+1, counter, counter, end_slot] + + Op.SWAP1 # [counter, counter+1, counter, end_slot] + + Op.SSTORE(**sstore_metadata) # [counter, end_slot] + ) + else: + # s[counter] = counter (existing slot) or 0 (non existing slot) + push_value = Op.DUP1 if existing_slots else Op.PUSH1(0) + sstore_op = ( + push_value # [value, counter, end_slot] + + Op.DUP2 # [counter, value, counter, end_slot] + + Op.SSTORE(**sstore_metadata) # [counter, end_slot] ) - # The cache mechanism touches the slot before SSTORE + loop += sstore_op - runtime_code = ( - setup - + While( - body=( - cache_op # [slot, value] - + Op.DUP2 # [value, slot, value] - + Op.DUP2 # [slot, value, slot, value] - + Op.SSTORE # [slot, value], s[slot] = value - + Op.PUSH1(1) # [1, slot, value] - + Op.ADD # [slot+1, value] - + Op.SWAP1 # [value, slot+1] - + Op.PUSH1(1) # [1, value, slot+1] - + Op.ADD # [value+1, slot+1] - + Op.SWAP1 # [slot+1, value+1] - ), - condition=Op.GT(Op.GAS, 0xFFFF), - ) - + Op.PUSH0 # [0, slot+1, value+1] - + Op.SSTORE # s[0] = slot+1 + # stack element: [counter, end_slot] + + loop += ( + Op.PUSH1(1) # [1, counter, end_slot] + + Op.ADD # [counter+1, end_slot] + + Op.DUP2 # [end_slot, counter+1, end_slot] + + Op.DUP2 # [counter+1, end_slot, counter+1, end_slot] + + Op.LT # [counter+1 bytes: + return Hash(start_iteration) + Hash(start_iteration + iteration_count) + + def tx_generator(sender: EOA) -> list[Transaction]: + return list( + runtime_code.transactions_by_gas_limit( + fork=fork, + gas_limit=gas_benchmark_value, + sender=sender, + to=authority, + start_iteration=start_slot, + calldata=calldata_gen, + ) + ) + run_bloated_eoa_benchmark( benchmark_test=benchmark_test, pre=pre, fork=fork, gas_benchmark_value=gas_benchmark_value, tx_gas_limit=tx_gas_limit, - authority=pre.stub_eoa(token_name), + authority=authority, existing_slots=existing_slots, runtime_code=runtime_code, cache_strategy=cache_strategy, + tx_generator=tx_generator, ) From 1d0b847047babd74446a5c53e704e7335f10c48c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E4=BD=B3=E8=AA=A0=20Louis=20Tsai?= <72684086+LouisTsai-Csie@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:49:22 +0200 Subject: [PATCH 2/5] refactor(test-benchmark): increase auth tx gas limit for storage benchmark (#2771) * refactor: failing 8037 benchmark cases * refactor: auth tx limit * fix: linitng issue * chore: ignore local EIP-8037 notes in `markdownlint-cli2` Add `.markdownlint-cli2.yaml` with an `ignores` list for the `EIP8037_REMAINING_FAILURES.md`, `EIP8037_PORTED_STATIC_FAILURES.md`, and `EIP8037_IMPLEMENTATION.md` working notes at the repo root. * refactor: deploy contract gas limit accounting * fix: gas accounting * feat: add stub for new snapshot --------- Co-authored-by: danceratopz --- .markdownlint-cli2.yaml | 4 ++ .../plugins/execute/pre_alloc.py | 37 ++++++++++++++----- .../stateful/bloatnet/test_single_opcode.py | 25 +++++++++++-- .../stateful/stubs/stubs_jochemnet.json | 6 +++ 4 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 .markdownlint-cli2.yaml create mode 100644 tests/benchmark/stateful/stubs/stubs_jochemnet.json diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml new file mode 100644 index 00000000000..73f4e0a9de0 --- /dev/null +++ b/.markdownlint-cli2.yaml @@ -0,0 +1,4 @@ +ignores: + - EIP8037_REMAINING_FAILURES.md + - EIP8037_PORTED_STATIC_FAILURES.md + - EIP8037_IMPLEMENTATION.md diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py index 7a7d5b81660..2027bdc0b43 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py @@ -595,6 +595,11 @@ def _fund_eoa( # Send a transaction to fund the EOA fund_tx: PendingTransaction | None = None if delegation is not None or storage is not None: + fork = self._fork.fork_at( + block_number=self._block_number, timestamp=self._timestamp + ) + intrinsic_calc = fork.transaction_intrinsic_cost_calculator() + if storage is not None: if not isinstance(storage, Storage): storage = Storage.model_validate(storage) @@ -602,14 +607,24 @@ def _fund_eoa( f"Deploying storage contract for EOA {eoa} " f"with {len(storage)} storage slots" ) - sstore_address = self.deploy_contract( - code=( - sum( - Op.SSTORE(key, value) - for key, value in storage.items() + + storage_init_code = ( + sum( + Op.SSTORE( + key, + value, + # gas accounting + key_warm=False, + original_value=0, + current_value=0, + new_value=1, ) - + Op.STOP + for key, value in storage.items() ) + + Op.STOP + ) + sstore_address = self.deploy_contract( + code=storage_init_code, ) logger.debug( f"Storage contract deployed at {sstore_address} " @@ -629,7 +644,11 @@ def _fund_eoa( signer=eoa, ), ], - gas_limit=100_000, + gas_limit=( + intrinsic_calc(authorization_list_or_count=1) + + storage_init_code.gas_cost(fork) + + 500_000 + ), ) eoa.nonce = Number(eoa.nonce + 1) @@ -654,7 +673,7 @@ def _fund_eoa( signer=eoa, ), ], - gas_limit=100_000, + gas_limit=(intrinsic_calc(authorization_list_or_count=1)), ) eoa.nonce = Number(eoa.nonce + 1) else: @@ -672,7 +691,7 @@ def _fund_eoa( signer=eoa, ), ], - gas_limit=100_000, + gas_limit=intrinsic_calc(authorization_list_or_count=1), ) eoa.nonce = Number(eoa.nonce + 1) diff --git a/tests/benchmark/stateful/bloatnet/test_single_opcode.py b/tests/benchmark/stateful/bloatnet/test_single_opcode.py index 1403c95b704..46abd037841 100644 --- a/tests/benchmark/stateful/bloatnet/test_single_opcode.py +++ b/tests/benchmark/stateful/bloatnet/test_single_opcode.py @@ -93,6 +93,7 @@ def _sender_generator( def delegate_with_calldata( pre: Alloc, + fork: Fork, authority: EOA, address: Address, calldata: Hash, @@ -103,8 +104,12 @@ def delegate_with_calldata( The delegated code determines what happens with the calldata. The authority nonce is incremented in-place. """ + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=bytes(calldata), + authorization_list_or_count=1, + ) tx = Transaction( - gas_limit=100_000, + gas_limit=intrinsic_gas, to=authority, value=0, data=calldata, @@ -144,10 +149,18 @@ def run_bloated_eoa_benchmark( runtime_address = pre.deploy_contract(code=runtime_code) init_tx = delegate_with_calldata( - pre, authority, setter_address, slot_0_value + pre, + fork, + authority, + setter_address, + slot_0_value, ) runtime_tx = delegate_with_calldata( - pre, authority, runtime_address, Hash(0) + pre, + fork, + authority, + runtime_address, + Hash(0), ) blocks: list[Block] = [Block(txs=[init_tx, runtime_tx])] @@ -311,7 +324,11 @@ def test_sload_bloated_prefetch_miss( # forcing the prefetcher's pre-block snapshot to disagree with # the actual slot 0 value seen by every max-gas tx that follows. delegation_tx = delegate_with_calldata( - pre, authority, runtime_address, Hash(0) + pre, + fork, + authority, + runtime_address, + Hash(0), ) blocks: list[Block] = [Block(txs=[delegation_tx])] diff --git a/tests/benchmark/stateful/stubs/stubs_jochemnet.json b/tests/benchmark/stateful/stubs/stubs_jochemnet.json new file mode 100644 index 00000000000..a52d9d9b7de --- /dev/null +++ b/tests/benchmark/stateful/stubs/stubs_jochemnet.json @@ -0,0 +1,6 @@ +{ + "bloated_eoa_10GB": { + "addr": "0x87a6314da5ac8832f6e7a176c8fb133b19f5be04", + "pkey": "0x4da32d29f6dcffa26e09dc4e102033f2d105de1444fb893493ae703289275e0e" + } +} From c196ab2848be5d35d95c1abebb0dc25577d55677 Mon Sep 17 00:00:00 2001 From: CPerezz <37264926+CPerezz@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:09:33 +0200 Subject: [PATCH 3/5] fix(test-benchmark): disable cache strats except NO_CACHE for now (#2786) --- tests/benchmark/stateful/bloatnet/test_single_opcode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/benchmark/stateful/bloatnet/test_single_opcode.py b/tests/benchmark/stateful/bloatnet/test_single_opcode.py index 46abd037841..447a80b5e26 100644 --- a/tests/benchmark/stateful/bloatnet/test_single_opcode.py +++ b/tests/benchmark/stateful/bloatnet/test_single_opcode.py @@ -212,7 +212,7 @@ def run_bloated_eoa_benchmark( @pytest.mark.repricing @pytest.mark.stub_parametrize("token_name", "bloated_eoa_") @pytest.mark.parametrize("existing_slots", [False, True]) -@pytest.mark.parametrize("cache_strategy", list(CacheStrategy)) +@pytest.mark.parametrize("cache_strategy", [CacheStrategy.NO_CACHE]) def test_sload_bloated( benchmark_test: BenchmarkTestFiller, pre: Alloc, @@ -597,7 +597,7 @@ def test_sload_bloated_multi_contract( @pytest.mark.stub_parametrize("token_name", "bloated_eoa_") @pytest.mark.parametrize("write_new_value", [False, True]) @pytest.mark.parametrize("existing_slots", [True, False]) -@pytest.mark.parametrize("cache_strategy", list(CacheStrategy)) +@pytest.mark.parametrize("cache_strategy", [CacheStrategy.NO_CACHE]) def test_sstore_bloated( benchmark_test: BenchmarkTestFiller, pre: Alloc, @@ -1575,7 +1575,7 @@ class AccountMode(Enum): @pytest.mark.repricing -@pytest.mark.parametrize("cache_strategy", list(CacheStrategy)) +@pytest.mark.parametrize("cache_strategy", [CacheStrategy.NO_CACHE]) @pytest.mark.parametrize( "opcode,value_sent,account_mode", account_access_params() ) From 23eaeca6bc24202c2f7c02d63173d965bc6b0837 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Tue, 12 May 2026 20:28:56 +0800 Subject: [PATCH 4/5] chore: bump gas budget --- tests/benchmark/stateful/bloatnet/test_single_opcode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/benchmark/stateful/bloatnet/test_single_opcode.py b/tests/benchmark/stateful/bloatnet/test_single_opcode.py index 447a80b5e26..a80756be67f 100644 --- a/tests/benchmark/stateful/bloatnet/test_single_opcode.py +++ b/tests/benchmark/stateful/bloatnet/test_single_opcode.py @@ -108,8 +108,9 @@ def delegate_with_calldata( calldata=bytes(calldata), authorization_list_or_count=1, ) + gas_limit = intrinsic_gas + 500_000 tx = Transaction( - gas_limit=intrinsic_gas, + gas_limit=gas_limit, to=authority, value=0, data=calldata, From 0d9eebf0893b23f74c56953820f2f3c53151728f Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Thu, 14 May 2026 10:20:39 +0200 Subject: [PATCH 5/5] feat(test-benchmark): add eth transfer cases for repricing (#2837) * (Claude): add eth transfer cases for repricing * refactor: split test based on pre-alloc * refactor: bump execution cost for contract ether reception * chore: remove unnecessary parametrization * refactor: move new benchmark under stateful folder * fix: apply suggested changes * feat: add receipt check * (Claude): add distinct senders * (Claude): do not limit distinct senders * (Claude): add uniq jumpdest contract test * refactor unique contract code receiver case --------- Co-authored-by: LouisTsai --- .../bloatnet/test_transaction_types.py | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 tests/benchmark/stateful/bloatnet/test_transaction_types.py diff --git a/tests/benchmark/stateful/bloatnet/test_transaction_types.py b/tests/benchmark/stateful/bloatnet/test_transaction_types.py new file mode 100644 index 00000000000..875c69fecdf --- /dev/null +++ b/tests/benchmark/stateful/bloatnet/test_transaction_types.py @@ -0,0 +1,207 @@ +"""Benchmark ether transfers to receivers that exist on-chain.""" + +import itertools +from typing import Generator + +import pytest +from execution_testing import ( + DETERMINISTIC_FACTORY_ADDRESS, + EOA, + Address, + Alloc, + BenchmarkTestFiller, + Block, + Fork, + Op, + Transaction, + compute_create2_address, + compute_create_address, +) + +# Deterministic sender pool of 15K accounts. +# Funded via system contract withdrawals (funding.txt) in payload generation. +# Placed outside pre-allocation to ensure accounts remain uncached. +SENDER_BASE_KEY = ( + 0x1111111111111111111111111111111111111111111111111111111111111111 +) + + +def yield_distinct_sender() -> Generator[EOA, None, None]: + """Yield deterministic sender EOAs pre-funded on-chain.""" + for i in itertools.count(0): + yield EOA(key=SENDER_BASE_KEY + i) + + +def build_unique_contract_initcode() -> bytes: + """ + Deployed runtime contract layout. + + offset size contents + ------ ---- -------------------------------- + 0x0000 4 PUSH2 0x5FFF; JUMP <- entry + 0x0004 28 JUMPDEST padding + 0x0020 12 JUMPDEST padding + 0x002C 20 contract ADDRESS <- unique + 0x0040 24512 JUMPDEST <- 0x5FFF lands here + 0x6000 STOP + + Embedded ADDRESS makes runtime unique per contract; + initcode and its CREATE2 hash is shared across all salts. + """ + max_code_size = 0x6000 # EIP-170 contract code size limit + + # MCOPY fills MEM[0:0x8000] with JUMPDEST. + # Runtime only uses MEM[0:0x6000]. + code = Op.MSTORE(0, bytes(Op.JUMPDEST * 32)) + for size in (1 << s for s in range(5, 15)): + code += Op.MCOPY(size, 0, size) + + # Runtime entry: JUMP to final JUMPDEST, then STOP. + entry = Op.JUMP(max_code_size - 1) + entry += Op.JUMPDEST * (32 - len(entry)) # Padding + + code += Op.MSTORE(0, bytes(entry)) + + # Mask ADDRESS into a JUMPDEST template via OR: + # bytes 0..12 bytes 12..32 + # ----------- ------------ + # ADDRESS 00 .. 00 <20-byte address> + # addr_slot 5b .. 5b 00 .. 00 + # OR result 5b .. 5b <20-byte address> + addr_slot = Op.JUMPDEST * 12 + Op.STOP * 20 + code += Op.MSTORE(0x20, Op.OR(Op.ADDRESS, bytes(addr_slot))) + + code += Op.RETURN(0, max_code_size) + + return bytes(code) + + +JOCHEMNET_UNIQUE_CONTRACT_INITCODE = build_unique_contract_initcode() + + +def yield_distinct_unique_code_jumpdest_receiver() -> Generator[ + Address, None, None +]: + """ + Yield contract addresses deployed by the deterministic CREATE2 factory. + """ + for salt in itertools.count(0): + yield compute_create2_address( + address=DETERMINISTIC_FACTORY_ADDRESS, + salt=salt, + initcode=JOCHEMNET_UNIQUE_CONTRACT_INITCODE, + ) + + +# Bittrex controller mainnet address +# Creates 1.5M contracts with deterministic address via CREATE +# It is guaranteed no contract is destructed +# Used for existing contract targets in benchmark +BITTREX_CONTROLLER_ADDRESS = Address( + 0xA3C1E324CA1CE40DB73ED6026C4A177F099B5770 +) + + +def yield_distinct_contract_receiver() -> Generator[Address, None, None]: + """Yield contract account created by Bittrex controller via CREATE.""" + for nonce in itertools.count(2): + yield compute_create_address( + address=BITTREX_CONTROLLER_ADDRESS, nonce=nonce + ) + + +def yield_distinct_existent_receiver() -> Generator[Address, None, None]: + """ + Yield existing balance-only EOA on bloatnet. pre-funded by Spamoor + (https://github.com/CPerezz/spamoor/pull/12). + """ + for address in itertools.count(0x1000): + yield Address(address) + + +def yield_distinct_nonexistent_receiver() -> Generator[Address, None, None]: + """Yield non-existent accounts starting from keccak256('random').""" + for address in itertools.count(0xF3CF193BB4AF1022AF7D2089F37D8BAE7157B85F): + yield Address(address) + + +@pytest.mark.repricing +@pytest.mark.parametrize( + "case_id", + [ + "diff_to_nonexistent", + "diff_to_existent", + "diff_to_contract", + "diff_to_unique_code_jumpdest_contract", + ], +) +@pytest.mark.parametrize("transfer_amount", [0, 1]) +def test_ether_transfers_onchain_receivers( + benchmark_test: BenchmarkTestFiller, + pre: Alloc, + case_id: str, + transfer_amount: int, + fork: Fork, + gas_benchmark_value: int, +) -> None: + """ + Ether transfers to receivers that exist on-chain at run time. + + Scenarios: + - diff_to_nonexistent: distinct nonexistent receivers + (matches AccountMode.NON_EXISTING_ACCOUNT) + - diff_to_existent: distinct existent EOA receivers + (matches AccountMode.EXISTING_EOA) + - diff_to_contract: distinct contract receivers + (matches AccountMode.EXISTING_CONTRACT) + - diff_to_unique_code_jumpdest_contract: distinct CREATE2 contract + receivers each holding unique deployed code + """ + senders = yield_distinct_sender() + receiver_execution_gas = 0 + if case_id == "diff_to_nonexistent": + receivers = yield_distinct_nonexistent_receiver() + elif case_id == "diff_to_existent": + receivers = yield_distinct_existent_receiver() + elif case_id == "diff_to_contract": + receivers = yield_distinct_contract_receiver() + # Runtime code is the same across all the receivers + # Example contract: https://etherscan.io/address/0xa888df3ef62286dde06a79395760b9bce6c83c83#code + runtime = ( + Op.MSTORE(0x40, 0x60, new_memory_size=0x60) + + Op.JUMPI(Op.PUSH2(0x49), Op.ISZERO(Op.CALLDATASIZE)) + + Op.JUMPDEST * 3 + + Op.JUMP(Op.PUSH2(0x50)) + + Op.JUMPDEST + ) + receiver_execution_gas = runtime.gas_cost(fork) + elif case_id == "diff_to_unique_code_jumpdest_contract": + receivers = yield_distinct_unique_code_jumpdest_receiver() + # Runtime code aligns entry code path. + runtime = Op.JUMP(Op.PUSH2(0x5FFF)) + Op.JUMPDEST + receiver_execution_gas = runtime.gas_cost(fork) + else: + raise ValueError(f"Unknown case: {case_id}") + + iteration_cost = ( + fork.transaction_intrinsic_cost_calculator()() + receiver_execution_gas + ) + iteration_count = gas_benchmark_value // iteration_cost + + txs = [ + Transaction( + to=next(receivers), + value=transfer_amount, + gas_limit=iteration_cost, + sender=next(senders), + ) + for _ in range(iteration_count) + ] + + benchmark_test( + pre=pre, + post={}, + blocks=[Block(txs=txs)], + expected_benchmark_gas_used=iteration_count * iteration_cost, + expected_receipt_status=1, + )