1313from ethereum_test_specs import BlockchainTestFiller
1414from ethereum_test_specs .blockchain import Block
1515from ethereum_test_types import Alloc , Transaction
16+ from ethereum_test_vm import Bytecode
17+ from ethereum_test_vm import Opcodes as Op
1618
1719
1820class DeploymentTestType (Enum ):
@@ -23,6 +25,23 @@ class DeploymentTestType(Enum):
2325 DEPLOY_AFTER_FORK = "deploy_after_fork"
2426
2527
28+ class SystemContractTestType (Enum ):
29+ """Represents the type of system contract test."""
30+
31+ GAS_LIMIT = "system_contract_reaches_gas_limit"
32+ OUT_OF_GAS_ERROR = "system_contract_out_of_gas"
33+ REVERT_ERROR = "system_contract_reverts"
34+ EXCEPTION_ERROR = "system_contract_throws"
35+
36+ def param (self ):
37+ """Return the parameter for the test."""
38+ return pytest .param (
39+ self ,
40+ id = self .value ,
41+ marks = pytest .mark .exception_test if self != SystemContractTestType .GAS_LIMIT else [],
42+ )
43+
44+
2645class ContractAddressHasBalance (Enum ):
2746 """Represents whether the target deployment test has a balance before deployment."""
2847
@@ -239,3 +258,116 @@ def wrapper(
239258 return wrapper
240259
241260 return decorator
261+
262+
263+ def generate_system_contract_error_test (
264+ * ,
265+ max_gas_limit : int ,
266+ ):
267+ """
268+ Generate a test that verifies the correct behavior when a system contract fails execution.
269+
270+ Parametrizations required:
271+ - system_contract (Address): The address of the system contract to deploy.
272+ - valid_from (Fork): The fork from which the test is valid.
273+
274+ Args:
275+ max_gas_limit (int): The maximum gas limit for the system transaction.
276+
277+ """
278+
279+ def decorator (func : SystemContractDeployTestFunction ):
280+ @pytest .mark .parametrize ("test_type" , [v .param () for v in SystemContractTestType ])
281+ @pytest .mark .execute (pytest .mark .skip (reason = "modifies pre-alloc" ))
282+ def wrapper (
283+ blockchain_test : BlockchainTestFiller ,
284+ pre : Alloc ,
285+ test_type : SystemContractTestType ,
286+ system_contract : Address ,
287+ fork : Fork ,
288+ ):
289+ modified_system_contract_code = Bytecode ()
290+
291+ # Depending on the test case, we need to modify the system contract code accordingly.
292+ if (
293+ test_type == SystemContractTestType .GAS_LIMIT
294+ or test_type == SystemContractTestType .OUT_OF_GAS_ERROR
295+ ):
296+ # Run code so that it reaches the gas limit.
297+ gas_costs = fork .gas_costs ()
298+ # The code works by storing N values to storage, and N is calculated based on the
299+ # gas costs for the given fork.
300+ # This code will only work once, so if the system contract is re-executed
301+ # in a subsequent block, it will consume less gas.
302+ gas_used_per_storage = (
303+ gas_costs .G_STORAGE_SET + gas_costs .G_COLD_SLOAD + (gas_costs .G_VERY_LOW * 2 )
304+ )
305+ modified_system_contract_code += sum (
306+ Op .SSTORE (i , 1 ) for i in range (max_gas_limit // gas_used_per_storage )
307+ )
308+ # If the gas limit is not divisible by the gas used per storage, we need to add
309+ # some NO-OP (JUMPDEST) to the code that each consume 1 gas.
310+ assert gas_costs .G_JUMPDEST == 1 , (
311+ f"JUMPDEST gas cost should be 1, but got { gas_costs .G_JUMPDEST } . "
312+ "Generator `generate_system_contract_error_test` needs to be updated."
313+ )
314+ modified_system_contract_code += sum (
315+ Op .JUMPDEST for _ in range (max_gas_limit % gas_used_per_storage )
316+ )
317+
318+ if test_type == SystemContractTestType .OUT_OF_GAS_ERROR :
319+ # If the test type is OUT_OF_GAS_ERROR, we need to add a JUMPDEST to the code
320+ # to ensure that we go over the limit by one gas.
321+ modified_system_contract_code += Op .JUMPDEST
322+ modified_system_contract_code += Op .STOP
323+ elif test_type == SystemContractTestType .REVERT_ERROR :
324+ # Run a simple revert.
325+ modified_system_contract_code = Op .REVERT (0 , 0 )
326+ elif test_type == SystemContractTestType .EXCEPTION_ERROR :
327+ # Run a simple exception.
328+ modified_system_contract_code = Op .INVALID ()
329+ else :
330+ raise ValueError (f"Invalid test type: { test_type } " )
331+
332+ pre [system_contract ] = Account (
333+ code = modified_system_contract_code ,
334+ nonce = 1 ,
335+ balance = 0 ,
336+ )
337+
338+ # Simple test transaction to verify the block failed to modify the state.
339+ value_receiver = pre .fund_eoa (amount = 0 )
340+ test_tx = Transaction (
341+ to = value_receiver ,
342+ value = 1 ,
343+ gas_limit = 100_000 ,
344+ sender = pre .fund_eoa (),
345+ )
346+ post = Alloc ()
347+ post [value_receiver ] = (
348+ Account .NONEXISTENT
349+ if test_type != SystemContractTestType .GAS_LIMIT
350+ else Account (
351+ balance = 1 ,
352+ )
353+ )
354+
355+ blockchain_test (
356+ pre = pre ,
357+ blocks = [
358+ Block ( # Deployment block
359+ txs = [test_tx ],
360+ exception = BlockException .SYSTEM_CONTRACT_CALL_FAILED
361+ if test_type != SystemContractTestType .GAS_LIMIT
362+ else None ,
363+ )
364+ ],
365+ post = post ,
366+ )
367+
368+ wrapper .__name__ = func .__name__ # type: ignore
369+ wrapper .__doc__ = func .__doc__ # type: ignore
370+
371+ return wrapper
372+
373+ return decorator
0 commit comments