2323"""
2424
2525from pathlib import Path
26- from typing import Any , List , Self
26+ from typing import Any , Callable , List , Self
2727
2828import pytest
2929from execution_testing import (
3838 Bytes ,
3939 Fork ,
4040 Hash ,
41- Initcode ,
4241 Op ,
4342 Transaction ,
4443 While ,
45- compute_deterministic_create2_address ,
4644)
4745from pydantic import BaseModel , Field
4846
@@ -195,21 +193,34 @@ class Attack(BaseModel):
195193 value : int
196194 start : int
197195 end : int
198- initcode_hash : Hash
196+ fork : Fork
197+ mined_contract_file : MinedContractFile
198+ attack_orchestrator_address : Address
199+
200+ def next (self ) -> Self :
201+ """Create a copy of the instance with the next salt as the new end."""
202+ return self .__class__ (
203+ value = self .value ,
204+ start = self .start ,
205+ end = self .end + 1 ,
206+ fork = self .fork ,
207+ mined_contract_file = self .mined_contract_file ,
208+ attack_orchestrator_address = self .attack_orchestrator_address ,
209+ )
199210
200211 def calldata (self ) -> bytes :
201212 """Return the calldata that needs to be passed to the orchestrator."""
202213 return Bytes (
203214 self .value .to_bytes (32 , "big" )
204215 + self .start .to_bytes (32 , "big" )
205216 + self .end .to_bytes (32 , "big" )
206- + self .initcode_hash
217+ + self .mined_contract_file . initcode_hash
207218 )
208219
209- def calculate_inner_call_cost (self , fork : Fork ) -> int :
220+ def calculate_inner_call_cost (self ) -> int :
210221 """Calculate the exact gas this inner call would use."""
211- gas_costs = fork .gas_costs ()
212- mem_expand_calc = fork .memory_expansion_gas_calculator ()
222+ gas_costs = self . fork .gas_costs ()
223+ mem_expand_calc = self . fork .memory_expansion_gas_calculator ()
213224 inner_call_cost = (
214225 mem_expand_calc (new_bytes = 96 )
215226 + 17 * gas_costs .G_VERY_LOW # PUSHN operations
@@ -238,11 +249,13 @@ def calculate_inner_call_cost(self, fork: Fork) -> int:
238249 )
239250 return inner_call_cost
240251
241- def calculate_gas (self , fork : Fork ) -> int :
252+ def calculate_gas (self ) -> int :
242253 """Calculate the exact gas this attack transaction will use."""
243- tx_intrinsic_gas_calc = fork .transaction_intrinsic_cost_calculator ()
244- gas_costs = fork .gas_costs ()
245- mem_expand_calc = fork .memory_expansion_gas_calculator ()
254+ tx_intrinsic_gas_calc = (
255+ self .fork .transaction_intrinsic_cost_calculator ()
256+ )
257+ gas_costs = self .fork .gas_costs ()
258+ mem_expand_calc = self .fork .memory_expansion_gas_calculator ()
246259 tx_overhead = tx_intrinsic_gas_calc (
247260 calldata = self .calldata (),
248261 return_cost_deducted_prior_execution = True ,
@@ -253,7 +266,7 @@ def calculate_gas(self, fork: Fork) -> int:
253266 + 10 * gas_costs .G_VERY_LOW # PUSH operations
254267 + 3 * gas_costs .G_VERY_LOW # CALLDATALOAD operations
255268 )
256- inner_call_cost = self .calculate_inner_call_cost (fork )
269+ inner_call_cost = self .calculate_inner_call_cost ()
257270 gas_per_attack = (
258271 1 * gas_costs .G_JUMPDEST # JUMPDEST operations
259272 + 15 * gas_costs .G_VERY_LOW # PUSH operations
@@ -280,37 +293,83 @@ def calculate_gas(self, fork: Fork) -> int:
280293 )
281294 return call_count * gas_per_attack + setup_gas + tx_overhead
282295
283- def calculate_tx_gas_limit (self , fork : Fork ) -> int :
296+ def calculate_tx_gas_limit (self ) -> int :
284297 """Calculate the gas limit required for the transaction."""
285- gas_cost = self .calculate_gas (fork )
298+ gas_cost = self .calculate_gas ()
286299 # Add the 63/64 margin for the last inner call.
287- inner_call_cost = self .calculate_inner_call_cost (fork )
300+ inner_call_cost = self .calculate_inner_call_cost ()
288301 return gas_cost + ((inner_call_cost * 64 // 63 ) - inner_call_cost )
289302
290- def generate_transaction (self , fork : Fork , sender : EOA ) -> Transaction :
303+ def generate_transaction (self , * , sender : EOA ) -> Transaction :
291304 """Generate the transaction to perform the attack."""
292- attack_orchestrator_address = compute_deterministic_create2_address (
293- salt = Hash (0 ),
294- initcode = Initcode (deploy_code = attack_orchestrator_bytecode (fork )),
295- fork = fork ,
296- )
297305 return Transaction (
298- to = attack_orchestrator_address ,
299- gas_limit = self .calculate_tx_gas_limit (fork ),
306+ to = self . attack_orchestrator_address ,
307+ gas_limit = self .calculate_tx_gas_limit (),
300308 sender = sender ,
301309 data = self .calldata (),
302310 )
303311
304- def add_post_verification (
305- self , post : Alloc , mined_contract_file : MinedContractFile
306- ) -> None :
312+ def add_post_verification (self , * , post : Alloc ) -> None :
307313 """Add the post-verification transaction to the post-state."""
308- contract = mined_contract_file .contracts [self .end ]
309- storage = dict .fromkeys (mined_contract_file .storage_keys , 1 )
310- storage [mined_contract_file .storage_keys [- 1 ]] = self .value
314+ contract = self . mined_contract_file .contracts [self .end ]
315+ storage = dict .fromkeys (self . mined_contract_file .storage_keys , 1 )
316+ storage [self . mined_contract_file .storage_keys [- 1 ]] = self .value
311317 post [contract .contract_address ] = Account (storage = storage )
312318
313319
320+ @pytest .fixture
321+ def mined_contract_file (
322+ storage_depth : int ,
323+ account_depth : int ,
324+ ) -> MinedContractFile :
325+ """Return the correct file for the given test."""
326+ mined_contract_file = MinedContractFile .load (storage_depth , account_depth )
327+ # Verify we have contracts in the JSON
328+ available_contracts = len (mined_contract_file .contracts )
329+ if available_contracts == 0 :
330+ json_name = f"s{ storage_depth } _acc{ account_depth } .json"
331+ raise ValueError (f"No contracts available in { json_name } " )
332+ return mined_contract_file
333+
334+
335+ @pytest .fixture
336+ def mined_contract_deployer (
337+ pre : Alloc ,
338+ mined_contract_file : MinedContractFile ,
339+ ) -> Callable [[int ], None ]:
340+ """Return a helper to deploy a contract for a given salt when needed."""
341+
342+ def _mined_contract_deployer (salt : int ) -> None :
343+ if salt >= len (mined_contract_file .contracts ):
344+ raise RuntimeError (
345+ f"Requested salt { salt } but only "
346+ f"{ len (mined_contract_file .contracts )} available"
347+ )
348+ salted_contract_info = mined_contract_file .contracts [salt ]
349+ assert salted_contract_info .salt == salt , (
350+ f"Salt out of order: { salted_contract_info .salt } != { salt } "
351+ )
352+ deployed_contract_address = pre .deterministic_deploy_contract (
353+ deploy_code = mined_contract_file .deploy_code ,
354+ salt = Hash (salt ),
355+ initcode = mined_contract_file .initcode ,
356+ storage = dict .fromkeys (mined_contract_file .storage_keys , 1 ),
357+ )
358+ assert (
359+ deployed_contract_address == salted_contract_info .contract_address
360+ ), (
361+ f"Contract address mismatch: { deployed_contract_address } != "
362+ f"{ salted_contract_info .contract_address } , salt: { salt } "
363+ )
364+ for auxiliary_account in salted_contract_info .auxiliary_accounts :
365+ # Ensure the account exists in the state trie
366+ pre .fund_address (
367+ address = auxiliary_account , amount = 1 , minimum_balance = True
368+ )
369+
370+ return _mined_contract_deployer
371+
372+
314373@pytest .mark .valid_from ("Prague" )
315374@pytest .mark .parametrize (
316375 "storage_depth,account_depth" ,
@@ -323,8 +382,8 @@ def test_worst_depth_stateroot_recomp(
323382 pre : Alloc ,
324383 fork : Fork ,
325384 gas_benchmark_value : int ,
326- storage_depth : int ,
327- account_depth : int ,
385+ mined_contract_file : MinedContractFile ,
386+ mined_contract_deployer : Callable [[ int ], None ] ,
328387) -> None :
329388 """
330389 BloatNet worst-case SSTORE attack benchmark with pre-deployed contracts.
@@ -341,117 +400,65 @@ def test_worst_depth_stateroot_recomp(
341400 fork: The fork to test on
342401 env: Environment object that will be used to fill/execute
343402 gas_benchmark_value: Gas budget for benchmark
344- storage_depth: Depth of storage slots in the contract
345- account_depth: Account address prefix sharing depth
403+ mined_contract_file: The mined contract file
404+ mined_contract_deployer: A function to deploy a mined contract
346405
347406 """
348- # Dynamically calculate number of contracts based on gas budget
349- print ("\n Testing with pre-deployed contracts:" )
350- print (f" Storage depth: { storage_depth } " )
351- print (f" Account depth: { account_depth } " )
352- print (f" Gas benchmark value: { gas_benchmark_value :,} " )
353-
354- # Load the CREATE2 data to get the init code hash
355- mined_contract_file = MinedContractFile .load (storage_depth , account_depth )
356- initcode_hash = mined_contract_file .initcode_hash
357-
358- # Verify we have enough contracts in the JSON
359- available_contracts = len (mined_contract_file .contracts )
360- if available_contracts == 0 :
361- json_name = f"s{ storage_depth } _acc{ account_depth } .json"
362- raise ValueError (f"No contracts available in { json_name } " )
363-
364- # Create an EOA with funds for the deployer
365- sender = pre .fund_eoa ()
366-
367407 # Deploy orchestrator to deterministic address
368- orchestrator_address = pre .deterministic_deploy_contract (
408+ attack_orchestrator_address = pre .deterministic_deploy_contract (
369409 deploy_code = attack_orchestrator_bytecode (fork )
370410 )
371- print (f" Orchestrator will be deployed at: { orchestrator_address } " )
411+ print (f" Orchestrator will be deployed at: { attack_orchestrator_address } " )
412+
413+ # Create an EOA with funds for the deployer
414+ sender = pre .fund_eoa ()
372415
373416 # Build attack transactions
374417 attack_txs : list [Transaction ] = []
375- attack_value = DEFAULT_ATTACK_VALUE
376418 accrued_tx_gas_usage = 0
377419 tx_gas_limit_cap = fork .transaction_gas_limit_cap ()
378420 post = Alloc ({})
379421
422+ # Create the starting attack
380423 current_attack_batch = Attack (
381- value = attack_value ,
424+ value = DEFAULT_ATTACK_VALUE ,
382425 start = 0 ,
383426 end = 0 ,
384- initcode_hash = initcode_hash ,
427+ fork = fork ,
428+ mined_contract_file = mined_contract_file ,
429+ attack_orchestrator_address = attack_orchestrator_address ,
385430 )
386431
387- # Create a helper to deploy a contract for a given salt when needed.
388- def deploy_mined_contract (salt : int ) -> None :
389- salted_contract_info = mined_contract_file .contracts [salt ]
390- assert salted_contract_info .salt == salt , (
391- f"Salt out of order: { salted_contract_info .salt } != { salt } "
392- )
393- deployed_contract_address = pre .deterministic_deploy_contract (
394- deploy_code = mined_contract_file .deploy_code ,
395- salt = Hash (salt ),
396- initcode = mined_contract_file .initcode ,
397- storage = dict .fromkeys (mined_contract_file .storage_keys , 1 ),
398- )
399- assert (
400- deployed_contract_address == salted_contract_info .contract_address
401- ), (
402- f"Contract address mismatch: { deployed_contract_address } != "
403- f"{ salted_contract_info .contract_address } , salt: { salt } "
404- )
405- for auxiliary_account in salted_contract_info .auxiliary_accounts :
406- # Ensure the account exists in the state trie
407- pre .fund_address (
408- address = auxiliary_account , amount = 1 , minimum_balance = True
409- )
410-
411432 # Deploy the starting contract
412- deploy_mined_contract (current_attack_batch .start )
433+ mined_contract_deployer (current_attack_batch .start )
413434
414435 while True :
415- next_attack_batch = Attack (
416- value = attack_value ,
417- start = current_attack_batch .start ,
418- end = current_attack_batch .end + 1 ,
419- initcode_hash = initcode_hash ,
420- )
421- next_batch_cost = next_attack_batch .calculate_tx_gas_limit (fork = fork )
436+ next_attack_batch = current_attack_batch .next ()
437+ next_batch_cost = next_attack_batch .calculate_tx_gas_limit ()
422438
423439 if next_batch_cost + accrued_tx_gas_usage > gas_benchmark_value :
424440 # Next contract cost would go above benchmark limit, we are done.
425441 attack_txs .append (
426- current_attack_batch .generate_transaction (fork , sender )
427- )
428- current_attack_batch .add_post_verification (
429- post , mined_contract_file
442+ current_attack_batch .generate_transaction (sender = sender )
430443 )
431- accrued_tx_gas_usage += current_attack_batch .calculate_gas (fork )
444+ current_attack_batch .add_post_verification (post = post )
445+ accrued_tx_gas_usage += current_attack_batch .calculate_gas ()
432446 break
433447
434448 # Next contract would not go above limit, but we need to check
435449 # whether we have gone above the tx limit.
436450
437451 # We are going to use the next contract regardless
438- if next_attack_batch .end > available_contracts :
439- raise RuntimeError (
440- f"Requested { next_attack_batch .end } contracts but only "
441- f"{ available_contracts } available, using { available_contracts } "
442- )
443- deploy_mined_contract (next_attack_batch .end )
452+ mined_contract_deployer (next_attack_batch .end )
444453
445454 if tx_gas_limit_cap is not None and next_batch_cost > tx_gas_limit_cap :
446455 # Adding a contract would go above the transaction gas limit cap,
447456 # make the cut here.
448457 attack_txs .append (
449- current_attack_batch .generate_transaction (fork , sender )
450- )
451- current_attack_batch .add_post_verification (
452- post , mined_contract_file
458+ current_attack_batch .generate_transaction (sender = sender )
453459 )
454- accrued_tx_gas_usage += current_attack_batch .calculate_gas (fork )
460+ current_attack_batch .add_post_verification (post = post )
461+ accrued_tx_gas_usage += current_attack_batch .calculate_gas ()
455462
456463 next_attack_batch .start = next_attack_batch .end
457464
0 commit comments