Skip to content

Commit 2ad25ea

Browse files
committed
refactor(tests): use pytest parametrize to reduce code duplication in BloatNet tests
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 <[email protected]>
1 parent 476f3c3 commit 2ad25ea

File tree

1 file changed

+178
-26
lines changed

1 file changed

+178
-26
lines changed

tests/benchmark/stateful/bloatnet/test_multi_opcode.py

Lines changed: 178 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,18 @@
4949
# 4. Attack rapidly accesses all contracts, stressing client's state handling
5050

5151

52+
@pytest.mark.parametrize(
53+
"balance_first",
54+
[True, False],
55+
ids=["balance_extcodesize", "extcodesize_balance"],
56+
)
5257
@pytest.mark.valid_from("Prague")
5358
def test_bloatnet_balance_extcodesize(
5459
blockchain_test: BlockchainTestFiller,
5560
pre: Alloc,
5661
fork: Fork,
5762
gas_benchmark_value: int,
63+
balance_first: bool,
5864
):
5965
"""
6066
BloatNet test using BALANCE + EXTCODESIZE with "on-the-fly" CREATE2
@@ -63,7 +69,7 @@ def test_bloatnet_balance_extcodesize(
6369
This test:
6470
1. Assumes contracts are already deployed via the factory (salt 0 to N-1)
6571
2. Generates CREATE2 addresses dynamically during execution
66-
3. Calls BALANCE (cold) then EXTCODESIZE (warm) on each
72+
3. Calls BALANCE and EXTCODESIZE (order controlled by balance_first param)
6773
4. Maximizes cache eviction by accessing many contracts
6874
"""
6975
gas_costs = fork.gas_costs()
@@ -75,11 +81,11 @@ def test_bloatnet_balance_extcodesize(
7581
cost_per_contract = (
7682
gas_costs.G_KECCAK_256 # SHA3 static cost for address generation (30)
7783
+ gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6)
78-
+ gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600)
79-
+ gas_costs.G_BASE # POP balance (2)
80-
+ gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODESIZE (100)
81-
+ gas_costs.G_BASE # POP code size (2)
82-
+ gas_costs.G_BASE # DUP1 before BALANCE (3)
84+
+ gas_costs.G_COLD_ACCOUNT_ACCESS # Cold access (2600)
85+
+ gas_costs.G_BASE # POP first result (2)
86+
+ gas_costs.G_WARM_ACCOUNT_ACCESS # Warm access (100)
87+
+ gas_costs.G_BASE # POP second result (2)
88+
+ gas_costs.G_BASE # DUP1 before first op (3)
8389
+ gas_costs.G_VERY_LOW * 4 # PUSH1 operations (4 * 3)
8490
+ gas_costs.G_LOW # MLOAD for salt (3)
8591
+ gas_costs.G_VERY_LOW # ADD for increment (3)
@@ -108,6 +114,13 @@ def test_bloatnet_balance_extcodesize(
108114
f"Factory storage will be checked during execution."
109115
)
110116

117+
# Define operations that differ based on parameter
118+
balance_op = Op.POP(Op.BALANCE)
119+
extcodesize_op = Op.POP(Op.EXTCODESIZE)
120+
benchmark_ops = (
121+
(balance_op + extcodesize_op) if balance_first else (extcodesize_op + balance_op)
122+
)
123+
111124
# Build attack contract that reads config from factory and performs attack
112125
attack_code = (
113126
# Call getConfig() on factory to get num_deployed and init_code_hash
@@ -143,9 +156,8 @@ def test_bloatnet_balance_extcodesize(
143156
# Generate CREATE2 addr: keccak256(0xFF+factory+salt+hash)
144157
Op.SHA3(11, 85) # Generate CREATE2 address from memory[11:96]
145158
# The address is now on the stack
146-
+ Op.DUP1 # Duplicate for EXTCODESIZE
147-
+ Op.POP(Op.BALANCE) # Cold access
148-
+ Op.POP(Op.EXTCODESIZE) # Warm access
159+
+ Op.DUP1 # Duplicate for second operation
160+
+ benchmark_ops # Execute operations in specified order
149161
# Increment salt for next iteration
150162
+ Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) # Increment and store salt
151163
),
@@ -177,12 +189,18 @@ def test_bloatnet_balance_extcodesize(
177189
)
178190

179191

