Skip to content

Commit d5862be

Browse files
committed
feat(tools): Add system contract error tests generator
1 parent ca18196 commit d5862be

File tree

3 files changed

+139
-1
lines changed

3 files changed

+139
-1
lines changed

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Test fixtures for use by clients are available for each release on the [Github r
2323
#### Tools
2424

2525
- 🔀 `generate_system_contract_deploy_test` test generator has been updated to handle system contracts that are not allowed to be absent when the fork happens ([#1394](https://github.com/ethereum/execution-spec-tests/pull/1394)).
26+
- ✨ Add `generate_system_contract_error_test` to generate tests on system contracts that invalidate a block in case of error ([#1394](https://github.com/ethereum/execution-spec-tests/pull/1394)).
2627

2728
#### Exceptions
2829

src/ethereum_test_tools/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@
8282
Yul,
8383
YulCompiler,
8484
)
85-
from .utility.generators import DeploymentTestType, generate_system_contract_deploy_test
85+
from .utility.generators import (
86+
DeploymentTestType,
87+
generate_system_contract_deploy_test,
88+
generate_system_contract_error_test,
89+
)
8690
from .utility.pytest import extend_with_defaults
8791

8892
__all__ = (
@@ -157,6 +161,7 @@
157161
"compute_eofcreate_address",
158162
"extend_with_defaults",
159163
"generate_system_contract_deploy_test",
164+
"generate_system_contract_error_test",
160165
"keccak256",
161166
"vm",
162167
)

src/ethereum_test_tools/utility/generators.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from ethereum_test_specs import BlockchainTestFiller
1414
from ethereum_test_specs.blockchain import Block
1515
from ethereum_test_types import Alloc, Transaction
16+
from ethereum_test_vm import Bytecode
17+
from ethereum_test_vm import Opcodes as Op
1618

1719

1820
class 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+
2645
class 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

Comments
 (0)