From 126bc5312587624c0f589bb5e9e71fbc3267c09e Mon Sep 17 00:00:00 2001 From: CPerezz Date: Tue, 23 Dec 2025 16:23:37 +0100 Subject: [PATCH 01/15] feat(benchmark): Add EXTCODESIZE bytecode size benchmarks Support for multiple contract sizes (0.5KB to 24KB) with automatic splitting of gas budget into 16M transactions respecting Fusaka limit. Includes deployment tools for test infrastructure setup. --- .../bloatnet/calculate_init_hashes.py | 74 +++++ .../bloatnet/deploy_contracts_multi.py | 313 ++++++++++++++++++ .../stateful/bloatnet/deploy_factory_multi.py | 292 ++++++++++++++++ .../bloatnet/deploy_initcode_multi.py | 212 ++++++++++++ .../test_extcodesize_bytecode_sizes.py | 241 ++++++++++++++ 5 files changed, 1132 insertions(+) create mode 100644 tests/benchmark/stateful/bloatnet/calculate_init_hashes.py create mode 100644 tests/benchmark/stateful/bloatnet/deploy_contracts_multi.py create mode 100644 tests/benchmark/stateful/bloatnet/deploy_factory_multi.py create mode 100644 tests/benchmark/stateful/bloatnet/deploy_initcode_multi.py create mode 100644 tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py diff --git a/tests/benchmark/stateful/bloatnet/calculate_init_hashes.py b/tests/benchmark/stateful/bloatnet/calculate_init_hashes.py new file mode 100644 index 00000000000..77d3e9a0668 --- /dev/null +++ b/tests/benchmark/stateful/bloatnet/calculate_init_hashes.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +Calculate init code hashes for all contract sizes. +""" + +from execution_testing import Op, While +from eth_utils import keccak + +# Maximum contract size in bytes (24 KB) +MAX_CONTRACT_SIZE = 24576 + +def build_initcode(target_size_kb: float) -> bytes: + """ + Build initcode that generates contracts of specific size using ADDRESS for randomness. + """ + target_size = int(target_size_kb * 1024) + + if target_size > MAX_CONTRACT_SIZE: + target_size = MAX_CONTRACT_SIZE + + # For small contracts (< 1KB), use simple padding + if target_size < 1024: + initcode = ( + # Store deployer address for uniqueness + Op.MSTORE(0, Op.ADDRESS) + # Pad with JUMPDEST opcodes (1 byte each) + + Op.JUMPDEST * max(0, target_size - 33 - 10) # Account for other opcodes + # Ensure first byte is STOP + + Op.MSTORE8(0, 0x00) + # Return the contract + + Op.RETURN(0, target_size) + ) + else: + # For larger contracts, use the keccak256 expansion pattern + # Generate XOR table for expansion + xor_table_size = min(256, target_size // 256) + xor_table = [keccak(i.to_bytes(32, "big")) for i in range(xor_table_size)] + + initcode = ( + # Store ADDRESS as initial seed - creates uniqueness per deployment + Op.MSTORE(0, Op.ADDRESS) + # Loop to expand bytecode using SHA3 and XOR operations + + While( + body=( + Op.SHA3(Op.SUB(Op.MSIZE, 32), 32) + # Use XOR table to expand without excessive SHA3 calls + + sum( + (Op.PUSH32(xor_value) + Op.XOR + Op.DUP1 + Op.MSIZE + Op.MSTORE) + for xor_value in xor_table + ) + + Op.POP + ), + condition=Op.LT(Op.MSIZE, target_size), + ) + # Set first byte to STOP for efficient CALL handling + + Op.MSTORE8(0, 0x00) + # Return the full contract + + Op.RETURN(0, target_size) + ) + + return bytes(initcode) + +# Calculate hashes for all sizes +sizes = [0.5, 1.0, 5.0, 10.0, 24.0] + +print("Init code hashes for each size:") +print("="*60) + +for size_kb in sizes: + initcode = build_initcode(size_kb) + init_hash = keccak(initcode) + print(f"{size_kb:4.1f} KB: 0x{init_hash.hex()}") + print(f" Initcode size: {len(initcode)} bytes") + print() \ No newline at end of file diff --git a/tests/benchmark/stateful/bloatnet/deploy_contracts_multi.py b/tests/benchmark/stateful/bloatnet/deploy_contracts_multi.py new file mode 100644 index 00000000000..75525766796 --- /dev/null +++ b/tests/benchmark/stateful/bloatnet/deploy_contracts_multi.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +""" +Deploy multiple contracts via CREATE2 factories for different sizes for BloatNet benchmarks. + +This script deploys contracts of specific sizes using the corresponding CREATE2 factories. + +USAGE: + 1. First deploy initcode contracts: + python3 deploy_initcode_multi.py + + 2. Then deploy the factories: + python3 deploy_factory_multi.py + + 3. Finally deploy contracts: + python3 deploy_contracts_multi.py --size 5 --count 1000 + python3 deploy_contracts_multi.py --size 24 --count 350 + + 4. Run EEST benchmarks: + uv run execute remote --fork Prague \\ + --rpc-endpoint http://127.0.0.1:8545 \\ + --address-stubs stubs.json \\ + -- --gas-benchmark-values 30 \\ + tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py -v + +REQUIREMENTS: + - web3.py + - eth-utils + - Local geth instance running on http://127.0.0.1:8545 + - stubs.json from deploy_factory_multi.py +""" + +import sys +import json +import time +from eth_utils import keccak +from web3 import Web3 + + +def estimate_gas_for_size(size_kb: float, block_gas_limit: int) -> int: + """Estimate gas needed to deploy a contract of given size, respecting network limits. + + Based on actual measurements: + - 0.5KB deployment used ~183,163 gas + + Gas costs breakdown: + - Transaction intrinsic: 21,000 + - Factory execution: ~1,000 + - CREATE2 overhead: ~32,000 + - Contract bytecode storage: 200 gas per byte + - Init code execution: varies by size + """ + size_bytes = int(size_kb * 1024) + + # Precise gas calculation based on actual measurements + if size_kb <= 0.5: + # 0.5KB used 183,163 gas = ~21K intrinsic + ~32K CREATE2 + ~102K storage (512*200) + ~28K execution + base_gas = 21_000 + 32_000 + (size_bytes * 200) + 30_000 + elif size_kb <= 1: + # 1KB: similar but with more storage cost + base_gas = 21_000 + 32_000 + (size_bytes * 200) + 35_000 + elif size_kb <= 5: + # 5KB uses While loop for init, more execution cost + base_gas = 21_000 + 32_000 + (size_bytes * 200) + 150_000 + elif size_kb <= 10: + # 10KB: more While iterations + base_gas = 21_000 + 32_000 + (size_bytes * 200) + 300_000 + else: + # 24KB: maximum While iterations + base_gas = 21_000 + 32_000 + (size_bytes * 200) + 500_000 + + # Add 10% buffer for safety + final_gas = int(base_gas * 1.1) + + # Cap at 80% of block gas limit to ensure inclusion + max_safe_gas = int(block_gas_limit * 0.8) + + return min(final_gas, max_safe_gas) + + +def deploy_contracts(w3: Web3, factory_address: str, count: int, size_kb: float) -> int: + """Deploy contracts via a CREATE2 factory.""" + account = w3.eth.accounts[0] + + # Get network parameters dynamically + latest_block = w3.eth.get_block('latest') + block_gas_limit = latest_block.gasLimit + print(f"Network block gas limit: {block_gas_limit:,}") + + # Get current counter + current = int.from_bytes(w3.eth.get_storage_at(factory_address, 0), 'big') + print(f"Factory has already deployed {current} contracts") + + if count <= current: + print(f"✅ Already have {current} contracts (target: {count})") + return current + + remaining = count - current + print(f"Deploying {remaining} more contracts of {size_kb}KB...") + + # Estimate gas needed based on network's block gas limit + gas_limit = estimate_gas_for_size(size_kb, block_gas_limit) + print(f"Using gas limit: {gas_limit:,} per deployment") + print(f" (Network allows up to {int(block_gas_limit * 0.8):,} per transaction)") + + # Calculate optimal batch size based on gas costs + # Fusaka limit: 16M gas per transaction/block + FUSAKA_GAS_LIMIT = 16_000_000 + + # Calculate how many deployments can fit per block + # Each deployment is a separate transaction with our current factory + # Leave some margin for safety (use 95% of block gas limit) + usable_gas = int(FUSAKA_GAS_LIMIT * 0.95) + deployments_per_block = usable_gas // gas_limit + + print(f"Optimal batch size: {deployments_per_block} deployments per block") + print(f" (Each deployment uses {gas_limit:,} gas)") + print(f" (Block limit is {FUSAKA_GAS_LIMIT:,}, using {usable_gas:,})") + + # Deploy contracts in optimized batches + batch_size = deployments_per_block + deployed = 0 + failed = 0 + start_time = time.time() + + print(f"\nDeploying {remaining} contracts in batches of {batch_size}...") + print(f"Expected blocks needed: {(remaining + batch_size - 1) // batch_size}") + + for batch_start in range(0, remaining, batch_size): + batch_end = min(batch_start + batch_size, remaining) + batch_count = batch_end - batch_start + + # Get fresh nonce for this batch to avoid "already known" errors + nonce = w3.eth.get_transaction_count(account) + + # Send batch of transactions rapidly to fill a block + tx_hashes = [] + batch_time = time.time() + + print(f"\nBatch {batch_start//batch_size + 1}: Sending {batch_count} transactions rapidly to fill block...") + print(f" Starting nonce: {nonce}") + + # Send all transactions as fast as possible with pre-calculated nonces + for i in range(batch_count): + try: + # Call factory to deploy a contract + tx_hash = w3.eth.send_transaction({ + "from": account, + "to": factory_address, + "data": "0x01", # Non-empty data to trigger CREATE2 + "gas": gas_limit, + "nonce": nonce + i # Pre-calculate nonce for speed + }) + tx_hashes.append(tx_hash) + except Exception as e: + print(f" Error sending tx {batch_start + i + 1}: {e}") + # Try to recover and continue + break + + send_time = time.time() - batch_time + print(f" Sent {len(tx_hashes)} transactions in {send_time:.2f}s ({len(tx_hashes)/send_time:.1f} tx/s)") + print(f" Waiting for confirmations...") + + # Wait for batch receipts + batch_deployed = 0 + for j, tx_hash in enumerate(tx_hashes): + try: + receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=60) + if receipt['status'] == 1: + deployed += 1 + batch_deployed += 1 + else: + failed += 1 + print(f" Transaction {j+1} failed") + except Exception as e: + print(f" Transaction {j+1} failed or timed out: {e}") + failed += 1 + + # Progress update + counter = int.from_bytes(w3.eth.get_storage_at(factory_address, 0), 'big') + elapsed = time.time() - start_time + rate = deployed / elapsed if elapsed > 0 else 0 + eta = (remaining - deployed) / rate if rate > 0 else 0 + batch_elapsed = time.time() - batch_time + + print(f" Batch complete: {batch_deployed}/{batch_count} deployed in {batch_elapsed:.1f}s") + print(f" Gas used per deployment: {gas_limit:,} ({batch_deployed * gas_limit:,} total)") + print(f" Overall: {counter}/{count} contracts ({deployed}/{remaining} new)") + print(f" Rate: {rate:.1f} contracts/sec, ETA: {eta:.0f}s") + + if failed > 20: + print("\n⚠️ Too many failures, stopping...") + break + + # Final check + final_counter = int.from_bytes(w3.eth.get_storage_at(factory_address, 0), 'big') + elapsed = time.time() - start_time + print(f"\n✅ Deployment complete in {elapsed:.1f} seconds") + print(f"Total contracts deployed: {final_counter}") + + return final_counter + + +def verify_contracts(w3: Web3, factory_address: str, count: int, size_kb: float) -> bool: + """Verify that contracts exist at expected CREATE2 addresses.""" + print(f"\n--- Verifying CREATE2 Addresses for {size_kb}KB contracts ---") + + # Get init code hash from factory storage + stored_hash = w3.eth.get_storage_at(factory_address, 1) + + # Verify a sample of contracts + sample_size = min(5, count) + verified = 0 + + for salt in range(sample_size): + # Calculate CREATE2 address + create2_input = ( + b"\xff" + + bytes.fromhex(factory_address[2:].lower()) + + salt.to_bytes(32, "big") + + stored_hash + ) + expected_addr = Web3.to_checksum_address("0x" + keccak(create2_input)[-20:].hex()) + + # Check if contract exists + code = w3.eth.get_code(expected_addr) + if len(code) > 0: + print(f" Salt {salt}: ✅ Found at {expected_addr} ({len(code)} bytes)") + verified += 1 + else: + print(f" Salt {salt}: ❌ Not found at {expected_addr}") + + return verified == sample_size + + +def main(): + """Main deployment script.""" + import argparse + + parser = argparse.ArgumentParser(description='Deploy BloatNet contracts via CREATE2 factory') + parser.add_argument('--size', type=float, required=True, + choices=[0.5, 1, 2, 5, 10, 24], + help='Contract size in KB') + parser.add_argument('--count', type=int, required=True, + help='Total number of contracts to deploy') + parser.add_argument('--rpc-url', default='http://127.0.0.1:8545', + help='RPC URL') + parser.add_argument('--stubs', default='stubs.json', + help='Path to stubs JSON file') + args = parser.parse_args() + + # Connect to local geth instance + w3 = Web3(Web3.HTTPProvider(args.rpc_url)) + if not w3.is_connected(): + print(f"❌ Failed to connect to {args.rpc_url}") + sys.exit(1) + + print(f"Connected to: {args.rpc_url}") + print(f"Account: {w3.eth.accounts[0]}") + + # Load factory address from stubs + try: + with open(args.stubs, 'r') as f: + stubs = json.load(f) + except FileNotFoundError: + print(f"❌ Stubs file not found: {args.stubs}") + print("Run deploy_factory_multi.py first") + sys.exit(1) + + # Find the appropriate factory + size_key = f"{args.size}kb".replace(".", "_") + factory_key = f"bloatnet_factory_{size_key}" + factory_address = stubs.get(factory_key) + + if not factory_address: + print(f"❌ Factory not found for {args.size}KB contracts") + print(f"Looking for key: {factory_key}") + print(f"Available factories: {list(stubs.keys())}") + sys.exit(1) + + print(f"Using factory at: {factory_address}") + + # Deploy contracts + final_count = deploy_contracts(w3, factory_address, args.count, args.size) + + # Verify deployment + if verify_contracts(w3, factory_address, final_count, args.size): + print(f"\n✅ Successfully verified {args.size}KB contracts") + + # Calculate gas requirements for testing + cost_per_contract = 2660 # Approximate gas per EXTCODESIZE with CREATE2 + test_gas_30m = 30_000_000 + max_contracts_30m = (test_gas_30m - 21000 - 1000) // cost_per_contract + + print(f"\n=== Ready for Testing ===") + print(f"Contract size: {args.size}KB") + print(f"Contracts deployed: {final_count}") + print(f"Factory address: {factory_address}") + print(f"Max contracts for 30M gas: ~{max_contracts_30m}") + + if final_count < max_contracts_30m: + print(f"⚠️ Consider deploying {max_contracts_30m - final_count} more contracts " + f"to fully utilize 30M gas") + + print("\nTo run the benchmark test:") + print("uv run execute remote --fork Prague \\") + print(f" --rpc-endpoint {args.rpc_url} \\") + print(f" --address-stubs {args.stubs} \\") + print(" -- --gas-benchmark-values 30 \\") + print(" tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py \\") + print(f" -k '{args.size}KB' -v") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/benchmark/stateful/bloatnet/deploy_factory_multi.py b/tests/benchmark/stateful/bloatnet/deploy_factory_multi.py new file mode 100644 index 00000000000..7e161c2a3d0 --- /dev/null +++ b/tests/benchmark/stateful/bloatnet/deploy_factory_multi.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python3 +""" +Deploy CREATE2 factories for different contract sizes for BloatNet benchmarks. + +This script deploys CREATE2 factories that can deploy contracts of specific sizes +(0.5, 1, 2, 5, 10, 24 KB) using the corresponding initcode contracts. + +USAGE: + 1. First deploy initcode contracts: + python3 deploy_initcode_multi.py + + 2. Then deploy the factories: + python3 deploy_factory_multi.py + + 3. The factory addresses will be saved to stubs.json + +REQUIREMENTS: + - web3.py + - eth-utils + - ethereum-test-tools (for Opcodes) + - Local geth instance running on http://127.0.0.1:8545 + - initcode_addresses.json from deploy_initcode_multi.py +""" + +import sys +import json +from pathlib import Path +from typing import Dict, Any, Tuple + +# Add path to execution-spec-tests if needed +# sys.path.insert(0, "/path/to/execution-spec-tests") + +from execution_testing import Op +from eth_utils import keccak +from web3 import Web3 + + +def build_factory(initcode_address: str, initcode_hash: bytes, initcode_size: int) -> bytes: + """ + Build a CREATE2 factory contract with getConfig() method. + + Storage layout: + - Slot 0: Counter (number of deployed contracts) + - Slot 1: Init code hash for CREATE2 address calculation + - Slot 2: Init code address + + Interface: + - When called with CALLDATASIZE == 0: Returns (num_deployed_contracts, init_code_hash) + - When called otherwise: Deploys a new contract via CREATE2 + """ + + # Factory constructor: Store init code hash and address + factory_constructor = ( + Op.PUSH32(initcode_hash) # Push init code hash + + Op.PUSH1(1) # Slot 1 + + Op.SSTORE # Store init code hash + + Op.PUSH20(bytes.fromhex(initcode_address[2:])) # Push initcode address + + Op.PUSH1(2) # Slot 2 + + Op.SSTORE # Store initcode address + ) + + # Factory runtime code + factory_runtime = ( + # Check if this is a getConfig() call (CALLDATASIZE == 0) + Op.CALLDATASIZE + + Op.ISZERO + + Op.PUSH1(0x31) # Jump to getConfig (hardcoded offset) + + Op.JUMPI + + # === CREATE2 DEPLOYMENT PATH === + # Load initcode address from storage slot 2 + + Op.PUSH1(2) # Slot 2 + + Op.SLOAD # Load initcode address + + # EXTCODECOPY: copy initcode to memory + + Op.PUSH2(initcode_size) # Size + + Op.PUSH1(0) # Source offset + + Op.PUSH1(0) # Dest offset + + Op.DUP4 # Address (from bottom of stack) + + Op.EXTCODECOPY # Copy initcode to memory + + # Prepare for CREATE2 + + Op.PUSH2(initcode_size) # Size + + Op.SWAP1 # Put size under address + + Op.POP # Remove address + + # CREATE2 with current counter as salt + + Op.PUSH1(0) # Slot 0 + + Op.SLOAD # Load counter (use as salt) + + Op.SWAP1 # Put size on top + + Op.PUSH1(0) # Offset in memory + + Op.PUSH1(0) # Value + + Op.CREATE2 # Create contract + + # Store the created address for return + + Op.DUP1 + + Op.PUSH1(0) + + Op.MSTORE + + # Increment counter + + Op.PUSH1(0) # Slot 0 + + Op.DUP1 # Duplicate + + Op.SLOAD # Load counter + + Op.PUSH1(1) # Increment + + Op.ADD # Add + + Op.SWAP1 # Swap + + Op.SSTORE # Store new counter + + # Return the created address + + Op.PUSH1(32) # Return 32 bytes + + Op.PUSH1(0) # From memory position 0 + + Op.RETURN + + # === GETCONFIG PATH === + + Op.JUMPDEST # Destination for getConfig (0x31) + + Op.PUSH1(0) # Slot 0 + + Op.SLOAD # Load number of deployed contracts + + Op.PUSH1(0) # Memory position 0 + + Op.MSTORE # Store in memory + + + Op.PUSH1(1) # Slot 1 + + Op.SLOAD # Load init code hash + + Op.PUSH1(32) # Memory position 32 + + Op.MSTORE # Store in memory + + + Op.PUSH1(64) # Return 64 bytes (2 * 32) + + Op.PUSH1(0) # From memory position 0 + + Op.RETURN + ) + + # Build deployment bytecode + factory_runtime_bytes = bytes(factory_runtime) + runtime_size = len(factory_runtime_bytes) + + # Deployment code that copies and returns runtime + constructor_bytes = bytes(factory_constructor) + constructor_size = len(constructor_bytes) + deployer_size = 14 # Size of deployer code below + runtime_offset = constructor_size + deployer_size + + deployer = ( + # Copy runtime code to memory + Op.PUSH2(runtime_size) # Size of runtime (3 bytes) + + Op.PUSH1(runtime_offset) # Offset to runtime (2 bytes) + + Op.PUSH1(0) # Dest in memory (2 bytes) + + Op.CODECOPY # (1 byte) + # Return runtime code + + Op.PUSH2(runtime_size) # Size to return (3 bytes) + + Op.PUSH1(0) # Offset in memory (2 bytes) + + Op.RETURN # (1 byte) = Total: 14 bytes + ) + + factory_deployment = factory_constructor + deployer + factory_runtime + return bytes(factory_deployment) + + +def deploy_factory(w3: Web3, size_kb: float, initcode_info: Dict[str, Any]) -> Tuple[str, bytes]: + """Deploy a CREATE2 factory for a specific contract size.""" + account = w3.eth.accounts[0] + + print(f"\n--- Deploying Factory for {size_kb}KB contracts ---") + + initcode_address = initcode_info["address"] + initcode_hash_str = initcode_info["hash"] + initcode_size = initcode_info["bytecode_size"] + + # Convert hash string to bytes + initcode_hash = bytes.fromhex(initcode_hash_str[2:] if initcode_hash_str.startswith("0x") else initcode_hash_str) + + print(f"Using initcode at: {initcode_address}") + print(f"Init code hash: {initcode_hash_str}") + print(f"Init code size: {initcode_size} bytes") + + # Build factory bytecode + factory_bytecode = build_factory(initcode_address, initcode_hash, initcode_size) + print(f"Factory deployment size: {len(factory_bytecode)} bytes") + + # Deploy factory + tx_hash = w3.eth.send_transaction({ + "from": account, + "data": "0x" + factory_bytecode.hex(), + "gas": 10000000 # 10M gas for deployment + }) + receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + + if receipt['status'] != 1: + print(f"❌ Failed to deploy factory for {size_kb}KB") + return None, None + + factory_address = receipt['contractAddress'] + print(f"✅ Factory deployed at: {factory_address}") + + # Verify factory storage + counter = w3.eth.get_storage_at(factory_address, 0) + stored_hash = w3.eth.get_storage_at(factory_address, 1) + stored_initcode_addr = w3.eth.get_storage_at(factory_address, 2) + + print(f"Factory storage verification:") + print(f" Slot 0 (counter): {int.from_bytes(counter, 'big')}") + print(f" Slot 1 (hash): 0x{stored_hash.hex()}") + print(f" Slot 2 (initcode): 0x{stored_initcode_addr.hex()}") + print(f" Hash matches: {stored_hash == initcode_hash}") + + return factory_address, initcode_hash + + +def test_getconfig(w3: Web3, factory_address: str, expected_hash: bytes) -> bool: + """Test the getConfig() method of a factory.""" + print(f"Testing getConfig() on factory at {factory_address}...") + + # Call getConfig() with empty calldata + result = w3.eth.call({ + "to": factory_address, + "data": "" # Empty calldata triggers getConfig + }) + + if len(result) != 64: + print(f"❌ Unexpected result length: {len(result)} (expected 64)") + return False + + # Parse the result + num_deployed = int.from_bytes(result[:32], 'big') + returned_hash = result[32:64] + + print(f" Number of deployed contracts: {num_deployed}") + print(f" Init code hash: 0x{returned_hash.hex()}") + print(f" Hash matches expected: {returned_hash == expected_hash}") + + return returned_hash == expected_hash + + +def main(): + """Deploy factories for all contract sizes.""" + import argparse + + parser = argparse.ArgumentParser(description='Deploy CREATE2 factories for multiple sizes') + parser.add_argument('--rpc-url', default='http://127.0.0.1:8545', help='RPC URL') + parser.add_argument('--initcode-file', default='initcode_addresses.json', + help='Path to initcode addresses JSON file') + parser.add_argument('--output', default='stubs.json', + help='Output file for factory addresses (default: stubs.json)') + args = parser.parse_args() + + # Connect to local geth instance + w3 = Web3(Web3.HTTPProvider(args.rpc_url)) + if not w3.is_connected(): + print(f"❌ Failed to connect to {args.rpc_url}") + sys.exit(1) + + print(f"Connected to: {args.rpc_url}") + print(f"Account: {w3.eth.accounts[0]}") + + # Load initcode addresses + try: + with open(args.initcode_file, 'r') as f: + initcode_data = json.load(f) + except FileNotFoundError: + print(f"❌ {args.initcode_file} not found") + print("Run deploy_initcode_multi.py first") + sys.exit(1) + + # Deploy factory for each size + stubs = {} + for key, initcode_info in initcode_data.items(): + size_kb = initcode_info["size_kb"] + factory_address, initcode_hash = deploy_factory(w3, size_kb, initcode_info) + + if factory_address: + # Test getConfig + if test_getconfig(w3, factory_address, initcode_hash): + print(f"✅ Factory for {size_kb}KB working correctly") + + # Add to stubs with descriptive key + stub_key = f"bloatnet_factory_{key}" + stubs[stub_key] = factory_address + + # Save stubs to file + if stubs: + with open(args.output, 'w') as f: + json.dump(stubs, f, indent=2) + print(f"\n✅ All factory addresses saved to {args.output}") + + # Print summary + print("\n=== Factory Summary ===") + for stub_key, address in stubs.items(): + print(f"{stub_key}: {address}") + else: + print("\n❌ No factories deployed successfully") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/benchmark/stateful/bloatnet/deploy_initcode_multi.py b/tests/benchmark/stateful/bloatnet/deploy_initcode_multi.py new file mode 100644 index 00000000000..96e4f47e2e8 --- /dev/null +++ b/tests/benchmark/stateful/bloatnet/deploy_initcode_multi.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +""" +Deploy multiple initcode contracts for different bytecode sizes for BloatNet benchmarks. + +This script deploys initcode contracts that will be used by CREATE2 factories +to create contracts of different sizes (0.5, 1, 2, 5, 10, 24 KB). + +USAGE: + 1. Start a local geth instance: + geth --dev --http --http.api eth,web3,net,debug --http.corsdomain "*" + + 2. Run this script to deploy all initcode contracts: + python3 deploy_initcode_multi.py + + 3. The initcode addresses will be saved to initcode_addresses.json + +REQUIREMENTS: + - web3.py + - eth-utils + - ethereum-test-tools (for Opcodes) + - Local geth instance running on http://127.0.0.1:8545 +""" + +import sys +import json +from pathlib import Path +from typing import Dict, Any + +# Add path to execution-spec-tests if needed +# sys.path.insert(0, "/path/to/execution-spec-tests") + +from execution_testing import Op, While +from eth_utils import keccak +from web3 import Web3 + +# Contract size configurations (in KB) +CONTRACT_SIZES_KB = [0.5, 1, 2, 5, 10, 24] + +# Maximum contract size in bytes (24 KB) +MAX_CONTRACT_SIZE = 24576 + +def build_initcode(target_size_kb: float) -> bytes: + """ + Build initcode that generates contracts of specific size using ADDRESS for randomness. + + Args: + target_size_kb: Target contract size in kilobytes + + Returns: + The initcode bytecode that will generate a contract of the specified size + """ + target_size = int(target_size_kb * 1024) + + if target_size > MAX_CONTRACT_SIZE: + target_size = MAX_CONTRACT_SIZE + + # For small contracts (< 1KB), use simple padding + if target_size < 1024: + initcode = ( + # Store deployer address for uniqueness + Op.MSTORE(0, Op.ADDRESS) + # Pad with JUMPDEST opcodes (1 byte each) + + Op.JUMPDEST * max(0, target_size - 33 - 10) # Account for other opcodes + # Ensure first byte is STOP + + Op.MSTORE8(0, 0x00) + # Return the contract + + Op.RETURN(0, target_size) + ) + else: + # For larger contracts, use the keccak256 expansion pattern + # Generate XOR table for expansion + xor_table_size = min(256, target_size // 256) + xor_table = [keccak(i.to_bytes(32, "big")) for i in range(xor_table_size)] + + initcode = ( + # Store ADDRESS as initial seed - creates uniqueness per deployment + Op.MSTORE(0, Op.ADDRESS) + # Loop to expand bytecode using SHA3 and XOR operations + + While( + body=( + Op.SHA3(Op.SUB(Op.MSIZE, 32), 32) + # Use XOR table to expand without excessive SHA3 calls + + sum( + (Op.PUSH32(xor_value) + Op.XOR + Op.DUP1 + Op.MSIZE + Op.MSTORE) + for xor_value in xor_table + ) + + Op.POP + ), + condition=Op.LT(Op.MSIZE, target_size), + ) + # Set first byte to STOP for efficient CALL handling + + Op.MSTORE8(0, 0x00) + # Return the full contract + + Op.RETURN(0, target_size) + ) + + return bytes(initcode) + +def deploy_initcode(w3: Web3, size_kb: float) -> Dict[str, Any]: + """Deploy an initcode contract for a specific size.""" + account = w3.eth.accounts[0] + + print(f"\n--- Building initcode for {size_kb}KB contracts ---") + initcode_bytes = build_initcode(size_kb) + print(f"Initcode size: {len(initcode_bytes)} bytes") + + # Calculate the hash for verification + initcode_hash = keccak(initcode_bytes) + print(f"Initcode hash: 0x{initcode_hash.hex()}") + + # Deploy the initcode as a contract + # We need to deploy the raw initcode bytecode without executing it + # To do this, we create deployment code that just returns the initcode + print(f"Deploying initcode contract for {size_kb}KB...") + + # Create simple deployment bytecode that returns the initcode as-is + deployment_code = ( + # CODECOPY(destOffset=0, offset=codesize_of_prefix, size=initcode_size) + bytes(Op.PUSH2(len(initcode_bytes))) # Push initcode size (3 bytes) + + bytes(Op.DUP1) # Duplicate for RETURN (1 byte) + + bytes(Op.PUSH1(0x0c)) # Offset after this prefix (2 bytes) + + bytes(Op.PUSH1(0)) # Dest offset (2 bytes) + + bytes(Op.CODECOPY) # Copy initcode (1 byte) + # RETURN(offset=0, size=initcode_size) + + bytes(Op.PUSH1(0)) # Offset (2 bytes) + + bytes(Op.RETURN) # Return (1 byte) = Total: 12 bytes (0x0c) + + initcode_bytes # Actual initcode + ) + + # Deploy the contract + tx_hash = w3.eth.send_transaction({ + "from": account, + "data": "0x" + deployment_code.hex(), + "gas": 16_000_000 # Fusaka tx gas limit (16M) + }) + receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + + if receipt['status'] != 1: + print(f"❌ Failed to deploy initcode for {size_kb}KB") + return None + + initcode_address = receipt['contractAddress'] + print(f"✅ Initcode deployed at: {initcode_address}") + + # Verify the deployed bytecode + deployed_code = w3.eth.get_code(initcode_address) + print(f"Deployed bytecode size: {len(deployed_code)} bytes") + + # Verify hash + actual_hash = keccak(deployed_code) + print(f"Actual hash: 0x{actual_hash.hex()}") + + if actual_hash == initcode_hash: + print(f"✅ Hash verification successful") + else: + print(f"⚠️ Hash mismatch! Expected: 0x{initcode_hash.hex()}") + + return { + "size_kb": size_kb, + "address": initcode_address, + "hash": "0x" + initcode_hash.hex(), + "bytecode_size": len(initcode_bytes) + } + +def main(): + """Deploy all initcode contracts for different sizes.""" + import argparse + + parser = argparse.ArgumentParser(description='Deploy initcode contracts for multiple sizes') + parser.add_argument('--rpc-url', default='http://127.0.0.1:8545', help='RPC URL') + parser.add_argument('--sizes', nargs='+', type=float, + help='Contract sizes in KB (default: 0.5 1 2 5 10 24)') + args = parser.parse_args() + + # Use custom sizes if provided + sizes = args.sizes if args.sizes else CONTRACT_SIZES_KB + + # Connect to local geth instance + w3 = Web3(Web3.HTTPProvider(args.rpc_url)) + if not w3.is_connected(): + print(f"❌ Failed to connect to {args.rpc_url}") + sys.exit(1) + + print(f"Connected to: {args.rpc_url}") + print(f"Account: {w3.eth.accounts[0]}") + + # Deploy initcode for each size + results = {} + for size_kb in sizes: + result = deploy_initcode(w3, size_kb) + if result: + # Use string key for JSON compatibility + key = f"{size_kb}kb".replace(".", "_") + results[key] = result + + # Save all results to file + if results: + output_file = "initcode_addresses.json" + with open(output_file, "w") as f: + json.dump(results, f, indent=2) + print(f"\n✅ All initcode addresses saved to {output_file}") + + # Print summary + print("\n=== Summary ===") + for key, info in results.items(): + print(f"{info['size_kb']}KB: {info['address']} (hash: {info['hash'][:10]}...)") + else: + print("\n❌ No initcode contracts deployed successfully") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py new file mode 100644 index 00000000000..253a7c70299 --- /dev/null +++ b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py @@ -0,0 +1,241 @@ +""" +Test EXTCODESIZE with parametrized bytecode sizes using CREATE2 factory pattern. + +This test executes against pre-deployed contracts via factories, measuring the +performance impact of different contract sizes on EXTCODESIZE operations. +Designed for execute mode only - contracts must be pre-deployed. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + Block, + BlockchainTestFiller, + Bytecode, + Fork, + Op, + Transaction, +) + +# Hardcoded init code hashes for each contract size +# These are deterministic based on our initcode generation in deploy_initcode_multi.py +INIT_CODE_HASHES = { + 0.5: 0xaa03809400f3a470a717403a9600140150129b24180fbaab4a4f58334fc5e5a8, + 1.0: 0x62b07e407cbb2f5bf8d706444fe89b0b40331a14efb0081ba02534ca4f6438ee, + 5.0: 0xdfcaab76f37cb182d0a9e24827c65057c0540c5b736085e3465b99eecd4502c1, + 10.0: 0xc39b2c8f715e341c46b43fda72209e073fe08f9845a586cc5b16ed9cb8a1c5a8, + 24.0: 0xd570c69a8b04a4e65932da40d0f5b2b7f11aaa72d8b8ca3a714fa43077197172, +} + + +def get_factory_stub_name(size_kb: float) -> str: + """Generate stub name for factory based on size.""" + if size_kb == 0.5: + return "factory_0_5kb" + elif size_kb == 1.0: + return "factory_1kb" + elif size_kb == 5.0: + return "factory_5kb" + elif size_kb == 10.0: + return "factory_10kb" + elif size_kb == 24.0: + return "factory_24kb" + else: + raise ValueError(f"Unsupported size: {size_kb}KB") + + +@pytest.mark.parametrize( + "bytecode_size_kb", + [0.5, 1.0, 5.0, 10.0, 24.0], + ids=lambda size: f"{size}KB", +) +@pytest.mark.valid_from("Prague") +def test_extcodesize_bytecode_sizes( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + bytecode_size_kb: float, + gas_benchmark_value: int, +) -> None: + """ + Execute EXTCODESIZE attack against pre-deployed contracts. + + This test: + 1. Uses factory addresses passed via stubs (one factory per size) + 2. Reads factory state to get number of deployed contracts + 3. Generates CREATE2 addresses dynamically during execution + 4. Calls EXTCODESIZE on as many contracts as gas allows + 5. Aims to consume all available gas to measure maximum attack capacity + """ + gas_costs = fork.gas_costs() + + # Calculate gas costs for operations + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") + + # Get factory stub name and init code hash for this size + factory_stub = get_factory_stub_name(bytecode_size_kb) + init_code_hash = INIT_CODE_HASHES[bytecode_size_kb] + + # Deploy factory stub (address comes from stub file) + factory_address = pre.deploy_contract( + code=Bytecode(), # Empty bytecode - address from stub + stub=factory_stub, + ) + + # Create attack contract that maximizes EXTCODESIZE calls + # The attack will: + # 1. Call factory.getConfig() to get (num_deployed, init_code_hash) + # 2. Generate CREATE2 addresses for all deployed contracts + # 3. Call EXTCODESIZE on each until gas runs out + + # First, call factory to get config + attack_code = ( + # Call factory.getConfig() - returns (uint256 num_deployed, bytes32 init_code_hash) + Op.PUSH1(0x40) # retSize (64 bytes for 2 uint256s) + + Op.PUSH1(0x60) # retOffset + + Op.PUSH1(0) # argSize + + Op.PUSH1(0) # argOffset + + Op.PUSH20(factory_address) # Factory address + + Op.GAS # Use all available gas + + Op.STATICCALL + + # Check if call succeeded + + Op.ISZERO + + Op.PUSH2(0x1000) # Jump to end if failed + + Op.JUMPI + + # Load num_deployed and init_code_hash from memory + + Op.PUSH1(0x60) + + Op.MLOAD # num_deployed at memory[96] + + Op.PUSH1(0x80) + + Op.MLOAD # init_code_hash at memory[128] + + # Prepare for CREATE2 address generation loop + # Memory layout for CREATE2 hash: + # [0x00-0x0A]: padding (11 bytes) + # [0x0B]: 0xFF (1 byte) + # [0x0C-0x1F]: factory address (20 bytes) + # [0x20-0x3F]: salt (32 bytes) + # [0x40-0x5F]: init_code_hash (32 bytes) + + # Store factory address at correct position + + Op.PUSH20(factory_address) + + Op.PUSH1(0) + + Op.MSTORE # Store at 0x00 (will be at 0x0C after 12 byte offset) + + # Store 0xFF marker + + Op.PUSH1(0xFF) + + Op.PUSH1(0x0B) + + Op.MSTORE8 + + # Store init_code_hash + + Op.DUP2 # init_code_hash + + Op.PUSH1(0x40) + + Op.MSTORE + + # Initialize salt counter (starts at 0) + + Op.PUSH1(0) + + Op.PUSH1(0x20) + + Op.MSTORE + + # Main loop: generate addresses and call EXTCODESIZE + # Stack: [num_deployed, init_code_hash] + + Op.SWAP1 + + Op.POP # Remove init_code_hash, keep num_deployed + + # Loop start + + Op.JUMPDEST # Loop label at PC ~100 + + # Check if we've processed all contracts + + Op.DUP1 # Duplicate num_deployed + + Op.PUSH1(0x20) + + Op.MLOAD # Load current salt + + Op.GT # num_deployed > salt? + + Op.ISZERO + + Op.PUSH2(0x1000) # Jump to end if done + + Op.JUMPI + + # Generate CREATE2 address + # Hash the CREATE2 input (0xFF + factory + salt + init_code_hash) + + Op.PUSH1(0x55) # Size: 1 + 20 + 32 + 32 = 85 bytes + + Op.PUSH1(0x0B) # Offset: start from 0xFF marker + + Op.SHA3 + + # The address is the last 20 bytes of the hash + # Call EXTCODESIZE on this address + + Op.EXTCODESIZE + + Op.POP # Discard the result + + # Increment salt + + Op.PUSH1(0x20) + + Op.MLOAD # Load current salt + + Op.PUSH1(1) + + Op.ADD + + Op.PUSH1(0x20) + + Op.MSTORE # Store updated salt + + # Continue loop + + Op.PUSH1(100) # Jump back to loop start (approximate PC) + + Op.JUMP + + # End of execution + + Op.JUMPDEST # End label at PC 0x1000 + ) + + # Deploy the attack contract + attack_address = pre.deploy_contract(code=attack_code) + + # Fund the sender + # G_BASE is 21000 for transaction intrinsic cost + # Fusaka transaction gas limit (16M gas) + FUSAKA_TX_GAS_LIMIT = 16_000_000 + + # Calculate how many transactions we need for the total gas + total_gas_needed = gas_benchmark_value + num_txs = (total_gas_needed + FUSAKA_TX_GAS_LIMIT - 1) // FUSAKA_TX_GAS_LIMIT # Ceiling division + + print(f"EXTCODESIZE Attack Configuration:") + print(f" Total gas budget: {total_gas_needed:,}") + print(f" Fusaka TX limit: {FUSAKA_TX_GAS_LIMIT:,}") + print(f" Number of transactions: {num_txs}") + print(f" Contracts to attack: {38000:,}") + + # Fund the sender with enough for all transactions + sender = pre.fund_eoa(total_gas_needed * 21000 * 2) + + # Create multiple transactions to fill the block + txs = [] + remaining_gas = total_gas_needed + + for i in range(num_txs): + # Each transaction uses up to FUSAKA_TX_GAS_LIMIT + tx_gas = min(remaining_gas, FUSAKA_TX_GAS_LIMIT) + + tx = Transaction( + gas_limit=tx_gas, + to=attack_address, + sender=sender, + data=b"", # No calldata needed + value=0, + nonce=i, # Increment nonce for each transaction + ) + txs.append(tx) + remaining_gas -= tx_gas + + # Create block with all attack transactions + # In execute mode, this will run against real deployed contracts + block = Block( + txs=txs, + exception=None, # Transactions should succeed and use most/all gas + ) + + # No post-state verification needed in execute mode + # The test passes if it executes without error and consumes gas + post = {} + + blockchain_test( + pre=pre, + post=post, + blocks=[block], + ) \ No newline at end of file From 39142e6ecd12a07f286da4cfaf80d57edbcf1a75 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 1 Jan 2026 22:16:26 +0100 Subject: [PATCH 02/15] fix(bloatnet): improve contract deployment reliability - Remove unused calculate_init_hashes.py script - Fix nonce handling by using pending tx count to avoid "already known" errors - Use actual network block gas limit instead of hardcoded Fusaka limit - Add fixed gas price for dev mode compatibility - Fix size key naming for integer sizes (e.g., 1.0 -> "1kb" not "1.0kb") --- .../bloatnet/calculate_init_hashes.py | 74 ------------------- .../bloatnet/deploy_contracts_multi.py | 20 ++--- 2 files changed, 11 insertions(+), 83 deletions(-) delete mode 100644 tests/benchmark/stateful/bloatnet/calculate_init_hashes.py diff --git a/tests/benchmark/stateful/bloatnet/calculate_init_hashes.py b/tests/benchmark/stateful/bloatnet/calculate_init_hashes.py deleted file mode 100644 index 77d3e9a0668..00000000000 --- a/tests/benchmark/stateful/bloatnet/calculate_init_hashes.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python3 -""" -Calculate init code hashes for all contract sizes. -""" - -from execution_testing import Op, While -from eth_utils import keccak - -# Maximum contract size in bytes (24 KB) -MAX_CONTRACT_SIZE = 24576 - -def build_initcode(target_size_kb: float) -> bytes: - """ - Build initcode that generates contracts of specific size using ADDRESS for randomness. - """ - target_size = int(target_size_kb * 1024) - - if target_size > MAX_CONTRACT_SIZE: - target_size = MAX_CONTRACT_SIZE - - # For small contracts (< 1KB), use simple padding - if target_size < 1024: - initcode = ( - # Store deployer address for uniqueness - Op.MSTORE(0, Op.ADDRESS) - # Pad with JUMPDEST opcodes (1 byte each) - + Op.JUMPDEST * max(0, target_size - 33 - 10) # Account for other opcodes - # Ensure first byte is STOP - + Op.MSTORE8(0, 0x00) - # Return the contract - + Op.RETURN(0, target_size) - ) - else: - # For larger contracts, use the keccak256 expansion pattern - # Generate XOR table for expansion - xor_table_size = min(256, target_size // 256) - xor_table = [keccak(i.to_bytes(32, "big")) for i in range(xor_table_size)] - - initcode = ( - # Store ADDRESS as initial seed - creates uniqueness per deployment - Op.MSTORE(0, Op.ADDRESS) - # Loop to expand bytecode using SHA3 and XOR operations - + While( - body=( - Op.SHA3(Op.SUB(Op.MSIZE, 32), 32) - # Use XOR table to expand without excessive SHA3 calls - + sum( - (Op.PUSH32(xor_value) + Op.XOR + Op.DUP1 + Op.MSIZE + Op.MSTORE) - for xor_value in xor_table - ) - + Op.POP - ), - condition=Op.LT(Op.MSIZE, target_size), - ) - # Set first byte to STOP for efficient CALL handling - + Op.MSTORE8(0, 0x00) - # Return the full contract - + Op.RETURN(0, target_size) - ) - - return bytes(initcode) - -# Calculate hashes for all sizes -sizes = [0.5, 1.0, 5.0, 10.0, 24.0] - -print("Init code hashes for each size:") -print("="*60) - -for size_kb in sizes: - initcode = build_initcode(size_kb) - init_hash = keccak(initcode) - print(f"{size_kb:4.1f} KB: 0x{init_hash.hex()}") - print(f" Initcode size: {len(initcode)} bytes") - print() \ No newline at end of file diff --git a/tests/benchmark/stateful/bloatnet/deploy_contracts_multi.py b/tests/benchmark/stateful/bloatnet/deploy_contracts_multi.py index 75525766796..9fb99df503c 100644 --- a/tests/benchmark/stateful/bloatnet/deploy_contracts_multi.py +++ b/tests/benchmark/stateful/bloatnet/deploy_contracts_multi.py @@ -103,18 +103,15 @@ def deploy_contracts(w3: Web3, factory_address: str, count: int, size_kb: float) print(f" (Network allows up to {int(block_gas_limit * 0.8):,} per transaction)") # Calculate optimal batch size based on gas costs - # Fusaka limit: 16M gas per transaction/block - FUSAKA_GAS_LIMIT = 16_000_000 - - # Calculate how many deployments can fit per block + # Use actual network block gas limit to maximize throughput # Each deployment is a separate transaction with our current factory # Leave some margin for safety (use 95% of block gas limit) - usable_gas = int(FUSAKA_GAS_LIMIT * 0.95) + usable_gas = int(block_gas_limit * 0.95) deployments_per_block = usable_gas // gas_limit print(f"Optimal batch size: {deployments_per_block} deployments per block") print(f" (Each deployment uses {gas_limit:,} gas)") - print(f" (Block limit is {FUSAKA_GAS_LIMIT:,}, using {usable_gas:,})") + print(f" (Network block limit is {block_gas_limit:,}, using {usable_gas:,})") # Deploy contracts in optimized batches batch_size = deployments_per_block @@ -129,8 +126,8 @@ def deploy_contracts(w3: Web3, factory_address: str, count: int, size_kb: float) batch_end = min(batch_start + batch_size, remaining) batch_count = batch_end - batch_start - # Get fresh nonce for this batch to avoid "already known" errors - nonce = w3.eth.get_transaction_count(account) + # Get fresh nonce for this batch including pending txs to avoid "already known" errors + nonce = w3.eth.get_transaction_count(account, "pending") # Send batch of transactions rapidly to fill a block tx_hashes = [] @@ -148,6 +145,7 @@ def deploy_contracts(w3: Web3, factory_address: str, count: int, size_kb: float) "to": factory_address, "data": "0x01", # Non-empty data to trigger CREATE2 "gas": gas_limit, + "gasPrice": w3.to_wei(1, 'gwei'), # Fixed low gas price for dev mode "nonce": nonce + i # Pre-calculate nonce for speed }) tx_hashes.append(tx_hash) @@ -267,7 +265,11 @@ def main(): sys.exit(1) # Find the appropriate factory - size_key = f"{args.size}kb".replace(".", "_") + # Handle size key naming: 0.5 -> "0_5kb", 1.0 -> "1kb", 24.0 -> "24kb" + if args.size == int(args.size): + size_key = f"{int(args.size)}kb" + else: + size_key = f"{args.size}kb".replace(".", "_") factory_key = f"bloatnet_factory_{size_key}" factory_address = stubs.get(factory_key) From 46dd391e5fad4778abf594211444b8a94675963c Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 1 Jan 2026 22:17:37 +0100 Subject: [PATCH 03/15] feat(bloatnet): rewrite EXTCODESIZE benchmark for cold access testing Rewrite the test to maximize cold EXTCODESIZE calls per block: - Add attack contract that loops EXTCODESIZE on CREATE2-derived addresses - Each tx passes a starting salt via calldata to access unique contracts - Fill blocks with 3x 16M gas transactions (~17,637 cold accesses/block) - Add verification tx that stores EXTCODESIZE result in storage - Add 2KB size to test parameters - Calculate gas per iteration dynamically from fork gas costs The test now properly stresses client state loading by ensuring all EXTCODESIZE operations are cold (first access to each contract). --- .../test_extcodesize_bytecode_sizes.py | 483 ++++++++++++------ 1 file changed, 324 insertions(+), 159 deletions(-) diff --git a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py index 253a7c70299..98313bb829b 100644 --- a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py +++ b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py @@ -1,9 +1,14 @@ """ Test EXTCODESIZE with parametrized bytecode sizes using CREATE2 factory pattern. -This test executes against pre-deployed contracts via factories, measuring the -performance impact of different contract sizes on EXTCODESIZE operations. +This test executes EXTCODESIZE operations against pre-deployed contracts via factories, +measuring the performance impact of different contract sizes on EXTCODESIZE operations. Designed for execute mode only - contracts must be pre-deployed. + +The test maximizes cold EXTCODESIZE calls to stress client state loading by: +1. Using CREATE2 address derivation to access many unique contracts +2. Filling block gas with transactions as close to FUSAKA_TX_GAS_LIMIT (16M) as possible +3. Verifying contracts exist by checking the last accessed contract's size """ import pytest @@ -16,38 +21,241 @@ Fork, Op, Transaction, + While, ) -# Hardcoded init code hashes for each contract size -# These are deterministic based on our initcode generation in deploy_initcode_multi.py -INIT_CODE_HASHES = { - 0.5: 0xaa03809400f3a470a717403a9600140150129b24180fbaab4a4f58334fc5e5a8, - 1.0: 0x62b07e407cbb2f5bf8d706444fe89b0b40331a14efb0081ba02534ca4f6438ee, - 5.0: 0xdfcaab76f37cb182d0a9e24827c65057c0540c5b736085e3465b99eecd4502c1, - 10.0: 0xc39b2c8f715e341c46b43fda72209e073fe08f9845a586cc5b16ed9cb8a1c5a8, - 24.0: 0xd570c69a8b04a4e65932da40d0f5b2b7f11aaa72d8b8ca3a714fa43077197172, -} +REFERENCE_SPEC_GIT_PATH = "DUMMY/bloatnet.md" +REFERENCE_SPEC_VERSION = "1.0" + +# Fusaka transaction gas limit (16M gas) +FUSAKA_TX_GAS_LIMIT = 16_000_000 def get_factory_stub_name(size_kb: float) -> str: """Generate stub name for factory based on size.""" if size_kb == 0.5: - return "factory_0_5kb" + return "bloatnet_factory_0_5kb" elif size_kb == 1.0: - return "factory_1kb" + return "bloatnet_factory_1kb" + elif size_kb == 2.0: + return "bloatnet_factory_2kb" elif size_kb == 5.0: - return "factory_5kb" + return "bloatnet_factory_5kb" elif size_kb == 10.0: - return "factory_10kb" + return "bloatnet_factory_10kb" elif size_kb == 24.0: - return "factory_24kb" + return "bloatnet_factory_24kb" else: raise ValueError(f"Unsupported size: {size_kb}KB") +def calculate_gas_per_iteration(gas_costs) -> int: + """ + Calculate gas cost per EXTCODESIZE loop iteration. + + Each iteration: + 1. Generate CREATE2 address via SHA3 + 2. Cold EXTCODESIZE access + 3. Increment salt and loop overhead + """ + return ( + # SHA3 for CREATE2 address generation (85 bytes = 3 words) + gas_costs.G_KECCAK_256 # Static cost (30) + + gas_costs.G_KECCAK_256_WORD * 3 # Dynamic cost (3 * 6 = 18) + # EXTCODESIZE cold access + + gas_costs.G_COLD_ACCOUNT_ACCESS # 2600 + # Stack and memory operations for address extraction + + gas_costs.G_VERY_LOW * 2 # PUSH20 + AND for address extraction (6) + # Loop overhead: increment salt, decrement counter, check condition + + gas_costs.G_LOW # MLOAD salt (3) + + gas_costs.G_VERY_LOW # ADD (3) + + gas_costs.G_LOW # MSTORE salt (3) + + gas_costs.G_VERY_LOW * 3 # Counter decrement ops (9) + + gas_costs.G_BASE * 2 # ISZERO checks (4) + + gas_costs.G_MID # JUMPI (8) + + gas_costs.G_JUMPDEST # Loop start (1) + # POP the EXTCODESIZE result + + gas_costs.G_BASE # POP (2) + ) + + +def build_attack_contract(factory_address) -> Bytecode: + """ + Build an attack contract that maximizes EXTCODESIZE calls. + + The contract reads a starting salt from calldata (32 bytes) and: + 1. Calls factory.getConfig() to get (num_deployed, init_code_hash) + 2. Sets up memory for CREATE2 address derivation + 3. Loops through salts starting_salt..starting_salt+N calling EXTCODESIZE + 4. Does NOT write to storage (pure computation for maximum efficiency) + + Calldata format: 32 bytes representing the starting salt (big-endian uint256) + """ + return ( + # === Step 0: Load starting salt from calldata === + # CALLDATALOAD(0) reads 32 bytes from calldata offset 0 + Op.CALLDATALOAD(0) # Stack: [starting_salt] + # === Step 1: Get factory configuration === + # Call factory.getConfig() - returns (uint256 num_deployed, bytes32 init_code_hash) + + Op.STATICCALL( + gas=Op.GAS, + address=factory_address, + args_offset=0, + args_size=0, + ret_offset=96, # Store result at memory[96] + ret_size=64, # 64 bytes for 2 uint256s + ) + # Check if call succeeded (STATICCALL returns 1 on success) + # Stack: [starting_salt, success] + + Op.ISZERO + + Op.PUSH2(0x1000) # Jump to end if failed + + Op.JUMPI + # Stack: [starting_salt] + # Load results from memory + # Memory[96:128] = num_deployed_contracts + # Memory[128:160] = init_code_hash + + Op.MLOAD(96) # Stack: [starting_salt, num_deployed] + + Op.MLOAD(128) # Stack: [starting_salt, num_deployed, init_code_hash] + # === Step 2: Setup CREATE2 address generation in memory === + # Memory layout at offset 0: + # [0x00-0x0A]: padding (11 bytes) + # [0x0B]: 0xFF marker (1 byte) + # [0x0C-0x1F]: factory address right-aligned in 32-byte word (20 bytes used) + # [0x20-0x3F]: salt (32 bytes) + # [0x40-0x5F]: init_code_hash (32 bytes) + # Total CREATE2 input: 85 bytes starting at offset 0x0B + # Store factory address at memory[0] (will be right-aligned, address at bytes 12-31) + + Op.MSTORE(0, factory_address) + # Store 0xFF marker at position 11 (just before the address) + + Op.MSTORE8(11, 0xFF) + # Store init_code_hash at memory[64] + # Stack: [starting_salt, num_deployed, init_code_hash] + + Op.PUSH1(64) + + Op.MSTORE # Stores init_code_hash at memory[64] + # Stack: [starting_salt, num_deployed] + # Store starting salt at memory[32] + + Op.SWAP1 # Stack: [num_deployed, starting_salt] + + Op.DUP1 # Stack: [num_deployed, starting_salt, starting_salt] + + Op.PUSH1(32) + + Op.MSTORE # Store starting_salt at memory[32] + # Stack: [num_deployed, starting_salt] + + Op.POP # Stack: [num_deployed] + # === Step 3: Main loop - EXTCODESIZE operations === + + While( + body=( + # Generate CREATE2 address: keccak256(0xFF ++ factory ++ salt ++ hash) + # Input is 85 bytes starting at offset 11 + Op.SHA3(11, 85) + # Result is 32-byte hash, address is last 20 bytes (low bits) + # AND with address mask to extract address from hash + + Op.PUSH20(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.AND + # Call EXTCODESIZE and discard result (no storage writes!) + + Op.EXTCODESIZE + + Op.POP + # Increment salt for next iteration + + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) + ), + # Continue while counter > 0 + # Decrement counter and check if non-zero + condition=( + Op.PUSH1(1) + + Op.SWAP1 + + Op.SUB + + Op.DUP1 + + Op.ISZERO + + Op.ISZERO # Convert to boolean: 1 if counter > 0 + ), + ) + + Op.POP # Clean up remaining counter + # === End === + + Op.JUMPDEST # 0x1000 - error/end handler + + Op.STOP + ) + + +def calculate_verification_gas(gas_costs, intrinsic_gas: int) -> int: + """ + Calculate the minimum gas needed for the verification transaction. + + The verification contract: + 1. STATICCALL to factory.getConfig() - cold access + 2. Memory operations for CREATE2 setup + 3. SHA3 for address derivation + 4. EXTCODESIZE on target contract - cold access + 5. SSTORE to save result - cold, zero-to-nonzero + """ + verification_execution_gas = ( + # STATICCALL to factory (cold access) + gas_costs.G_COLD_ACCOUNT_ACCESS # 2600 + + 100 # STATICCALL base cost + # Memory operations (MSTORE, MSTORE8, MLOAD) + + gas_costs.G_LOW * 5 # 5 memory ops (3 * 5 = 15) + + gas_costs.G_VERY_LOW # MSTORE8 (3) + # SHA3 for CREATE2 address (85 bytes = 3 words) + + gas_costs.G_KECCAK_256 # 30 + + gas_costs.G_KECCAK_256_WORD * 3 # 18 + # SHR for address extraction + + gas_costs.G_VERY_LOW # 3 + # EXTCODESIZE (cold access to target contract) + + gas_costs.G_COLD_ACCOUNT_ACCESS # 2600 + # SSTORE (cold slot, zero-to-nonzero) + + gas_costs.G_STORAGE_SET # 20000 + + gas_costs.G_COLD_SLOAD # 2100 (cold access) + # STOP + + 0 + ) + # Add intrinsic gas + 10% buffer for safety + total = intrinsic_gas + verification_execution_gas + return int(total * 1.1) + + +def build_verification_contract(factory_address, verification_salt: int) -> Bytecode: + """ + Build a verification contract that stores EXTCODESIZE result. + + The contract: + 1. Calls factory.getConfig() to get init_code_hash + 2. Computes CREATE2 address for the given salt + 3. Calls EXTCODESIZE on that address + 4. Stores the result in storage slot 0 + """ + return ( + # Call factory.getConfig() to get init_code_hash + Op.STATICCALL( + gas=Op.GAS, + address=factory_address, + args_offset=0, + args_size=0, + ret_offset=96, + ret_size=64, + ) + + Op.POP # Discard success flag (assume it works) + # Setup CREATE2 address generation (same layout as attack contract) + + Op.MSTORE(0, factory_address) + + Op.MSTORE8(11, 0xFF) + + Op.MSTORE(32, verification_salt) # Use the target salt + # Load init_code_hash from memory[128] and store at memory[64] + + Op.MLOAD(128) + + Op.PUSH1(64) + + Op.MSTORE + # Generate CREATE2 address + + Op.SHA3(11, 85) + # Address is last 20 bytes (low bits) of hash + + Op.PUSH20(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.AND + # Call EXTCODESIZE + + Op.EXTCODESIZE + # Store result in storage slot 0 + + Op.PUSH1(0) + + Op.SSTORE + + Op.STOP + ) + + @pytest.mark.parametrize( "bytecode_size_kb", - [0.5, 1.0, 5.0, 10.0, 24.0], + [0.5, 1.0, 2.0, 5.0, 10.0, 24.0], ids=lambda size: f"{size}KB", ) @pytest.mark.valid_from("Prague") @@ -59,23 +267,27 @@ def test_extcodesize_bytecode_sizes( gas_benchmark_value: int, ) -> None: """ - Execute EXTCODESIZE attack against pre-deployed contracts. + Execute EXTCODESIZE benchmark against pre-deployed contracts of various sizes. This test: 1. Uses factory addresses passed via stubs (one factory per size) - 2. Reads factory state to get number of deployed contracts + 2. Reads factory state to get number of deployed contracts and init code hash 3. Generates CREATE2 addresses dynamically during execution - 4. Calls EXTCODESIZE on as many contracts as gas allows - 5. Aims to consume all available gas to measure maximum attack capacity + 4. Calls EXTCODESIZE on as many contracts as gas allows (cold access each time) + 5. Fills the block with transactions close to FUSAKA_TX_GAS_LIMIT (16M gas) + 6. Verifies that contracts exist by checking a sample contract's size """ gas_costs = fork.gas_costs() + expected_size_bytes = int(bytecode_size_kb * 1024) - # Calculate gas costs for operations + # Calculate intrinsic transaction cost intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - # Get factory stub name and init code hash for this size + # Calculate gas per EXTCODESIZE iteration + gas_per_iteration = calculate_gas_per_iteration(gas_costs) + + # Get factory stub name for this size factory_stub = get_factory_stub_name(bytecode_size_kb) - init_code_hash = INIT_CODE_HASHES[bytecode_size_kb] # Deploy factory stub (address comes from stub file) factory_address = pre.deploy_contract( @@ -83,159 +295,112 @@ def test_extcodesize_bytecode_sizes( stub=factory_stub, ) - # Create attack contract that maximizes EXTCODESIZE calls - # The attack will: - # 1. Call factory.getConfig() to get (num_deployed, init_code_hash) - # 2. Generate CREATE2 addresses for all deployed contracts - # 3. Call EXTCODESIZE on each until gas runs out - - # First, call factory to get config - attack_code = ( - # Call factory.getConfig() - returns (uint256 num_deployed, bytes32 init_code_hash) - Op.PUSH1(0x40) # retSize (64 bytes for 2 uint256s) - + Op.PUSH1(0x60) # retOffset - + Op.PUSH1(0) # argSize - + Op.PUSH1(0) # argOffset - + Op.PUSH20(factory_address) # Factory address - + Op.GAS # Use all available gas - + Op.STATICCALL - - # Check if call succeeded - + Op.ISZERO - + Op.PUSH2(0x1000) # Jump to end if failed - + Op.JUMPI - - # Load num_deployed and init_code_hash from memory - + Op.PUSH1(0x60) - + Op.MLOAD # num_deployed at memory[96] - + Op.PUSH1(0x80) - + Op.MLOAD # init_code_hash at memory[128] - - # Prepare for CREATE2 address generation loop - # Memory layout for CREATE2 hash: - # [0x00-0x0A]: padding (11 bytes) - # [0x0B]: 0xFF (1 byte) - # [0x0C-0x1F]: factory address (20 bytes) - # [0x20-0x3F]: salt (32 bytes) - # [0x40-0x5F]: init_code_hash (32 bytes) - - # Store factory address at correct position - + Op.PUSH20(factory_address) - + Op.PUSH1(0) - + Op.MSTORE # Store at 0x00 (will be at 0x0C after 12 byte offset) - - # Store 0xFF marker - + Op.PUSH1(0xFF) - + Op.PUSH1(0x0B) - + Op.MSTORE8 - - # Store init_code_hash - + Op.DUP2 # init_code_hash - + Op.PUSH1(0x40) - + Op.MSTORE + # Build and deploy the attack contract + attack_code = build_attack_contract(factory_address) + attack_address = pre.deploy_contract(code=attack_code) - # Initialize salt counter (starts at 0) - + Op.PUSH1(0) - + Op.PUSH1(0x20) - + Op.MSTORE + # Calculate intrinsic cost with 32 bytes of calldata (starting salt) + calldata_for_attack = b"\x00" * 32 # 32 zero bytes for salt + intrinsic_gas_with_calldata = fork.transaction_intrinsic_cost_calculator()( + calldata=calldata_for_attack + ) - # Main loop: generate addresses and call EXTCODESIZE - # Stack: [num_deployed, init_code_hash] - + Op.SWAP1 - + Op.POP # Remove init_code_hash, keep num_deployed + # Calculate how many iterations fit in one transaction + # Reserve gas for: intrinsic cost + setup (getConfig call, memory setup) + cleanup + setup_gas = 5000 # Approximate gas for factory call and memory setup + cleanup_gas = 1000 # Reserve for loop exit and cleanup + available_gas_per_tx = ( + FUSAKA_TX_GAS_LIMIT - intrinsic_gas_with_calldata - setup_gas - cleanup_gas + ) + iterations_per_tx = available_gas_per_tx // gas_per_iteration - # Loop start - + Op.JUMPDEST # Loop label at PC ~100 + # Calculate how many transactions we need to fill the block + # gas_benchmark_value is the total block gas budget + num_attack_txs = gas_benchmark_value // FUSAKA_TX_GAS_LIMIT + if num_attack_txs == 0: + num_attack_txs = 1 - # Check if we've processed all contracts - + Op.DUP1 # Duplicate num_deployed - + Op.PUSH1(0x20) - + Op.MLOAD # Load current salt - + Op.GT # num_deployed > salt? - + Op.ISZERO - + Op.PUSH2(0x1000) # Jump to end if done - + Op.JUMPI + # Total iterations across all transactions (each tx accesses unique contracts) + total_iterations = iterations_per_tx * num_attack_txs - # Generate CREATE2 address - # Hash the CREATE2 input (0xFF + factory + salt + init_code_hash) - + Op.PUSH1(0x55) # Size: 1 + 20 + 32 + 32 = 85 bytes - + Op.PUSH1(0x0B) # Offset: start from 0xFF marker - + Op.SHA3 + # For verification, check the last contract accessed by the last attack tx + # Last tx starts at salt (num_attack_txs - 1) * iterations_per_tx + # and ends at salt (num_attack_txs * iterations_per_tx - 1) + verification_salt = total_iterations - 1 if total_iterations > 0 else 0 - # The address is the last 20 bytes of the hash - # Call EXTCODESIZE on this address - + Op.EXTCODESIZE - + Op.POP # Discard the result - - # Increment salt - + Op.PUSH1(0x20) - + Op.MLOAD # Load current salt - + Op.PUSH1(1) - + Op.ADD - + Op.PUSH1(0x20) - + Op.MSTORE # Store updated salt - - # Continue loop - + Op.PUSH1(100) # Jump back to loop start (approximate PC) - + Op.JUMP - - # End of execution - + Op.JUMPDEST # End label at PC 0x1000 - ) + # Build and deploy verification contract + verification_code = build_verification_contract(factory_address, verification_salt) + verification_address = pre.deploy_contract(code=verification_code) - # Deploy the attack contract - attack_address = pre.deploy_contract(code=attack_code) + # Calculate minimum gas needed for verification tx + verification_gas = calculate_verification_gas(gas_costs, intrinsic_gas) # Fund the sender - # G_BASE is 21000 for transaction intrinsic cost - # Fusaka transaction gas limit (16M gas) - FUSAKA_TX_GAS_LIMIT = 16_000_000 - - # Calculate how many transactions we need for the total gas - total_gas_needed = gas_benchmark_value - num_txs = (total_gas_needed + FUSAKA_TX_GAS_LIMIT - 1) // FUSAKA_TX_GAS_LIMIT # Ceiling division - - print(f"EXTCODESIZE Attack Configuration:") - print(f" Total gas budget: {total_gas_needed:,}") - print(f" Fusaka TX limit: {FUSAKA_TX_GAS_LIMIT:,}") - print(f" Number of transactions: {num_txs}") - print(f" Contracts to attack: {38000:,}") + sender = pre.fund_eoa() - # Fund the sender with enough for all transactions - sender = pre.fund_eoa(total_gas_needed * 21000 * 2) - - # Create multiple transactions to fill the block + # Build transactions txs = [] - remaining_gas = total_gas_needed - - for i in range(num_txs): - # Each transaction uses up to FUSAKA_TX_GAS_LIMIT - tx_gas = min(remaining_gas, FUSAKA_TX_GAS_LIMIT) - tx = Transaction( - gas_limit=tx_gas, + # First transaction: verification (runs first, uses minimal gas) + verification_tx = Transaction( + gas_limit=verification_gas, + to=verification_address, + sender=sender, + data=b"", + value=0, + ) + txs.append(verification_tx) + + # Attack transactions: fill remaining block gas with ~16M gas txs + # Each transaction gets a different starting salt to access unique contracts + for tx_index in range(num_attack_txs): + starting_salt = tx_index * iterations_per_tx + # Encode starting salt as 32-byte big-endian + salt_calldata = starting_salt.to_bytes(32, "big") + attack_tx = Transaction( + gas_limit=FUSAKA_TX_GAS_LIMIT, to=attack_address, sender=sender, - data=b"", # No calldata needed + data=salt_calldata, value=0, - nonce=i, # Increment nonce for each transaction ) - txs.append(tx) - remaining_gas -= tx_gas - - # Create block with all attack transactions - # In execute mode, this will run against real deployed contracts - block = Block( - txs=txs, - exception=None, # Transactions should succeed and use most/all gas - ) - - # No post-state verification needed in execute mode - # The test passes if it executes without error and consumes gas - post = {} + txs.append(attack_tx) + + # Log test configuration + print(f"\n{'='*60}") + print(f"EXTCODESIZE Benchmark: {bytecode_size_kb}KB contracts") + print(f"{'='*60}") + print(f"Block gas budget: {gas_benchmark_value:,}") + print(f"Gas per iteration: ~{gas_per_iteration}") + print(f"Iterations per tx (16M gas): ~{iterations_per_tx:,}") + print(f"Number of attack txs: {num_attack_txs}") + print(f"Total unique EXTCODESIZE calls: ~{total_iterations:,}") + print(f"Salt ranges per tx:") + for i in range(num_attack_txs): + start = i * iterations_per_tx + end = (i + 1) * iterations_per_tx - 1 + print(f" TX{i+1}: salts {start:,} - {end:,}") + print(f"Verification tx gas: {verification_gas:,}") + print(f"Verification salt: {verification_salt:,} (last contract accessed)") + print(f"Expected contract size: {expected_size_bytes} bytes") + print(f"Contracts required: {total_iterations:,}") + print(f"{'='*60}\n") + + # Create block with all transactions + block = Block(txs=txs) + + # Post-state verification: + # Verify that the verification contract stored the expected size + post = { + verification_address: Account( + storage={ + 0: expected_size_bytes, # EXTCODESIZE should return the expected size + } + ), + } blockchain_test( pre=pre, post=post, blocks=[block], - ) \ No newline at end of file + ) From 84c2bebc454d7bf0e34a196b40fd3b46f99ac888 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 1 Jan 2026 22:36:58 +0100 Subject: [PATCH 04/15] docs(bloatnet): add inline documentation for EXTCODESIZE benchmark Add comprehensive docstring explaining the benchmark architecture, execution commands, and block structure with cold access guarantees. --- .../test_extcodesize_bytecode_sizes.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py index 98313bb829b..390266e7089 100644 --- a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py +++ b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py @@ -9,6 +9,59 @@ 1. Using CREATE2 address derivation to access many unique contracts 2. Filling block gas with transactions as close to FUSAKA_TX_GAS_LIMIT (16M) as possible 3. Verifying contracts exist by checking the last accessed contract's size + +This benchmark measures the performance impact of `EXTCODESIZE` operations on contracts +of varying sizes (0.5KB to 24KB). +It stresses client state loading by maximizing **cold** EXTCODESIZE calls per block. + +## Overview + +The test deploys attack contracts that loop through thousands of unique contract addresses, +calling `EXTCODESIZE` on each. +By using CREATE2 address derivation, the test accesses pre-deployed contracts without storing +their addresses, maximizing the number of cold state accesses per block. + +┌─────────────────────────────────────────────────────────────────┐ +│ Test Block │ +├─────────────────────────────────────────────────────────────────┤ +│ TX1: Verification (~30K gas) │ +│ └─> Calls EXTCODESIZE on last contract, stores result │ +│ │ +│ TX2: Attack (~16M gas) │ +│ └─> Loops EXTCODESIZE on salts 0..5,878 │ +│ │ +│ TX3: Attack (~16M gas) │ +│ └─> Loops EXTCODESIZE on salts 5,879..11,757 │ +│ │ +│ TX4: Attack (~16M gas) │ +│ └─> Loops EXTCODESIZE on salts 11,758..17,636 │ +└─────────────────────────────────────────────────────────────────┘ + +### Execute a Single Size + +```bash +uv run execute remote \ + --fork Prague \ + --rpc-endpoint http://127.0.0.1:8545 \ + --rpc-seed-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ + --rpc-chain-id 1337 \ + --address-stubs tests/benchmark/stateful/bloatnet/stubs.json \ + -- -m stateful --gas-benchmark-values 60 \ + tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py -k '24KB' -v +``` + +### Execute All Sizes + +```bash +uv run execute remote \ + --fork Prague \ + --rpc-endpoint http://127.0.0.1:8545 \ + --rpc-seed-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ + --rpc-chain-id 1337 \ + --address-stubs tests/benchmark/stateful/bloatnet/stubs.json \ + -- -m stateful --gas-benchmark-values 60 \ + tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py -v +``` """ import pytest From 38abe437f0c47f9f8befdd110edf721af312b2c7 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 1 Jan 2026 23:18:28 +0100 Subject: [PATCH 05/15] chore(bloatnet): remove deployment helper scripts from PR These utility scripts are for local deployment only and don't need to be part of the test suite. They contain linting issues that are not worth fixing for non-test utility code. --- .../bloatnet/deploy_contracts_multi.py | 315 ------------------ .../stateful/bloatnet/deploy_factory_multi.py | 292 ---------------- .../bloatnet/deploy_initcode_multi.py | 212 ------------ 3 files changed, 819 deletions(-) delete mode 100644 tests/benchmark/stateful/bloatnet/deploy_contracts_multi.py delete mode 100644 tests/benchmark/stateful/bloatnet/deploy_factory_multi.py delete mode 100644 tests/benchmark/stateful/bloatnet/deploy_initcode_multi.py diff --git a/tests/benchmark/stateful/bloatnet/deploy_contracts_multi.py b/tests/benchmark/stateful/bloatnet/deploy_contracts_multi.py deleted file mode 100644 index 9fb99df503c..00000000000 --- a/tests/benchmark/stateful/bloatnet/deploy_contracts_multi.py +++ /dev/null @@ -1,315 +0,0 @@ -#!/usr/bin/env python3 -""" -Deploy multiple contracts via CREATE2 factories for different sizes for BloatNet benchmarks. - -This script deploys contracts of specific sizes using the corresponding CREATE2 factories. - -USAGE: - 1. First deploy initcode contracts: - python3 deploy_initcode_multi.py - - 2. Then deploy the factories: - python3 deploy_factory_multi.py - - 3. Finally deploy contracts: - python3 deploy_contracts_multi.py --size 5 --count 1000 - python3 deploy_contracts_multi.py --size 24 --count 350 - - 4. Run EEST benchmarks: - uv run execute remote --fork Prague \\ - --rpc-endpoint http://127.0.0.1:8545 \\ - --address-stubs stubs.json \\ - -- --gas-benchmark-values 30 \\ - tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py -v - -REQUIREMENTS: - - web3.py - - eth-utils - - Local geth instance running on http://127.0.0.1:8545 - - stubs.json from deploy_factory_multi.py -""" - -import sys -import json -import time -from eth_utils import keccak -from web3 import Web3 - - -def estimate_gas_for_size(size_kb: float, block_gas_limit: int) -> int: - """Estimate gas needed to deploy a contract of given size, respecting network limits. - - Based on actual measurements: - - 0.5KB deployment used ~183,163 gas - - Gas costs breakdown: - - Transaction intrinsic: 21,000 - - Factory execution: ~1,000 - - CREATE2 overhead: ~32,000 - - Contract bytecode storage: 200 gas per byte - - Init code execution: varies by size - """ - size_bytes = int(size_kb * 1024) - - # Precise gas calculation based on actual measurements - if size_kb <= 0.5: - # 0.5KB used 183,163 gas = ~21K intrinsic + ~32K CREATE2 + ~102K storage (512*200) + ~28K execution - base_gas = 21_000 + 32_000 + (size_bytes * 200) + 30_000 - elif size_kb <= 1: - # 1KB: similar but with more storage cost - base_gas = 21_000 + 32_000 + (size_bytes * 200) + 35_000 - elif size_kb <= 5: - # 5KB uses While loop for init, more execution cost - base_gas = 21_000 + 32_000 + (size_bytes * 200) + 150_000 - elif size_kb <= 10: - # 10KB: more While iterations - base_gas = 21_000 + 32_000 + (size_bytes * 200) + 300_000 - else: - # 24KB: maximum While iterations - base_gas = 21_000 + 32_000 + (size_bytes * 200) + 500_000 - - # Add 10% buffer for safety - final_gas = int(base_gas * 1.1) - - # Cap at 80% of block gas limit to ensure inclusion - max_safe_gas = int(block_gas_limit * 0.8) - - return min(final_gas, max_safe_gas) - - -def deploy_contracts(w3: Web3, factory_address: str, count: int, size_kb: float) -> int: - """Deploy contracts via a CREATE2 factory.""" - account = w3.eth.accounts[0] - - # Get network parameters dynamically - latest_block = w3.eth.get_block('latest') - block_gas_limit = latest_block.gasLimit - print(f"Network block gas limit: {block_gas_limit:,}") - - # Get current counter - current = int.from_bytes(w3.eth.get_storage_at(factory_address, 0), 'big') - print(f"Factory has already deployed {current} contracts") - - if count <= current: - print(f"✅ Already have {current} contracts (target: {count})") - return current - - remaining = count - current - print(f"Deploying {remaining} more contracts of {size_kb}KB...") - - # Estimate gas needed based on network's block gas limit - gas_limit = estimate_gas_for_size(size_kb, block_gas_limit) - print(f"Using gas limit: {gas_limit:,} per deployment") - print(f" (Network allows up to {int(block_gas_limit * 0.8):,} per transaction)") - - # Calculate optimal batch size based on gas costs - # Use actual network block gas limit to maximize throughput - # Each deployment is a separate transaction with our current factory - # Leave some margin for safety (use 95% of block gas limit) - usable_gas = int(block_gas_limit * 0.95) - deployments_per_block = usable_gas // gas_limit - - print(f"Optimal batch size: {deployments_per_block} deployments per block") - print(f" (Each deployment uses {gas_limit:,} gas)") - print(f" (Network block limit is {block_gas_limit:,}, using {usable_gas:,})") - - # Deploy contracts in optimized batches - batch_size = deployments_per_block - deployed = 0 - failed = 0 - start_time = time.time() - - print(f"\nDeploying {remaining} contracts in batches of {batch_size}...") - print(f"Expected blocks needed: {(remaining + batch_size - 1) // batch_size}") - - for batch_start in range(0, remaining, batch_size): - batch_end = min(batch_start + batch_size, remaining) - batch_count = batch_end - batch_start - - # Get fresh nonce for this batch including pending txs to avoid "already known" errors - nonce = w3.eth.get_transaction_count(account, "pending") - - # Send batch of transactions rapidly to fill a block - tx_hashes = [] - batch_time = time.time() - - print(f"\nBatch {batch_start//batch_size + 1}: Sending {batch_count} transactions rapidly to fill block...") - print(f" Starting nonce: {nonce}") - - # Send all transactions as fast as possible with pre-calculated nonces - for i in range(batch_count): - try: - # Call factory to deploy a contract - tx_hash = w3.eth.send_transaction({ - "from": account, - "to": factory_address, - "data": "0x01", # Non-empty data to trigger CREATE2 - "gas": gas_limit, - "gasPrice": w3.to_wei(1, 'gwei'), # Fixed low gas price for dev mode - "nonce": nonce + i # Pre-calculate nonce for speed - }) - tx_hashes.append(tx_hash) - except Exception as e: - print(f" Error sending tx {batch_start + i + 1}: {e}") - # Try to recover and continue - break - - send_time = time.time() - batch_time - print(f" Sent {len(tx_hashes)} transactions in {send_time:.2f}s ({len(tx_hashes)/send_time:.1f} tx/s)") - print(f" Waiting for confirmations...") - - # Wait for batch receipts - batch_deployed = 0 - for j, tx_hash in enumerate(tx_hashes): - try: - receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=60) - if receipt['status'] == 1: - deployed += 1 - batch_deployed += 1 - else: - failed += 1 - print(f" Transaction {j+1} failed") - except Exception as e: - print(f" Transaction {j+1} failed or timed out: {e}") - failed += 1 - - # Progress update - counter = int.from_bytes(w3.eth.get_storage_at(factory_address, 0), 'big') - elapsed = time.time() - start_time - rate = deployed / elapsed if elapsed > 0 else 0 - eta = (remaining - deployed) / rate if rate > 0 else 0 - batch_elapsed = time.time() - batch_time - - print(f" Batch complete: {batch_deployed}/{batch_count} deployed in {batch_elapsed:.1f}s") - print(f" Gas used per deployment: {gas_limit:,} ({batch_deployed * gas_limit:,} total)") - print(f" Overall: {counter}/{count} contracts ({deployed}/{remaining} new)") - print(f" Rate: {rate:.1f} contracts/sec, ETA: {eta:.0f}s") - - if failed > 20: - print("\n⚠️ Too many failures, stopping...") - break - - # Final check - final_counter = int.from_bytes(w3.eth.get_storage_at(factory_address, 0), 'big') - elapsed = time.time() - start_time - print(f"\n✅ Deployment complete in {elapsed:.1f} seconds") - print(f"Total contracts deployed: {final_counter}") - - return final_counter - - -def verify_contracts(w3: Web3, factory_address: str, count: int, size_kb: float) -> bool: - """Verify that contracts exist at expected CREATE2 addresses.""" - print(f"\n--- Verifying CREATE2 Addresses for {size_kb}KB contracts ---") - - # Get init code hash from factory storage - stored_hash = w3.eth.get_storage_at(factory_address, 1) - - # Verify a sample of contracts - sample_size = min(5, count) - verified = 0 - - for salt in range(sample_size): - # Calculate CREATE2 address - create2_input = ( - b"\xff" + - bytes.fromhex(factory_address[2:].lower()) + - salt.to_bytes(32, "big") + - stored_hash - ) - expected_addr = Web3.to_checksum_address("0x" + keccak(create2_input)[-20:].hex()) - - # Check if contract exists - code = w3.eth.get_code(expected_addr) - if len(code) > 0: - print(f" Salt {salt}: ✅ Found at {expected_addr} ({len(code)} bytes)") - verified += 1 - else: - print(f" Salt {salt}: ❌ Not found at {expected_addr}") - - return verified == sample_size - - -def main(): - """Main deployment script.""" - import argparse - - parser = argparse.ArgumentParser(description='Deploy BloatNet contracts via CREATE2 factory') - parser.add_argument('--size', type=float, required=True, - choices=[0.5, 1, 2, 5, 10, 24], - help='Contract size in KB') - parser.add_argument('--count', type=int, required=True, - help='Total number of contracts to deploy') - parser.add_argument('--rpc-url', default='http://127.0.0.1:8545', - help='RPC URL') - parser.add_argument('--stubs', default='stubs.json', - help='Path to stubs JSON file') - args = parser.parse_args() - - # Connect to local geth instance - w3 = Web3(Web3.HTTPProvider(args.rpc_url)) - if not w3.is_connected(): - print(f"❌ Failed to connect to {args.rpc_url}") - sys.exit(1) - - print(f"Connected to: {args.rpc_url}") - print(f"Account: {w3.eth.accounts[0]}") - - # Load factory address from stubs - try: - with open(args.stubs, 'r') as f: - stubs = json.load(f) - except FileNotFoundError: - print(f"❌ Stubs file not found: {args.stubs}") - print("Run deploy_factory_multi.py first") - sys.exit(1) - - # Find the appropriate factory - # Handle size key naming: 0.5 -> "0_5kb", 1.0 -> "1kb", 24.0 -> "24kb" - if args.size == int(args.size): - size_key = f"{int(args.size)}kb" - else: - size_key = f"{args.size}kb".replace(".", "_") - factory_key = f"bloatnet_factory_{size_key}" - factory_address = stubs.get(factory_key) - - if not factory_address: - print(f"❌ Factory not found for {args.size}KB contracts") - print(f"Looking for key: {factory_key}") - print(f"Available factories: {list(stubs.keys())}") - sys.exit(1) - - print(f"Using factory at: {factory_address}") - - # Deploy contracts - final_count = deploy_contracts(w3, factory_address, args.count, args.size) - - # Verify deployment - if verify_contracts(w3, factory_address, final_count, args.size): - print(f"\n✅ Successfully verified {args.size}KB contracts") - - # Calculate gas requirements for testing - cost_per_contract = 2660 # Approximate gas per EXTCODESIZE with CREATE2 - test_gas_30m = 30_000_000 - max_contracts_30m = (test_gas_30m - 21000 - 1000) // cost_per_contract - - print(f"\n=== Ready for Testing ===") - print(f"Contract size: {args.size}KB") - print(f"Contracts deployed: {final_count}") - print(f"Factory address: {factory_address}") - print(f"Max contracts for 30M gas: ~{max_contracts_30m}") - - if final_count < max_contracts_30m: - print(f"⚠️ Consider deploying {max_contracts_30m - final_count} more contracts " - f"to fully utilize 30M gas") - - print("\nTo run the benchmark test:") - print("uv run execute remote --fork Prague \\") - print(f" --rpc-endpoint {args.rpc_url} \\") - print(f" --address-stubs {args.stubs} \\") - print(" -- --gas-benchmark-values 30 \\") - print(" tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py \\") - print(f" -k '{args.size}KB' -v") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/tests/benchmark/stateful/bloatnet/deploy_factory_multi.py b/tests/benchmark/stateful/bloatnet/deploy_factory_multi.py deleted file mode 100644 index 7e161c2a3d0..00000000000 --- a/tests/benchmark/stateful/bloatnet/deploy_factory_multi.py +++ /dev/null @@ -1,292 +0,0 @@ -#!/usr/bin/env python3 -""" -Deploy CREATE2 factories for different contract sizes for BloatNet benchmarks. - -This script deploys CREATE2 factories that can deploy contracts of specific sizes -(0.5, 1, 2, 5, 10, 24 KB) using the corresponding initcode contracts. - -USAGE: - 1. First deploy initcode contracts: - python3 deploy_initcode_multi.py - - 2. Then deploy the factories: - python3 deploy_factory_multi.py - - 3. The factory addresses will be saved to stubs.json - -REQUIREMENTS: - - web3.py - - eth-utils - - ethereum-test-tools (for Opcodes) - - Local geth instance running on http://127.0.0.1:8545 - - initcode_addresses.json from deploy_initcode_multi.py -""" - -import sys -import json -from pathlib import Path -from typing import Dict, Any, Tuple - -# Add path to execution-spec-tests if needed -# sys.path.insert(0, "/path/to/execution-spec-tests") - -from execution_testing import Op -from eth_utils import keccak -from web3 import Web3 - - -def build_factory(initcode_address: str, initcode_hash: bytes, initcode_size: int) -> bytes: - """ - Build a CREATE2 factory contract with getConfig() method. - - Storage layout: - - Slot 0: Counter (number of deployed contracts) - - Slot 1: Init code hash for CREATE2 address calculation - - Slot 2: Init code address - - Interface: - - When called with CALLDATASIZE == 0: Returns (num_deployed_contracts, init_code_hash) - - When called otherwise: Deploys a new contract via CREATE2 - """ - - # Factory constructor: Store init code hash and address - factory_constructor = ( - Op.PUSH32(initcode_hash) # Push init code hash - + Op.PUSH1(1) # Slot 1 - + Op.SSTORE # Store init code hash - + Op.PUSH20(bytes.fromhex(initcode_address[2:])) # Push initcode address - + Op.PUSH1(2) # Slot 2 - + Op.SSTORE # Store initcode address - ) - - # Factory runtime code - factory_runtime = ( - # Check if this is a getConfig() call (CALLDATASIZE == 0) - Op.CALLDATASIZE - + Op.ISZERO - + Op.PUSH1(0x31) # Jump to getConfig (hardcoded offset) - + Op.JUMPI - - # === CREATE2 DEPLOYMENT PATH === - # Load initcode address from storage slot 2 - + Op.PUSH1(2) # Slot 2 - + Op.SLOAD # Load initcode address - - # EXTCODECOPY: copy initcode to memory - + Op.PUSH2(initcode_size) # Size - + Op.PUSH1(0) # Source offset - + Op.PUSH1(0) # Dest offset - + Op.DUP4 # Address (from bottom of stack) - + Op.EXTCODECOPY # Copy initcode to memory - - # Prepare for CREATE2 - + Op.PUSH2(initcode_size) # Size - + Op.SWAP1 # Put size under address - + Op.POP # Remove address - - # CREATE2 with current counter as salt - + Op.PUSH1(0) # Slot 0 - + Op.SLOAD # Load counter (use as salt) - + Op.SWAP1 # Put size on top - + Op.PUSH1(0) # Offset in memory - + Op.PUSH1(0) # Value - + Op.CREATE2 # Create contract - - # Store the created address for return - + Op.DUP1 - + Op.PUSH1(0) - + Op.MSTORE - - # Increment counter - + Op.PUSH1(0) # Slot 0 - + Op.DUP1 # Duplicate - + Op.SLOAD # Load counter - + Op.PUSH1(1) # Increment - + Op.ADD # Add - + Op.SWAP1 # Swap - + Op.SSTORE # Store new counter - - # Return the created address - + Op.PUSH1(32) # Return 32 bytes - + Op.PUSH1(0) # From memory position 0 - + Op.RETURN - - # === GETCONFIG PATH === - + Op.JUMPDEST # Destination for getConfig (0x31) - + Op.PUSH1(0) # Slot 0 - + Op.SLOAD # Load number of deployed contracts - + Op.PUSH1(0) # Memory position 0 - + Op.MSTORE # Store in memory - - + Op.PUSH1(1) # Slot 1 - + Op.SLOAD # Load init code hash - + Op.PUSH1(32) # Memory position 32 - + Op.MSTORE # Store in memory - - + Op.PUSH1(64) # Return 64 bytes (2 * 32) - + Op.PUSH1(0) # From memory position 0 - + Op.RETURN - ) - - # Build deployment bytecode - factory_runtime_bytes = bytes(factory_runtime) - runtime_size = len(factory_runtime_bytes) - - # Deployment code that copies and returns runtime - constructor_bytes = bytes(factory_constructor) - constructor_size = len(constructor_bytes) - deployer_size = 14 # Size of deployer code below - runtime_offset = constructor_size + deployer_size - - deployer = ( - # Copy runtime code to memory - Op.PUSH2(runtime_size) # Size of runtime (3 bytes) - + Op.PUSH1(runtime_offset) # Offset to runtime (2 bytes) - + Op.PUSH1(0) # Dest in memory (2 bytes) - + Op.CODECOPY # (1 byte) - # Return runtime code - + Op.PUSH2(runtime_size) # Size to return (3 bytes) - + Op.PUSH1(0) # Offset in memory (2 bytes) - + Op.RETURN # (1 byte) = Total: 14 bytes - ) - - factory_deployment = factory_constructor + deployer + factory_runtime - return bytes(factory_deployment) - - -def deploy_factory(w3: Web3, size_kb: float, initcode_info: Dict[str, Any]) -> Tuple[str, bytes]: - """Deploy a CREATE2 factory for a specific contract size.""" - account = w3.eth.accounts[0] - - print(f"\n--- Deploying Factory for {size_kb}KB contracts ---") - - initcode_address = initcode_info["address"] - initcode_hash_str = initcode_info["hash"] - initcode_size = initcode_info["bytecode_size"] - - # Convert hash string to bytes - initcode_hash = bytes.fromhex(initcode_hash_str[2:] if initcode_hash_str.startswith("0x") else initcode_hash_str) - - print(f"Using initcode at: {initcode_address}") - print(f"Init code hash: {initcode_hash_str}") - print(f"Init code size: {initcode_size} bytes") - - # Build factory bytecode - factory_bytecode = build_factory(initcode_address, initcode_hash, initcode_size) - print(f"Factory deployment size: {len(factory_bytecode)} bytes") - - # Deploy factory - tx_hash = w3.eth.send_transaction({ - "from": account, - "data": "0x" + factory_bytecode.hex(), - "gas": 10000000 # 10M gas for deployment - }) - receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - - if receipt['status'] != 1: - print(f"❌ Failed to deploy factory for {size_kb}KB") - return None, None - - factory_address = receipt['contractAddress'] - print(f"✅ Factory deployed at: {factory_address}") - - # Verify factory storage - counter = w3.eth.get_storage_at(factory_address, 0) - stored_hash = w3.eth.get_storage_at(factory_address, 1) - stored_initcode_addr = w3.eth.get_storage_at(factory_address, 2) - - print(f"Factory storage verification:") - print(f" Slot 0 (counter): {int.from_bytes(counter, 'big')}") - print(f" Slot 1 (hash): 0x{stored_hash.hex()}") - print(f" Slot 2 (initcode): 0x{stored_initcode_addr.hex()}") - print(f" Hash matches: {stored_hash == initcode_hash}") - - return factory_address, initcode_hash - - -def test_getconfig(w3: Web3, factory_address: str, expected_hash: bytes) -> bool: - """Test the getConfig() method of a factory.""" - print(f"Testing getConfig() on factory at {factory_address}...") - - # Call getConfig() with empty calldata - result = w3.eth.call({ - "to": factory_address, - "data": "" # Empty calldata triggers getConfig - }) - - if len(result) != 64: - print(f"❌ Unexpected result length: {len(result)} (expected 64)") - return False - - # Parse the result - num_deployed = int.from_bytes(result[:32], 'big') - returned_hash = result[32:64] - - print(f" Number of deployed contracts: {num_deployed}") - print(f" Init code hash: 0x{returned_hash.hex()}") - print(f" Hash matches expected: {returned_hash == expected_hash}") - - return returned_hash == expected_hash - - -def main(): - """Deploy factories for all contract sizes.""" - import argparse - - parser = argparse.ArgumentParser(description='Deploy CREATE2 factories for multiple sizes') - parser.add_argument('--rpc-url', default='http://127.0.0.1:8545', help='RPC URL') - parser.add_argument('--initcode-file', default='initcode_addresses.json', - help='Path to initcode addresses JSON file') - parser.add_argument('--output', default='stubs.json', - help='Output file for factory addresses (default: stubs.json)') - args = parser.parse_args() - - # Connect to local geth instance - w3 = Web3(Web3.HTTPProvider(args.rpc_url)) - if not w3.is_connected(): - print(f"❌ Failed to connect to {args.rpc_url}") - sys.exit(1) - - print(f"Connected to: {args.rpc_url}") - print(f"Account: {w3.eth.accounts[0]}") - - # Load initcode addresses - try: - with open(args.initcode_file, 'r') as f: - initcode_data = json.load(f) - except FileNotFoundError: - print(f"❌ {args.initcode_file} not found") - print("Run deploy_initcode_multi.py first") - sys.exit(1) - - # Deploy factory for each size - stubs = {} - for key, initcode_info in initcode_data.items(): - size_kb = initcode_info["size_kb"] - factory_address, initcode_hash = deploy_factory(w3, size_kb, initcode_info) - - if factory_address: - # Test getConfig - if test_getconfig(w3, factory_address, initcode_hash): - print(f"✅ Factory for {size_kb}KB working correctly") - - # Add to stubs with descriptive key - stub_key = f"bloatnet_factory_{key}" - stubs[stub_key] = factory_address - - # Save stubs to file - if stubs: - with open(args.output, 'w') as f: - json.dump(stubs, f, indent=2) - print(f"\n✅ All factory addresses saved to {args.output}") - - # Print summary - print("\n=== Factory Summary ===") - for stub_key, address in stubs.items(): - print(f"{stub_key}: {address}") - else: - print("\n❌ No factories deployed successfully") - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/tests/benchmark/stateful/bloatnet/deploy_initcode_multi.py b/tests/benchmark/stateful/bloatnet/deploy_initcode_multi.py deleted file mode 100644 index 96e4f47e2e8..00000000000 --- a/tests/benchmark/stateful/bloatnet/deploy_initcode_multi.py +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env python3 -""" -Deploy multiple initcode contracts for different bytecode sizes for BloatNet benchmarks. - -This script deploys initcode contracts that will be used by CREATE2 factories -to create contracts of different sizes (0.5, 1, 2, 5, 10, 24 KB). - -USAGE: - 1. Start a local geth instance: - geth --dev --http --http.api eth,web3,net,debug --http.corsdomain "*" - - 2. Run this script to deploy all initcode contracts: - python3 deploy_initcode_multi.py - - 3. The initcode addresses will be saved to initcode_addresses.json - -REQUIREMENTS: - - web3.py - - eth-utils - - ethereum-test-tools (for Opcodes) - - Local geth instance running on http://127.0.0.1:8545 -""" - -import sys -import json -from pathlib import Path -from typing import Dict, Any - -# Add path to execution-spec-tests if needed -# sys.path.insert(0, "/path/to/execution-spec-tests") - -from execution_testing import Op, While -from eth_utils import keccak -from web3 import Web3 - -# Contract size configurations (in KB) -CONTRACT_SIZES_KB = [0.5, 1, 2, 5, 10, 24] - -# Maximum contract size in bytes (24 KB) -MAX_CONTRACT_SIZE = 24576 - -def build_initcode(target_size_kb: float) -> bytes: - """ - Build initcode that generates contracts of specific size using ADDRESS for randomness. - - Args: - target_size_kb: Target contract size in kilobytes - - Returns: - The initcode bytecode that will generate a contract of the specified size - """ - target_size = int(target_size_kb * 1024) - - if target_size > MAX_CONTRACT_SIZE: - target_size = MAX_CONTRACT_SIZE - - # For small contracts (< 1KB), use simple padding - if target_size < 1024: - initcode = ( - # Store deployer address for uniqueness - Op.MSTORE(0, Op.ADDRESS) - # Pad with JUMPDEST opcodes (1 byte each) - + Op.JUMPDEST * max(0, target_size - 33 - 10) # Account for other opcodes - # Ensure first byte is STOP - + Op.MSTORE8(0, 0x00) - # Return the contract - + Op.RETURN(0, target_size) - ) - else: - # For larger contracts, use the keccak256 expansion pattern - # Generate XOR table for expansion - xor_table_size = min(256, target_size // 256) - xor_table = [keccak(i.to_bytes(32, "big")) for i in range(xor_table_size)] - - initcode = ( - # Store ADDRESS as initial seed - creates uniqueness per deployment - Op.MSTORE(0, Op.ADDRESS) - # Loop to expand bytecode using SHA3 and XOR operations - + While( - body=( - Op.SHA3(Op.SUB(Op.MSIZE, 32), 32) - # Use XOR table to expand without excessive SHA3 calls - + sum( - (Op.PUSH32(xor_value) + Op.XOR + Op.DUP1 + Op.MSIZE + Op.MSTORE) - for xor_value in xor_table - ) - + Op.POP - ), - condition=Op.LT(Op.MSIZE, target_size), - ) - # Set first byte to STOP for efficient CALL handling - + Op.MSTORE8(0, 0x00) - # Return the full contract - + Op.RETURN(0, target_size) - ) - - return bytes(initcode) - -def deploy_initcode(w3: Web3, size_kb: float) -> Dict[str, Any]: - """Deploy an initcode contract for a specific size.""" - account = w3.eth.accounts[0] - - print(f"\n--- Building initcode for {size_kb}KB contracts ---") - initcode_bytes = build_initcode(size_kb) - print(f"Initcode size: {len(initcode_bytes)} bytes") - - # Calculate the hash for verification - initcode_hash = keccak(initcode_bytes) - print(f"Initcode hash: 0x{initcode_hash.hex()}") - - # Deploy the initcode as a contract - # We need to deploy the raw initcode bytecode without executing it - # To do this, we create deployment code that just returns the initcode - print(f"Deploying initcode contract for {size_kb}KB...") - - # Create simple deployment bytecode that returns the initcode as-is - deployment_code = ( - # CODECOPY(destOffset=0, offset=codesize_of_prefix, size=initcode_size) - bytes(Op.PUSH2(len(initcode_bytes))) # Push initcode size (3 bytes) - + bytes(Op.DUP1) # Duplicate for RETURN (1 byte) - + bytes(Op.PUSH1(0x0c)) # Offset after this prefix (2 bytes) - + bytes(Op.PUSH1(0)) # Dest offset (2 bytes) - + bytes(Op.CODECOPY) # Copy initcode (1 byte) - # RETURN(offset=0, size=initcode_size) - + bytes(Op.PUSH1(0)) # Offset (2 bytes) - + bytes(Op.RETURN) # Return (1 byte) = Total: 12 bytes (0x0c) - + initcode_bytes # Actual initcode - ) - - # Deploy the contract - tx_hash = w3.eth.send_transaction({ - "from": account, - "data": "0x" + deployment_code.hex(), - "gas": 16_000_000 # Fusaka tx gas limit (16M) - }) - receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - - if receipt['status'] != 1: - print(f"❌ Failed to deploy initcode for {size_kb}KB") - return None - - initcode_address = receipt['contractAddress'] - print(f"✅ Initcode deployed at: {initcode_address}") - - # Verify the deployed bytecode - deployed_code = w3.eth.get_code(initcode_address) - print(f"Deployed bytecode size: {len(deployed_code)} bytes") - - # Verify hash - actual_hash = keccak(deployed_code) - print(f"Actual hash: 0x{actual_hash.hex()}") - - if actual_hash == initcode_hash: - print(f"✅ Hash verification successful") - else: - print(f"⚠️ Hash mismatch! Expected: 0x{initcode_hash.hex()}") - - return { - "size_kb": size_kb, - "address": initcode_address, - "hash": "0x" + initcode_hash.hex(), - "bytecode_size": len(initcode_bytes) - } - -def main(): - """Deploy all initcode contracts for different sizes.""" - import argparse - - parser = argparse.ArgumentParser(description='Deploy initcode contracts for multiple sizes') - parser.add_argument('--rpc-url', default='http://127.0.0.1:8545', help='RPC URL') - parser.add_argument('--sizes', nargs='+', type=float, - help='Contract sizes in KB (default: 0.5 1 2 5 10 24)') - args = parser.parse_args() - - # Use custom sizes if provided - sizes = args.sizes if args.sizes else CONTRACT_SIZES_KB - - # Connect to local geth instance - w3 = Web3(Web3.HTTPProvider(args.rpc_url)) - if not w3.is_connected(): - print(f"❌ Failed to connect to {args.rpc_url}") - sys.exit(1) - - print(f"Connected to: {args.rpc_url}") - print(f"Account: {w3.eth.accounts[0]}") - - # Deploy initcode for each size - results = {} - for size_kb in sizes: - result = deploy_initcode(w3, size_kb) - if result: - # Use string key for JSON compatibility - key = f"{size_kb}kb".replace(".", "_") - results[key] = result - - # Save all results to file - if results: - output_file = "initcode_addresses.json" - with open(output_file, "w") as f: - json.dump(results, f, indent=2) - print(f"\n✅ All initcode addresses saved to {output_file}") - - # Print summary - print("\n=== Summary ===") - for key, info in results.items(): - print(f"{info['size_kb']}KB: {info['address']} (hash: {info['hash'][:10]}...)") - else: - print("\n❌ No initcode contracts deployed successfully") - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file From 343b6724e6e5f5fd1b634449e1e557f541296111 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 1 Jan 2026 23:24:14 +0100 Subject: [PATCH 06/15] style(bloatnet): fix linting issues in EXTCODESIZE benchmark test - Wrap long lines in docstrings to comply with 79 char limit - Use raw docstring for bash command examples with backslashes - Shorten comments to fit line length requirements --- .../test_extcodesize_bytecode_sizes.py | 139 ++++++++++-------- 1 file changed, 74 insertions(+), 65 deletions(-) diff --git a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py index 390266e7089..d996020d628 100644 --- a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py +++ b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py @@ -1,65 +1,68 @@ -""" -Test EXTCODESIZE with parametrized bytecode sizes using CREATE2 factory pattern. +r""" +Test EXTCODESIZE with parametrized bytecode sizes using CREATE2 factory. -This test executes EXTCODESIZE operations against pre-deployed contracts via factories, -measuring the performance impact of different contract sizes on EXTCODESIZE operations. +This test executes EXTCODESIZE operations against pre-deployed contracts +via factories, measuring the performance impact of different contract +sizes on EXTCODESIZE operations. Designed for execute mode only - contracts must be pre-deployed. -The test maximizes cold EXTCODESIZE calls to stress client state loading by: +The test maximizes cold EXTCODESIZE calls to stress client state loading: 1. Using CREATE2 address derivation to access many unique contracts -2. Filling block gas with transactions as close to FUSAKA_TX_GAS_LIMIT (16M) as possible +2. Filling block gas with transactions close to FUSAKA_TX_GAS_LIMIT (16M) 3. Verifying contracts exist by checking the last accessed contract's size -This benchmark measures the performance impact of `EXTCODESIZE` operations on contracts -of varying sizes (0.5KB to 24KB). -It stresses client state loading by maximizing **cold** EXTCODESIZE calls per block. +This benchmark measures the performance impact of `EXTCODESIZE` operations +on contracts of varying sizes (0.5KB to 24KB). +It stresses client state loading by maximizing **cold** EXTCODESIZE calls. ## Overview -The test deploys attack contracts that loop through thousands of unique contract addresses, -calling `EXTCODESIZE` on each. -By using CREATE2 address derivation, the test accesses pre-deployed contracts without storing -their addresses, maximizing the number of cold state accesses per block. - -┌─────────────────────────────────────────────────────────────────┐ -│ Test Block │ -├─────────────────────────────────────────────────────────────────┤ -│ TX1: Verification (~30K gas) │ -│ └─> Calls EXTCODESIZE on last contract, stores result │ -│ │ -│ TX2: Attack (~16M gas) │ -│ └─> Loops EXTCODESIZE on salts 0..5,878 │ -│ │ -│ TX3: Attack (~16M gas) │ -│ └─> Loops EXTCODESIZE on salts 5,879..11,757 │ -│ │ -│ TX4: Attack (~16M gas) │ -│ └─> Loops EXTCODESIZE on salts 11,758..17,636 │ -└─────────────────────────────────────────────────────────────────┘ +The test deploys attack contracts that loop through thousands of unique +contract addresses, calling `EXTCODESIZE` on each. +By using CREATE2 address derivation, the test accesses pre-deployed +contracts without storing their addresses, maximizing the number of cold +state accesses per block. + +┌───────────────────────────────────────────────────────────────┐ +│ Test Block │ +├───────────────────────────────────────────────────────────────┤ +│ TX1: Verification (~30K gas) │ +│ └─> Calls EXTCODESIZE on last contract, stores result │ +│ │ +│ TX2: Attack (~16M gas) │ +│ └─> Loops EXTCODESIZE on salts 0..5,878 │ +│ │ +│ TX3: Attack (~16M gas) │ +│ └─> Loops EXTCODESIZE on salts 5,879..11,757 │ +│ │ +│ TX4: Attack (~16M gas) │ +│ └─> Loops EXTCODESIZE on salts 11,758..17,636 │ +└───────────────────────────────────────────────────────────────┘ ### Execute a Single Size ```bash -uv run execute remote \ - --fork Prague \ - --rpc-endpoint http://127.0.0.1:8545 \ - --rpc-seed-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ - --rpc-chain-id 1337 \ - --address-stubs tests/benchmark/stateful/bloatnet/stubs.json \ - -- -m stateful --gas-benchmark-values 60 \ - tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py -k '24KB' -v +uv run execute remote \\ + --fork Prague \\ + --rpc-endpoint http://127.0.0.1:8545 \\ + --rpc-seed-key \\ + --rpc-chain-id 1337 \\ + --address-stubs tests/benchmark/stateful/bloatnet/stubs.json \\ + -- -m stateful --gas-benchmark-values 60 \\ + tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py \\ + -k '24KB' -v ``` ### Execute All Sizes ```bash -uv run execute remote \ - --fork Prague \ - --rpc-endpoint http://127.0.0.1:8545 \ - --rpc-seed-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ - --rpc-chain-id 1337 \ - --address-stubs tests/benchmark/stateful/bloatnet/stubs.json \ - -- -m stateful --gas-benchmark-values 60 \ +uv run execute remote \\ + --fork Prague \\ + --rpc-endpoint http://127.0.0.1:8545 \\ + --rpc-seed-key \\ + --rpc-chain-id 1337 \\ + --address-stubs tests/benchmark/stateful/bloatnet/stubs.json \\ + -- -m stateful --gas-benchmark-values 60 \\ tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py -v ``` """ @@ -142,14 +145,14 @@ def build_attack_contract(factory_address) -> Bytecode: 3. Loops through salts starting_salt..starting_salt+N calling EXTCODESIZE 4. Does NOT write to storage (pure computation for maximum efficiency) - Calldata format: 32 bytes representing the starting salt (big-endian uint256) + Calldata format: 32 bytes for the starting salt (big-endian uint256) """ return ( # === Step 0: Load starting salt from calldata === # CALLDATALOAD(0) reads 32 bytes from calldata offset 0 Op.CALLDATALOAD(0) # Stack: [starting_salt] # === Step 1: Get factory configuration === - # Call factory.getConfig() - returns (uint256 num_deployed, bytes32 init_code_hash) + # Call factory.getConfig() -> (num_deployed, init_code_hash) + Op.STATICCALL( gas=Op.GAS, address=factory_address, @@ -173,13 +176,13 @@ def build_attack_contract(factory_address) -> Bytecode: # Memory layout at offset 0: # [0x00-0x0A]: padding (11 bytes) # [0x0B]: 0xFF marker (1 byte) - # [0x0C-0x1F]: factory address right-aligned in 32-byte word (20 bytes used) + # [0x0C-0x1F]: factory address right-aligned (20 bytes) # [0x20-0x3F]: salt (32 bytes) # [0x40-0x5F]: init_code_hash (32 bytes) - # Total CREATE2 input: 85 bytes starting at offset 0x0B - # Store factory address at memory[0] (will be right-aligned, address at bytes 12-31) + # Total CREATE2 input: 85 bytes from offset 0x0B + # Store factory address at memory[0] (right-aligned, at bytes 12-31) + Op.MSTORE(0, factory_address) - # Store 0xFF marker at position 11 (just before the address) + # Store 0xFF marker at position 11 (before the address) + Op.MSTORE8(11, 0xFF) # Store init_code_hash at memory[64] # Stack: [starting_salt, num_deployed, init_code_hash] @@ -196,9 +199,8 @@ def build_attack_contract(factory_address) -> Bytecode: # === Step 3: Main loop - EXTCODESIZE operations === + While( body=( - # Generate CREATE2 address: keccak256(0xFF ++ factory ++ salt ++ hash) - # Input is 85 bytes starting at offset 11 - Op.SHA3(11, 85) + # CREATE2 address: keccak256(0xFF ++ factory ++ salt ++ hash) + Op.SHA3(11, 85) # 85 bytes from offset 11 # Result is 32-byte hash, address is last 20 bytes (low bits) # AND with address mask to extract address from hash + Op.PUSH20(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) @@ -263,7 +265,9 @@ def calculate_verification_gas(gas_costs, intrinsic_gas: int) -> int: return int(total * 1.1) -def build_verification_contract(factory_address, verification_salt: int) -> Bytecode: +def build_verification_contract( + factory_address, verification_salt: int +) -> Bytecode: """ Build a verification contract that stores EXTCODESIZE result. @@ -320,14 +324,14 @@ def test_extcodesize_bytecode_sizes( gas_benchmark_value: int, ) -> None: """ - Execute EXTCODESIZE benchmark against pre-deployed contracts of various sizes. + Execute EXTCODESIZE benchmark against pre-deployed contracts. This test: 1. Uses factory addresses passed via stubs (one factory per size) - 2. Reads factory state to get number of deployed contracts and init code hash + 2. Reads factory state to get deployed count and init code hash 3. Generates CREATE2 addresses dynamically during execution - 4. Calls EXTCODESIZE on as many contracts as gas allows (cold access each time) - 5. Fills the block with transactions close to FUSAKA_TX_GAS_LIMIT (16M gas) + 4. Calls EXTCODESIZE on as many contracts as gas allows (cold access) + 5. Fills block with transactions close to FUSAKA_TX_GAS_LIMIT (16M gas) 6. Verifies that contracts exist by checking a sample contract's size """ gas_costs = fork.gas_costs() @@ -359,11 +363,14 @@ def test_extcodesize_bytecode_sizes( ) # Calculate how many iterations fit in one transaction - # Reserve gas for: intrinsic cost + setup (getConfig call, memory setup) + cleanup + # Reserve gas for: intrinsic + setup (getConfig, memory) + cleanup setup_gas = 5000 # Approximate gas for factory call and memory setup cleanup_gas = 1000 # Reserve for loop exit and cleanup available_gas_per_tx = ( - FUSAKA_TX_GAS_LIMIT - intrinsic_gas_with_calldata - setup_gas - cleanup_gas + FUSAKA_TX_GAS_LIMIT + - intrinsic_gas_with_calldata + - setup_gas + - cleanup_gas ) iterations_per_tx = available_gas_per_tx // gas_per_iteration @@ -373,7 +380,7 @@ def test_extcodesize_bytecode_sizes( if num_attack_txs == 0: num_attack_txs = 1 - # Total iterations across all transactions (each tx accesses unique contracts) + # Total iterations across all transactions (each tx uses unique contracts) total_iterations = iterations_per_tx * num_attack_txs # For verification, check the last contract accessed by the last attack tx @@ -382,7 +389,9 @@ def test_extcodesize_bytecode_sizes( verification_salt = total_iterations - 1 if total_iterations > 0 else 0 # Build and deploy verification contract - verification_code = build_verification_contract(factory_address, verification_salt) + verification_code = build_verification_contract( + factory_address, verification_salt + ) verification_address = pre.deploy_contract(code=verification_code) # Calculate minimum gas needed for verification tx @@ -405,7 +414,7 @@ def test_extcodesize_bytecode_sizes( txs.append(verification_tx) # Attack transactions: fill remaining block gas with ~16M gas txs - # Each transaction gets a different starting salt to access unique contracts + # Each tx uses a different starting salt to access unique contracts for tx_index in range(num_attack_txs): starting_salt = tx_index * iterations_per_tx # Encode starting salt as 32-byte big-endian @@ -428,7 +437,7 @@ def test_extcodesize_bytecode_sizes( print(f"Iterations per tx (16M gas): ~{iterations_per_tx:,}") print(f"Number of attack txs: {num_attack_txs}") print(f"Total unique EXTCODESIZE calls: ~{total_iterations:,}") - print(f"Salt ranges per tx:") + print("Salt ranges per tx:") for i in range(num_attack_txs): start = i * iterations_per_tx end = (i + 1) * iterations_per_tx - 1 @@ -447,7 +456,7 @@ def test_extcodesize_bytecode_sizes( post = { verification_address: Account( storage={ - 0: expected_size_bytes, # EXTCODESIZE should return the expected size + 0: expected_size_bytes, # EXTCODESIZE returns expected size } ), } From 9bf05a50f7673cfa6acf136950d24cb10ff3cd86 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 1 Jan 2026 23:38:04 +0100 Subject: [PATCH 07/15] style(bloatnet): apply ruff format to EXTCODESIZE benchmark --- .../stateful/bloatnet/test_extcodesize_bytecode_sizes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py index d996020d628..f15a21810a9 100644 --- a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py +++ b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py @@ -429,9 +429,9 @@ def test_extcodesize_bytecode_sizes( txs.append(attack_tx) # Log test configuration - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print(f"EXTCODESIZE Benchmark: {bytecode_size_kb}KB contracts") - print(f"{'='*60}") + print(f"{'=' * 60}") print(f"Block gas budget: {gas_benchmark_value:,}") print(f"Gas per iteration: ~{gas_per_iteration}") print(f"Iterations per tx (16M gas): ~{iterations_per_tx:,}") @@ -441,12 +441,12 @@ def test_extcodesize_bytecode_sizes( for i in range(num_attack_txs): start = i * iterations_per_tx end = (i + 1) * iterations_per_tx - 1 - print(f" TX{i+1}: salts {start:,} - {end:,}") + print(f" TX{i + 1}: salts {start:,} - {end:,}") print(f"Verification tx gas: {verification_gas:,}") print(f"Verification salt: {verification_salt:,} (last contract accessed)") print(f"Expected contract size: {expected_size_bytes} bytes") print(f"Contracts required: {total_iterations:,}") - print(f"{'='*60}\n") + print(f"{'=' * 60}\n") # Create block with all transactions block = Block(txs=txs) From bd8a7e1cedc48e6d9b616e0e3ce16eab7fc0e7fd Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 2 Jan 2026 12:13:12 +0100 Subject: [PATCH 08/15] fix(benchmark): add missing type annotations for mypy Add type annotations for GasCosts and Address parameters to satisfy mypy strict type checking. --- .../bloatnet/test_extcodesize_bytecode_sizes.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py index f15a21810a9..b5dbc96c52a 100644 --- a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py +++ b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py @@ -70,6 +70,7 @@ import pytest from execution_testing import ( Account, + Address, Alloc, Block, BlockchainTestFiller, @@ -79,6 +80,7 @@ Transaction, While, ) +from execution_testing.forks.gas_costs import GasCosts REFERENCE_SPEC_GIT_PATH = "DUMMY/bloatnet.md" REFERENCE_SPEC_VERSION = "1.0" @@ -105,7 +107,7 @@ def get_factory_stub_name(size_kb: float) -> str: raise ValueError(f"Unsupported size: {size_kb}KB") -def calculate_gas_per_iteration(gas_costs) -> int: +def calculate_gas_per_iteration(gas_costs: GasCosts) -> int: """ Calculate gas cost per EXTCODESIZE loop iteration. @@ -135,7 +137,7 @@ def calculate_gas_per_iteration(gas_costs) -> int: ) -def build_attack_contract(factory_address) -> Bytecode: +def build_attack_contract(factory_address: Address) -> Bytecode: """ Build an attack contract that maximizes EXTCODESIZE calls. @@ -229,7 +231,7 @@ def build_attack_contract(factory_address) -> Bytecode: ) -def calculate_verification_gas(gas_costs, intrinsic_gas: int) -> int: +def calculate_verification_gas(gas_costs: GasCosts, intrinsic_gas: int) -> int: """ Calculate the minimum gas needed for the verification transaction. @@ -266,7 +268,7 @@ def calculate_verification_gas(gas_costs, intrinsic_gas: int) -> int: def build_verification_contract( - factory_address, verification_salt: int + factory_address: Address, verification_salt: int ) -> Bytecode: """ Build a verification contract that stores EXTCODESIZE result. From d5f72dc0796b0f1efe3cb1042666a43c2c9cec76 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Tue, 6 Jan 2026 00:30:44 +0100 Subject: [PATCH 09/15] fix: remove unnecessary address masking in EXTCODESIZE benchmark EVM automatically truncates to 20-byte address when executing EXTCODESIZE, so the PUSH20(0xFF...FF) + AND masking is unnecessary. Ref: https://github.com/ethereum/go-ethereum/blob/b635e063/core/vm/instructions.go#L337 --- .../bloatnet/test_extcodesize_bytecode_sizes.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py index b5dbc96c52a..181e0beb24a 100644 --- a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py +++ b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py @@ -122,8 +122,7 @@ def calculate_gas_per_iteration(gas_costs: GasCosts) -> int: + gas_costs.G_KECCAK_256_WORD * 3 # Dynamic cost (3 * 6 = 18) # EXTCODESIZE cold access + gas_costs.G_COLD_ACCOUNT_ACCESS # 2600 - # Stack and memory operations for address extraction - + gas_costs.G_VERY_LOW * 2 # PUSH20 + AND for address extraction (6) + # Note: No masking needed - EVM auto-truncates to 20 bytes # Loop overhead: increment salt, decrement counter, check condition + gas_costs.G_LOW # MLOAD salt (3) + gas_costs.G_VERY_LOW # ADD (3) @@ -203,10 +202,7 @@ def build_attack_contract(factory_address: Address) -> Bytecode: body=( # CREATE2 address: keccak256(0xFF ++ factory ++ salt ++ hash) Op.SHA3(11, 85) # 85 bytes from offset 11 - # Result is 32-byte hash, address is last 20 bytes (low bits) - # AND with address mask to extract address from hash - + Op.PUSH20(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - + Op.AND + # Hash result - EVM auto-truncates to 20-byte address # Call EXTCODESIZE and discard result (no storage writes!) + Op.EXTCODESIZE + Op.POP @@ -252,8 +248,7 @@ def calculate_verification_gas(gas_costs: GasCosts, intrinsic_gas: int) -> int: # SHA3 for CREATE2 address (85 bytes = 3 words) + gas_costs.G_KECCAK_256 # 30 + gas_costs.G_KECCAK_256_WORD * 3 # 18 - # SHR for address extraction - + gas_costs.G_VERY_LOW # 3 + # Note: No masking needed - EVM auto-truncates to 20 bytes # EXTCODESIZE (cold access to target contract) + gas_costs.G_COLD_ACCOUNT_ACCESS # 2600 # SSTORE (cold slot, zero-to-nonzero) @@ -300,9 +295,7 @@ def build_verification_contract( + Op.MSTORE # Generate CREATE2 address + Op.SHA3(11, 85) - # Address is last 20 bytes (low bits) of hash - + Op.PUSH20(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - + Op.AND + # Result is 32-byte hash - EVM auto-truncates to 20-byte address # Call EXTCODESIZE + Op.EXTCODESIZE # Store result in storage slot 0 From 628f140b805a6e1e071ef8c41d82aeb1276c9935 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Tue, 6 Jan 2026 00:52:34 +0100 Subject: [PATCH 10/15] fix: use Conditional with REVERT instead of invalid JUMPDEST Replace the PUSH2(0x1000) + JUMPI pattern (which relied on jumping to an invalid offset to trigger failure) with an explicit Conditional + REVERT. This is cleaner, more explicit about error handling intent, and doesn't rely on undefined behavior of invalid jump destinations. --- .../bloatnet/test_extcodesize_bytecode_sizes.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py index 181e0beb24a..9fd8a430fe6 100644 --- a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py +++ b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py @@ -75,6 +75,7 @@ Block, BlockchainTestFiller, Bytecode, + Conditional, Fork, Op, Transaction, @@ -164,9 +165,10 @@ def build_attack_contract(factory_address: Address) -> Bytecode: ) # Check if call succeeded (STATICCALL returns 1 on success) # Stack: [starting_salt, success] - + Op.ISZERO - + Op.PUSH2(0x1000) # Jump to end if failed - + Op.JUMPI + + Conditional( + condition=Op.ISZERO, # If call failed (success=0) + if_true=Op.REVERT(0, 0), # Revert with no data + ) # Stack: [starting_salt] # Load results from memory # Memory[96:128] = num_deployed_contracts @@ -221,8 +223,6 @@ def build_attack_contract(factory_address: Address) -> Bytecode: ), ) + Op.POP # Clean up remaining counter - # === End === - + Op.JUMPDEST # 0x1000 - error/end handler + Op.STOP ) From 6f1b1a3748d23460f008445b2145184d8003397a Mon Sep 17 00:00:00 2001 From: CPerezz Date: Tue, 6 Jan 2026 21:45:43 +0100 Subject: [PATCH 11/15] refactor(benchmark): use fork.transaction_gas_limit_cap() instead of hardcoded value Replace hardcoded FUSAKA_TX_GAS_LIMIT constant with dynamic fork.transaction_gas_limit_cap() to correctly use 16,777,216 (2^24) per EIP-7825 and adapt to future fork gas limits. --- .../bloatnet/test_extcodesize_bytecode_sizes.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py index 9fd8a430fe6..382e816d8a7 100644 --- a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py +++ b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py @@ -86,9 +86,6 @@ REFERENCE_SPEC_GIT_PATH = "DUMMY/bloatnet.md" REFERENCE_SPEC_VERSION = "1.0" -# Fusaka transaction gas limit (16M gas) -FUSAKA_TX_GAS_LIMIT = 16_000_000 - def get_factory_stub_name(size_kb: float) -> str: """Generate stub name for factory based on size.""" @@ -326,10 +323,11 @@ def test_extcodesize_bytecode_sizes( 2. Reads factory state to get deployed count and init code hash 3. Generates CREATE2 addresses dynamically during execution 4. Calls EXTCODESIZE on as many contracts as gas allows (cold access) - 5. Fills block with transactions close to FUSAKA_TX_GAS_LIMIT (16M gas) + 5. Fills block with transactions up to the fork's tx gas limit cap 6. Verifies that contracts exist by checking a sample contract's size """ gas_costs = fork.gas_costs() + tx_gas_limit = fork.transaction_gas_limit_cap() expected_size_bytes = int(bytecode_size_kb * 1024) # Calculate intrinsic transaction cost @@ -362,16 +360,13 @@ def test_extcodesize_bytecode_sizes( setup_gas = 5000 # Approximate gas for factory call and memory setup cleanup_gas = 1000 # Reserve for loop exit and cleanup available_gas_per_tx = ( - FUSAKA_TX_GAS_LIMIT - - intrinsic_gas_with_calldata - - setup_gas - - cleanup_gas + tx_gas_limit - intrinsic_gas_with_calldata - setup_gas - cleanup_gas ) iterations_per_tx = available_gas_per_tx // gas_per_iteration # Calculate how many transactions we need to fill the block # gas_benchmark_value is the total block gas budget - num_attack_txs = gas_benchmark_value // FUSAKA_TX_GAS_LIMIT + num_attack_txs = gas_benchmark_value // tx_gas_limit if num_attack_txs == 0: num_attack_txs = 1 @@ -415,7 +410,7 @@ def test_extcodesize_bytecode_sizes( # Encode starting salt as 32-byte big-endian salt_calldata = starting_salt.to_bytes(32, "big") attack_tx = Transaction( - gas_limit=FUSAKA_TX_GAS_LIMIT, + gas_limit=tx_gas_limit, to=attack_address, sender=sender, data=salt_calldata, From 179b01136ad897ffdb49b421139db8223cfaa7a2 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Tue, 6 Jan 2026 21:53:13 +0100 Subject: [PATCH 12/15] fix: add missing overhead_per_contract to test_sload_empty_erc20_balanceof The gas calculation was missing the per-contract overhead term that accounts for loop setup/teardown costs (counter init, JUMPDEST, condition check, etc). This aligns with the pattern used in test_sstore_erc20_approve. --- .../stateful/bloatnet/test_single_opcode.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/benchmark/stateful/bloatnet/test_single_opcode.py b/tests/benchmark/stateful/bloatnet/test_single_opcode.py index 34e2c46434a..487399632d8 100644 --- a/tests/benchmark/stateful/bloatnet/test_single_opcode.py +++ b/tests/benchmark/stateful/bloatnet/test_single_opcode.py @@ -128,6 +128,17 @@ def test_sload_empty_erc20_balanceof( # 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_LOW # MLOAD for While condition check (3) + + gas_costs.G_BASE # ISZERO (2) + + gas_costs.G_BASE # ISZERO (2) + + gas_costs.G_MID # JUMPI (8) + + 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 @@ -155,7 +166,8 @@ def test_sload_empty_erc20_balanceof( ) # Calculate gas budget per contract - available_gas = gas_benchmark_value - intrinsic_gas + total_overhead = intrinsic_gas + (overhead_per_contract * num_contracts) + available_gas = gas_benchmark_value - total_overhead gas_per_contract = available_gas // num_contracts # For each contract: first call is COLD (2600), subsequent are WARM (100) @@ -188,6 +200,7 @@ def test_sload_empty_erc20_balanceof( # Log test requirements print( f"Total gas budget: {gas_benchmark_value / 1_000_000:.1f}M gas. " + f"Overhead per contract: {overhead_per_contract}. " f"~{gas_per_contract / 1_000_000:.1f}M gas per contract, " f"{calls_per_contract} balanceOf calls per contract." ) From e17c069a3602a11931d3a523419699e6e0319bc2 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Wed, 7 Jan 2026 09:49:06 +0100 Subject: [PATCH 13/15] refactor(benchmark): use gas-based loop exit for EXTCODESIZE benchmark Implement Jochem's suggested gas-based loop exit strategy: - Attack contract reads/writes salt from storage slot 0 - Loop exits when gas < 50K, saves salt for next TX to resume - Each TX automatically continues from where previous left off - No manual gas calculations needed - contract self-regulates Changes: - Remove calculate_gas_per_iteration() function - Simplify test function - no calldata/iteration calculations - Update build_attack_contract() with SLOAD/SSTORE for state - Store last EXTCODESIZE result in slot 1 for verification This eliminates error-prone gas calculations and makes the benchmark self-correcting regardless of EVM changes. --- .../test_extcodesize_bytecode_sizes.py | 252 ++++++++---------- 1 file changed, 114 insertions(+), 138 deletions(-) diff --git a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py index 382e816d8a7..1c4f7d53b3a 100644 --- a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py +++ b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py @@ -1,49 +1,45 @@ r""" Test EXTCODESIZE with parametrized bytecode sizes using CREATE2 factory. -This test executes EXTCODESIZE operations against pre-deployed contracts -via factories, measuring the performance impact of different contract -sizes on EXTCODESIZE operations. -Designed for execute mode only - contracts must be pre-deployed. - -The test maximizes cold EXTCODESIZE calls to stress client state loading: -1. Using CREATE2 address derivation to access many unique contracts -2. Filling block gas with transactions close to FUSAKA_TX_GAS_LIMIT (16M) -3. Verifying contracts exist by checking the last accessed contract's size - This benchmark measures the performance impact of `EXTCODESIZE` operations on contracts of varying sizes (0.5KB to 24KB). It stresses client state loading by maximizing **cold** EXTCODESIZE calls. -## Overview +Designed for execute mode only - contracts must be pre-deployed. + +## Gas-Based Loop Strategy + +The attack contract uses a gas-based loop exit (per Jochem's suggestion): +1. Reads current salt from storage slot 0 +2. Loops while gas > 50K, calling EXTCODESIZE on CREATE2 addresses +3. Saves final salt to storage slot 0 when exiting +4. Next TX automatically resumes from where previous left off -The test deploys attack contracts that loop through thousands of unique -contract addresses, calling `EXTCODESIZE` on each. -By using CREATE2 address derivation, the test accesses pre-deployed -contracts without storing their addresses, maximizing the number of cold -state accesses per block. +This eliminates manual gas calculations - the contract self-regulates. + +## Test Block Structure ┌───────────────────────────────────────────────────────────────┐ │ Test Block │ ├───────────────────────────────────────────────────────────────┤ │ TX1: Verification (~30K gas) │ -│ └─> Calls EXTCODESIZE on last contract, stores result │ +│ └─> Calls EXTCODESIZE on salt 0, stores result │ │ │ │ TX2: Attack (~16M gas) │ -│ └─> Loops EXTCODESIZE on salts 0..5,878 │ +│ └─> Loops EXTCODESIZE until gas < 50K, saves salt │ │ │ │ TX3: Attack (~16M gas) │ -│ └─> Loops EXTCODESIZE on salts 5,879..11,757 │ +│ └─> Resumes from TX2's salt, continues looping │ │ │ │ TX4: Attack (~16M gas) │ -│ └─> Loops EXTCODESIZE on salts 11,758..17,636 │ +│ └─> Resumes from TX3's salt, continues looping │ └───────────────────────────────────────────────────────────────┘ ### Execute a Single Size ```bash uv run execute remote \\ - --fork Prague \\ + --fork Osaka \\ --rpc-endpoint http://127.0.0.1:8545 \\ --rpc-seed-key \\ --rpc-chain-id 1337 \\ @@ -57,7 +53,7 @@ ```bash uv run execute remote \\ - --fork Prague \\ + --fork Osaka \\ --rpc-endpoint http://127.0.0.1:8545 \\ --rpc-seed-key \\ --rpc-chain-id 1337 \\ @@ -105,51 +101,33 @@ def get_factory_stub_name(size_kb: float) -> str: raise ValueError(f"Unsupported size: {size_kb}KB") -def calculate_gas_per_iteration(gas_costs: GasCosts) -> int: - """ - Calculate gas cost per EXTCODESIZE loop iteration. - - Each iteration: - 1. Generate CREATE2 address via SHA3 - 2. Cold EXTCODESIZE access - 3. Increment salt and loop overhead - """ - return ( - # SHA3 for CREATE2 address generation (85 bytes = 3 words) - gas_costs.G_KECCAK_256 # Static cost (30) - + gas_costs.G_KECCAK_256_WORD * 3 # Dynamic cost (3 * 6 = 18) - # EXTCODESIZE cold access - + gas_costs.G_COLD_ACCOUNT_ACCESS # 2600 - # Note: No masking needed - EVM auto-truncates to 20 bytes - # Loop overhead: increment salt, decrement counter, check condition - + gas_costs.G_LOW # MLOAD salt (3) - + gas_costs.G_VERY_LOW # ADD (3) - + gas_costs.G_LOW # MSTORE salt (3) - + gas_costs.G_VERY_LOW * 3 # Counter decrement ops (9) - + gas_costs.G_BASE * 2 # ISZERO checks (4) - + gas_costs.G_MID # JUMPI (8) - + gas_costs.G_JUMPDEST # Loop start (1) - # POP the EXTCODESIZE result - + gas_costs.G_BASE # POP (2) - ) - - def build_attack_contract(factory_address: Address) -> Bytecode: """ Build an attack contract that maximizes EXTCODESIZE calls. - The contract reads a starting salt from calldata (32 bytes) and: - 1. Calls factory.getConfig() to get (num_deployed, init_code_hash) - 2. Sets up memory for CREATE2 address derivation - 3. Loops through salts starting_salt..starting_salt+N calling EXTCODESIZE - 4. Does NOT write to storage (pure computation for maximum efficiency) + Uses a gas-based loop exit strategy (per Jochem's suggestion): + 1. Reads current salt from storage slot 0 (resumes from previous TX) + 2. Calls factory.getConfig() to get (num_deployed, init_code_hash) + 3. Loops while gas > GAS_RESERVE, calling EXTCODESIZE on each address + 4. Saves final salt to storage slot 0 for next TX to resume + 5. Saves last EXTCODESIZE result to storage slot 1 for verification + + Storage layout: + - Slot 0: Current/final salt (used for resuming across TXs) + - Slot 1: Last EXTCODESIZE result (used for verification) - Calldata format: 32 bytes for the starting salt (big-endian uint256) + This eliminates manual gas calculations - the contract self-regulates. + Each TX automatically continues from where the previous one left off. """ + # Gas reserve for 2x SSTORE + cleanup + # - Slot 0: warm (after SLOAD), ~5K worst case + # - Slot 1: cold on first TX (~22K), warm after (~3K) + # - 50K provides safe margin for both cases + gas_reserve = 50_000 + return ( - # === Step 0: Load starting salt from calldata === - # CALLDATALOAD(0) reads 32 bytes from calldata offset 0 - Op.CALLDATALOAD(0) # Stack: [starting_salt] + # === Step 0: Load current salt from storage slot 0 === + Op.SLOAD(0) # Stack: [current_salt] # === Step 1: Get factory configuration === # Call factory.getConfig() -> (num_deployed, init_code_hash) + Op.STATICCALL( @@ -161,17 +139,17 @@ def build_attack_contract(factory_address: Address) -> Bytecode: ret_size=64, # 64 bytes for 2 uint256s ) # Check if call succeeded (STATICCALL returns 1 on success) - # Stack: [starting_salt, success] + # Stack: [current_salt, success] + Conditional( condition=Op.ISZERO, # If call failed (success=0) if_true=Op.REVERT(0, 0), # Revert with no data ) - # Stack: [starting_salt] + # Stack: [current_salt] # Load results from memory # Memory[96:128] = num_deployed_contracts # Memory[128:160] = init_code_hash - + Op.MLOAD(96) # Stack: [starting_salt, num_deployed] - + Op.MLOAD(128) # Stack: [starting_salt, num_deployed, init_code_hash] + + Op.MLOAD(96) # Stack: [current_salt, num_deployed] + + Op.MLOAD(128) # Stack: [current_salt, num_deployed, init_code_hash] # === Step 2: Setup CREATE2 address generation in memory === # Memory layout at offset 0: # [0x00-0x0A]: padding (11 bytes) @@ -185,41 +163,61 @@ def build_attack_contract(factory_address: Address) -> Bytecode: # Store 0xFF marker at position 11 (before the address) + Op.MSTORE8(11, 0xFF) # Store init_code_hash at memory[64] - # Stack: [starting_salt, num_deployed, init_code_hash] + # Stack: [current_salt, num_deployed, init_code_hash] + Op.PUSH1(64) + Op.MSTORE # Stores init_code_hash at memory[64] - # Stack: [starting_salt, num_deployed] - # Store starting salt at memory[32] - + Op.SWAP1 # Stack: [num_deployed, starting_salt] - + Op.DUP1 # Stack: [num_deployed, starting_salt, starting_salt] + # Stack: [current_salt, num_deployed] + # Store current salt at memory[32] + + Op.SWAP1 # Stack: [num_deployed, current_salt] + + Op.DUP1 # Stack: [num_deployed, current_salt, current_salt] + Op.PUSH1(32) - + Op.MSTORE # Store starting_salt at memory[32] - # Stack: [num_deployed, starting_salt] + + Op.MSTORE # Store current_salt at memory[32] + # Stack: [num_deployed, current_salt] + Op.POP # Stack: [num_deployed] - # === Step 3: Main loop - EXTCODESIZE operations === + # Initialize last_size at memory[160] to 0 + + Op.PUSH1(0) + + Op.PUSH2(160) + + Op.MSTORE # memory[160] = 0 (last_size) + # === Step 3: Main loop - gas-based exit === + # Loop while gas > gas_reserve AND salt < num_deployed + While( body=( + # Stack: [num_deployed] # CREATE2 address: keccak256(0xFF ++ factory ++ salt ++ hash) Op.SHA3(11, 85) # 85 bytes from offset 11 # Hash result - EVM auto-truncates to 20-byte address - # Call EXTCODESIZE and discard result (no storage writes!) + # Call EXTCODESIZE and store result in memory[160] + Op.EXTCODESIZE - + Op.POP + + Op.PUSH2(160) + + Op.MSTORE # Store size at memory[160] for verification # Increment salt for next iteration + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) ), - # Continue while counter > 0 - # Decrement counter and check if non-zero + # Continue while: gas > gas_reserve AND salt < num_deployed condition=( - Op.PUSH1(1) - + Op.SWAP1 - + Op.SUB - + Op.DUP1 - + Op.ISZERO - + Op.ISZERO # Convert to boolean: 1 if counter > 0 + # Check gas > gas_reserve + Op.GAS + + Op.PUSH3(gas_reserve) + + Op.GT # gas > gas_reserve + # Check salt < num_deployed (num_deployed > salt) + + Op.DUP2 # Stack: [..., (gas>reserve), num_deployed] + + Op.MLOAD(32) # Stack: [..., (gas>res), num_deployed, salt] + + Op.GT # Stack: [..., (gas>reserve), (num_deployed > salt)] + # Both conditions must be true + + Op.AND # (gas > reserve) AND (salt < num_deployed) ), ) - + Op.POP # Clean up remaining counter + # === Step 4: Save state to storage for next TX and verification === + # Stack: [num_deployed] + + Op.POP # Clean up stack + # Save final salt to slot 0 + + Op.MLOAD(32) # Load final salt from memory + + Op.PUSH1(0) + + Op.SSTORE # SSTORE(0, final_salt) + # Save last EXTCODESIZE result to slot 1 for verification + + Op.MLOAD(160) # Load last_size from memory + + Op.PUSH1(1) + + Op.SSTORE # SSTORE(1, last_size) + Op.STOP ) @@ -318,24 +316,23 @@ def test_extcodesize_bytecode_sizes( """ Execute EXTCODESIZE benchmark against pre-deployed contracts. - This test: - 1. Uses factory addresses passed via stubs (one factory per size) - 2. Reads factory state to get deployed count and init code hash - 3. Generates CREATE2 addresses dynamically during execution - 4. Calls EXTCODESIZE on as many contracts as gas allows (cold access) - 5. Fills block with transactions up to the fork's tx gas limit cap - 6. Verifies that contracts exist by checking a sample contract's size + Uses a gas-based loop exit strategy (per Jochem's suggestion): + 1. Attack contract reads/writes salt from storage slot 0 + 2. Loop exits when gas < 50K, saves salt for next TX + 3. Each TX automatically resumes from where previous left off + 4. No manual gas calculations needed - contract self-regulates + + Verification TX checks that contracts exist by calling EXTCODESIZE + on salt 0 (first contract) and storing the result. """ gas_costs = fork.gas_costs() - tx_gas_limit = fork.transaction_gas_limit_cap() + # Use fork's TX gas limit cap, or 16M fallback for pre-Osaka forks + tx_gas_limit = fork.transaction_gas_limit_cap() or 16_000_000 expected_size_bytes = int(bytecode_size_kb * 1024) - # Calculate intrinsic transaction cost + # Calculate intrinsic transaction cost (no calldata needed) intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - # Calculate gas per EXTCODESIZE iteration - gas_per_iteration = calculate_gas_per_iteration(gas_costs) - # Get factory stub name for this size factory_stub = get_factory_stub_name(bytecode_size_kb) @@ -345,38 +342,20 @@ def test_extcodesize_bytecode_sizes( stub=factory_stub, ) - # Build and deploy the attack contract + # Build and deploy the attack contract with storage initialized attack_code = build_attack_contract(factory_address) - attack_address = pre.deploy_contract(code=attack_code) - - # Calculate intrinsic cost with 32 bytes of calldata (starting salt) - calldata_for_attack = b"\x00" * 32 # 32 zero bytes for salt - intrinsic_gas_with_calldata = fork.transaction_intrinsic_cost_calculator()( - calldata=calldata_for_attack - ) - - # Calculate how many iterations fit in one transaction - # Reserve gas for: intrinsic + setup (getConfig, memory) + cleanup - setup_gas = 5000 # Approximate gas for factory call and memory setup - cleanup_gas = 1000 # Reserve for loop exit and cleanup - available_gas_per_tx = ( - tx_gas_limit - intrinsic_gas_with_calldata - setup_gas - cleanup_gas + attack_address = pre.deploy_contract( + code=attack_code, + storage={0: 0}, # Initialize salt counter to 0 ) - iterations_per_tx = available_gas_per_tx // gas_per_iteration # Calculate how many transactions we need to fill the block - # gas_benchmark_value is the total block gas budget num_attack_txs = gas_benchmark_value // tx_gas_limit if num_attack_txs == 0: num_attack_txs = 1 - # Total iterations across all transactions (each tx uses unique contracts) - total_iterations = iterations_per_tx * num_attack_txs - - # For verification, check the last contract accessed by the last attack tx - # Last tx starts at salt (num_attack_txs - 1) * iterations_per_tx - # and ends at salt (num_attack_txs * iterations_per_tx - 1) - verification_salt = total_iterations - 1 if total_iterations > 0 else 0 + # Verification: check salt 0 (first contract, always accessed) + verification_salt = 0 # Build and deploy verification contract verification_code = build_verification_contract( @@ -403,17 +382,14 @@ def test_extcodesize_bytecode_sizes( ) txs.append(verification_tx) - # Attack transactions: fill remaining block gas with ~16M gas txs - # Each tx uses a different starting salt to access unique contracts - for tx_index in range(num_attack_txs): - starting_salt = tx_index * iterations_per_tx - # Encode starting salt as 32-byte big-endian - salt_calldata = starting_salt.to_bytes(32, "big") + # Attack transactions: all identical, no calldata needed + # Each TX reads salt from storage, loops until gas low, saves salt back + for _ in range(num_attack_txs): attack_tx = Transaction( gas_limit=tx_gas_limit, to=attack_address, sender=sender, - data=salt_calldata, + data=b"", # No calldata - salt comes from storage value=0, ) txs.append(attack_tx) @@ -423,30 +399,30 @@ def test_extcodesize_bytecode_sizes( print(f"EXTCODESIZE Benchmark: {bytecode_size_kb}KB contracts") print(f"{'=' * 60}") print(f"Block gas budget: {gas_benchmark_value:,}") - print(f"Gas per iteration: ~{gas_per_iteration}") - print(f"Iterations per tx (16M gas): ~{iterations_per_tx:,}") + print(f"TX gas limit: {tx_gas_limit:,}") print(f"Number of attack txs: {num_attack_txs}") - print(f"Total unique EXTCODESIZE calls: ~{total_iterations:,}") - print("Salt ranges per tx:") - for i in range(num_attack_txs): - start = i * iterations_per_tx - end = (i + 1) * iterations_per_tx - 1 - print(f" TX{i + 1}: salts {start:,} - {end:,}") print(f"Verification tx gas: {verification_gas:,}") - print(f"Verification salt: {verification_salt:,} (last contract accessed)") print(f"Expected contract size: {expected_size_bytes} bytes") - print(f"Contracts required: {total_iterations:,}") + print("Note: Using gas-based loop - each TX auto-resumes from storage") print(f"{'=' * 60}\n") # Create block with all transactions block = Block(txs=txs) # Post-state verification: - # Verify that the verification contract stored the expected size + # 1. Verify that verification contract stored expected size (salt 0) + # 2. Verify attack contract's last EXTCODESIZE returns expected size + # (proves the gas-based loop ran and accessed real contracts) post = { verification_address: Account( storage={ - 0: expected_size_bytes, # EXTCODESIZE returns expected size + 0: expected_size_bytes, # EXTCODESIZE on salt 0 + } + ), + attack_address: Account( + storage={ + # Slot 1: last EXTCODESIZE result should match expected size + 1: expected_size_bytes, } ), } From 43a7b0cb94c40d107b01eb8195da247609497425 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Wed, 7 Jan 2026 14:06:03 +0100 Subject: [PATCH 14/15] fix: address PR review comments for EXTCODESIZE benchmark - Fix GT operand order bugs (loop condition was inverted) - Add Storage.set_expect_any(0) for uv run fill compatibility - Refactor with cleaner code structure per LouisTsai-Csie suggestions - Use tx_gas_limit fixture instead of manual call - Remove redundant defaults and print statements --- .../test_extcodesize_bytecode_sizes.py | 232 +++++------------- 1 file changed, 64 insertions(+), 168 deletions(-) diff --git a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py index 1c4f7d53b3a..ba66da5b580 100644 --- a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py +++ b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py @@ -74,6 +74,7 @@ Conditional, Fork, Op, + Storage, Transaction, While, ) @@ -103,121 +104,53 @@ def get_factory_stub_name(size_kb: float) -> str: def build_attack_contract(factory_address: Address) -> Bytecode: """ - Build an attack contract that maximizes EXTCODESIZE calls. + Benchmark EXTCODESIZE calls with gas-based loop exit. - Uses a gas-based loop exit strategy (per Jochem's suggestion): - 1. Reads current salt from storage slot 0 (resumes from previous TX) - 2. Calls factory.getConfig() to get (num_deployed, init_code_hash) - 3. Loops while gas > GAS_RESERVE, calling EXTCODESIZE on each address - 4. Saves final salt to storage slot 0 for next TX to resume - 5. Saves last EXTCODESIZE result to storage slot 1 for verification + Storage Layout: + - Slot 0: current salt (persists across transactions) + - Slot 1: last EXTCODESIZE result (for verification) - Storage layout: - - Slot 0: Current/final salt (used for resuming across TXs) - - Slot 1: Last EXTCODESIZE result (used for verification) - - This eliminates manual gas calculations - the contract self-regulates. - Each TX automatically continues from where the previous one left off. + CREATE2 Memory Layout (85 bytes from offset 11): + - MEM[11] = 0xFF prefix + - MEM[12-31] = factory address (20 bytes) + - MEM[32-63] = salt (32 bytes) + - MEM[64-95] = init_code_hash (32 bytes) """ - # Gas reserve for 2x SSTORE + cleanup - # - Slot 0: warm (after SLOAD), ~5K worst case - # - Slot 1: cold on first TX (~22K), warm after (~3K) - # - 50K provides safe margin for both cases - gas_reserve = 50_000 + gas_reserve = 50_000 # Reserve for 2x SSTORE + cleanup return ( - # === Step 0: Load current salt from storage slot 0 === - Op.SLOAD(0) # Stack: [current_salt] - # === Step 1: Get factory configuration === # Call factory.getConfig() -> (num_deployed, init_code_hash) - + Op.STATICCALL( - gas=Op.GAS, - address=factory_address, - args_offset=0, - args_size=0, - ret_offset=96, # Store result at memory[96] - ret_size=64, # 64 bytes for 2 uint256s - ) - # Check if call succeeded (STATICCALL returns 1 on success) - # Stack: [current_salt, success] - + Conditional( - condition=Op.ISZERO, # If call failed (success=0) - if_true=Op.REVERT(0, 0), # Revert with no data + Conditional( + condition=Op.STATICCALL( + gas=Op.GAS, + address=factory_address, + args_offset=0, + args_size=0, + ret_offset=96, # MEM[96]=num_deployed, MEM[128]=init_code_hash + ret_size=64, + ), + if_false=Op.REVERT(0, 0), ) - # Stack: [current_salt] - # Load results from memory - # Memory[96:128] = num_deployed_contracts - # Memory[128:160] = init_code_hash - + Op.MLOAD(96) # Stack: [current_salt, num_deployed] - + Op.MLOAD(128) # Stack: [current_salt, num_deployed, init_code_hash] - # === Step 2: Setup CREATE2 address generation in memory === - # Memory layout at offset 0: - # [0x00-0x0A]: padding (11 bytes) - # [0x0B]: 0xFF marker (1 byte) - # [0x0C-0x1F]: factory address right-aligned (20 bytes) - # [0x20-0x3F]: salt (32 bytes) - # [0x40-0x5F]: init_code_hash (32 bytes) - # Total CREATE2 input: 85 bytes from offset 0x0B - # Store factory address at memory[0] (right-aligned, at bytes 12-31) + # Setup CREATE2 memory: keccak256(0xFF ++ factory ++ salt ++ hash) + Op.MSTORE(0, factory_address) - # Store 0xFF marker at position 11 (before the address) + Op.MSTORE8(11, 0xFF) - # Store init_code_hash at memory[64] - # Stack: [current_salt, num_deployed, init_code_hash] - + Op.PUSH1(64) - + Op.MSTORE # Stores init_code_hash at memory[64] - # Stack: [current_salt, num_deployed] - # Store current salt at memory[32] - + Op.SWAP1 # Stack: [num_deployed, current_salt] - + Op.DUP1 # Stack: [num_deployed, current_salt, current_salt] - + Op.PUSH1(32) - + Op.MSTORE # Store current_salt at memory[32] - # Stack: [num_deployed, current_salt] - + Op.POP # Stack: [num_deployed] - # Initialize last_size at memory[160] to 0 - + Op.PUSH1(0) - + Op.PUSH2(160) - + Op.MSTORE # memory[160] = 0 (last_size) - # === Step 3: Main loop - gas-based exit === - # Loop while gas > gas_reserve AND salt < num_deployed + + Op.MSTORE(32, Op.SLOAD(0)) # Load salt directly to memory + + Op.MSTORE(64, Op.MLOAD(128)) # init_code_hash + + Op.MSTORE(160, 0) # Initialize last_size + While( body=( - # Stack: [num_deployed] - # CREATE2 address: keccak256(0xFF ++ factory ++ salt ++ hash) - Op.SHA3(11, 85) # 85 bytes from offset 11 - # Hash result - EVM auto-truncates to 20-byte address - # Call EXTCODESIZE and store result in memory[160] - + Op.EXTCODESIZE - + Op.PUSH2(160) - + Op.MSTORE # Store size at memory[160] for verification - # Increment salt for next iteration + Op.MSTORE(160, Op.EXTCODESIZE(Op.SHA3(11, 85))) + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) ), - # Continue while: gas > gas_reserve AND salt < num_deployed condition=( - # Check gas > gas_reserve - Op.GAS - + Op.PUSH3(gas_reserve) - + Op.GT # gas > gas_reserve - # Check salt < num_deployed (num_deployed > salt) - + Op.DUP2 # Stack: [..., (gas>reserve), num_deployed] - + Op.MLOAD(32) # Stack: [..., (gas>res), num_deployed, salt] - + Op.GT # Stack: [..., (gas>reserve), (num_deployed > salt)] - # Both conditions must be true - + Op.AND # (gas > reserve) AND (salt < num_deployed) + Op.AND( + Op.GT(Op.GAS, gas_reserve), + Op.GT(Op.MLOAD(96), Op.MLOAD(32)), # num_deployed > salt + ) ), ) - # === Step 4: Save state to storage for next TX and verification === - # Stack: [num_deployed] - + Op.POP # Clean up stack - # Save final salt to slot 0 - + Op.MLOAD(32) # Load final salt from memory - + Op.PUSH1(0) - + Op.SSTORE # SSTORE(0, final_salt) - # Save last EXTCODESIZE result to slot 1 for verification - + Op.MLOAD(160) # Load last_size from memory - + Op.PUSH1(1) - + Op.SSTORE # SSTORE(1, last_size) + + Op.SSTORE(0, Op.MLOAD(32)) # Save final salt + + Op.SSTORE(1, Op.MLOAD(160)) # Save last result + Op.STOP ) @@ -261,41 +194,33 @@ def build_verification_contract( factory_address: Address, verification_salt: int ) -> Bytecode: """ - Build a verification contract that stores EXTCODESIZE result. + Verify EXTCODESIZE result for a specific salt by storing it in slot 0. - The contract: - 1. Calls factory.getConfig() to get init_code_hash - 2. Computes CREATE2 address for the given salt - 3. Calls EXTCODESIZE on that address - 4. Stores the result in storage slot 0 + CREATE2 Memory Layout (same as attack contract): + - MEM[11] = 0xFF prefix + - MEM[12-31] = factory address + - MEM[32-63] = salt + - MEM[64-95] = init_code_hash """ return ( - # Call factory.getConfig() to get init_code_hash - Op.STATICCALL( - gas=Op.GAS, - address=factory_address, - args_offset=0, - args_size=0, - ret_offset=96, - ret_size=64, + # Call factory.getConfig() -> (num_deployed, init_code_hash) + Op.POP( + Op.STATICCALL( + gas=Op.GAS, + address=factory_address, + args_offset=0, + args_size=0, + ret_offset=96, # MEM[96]=num_deployed, MEM[128]=init_code_hash + ret_size=64, + ) ) - + Op.POP # Discard success flag (assume it works) - # Setup CREATE2 address generation (same layout as attack contract) + # Setup CREATE2 memory + Op.MSTORE(0, factory_address) + Op.MSTORE8(11, 0xFF) - + Op.MSTORE(32, verification_salt) # Use the target salt - # Load init_code_hash from memory[128] and store at memory[64] - + Op.MLOAD(128) - + Op.PUSH1(64) - + Op.MSTORE - # Generate CREATE2 address - + Op.SHA3(11, 85) - # Result is 32-byte hash - EVM auto-truncates to 20-byte address - # Call EXTCODESIZE - + Op.EXTCODESIZE - # Store result in storage slot 0 - + Op.PUSH1(0) - + Op.SSTORE + + Op.MSTORE(32, verification_salt) + + Op.MSTORE(64, Op.MLOAD(128)) + # EXTCODESIZE on CREATE2 address, store result + + Op.SSTORE(0, Op.EXTCODESIZE(Op.SHA3(11, 85))) + Op.STOP ) @@ -312,22 +237,20 @@ def test_extcodesize_bytecode_sizes( fork: Fork, bytecode_size_kb: float, gas_benchmark_value: int, + tx_gas_limit: int, ) -> None: """ Execute EXTCODESIZE benchmark against pre-deployed contracts. - Uses a gas-based loop exit strategy (per Jochem's suggestion): + Uses a gas-based loop exit strategy: 1. Attack contract reads/writes salt from storage slot 0 2. Loop exits when gas < 50K, saves salt for next TX 3. Each TX automatically resumes from where previous left off - 4. No manual gas calculations needed - contract self-regulates Verification TX checks that contracts exist by calling EXTCODESIZE on salt 0 (first contract) and storing the result. """ gas_costs = fork.gas_costs() - # Use fork's TX gas limit cap, or 16M fallback for pre-Osaka forks - tx_gas_limit = fork.transaction_gas_limit_cap() or 16_000_000 expected_size_bytes = int(bytecode_size_kb * 1024) # Calculate intrinsic transaction cost (no calldata needed) @@ -342,12 +265,9 @@ def test_extcodesize_bytecode_sizes( stub=factory_stub, ) - # Build and deploy the attack contract with storage initialized + # Build and deploy the attack contract attack_code = build_attack_contract(factory_address) - attack_address = pre.deploy_contract( - code=attack_code, - storage={0: 0}, # Initialize salt counter to 0 - ) + attack_address = pre.deploy_contract(code=attack_code) # Calculate how many transactions we need to fill the block num_attack_txs = gas_benchmark_value // tx_gas_limit @@ -377,54 +297,30 @@ def test_extcodesize_bytecode_sizes( gas_limit=verification_gas, to=verification_address, sender=sender, - data=b"", - value=0, ) txs.append(verification_tx) # Attack transactions: all identical, no calldata needed - # Each TX reads salt from storage, loops until gas low, saves salt back for _ in range(num_attack_txs): attack_tx = Transaction( gas_limit=tx_gas_limit, to=attack_address, sender=sender, - data=b"", # No calldata - salt comes from storage - value=0, ) txs.append(attack_tx) - # Log test configuration - print(f"\n{'=' * 60}") - print(f"EXTCODESIZE Benchmark: {bytecode_size_kb}KB contracts") - print(f"{'=' * 60}") - print(f"Block gas budget: {gas_benchmark_value:,}") - print(f"TX gas limit: {tx_gas_limit:,}") - print(f"Number of attack txs: {num_attack_txs}") - print(f"Verification tx gas: {verification_gas:,}") - print(f"Expected contract size: {expected_size_bytes} bytes") - print("Note: Using gas-based loop - each TX auto-resumes from storage") - print(f"{'=' * 60}\n") - # Create block with all transactions block = Block(txs=txs) # Post-state verification: - # 1. Verify that verification contract stored expected size (salt 0) - # 2. Verify attack contract's last EXTCODESIZE returns expected size - # (proves the gas-based loop ran and accessed real contracts) + # - Verification contract: slot 0 = expected size + # - Attack contract: slot 1 = expected size, slot 0 = any (final salt) + attack_storage = Storage({1: expected_size_bytes}) # type: ignore[dict-item] + attack_storage.set_expect_any(0) + post = { - verification_address: Account( - storage={ - 0: expected_size_bytes, # EXTCODESIZE on salt 0 - } - ), - attack_address: Account( - storage={ - # Slot 1: last EXTCODESIZE result should match expected size - 1: expected_size_bytes, - } - ), + verification_address: Account(storage={0: expected_size_bytes}), + attack_address: Account(storage=attack_storage), } blockchain_test( From 4790f737bce55408056f0de4ab0abc5c7f00b828 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Wed, 7 Jan 2026 18:07:48 +0100 Subject: [PATCH 15/15] fix: remove verification TX to fix GasUsedExceedsLimitError jochem-brouwer identified that verification TX + attack TX exceed block gas limit when running `uv run fill`. The attack contract's slot 1 already provides sufficient verification (stores last EXTCODESIZE result). Changes: - Remove verification TX and related code - Remove unused build_verification_contract() function - Remove unused calculate_verification_gas() function - Revert unintended changes to test_single_opcode.py - Update module docstring to reflect new block structure --- .../test_extcodesize_bytecode_sizes.py | 117 ++---------------- .../stateful/bloatnet/test_single_opcode.py | 15 +-- 2 files changed, 10 insertions(+), 122 deletions(-) diff --git a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py index ba66da5b580..42da6314c2a 100644 --- a/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py +++ b/tests/benchmark/stateful/bloatnet/test_extcodesize_bytecode_sizes.py @@ -22,19 +22,18 @@ ┌───────────────────────────────────────────────────────────────┐ │ Test Block │ ├───────────────────────────────────────────────────────────────┤ -│ TX1: Verification (~30K gas) │ -│ └─> Calls EXTCODESIZE on salt 0, stores result │ +│ TX1: Attack (~16M gas) │ +│ └─> Loops EXTCODESIZE until gas < 50K, saves salt │ │ │ │ TX2: Attack (~16M gas) │ -│ └─> Loops EXTCODESIZE until gas < 50K, saves salt │ +│ └─> Resumes from TX1's salt, continues looping │ │ │ │ TX3: Attack (~16M gas) │ │ └─> Resumes from TX2's salt, continues looping │ -│ │ -│ TX4: Attack (~16M gas) │ -│ └─> Resumes from TX3's salt, continues looping │ └───────────────────────────────────────────────────────────────┘ +Post-state verification checks attack contract's slot 1 for expected size. + ### Execute a Single Size ```bash @@ -72,13 +71,11 @@ BlockchainTestFiller, Bytecode, Conditional, - Fork, Op, Storage, Transaction, While, ) -from execution_testing.forks.gas_costs import GasCosts REFERENCE_SPEC_GIT_PATH = "DUMMY/bloatnet.md" REFERENCE_SPEC_VERSION = "1.0" @@ -155,76 +152,6 @@ def build_attack_contract(factory_address: Address) -> Bytecode: ) -def calculate_verification_gas(gas_costs: GasCosts, intrinsic_gas: int) -> int: - """ - Calculate the minimum gas needed for the verification transaction. - - The verification contract: - 1. STATICCALL to factory.getConfig() - cold access - 2. Memory operations for CREATE2 setup - 3. SHA3 for address derivation - 4. EXTCODESIZE on target contract - cold access - 5. SSTORE to save result - cold, zero-to-nonzero - """ - verification_execution_gas = ( - # STATICCALL to factory (cold access) - gas_costs.G_COLD_ACCOUNT_ACCESS # 2600 - + 100 # STATICCALL base cost - # Memory operations (MSTORE, MSTORE8, MLOAD) - + gas_costs.G_LOW * 5 # 5 memory ops (3 * 5 = 15) - + gas_costs.G_VERY_LOW # MSTORE8 (3) - # SHA3 for CREATE2 address (85 bytes = 3 words) - + gas_costs.G_KECCAK_256 # 30 - + gas_costs.G_KECCAK_256_WORD * 3 # 18 - # Note: No masking needed - EVM auto-truncates to 20 bytes - # EXTCODESIZE (cold access to target contract) - + gas_costs.G_COLD_ACCOUNT_ACCESS # 2600 - # SSTORE (cold slot, zero-to-nonzero) - + gas_costs.G_STORAGE_SET # 20000 - + gas_costs.G_COLD_SLOAD # 2100 (cold access) - # STOP - + 0 - ) - # Add intrinsic gas + 10% buffer for safety - total = intrinsic_gas + verification_execution_gas - return int(total * 1.1) - - -def build_verification_contract( - factory_address: Address, verification_salt: int -) -> Bytecode: - """ - Verify EXTCODESIZE result for a specific salt by storing it in slot 0. - - CREATE2 Memory Layout (same as attack contract): - - MEM[11] = 0xFF prefix - - MEM[12-31] = factory address - - MEM[32-63] = salt - - MEM[64-95] = init_code_hash - """ - return ( - # Call factory.getConfig() -> (num_deployed, init_code_hash) - Op.POP( - Op.STATICCALL( - gas=Op.GAS, - address=factory_address, - args_offset=0, - args_size=0, - ret_offset=96, # MEM[96]=num_deployed, MEM[128]=init_code_hash - ret_size=64, - ) - ) - # Setup CREATE2 memory - + Op.MSTORE(0, factory_address) - + Op.MSTORE8(11, 0xFF) - + Op.MSTORE(32, verification_salt) - + Op.MSTORE(64, Op.MLOAD(128)) - # EXTCODESIZE on CREATE2 address, store result - + Op.SSTORE(0, Op.EXTCODESIZE(Op.SHA3(11, 85))) - + Op.STOP - ) - - @pytest.mark.parametrize( "bytecode_size_kb", [0.5, 1.0, 2.0, 5.0, 10.0, 24.0], @@ -234,7 +161,6 @@ def build_verification_contract( def test_extcodesize_bytecode_sizes( blockchain_test: BlockchainTestFiller, pre: Alloc, - fork: Fork, bytecode_size_kb: float, gas_benchmark_value: int, tx_gas_limit: int, @@ -247,15 +173,11 @@ def test_extcodesize_bytecode_sizes( 2. Loop exits when gas < 50K, saves salt for next TX 3. Each TX automatically resumes from where previous left off - Verification TX checks that contracts exist by calling EXTCODESIZE - on salt 0 (first contract) and storing the result. + Post-state verifies that the attack contract's slot 1 contains the + expected bytecode size (last EXTCODESIZE result). """ - gas_costs = fork.gas_costs() expected_size_bytes = int(bytecode_size_kb * 1024) - # Calculate intrinsic transaction cost (no calldata needed) - intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - # Get factory stub name for this size factory_stub = get_factory_stub_name(bytecode_size_kb) @@ -274,32 +196,12 @@ def test_extcodesize_bytecode_sizes( if num_attack_txs == 0: num_attack_txs = 1 - # Verification: check salt 0 (first contract, always accessed) - verification_salt = 0 - - # Build and deploy verification contract - verification_code = build_verification_contract( - factory_address, verification_salt - ) - verification_address = pre.deploy_contract(code=verification_code) - - # Calculate minimum gas needed for verification tx - verification_gas = calculate_verification_gas(gas_costs, intrinsic_gas) - # Fund the sender sender = pre.fund_eoa() # Build transactions txs = [] - # First transaction: verification (runs first, uses minimal gas) - verification_tx = Transaction( - gas_limit=verification_gas, - to=verification_address, - sender=sender, - ) - txs.append(verification_tx) - # Attack transactions: all identical, no calldata needed for _ in range(num_attack_txs): attack_tx = Transaction( @@ -313,13 +215,12 @@ def test_extcodesize_bytecode_sizes( block = Block(txs=txs) # Post-state verification: - # - Verification contract: slot 0 = expected size - # - Attack contract: slot 1 = expected size, slot 0 = any (final salt) + # Attack contract slot 1 = expected size (last EXTCODESIZE result) + # Slot 0 can be any value (final salt depends on gas used) attack_storage = Storage({1: expected_size_bytes}) # type: ignore[dict-item] attack_storage.set_expect_any(0) post = { - verification_address: Account(storage={0: expected_size_bytes}), attack_address: Account(storage=attack_storage), } diff --git a/tests/benchmark/stateful/bloatnet/test_single_opcode.py b/tests/benchmark/stateful/bloatnet/test_single_opcode.py index 487399632d8..34e2c46434a 100644 --- a/tests/benchmark/stateful/bloatnet/test_single_opcode.py +++ b/tests/benchmark/stateful/bloatnet/test_single_opcode.py @@ -128,17 +128,6 @@ def test_sload_empty_erc20_balanceof( # 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_LOW # MLOAD for While condition check (3) - + gas_costs.G_BASE # ISZERO (2) - + gas_costs.G_BASE # ISZERO (2) - + gas_costs.G_MID # JUMPI (8) - + 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 @@ -166,8 +155,7 @@ def test_sload_empty_erc20_balanceof( ) # Calculate gas budget per contract - total_overhead = intrinsic_gas + (overhead_per_contract * num_contracts) - available_gas = gas_benchmark_value - total_overhead + available_gas = gas_benchmark_value - intrinsic_gas gas_per_contract = available_gas // num_contracts # For each contract: first call is COLD (2600), subsequent are WARM (100) @@ -200,7 +188,6 @@ def test_sload_empty_erc20_balanceof( # Log test requirements print( f"Total gas budget: {gas_benchmark_value / 1_000_000:.1f}M gas. " - f"Overhead per contract: {overhead_per_contract}. " f"~{gas_per_contract / 1_000_000:.1f}M gas per contract, " f"{calls_per_contract} balanceOf calls per contract." )