192+
@pytest.mark.parametrize(
193+
"balance_first",
194+
[True, False],
195+
ids=["balance_extcodecopy", "extcodecopy_balance"],
196+
)
180197
@pytest.mark.valid_from("Prague")
181198
def test_bloatnet_balance_extcodecopy(
182199
blockchain_test: BlockchainTestFiller,
183200
pre: Alloc,
184201
fork: Fork,
185202
gas_benchmark_value: int,
203+
balance_first: bool,
186204
):
187205
"""
188206
BloatNet test using BALANCE + EXTCODECOPY with on-the-fly CREATE2
@@ -191,8 +209,8 @@ def test_bloatnet_balance_extcodecopy(
191209
This test forces actual bytecode reads from disk by:
192210
1. Assumes contracts are already deployed via the factory
193211
2. Generating CREATE2 addresses dynamically during execution
194-
3. Using BALANCE (cold) to warm the account
195-
4. Using EXTCODECOPY (warm) to read 1 byte from the END of the bytecode
212+
3. Using BALANCE and EXTCODECOPY (order controlled by balance_first param)
213+
4. Reading 1 byte from the END of the bytecode to force full contract load
196214
"""
197215
gas_costs = fork.gas_costs()
198216
max_contract_size = fork.max_code_size()
@@ -204,16 +222,16 @@ def test_bloatnet_balance_extcodecopy(
204222
cost_per_contract = (
205223
gas_costs.G_KECCAK_256 # SHA3 static cost for address generation (30)
206224
+ gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6)
207-
+ gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600)
208-
+ gas_costs.G_BASE # POP balance (2)
209-
+ gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODECOPY base (100)
225+
+ gas_costs.G_COLD_ACCOUNT_ACCESS # Cold access (2600)
226+
+ gas_costs.G_BASE # POP first result (2)
227+
+ gas_costs.G_WARM_ACCOUNT_ACCESS # Warm access base (100)
210228
+ gas_costs.G_COPY * 1 # Copy cost for 1 byte (3)
211-
+ gas_costs.G_BASE * 2 # DUP1 before BALANCE, DUP4 for address (6)
229+
+ gas_costs.G_BASE * 2 # DUP1 before first op, DUP4 for address (6)
212230
+ gas_costs.G_VERY_LOW * 8 # PUSH operations (8 * 3 = 24)
213231
+ gas_costs.G_LOW * 2 # MLOAD for salt twice (6)
214232
+ gas_costs.G_VERY_LOW * 2 # ADD operations (6)
215233
+ gas_costs.G_LOW # MSTORE salt back (3)
216-
+ gas_costs.G_BASE # POP after EXTCODECOPY (2)
234+
+ gas_costs.G_BASE # POP after second op (2)
217235
+ 10 # While loop overhead
218236
)
219237

@@ -238,6 +256,20 @@ def test_bloatnet_balance_extcodecopy(
238256
f"Factory storage will be checked during execution."
239257
)
240258

