From fa5ec8cb8b16ca509b484945d1969668bb672c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E4=BD=B3=E8=AA=A0=20Louis=20Tsai?= <72684086+LouisTsai-Csie@users.noreply.github.com> Date: Fri, 27 Feb 2026 04:53:22 +0800 Subject: [PATCH 1/2] refactor: optimize execute remote with pooling (#2330) --- packages/testing/src/execution_testing/rpc/rpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/testing/src/execution_testing/rpc/rpc.py b/packages/testing/src/execution_testing/rpc/rpc.py index 677f2905d2..8bd603bbc0 100644 --- a/packages/testing/src/execution_testing/rpc/rpc.py +++ b/packages/testing/src/execution_testing/rpc/rpc.py @@ -178,6 +178,7 @@ def __init__( self.url = url self.request_id_counter = count(1) self.response_validation_context = response_validation_context + self.session = requests.Session() def __init_subclass__(cls, namespace: str | None = None) -> None: """ @@ -218,7 +219,7 @@ def _make_request( application-level issues rather than transient network problems """ logger.debug(f"Making HTTP request to {url}, timeout={timeout}") - return requests.post( + return self.session.post( url, json=json_payload, headers=headers, timeout=timeout ) From 77519e9123ccf59d8da680a24b3432ffbd642cc2 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Fri, 27 Feb 2026 01:00:35 +0000 Subject: [PATCH 2/2] feat(benchmarks): SLOAD/SSTORE tests for ERC20 bloated contracts cached/non-cached with existing/non-existing keys (#2327) * feat(benchmarks): target bloated contracts for existing slots * feat(benchmarks): add cached variant * TO BE VERIFIED: (claude): add mint test for storage existing/non-existing cached/non-cached * fix(benchmarks): tweaks to claudes code * fix(benchmarks): format * feat(benchmarks): introduce cache strategy * feat(tests): add cache strategy to mint test erc20 * fix: linting issue * fix: linting issue * Apply suggestions from code review * Apply suggestion from @marioevz * fix dispatch stack --------- Co-authored-by: LouisTsai Co-authored-by: Mario Vega --- .../stateful/bloatnet/stubs_bloatnet.json | 1 + .../stateful/bloatnet/test_single_opcode.py | 338 ++++++++++++++++-- tests/benchmark/stateful/helpers.py | 19 + 3 files changed, 330 insertions(+), 28 deletions(-) diff --git a/tests/benchmark/stateful/bloatnet/stubs_bloatnet.json b/tests/benchmark/stateful/bloatnet/stubs_bloatnet.json index 1aa4555bbe..65b7355eef 100644 --- a/tests/benchmark/stateful/bloatnet/stubs_bloatnet.json +++ b/tests/benchmark/stateful/bloatnet/stubs_bloatnet.json @@ -3,6 +3,7 @@ "test_sload_empty_erc20_balanceof_XEN": "0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8", "test_sload_empty_erc20_balanceof_USDC": "0xA0b86991C6218B36c1d19D4a2E9Eb0CE3606EB48", "test_sload_empty_erc20_balanceof_IMT": "0x13119e34e140097a507b07a5564bde1bc375d9e6", + "test_sstore_erc20_mint_30GB_ERC20": "0x19fc17d87D946BBA47ca276f7b06Ee5737c4679C", "test_sstore_erc20_approve_30GB_ERC20": "0x19fc17d87D946BBA47ca276f7b06Ee5737c4679C", "test_sstore_erc20_approve_XEN": "0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8", "test_sstore_erc20_approve_USDC": "0xA0b86991C6218B36c1d19D4a2E9Eb0CE3606EB48", diff --git a/tests/benchmark/stateful/bloatnet/test_single_opcode.py b/tests/benchmark/stateful/bloatnet/test_single_opcode.py index dc0da3a20d..d7879ac052 100644 --- a/tests/benchmark/stateful/bloatnet/test_single_opcode.py +++ b/tests/benchmark/stateful/bloatnet/test_single_opcode.py @@ -34,8 +34,11 @@ from tests.benchmark.stateful.helpers import ( APPROVE_SELECTOR, BALANCEOF_SELECTOR, + MINT_SELECTOR, SLOAD_TOKENS, + SSTORE_MINT_TOKENS, SSTORE_TOKENS, + CacheStrategy, ) REFERENCE_SPEC_GIT_PATH = "DUMMY/bloatnet.md" @@ -88,13 +91,17 @@ @pytest.mark.parametrize("token_name", SLOAD_TOKENS) -def test_sload_empty_erc20_balanceof( +@pytest.mark.parametrize("existing_slots", [False, True]) +@pytest.mark.parametrize("cache_strategy", list(CacheStrategy)) +def test_sload_erc20_balanceof( benchmark_test: BenchmarkTestFiller, pre: Alloc, fork: Fork, gas_benchmark_value: int, tx_gas_limit: int, token_name: str, + existing_slots: bool, + cache_strategy: CacheStrategy, ) -> None: """Benchmark SLOAD using ERC20 balanceOf on bloatnet.""" # Stub Account @@ -123,19 +130,29 @@ def test_sload_empty_erc20_balanceof( + Op.CALLDATALOAD(0) # [num_calls] ) - loop = While( - body=Op.POP( - Op.CALL( - address=erc20_address, - value=0, - args_offset=28, - args_size=36, - ret_offset=0, - ret_size=0, - # gas accounting - address_warm=True, - ) + call_balance_of = Op.POP( + Op.CALL( + address=erc20_address, + value=0, + args_offset=32 - 4, + args_size=32 + 4, + ret_offset=0, + ret_size=0, + # gas accounting + address_warm=True, ) + ) + + cache_loop = ( + call_balance_of + if cache_strategy == CacheStrategy.CACHE_TX + else Bytecode() + ) + + loop = While( + body=call_balance_of + # Do the same call again for the cached variant + + cache_loop + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)), condition=Op.PUSH1(1) # [1, num_calls] + Op.SWAP1 # [num_calls, 1] @@ -170,8 +187,8 @@ def test_sload_empty_erc20_balanceof( # Function body + Op.JUMPDEST + Op.CALLDATALOAD(4) - + Op.MSTORE(0) - + Op.MSTORE(32, 0) + + Op.MSTORE(0, 0) + + Op.MSTORE(32, Op.CALLDATALOAD(4)) + Op.SHA3( 0, 64, @@ -182,7 +199,8 @@ def test_sload_empty_erc20_balanceof( ) + Op.SLOAD # Return value - + Op.MSTORE(0) + + Op.PUSH0 + + Op.MSTORE + Op.RETURN(0, 32) ) @@ -190,8 +208,15 @@ def test_sload_empty_erc20_balanceof( # Transaction Loops txs = [] + cache_txs = [] gas_remaining = gas_benchmark_value - slot_offset = 0 + # Start at 1 (ERC20 bloater writes the balance of address to the slot) + # or start at keccak256("random") for non-existing slots + slot_offset = ( + 1 + if existing_slots + else 0xA4896A3F93BF4BF58378E579F3CF193BB4AF1022AF7D2089F37D8BAE7157B85F + ) while gas_remaining > intrinsic_gas_with_access_list: gas_available = min(gas_remaining, tx_gas_limit) @@ -208,24 +233,42 @@ def test_sload_empty_erc20_balanceof( calldata = Hash(num_calls) + Hash(slot_offset) - txs.append( - Transaction( - gas_limit=gas_available, - data=calldata, - to=attack_contract_address, - sender=pre.fund_eoa(), - access_list=access_list, + if cache_strategy == CacheStrategy.CACHE_PREVIOUS_BLOCK: + with TestPhaseManager.setup(): + # For block-level caching, + # we need to warm the slot in a separate transaction + cache_txs.append( + Transaction( + gas_limit=gas_available, + data=calldata, + to=attack_contract_address, + sender=pre.fund_eoa(), + access_list=access_list, + ) + ) + + with TestPhaseManager.execution(): + txs.append( + Transaction( + gas_limit=gas_available, + data=calldata, + to=attack_contract_address, + sender=pre.fund_eoa(), + access_list=access_list, + ) ) - ) gas_remaining -= gas_available slot_offset += num_calls - benchmark_test( - pre=pre, - blocks=[Block(txs=txs)], + blocks = ( + [Block(txs=txs)] + if cache_strategy != CacheStrategy.CACHE_PREVIOUS_BLOCK + else [Block(txs=cache_txs), Block(txs=txs)] ) + benchmark_test(pre=pre, blocks=blocks) + @pytest.mark.parametrize("token_name", SSTORE_TOKENS) def test_sstore_erc20_approve( @@ -383,6 +426,245 @@ def test_sstore_erc20_approve( ) +@pytest.mark.parametrize("token_name", SSTORE_MINT_TOKENS) +@pytest.mark.parametrize("existing_slots", [False, True]) +@pytest.mark.parametrize("cache_strategy", list(CacheStrategy)) +@pytest.mark.parametrize("no_change", [False, True]) +def test_sstore_erc20_mint( + benchmark_test: BenchmarkTestFiller, + pre: Alloc, + fork: Fork, + gas_benchmark_value: int, + tx_gas_limit: int, + token_name: str, + existing_slots: bool, + cache_strategy: CacheStrategy, + no_change: bool, +) -> None: + """ + Benchmark SSTORE using ERC20 mint on bloatnet. + This contract calls mint() on an ERC20 contract + which supports the mint() function. It is intended + to be used with ERC20 contracts bloated via bloatStorage. + The mint will increase the total supply and the target account. + """ + # Stub Account + erc20_address = pre.deploy_contract( + code=Bytecode(), + stub=f"test_sstore_erc20_mint_{token_name}", + ) + + mint_amount = 0 if no_change else 1 + + # MEM[0] = function selector + # MEM[32] = target address + # MEM[64] = mint amount + setup = ( + Op.MSTORE( + 0, + MINT_SELECTOR, + # gas accounting + old_memory_size=0, + new_memory_size=32, + ) + + Op.MSTORE( + 32, + Op.CALLDATALOAD(32), # Address Offset + # gas accounting + old_memory_size=32, + new_memory_size=64, + ) + + Op.MSTORE( + 64, + mint_amount, + # gas accounting + old_memory_size=64, + new_memory_size=96, + ) + + Op.CALLDATALOAD(0) # [num_calls] + ) + + call_mint = Op.POP( + Op.CALL( + address=erc20_address, + value=0, + args_offset=32 - 4, + args_size=32 + 32 + 4, + ret_offset=0, + ret_size=0, + # gas accounting + address_warm=True, + ) + ) + + if cache_strategy == CacheStrategy.CACHE_TX: + # Call balanceOf first to warm the storage slot, then restore + # the mint selector + cache_warmup = ( + Op.MSTORE(0, BALANCEOF_SELECTOR) + + Op.POP( + Op.CALL( + address=erc20_address, + value=0, + args_offset=32 - 4, + args_size=32 + 4, + ret_offset=0, + ret_size=0, + # gas accounting + address_warm=True, + ) + ) + + Op.MSTORE(0, MINT_SELECTOR) + ) + else: + cache_warmup = Bytecode() + + loop = While( + body=cache_warmup + call_mint + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)), + condition=Op.PUSH1(1) # [1, num_calls] + + Op.SWAP1 # [num_calls, 1] + + Op.SUB # [num_calls-1] + + Op.DUP1 # [num_calls-1, num_calls-1] + + Op.ISZERO # [num_calls-1==0, num_calls-1] + + Op.ISZERO, # [num_calls-1!=0, num_calls-1] + ) + + # Contract Deployment + code = setup + loop + attack_contract_address = pre.deploy_contract(code=code) + + # Gas Accounting + setup_cost = setup.gas_cost(fork) + loop_cost = loop.gas_cost(fork) + access_list = [AccessList(address=erc20_address, storage_keys=[])] + intrinsic_gas_with_access_list = ( + fork.transaction_intrinsic_cost_calculator()( + access_list=access_list, + calldata=b"\xff" * 64, + ) + ) + + # Mint function dispatch: hash balance slot, SLOAD, ADD, SSTORE + function_dispatch_mint = ( + Op.PUSH4(MINT_SELECTOR) + + Op.EQ + + Op.JUMPI + + Op.JUMPDEST + + Op.MSTORE(0, Op.CALLDATALOAD(4)) + + Op.MSTORE(32, 0) + + Op.SHA3( + 0, + 64, + # gas accounting + data_size=64, + old_memory_size=64, + new_memory_size=64, + ) + + Op.DUP1 + + Op.SLOAD.with_metadata( + key_warm=cache_strategy == CacheStrategy.CACHE_TX + ) + + Op.CALLDATALOAD(36) + + Op.ADD + + Op.SSTORE + # Increase total supply + + Op.SSTORE(0, Op.ADD(Op.SLOAD(0), Op.CALLDATALOAD(36))) + + Op.MSTORE(0, 1) + + Op.RETURN(0, 32) + ) + + function_dispatch_cost = function_dispatch_mint.gas_cost(fork) + + if cache_strategy == CacheStrategy.CACHE_TX: + # Add balanceOf dispatch cost for the warmup call + function_dispatch_balanceof = ( + Op.PUSH4(BALANCEOF_SELECTOR) + + Op.EQ + + Op.JUMPI + + Op.JUMPDEST + + Op.MSTORE(0, Op.CALLDATALOAD(4)) + + Op.MSTORE(32, 0) + + Op.SHA3( + 0, + 64, + # gas accounting + data_size=64, + old_memory_size=64, + new_memory_size=64, + ) + + Op.SLOAD + + Op.PUSH0 + + Op.MSTORE + + Op.RETURN(0, 32) + ) + function_dispatch_cost += function_dispatch_balanceof.gas_cost(fork) + + # Transaction Loops + txs = [] + cache_txs = [] + gas_remaining = gas_benchmark_value + # Start at 1 for existing balance slots, + # or at keccak256("random") for non-existing slots + slot_offset = ( + 1 + if existing_slots + else 0xA4896A3F93BF4BF58378E579F3CF193BB4AF1022AF7D2089F37D8BAE7157B85F + ) + + while gas_remaining > intrinsic_gas_with_access_list: + gas_available = min(gas_remaining, tx_gas_limit) + + if gas_available < intrinsic_gas_with_access_list + setup_cost: + break + + num_calls = ( + gas_available - intrinsic_gas_with_access_list - setup_cost + ) // (function_dispatch_cost + loop_cost) + + if num_calls == 0: + break + + calldata = Hash(num_calls) + Hash(slot_offset) + if cache_strategy == CacheStrategy.CACHE_PREVIOUS_BLOCK: + with TestPhaseManager.setup(): + cache_txs.append( + Transaction( + gas_limit=gas_available, + data=calldata, + to=attack_contract_address, + sender=pre.fund_eoa(), + access_list=access_list, + ) + ) + + with TestPhaseManager.execution(): + # Same here, does this create tx is execution mode? + # And above setup mode? + txs.append( + Transaction( + gas_limit=gas_available, + data=calldata, + to=attack_contract_address, + sender=pre.fund_eoa(), + access_list=access_list, + ) + ) + + gas_remaining -= gas_available + slot_offset += num_calls + + blocks = ( + [Block(txs=txs)] + if cache_strategy != CacheStrategy.CACHE_PREVIOUS_BLOCK + else [Block(txs=cache_txs), Block(txs=txs)] + ) + + benchmark_test( + pre=pre, + blocks=blocks, + ) + + def create_sstore_initializer(init_val: int) -> IteratingBytecode: """ Create a contract that initializes storage slots from calldata parameters. diff --git a/tests/benchmark/stateful/helpers.py b/tests/benchmark/stateful/helpers.py index de2a3a78e4..c12bd163d9 100644 --- a/tests/benchmark/stateful/helpers.py +++ b/tests/benchmark/stateful/helpers.py @@ -1,12 +1,14 @@ """Shared constants and helpers for stateful benchmark tests.""" import json +from enum import Enum from pathlib import Path # ERC20 function selectors BALANCEOF_SELECTOR = 0x70A08231 # balanceOf(address) APPROVE_SELECTOR = 0x095EA7B3 # approve(address,uint256) ALLOWANCE_SELECTOR = 0xDD62ED3E # allowance(address,address) +MINT_SELECTOR = 0x40C10F19 # mint(address,uint256) # Load token names from stubs_bloatnet.json for test parametrization _STUBS_FILE = Path(__file__).parent / "bloatnet" / "stubs_bloatnet.json" @@ -24,8 +26,25 @@ for k in _STUBS.keys() if k.startswith("test_sstore_erc20_approve_") ] +SSTORE_MINT_TOKENS = [ + k.replace("test_sstore_erc20_mint_", "") + for k in _STUBS.keys() + if k.startswith("test_sstore_erc20_mint_") +] MIXED_TOKENS = [ k.replace("test_mixed_sload_sstore_", "") for k in _STUBS.keys() if k.startswith("test_mixed_sload_sstore_") ] + + +class CacheStrategy(str, Enum): + """Defines cache assumptions for benchmarked state access.""" + + # No caching strategy: target state is cold in EVM and cache + NO_CACHE = "no_cache" + # Caching at tx level: target state is warm in EVM and cache + CACHE_TX = "cache_tx" + # Caching at previous block: + # Target state is cold in EVM but (assumed) to be cached + CACHE_PREVIOUS_BLOCK = "cache_previous_block"