From 2ad25ea89c2ba6492e2aa40381cbf9e8f92ed59c Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 3 Oct 2025 12:22:23 +0200 Subject: [PATCH] refactor(tests): use pytest parametrize to reduce code duplication in BloatNet tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace duplicate test functions with parametrized versions to avoid repetitive code. Each test now accepts a `balance_first` parameter that controls the order of operations, eliminating the need for separate `_extcodesize_balance`, `_extcodecopy_balance`, and `_extcodehash_balance` variants. Changes: - Add @pytest.mark.parametrize to test_bloatnet_balance_extcodesize, test_bloatnet_balance_extcodecopy, and test_bloatnet_balance_extcodehash - Each test now generates two variants via parametrization with descriptive IDs (e.g., "balance_extcodesize" and "extcodesize_balance") - Extract operation sequences into variables and conditionally compose them based on balance_first parameter - Remove test_bloatnet_extcodesize_balance, test_bloatnet_extcodecopy_balance, and test_bloatnet_extcodehash_balance (now covered by parametrization) This reduces the file from 793 lines to 462 lines while maintaining the same test coverage (6 tests total: 3 test functions × 2 parametrization values). To run specific parameter variants, use the -k flag: fill -k "balance_extcodesize" tests/benchmark/bloatnet/test_multi_opcode.py fill -k "extcodesize_balance" tests/benchmark/bloatnet/test_multi_opcode.py Co-Authored-By: LouisTsai-Csie <72684086+LouisTsai-Csie@users.noreply.github.com> --- .../stateful/bloatnet/test_multi_opcode.py | 204 +++++++++++++++--- 1 file changed, 178 insertions(+), 26 deletions(-) diff --git a/tests/benchmark/stateful/bloatnet/test_multi_opcode.py b/tests/benchmark/stateful/bloatnet/test_multi_opcode.py index 4d27d6fca49..ea6f9872646 100644 --- a/tests/benchmark/stateful/bloatnet/test_multi_opcode.py +++ b/tests/benchmark/stateful/bloatnet/test_multi_opcode.py @@ -49,12 +49,18 @@ # 4. Attack rapidly accesses all contracts, stressing client's state handling +@pytest.mark.parametrize( + "balance_first", + [True, False], + ids=["balance_extcodesize", "extcodesize_balance"], +) @pytest.mark.valid_from("Prague") def test_bloatnet_balance_extcodesize( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, gas_benchmark_value: int, + balance_first: bool, ): """ BloatNet test using BALANCE + EXTCODESIZE with "on-the-fly" CREATE2 @@ -63,7 +69,7 @@ def test_bloatnet_balance_extcodesize( This test: 1. Assumes contracts are already deployed via the factory (salt 0 to N-1) 2. Generates CREATE2 addresses dynamically during execution - 3. Calls BALANCE (cold) then EXTCODESIZE (warm) on each + 3. Calls BALANCE and EXTCODESIZE (order controlled by balance_first param) 4. Maximizes cache eviction by accessing many contracts """ gas_costs = fork.gas_costs() @@ -75,11 +81,11 @@ def test_bloatnet_balance_extcodesize( cost_per_contract = ( gas_costs.G_KECCAK_256 # SHA3 static cost for address generation (30) + gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6) - + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600) - + gas_costs.G_BASE # POP balance (2) - + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODESIZE (100) - + gas_costs.G_BASE # POP code size (2) - + gas_costs.G_BASE # DUP1 before BALANCE (3) + + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold access (2600) + + gas_costs.G_BASE # POP first result (2) + + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm access (100) + + gas_costs.G_BASE # POP second result (2) + + gas_costs.G_BASE # DUP1 before first op (3) + gas_costs.G_VERY_LOW * 4 # PUSH1 operations (4 * 3) + gas_costs.G_LOW # MLOAD for salt (3) + gas_costs.G_VERY_LOW # ADD for increment (3) @@ -108,6 +114,13 @@ def test_bloatnet_balance_extcodesize( f"Factory storage will be checked during execution." ) + # Define operations that differ based on parameter + balance_op = Op.POP(Op.BALANCE) + extcodesize_op = Op.POP(Op.EXTCODESIZE) + benchmark_ops = ( + (balance_op + extcodesize_op) if balance_first else (extcodesize_op + balance_op) + ) + # Build attack contract that reads config from factory and performs attack attack_code = ( # Call getConfig() on factory to get num_deployed and init_code_hash @@ -143,9 +156,8 @@ def test_bloatnet_balance_extcodesize( # Generate CREATE2 addr: keccak256(0xFF+factory+salt+hash) Op.SHA3(11, 85) # Generate CREATE2 address from memory[11:96] # The address is now on the stack - + Op.DUP1 # Duplicate for EXTCODESIZE - + Op.POP(Op.BALANCE) # Cold access - + Op.POP(Op.EXTCODESIZE) # Warm access + + Op.DUP1 # Duplicate for second operation + + benchmark_ops # Execute operations in specified order # Increment salt for next iteration + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) # Increment and store salt ), @@ -177,12 +189,18 @@ def test_bloatnet_balance_extcodesize( ) +@pytest.mark.parametrize( + "balance_first", + [True, False], + ids=["balance_extcodecopy", "extcodecopy_balance"], +) @pytest.mark.valid_from("Prague") def test_bloatnet_balance_extcodecopy( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, gas_benchmark_value: int, + balance_first: bool, ): """ BloatNet test using BALANCE + EXTCODECOPY with on-the-fly CREATE2 @@ -191,8 +209,8 @@ def test_bloatnet_balance_extcodecopy( This test forces actual bytecode reads from disk by: 1. Assumes contracts are already deployed via the factory 2. Generating CREATE2 addresses dynamically during execution - 3. Using BALANCE (cold) to warm the account - 4. Using EXTCODECOPY (warm) to read 1 byte from the END of the bytecode + 3. Using BALANCE and EXTCODECOPY (order controlled by balance_first param) + 4. Reading 1 byte from the END of the bytecode to force full contract load """ gas_costs = fork.gas_costs() max_contract_size = fork.max_code_size() @@ -204,16 +222,16 @@ def test_bloatnet_balance_extcodecopy( cost_per_contract = ( gas_costs.G_KECCAK_256 # SHA3 static cost for address generation (30) + gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6) - + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600) - + gas_costs.G_BASE # POP balance (2) - + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODECOPY base (100) + + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold access (2600) + + gas_costs.G_BASE # POP first result (2) + + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm access base (100) + gas_costs.G_COPY * 1 # Copy cost for 1 byte (3) - + gas_costs.G_BASE * 2 # DUP1 before BALANCE, DUP4 for address (6) + + gas_costs.G_BASE * 2 # DUP1 before first op, DUP4 for address (6) + gas_costs.G_VERY_LOW * 8 # PUSH operations (8 * 3 = 24) + gas_costs.G_LOW * 2 # MLOAD for salt twice (6) + gas_costs.G_VERY_LOW * 2 # ADD operations (6) + gas_costs.G_LOW # MSTORE salt back (3) - + gas_costs.G_BASE # POP after EXTCODECOPY (2) + + gas_costs.G_BASE # POP after second op (2) + 10 # While loop overhead ) @@ -238,6 +256,20 @@ def test_bloatnet_balance_extcodecopy( f"Factory storage will be checked during execution." ) + # Define operations that differ based on parameter + balance_op = Op.POP(Op.BALANCE) + extcodecopy_op = ( + Op.PUSH1(1) # size (1 byte) + + Op.PUSH2(max_contract_size - 1) # code offset (last byte) + + Op.ADD(Op.MLOAD(32), 96) # unique memory offset + + Op.DUP4 # address (duplicated earlier) + + Op.EXTCODECOPY + + Op.POP # clean up address + ) + benchmark_ops = ( + (balance_op + extcodecopy_op) if balance_first else (extcodecopy_op + balance_op) + ) + # Build attack contract that reads config from factory and performs attack attack_code = ( # Call getConfig() on factory to get num_deployed and init_code_hash @@ -274,16 +306,7 @@ def test_bloatnet_balance_extcodecopy( Op.SHA3(11, 85) # Generate CREATE2 address from memory[11:96] # The address is now on the stack + Op.DUP1 # Duplicate for later operations - + Op.POP(Op.BALANCE) # Cold access - # EXTCODECOPY(addr, mem_offset, last_byte_offset, 1) - # Read the LAST byte to force full contract load - + Op.PUSH1(1) # size (1 byte) - + Op.PUSH2(max_contract_size - 1) # code offset (last byte) - # Use salt as memory offset to avoid overlap - + Op.ADD(Op.MLOAD(32), 96) # Add base memory offset for unique position - + Op.DUP4 # address (duplicated earlier) - + Op.EXTCODECOPY - + Op.POP # Clean up address + + benchmark_ops # Execute operations in specified order # Increment salt for next iteration + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) # Increment and store salt ), @@ -313,3 +336,132 @@ def test_bloatnet_balance_extcodecopy( blocks=[Block(txs=[attack_tx])], post=post, ) + + +@pytest.mark.parametrize( + "balance_first", + [True, False], + ids=["balance_extcodehash", "extcodehash_balance"], +) +@pytest.mark.valid_from("Prague") +def test_bloatnet_balance_extcodehash( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + gas_benchmark_value: int, + balance_first: bool, +): + """ + BloatNet test using BALANCE + EXTCODEHASH with on-the-fly CREATE2 + address generation. + + This test: + 1. Assumes contracts are already deployed via the factory + 2. Generates CREATE2 addresses dynamically during execution + 3. Calls BALANCE and EXTCODEHASH (order controlled by balance_first param) + 4. Forces client to compute code hash for 24KB bytecode + """ + gas_costs = fork.gas_costs() + + # Calculate gas costs + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") + + # Cost per contract access with CREATE2 address generation + cost_per_contract = ( + gas_costs.G_KECCAK_256 # SHA3 static cost for address generation (30) + + gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6) + + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold access (2600) + + gas_costs.G_BASE # POP first result (2) + + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm access (100) + + gas_costs.G_BASE # POP second result (2) + + gas_costs.G_BASE # DUP1 before first op (3) + + gas_costs.G_VERY_LOW * 4 # PUSH1 operations (4 * 3) + + gas_costs.G_LOW # MLOAD for salt (3) + + gas_costs.G_VERY_LOW # ADD for increment (3) + + gas_costs.G_LOW # MSTORE salt back (3) + + 10 # While loop overhead + ) + + # Calculate how many contracts to access based on available gas + available_gas = gas_benchmark_value - intrinsic_gas - 1000 # Reserve for cleanup + contracts_needed = int(available_gas // cost_per_contract) + + # Deploy factory using stub contract + factory_address = pre.deploy_contract( + code=Bytecode(), + stub="bloatnet_factory", + ) + + # Log test requirements + print( + f"Test needs {contracts_needed} contracts for " + f"{gas_benchmark_value / 1_000_000:.1f}M gas. " + f"Factory storage will be checked during execution." + ) + + # Define operations that differ based on parameter + balance_op = Op.POP(Op.BALANCE) + extcodehash_op = Op.POP(Op.EXTCODEHASH) + benchmark_ops = ( + (balance_op + extcodehash_op) if balance_first else (extcodehash_op + balance_op) + ) + + # Build attack contract that reads config from factory and performs attack + attack_code = ( + # Call getConfig() on factory to get num_deployed and init_code_hash + Op.STATICCALL( + gas=Op.GAS, + address=factory_address, + args_offset=0, + args_size=0, + ret_offset=96, + ret_size=64, + ) + # Check if call succeeded + + Op.ISZERO + + Op.PUSH2(0x1000) # Jump to error handler if failed + + Op.JUMPI + # Load results from memory + + Op.MLOAD(96) # Load num_deployed_contracts + + Op.MLOAD(128) # Load init_code_hash + # Setup memory for CREATE2 address generation + + Op.MSTORE(0, factory_address) + + Op.MSTORE8(11, 0xFF) + + Op.MSTORE(32, 0) # Initial salt + + Op.PUSH1(64) + + Op.MSTORE # Store init_code_hash + # Main attack loop + + While( + body=( + # Generate CREATE2 address + Op.SHA3(11, 85) + + Op.DUP1 # Duplicate for second operation + + benchmark_ops # Execute operations in specified order + # Increment salt + + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) + ), + condition=Op.DUP1 + Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO, + ) + + Op.POP # Clean up counter + ) + + # Deploy attack contract + attack_address = pre.deploy_contract(code=attack_code) + + # Run the attack + attack_tx = Transaction( + to=attack_address, + gas_limit=gas_benchmark_value, + sender=pre.fund_eoa(), + ) + + # Post-state + post = { + attack_address: Account(storage={}), + } + + blockchain_test( + pre=pre, + blocks=[Block(txs=[attack_tx])], + post=post, + )