diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index 17ac97dce5..275888ff0b 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -9,16 +9,11 @@ develop: benchmark: evm-type: benchmark - fill-params: --no-html --fork=Prague --gas-benchmark-values 1,5,10,30,60,100,150 -m benchmark ./tests/benchmark - -benchmark_develop: - evm-type: benchmark - fill-params: --no-html --fork=Osaka --gas-benchmark-values 1,5,10,30,60,100,150 -m "benchmark" ./tests/benchmark - feature_only: true + fill-params: --no-html --fork=Osaka --gas-benchmark-values 1,5,10,30,60,100,150 -m benchmark ./tests/benchmark benchmark_fast: evm-type: benchmark - fill-params: --no-html --fork=Prague --gas-benchmark-values 100 -m "benchmark" ./tests/benchmark + fill-params: --no-html --fork=Osaka --gas-benchmark-values 100 -m "benchmark" ./tests/benchmark feature_only: true bal: diff --git a/docs/writing_tests/benchmarks.md b/docs/writing_tests/benchmarks.md index 818802b18f..ae8476c766 100644 --- a/docs/writing_tests/benchmarks.md +++ b/docs/writing_tests/benchmarks.md @@ -1,16 +1,85 @@ -# Benchmark Test Cases +# Benchmark Tests -Benchmark tests aim to maximize the usage of a specific opcode, precompile, or operation within a transaction or block. They are located in the `./tests/benchmarks` folder and the available test cases are documented in [test case reference](../tests/benchmark/index.md). +The EELS benchmark serves as a centralized hub for benchmarking test cases, evaluating execution layer performance across a wide range of scenarios, including gas limit testing, zkEVM, Bloatnet, gas repricing and EIPs that introduce new opcodes, precompiles, transaction types, or more use cases. -To fill a benchmark test, in addition to the usual test flags, you must include the `-m benchmark` flag. This is necessary because benchmark tests are ignored by default; they must be manually selected via the `benchmark` pytest marker (="tag"). This marker is applied to all tests under `./tests/benchmark/` automatically by the framework. +All benchmark tests are maintained under the `./tests/benchmark` directory. The benchmark suite is further organized based on whether tests require a pre-configured, stateful environment. + +## Directory Structure + +The benchmark suite is organized as follows: + +```text +tests/benchmark/ +├── compute/ +│ ├── instruction/ # Individual EVM opcodes +│ ├── precompile/ # EVM precompiles +│ └── scenario/ # Mix of operations, transaction types, etc. +└── stateful/ # Pre-configured state environments required +``` + +There are multiple files under `instruction/`, users can check each file's docstring to understand which opcodes are covered in the file. + +### Stateful Benchmarks + +A subset of benchmark test cases run on top of stateful environments (such as bloatnet or mainnet-like setups), in order to analyze how state size, structure, and access patterns influence performance. These tests may (1) pre-deploy contracts (2) construct initial storage state (3) Interact with pre-deployed contracts via stub addresses. + +Such tests are located under `./tests/benchmark/stateful`. When running these tests, users should specify the `stateful` flag as `-m stateful`, or the test would be ignored, even the path is specified correctly. + +### Compute Benchmarks + +Other benchmark tests do not require any pre-state configuration. These benchmarks could be run even without pre-deployed contracts or initialized storage. + +These tests are located under `./tests/benchmark/compute.` When running these cases, users should specify the `benchmark` marker like `-m benchmark`, or the test would be ignored, even the path is specified correctly. + +**Note:** Using `-m benchmark` under `tests/benchmark/stateful`, or `-m stateful` under `tests/benchmark/compute`, will cause the tests to be ignored. Make sure the user-provided flag matches the directory of the test being executed. **Note:** Benchmark tests are now only available starting from the `Prague` fork. Tests targeting earlier forks (`Cancun` or prior) are not supported in benchmark mode. -## Setting the Gas Limit for Benchmarking +## Benchmark Modes + +### Fixed Opcode Count Mode + +In this mode, users either: + +- First generate an opcode-count configuration mapping file via `uv run benchmark_parser`, then run the benchmark test with the `--fixed-opcode-count` flag **without parameters**, or +- Specify the opcode count directly via a CLI flag (e.g., `--fixed-opcode-count N`) + +The benchmark test wrapper then constructs a test that executes approximately `N × 1000` opcode invocations during execution, allowing for up to ±5% deviation in the final opcode count. + +This mode is primarily used for gas repricing analysis, where it enables: + +- Controlled executed opcode/precompile counts. +- Measurement of execution time as a function of opcode count. +- Derivation of regression models between opcode frequency and execution time. + +**Note:** Flag ordering matters: if `--fixed-opcode-count` is followed immediately by another flag, that flag may be incorrectly interpreted as its parameter. + +### Worst-Case Mode + +In worst-case mode, users specify a target block gas limit instead of an opcode count. +By providing `--gas-benchmark-values N` (where N denotes the gas limit in millions), the benchmark construction process packs each block with as many instances as possible of the selected operation. + +This mode is designed for gas limit testing, and gas repricing, where it enables: + +- Evaluate execution-layer performance under extreme, worst-case conditions given certain operations. +- Identify bottlenecks that may only surface at high gas utilization levels + +**Note:** For both benchmark modes, users may supply multiple values in a single invocation. For example: + +- `--gas-benchmark-values 1,2,3` runs the test with 1M, 2M, and 3M block gas limits +- `--fixed-opcode-count 4,5` runs the test with approximately 4K and 5K opcode executions + +## Developing Benchmarks + +Before writing benchmark-specific tests, please refer to the [general documentation](./writing_a_new_test.md) for the fundamentals of writing tests in the EELS framework. + +### Environment Variables + +#### Accessing the Block Gas Limit -To consume the full benchmark gas limit, use the `gas_benchmark_value` fixture as the gas limit: +When using `--gas-benchmark-values`, do not read the block gas limit from `env.gas_limit`. Instead, tests consume the injected `gas_benchmark_value` parameter, which reflects the current benchmark iteration block gas limit value. -```py +```python def test_benchmark( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -19,11 +88,11 @@ def test_benchmark( ... ``` -You can specify the block gas limit used in benchmark tests by setting the `--gas-benchmark-values` flag. This flag accepts a comma-separated list of values (in millions of gas), e.g. `--gas-benchmark-values 1,10,45,60`. This example would run the test 4 times, using a `gas_benchmark_value` of 1M, 10M, 45M, and 60M respectively. +For example, running the test with `--gas-benchmark-values 1,10,45,60`. will execute the test 4 times, passing `gas_benchmark_value` as 1M, 10M, 45M, and 60M respectively. -Do not configure the transaction/block gas limit to `env.gas_limit`. When running in benchmark mode, the test framework sets this value to a very large number (e.g., `1_000_000_000_000`), this setup allows the framework to reuse a single genesis file for all specified gas limits. I.e., the example below is invalid: +Never configure the transaction / block gas limit to `env.gas_limit`. When running in benchmark mode, the test framework sets this value to a very large number (e.g., `1_000_000_000_000`), this setup allows the framework to reuse a single genesis file for all specified gas limits. I.e., the example below should be avoided: -```py +```python def test_benchmark( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -38,11 +107,214 @@ def test_benchmark( ... ``` -## Expected Gas Usage +#### Referencing Transaction Gas Limit + +Since the Osaka fork, EIP-7825 introduces a transaction gas limit cap (approximately 16M). Instead of hardcoding this value in the test, use `fork.transaction_gas_limit_cap()` for a cleaner, fork-aware approach. + +This helper fixture could simplify the logic of determine the transaction gas limit cap, it returns the value if available, otherwise falls back to the block gas limit: + +```python +@pytest.fixture +def tx_gas_limit(fork: Fork, gas_benchmark_value: int) -> int: + """Return the transaction gas limit cap, or block gas limit if not available.""" + return fork.transaction_gas_limit_cap() or gas_benchmark_value +``` + +Example usage: import `tx_gas_limit` to calculate how many transactions fit in the block: + +```python +def test_benchmark( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + gas_benchmark_value: int, + tx_gas_limit: int +): + ... + num_of_full_tx_num = gas_benchmark_value // tx_gas_limit + gas_for_last_tx = gas_benchmark_value % tx_gas_limit + ... +``` + +#### Specifying Execution Semantics + +When constructing benchmark tests with multiple transactions or blocks, identify which transaction is the actual benchmark transaction becomes difficult. `TestPhaseManager` is used to label transactions as either setup or execution phases. + +```python +def test_complex_benchmark( + benchmark_test: BenchmarkTestFiller, + pre: Alloc, +) -> None: + # Setup phase + with TestPhaseManager.setup(): + setup_tx = Transaction(...) + + # Execution phase + with TestPhaseManager.execution(): + exec_tx = Transaction(...) + + benchmark_test( + blocks=[Block(txs=[setup_tx]), Block(txs=[exec_tx])], + expected_benchmark_gas_used=..., + ) +``` + +Import `TestPhaseManager` and use it to annotate each transaction or block with its corresponding phase. During analysis, filters transactions by metadata, excluding setup transactions and measuring only the actual benchmark transaction. + +### BenchmarkTest Wrapper + +Within the EELS framework, tests can be written using existing fixtures such as `BlockchainTest` and `StateTest`. However, for benchmark scenarios, we strongly recommend using the `BenchmarkTest` wrapper, which encapsulates repetitive logic commonly required in benchmark test construction. + +Note that `BenchmarkTest` is a wrapper, not a new fixture type. It does not introduce a new fixture format, and therefore clients do not need to add special support for it. Internally, `BenchmarkTest` accepts user-provided parameters and converts them into the corresponding `BlockchainTest` representation. + +#### Mode 1: Using Custom Blocks + +This mode is suitable for complex scenarios that require multiple transactions, where each transaction's logic is completely different. + +```python +def test_complex_benchmark( + benchmark_test: BenchmarkTestFiller, + pre: Alloc, +) -> None: + ... + exec_tx_1 = Transaction(...) + exec_tx_2 = Transaction(...) + attack_block = Block(txs=[exec_tx_1, exec_tx_2]) + ... + benchmark_test( + blocks=[attack_block], + ) +``` + +#### Mode 2: Using a Single Transaction + +Users may also provide a single transaction directly. In this mode, the wrapper automatically generates multiple transactions to fully utilize the target block gas limit. + +For example, assume 60M block gas limit, and 16M transaction gas limit cap, the wrapper will construct 3 transactions with 16M gas limit and the final transaction with 12M gas limit + +```python +def test_simple_benchmark( + benchmark_test: BenchmarkTestFiller, + pre: Alloc, + gas_benchmark_value: int, +) -> None: + contract_address = pre.deploy_contract(code=Op.PUSH1(1) + Op.STOP) + benchmark_test( + tx=Transaction( + to=contract_address, + gas_limit=gas_benchmark_value, + sender=pre.fund_eoa(), + ), + ) +``` + +#### Mode 3: Using a Code Generator (Recommended) + +This mode allows users to provide a code generator that emits execution payloads dynamically. It is the recommended approach for most benchmark use cases, as it offers the greatest flexibility and reuse. + +Currently, EELS provides two built-in code generators, `JumpLoopGenerator` and `ExtCallGenerator`. Both generators accept the following components to construct the benchmark contracts: -In benchmark mode, the developer should set the expected gas consumption using the `expected_benchmark_gas_used` field. Benchmark tests do not need to consume the full gas limit, instead, you could calculate and specify the expected usage. If `expected_benchmark_gas_used` is not set, the test will fall back to using `gas_benchmark_value` as the expected value. +- `setup`: Code executed once before the attack loop +- `attack_block`: The core operation to be benchmarked +- `cleanup`: Optional cleanup logic executed after benchmarking + +In addition, users may customize transaction and contract construction via: + +- `tx_kwargs`: Transaction-level parameters (e.g., calldata, blob fields) +- `code_padding_opcode`: If specified, the contract bytecode will be padded with the given opcode up to the maximum contract size + +##### JumpLoopGenerator + +`JumpLoopGenerator` maximizes the number of `attack_block` repetitions within a single contract by looping via `JUMP`. The benchmark construction repeats the `attack_block` as many times as possible. + +```python +target_contract = ( + setup + + JUMPDEST + + attack_block + + ... + + attack_block + + cleanup + + JUMP(len(setup)) +) +``` + +This generator is suitable when the benchmarked operation does **not** grow the EVM stack unboundedly, or when stack growth is explicitly managed (e.g., by pairing stack-producing opcodes with `POP`). + +##### ExtCallGenerator + +`ExtCallGenerator` constructs two contracts: (1) a target contract, which contains the benchmarked logic and (2) a loop contract, which repeatedly calls into the target contract + +In this design, The `attack_block` inside the target contract is repeated 1024 times, corresponding to the EVM maximum stack size. And the loop contract repeatedly invokes the target contract to amplify execution via `STATICCALL`. + +The contract structures are as follows: + +Target contract: + +```python +target_contract = ( + setup + + attack_block + + (repeat another 1022 times) + + attack_block + + cleanup # usually empty +) +``` -```py +Loop contract: + +```python +attack_block = pop(staticcall(addr=target_contract, argsize=CALLDATASIZE)) + +loop_contract = ( + CALLDATACOPY(size=CALLDATASIZE) + + JUMPDEST + + attack_block + + ... + + attack_block + + cleanup + + JUMP(len(setup)) +) +``` + +`CALLDATACOPY` is required in loop contract since some target operation requires access to calldata, while the calldata is supplied in the transaction object. As a result, the loop contract must explicitly forward the calldata to the target contract. The calldata is first copied from transaction to the memory via `CALLDATACOPY` and pass to the target contract via `STATICCALL`. + +##### Choosing Between Generators + +`ExtCallGenerator` is particularly useful when benchmarking stack-growing opcodes (i.e., opcodes that push values onto the stack). + +For example: + +- When benchmarking `CALLDATASIZE` using `JumpLoopGenerator`, the `attack_block` must be written as `POP(CALLDATASIZE)` to avoid stack overflow +- With `ExtCallGenerator`, this restriction does not apply, as target contract execution naturally stops at the maximum stack size + +Based on experimental results, `ExtCallGenerator` is often more optimized than `JumpLoopGenerator`, as it requires fewer glue opcodes in the benchmarked execution path. + +**Note:** Users must provide exactly one parameter, either `tx`, `block` or `code_generator` to `BenchmarkTest`, more than one of these inputs simultaneously is not allowed. + +##### Fixed Opcode Count Test Construction + +The fixed-opcode-count feature is currently limited to benchmark tests that: + +1. Use the `BenchmarkTest` wrapper, and +2. Use a code generator (`JumpLoopGenerator` or `ExtCallGenerator`) + +If one of the condition is not met, the benchmark does not support fixed-opcode-count mode and the test will be ignored during the test-selection phase. + +As a result, the test construction logic for fixed-opcode-count mode **differs** from the general code-generation behavior described above. + +In fixed-opcode-count mode, both `ExtCallGenerator` and `JumpLoopGenerator` always constructs target contract and loop contract. The target contract executes the `attack_block` exactly 1000 times, while the loop contract repeatedly calls into the target contract N times. + +As a result, the total opcode execution count is `1000 * N`, which matches the semantics of the `--fixed-opcode-count N` flag. + +## Validating Benchmarks + +### Setting Expected Gas Usage + +In benchmark mode, set the expected gas consumption using the `expected_benchmark_gas_used` field, if the test do not need to consume the full gas limit. Developer could calculate and specify the expected usage. If `expected_benchmark_gas_used` is not available, the setting will fall back to using `gas_benchmark_value` as the expected value. + +This feature is primarily used in `worst-case` benchmark mode. + +```python @pytest.mark.valid_from("Prague") def test_empty_block( blockchain_test: BlockchainTestFiller, @@ -62,3 +334,20 @@ This is a safety check to make sure the benchmark works as expected. For example This check helps catch such issues. As a result, the post-storage comparison method via `SSTORE` is no longer needed, thereby reducing the additional storage cost. However, in cases where it is difficult to determine the total gas usage, or if an alternative verification method is used, developers may set `skip_gas_used_validation` to `True` to disable the gas usage check. + +### Setting Target Operation + +For `fixed-opcode-count` mode, specify which opcode to target using the `target_opcode` parameter. The benchmark will compare the opcode count of `target_opcode` from test execution to the expected count for verification. + +```python +def test_jumpdests( + benchmark_test: BenchmarkTestFiller, +) -> None: + """Benchmark JUMPDEST instruction.""" + benchmark_test( + target_opcode=Op.JUMPDEST, + code_generator=JumpLoopGenerator(attack_block=Op.JUMPDEST), + ) +``` + +**Note:** This verification currently only works in `fill` mode, not in `execute-remote` mode. diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/helpers/ruleset.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/helpers/ruleset.py index 87d3783ce9..b14c9427c5 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/helpers/ruleset.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/helpers/ruleset.py @@ -16,6 +16,7 @@ Berlin, BerlinToLondonAt5, BPO1ToBPO2AtTime15k, + BPO2ToAmsterdamAtTime15k, BPO2ToBPO3AtTime15k, BPO3ToBPO4AtTime15k, Byzantium, @@ -480,6 +481,26 @@ def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]: "HIVE_BPO4_TIMESTAMP": 15000, **get_blob_schedule_entries(BPO4), }, + BPO2ToAmsterdamAtTime15k: { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 0, + "HIVE_FORK_MERGE": 0, + "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0, + "HIVE_SHANGHAI_TIMESTAMP": 0, + "HIVE_CANCUN_TIMESTAMP": 0, + "HIVE_PRAGUE_TIMESTAMP": 0, + "HIVE_OSAKA_TIMESTAMP": 0, + "HIVE_BPO1_TIMESTAMP": 0, + "HIVE_BPO2_TIMESTAMP": 0, + "HIVE_AMSTERDAM_TIMESTAMP": 15000, + **get_blob_schedule_entries(BPO2), + }, Amsterdam: { "HIVE_FORK_HOMESTEAD": 0, "HIVE_FORK_TANGERINE": 0, @@ -504,6 +525,6 @@ def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]: # "HIVE_BPO3_TIMESTAMP": 0, # "HIVE_BPO4_TIMESTAMP": 0, "HIVE_AMSTERDAM_TIMESTAMP": 0, - **get_blob_schedule_entries(Amsterdam), + **get_blob_schedule_entries(BPO2), }, } diff --git a/packages/testing/src/execution_testing/forks/__init__.py b/packages/testing/src/execution_testing/forks/__init__.py index 760dbd0677..efd39010c4 100644 --- a/packages/testing/src/execution_testing/forks/__init__.py +++ b/packages/testing/src/execution_testing/forks/__init__.py @@ -30,6 +30,7 @@ from .forks.transition import ( BerlinToLondonAt5, BPO1ToBPO2AtTime15k, + BPO2ToAmsterdamAtTime15k, BPO2ToBPO3AtTime15k, BPO3ToBPO4AtTime15k, CancunToPragueAtTime15k, @@ -117,6 +118,7 @@ "BPO1ToBPO2AtTime15k", "BPO2", "BPO2ToBPO3AtTime15k", + "BPO2ToAmsterdamAtTime15k", "BPO3", "BPO3ToBPO4AtTime15k", "BPO4", diff --git a/tests/benchmark/compute/instruction/test_storage.py b/tests/benchmark/compute/instruction/test_storage.py index aa1c08725e..ef5bb02f6f 100644 --- a/tests/benchmark/compute/instruction/test_storage.py +++ b/tests/benchmark/compute/instruction/test_storage.py @@ -13,12 +13,13 @@ import pytest from execution_testing import ( Alloc, + AuthorizationTuple, BenchmarkTestFiller, Block, Bytecode, - Environment, ExtCallGenerator, Fork, + Hash, JumpLoopGenerator, Op, TestPhaseManager, @@ -75,12 +76,7 @@ def test_tstore( init_key = 42 setup = Op.PUSH1(init_key) - # If fixed_value is False, we use GAS as a cheap way of always - # storing a different value than the previous one. attack_block = Op.TSTORE(Op.DUP2, Op.GAS if not fixed_value else Op.DUP1) - - # If fixed_key is False, we mutate the key on every iteration of the - # big loop. cleanup = Op.POP + Op.GAS if not fixed_key else Bytecode() benchmark_test( @@ -91,13 +87,134 @@ def test_tstore( ) +def create_storage_initializer(fork: Fork) -> tuple[Bytecode, int, int]: + """ + Create a contract that initializes storage slots from calldata parameters. + + - CALLDATA[0..32] start slot (index) + - CALLDATA[32..64] slot count (num) + + storage[i] = i for i in [index, index + num). + + Returns: (bytecode, loop_cost, overhead) + """ + prefix = ( + Op.CALLDATALOAD(0) # [index] + + Op.DUP1 # [index, index] + + Op.CALLDATALOAD(32) # [index, index, num] + + Op.ADD # [index, index + num] + ) + + loop = ( + Op.JUMPDEST + + Op.PUSH1(1) # [index, index + num, 1] + + Op.SWAP1 # [index, 1, index + num] + + Op.SUB # [index, index + num - 1] + + Op.SSTORE( + Op.DUP1, + Op.DUP1, + key_warm=False, + original_value=0, + current_value=0, + new_value=1, + ) + + Op.JUMPI(len(prefix), Op.GT(Op.DUP2, Op.DUP2)) + ) + + return prefix + loop, loop.gas_cost(fork), prefix.gas_cost(fork) + + +def create_benchmark_executor( + storage_action: StorageAction, + absent_slots: bool, + tx_result: TransactionResult, + fork: Fork, +) -> tuple[Bytecode, int, int]: + """ + Create a contract that executes benchmark operations. + + - CALLDATA[0..32] start slot (index) + - CALLDATA[32..64] slot count (num) + + Returns: (bytecode, loop_cost, overhead) + """ + prefix = ( + Op.CALLDATALOAD(0) # [index] + + Op.CALLDATALOAD(32) # [index, num] + ) + + slot_calculation = ( + Op.DUP2 # [index, num, index] + + Op.DUP2 # [index, num, index, num] + + Op.ADD # [index, num, index + num] + + Op.PUSH1(1) # [index, num, index + num, 1] + + Op.SWAP1 # [index, num, 1, index + num] + + Op.SUB # [index, num, index + num - 1] + ) + + original = 0 if absent_slots else 1 + + # [index, num, index + num - 1] + match storage_action: + case StorageAction.READ: + operation = Op.POP(Op.SLOAD.with_metadata(key_warm=False)) + case StorageAction.WRITE_SAME_VALUE: + new_value = 1 if absent_slots else original + operation = ( + Op.SSTORE( + Op.DUP1, + Op.DUP1, + key_warm=False, + original_value=original, + current_value=original, + new_value=new_value, + ) + + Op.POP + ) + case StorageAction.WRITE_NEW_VALUE: + operation = Op.SSTORE( + Op.SWAP1, + Op.NOT(0), + key_warm=False, + original_value=original, + current_value=original, + new_value=2**256 - 1, + ) + + # [index, num] + loop_condition = ( + Op.PUSH1(1) # [index, num, 1] + + Op.SWAP1 # [index, 1, num] + + Op.SUB # [index, num - 1] + + Op.DUP1 # [index, num - 1, num - 1] + + Op.ISZERO # [index, num - 1 == 0] + + Op.ISZERO # [index, num - 1 != 0] + ) + + match tx_result: + case TransactionResult.REVERT: + suffix = Op.REVERT(0, 0) + case TransactionResult.OUT_OF_GAS: + suffix = Bytecode() + case _: + suffix = Op.STOP + + loop = ( + Op.JUMPDEST + + slot_calculation + + operation + + Op.JUMPI(len(prefix), loop_condition) + ) + code = prefix + loop + suffix + + return code, loop.gas_cost(fork), (prefix + suffix).gas_cost(fork) + + @pytest.mark.parametrize( "storage_action,tx_result", [ pytest.param( - StorageAction.READ, - TransactionResult.SUCCESS, - id="SSLOAD", + StorageAction.READ, TransactionResult.SUCCESS, id="SSLOAD" ), pytest.param( StorageAction.WRITE_SAME_VALUE, @@ -131,161 +248,201 @@ def test_tstore( ), ], ) -@pytest.mark.parametrize( - "absent_slots", - [ - True, - False, - ], -) +@pytest.mark.parametrize("absent_slots", [True, False]) def test_storage_access_cold( benchmark_test: BenchmarkTestFiller, pre: Alloc, fork: Fork, storage_action: StorageAction, absent_slots: bool, - env: Environment, gas_benchmark_value: int, + tx_gas_limit: int, tx_result: TransactionResult, ) -> None: """ - Benchmark cold storage slot accesses. + Benchmark cold storage slot accesses using EIP-7702 delegation. + + The authority EOA delegates to: + - StorageInitializer: storage[i] = i for each slot (absent_slots=False) + - BenchmarkExecutor: performs the benchmark operation (SLOAD/SSTORE) """ + intrinsic_calc = fork.transaction_intrinsic_cost_calculator() gas_costs = fork.gas_costs() - intrinsic_gas_cost_calc = fork.transaction_intrinsic_cost_calculator() - - loop_cost = gas_costs.G_COLD_SLOAD # All accesses are always cold - if storage_action == StorageAction.WRITE_NEW_VALUE: - if not absent_slots: - loop_cost += gas_costs.G_STORAGE_RESET - else: - loop_cost += gas_costs.G_STORAGE_SET - elif storage_action == StorageAction.WRITE_SAME_VALUE: - if absent_slots: - loop_cost += gas_costs.G_STORAGE_SET - else: - loop_cost += gas_costs.G_WARM_SLOAD - elif storage_action == StorageAction.READ: - loop_cost += 0 # Only G_COLD_SLOAD is charged - - # Contract code - execution_code_body = Bytecode() - if storage_action == StorageAction.WRITE_SAME_VALUE: - # All the storage slots in the contract are initialized to their index. - # That is, storage slot `i` is initialized to `i`. - execution_code_body = Op.SSTORE(Op.DUP1, Op.DUP1) - loop_cost += gas_costs.G_VERY_LOW * 2 - elif storage_action == StorageAction.WRITE_NEW_VALUE: - # The new value 2^256-1 is guaranteed to be different from the initial - # value. - execution_code_body = Op.SSTORE(Op.DUP2, Op.NOT(0)) - loop_cost += gas_costs.G_VERY_LOW * 3 - elif storage_action == StorageAction.READ: - execution_code_body = Op.POP(Op.SLOAD(Op.DUP1)) - loop_cost += gas_costs.G_VERY_LOW + gas_costs.G_BASE - - # Add costs jump-logic costs - loop_cost += ( - gas_costs.G_JUMPDEST # Prefix Jumpdest - + gas_costs.G_VERY_LOW * 7 # ISZEROs, PUSHs, SWAPs, SUB, DUP - + gas_costs.G_HIGH # JUMPI - ) - prefix_cost = ( - gas_costs.G_VERY_LOW # Target slots push + executor_code, exec_loop_cost, exec_overhead = create_benchmark_executor( + storage_action, absent_slots, tx_result, fork + ) + initializer_code, init_loop_cost, init_overhead = ( + create_storage_initializer(fork) ) - suffix_cost = 0 - if tx_result == TransactionResult.REVERT: - suffix_cost = ( - gas_costs.G_VERY_LOW * 2 # Revert PUSHs - ) + authority = pre.fund_eoa(amount=0) + initializer_addr = pre.deploy_contract(code=initializer_code) + executor_addr = pre.deploy_contract(code=executor_code) - num_target_slots = ( - gas_benchmark_value - - intrinsic_gas_cost_calc() - - prefix_cost - - suffix_cost - ) // loop_cost - if tx_result == TransactionResult.OUT_OF_GAS: - # Add an extra slot to make it run out-of-gas - num_target_slots += 1 - - code_prefix = Op.PUSH4(num_target_slots) + Op.JUMPDEST - code_loop = execution_code_body + Op.JUMPI( - len(code_prefix) - 1, - Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO, - ) - execution_code = code_prefix + code_loop + delegation_intrinsic = intrinsic_calc(authorization_list_or_count=1) + max_intrinsic = intrinsic_calc(calldata=bytes([0xFF] * 64)) - if tx_result == TransactionResult.REVERT: - execution_code += Op.REVERT(0, 0) - else: - execution_code += Op.STOP + # Number of slots that can be processed in the execution phase + num_target_slots = 0 + current_slot = 1 + gas_remaining = gas_benchmark_value - delegation_intrinsic + while gas_remaining > 0: + tx_gas = min(tx_gas_limit, gas_remaining) + if tx_gas < max_intrinsic + exec_overhead + exec_loop_cost: + break - execution_code_address = pre.deploy_contract(code=execution_code) + slots = (tx_gas - max_intrinsic - exec_overhead) // exec_loop_cost - total_gas_used = ( - num_target_slots * loop_cost - + intrinsic_gas_cost_calc() - + prefix_cost - + suffix_cost - ) + calldata = bytes(Hash(current_slot)) + bytes(Hash(slots)) + execution_intrinsic = intrinsic_calc(calldata=calldata) + + slots = ( + tx_gas - execution_intrinsic - exec_overhead + ) // exec_loop_cost + + num_target_slots += slots + current_slot += slots + gas_remaining -= tx_gas + + blocks = [] + authority_nonce = 0 - # Contract creation - slots_init = Bytecode() + # Setup phase: initialize storage slots (only if absent_slots=False) if not absent_slots: - slots_init = Op.PUSH4(num_target_slots) + While( - body=Op.SSTORE(Op.DUP1, Op.DUP1), - condition=Op.PUSH1(1) - + Op.SWAP1 - + Op.SUB - + Op.DUP1 - + Op.ISZERO - + Op.ISZERO, - ) + setup_txs = [] + + with TestPhaseManager.setup(): + delegation_sender = pre.fund_eoa() + delegation_tx = Transaction( + to=delegation_sender, + gas_limit=tx_gas_limit, + sender=delegation_sender, + authorization_list=[ + AuthorizationTuple( + address=initializer_addr, + nonce=authority_nonce, + signer=authority, + ), + ], + ) + authority_nonce += 1 + + setup_txs.append(delegation_tx) + + current_slot = 1 + remaining_slots = num_target_slots + + while remaining_slots > 0: + if ( + tx_gas_limit + < max_intrinsic + init_overhead + init_loop_cost + ): + break + + slots = ( + tx_gas_limit - max_intrinsic - init_overhead + ) // init_loop_cost + slots = min(slots, remaining_slots) + + calldata = bytes(Hash(current_slot)) + bytes(Hash(slots)) + execution_intrinsic = intrinsic_calc(calldata=calldata) + + slots = ( + tx_gas_limit - execution_intrinsic - init_overhead + ) // init_loop_cost + slots = min(slots, remaining_slots) + + setup_txs.append( + Transaction( + to=authority, + gas_limit=tx_gas_limit, + data=Hash(current_slot) + Hash(slots), + sender=pre.fund_eoa(), + ) + ) + current_slot += slots + remaining_slots -= slots + + blocks.append(Block(txs=setup_txs)) + + # Execution phase: run benchmark + # For absent_slots=False, authority has storage, triggering refund + expected_gas_used = delegation_intrinsic + exec_txs = [] - # To create the contract, we apply the slots_init code to initialize the - # storage slots (int the case of absent_slots=False) and then copy the - # execution code to the contract. - creation_code = ( - slots_init - + Op.EXTCODECOPY( - address=execution_code_address, - dest_offset=0, - offset=0, - size=Op.EXTCODESIZE(execution_code_address), + if not absent_slots: + expected_gas_used -= min( + gas_costs.R_AUTHORIZATION_EXISTING_AUTHORITY, + delegation_intrinsic // 5, ) - + Op.RETURN(0, Op.MSIZE) - ) - sender_addr = pre.fund_eoa() + with TestPhaseManager.setup(): - setup_tx = Transaction( - to=None, - gas_limit=env.gas_limit, - data=creation_code, - sender=sender_addr, + delegation_sender = pre.fund_eoa() + delegation_tx = Transaction( + to=delegation_sender, + gas_limit=tx_gas_limit, + sender=delegation_sender, + authorization_list=[ + AuthorizationTuple( + address=executor_addr, + nonce=authority_nonce, + signer=authority, + ), + ], ) - blocks = [Block(txs=[setup_tx])] - - contract_address = compute_create_address(address=sender_addr, nonce=0) + exec_txs.append(delegation_tx) + current_slot = 1 + gas_remaining = gas_benchmark_value - delegation_intrinsic with TestPhaseManager.execution(): - op_tx = Transaction( - to=contract_address, - gas_limit=gas_benchmark_value, - sender=pre.fund_eoa(), - ) - blocks.append(Block(txs=[op_tx])) + while gas_remaining > 0: + tx_gas = min(tx_gas_limit, gas_remaining) + + if tx_gas < max_intrinsic + exec_overhead + exec_loop_cost: + break + + slots = (tx_gas - max_intrinsic - exec_overhead) // exec_loop_cost + + calldata = bytes(Hash(current_slot)) + bytes(Hash(slots)) + execution_intrinsic = intrinsic_calc(calldata=calldata) + slots = ( + tx_gas - execution_intrinsic - exec_overhead + ) // exec_loop_cost + + if tx_result == TransactionResult.OUT_OF_GAS: + slots = slots * 2 + + exec_txs.append( + Transaction( + to=authority, + gas_limit=tx_gas, + data=Hash(current_slot) + Hash(slots), + sender=pre.fund_eoa(), + ) + ) + + if tx_result == TransactionResult.OUT_OF_GAS: + expected_gas_used += tx_gas + else: + expected_gas_used += ( + intrinsic_calc( + calldata=calldata, + return_cost_deducted_prior_execution=True, + ) + + slots * exec_loop_cost + + exec_overhead + ) + current_slot += slots + + gas_remaining -= tx_gas + + blocks.append(Block(txs=exec_txs)) benchmark_test( blocks=blocks, - expected_benchmark_gas_used=( - total_gas_used - if tx_result != TransactionResult.OUT_OF_GAS - else gas_benchmark_value - ), + expected_benchmark_gas_used=expected_gas_used, ) @@ -319,9 +476,7 @@ def test_storage_access_cold_benchmark( target_opcode=Op.SLOAD if storage_action == StorageAction.READ else Op.SSTORE, - code_generator=ExtCallGenerator( - attack_block=attack_block, - ), + code_generator=ExtCallGenerator(attack_block=attack_block), ) @@ -340,25 +495,20 @@ def test_storage_access_warm( gas_benchmark_value: int, tx_gas_limit: int, ) -> None: - """ - Benchmark warm storage slot accesses. - """ + """Benchmark warm storage slot accesses.""" blocks = [] - # The warm access is done in storage slot 0. - - # Contract code - execution_code_body = Bytecode() - if storage_action == StorageAction.WRITE_SAME_VALUE: - execution_code_body = Op.SSTORE(0, Op.DUP1) - elif storage_action == StorageAction.WRITE_NEW_VALUE: - execution_code_body = Op.SSTORE(0, Op.GAS) - elif storage_action == StorageAction.READ: - execution_code_body = Op.POP(Op.SLOAD(0)) - - execution_code = Op.SLOAD(0) + While( - body=execution_code_body, - ) + match storage_action: + case StorageAction.WRITE_SAME_VALUE: + execution_code_body = Op.SSTORE(0, Op.DUP1) + case StorageAction.WRITE_NEW_VALUE: + execution_code_body = Op.SSTORE(0, Op.GAS) + case StorageAction.READ: + execution_code_body = Op.POP(Op.SLOAD(0)) + case _: + raise ValueError("Unspecified storage action") + + execution_code = Op.SLOAD(0) + While(body=execution_code_body) execution_code_address = pre.deploy_contract(code=execution_code) creation_code = ( @@ -391,12 +541,13 @@ def test_storage_access_warm( gas_limit = min( tx_gas_limit, gas_benchmark_value - i * tx_gas_limit ) - op_tx = Transaction( - to=contract_address, - gas_limit=gas_limit, - sender=pre.fund_eoa(), + txs.append( + Transaction( + to=contract_address, + gas_limit=gas_limit, + sender=pre.fund_eoa(), + ) ) - txs.append(op_tx) blocks.append(Block(txs=txs)) benchmark_test(blocks=blocks) @@ -421,19 +572,19 @@ def test_storage_access_warm_benchmark( Each iteration accesses a different storage slot (incrementing key) to ensure warm access costs are measured. """ - attack_block = Bytecode() - if storage_action == StorageAction.WRITE_SAME_VALUE: - attack_block = Op.SSTORE(Op.PUSH0, Op.PUSH0) - elif storage_action == StorageAction.WRITE_NEW_VALUE: - attack_block = Op.SSTORE(Op.PUSH0, Op.GAS) - elif storage_action == StorageAction.READ: - attack_block = Op.SLOAD(Op.PUSH0) + match storage_action: + case StorageAction.WRITE_SAME_VALUE: + attack_block = Op.SSTORE(Op.PUSH0, Op.PUSH0) + case StorageAction.WRITE_NEW_VALUE: + attack_block = Op.SSTORE(Op.PUSH0, Op.GAS) + case StorageAction.READ: + attack_block = Op.SLOAD(Op.PUSH0) + case _: + raise ValueError("Unspecified storage action") benchmark_test( target_opcode=Op.SLOAD if storage_action == StorageAction.READ else Op.SSTORE, - code_generator=ExtCallGenerator( - attack_block=attack_block, - ), + code_generator=ExtCallGenerator(attack_block=attack_block), ) diff --git a/tests/benchmark/compute/scenario/test_transaction_types.py b/tests/benchmark/compute/scenario/test_transaction_types.py index 23c00f0ce7..e9c047d9bd 100644 --- a/tests/benchmark/compute/scenario/test_transaction_types.py +++ b/tests/benchmark/compute/scenario/test_transaction_types.py @@ -190,15 +190,15 @@ def test_block_full_of_ether_transfers( @pytest.fixture -def total_cost_floor_per_token() -> int: - """Total cost floor per token.""" - return 10 +def total_cost_floor_per_token(fork: Fork) -> int: + """Total cost floor per token (EIP-7623).""" + return fork.gas_costs().G_TX_DATA_FLOOR_TOKEN_COST @pytest.fixture -def total_cost_standard_per_token() -> int: - """Total cost floor per token.""" - return 4 +def total_cost_standard_per_token(fork: Fork) -> int: + """Standard cost per token (EIP-7623).""" + return fork.gas_costs().G_TX_DATA_STANDARD_TOKEN_COST def calldata_generator( @@ -251,13 +251,37 @@ def test_block_full_data( tx_gas_limit: int, fork: Fork, ) -> None: - """Test a block with empty payload.""" + """Test a block full of calldata, respecting RLP size limits.""" iteration_count = math.ceil(gas_benchmark_value / tx_gas_limit) - gas_remaining = gas_benchmark_value + # check for EIP-7934 block RLP size limit and cap gas to stay under it + block_rlp_limit = fork.block_rlp_size_limit() + effective_gas = gas_benchmark_value + + if block_rlp_limit: + # Max calldata bytes at 99% of limit (Osaka: 8,388,608 * 0.99 ≈ 8.3 MB) + safe_calldata_bytes = int(block_rlp_limit * 0.99) + + # convert to gas: zero bytes = 10 gas/byte, non-zero = 40 gas/byte + gas_per_byte = ( + total_cost_floor_per_token + if zero_byte + else total_cost_floor_per_token * 4 + ) + # For zero bytes: 8.3MB * 10 = 83M gas just for calldata + max_calldata_gas = safe_calldata_bytes * gas_per_byte + # Add intrinsic cost per tx (Osaka): 83M + 6 txs * 21k ≈ 83.1M total + rlp_limited_gas = max_calldata_gas + iteration_count * intrinsic_cost + + # use the min between benchmark target and the RLP limit + effective_gas = min(gas_benchmark_value, rlp_limited_gas) + + gas_remaining = effective_gas total_gas_used = 0 txs = [] for _ in range(iteration_count): + if gas_remaining <= intrinsic_cost: + break gas_available = min(tx_gas_limit, gas_remaining) - intrinsic_cost data = calldata_generator( gas_available, diff --git a/tests/benchmark/stateful/bloatnet/stubs.json b/tests/benchmark/stateful/bloatnet/stubs.json new file mode 100644 index 0000000000..f10cc6c5e2 --- /dev/null +++ b/tests/benchmark/stateful/bloatnet/stubs.json @@ -0,0 +1,299 @@ +{ + "test_sload_empty_erc20_balanceof_30GB_ERC20": "0x19fc17d87D946BBA47ca276f7b06Ee5737c4679C", + "test_sload_empty_erc20_balanceof_XEN": "0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8", + "test_sload_empty_erc20_balanceof_USDT": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "test_sload_empty_erc20_balanceof_USDC": "0xA0b86991C6218B36c1d19D4a2E9Eb0CE3606EB48", + "test_sload_empty_erc20_balanceof_LPT": "0x58b6A8a3302369DAEc383334672404Ee733AB239", + "test_sload_empty_erc20_balanceof_SHIB": "0x95aD61B0a150d79219dCF64E1E6Cc01f0B64C4cE", + "test_sload_empty_erc20_balanceof_WETH": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "test_sload_empty_erc20_balanceof_G-CRE": "0xa3Ee21c306A700E682AbcDfE9bAA6A08F3820419", + "test_sload_empty_erc20_balanceof_MEME": "0xB131F4A55907B10d1F0A50d8Ab8FA09EC342CD74", + "test_sload_empty_erc20_balanceof_OMG": "0xd26114cD6EE289AccF82350c8d8487fedB8A0C07", + "test_sload_empty_erc20_balanceof_MATIC": "0x7d1Afa7B718fb893DB30A3abc0Cfc608AaCfEbB0", + "test_sload_empty_erc20_balanceof_stETH": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + "test_sload_empty_erc20_balanceof_DAI": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "test_sload_empty_erc20_balanceof_PEPE": "0x6982508145454Ce325dDbE47a25d4eC3d2311933", + "test_sload_empty_erc20_balanceof_old": "0x0cf0ee63788A0849FE5297F3407f701E122CC023", + "test_sload_empty_erc20_balanceof_BAT": "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", + "test_sload_empty_erc20_balanceof_UNI": "0x1F9840a85d5aF5bf1D1762F925BdADdC4201F984", + "test_sload_empty_erc20_balanceof_AMB": "0x4dc3643Dbc642b72C158E7F3d2FF232df61cB6CE", + "test_sload_empty_erc20_balanceof_HEX": "0x2b591e99afE9f32eAA6214f7B7629768c40eEb39", + "test_sload_empty_erc20_balanceof_CRO": "0xa0b73e1ff0b80914ab6fe0444e65848c4c34450b", + "test_sload_empty_erc20_balanceof_UCASH": "0x92e52a1A235d9A103D970901066CE910AAceFD37", + "test_sload_empty_erc20_balanceof_BNB": "0xB8c77482e45F1F44dE1745F52C74426C631bDd52", + "test_sload_empty_erc20_balanceof_GSE": "0xe530441f4f73bdb6dc2fa5af7c3fc5fd551ec838", + "test_sload_empty_erc20_balanceof_MANA": "0x0F5D2FB29fb7d3cFeE444A200298f468908cC942", + "test_sload_empty_erc20_balanceof_OCN": "0x4092678e4E78230F46A1534C0fBC8Fa39780892B", + "test_sload_empty_erc20_balanceof_EIGEN": "0xEC53BF9167F50cDEb3aE105F56099AaAb9061F83", + "test_sload_empty_erc20_balanceof_COMP": "0xc00e94Cb662C3520282E6f5717214004A7f26888", + "test_sload_empty_erc20_balanceof_cUSDC": "0x39AA39c021dfbaE8faC545936693aC917d5E7563", + "test_sload_empty_erc20_balanceof_sMEME": "0xc059A531B4234d05e9EF4aC51028f7E6156E2CcE", + "test_sload_empty_erc20_balanceof_SAND": "0x3845badade8e6dff049820680d1f14bd3903a5d0", + "test_sload_empty_erc20_balanceof_AAVE": "0x7Fc66500c84A76Ad7e9c93437bFc5AC33E2DDAe9", + "test_sload_empty_erc20_balanceof_ZRX": "0xE41d2489571d322189246DaFA5ebDe1F4699F498", + "test_sload_empty_erc20_balanceof_KOK": "0x9B9647431632AF44be02ddd22477Ed94d14AacAa", + "test_sload_empty_erc20_balanceof_APE": "0x4d224452801ACEd8B2F0aebe155379bb5D594381", + "test_sload_empty_erc20_balanceof_SAI": "0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359", + "test_sload_empty_erc20_balanceof_GRT": "0xc944E90C64B2c07662A292be6244BDf05Cda44a7", + "test_sload_empty_erc20_balanceof_LRC": "0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD", + "test_sload_empty_erc20_balanceof_ELON": "0x761D38e5DDf6ccf6Cf7C55759d5210750B5D60F3", + "test_sload_empty_erc20_balanceof_QNT": "0x4a220E6096B25EADb88358cb44068A3248254675", + "test_sload_empty_erc20_balanceof_ONDO": "0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3", + "test_sload_empty_erc20_balanceof_ENJ": "0xF629cBd94d3791c9250152BD8dfBDF380E2a3B9c", + "test_sload_empty_erc20_balanceof_FET": "0x1D287CC25dAD7cCaF76a26bc660c5F7C8E2a05BD", + "test_sload_empty_erc20_balanceof_eETH": "0x6c5024Cd4F8A59110119C56f8933403A539555EB", + "test_sload_empty_erc20_balanceof_XMX": "0x0F8c45B896784A1E408526B9300519ef8660209c", + "test_sload_empty_erc20_balanceof_FTI": "0x943ed852Dadb5C3938ECdC6883718df8142de4C8", + "test_sload_empty_erc20_balanceof_WBTC": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + "test_sload_empty_erc20_balanceof_LEND": "0x80fB784B7eD66730e8b1DBd9820aFD29931aab03", + "test_sload_empty_erc20_balanceof_ELEC": "0xd49ff13661451313ca1553fd6954bd1d9b6e02b9", + "test_sload_empty_erc20_balanceof_SUSHI": "0x6B3595068778DD592e39A122f4f5a5CF09C90fE2", + "test_sload_empty_erc20_balanceof_HOT": "0x6c6EE5e31d828De241282B9606C8e98Ea48526E2", + "test_sload_empty_erc20_balanceof_MITx": "0x4a527d8fc13c5203ab24ba0944f4cb14658d1db6", + "test_sload_empty_erc20_balanceof_1INCH": "0x111111111117dC0aa78b770fA6A738034120C302", + "test_sload_empty_erc20_balanceof_USDP": "0x1456688345527bE1f37E9e627DA0837D6f08C925", + "test_sload_empty_erc20_balanceof_ETHFI": "0xfe0c30065b384f05761f15d0cc899d4f9f9cc0eb", + "test_sload_empty_erc20_balanceof_POLY": "0x9992ec3cf6a55b00978cddf2b27bc6882d88d1ec", + "test_sload_empty_erc20_balanceof_AOA": "0x9ab165d795019b6d8b3e971dda91071421305e5a", + "test_sload_empty_erc20_balanceof_STORJ": "0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC", + "test_sload_empty_erc20_balanceof_MKR": "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", + "test_sload_empty_erc20_balanceof_AMP": "0xfF20817765cB7F73d4Bde2e66e067e58d11095c2", + "test_sload_empty_erc20_balanceof_VRA": "0xF411903cbc70a74d22900a5DE66A2dda66507255", + "test_sload_empty_erc20_balanceof_GTC": "0xde30da39c46104798bb5aa3fe8b9e0e1f348163f", + "test_sload_empty_erc20_balanceof_FLOKI": "0x43F11c02439E2736800433B4594994Bd43Cd066D", + "test_sload_empty_erc20_balanceof_ALT": "0x8457CA5040ad67fdebbCC8EdCE889A335Bc0fbFB", + "test_sload_empty_erc20_balanceof_IMX": "0xf57e7e7c23978c3caec3c3548e3d615c346e79ff", + "test_sload_empty_erc20_balanceof_XYO": "0x55296f69f40ea6d20e478533c15A6b08B654E758", + "test_sload_empty_erc20_balanceof_REV": "0x2ef27bf41236bd859a95209e17a43fbd26851f92", + "test_sload_empty_erc20_balanceof_FUN": "0x419d0d8bdd9af5e606ae2232ed285aff190e711b", + "test_sload_empty_erc20_balanceof_CRV": "0xD533a949740bb3306d119CC777fa900bA034cd52", + "test_sload_empty_erc20_balanceof_CHZ": "0x3506424f91fd33084466f402d5d97f05f8e3b4af", + "test_sload_empty_erc20_balanceof_SMT": "0x78Eb8DC641077F049f910659b6d580E80dC4d237", + "test_sload_empty_erc20_balanceof_SNX": "0xC011A72400E58ecD99Ee497CF89E3775d4bd732F", + "test_sload_empty_erc20_balanceof_DENT": "0x3597bfD533a99c9aa083587B074434E61Eb0A258", + "test_sload_empty_erc20_balanceof_RNDR": "0x6De037ef9aD2725EB40118Bb1702EBb27e4Aeb24", + "test_sload_empty_erc20_balanceof_SNT": "0x744d70FDBe2Ba4CF95131626614a1763DF805B9E", + "test_sload_empty_erc20_balanceof_AXS": "0xBB0E17EF65F82Ab018d8EDd776e8DD940327B28b", + "test_sload_empty_erc20_balanceof_KNC": "0xdd974D5C2e2928deA5F71b9825b8b646686BD200", + "test_sload_empty_erc20_balanceof_WEPE": "0xccB365D2e11aE4D6d74715c680f56cf58bF4bF10", + "test_sload_empty_erc20_balanceof_ZETA": "0xf091867ec603a6628ed83d274e835539d82e9cc8", + "test_sload_empty_erc20_balanceof_LYM": "0xc690f7c7fcffa6a82b79fab7508c466fefdfc8c5", + "test_sload_empty_erc20_balanceof_nCASH": "0x809826cceAb68c387726af962713b64Cb5Cb3CCA", + "test_sload_empty_erc20_balanceof_LOOKS": "0xf4d2888d29D722226FafA5d9B24F9164c092421E", + "test_sload_empty_erc20_balanceof_Monfter/Monavale": "0x275f5ad03be0fa221b4c6649b8aee09a42d9412a", + "test_sload_empty_erc20_balanceof_cETH": "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", + "test_sload_empty_erc20_balanceof_SALT": "0x4156D3342D5c385a87D264F90653733592000581", + "test_sload_empty_erc20_balanceof_HOGE": "0xfAd45E47083e4607302aa43c65fB3106F1cd7607", + "test_sload_empty_erc20_balanceof_REN": "0x408e41876cCCDC0F92210600ef50372656052a38", + "test_sload_empty_erc20_balanceof_ENS": "0xC56b13EBBCffa67cfB7979b900B736b3fb480D78", + "test_sload_empty_erc20_balanceof_NEXO": "0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206", + "test_sload_empty_erc20_balanceof_RFR": "0xD0929d411954c47438Dc1D871dd6081F5C5e149c", + "test_sload_empty_erc20_balanceof_COFI": "0x3137619705b5fc22a3048989F983905e456B59Ab", + "test_sload_empty_erc20_balanceof_SLP": "0xcc8fa225d80b9c7d42f96e9570156c65d6cAAa25", + "test_sload_empty_erc20_balanceof_FUEL": "0xea38eaa3c86c8f9b751533ba2e562deb9acded40", + "test_sload_empty_erc20_balanceof_ENA": "0x57e114B691Db790C35207b2e685D4A43181e6061", + "test_sload_empty_erc20_balanceof_AKITA": "0x3301Ee63Fb29F863f2333Bd4466acb46CD8323E6", + "test_sload_empty_erc20_balanceof_CVC": "0x41e5560054824ea6B0732E656e3Ad64E20e94e45", + "test_sload_empty_erc20_balanceof_IHT": "0xEda8B016efa8b1161208Cf041cD86972EEE0F31E", + "test_sload_empty_erc20_balanceof_ZSC": "0x7A41e0517a5ecA4FdbC7FbebA4D4c47B9fF6DC63", + "test_sload_empty_erc20_balanceof_cbETH": "0xBe9895146f7AF43049ca1c1AE358B0541Ea49704", + "test_sload_empty_erc20_balanceof_IMT": "0x13119e34e140097a507b07a5564bde1bc375d9e6", + "test_sstore_erc20_approve_30GB_ERC20": "0x19fc17d87D946BBA47ca276f7b06Ee5737c4679C", + "test_sstore_erc20_approve_XEN": "0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8", + "test_sstore_erc20_approve_USDT": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "test_sstore_erc20_approve_USDC": "0xA0b86991C6218B36c1d19D4a2E9Eb0CE3606EB48", + "test_sstore_erc20_approve_LPT": "0x58b6A8a3302369DAEc383334672404Ee733AB239", + "test_sstore_erc20_approve_SHIB": "0x95aD61B0a150d79219dCF64E1E6Cc01f0B64C4cE", + "test_sstore_erc20_approve_WETH": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "test_sstore_erc20_approve_G-CRE": "0xa3Ee21c306A700E682AbcDfE9bAA6A08F3820419", + "test_sstore_erc20_approve_MEME": "0xB131F4A55907B10d1F0A50d8Ab8FA09EC342CD74", + "test_sstore_erc20_approve_OMG": "0xd26114cD6EE289AccF82350c8d8487fedB8A0C07", + "test_sstore_erc20_approve_MATIC": "0x7d1Afa7B718fb893DB30A3abc0Cfc608AaCfEbB0", + "test_sstore_erc20_approve_stETH": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + "test_sstore_erc20_approve_DAI": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "test_sstore_erc20_approve_PEPE": "0x6982508145454Ce325dDbE47a25d4eC3d2311933", + "test_sstore_erc20_approve_old": "0x0cf0ee63788A0849FE5297F3407f701E122CC023", + "test_sstore_erc20_approve_BAT": "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", + "test_sstore_erc20_approve_UNI": "0x1F9840a85d5aF5bf1D1762F925BdADdC4201F984", + "test_sstore_erc20_approve_AMB": "0x4dc3643Dbc642b72C158E7F3d2FF232df61cB6CE", + "test_sstore_erc20_approve_HEX": "0x2b591e99afE9f32eAA6214f7B7629768c40eEb39", + "test_sstore_erc20_approve_CRO": "0xa0b73e1ff0b80914ab6fe0444e65848c4c34450b", + "test_sstore_erc20_approve_UCASH": "0x92e52a1A235d9A103D970901066CE910AAceFD37", + "test_sstore_erc20_approve_BNB": "0xB8c77482e45F1F44dE1745F52C74426C631bDd52", + "test_sstore_erc20_approve_GSE": "0xe530441f4f73bdb6dc2fa5af7c3fc5fd551ec838", + "test_sstore_erc20_approve_MANA": "0x0F5D2FB29fb7d3cFeE444A200298f468908cC942", + "test_sstore_erc20_approve_OCN": "0x4092678e4E78230F46A1534C0fBC8Fa39780892B", + "test_sstore_erc20_approve_EIGEN": "0xEC53BF9167F50cDEb3aE105F56099AaAb9061F83", + "test_sstore_erc20_approve_COMP": "0xc00e94Cb662C3520282E6f5717214004A7f26888", + "test_sstore_erc20_approve_cUSDC": "0x39AA39c021dfbaE8faC545936693aC917d5E7563", + "test_sstore_erc20_approve_sMEME": "0xc059A531B4234d05e9EF4aC51028f7E6156E2CcE", + "test_sstore_erc20_approve_SAND": "0x3845badade8e6dff049820680d1f14bd3903a5d0", + "test_sstore_erc20_approve_AAVE": "0x7Fc66500c84A76Ad7e9c93437bFc5AC33E2DDAe9", + "test_sstore_erc20_approve_ZRX": "0xE41d2489571d322189246DaFA5ebDe1F4699F498", + "test_sstore_erc20_approve_KOK": "0x9B9647431632AF44be02ddd22477Ed94d14AacAa", + "test_sstore_erc20_approve_APE": "0x4d224452801ACEd8B2F0aebe155379bb5D594381", + "test_sstore_erc20_approve_SAI": "0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359", + "test_sstore_erc20_approve_GRT": "0xc944E90C64B2c07662A292be6244BDf05Cda44a7", + "test_sstore_erc20_approve_LRC": "0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD", + "test_sstore_erc20_approve_ELON": "0x761D38e5DDf6ccf6Cf7C55759d5210750B5D60F3", + "test_sstore_erc20_approve_QNT": "0x4a220E6096B25EADb88358cb44068A3248254675", + "test_sstore_erc20_approve_ONDO": "0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3", + "test_sstore_erc20_approve_ENJ": "0xF629cBd94d3791c9250152BD8dfBDF380E2a3B9c", + "test_sstore_erc20_approve_FET": "0x1D287CC25dAD7cCaF76a26bc660c5F7C8E2a05BD", + "test_sstore_erc20_approve_eETH": "0x6c5024Cd4F8A59110119C56f8933403A539555EB", + "test_sstore_erc20_approve_XMX": "0x0F8c45B896784A1E408526B9300519ef8660209c", + "test_sstore_erc20_approve_FTI": "0x943ed852Dadb5C3938ECdC6883718df8142de4C8", + "test_sstore_erc20_approve_WBTC": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + "test_sstore_erc20_approve_LEND": "0x80fB784B7eD66730e8b1DBd9820aFD29931aab03", + "test_sstore_erc20_approve_ELEC": "0xd49ff13661451313ca1553fd6954bd1d9b6e02b9", + "test_sstore_erc20_approve_SUSHI": "0x6B3595068778DD592e39A122f4f5a5CF09C90fE2", + "test_sstore_erc20_approve_HOT": "0x6c6EE5e31d828De241282B9606C8e98Ea48526E2", + "test_sstore_erc20_approve_MITx": "0x4a527d8fc13c5203ab24ba0944f4cb14658d1db6", + "test_sstore_erc20_approve_1INCH": "0x111111111117dC0aa78b770fA6A738034120C302", + "test_sstore_erc20_approve_USDP": "0x1456688345527bE1f37E9e627DA0837D6f08C925", + "test_sstore_erc20_approve_ETHFI": "0xfe0c30065b384f05761f15d0cc899d4f9f9cc0eb", + "test_sstore_erc20_approve_POLY": "0x9992ec3cf6a55b00978cddf2b27bc6882d88d1ec", + "test_sstore_erc20_approve_AOA": "0x9ab165d795019b6d8b3e971dda91071421305e5a", + "test_sstore_erc20_approve_STORJ": "0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC", + "test_sstore_erc20_approve_MKR": "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", + "test_sstore_erc20_approve_AMP": "0xfF20817765cB7F73d4Bde2e66e067e58d11095c2", + "test_sstore_erc20_approve_VRA": "0xF411903cbc70a74d22900a5DE66A2dda66507255", + "test_sstore_erc20_approve_GTC": "0xde30da39c46104798bb5aa3fe8b9e0e1f348163f", + "test_sstore_erc20_approve_FLOKI": "0x43F11c02439E2736800433B4594994Bd43Cd066D", + "test_sstore_erc20_approve_ALT": "0x8457CA5040ad67fdebbCC8EdCE889A335Bc0fbFB", + "test_sstore_erc20_approve_IMX": "0xf57e7e7c23978c3caec3c3548e3d615c346e79ff", + "test_sstore_erc20_approve_XYO": "0x55296f69f40ea6d20e478533c15A6b08B654E758", + "test_sstore_erc20_approve_REV": "0x2ef27bf41236bd859a95209e17a43fbd26851f92", + "test_sstore_erc20_approve_FUN": "0x419d0d8bdd9af5e606ae2232ed285aff190e711b", + "test_sstore_erc20_approve_CRV": "0xD533a949740bb3306d119CC777fa900bA034cd52", + "test_sstore_erc20_approve_CHZ": "0x3506424f91fd33084466f402d5d97f05f8e3b4af", + "test_sstore_erc20_approve_SMT": "0x78Eb8DC641077F049f910659b6d580E80dC4d237", + "test_sstore_erc20_approve_SNX": "0xC011A72400E58ecD99Ee497CF89E3775d4bd732F", + "test_sstore_erc20_approve_DENT": "0x3597bfD533a99c9aa083587B074434E61Eb0A258", + "test_sstore_erc20_approve_RNDR": "0x6De037ef9aD2725EB40118Bb1702EBb27e4Aeb24", + "test_sstore_erc20_approve_SNT": "0x744d70FDBe2Ba4CF95131626614a1763DF805B9E", + "test_sstore_erc20_approve_AXS": "0xBB0E17EF65F82Ab018d8EDd776e8DD940327B28b", + "test_sstore_erc20_approve_KNC": "0xdd974D5C2e2928deA5F71b9825b8b646686BD200", + "test_sstore_erc20_approve_WEPE": "0xccB365D2e11aE4D6d74715c680f56cf58bF4bF10", + "test_sstore_erc20_approve_ZETA": "0xf091867ec603a6628ed83d274e835539d82e9cc8", + "test_sstore_erc20_approve_LYM": "0xc690f7c7fcffa6a82b79fab7508c466fefdfc8c5", + "test_sstore_erc20_approve_nCASH": "0x809826cceAb68c387726af962713b64Cb5Cb3CCA", + "test_sstore_erc20_approve_LOOKS": "0xf4d2888d29D722226FafA5d9B24F9164c092421E", + "test_sstore_erc20_approve_Monfter/Monavale": "0x275f5ad03be0fa221b4c6649b8aee09a42d9412a", + "test_sstore_erc20_approve_cETH": "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", + "test_sstore_erc20_approve_SALT": "0x4156D3342D5c385a87D264F90653733592000581", + "test_sstore_erc20_approve_HOGE": "0xfAd45E47083e4607302aa43c65fB3106F1cd7607", + "test_sstore_erc20_approve_REN": "0x408e41876cCCDC0F92210600ef50372656052a38", + "test_sstore_erc20_approve_ENS": "0xC56b13EBBCffa67cfB7979b900B736b3fb480D78", + "test_sstore_erc20_approve_NEXO": "0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206", + "test_sstore_erc20_approve_RFR": "0xD0929d411954c47438Dc1D871dd6081F5C5e149c", + "test_sstore_erc20_approve_COFI": "0x3137619705b5fc22a3048989F983905e456B59Ab", + "test_sstore_erc20_approve_SLP": "0xcc8fa225d80b9c7d42f96e9570156c65d6cAAa25", + "test_sstore_erc20_approve_FUEL": "0xea38eaa3c86c8f9b751533ba2e562deb9acded40", + "test_sstore_erc20_approve_ENA": "0x57e114B691Db790C35207b2e685D4A43181e6061", + "test_sstore_erc20_approve_AKITA": "0x3301Ee63Fb29F863f2333Bd4466acb46CD8323E6", + "test_sstore_erc20_approve_CVC": "0x41e5560054824ea6B0732E656e3Ad64E20e94e45", + "test_sstore_erc20_approve_IHT": "0xEda8B016efa8b1161208Cf041cD86972EEE0F31E", + "test_sstore_erc20_approve_ZSC": "0x7A41e0517a5ecA4FdbC7FbebA4D4c47B9fF6DC63", + "test_sstore_erc20_approve_cbETH": "0xBe9895146f7AF43049ca1c1AE358B0541Ea49704", + "test_sstore_erc20_approve_IMT": "0x13119e34e140097a507b07a5564bde1bc375d9e6", + "test_mixed_sload_sstore_30GB_ERC20": "0x19fc17d87D946BBA47ca276f7b06Ee5737c4679C", + "test_mixed_sload_sstore_XEN": "0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8", + "test_mixed_sload_sstore_USDT": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "test_mixed_sload_sstore_USDC": "0xA0b86991C6218B36c1d19D4a2E9Eb0CE3606EB48", + "test_mixed_sload_sstore_LPT": "0x58b6A8a3302369DAEc383334672404Ee733AB239", + "test_mixed_sload_sstore_SHIB": "0x95aD61B0a150d79219dCF64E1E6Cc01f0B64C4cE", + "test_mixed_sload_sstore_WETH": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "test_mixed_sload_sstore_G-CRE": "0xa3Ee21c306A700E682AbcDfE9bAA6A08F3820419", + "test_mixed_sload_sstore_MEME": "0xB131F4A55907B10d1F0A50d8Ab8FA09EC342CD74", + "test_mixed_sload_sstore_OMG": "0xd26114cD6EE289AccF82350c8d8487fedB8A0C07", + "test_mixed_sload_sstore_MATIC": "0x7d1Afa7B718fb893DB30A3abc0Cfc608AaCfEbB0", + "test_mixed_sload_sstore_stETH": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + "test_mixed_sload_sstore_DAI": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "test_mixed_sload_sstore_PEPE": "0x6982508145454Ce325dDbE47a25d4eC3d2311933", + "test_mixed_sload_sstore_old": "0x0cf0ee63788A0849FE5297F3407f701E122CC023", + "test_mixed_sload_sstore_BAT": "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", + "test_mixed_sload_sstore_UNI": "0x1F9840a85d5aF5bf1D1762F925BdADdC4201F984", + "test_mixed_sload_sstore_AMB": "0x4dc3643Dbc642b72C158E7F3d2FF232df61cB6CE", + "test_mixed_sload_sstore_HEX": "0x2b591e99afE9f32eAA6214f7B7629768c40eEb39", + "test_mixed_sload_sstore_CRO": "0xa0b73e1ff0b80914ab6fe0444e65848c4c34450b", + "test_mixed_sload_sstore_UCASH": "0x92e52a1A235d9A103D970901066CE910AAceFD37", + "test_mixed_sload_sstore_BNB": "0xB8c77482e45F1F44dE1745F52C74426C631bDd52", + "test_mixed_sload_sstore_GSE": "0xe530441f4f73bdb6dc2fa5af7c3fc5fd551ec838", + "test_mixed_sload_sstore_MANA": "0x0F5D2FB29fb7d3cFeE444A200298f468908cC942", + "test_mixed_sload_sstore_OCN": "0x4092678e4E78230F46A1534C0fBC8Fa39780892B", + "test_mixed_sload_sstore_EIGEN": "0xEC53BF9167F50cDEb3aE105F56099AaAb9061F83", + "test_mixed_sload_sstore_COMP": "0xc00e94Cb662C3520282E6f5717214004A7f26888", + "test_mixed_sload_sstore_cUSDC": "0x39AA39c021dfbaE8faC545936693aC917d5E7563", + "test_mixed_sload_sstore_sMEME": "0xc059A531B4234d05e9EF4aC51028f7E6156E2CcE", + "test_mixed_sload_sstore_SAND": "0x3845badade8e6dff049820680d1f14bd3903a5d0", + "test_mixed_sload_sstore_AAVE": "0x7Fc66500c84A76Ad7e9c93437bFc5AC33E2DDAe9", + "test_mixed_sload_sstore_ZRX": "0xE41d2489571d322189246DaFA5ebDe1F4699F498", + "test_mixed_sload_sstore_KOK": "0x9B9647431632AF44be02ddd22477Ed94d14AacAa", + "test_mixed_sload_sstore_APE": "0x4d224452801ACEd8B2F0aebe155379bb5D594381", + "test_mixed_sload_sstore_SAI": "0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359", + "test_mixed_sload_sstore_GRT": "0xc944E90C64B2c07662A292be6244BDf05Cda44a7", + "test_mixed_sload_sstore_LRC": "0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD", + "test_mixed_sload_sstore_ELON": "0x761D38e5DDf6ccf6Cf7C55759d5210750B5D60F3", + "test_mixed_sload_sstore_QNT": "0x4a220E6096B25EADb88358cb44068A3248254675", + "test_mixed_sload_sstore_ONDO": "0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3", + "test_mixed_sload_sstore_ENJ": "0xF629cBd94d3791c9250152BD8dfBDF380E2a3B9c", + "test_mixed_sload_sstore_FET": "0x1D287CC25dAD7cCaF76a26bc660c5F7C8E2a05BD", + "test_mixed_sload_sstore_eETH": "0x6c5024Cd4F8A59110119C56f8933403A539555EB", + "test_mixed_sload_sstore_XMX": "0x0F8c45B896784A1E408526B9300519ef8660209c", + "test_mixed_sload_sstore_FTI": "0x943ed852Dadb5C3938ECdC6883718df8142de4C8", + "test_mixed_sload_sstore_WBTC": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + "test_mixed_sload_sstore_LEND": "0x80fB784B7eD66730e8b1DBd9820aFD29931aab03", + "test_mixed_sload_sstore_ELEC": "0xd49ff13661451313ca1553fd6954bd1d9b6e02b9", + "test_mixed_sload_sstore_SUSHI": "0x6B3595068778DD592e39A122f4f5a5CF09C90fE2", + "test_mixed_sload_sstore_HOT": "0x6c6EE5e31d828De241282B9606C8e98Ea48526E2", + "test_mixed_sload_sstore_MITx": "0x4a527d8fc13c5203ab24ba0944f4cb14658d1db6", + "test_mixed_sload_sstore_1INCH": "0x111111111117dC0aa78b770fA6A738034120C302", + "test_mixed_sload_sstore_USDP": "0x1456688345527bE1f37E9e627DA0837D6f08C925", + "test_mixed_sload_sstore_ETHFI": "0xfe0c30065b384f05761f15d0cc899d4f9f9cc0eb", + "test_mixed_sload_sstore_POLY": "0x9992ec3cf6a55b00978cddf2b27bc6882d88d1ec", + "test_mixed_sload_sstore_AOA": "0x9ab165d795019b6d8b3e971dda91071421305e5a", + "test_mixed_sload_sstore_STORJ": "0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC", + "test_mixed_sload_sstore_MKR": "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", + "test_mixed_sload_sstore_AMP": "0xfF20817765cB7F73d4Bde2e66e067e58d11095c2", + "test_mixed_sload_sstore_VRA": "0xF411903cbc70a74d22900a5DE66A2dda66507255", + "test_mixed_sload_sstore_GTC": "0xde30da39c46104798bb5aa3fe8b9e0e1f348163f", + "test_mixed_sload_sstore_FLOKI": "0x43F11c02439E2736800433B4594994Bd43Cd066D", + "test_mixed_sload_sstore_ALT": "0x8457CA5040ad67fdebbCC8EdCE889A335Bc0fbFB", + "test_mixed_sload_sstore_IMX": "0xf57e7e7c23978c3caec3c3548e3d615c346e79ff", + "test_mixed_sload_sstore_XYO": "0x55296f69f40ea6d20e478533c15A6b08B654E758", + "test_mixed_sload_sstore_REV": "0x2ef27bf41236bd859a95209e17a43fbd26851f92", + "test_mixed_sload_sstore_FUN": "0x419d0d8bdd9af5e606ae2232ed285aff190e711b", + "test_mixed_sload_sstore_CRV": "0xD533a949740bb3306d119CC777fa900bA034cd52", + "test_mixed_sload_sstore_CHZ": "0x3506424f91fd33084466f402d5d97f05f8e3b4af", + "test_mixed_sload_sstore_SMT": "0x78Eb8DC641077F049f910659b6d580E80dC4d237", + "test_mixed_sload_sstore_SNX": "0xC011A72400E58ecD99Ee497CF89E3775d4bd732F", + "test_mixed_sload_sstore_DENT": "0x3597bfD533a99c9aa083587B074434E61Eb0A258", + "test_mixed_sload_sstore_RNDR": "0x6De037ef9aD2725EB40118Bb1702EBb27e4Aeb24", + "test_mixed_sload_sstore_SNT": "0x744d70FDBe2Ba4CF95131626614a1763DF805B9E", + "test_mixed_sload_sstore_AXS": "0xBB0E17EF65F82Ab018d8EDd776e8DD940327B28b", + "test_mixed_sload_sstore_KNC": "0xdd974D5C2e2928deA5F71b9825b8b646686BD200", + "test_mixed_sload_sstore_WEPE": "0xccB365D2e11aE4D6d74715c680f56cf58bF4bF10", + "test_mixed_sload_sstore_ZETA": "0xf091867ec603a6628ed83d274e835539d82e9cc8", + "test_mixed_sload_sstore_LYM": "0xc690f7c7fcffa6a82b79fab7508c466fefdfc8c5", + "test_mixed_sload_sstore_nCASH": "0x809826cceAb68c387726af962713b64Cb5Cb3CCA", + "test_mixed_sload_sstore_LOOKS": "0xf4d2888d29D722226FafA5d9B24F9164c092421E", + "test_mixed_sload_sstore_Monfter/Monavale": "0x275f5ad03be0fa221b4c6649b8aee09a42d9412a", + "test_mixed_sload_sstore_cETH": "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", + "test_mixed_sload_sstore_SALT": "0x4156D3342D5c385a87D264F90653733592000581", + "test_mixed_sload_sstore_HOGE": "0xfAd45E47083e4607302aa43c65fB3106F1cd7607", + "test_mixed_sload_sstore_REN": "0x408e41876cCCDC0F92210600ef50372656052a38", + "test_mixed_sload_sstore_ENS": "0xC56b13EBBCffa67cfB7979b900B736b3fb480D78", + "test_mixed_sload_sstore_NEXO": "0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206", + "test_mixed_sload_sstore_RFR": "0xD0929d411954c47438Dc1D871dd6081F5C5e149c", + "test_mixed_sload_sstore_COFI": "0x3137619705b5fc22a3048989F983905e456B59Ab", + "test_mixed_sload_sstore_SLP": "0xcc8fa225d80b9c7d42f96e9570156c65d6cAAa25", + "test_mixed_sload_sstore_FUEL": "0xea38eaa3c86c8f9b751533ba2e562deb9acded40", + "test_mixed_sload_sstore_ENA": "0x57e114B691Db790C35207b2e685D4A43181e6061", + "test_mixed_sload_sstore_AKITA": "0x3301Ee63Fb29F863f2333Bd4466acb46CD8323E6", + "test_mixed_sload_sstore_CVC": "0x41e5560054824ea6B0732E656e3Ad64E20e94e45", + "test_mixed_sload_sstore_IHT": "0xEda8B016efa8b1161208Cf041cD86972EEE0F31E", + "test_mixed_sload_sstore_ZSC": "0x7A41e0517a5ecA4FdbC7FbebA4D4c47B9fF6DC63", + "test_mixed_sload_sstore_cbETH": "0xBe9895146f7AF43049ca1c1AE358B0541Ea49704", + "test_mixed_sload_sstore_IMT": "0x13119e34e140097a507b07a5564bde1bc375d9e6" +} diff --git a/tests/benchmark/stateful/bloatnet/test_multi_opcode.py b/tests/benchmark/stateful/bloatnet/test_multi_opcode.py index 691d39b46c..0521462066 100755 --- a/tests/benchmark/stateful/bloatnet/test_multi_opcode.py +++ b/tests/benchmark/stateful/bloatnet/test_multi_opcode.py @@ -6,6 +6,10 @@ operations. """ +import json +import math +from pathlib import Path + import pytest from execution_testing import ( Account, @@ -13,15 +17,11 @@ Block, BlockchainTestFiller, Bytecode, - Create2PreimageLayout, Fork, Op, Transaction, While, ) -from execution_testing.cli.pytest_commands.plugins.execute.pre_alloc import ( - AddressStubs, -) REFERENCE_SPEC_GIT_PATH = "DUMMY/bloatnet.md" REFERENCE_SPEC_VERSION = "1.0" @@ -81,56 +81,23 @@ def test_bloatnet_balance_extcodesize( # Calculate gas costs intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - # Setup overhead (before loop): STATICCALL + result handling + memory setup - setup_overhead = ( - gas_costs.G_COLD_ACCOUNT_ACCESS # STATICCALL to factory (2600) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_VERY_LOW # PUSH2 (3) - + gas_costs.G_HIGH # JUMPI (10) - + gas_costs.G_VERY_LOW * 2 # MLOAD × 2 for factory results (3 * 2) - + gas_costs.G_VERY_LOW * 3 # MSTORE × 3 for memory setup (3 * 3) - + gas_costs.G_VERY_LOW # MSTORE8 for 0xFF prefix (3) - + gas_costs.G_VERY_LOW # PUSH1 for memory position (3) - ) - - # Cleanup overhead (after loop) - cleanup_overhead = gas_costs.G_BASE # POP counter (2) - - # While loop condition overhead per iteration - loop_condition_overhead = ( - gas_costs.G_VERY_LOW # DUP1 (3) - + gas_costs.G_VERY_LOW # PUSH1 (3) - + gas_costs.G_VERY_LOW # SWAP1 (3) - + gas_costs.G_VERY_LOW # SUB (3) - + gas_costs.G_VERY_LOW # DUP1 (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_HIGH # JUMPI (10) - ) - # Cost per contract access with CREATE2 address generation cost_per_contract = ( gas_costs.G_KECCAK_256 # SHA3 static cost for address generation (30) - + gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic (85 bytes = 3 words) + + gas_costs.G_KECCAK_256_WORD + * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6) + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold access (2600) + gas_costs.G_BASE # POP first result (2) + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm access (100) + gas_costs.G_BASE # POP second result (2) - + gas_costs.G_VERY_LOW # DUP1 before first op (3) - + gas_costs.G_VERY_LOW # MLOAD for salt (3) + + gas_costs.G_BASE # DUP1 before first op (3) + + gas_costs.G_VERY_LOW * 4 # PUSH1 operations (4 * 3) + + gas_costs.G_LOW # MLOAD for salt (3) + gas_costs.G_VERY_LOW # ADD for increment (3) - + gas_costs.G_VERY_LOW # MSTORE salt back (3) - + loop_condition_overhead # While loop condition + + gas_costs.G_LOW # MSTORE salt back (3) + + 10 # While loop overhead ) - # Calculate how many transactions we need to fill the block - num_txs = max(1, gas_benchmark_value // tx_gas_limit) - - # Calculate how many contracts to access per transaction - total_overhead = setup_overhead + cleanup_overhead - available_gas_per_tx = tx_gas_limit - intrinsic_gas - total_overhead - contracts_per_tx = int(available_gas_per_tx // cost_per_contract) - # Deploy factory using stub contract - NO HARDCODED VALUES # The stub "bloatnet_factory" must be provided via --address-stubs flag # The factory at that address MUST have: @@ -141,11 +108,21 @@ def test_bloatnet_balance_extcodesize( stub="bloatnet_factory", ) + # Calculate number of transactions needed (EIP-7825 compliance) + num_txs = max(1, math.ceil(gas_benchmark_value / tx_gas_limit)) + + # Calculate how many contracts to access based on available gas + total_available_gas = ( + gas_benchmark_value - (intrinsic_gas * num_txs) - 1000 + ) + total_contracts = int(total_available_gas // cost_per_contract) + contracts_per_tx = total_contracts // num_txs + # Log test requirements - deployed count read from factory storage print( - f"Tx gas limit: {tx_gas_limit / 1_000_000:.1f}M gas. " - f"Number of txs: {num_txs}. " - f"Contracts per tx: {contracts_per_tx}. " + f"Test needs {total_contracts} contracts for " + f"{gas_benchmark_value / 1_000_000:.1f}M gas " + f"across {num_txs} transaction(s). " f"Factory storage will be checked during execution." ) @@ -158,78 +135,100 @@ def test_bloatnet_balance_extcodesize( else (extcodesize_op + balance_op) ) - # Build attack contract that reads config from factory and performs attack - attack_code = ( - # Call getConfig() on factory to get num_deployed and init_code_hash - Op.STATICCALL( - gas=Op.GAS, - address=factory_address, - args_offset=0, - args_size=0, - ret_offset=96, - ret_size=64, + # Build transactions + txs = [] + post = {} + contracts_remaining = total_contracts + salt_offset = 0 + + for i in range(num_txs): + # Last tx gets remaining contracts + tx_contracts = ( + contracts_per_tx if i < num_txs - 1 else contracts_remaining ) - # Check if call succeeded - + Op.ISZERO - + Op.PUSH2(0x1000) # Jump to error handler if failed (far jump) - + Op.JUMPI - # Load results from memory - # Memory[96:128] = num_deployed_contracts - # Memory[128:160] = init_code_hash - + Op.MLOAD(96) # Load num_deployed_contracts to stack - + ( - create2_preimage := Create2PreimageLayout( - factory_address=factory_address, - salt=0, - init_code_hash=Op.MLOAD(128), + contracts_remaining -= tx_contracts + + # Build attack contract that reads config from factory + attack_code = ( + # Call getConfig() on factory to get config + Op.STATICCALL( + gas=Op.GAS, + address=factory_address, + args_offset=0, + args_size=0, + ret_offset=96, + ret_size=64, ) - ) - # Main attack loop - iterate through all deployed contracts - + While( - body=( - # Generate CREATE2 addr: keccak256(0xFF+factory+salt+hash) - # Hash CREATE2 address from memory - create2_preimage.address_op() - # The address is now on the stack - + Op.DUP1 # Duplicate for second operation - + benchmark_ops # Execute operations in specified order - # Increment salt for next iteration - + create2_preimage.increment_salt_op() - ), - # Continue while we haven't reached the limit - condition=Op.DUP1 - + Op.PUSH1(1) - + Op.SWAP1 - + Op.SUB - + Op.DUP1 + # Check if call succeeded + Op.ISZERO - + Op.ISZERO, + + Op.PUSH2(0x1000) # Jump to error handler if failed (far jump) + + Op.JUMPI + # Load results from memory + # Memory[96:128] = num_deployed_contracts + # Memory[128:160] = init_code_hash + + Op.MLOAD(128) # Load init_code_hash + # Setup memory for CREATE2 address generation + # Memory layout at 0: 0xFF + factory_addr(20) + salt(32) + hash(32) + + Op.MSTORE( + 0, factory_address + ) # Store factory address at memory position 0 + + Op.MSTORE8(11, 0xFF) # Store 0xFF prefix at byte 11 + + Op.MSTORE(32, salt_offset) # Store starting salt at position 32 + # Stack now has: [init_code_hash] + + Op.PUSH1(64) # Push memory position + + Op.MSTORE # Store init_code_hash at memory[64] + # Push our iteration count onto stack + + Op.PUSH4(tx_contracts) + # Main attack loop - iterate through contracts for this tx + + While( + body=( + # Generate CREATE2 addr: keccak256(0xFF+factory+salt+hash) + Op.SHA3(11, 85) # CREATE2 addr from memory[11:96] + # The address is now on the stack + + Op.DUP1 # Duplicate for second operation + + benchmark_ops # Execute operations in specified order + # Increment salt for next iteration + + Op.MSTORE( + 32, Op.ADD(Op.MLOAD(32), 1) + ) # Increment and store salt + ), + # Continue while we haven't reached the limit + condition=Op.DUP1 + + Op.PUSH1(1) + + Op.SWAP1 + + Op.SUB + + Op.DUP1 + + Op.ISZERO + + Op.ISZERO, + ) + + Op.POP # Clean up counter ) - + Op.POP # Clean up counter - ) - # Deploy attack contract - attack_address = pre.deploy_contract(code=attack_code) + # Deploy attack contract for this tx + attack_address = pre.deploy_contract(code=attack_code) + + # Calculate gas for this transaction + this_tx_gas = min( + tx_gas_limit, gas_benchmark_value - (i * tx_gas_limit) + ) - # Create multiple attack transactions to fill the block - sender = pre.fund_eoa() - attack_txs = [ - Transaction( - to=attack_address, - gas_limit=tx_gas_limit, - sender=sender, + txs.append( + Transaction( + to=attack_address, + gas_limit=this_tx_gas, + sender=pre.fund_eoa(), + ) ) - for _ in range(num_txs) - ] - # Post-state: just verify attack contract exists - post = { - attack_address: Account(storage={}), - } + # Add to post-state + post[attack_address] = Account(storage={}) + + # Update salt offset for next transaction + salt_offset += tx_contracts blockchain_test( pre=pre, - blocks=[Block(txs=attack_txs)], + blocks=[Block(txs=txs)], post=post, ) @@ -264,57 +263,24 @@ def test_bloatnet_balance_extcodecopy( # Calculate costs intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - # Setup overhead (before loop): STATICCALL + result handling + memory setup - setup_overhead = ( - gas_costs.G_COLD_ACCOUNT_ACCESS # STATICCALL to factory (2600) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_VERY_LOW # PUSH2 (3) - + gas_costs.G_HIGH # JUMPI (10) - + gas_costs.G_VERY_LOW * 2 # MLOAD × 2 for factory results (3 * 2) - + gas_costs.G_VERY_LOW * 3 # MSTORE × 3 for memory setup (3 * 3) - + gas_costs.G_VERY_LOW # MSTORE8 for 0xFF prefix (3) - + gas_costs.G_VERY_LOW # PUSH1 for memory position (3) - ) - - # Cleanup overhead (after loop) - cleanup_overhead = gas_costs.G_BASE # POP counter (2) - - # While loop condition overhead per iteration - loop_condition_overhead = ( - gas_costs.G_VERY_LOW # DUP1 (3) - + gas_costs.G_VERY_LOW # PUSH1 (3) - + gas_costs.G_VERY_LOW # SWAP1 (3) - + gas_costs.G_VERY_LOW # SUB (3) - + gas_costs.G_VERY_LOW # DUP1 (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_HIGH # JUMPI (10) - ) - # Cost per contract with EXTCODECOPY and CREATE2 address generation cost_per_contract = ( gas_costs.G_KECCAK_256 # SHA3 static cost for address generation (30) - + gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic (85 bytes = 3 words) + + gas_costs.G_KECCAK_256_WORD + * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6) + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold access (2600) + gas_costs.G_BASE # POP first result (2) + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm access base (100) + gas_costs.G_COPY * 1 # Copy cost for 1 byte (3) - + gas_costs.G_VERY_LOW * 2 # DUP1 + DUP4 for address (6) - + gas_costs.G_VERY_LOW * 2 # MLOAD for salt twice (6) + + gas_costs.G_BASE * 2 # DUP1 before first op, DUP4 for address (6) + + gas_costs.G_VERY_LOW * 8 # PUSH operations (8 * 3 = 24) + + gas_costs.G_LOW * 2 # MLOAD for salt twice (6) + gas_costs.G_VERY_LOW * 2 # ADD operations (6) - + gas_costs.G_VERY_LOW # MSTORE salt back (3) + + gas_costs.G_LOW # MSTORE salt back (3) + gas_costs.G_BASE # POP after second op (2) - + loop_condition_overhead # While loop condition + + 10 # While loop overhead ) - # Calculate how many transactions we need to fill the block - num_txs = max(1, gas_benchmark_value // tx_gas_limit) - - # Calculate how many contracts to access per transaction - total_overhead = setup_overhead + cleanup_overhead - available_gas_per_tx = tx_gas_limit - intrinsic_gas - total_overhead - contracts_per_tx = int(available_gas_per_tx // cost_per_contract) - # Deploy factory using stub contract - NO HARDCODED VALUES # The stub "bloatnet_factory" must be provided via --address-stubs flag # The factory at that address MUST have: @@ -325,11 +291,21 @@ def test_bloatnet_balance_extcodecopy( stub="bloatnet_factory", ) + # Calculate number of transactions needed (EIP-7825 compliance) + num_txs = max(1, math.ceil(gas_benchmark_value / tx_gas_limit)) + + # Calculate how many contracts to access + total_available_gas = ( + gas_benchmark_value - (intrinsic_gas * num_txs) - 1000 + ) + total_contracts = int(total_available_gas // cost_per_contract) + contracts_per_tx = total_contracts // num_txs + # Log test requirements - deployed count read from factory storage print( - f"Tx gas limit: {tx_gas_limit / 1_000_000:.1f}M gas. " - f"Number of txs: {num_txs}. " - f"Contracts per tx: {contracts_per_tx}. " + f"Test needs {total_contracts} contracts for " + f"{gas_benchmark_value / 1_000_000:.1f}M gas " + f"across {num_txs} transaction(s). " f"Factory storage will be checked during execution." ) @@ -349,77 +325,99 @@ def test_bloatnet_balance_extcodecopy( else (extcodecopy_op + balance_op) ) - # Build attack contract that reads config from factory and performs attack - attack_code = ( - # Call getConfig() on factory to get num_deployed and init_code_hash - Op.STATICCALL( - gas=Op.GAS, - address=factory_address, - args_offset=0, - args_size=0, - ret_offset=96, - ret_size=64, + # Build transactions + txs = [] + post = {} + contracts_remaining = total_contracts + salt_offset = 0 + + for i in range(num_txs): + # Last tx gets remaining contracts + tx_contracts = ( + contracts_per_tx if i < num_txs - 1 else contracts_remaining ) - # Check if call succeeded - + Op.ISZERO - + Op.PUSH2(0x1000) # Jump to error handler if failed (far jump) - + Op.JUMPI - # Load results from memory - # Memory[96:128] = num_deployed_contracts - # Memory[128:160] = init_code_hash - + Op.MLOAD(96) # Load num_deployed_contracts to stack - + ( - create2_preimage := Create2PreimageLayout( - factory_address=factory_address, - salt=0, - init_code_hash=Op.MLOAD(128), + contracts_remaining -= tx_contracts + + # Build attack contract that reads config from factory + attack_code = ( + # Call getConfig() on factory to get config + Op.STATICCALL( + gas=Op.GAS, + address=factory_address, + args_offset=0, + args_size=0, + ret_offset=96, + ret_size=64, ) - ) - # Main attack loop - iterate through all deployed contracts - + While( - body=( - # Hash CREATE2 address - create2_preimage.address_op() - # The address is now on the stack - + Op.DUP1 # Duplicate for later operations - + benchmark_ops # Execute operations in specified order - # Increment salt for next iteration - + create2_preimage.increment_salt_op() - ), - # Continue while counter > 0 - condition=Op.DUP1 - + Op.PUSH1(1) - + Op.SWAP1 - + Op.SUB - + Op.DUP1 + # Check if call succeeded + Op.ISZERO - + Op.ISZERO, + + Op.PUSH2(0x1000) # Jump to error handler if failed (far jump) + + Op.JUMPI + # Load results from memory + # Memory[128:160] = init_code_hash + + Op.MLOAD(128) # Load init_code_hash + # Setup memory for CREATE2 address generation + # Memory layout at 0: 0xFF + factory_addr(20) + salt(32) + hash(32) + + Op.MSTORE( + 0, factory_address + ) # Store factory address at memory position 0 + + Op.MSTORE8(11, 0xFF) # Store 0xFF prefix at byte 11 + + Op.MSTORE(32, salt_offset) # Store starting salt at position 32 + # Stack now has: [init_code_hash] + + Op.PUSH1(64) # Push memory position + + Op.MSTORE # Store init_code_hash at memory[64] + # Push our iteration count onto stack + + Op.PUSH4(tx_contracts) + # Main attack loop - iterate through contracts for this tx + + While( + body=( + # Generate CREATE2 address + Op.SHA3(11, 85) # CREATE2 addr from memory[11:96] + # The address is now on the stack + + Op.DUP1 # Duplicate for later operations + + benchmark_ops # Execute operations in specified order + # Increment salt for next iteration + + Op.MSTORE( + 32, Op.ADD(Op.MLOAD(32), 1) + ) # Increment and store salt + ), + # Continue while counter > 0 + condition=Op.DUP1 + + Op.PUSH1(1) + + Op.SWAP1 + + Op.SUB + + Op.DUP1 + + Op.ISZERO + + Op.ISZERO, + ) + + Op.POP # Clean up counter ) - + Op.POP # Clean up counter - ) - # Deploy attack contract - attack_address = pre.deploy_contract(code=attack_code) + # Deploy attack contract for this tx + attack_address = pre.deploy_contract(code=attack_code) - # Create multiple attack transactions to fill the block - sender = pre.fund_eoa() - attack_txs = [ - Transaction( - to=attack_address, - gas_limit=tx_gas_limit, - sender=sender, + # Calculate gas for this transaction + this_tx_gas = min( + tx_gas_limit, gas_benchmark_value - (i * tx_gas_limit) ) - for _ in range(num_txs) - ] - # Post-state - post = { - attack_address: Account(storage={}), - } + txs.append( + Transaction( + to=attack_address, + gas_limit=this_tx_gas, + sender=pre.fund_eoa(), + ) + ) + + # Add to post-state + post[attack_address] = Account(storage={}) + + # Update salt offset for next transaction + salt_offset += tx_contracts blockchain_test( pre=pre, - blocks=[Block(txs=attack_txs)], + blocks=[Block(txs=txs)], post=post, ) @@ -453,67 +451,44 @@ def test_bloatnet_balance_extcodehash( # Calculate gas costs intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - # Setup overhead (before loop): STATICCALL + result handling + memory setup - setup_overhead = ( - gas_costs.G_COLD_ACCOUNT_ACCESS # STATICCALL to factory (2600) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_VERY_LOW # PUSH2 (3) - + gas_costs.G_HIGH # JUMPI (10) - + gas_costs.G_VERY_LOW * 2 # MLOAD × 2 for factory results (3 * 2) - + gas_costs.G_VERY_LOW * 3 # MSTORE × 3 for memory setup (3 * 3) - + gas_costs.G_VERY_LOW # MSTORE8 for 0xFF prefix (3) - + gas_costs.G_VERY_LOW # PUSH1 for memory position (3) - ) - - # Cleanup overhead (after loop) - cleanup_overhead = gas_costs.G_BASE # POP counter (2) - - # While loop condition overhead per iteration - loop_condition_overhead = ( - gas_costs.G_VERY_LOW # DUP1 (3) - + gas_costs.G_VERY_LOW # PUSH1 (3) - + gas_costs.G_VERY_LOW # SWAP1 (3) - + gas_costs.G_VERY_LOW # SUB (3) - + gas_costs.G_VERY_LOW # DUP1 (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_HIGH # JUMPI (10) - ) - # Cost per contract access with CREATE2 address generation cost_per_contract = ( gas_costs.G_KECCAK_256 # SHA3 static cost for address generation (30) - + gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic (85 bytes = 3 words) + + gas_costs.G_KECCAK_256_WORD + * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6) + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold access (2600) + gas_costs.G_BASE # POP first result (2) + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm access (100) + gas_costs.G_BASE # POP second result (2) - + gas_costs.G_VERY_LOW # DUP1 before first op (3) - + gas_costs.G_VERY_LOW # MLOAD for salt (3) + + gas_costs.G_BASE # DUP1 before first op (3) + + gas_costs.G_VERY_LOW * 4 # PUSH1 operations (4 * 3) + + gas_costs.G_LOW # MLOAD for salt (3) + gas_costs.G_VERY_LOW # ADD for increment (3) - + gas_costs.G_VERY_LOW # MSTORE salt back (3) - + loop_condition_overhead # While loop condition + + gas_costs.G_LOW # MSTORE salt back (3) + + 10 # While loop overhead ) - # Calculate how many transactions we need to fill the block - num_txs = max(1, gas_benchmark_value // tx_gas_limit) - - # Calculate how many contracts to access per transaction - total_overhead = setup_overhead + cleanup_overhead - available_gas_per_tx = tx_gas_limit - intrinsic_gas - total_overhead - contracts_per_tx = int(available_gas_per_tx // cost_per_contract) - # Deploy factory using stub contract factory_address = pre.deploy_contract( code=Bytecode(), stub="bloatnet_factory", ) + # Calculate number of transactions needed (EIP-7825 compliance) + num_txs = max(1, math.ceil(gas_benchmark_value / tx_gas_limit)) + + # Calculate how many contracts to access based on available gas + total_available_gas = ( + gas_benchmark_value - (intrinsic_gas * num_txs) - 1000 + ) + total_contracts = int(total_available_gas // cost_per_contract) + contracts_per_tx = total_contracts // num_txs + # Log test requirements print( - f"Tx gas limit: {tx_gas_limit / 1_000_000:.1f}M gas. " - f"Number of txs: {num_txs}. " - f"Contracts per tx: {contracts_per_tx}. " + f"Test needs {total_contracts} contracts for " + f"{gas_benchmark_value / 1_000_000:.1f}M gas " + f"across {num_txs} transaction(s). " f"Factory storage will be checked during execution." ) @@ -526,73 +501,90 @@ def test_bloatnet_balance_extcodehash( else (extcodehash_op + balance_op) ) - # Build attack contract that reads config from factory and performs attack - attack_code = ( - # Call getConfig() on factory to get num_deployed and init_code_hash - Op.STATICCALL( - gas=Op.GAS, - address=factory_address, - args_offset=0, - args_size=0, - ret_offset=96, - ret_size=64, + # Build transactions + txs = [] + post = {} + contracts_remaining = total_contracts + salt_offset = 0 + + for i in range(num_txs): + # Last tx gets remaining contracts + tx_contracts = ( + contracts_per_tx if i < num_txs - 1 else contracts_remaining ) - # Check if call succeeded - + Op.ISZERO - + Op.PUSH2(0x1000) # Jump to error handler if failed - + Op.JUMPI - # Load results from memory - + Op.MLOAD(96) # Load num_deployed_contracts to stack - + ( - create2_preimage := Create2PreimageLayout( - factory_address=factory_address, - salt=0, - init_code_hash=Op.MLOAD(128), + contracts_remaining -= tx_contracts + + # Build attack contract that reads config from factory + attack_code = ( + # Call getConfig() on factory to get config + Op.STATICCALL( + gas=Op.GAS, + address=factory_address, + args_offset=0, + args_size=0, + ret_offset=96, + ret_size=64, ) - ) - # Main attack loop - + While( - body=( - # Hash CREATE2 address - create2_preimage.address_op() - + Op.DUP1 # Duplicate for second operation - + benchmark_ops # Execute operations in specified order - # Increment salt - + create2_preimage.increment_salt_op() - ), - condition=Op.DUP1 - + Op.PUSH1(1) - + Op.SWAP1 - + Op.SUB - + Op.DUP1 + # Check if call succeeded + Op.ISZERO - + Op.ISZERO, + + Op.PUSH2(0x1000) # Jump to error handler if failed + + Op.JUMPI + # Load results from memory + + Op.MLOAD(128) # Load init_code_hash + # Setup memory for CREATE2 address generation + + Op.MSTORE(0, factory_address) + + Op.MSTORE8(11, 0xFF) + + Op.MSTORE(32, salt_offset) # Starting salt for this tx + + Op.PUSH1(64) + + Op.MSTORE # Store init_code_hash + # Push our iteration count onto stack + + Op.PUSH4(tx_contracts) + # Main attack loop + + While( + body=( + # Generate CREATE2 address + Op.SHA3(11, 85) + + Op.DUP1 # Duplicate for second operation + + benchmark_ops # Execute operations in specified order + # Increment salt + + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) + ), + condition=Op.DUP1 + + Op.PUSH1(1) + + Op.SWAP1 + + Op.SUB + + Op.DUP1 + + Op.ISZERO + + Op.ISZERO, + ) + + Op.POP # Clean up counter ) - + Op.POP # Clean up counter - ) - # Deploy attack contract - attack_address = pre.deploy_contract(code=attack_code) + # Deploy attack contract for this tx + attack_address = pre.deploy_contract(code=attack_code) - # Create multiple attack transactions to fill the block - sender = pre.fund_eoa() - attack_txs = [ - Transaction( - to=attack_address, - gas_limit=tx_gas_limit, - sender=sender, + # Calculate gas for this transaction + this_tx_gas = min( + tx_gas_limit, gas_benchmark_value - (i * tx_gas_limit) ) - for _ in range(num_txs) - ] - # Post-state - post = { - attack_address: Account(storage={}), - } + txs.append( + Transaction( + to=attack_address, + gas_limit=this_tx_gas, + sender=pre.fund_eoa(), + ) + ) + + # Add to post-state + post[attack_address] = Account(storage={}) + + # Update salt offset for next transaction + salt_offset += tx_contracts blockchain_test( pre=pre, - blocks=[Block(txs=attack_txs)], + blocks=[Block(txs=txs)], post=post, ) @@ -601,9 +593,21 @@ def test_bloatnet_balance_extcodehash( BALANCEOF_SELECTOR = 0x70A08231 # balanceOf(address) APPROVE_SELECTOR = 0x095EA7B3 # approve(address,uint256) +# Load token names from stubs.json for test parametrization +_STUBS_FILE = Path(__file__).parent / "stubs.json" +with open(_STUBS_FILE) as f: + _STUBS = json.load(f) + +# Extract unique token names for mixed sload/sstore tests +MIXED_TOKENS = [ + k.replace("test_mixed_sload_sstore_", "") + for k in _STUBS.keys() + if k.startswith("test_mixed_sload_sstore_") +] + @pytest.mark.valid_from("Prague") -@pytest.mark.parametrize("num_contracts", [1, 5, 10, 20, 100]) +@pytest.mark.parametrize("token_name", MIXED_TOKENS) @pytest.mark.parametrize( "sload_percent,sstore_percent", [ @@ -620,74 +624,26 @@ def test_mixed_sload_sstore( fork: Fork, gas_benchmark_value: int, tx_gas_limit: int, - address_stubs: AddressStubs | None, - num_contracts: int, + token_name: str, sload_percent: int, sstore_percent: int, - request: pytest.FixtureRequest, ) -> None: """ BloatNet mixed SLOAD/SSTORE benchmark with configurable operation ratios. This test: - 1. Filters stubs matching test name prefix - (e.g., test_mixed_sload_sstore_*) - 2. Uses first N contracts based on num_contracts parameter - 3. Divides gas budget evenly across all selected contracts - 4. For each contract, divides gas into SLOAD and SSTORE portions by - percentage - 5. Executes balanceOf (SLOAD) and approve (SSTORE) calls per the ratio - 6. Stresses clients with combined read/write operations on large - contracts + 1. Uses a single ERC20 contract specified by token_name parameter + 2. Allocates full gas budget to that contract + 3. Divides gas into SLOAD and SSTORE portions by percentage + 4. Executes balanceOf (SLOAD) and approve (SSTORE) calls per the ratio + 5. Stresses clients with combined read/write operations on large contracts """ - # Extract test function name for stub filtering - # Remove parametrization suffix - test_name = request.node.name.split("[")[0] - - # Filter stubs that match the test name prefix - matching_stubs = [] - if address_stubs is not None: - matching_stubs = [ - stub_name - for stub_name in address_stubs.root.keys() - if stub_name.startswith(test_name) - ] - - # Validate we have enough stubs - if len(matching_stubs) < num_contracts: - pytest.fail( - f"Not enough matching stubs for test '{test_name}'. " - f"Required: {num_contracts}, Found: {len(matching_stubs)}. " - f"Matching stubs: {matching_stubs}" - ) - - # Select first N stubs - selected_stubs = matching_stubs[:num_contracts] + stub_name = f"test_mixed_sload_sstore_{token_name}" gas_costs = fork.gas_costs() # Calculate gas costs intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - # Per-contract fixed overhead (setup + teardown for each contract's loops) - # Each contract has two loops: SLOAD (balanceOf) and SSTORE (approve) - overhead_per_contract = ( - # SLOAD loop setup/teardown - gas_costs.G_VERY_LOW # MSTORE to initialize counter (3) - + gas_costs.G_JUMPDEST # JUMPDEST at loop start (1) - + gas_costs.G_VERY_LOW # MLOAD for While condition (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_HIGH # JUMPI (10) - # SSTORE loop setup/teardown - + gas_costs.G_VERY_LOW # MSTORE selector (3) - + gas_costs.G_VERY_LOW # MSTORE to initialize counter (3) - + gas_costs.G_JUMPDEST # JUMPDEST at loop start (1) - + gas_costs.G_VERY_LOW # MLOAD for While condition (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_HIGH # JUMPI (10) - ) - # Fixed overhead for SLOAD loop sload_loop_overhead = ( # Attack contract loop overhead @@ -695,16 +651,16 @@ def test_mixed_sload_sstore( + gas_costs.G_VERY_LOW * 2 # MSTORE selector (3*2) + gas_costs.G_VERY_LOW * 3 # MLOAD + MSTORE address (3*3) + gas_costs.G_BASE # POP (2) - + gas_costs.G_VERY_LOW * 3 # SUB + MLOAD + MSTORE decrement (3*3) - + gas_costs.G_VERY_LOW * 2 # ISZERO * 2 for loop condition (3*2) - + gas_costs.G_HIGH # JUMPI (10) + + gas_costs.G_BASE * 3 # SUB + MLOAD + MSTORE counter decrement + + gas_costs.G_BASE * 2 # ISZERO * 2 for loop condition (2*2) + + gas_costs.G_MID # JUMPI (8) ) # ERC20 balanceOf internal gas sload_erc20_internal = ( gas_costs.G_VERY_LOW # PUSH4 selector (3) + gas_costs.G_BASE # EQ selector match (2) - + gas_costs.G_HIGH # JUMPI to function (10) + + gas_costs.G_MID # JUMPI to function (8) + gas_costs.G_JUMPDEST # JUMPDEST at function start (1) + gas_costs.G_VERY_LOW * 2 # CALLDATALOAD arg (3*2) + gas_costs.G_KECCAK_256 # keccak256 static (30) @@ -717,19 +673,19 @@ def test_mixed_sload_sstore( sstore_loop_overhead = ( # Attack contract loop body operations gas_costs.G_VERY_LOW # MSTORE selector at memory[32] (3) - + gas_costs.G_VERY_LOW # MLOAD counter (3) + + gas_costs.G_LOW # MLOAD counter (5) + gas_costs.G_VERY_LOW # MSTORE spender at memory[64] (3) + gas_costs.G_BASE # POP call result (2) # Counter decrement - + gas_costs.G_VERY_LOW # MLOAD counter (3) + + gas_costs.G_LOW # MLOAD counter (5) + gas_costs.G_VERY_LOW # PUSH1 1 (3) + gas_costs.G_VERY_LOW # SUB (3) + gas_costs.G_VERY_LOW # MSTORE counter back (3) # While loop condition check - + gas_costs.G_VERY_LOW # MLOAD counter (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_HIGH # JUMPI back to loop start (10) + + gas_costs.G_LOW # MLOAD counter (5) + + gas_costs.G_BASE # ISZERO (2) + + gas_costs.G_BASE # ISZERO (2) + + gas_costs.G_MID # JUMPI back to loop start (8) ) # ERC20 approve internal gas @@ -737,7 +693,7 @@ def test_mixed_sload_sstore( sstore_erc20_internal = ( gas_costs.G_VERY_LOW # PUSH4 selector (3) + gas_costs.G_BASE # EQ selector match (2) - + gas_costs.G_HIGH # JUMPI to function (10) + + gas_costs.G_MID # JUMPI to function (8) + gas_costs.G_JUMPDEST # JUMPDEST at function start (1) + gas_costs.G_VERY_LOW # CALLDATALOAD spender (3) + gas_costs.G_VERY_LOW # CALLDATALOAD amount (3) @@ -752,20 +708,6 @@ def test_mixed_sload_sstore( + gas_costs.G_VERY_LOW # PUSH1 0 for return offset (3) ) - # Calculate how many transactions we need to fill the block - num_txs = max(1, gas_benchmark_value // tx_gas_limit) - - # Calculate gas budget per contract per transaction - total_overhead_per_tx = intrinsic_gas + ( - overhead_per_contract * num_contracts - ) - available_gas_per_tx = tx_gas_limit - total_overhead_per_tx - gas_per_contract_per_tx = available_gas_per_tx // num_contracts - - # For each contract, split gas by percentage - sload_gas_per_contract = (gas_per_contract_per_tx * sload_percent) // 100 - sstore_gas_per_contract = (gas_per_contract_per_tx * sstore_percent) // 100 - # Account for cold/warm transitions in CALL costs # First SLOAD call is COLD (2600), rest are WARM (100) sload_warm_cost = ( @@ -776,9 +718,6 @@ def test_mixed_sload_sstore( cold_warm_diff = ( gas_costs.G_COLD_ACCOUNT_ACCESS - gas_costs.G_WARM_ACCOUNT_ACCESS ) - sload_calls_per_contract = int( - (sload_gas_per_contract - cold_warm_diff) // sload_warm_cost - ) # First SSTORE call is COLD (2600), rest are WARM (100) sstore_warm_cost = ( @@ -786,49 +725,64 @@ def test_mixed_sload_sstore( + gas_costs.G_WARM_ACCOUNT_ACCESS + sstore_erc20_internal ) - sstore_calls_per_contract = int( - (sstore_gas_per_contract - cold_warm_diff) // sstore_warm_cost + + # Deploy ERC20 contract using stub + erc20_address = pre.deploy_contract( + code=Bytecode(), + stub=stub_name, ) - # Deploy selected ERC20 contracts using stubs - erc20_addresses = [] - for stub_name in selected_stubs: - addr = pre.deploy_contract( - code=Bytecode(), - stub=stub_name, - ) - erc20_addresses.append(addr) + # Calculate number of transactions needed (EIP-7825 compliance) + num_txs = max(1, math.ceil(gas_benchmark_value / tx_gas_limit)) + + # Calculate total available gas and split by percentage + total_available_gas = gas_benchmark_value - (intrinsic_gas * num_txs) + sload_gas = (total_available_gas * sload_percent) // 100 + sstore_gas = (total_available_gas * sstore_percent) // 100 + + # Calculate total calls for each operation type + total_sload_calls = int((sload_gas - cold_warm_diff) // sload_warm_cost) + total_sstore_calls = int((sstore_gas - cold_warm_diff) // sstore_warm_cost) + + # Distribute calls across transactions + sload_calls_per_tx = total_sload_calls // num_txs + sstore_calls_per_tx = total_sstore_calls // num_txs # Log test requirements print( - f"Total gas budget: {gas_benchmark_value / 1_000_000:.1f}M gas. " - f"Tx gas limit: {tx_gas_limit / 1_000_000:.1f}M gas. " - f"Number of txs: {num_txs}. " - f"~{gas_per_contract_per_tx / 1_000_000:.2f}M gas per contract per tx " + f"Token: {token_name}, " + f"Total gas budget: {gas_benchmark_value / 1_000_000:.1f}M gas " f"({sload_percent}% SLOAD, {sstore_percent}% SSTORE). " - f"Per contract per tx: {sload_calls_per_contract} balanceOf calls, " - f"{sstore_calls_per_contract} approve calls." + f"{total_sload_calls} balanceOf, {total_sstore_calls} approve " + f"across {num_txs} tx(s)." ) - # Build attack code that loops through each contract - attack_code: Bytecode = ( - Op.JUMPDEST # Entry point - # Store selector once for all contracts - + Op.MSTORE(offset=0, value=BALANCEOF_SELECTOR) - ) + # Build transactions + txs = [] + post = {} + sload_remaining = total_sload_calls + sstore_remaining = total_sstore_calls - for erc20_address in erc20_addresses: - # For each contract, execute SLOAD operations (balanceOf) - attack_code += ( - # Initialize counter in memory[32] = number of balanceOf calls - Op.MSTORE(offset=32, value=sload_calls_per_contract) - # Loop for balanceOf calls + for i in range(num_txs): + # Last tx gets remaining calls + tx_sload_calls = ( + sload_calls_per_tx if i < num_txs - 1 else sload_remaining + ) + tx_sstore_calls = ( + sstore_calls_per_tx if i < num_txs - 1 else sstore_remaining + ) + sload_remaining -= tx_sload_calls + sstore_remaining -= tx_sstore_calls + + # Build attack code for this transaction + attack_code: Bytecode = ( + Op.JUMPDEST # Entry point + + Op.MSTORE(offset=0, value=BALANCEOF_SELECTOR) + # SLOAD operations (balanceOf) + + Op.MSTORE(offset=32, value=tx_sload_calls) + While( condition=Op.MLOAD(32) + Op.ISZERO + Op.ISZERO, body=( - # Call balanceOf(address) on ERC20 contract - # args_offset=28 reads: selector from MEM[28:32] + address - # from MEM[32:64] Op.CALL( address=erc20_address, value=0, @@ -837,32 +791,17 @@ def test_mixed_sload_sstore( ret_offset=0, ret_size=0, ) - + Op.POP # Discard CALL success status - # Decrement counter + + Op.POP + Op.MSTORE(offset=32, value=Op.SUB(Op.MLOAD(32), 1)) ), ) - ) - - # For each contract, execute SSTORE operations (approve) - # Reuse the same memory layout as balanceOf - attack_code += ( - # Store approve selector at memory[0] (reusing same slot) - Op.MSTORE(offset=0, value=APPROVE_SELECTOR) - # Initialize counter in memory[32] = number of approve calls - # (reusing same slot) - + Op.MSTORE(offset=32, value=sstore_calls_per_contract) - # Loop for approve calls + # SSTORE operations (approve) + + Op.MSTORE(offset=0, value=APPROVE_SELECTOR) + + Op.MSTORE(offset=32, value=tx_sstore_calls) + While( condition=Op.MLOAD(32) + Op.ISZERO + Op.ISZERO, body=( - # Store spender at memory[64] (counter as spender/amount) Op.MSTORE(offset=64, value=Op.MLOAD(32)) - # Call approve(spender, amount) on ERC20 contract - # args_offset=28 reads: selector from MEM[28:32] + - # spender from MEM[32:64] + amount from MEM[64:96] - # Note: counter at MEM[32:64] is reused as spender, - # and value at MEM[64:96] serves as the amount + Op.CALL( address=erc20_address, value=0, @@ -871,34 +810,33 @@ def test_mixed_sload_sstore( ret_offset=0, ret_size=0, ) - + Op.POP # Discard CALL success status - # Decrement counter + + Op.POP + Op.MSTORE(offset=32, value=Op.SUB(Op.MLOAD(32), 1)) ), ) ) - # Deploy attack contract - attack_address = pre.deploy_contract(code=attack_code) + # Deploy attack contract for this tx + attack_address = pre.deploy_contract(code=attack_code) - # Create multiple attack transactions to fill the block - sender = pre.fund_eoa() - attack_txs = [ - Transaction( - to=attack_address, - gas_limit=tx_gas_limit, - sender=sender, + # Calculate gas for this transaction + this_tx_gas = min( + tx_gas_limit, gas_benchmark_value - (i * tx_gas_limit) + ) + + txs.append( + Transaction( + to=attack_address, + gas_limit=this_tx_gas, + sender=pre.fund_eoa(), + ) ) - for _ in range(num_txs) - ] - # Post-state - post = { - attack_address: Account(storage={}), - } + # Add to post-state + post[attack_address] = Account(storage={}) blockchain_test( pre=pre, - blocks=[Block(txs=attack_txs)], + blocks=[Block(txs=txs)], post=post, ) diff --git a/tests/benchmark/stateful/bloatnet/test_single_opcode.py b/tests/benchmark/stateful/bloatnet/test_single_opcode.py index 04dc629a80..664ba48db0 100644 --- a/tests/benchmark/stateful/bloatnet/test_single_opcode.py +++ b/tests/benchmark/stateful/bloatnet/test_single_opcode.py @@ -7,6 +7,10 @@ to benchmark specific state-handling bottlenecks. """ +import json +import math +from pathlib import Path + import pytest from execution_testing import ( Account, @@ -19,9 +23,6 @@ Transaction, While, ) -from execution_testing.cli.pytest_commands.plugins.execute.pre_alloc import ( - AddressStubs, -) REFERENCE_SPEC_GIT_PATH = "DUMMY/bloatnet.md" REFERENCE_SPEC_VERSION = "1.0" @@ -31,6 +32,23 @@ APPROVE_SELECTOR = 0x095EA7B3 # approve(address,uint256) ALLOWANCE_SELECTOR = 0xDD62ED3E # allowance(address,address) +# Load token names from stubs.json for test parametrization +_STUBS_FILE = Path(__file__).parent / "stubs.json" +with open(_STUBS_FILE) as f: + _STUBS = json.load(f) + +# Extract unique token names for each test type +SLOAD_TOKENS = [ + k.replace("test_sload_empty_erc20_balanceof_", "") + for k in _STUBS.keys() + if k.startswith("test_sload_empty_erc20_balanceof_") +] +SSTORE_TOKENS = [ + k.replace("test_sstore_erc20_approve_", "") + for k in _STUBS.keys() + if k.startswith("test_sstore_erc20_approve_") +] + # SLOAD BENCHMARK ARCHITECTURE: # @@ -78,68 +96,33 @@ @pytest.mark.valid_from("Prague") -@pytest.mark.parametrize("num_contracts", [1, 5, 10, 20, 100]) +@pytest.mark.parametrize("token_name", SLOAD_TOKENS) def test_sload_empty_erc20_balanceof( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, gas_benchmark_value: int, tx_gas_limit: int, - address_stubs: AddressStubs | None, - num_contracts: int, - request: pytest.FixtureRequest, + token_name: str, ) -> None: """ BloatNet SLOAD benchmark using ERC20 balanceOf queries on random addresses. This test: - 1. Filters stubs matching test name prefix - (e.g., test_sload_empty_erc20_balanceof_*) - 2. Uses first N contracts based on num_contracts parameter - 3. Splits gas budget evenly across the selected contracts - 4. Queries balanceOf() incrementally starting by 0 and increasing by 1 + 1. Uses a single ERC20 contract specified by token_name parameter + 2. Allocates full gas budget to that contract + 3. Queries balanceOf() incrementally starting by 0 and increasing by 1 (thus forcing SLOADs to non-existing addresses) + 4. Splits into multiple transactions if gas_benchmark_value > tx_gas_limit + (EIP-7825 compliance) """ - # Extract test function name for stub filtering - # Remove parametrization suffix - test_name = request.node.name.split("[")[0] - - # Filter stubs that match the test name prefix - matching_stubs = [] - if address_stubs is not None: - matching_stubs = [ - stub_name - for stub_name in address_stubs.root.keys() - if stub_name.startswith(test_name) - ] - - # Validate we have enough stubs - if len(matching_stubs) < num_contracts: - pytest.fail( - f"Not enough matching stubs for test '{test_name}'. " - f"Required: {num_contracts}, Found: {len(matching_stubs)}. " - f"Matching stubs: {matching_stubs}" - ) - - # Select first N stubs - selected_stubs = matching_stubs[:num_contracts] + stub_name = f"test_sload_empty_erc20_balanceof_{token_name}" gas_costs = fork.gas_costs() # Calculate gas costs intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - # Per-contract fixed overhead (setup + teardown for each contract's loop) - overhead_per_contract = ( - gas_costs.G_VERY_LOW # MSTORE to initialize counter (3) - + gas_costs.G_JUMPDEST # JUMPDEST at loop start (1) - + gas_costs.G_VERY_LOW # MLOAD for While condition check (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_HIGH # JUMPI (10) - + gas_costs.G_BASE # POP to clean up counter at end (2) - ) - # Fixed overhead per iteration (loop mechanics, independent of warm/cold) loop_overhead = ( # Attack contract loop overhead @@ -147,16 +130,16 @@ def test_sload_empty_erc20_balanceof( + gas_costs.G_VERY_LOW * 2 # MSTORE selector (3*2) + gas_costs.G_VERY_LOW * 3 # MLOAD + MSTORE address (3*3) + gas_costs.G_BASE # POP (2) - + gas_costs.G_VERY_LOW * 3 # SUB + MLOAD + MSTORE decrement (3*3) - + gas_costs.G_VERY_LOW * 2 # ISZERO * 2 for loop condition (3*2) - + gas_costs.G_HIGH # JUMPI (10) + + gas_costs.G_BASE * 3 # SUB + MLOAD + MSTORE counter decrement + + gas_costs.G_BASE * 2 # ISZERO * 2 for loop condition (2*2) + + gas_costs.G_MID # JUMPI (8) ) # ERC20 internal gas (same for all calls) erc20_internal_gas = ( gas_costs.G_VERY_LOW # PUSH4 selector (3) + gas_costs.G_BASE # EQ selector match (2) - + gas_costs.G_HIGH # JUMPI to function (10) + + gas_costs.G_MID # JUMPI to function (8) + gas_costs.G_JUMPDEST # JUMPDEST at function start (1) + gas_costs.G_VERY_LOW * 2 # CALLDATALOAD arg (3*2) + gas_costs.G_KECCAK_256 # keccak256 static (30) @@ -166,7 +149,7 @@ def test_sload_empty_erc20_balanceof( # RETURN costs 0 gas ) - # For each contract: first call is COLD (2600), subsequent are WARM (100) + # First call is COLD (2600), subsequent are WARM (100) warm_call_cost = ( loop_overhead + gas_costs.G_WARM_ACCOUNT_ACCESS + erc20_internal_gas ) @@ -174,65 +157,47 @@ def test_sload_empty_erc20_balanceof( gas_costs.G_COLD_ACCOUNT_ACCESS - gas_costs.G_WARM_ACCOUNT_ACCESS ) - # Calculate how many transactions we need to fill the block - num_txs = max(1, gas_benchmark_value // tx_gas_limit) - - # Calculate gas budget per contract per transaction - total_overhead_per_tx = intrinsic_gas + ( - overhead_per_contract * num_contracts - ) - available_gas_per_tx = tx_gas_limit - total_overhead_per_tx - gas_per_contract_per_tx = available_gas_per_tx // num_contracts - - # Solve for calls_per_contract per tx: - # gas_per_contract_per_tx = cold_call + (calls-1) * warm_call - # Simplifies to: gas = cold_warm_diff + calls * warm_call_cost - calls_per_contract = int( - (gas_per_contract_per_tx - cold_warm_diff) // warm_call_cost + # Deploy ERC20 contract using stub + # In execute mode: stub points to already-deployed contract on chain + # In fill mode: empty bytecode is deployed as placeholder + erc20_address = pre.deploy_contract( + code=Bytecode(), + stub=stub_name, ) - # Deploy selected ERC20 contracts using stubs - # In execute mode: stubs point to already-deployed contracts on chain - # In fill mode: empty bytecode is deployed as placeholder - erc20_addresses = [] - for stub_name in selected_stubs: - addr = pre.deploy_contract( - # Required parameter, ignored for stubs in execute mode - code=Bytecode(), - stub=stub_name, - ) - erc20_addresses.append(addr) + # Calculate number of transactions needed (EIP-7825 compliance) + num_txs = max(1, math.ceil(gas_benchmark_value / tx_gas_limit)) + + # Calculate total calls based on full gas budget + total_available_gas = gas_benchmark_value - (intrinsic_gas * num_txs) + total_calls = int((total_available_gas - cold_warm_diff) // warm_call_cost) + calls_per_tx = total_calls // num_txs # Log test requirements print( - f"Total gas budget: {gas_benchmark_value / 1_000_000:.1f}M gas. " - f"Tx gas limit: {tx_gas_limit / 1_000_000:.1f}M gas. " - f"Number of txs: {num_txs}. " - f"Overhead per contract: {overhead_per_contract}. " - f"~{gas_per_contract_per_tx / 1_000_000:.2f}M gas/contract/tx, " - f"{calls_per_contract} balanceOf calls/contract/tx." - ) - - # Build attack code that loops through each contract - attack_code: Bytecode = ( - Op.JUMPDEST # Entry point - # Store selector once for all contracts - + Op.MSTORE(offset=0, value=BALANCEOF_SELECTOR) + f"Token: {token_name}, " + f"Total gas budget: {gas_benchmark_value / 1_000_000:.1f}M gas, " + f"{total_calls} balanceOf calls across {num_txs} transaction(s)." ) - for erc20_address in erc20_addresses: - # For each contract, initialize counter and loop - attack_code += ( - # Initialize counter in memory[32] = number of calls - Op.MSTORE(offset=32, value=calls_per_contract) - # Loop for this specific contract + # Build transactions + txs = [] + post = {} + calls_remaining = total_calls + + for i in range(num_txs): + # Last tx gets remaining calls + tx_calls = calls_per_tx if i < num_txs - 1 else calls_remaining + calls_remaining -= tx_calls + + # Build attack code for this transaction + attack_code: Bytecode = ( + Op.JUMPDEST # Entry point + + Op.MSTORE(offset=0, value=BALANCEOF_SELECTOR) + + Op.MSTORE(offset=32, value=tx_calls) + While( - # Continue while counter > 0 condition=Op.MLOAD(32) + Op.ISZERO + Op.ISZERO, body=( - # Call balanceOf(address) on ERC20 contract - # args_offset=28 reads: selector from MEM[28:32] + address - # from MEM[32:64] Op.CALL( address=erc20_address, value=0, @@ -241,120 +206,82 @@ def test_sload_empty_erc20_balanceof( ret_offset=0, ret_size=0, ) - + Op.POP # Discard CALL success status - # Decrement counter: counter - 1 + + Op.POP + Op.MSTORE(offset=32, value=Op.SUB(Op.MLOAD(32), 1)) ), ) ) - # Deploy attack contract - attack_address = pre.deploy_contract(code=attack_code) + # Deploy attack contract for this tx + attack_address = pre.deploy_contract(code=attack_code) + + # Calculate gas for this transaction + this_tx_gas = min( + tx_gas_limit, gas_benchmark_value - (i * tx_gas_limit) + ) - # Create multiple attack transactions to fill the block - sender = pre.fund_eoa() - attack_txs = [ - Transaction( - to=attack_address, - gas_limit=tx_gas_limit, - sender=sender, + txs.append( + Transaction( + to=attack_address, + gas_limit=this_tx_gas, + sender=pre.fund_eoa(), + ) ) - for _ in range(num_txs) - ] - # Post-state - post = { - attack_address: Account(storage={}), - } + # Add to post-state + post[attack_address] = Account(storage={}) blockchain_test( pre=pre, - blocks=[Block(txs=attack_txs)], + blocks=[Block(txs=txs)], post=post, ) @pytest.mark.valid_from("Prague") -@pytest.mark.parametrize("num_contracts", [1, 5, 10, 20, 100]) +@pytest.mark.parametrize("token_name", SSTORE_TOKENS) def test_sstore_erc20_approve( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, gas_benchmark_value: int, tx_gas_limit: int, - address_stubs: AddressStubs | None, - num_contracts: int, - request: pytest.FixtureRequest, + token_name: str, ) -> None: """ BloatNet SSTORE benchmark using ERC20 approve to write to storage. This test: - 1. Filters stubs matching test name prefix - (e.g., test_sstore_erc20_approve_*) - 2. Uses first N contracts based on num_contracts parameter - 3. Splits gas budget evenly across the selected contracts - 4. Calls approve(spender, amount) incrementally (counter as spender) - 5. Forces SSTOREs to allowance mapping storage slots + 1. Uses a single ERC20 contract specified by token_name parameter + 2. Allocates full gas budget to that contract + 3. Calls approve(spender, amount) incrementally (counter as spender) + 4. Forces SSTOREs to allowance mapping storage slots + 5. Splits into multiple transactions if gas_benchmark_value > tx_gas_limit + (EIP-7825 compliance) """ - # Extract test function name for stub filtering - # Remove parametrization suffix - test_name = request.node.name.split("[")[0] - - # Filter stubs that match the test name prefix - matching_stubs = [] - if address_stubs is not None: - matching_stubs = [ - stub_name - for stub_name in address_stubs.root.keys() - if stub_name.startswith(test_name) - ] - - # Validate we have enough stubs - if len(matching_stubs) < num_contracts: - pytest.fail( - f"Not enough matching stubs for test '{test_name}'. " - f"Required: {num_contracts}, Found: {len(matching_stubs)}. " - f"Matching stubs: {matching_stubs}" - ) - - # Select first N stubs - selected_stubs = matching_stubs[:num_contracts] + stub_name = f"test_sstore_erc20_approve_{token_name}" gas_costs = fork.gas_costs() # Calculate gas costs intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - # Per-contract fixed overhead (setup + teardown) - memory_expansion_cost = 15 # Memory expansion to 160 bytes (5 words) - overhead_per_contract = ( - gas_costs.G_VERY_LOW # MSTORE to initialize counter (3) - + memory_expansion_cost # Memory expansion (15) - + gas_costs.G_JUMPDEST # JUMPDEST at loop start (1) - + gas_costs.G_VERY_LOW # MLOAD for While condition check (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_HIGH # JUMPI (10) - + gas_costs.G_BASE # POP to clean up counter at end (2) - ) # = 40 - # Fixed overhead per iteration (loop mechanics, independent of warm/cold) loop_overhead = ( # Attack contract loop body operations gas_costs.G_VERY_LOW # MSTORE selector at memory[32] (3) - + gas_costs.G_VERY_LOW # MLOAD counter (3) + + gas_costs.G_LOW # MLOAD counter (5) + gas_costs.G_VERY_LOW # MSTORE spender at memory[64] (3) + gas_costs.G_BASE # POP call result (2) # Counter decrement: MSTORE(0, SUB(MLOAD(0), 1)) - + gas_costs.G_VERY_LOW # MLOAD counter (3) + + gas_costs.G_LOW # MLOAD counter (5) + gas_costs.G_VERY_LOW # PUSH1 1 (3) + gas_costs.G_VERY_LOW # SUB (3) + gas_costs.G_VERY_LOW # MSTORE counter back (3) # While loop condition check - + gas_costs.G_VERY_LOW # MLOAD counter (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_VERY_LOW # ISZERO (3) - + gas_costs.G_HIGH # JUMPI back to loop start (10) + + gas_costs.G_LOW # MLOAD counter (5) + + gas_costs.G_BASE # ISZERO (2) + + gas_costs.G_BASE # ISZERO (2) + + gas_costs.G_MID # JUMPI back to loop start (8) ) # ERC20 internal gas (same for all calls) @@ -363,7 +290,7 @@ def test_sstore_erc20_approve( erc20_internal_gas = ( gas_costs.G_VERY_LOW # PUSH4 selector (3) + gas_costs.G_BASE # EQ selector match (2) - + gas_costs.G_HIGH # JUMPI to function (10) + + gas_costs.G_MID # JUMPI to function (8) + gas_costs.G_JUMPDEST # JUMPDEST at function start (1) + gas_costs.G_VERY_LOW # CALLDATALOAD spender (3) + gas_costs.G_VERY_LOW # CALLDATALOAD amount (3) @@ -379,8 +306,7 @@ def test_sstore_erc20_approve( # RETURN costs 0 gas ) - # For each contract: first call is COLD (2600), subsequent are WARM (100) - # Solve for calls per contract accounting for cold/warm transition + # First call is COLD (2600), subsequent are WARM (100) warm_call_cost = ( loop_overhead + gas_costs.G_WARM_ACCOUNT_ACCESS + erc20_internal_gas ) @@ -388,65 +314,48 @@ def test_sstore_erc20_approve( gas_costs.G_COLD_ACCOUNT_ACCESS - gas_costs.G_WARM_ACCOUNT_ACCESS ) - # Calculate how many transactions we need to fill the block - num_txs = max(1, gas_benchmark_value // tx_gas_limit) - - # Calculate gas budget per contract per transaction - total_overhead_per_tx = intrinsic_gas + ( - overhead_per_contract * num_contracts + # Deploy ERC20 contract using stub + erc20_address = pre.deploy_contract( + code=Bytecode(), + stub=stub_name, ) - available_gas_per_tx = tx_gas_limit - total_overhead_per_tx - gas_per_contract_per_tx = available_gas_per_tx // num_contracts - # Per contract per tx: gas = cold_warm_diff + calls * warm_call_cost - calls_per_contract = int( - (gas_per_contract_per_tx - cold_warm_diff) // warm_call_cost - ) + # Calculate number of transactions needed (EIP-7825 compliance) + num_txs = max(1, math.ceil(gas_benchmark_value / tx_gas_limit)) - # Deploy selected ERC20 contracts using stubs - erc20_addresses = [] - for stub_name in selected_stubs: - addr = pre.deploy_contract( - code=Bytecode(), - stub=stub_name, - ) - erc20_addresses.append(addr) + # Calculate total calls based on full gas budget + total_available_gas = gas_benchmark_value - (intrinsic_gas * num_txs) + total_calls = int((total_available_gas - cold_warm_diff) // warm_call_cost) + calls_per_tx = total_calls // num_txs # Log test requirements print( - f"Total gas budget: {gas_benchmark_value / 1_000_000:.1f}M gas. " - f"Tx gas limit: {tx_gas_limit / 1_000_000:.1f}M gas. " - f"Number of txs: {num_txs}. " - f"Overhead per contract: {overhead_per_contract}, " - f"Warm call cost: {warm_call_cost}. " - f"{calls_per_contract} approve calls per contract per tx " - f"({num_contracts} contracts)." - ) - - # Build attack code that loops through each contract - attack_code: Bytecode = ( - Op.JUMPDEST # Entry point - # Store selector once for all contracts - + Op.MSTORE(offset=0, value=APPROVE_SELECTOR) + f"Token: {token_name}, " + f"Total gas budget: {gas_benchmark_value / 1_000_000:.1f}M gas, " + f"{total_calls} approve calls across {num_txs} transaction(s)." ) - for erc20_address in erc20_addresses: - # For each contract, initialize counter and loop - attack_code += ( - # Initialize counter in memory[32] = number of calls - Op.MSTORE(offset=32, value=calls_per_contract) - # Loop for this specific contract + # Build transactions + txs = [] + post = {} + calls_remaining = total_calls + + for i in range(num_txs): + # Last tx gets remaining calls + tx_calls = calls_per_tx if i < num_txs - 1 else calls_remaining + calls_remaining -= tx_calls + + # Build attack code for this transaction + attack_code: Bytecode = ( + Op.JUMPDEST # Entry point + + Op.MSTORE(offset=0, value=APPROVE_SELECTOR) + + Op.MSTORE(offset=32, value=tx_calls) + While( - # Continue while counter > 0 condition=Op.MLOAD(32) + Op.ISZERO + Op.ISZERO, body=( # Store spender at memory[64] (counter as spender/amount) Op.MSTORE(offset=64, value=Op.MLOAD(32)) # Call approve(spender, amount) on ERC20 contract - # args_offset=28 reads: selector from MEM[28:32] + - # spender from MEM[32:64] + amount from MEM[64:96] - # Note: counter at MEM[32:64] is reused as spender, - # and value at MEM[64:96] serves as the amount + Op.CALL( address=erc20_address, value=0, @@ -455,34 +364,33 @@ def test_sstore_erc20_approve( ret_offset=0, ret_size=0, ) - + Op.POP # Discard CALL success status - # Decrement counter + + Op.POP + Op.MSTORE(offset=32, value=Op.SUB(Op.MLOAD(32), 1)) ), ) ) - # Deploy attack contract - attack_address = pre.deploy_contract(code=attack_code) + # Deploy attack contract for this tx + attack_address = pre.deploy_contract(code=attack_code) - # Create multiple attack transactions to fill the block - sender = pre.fund_eoa() - attack_txs = [ - Transaction( - to=attack_address, - gas_limit=tx_gas_limit, - sender=sender, + # Calculate gas for this transaction + this_tx_gas = min( + tx_gas_limit, gas_benchmark_value - (i * tx_gas_limit) + ) + + txs.append( + Transaction( + to=attack_address, + gas_limit=this_tx_gas, + sender=pre.fund_eoa(), + ) ) - for _ in range(num_txs) - ] - # Post-state - post = { - attack_address: Account(storage={}), - } + # Add to post-state + post[attack_address] = Account(storage={}) blockchain_test( pre=pre, - blocks=[Block(txs=attack_txs)], + blocks=[Block(txs=txs)], post=post, ) diff --git a/tox.ini b/tox.ini index 36f8fc652d..1eb80d0d5e 100644 --- a/tox.ini +++ b/tox.ini @@ -156,7 +156,7 @@ commands = --evm-bin={env:EVM_BIN:evmone-t8n} \ --gas-benchmark-values 1 \ --generate-pre-alloc-groups \ - --fork Prague \ + --fork Osaka \ -m "benchmark and not slow" \ -n auto --maxprocesses 10 --dist=loadgroup \ --basetemp="{temp_dir}/pytest" \