259+
# Define operations that differ based on parameter
260+
balance_op = Op.POP(Op.BALANCE)
261+
extcodecopy_op = (
262+
Op.PUSH1(1) # size (1 byte)
263+
+ Op.PUSH2(max_contract_size - 1) # code offset (last byte)
264+
+ Op.ADD(Op.MLOAD(32), 96) # unique memory offset
265+
+ Op.DUP4 # address (duplicated earlier)
266+
+ Op.EXTCODECOPY
267+
+ Op.POP # clean up address
268+
)
269+
benchmark_ops = (
270+
(balance_op + extcodecopy_op) if balance_first else (extcodecopy_op + balance_op)
271+
)
272+
241273
# Build attack contract that reads config from factory and performs attack
242274
attack_code = (
243275
# Call getConfig() on factory to get num_deployed and init_code_hash
@@ -274,16 +306,7 @@ def test_bloatnet_balance_extcodecopy(
274306
Op.SHA3(11, 85) # Generate CREATE2 address from memory[11:96]
275307
# The address is now on the stack
276308
+ Op.DUP1 # Duplicate for later operations
277-
+ Op.POP(Op.BALANCE) # Cold access
278-
# EXTCODECOPY(addr, mem_offset, last_byte_offset, 1)
279-
# Read the LAST byte to force full contract load
280-
+ Op.PUSH1(1) # size (1 byte)
281-
+ Op.PUSH2(max_contract_size - 1) # code offset (last byte)
282-
# Use salt as memory offset to avoid overlap
283-
+ Op.ADD(Op.MLOAD(32), 96) # Add base memory offset for unique position
284-
+ Op.DUP4 # address (duplicated earlier)
285-
+ Op.EXTCODECOPY
286-
+ Op.POP # Clean up address
309+
+ benchmark_ops # Execute operations in specified order
287310
# Increment salt for next iteration
288311
+ Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) # Increment and store salt
289312
),
@@ -313,3 +336,132 @@ def test_bloatnet_balance_extcodecopy(
313336
blocks=[Block(txs=[attack_tx])],
314337
post=post,
315338
)
339+
340+
341+
@pytest.mark.parametrize(
342+
"balance_first",
343+
[True, False],
344+
ids=["balance_extcodehash", "extcodehash_balance"],
345+
)
346+
@pytest.mark.valid_from("Prague")
347+
def test_bloatnet_balance_extcodehash(
348+
blockchain_test: BlockchainTestFiller,
349+
pre: Alloc,
350+
fork: Fork,
351+
gas_benchmark_value: int,
352+
balance_first: bool,
353+
):
354+
"""
355+
BloatNet test using BALANCE + EXTCODEHASH with on-the-fly CREATE2
356+
address generation.
357+
358+
This test:
359+
1. Assumes contracts are already deployed via the factory
360+
2. Generates CREATE2 addresses dynamically during execution
361+
3. Calls BALANCE and EXTCODEHASH (order controlled by balance_first param)
362+
4. Forces client to compute code hash for 24KB bytecode
363+
"""
364+
gas_costs = fork.gas_costs()
365+
366+
# Calculate gas costs
367+
intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"")
368+
369+
# Cost per contract access with CREATE2 address generation
370+
cost_per_contract = (
371+
gas_costs.G_KECCAK_256 # SHA3 static cost for address generation (30)
372+
+ gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6)
373+
+ gas_costs.G_COLD_ACCOUNT_ACCESS # Cold access (2600)
374+
+ gas_costs.G_BASE # POP first result (2)
375+
+ gas_costs.G_WARM_ACCOUNT_ACCESS # Warm access (100)
376+
+ gas_costs.G_BASE # POP second result (2)
377+
+ gas_costs.G_BASE # DUP1 before first op (3)
378+
+ gas_costs.G_VERY_LOW * 4 # PUSH1 operations (4 * 3)
379+
+ gas_costs.G_LOW # MLOAD for salt (3)
380+
+ gas_costs.G_VERY_LOW # ADD for increment (3)
381+
+ gas_costs.G_LOW # MSTORE salt back (3)
382+
+ 10 # While loop overhead
383+
)
384+
385+
# Calculate how many contracts to access based on available gas
386+
available_gas = gas_benchmark_value - intrinsic_gas - 1000 # Reserve for cleanup
387+
contracts_needed = int(available_gas // cost_per_contract)
388+
389+
# Deploy factory using stub contract
390+
factory_address = pre.deploy_contract(
391+
code=Bytecode(),
392+
stub="bloatnet_factory",
393+
)
394+
395+
# Log test requirements
396+
print(
397+
f"Test needs {contracts_needed} contracts for "
398+
f"{gas_benchmark_value / 1_000_000:.1f}M gas. "
399+
f"Factory storage will be checked during execution."
400+
)
401+
402+
# Define operations that differ based on parameter
403+
balance_op = Op.POP(Op.BALANCE)
404+
extcodehash_op = Op.POP(Op.EXTCODEHASH)
405+
benchmark_ops = (
406+
(balance_op + extcodehash_op) if balance_first else (extcodehash_op + balance_op)
407+
)
408+
409+
# Build attack contract that reads config from factory and performs attack
410+
attack_code = (
411+
# Call getConfig() on factory to get num_deployed and init_code_hash
412+
Op.STATICCALL(
413+
gas=Op.GAS,
414+
address=factory_address,
415+
args_offset=0,
416+
args_size=0,
417+
ret_offset=96,
418+
ret_size=64,
419+
)
420+
# Check if call succeeded
421+
+ Op.ISZERO
422+
+ Op.PUSH2(0x1000) # Jump to error handler if failed
423+
+ Op.JUMPI
424+
# Load results from memory
425+
+ Op.MLOAD(96) # Load num_deployed_contracts
426+
+ Op.MLOAD(128) # Load init_code_hash
427+
# Setup memory for CREATE2 address generation
428+
+ Op.MSTORE(0, factory_address)
429+
+ Op.MSTORE8(11, 0xFF)
430+
+ Op.MSTORE(32, 0) # Initial salt
431+
+ Op.PUSH1(64)
432+
+ Op.MSTORE # Store init_code_hash
433+
# Main attack loop
434+
+ While(
435+
body=(
436+
# Generate CREATE2 address
437+
Op.SHA3(11, 85)
438+
+ Op.DUP1 # Duplicate for second operation
439+
+ benchmark_ops # Execute operations in specified order
440+
# Increment salt
441+
+ Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1))
442+
),
443+
condition=Op.DUP1 + Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO,
444+
)
445+
+ Op.POP # Clean up counter
446+
)
447+
448+
# Deploy attack contract
449+
attack_address = pre.deploy_contract(code=attack_code)
450+
451+
# Run the attack
452+
attack_tx = Transaction(
453+
to=attack_address,
454+
gas_limit=gas_benchmark_value,
455+
sender=pre.fund_eoa(),
456+
)
457+
458+
# Post-state
459+
post = {
460+
attack_address: Account(storage={}),
461+
}
462+
463+
blockchain_test(
464+
pre=pre,
465+
blocks=[Block(txs=[attack_tx])],
466+
post=post,
467+
)

0 commit comments

Comments
 (0)