Skip to content

Commit c8ec6e6

Browse files
authored
Merge pull request #1394 from ethereum/eip-7002-7251-no-code-tests
refactor(tests): EIP-7002, EIP-7251: Test block invalidation if system contract is missing
2 parents 074dbc0 + 23f0ae2 commit c8ec6e6

File tree

14 files changed

+279
-34
lines changed

14 files changed

+279
-34
lines changed

docs/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ Test fixtures for use by clients are available for each release on the [Github r
2020

2121
- 🐞 Fix fixture tarball downloading with regular, non-Github release URLS and with numerical versions in regular release specs, e.g., `[email protected]` ([#1437](https://github.com/ethereum/execution-spec-tests/pull/1437)).
2222

23+
#### Tools
24+
25+
- 🔀 `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)).
27+
28+
#### Exceptions
29+
30+
- ✨ New exceptions `BlockException.SYSTEM_CONTRACT_EMPTY` and `BlockException.SYSTEM_CONTRACT_CALL_FAILED` to handle EIP updates [#9508](https://github.com/ethereum/EIPs/pull/9508) and [#9582](https://github.com/ethereum/EIPs/pull/9582) ([#1394](https://github.com/ethereum/execution-spec-tests/pull/1394)).
31+
2332
### 🧪 Test Cases
2433

2534
-[EIP-7702](https://eips.ethereum.org/EIPS/eip-7702): Test precompile case in same transaction as delegation without extra gas in case of precompile code execution; parametrize all call opcodes in existing precompile test ([#1431](https://github.com/ethereum/execution-spec-tests/pull/1431)).
@@ -29,10 +38,13 @@ Test fixtures for use by clients are available for each release on the [Github r
2938
-[EIP-7251](https://eips.ethereum.org/EIPS/eip-7251): Add EIP-7251 test cases for modified consolidations contract that allows more consolidations ([#1465](https://github.com/ethereum/execution-spec-tests/pull/1465)).
3039
-[EIP-6110](https://eips.ethereum.org/EIPS/eip-6110): Add extra deposit request edge cases, sending eth to the deposit contract while sending a deposit request ([#1467](https://github.com/ethereum/execution-spec-tests/pull/1467)).
3140
-[EIP-7251](https://eips.ethereum.org/EIPS/eip-7251): Remove pytest skips for consolidation request cases ([#1449](https://github.com/ethereum/execution-spec-tests/pull/1449)).
41+
-[EIP-7002](https://eips.ethereum.org/EIPS/eip-7002), [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251): Add cases to verify behavior of contracts missing at fork ([#1394](https://github.com/ethereum/execution-spec-tests/pull/1394)).
42+
-[EIP-7002](https://eips.ethereum.org/EIPS/eip-7002), [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251): Add cases to verify behavior of system contract errors invalidating a block ([#1394](https://github.com/ethereum/execution-spec-tests/pull/1394)).
3243

3344
### 📋 Misc
3445

3546
- 🐞 Configure `markdownlint` to expect an indent of 4 with unordered lists (otherwise HTML documentation is rendered incorrectly, [#1460](https://github.com/ethereum/execution-spec-tests/pull/1460)).
47+
- 🔀 Update `eels_resolutions.json` to point to temporary commit `bb0eb750d643ced0ebf5dec732cdd23558d0b7f2`, which is based on `forks/prague` branch, commit `d9a7ee24db359aacecd636349b4f3ac95a4a6e71`, with PRs https://github.com/ethereum/execution-specs/pull/1182, https://github.com/ethereum/execution-specs/pull/1183 and https://github.com/ethereum/execution-specs/pull/1191 merged ([#1394](https://github.com/ethereum/execution-spec-tests/pull/1394)).
3648

3749
## [v4.2.0](https://github.com/ethereum/execution-spec-tests/releases/tag/v4.2.0) - 2025-04-08
3850

eels_resolutions.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
"same_as": "EELSMaster"
3636
},
3737
"Prague": {
38-
"git_url": "https://github.com/gurukamath/execution-specs.git",
39-
"branch": "7702-to-precompile",
40-
"commit": "bbc469729ab095300b07de8fe323c3f9fac31857"
38+
"git_url": "https://github.com/marioevz/execution-specs.git",
39+
"branch": "forks/prague",
40+
"commit": "bb0eb750d643ced0ebf5dec732cdd23558d0b7f2"
4141
}
4242
}

src/ethereum_clis/clis/execution_specs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from typing import ClassVar, Dict, List, Optional
1414

1515
from ethereum_test_exceptions import (
16+
BlockException,
1617
EOFException,
1718
ExceptionBase,
1819
ExceptionMapper,
@@ -159,6 +160,8 @@ class ExecutionSpecsExceptionMapper(ExceptionMapper):
159160
TransactionException.TYPE_3_TX_CONTRACT_CREATION: "ction: ",
160161
TransactionException.NONCE_IS_MAX: "tion: ",
161162
TransactionException.GAS_ALLOWANCE_EXCEEDED: "ion: ",
163+
BlockException.SYSTEM_CONTRACT_EMPTY: "System contract address",
164+
BlockException.SYSTEM_CONTRACT_CALL_FAILED: "call failed:",
162165
# TODO EVMONE needs to differentiate when the section is missing in the header or body
163166
EOFException.MISSING_STOP_OPCODE: "err: no_terminating_instruction",
164167
EOFException.MISSING_CODE_HEADER: "err: code_section_missing",

src/ethereum_test_exceptions/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,14 @@ class BlockException(ExceptionBase):
574574
"""
575575
Trying to import a block after paris fork that has difficulty != 0.
576576
"""
577+
SYSTEM_CONTRACT_EMPTY = auto()
578+
"""
579+
A system contract address contains no code at the end of fork activation block.
580+
"""
581+
SYSTEM_CONTRACT_CALL_FAILED = auto()
582+
"""
583+
A system contract call at the end of block execution (from the system address) fails.
584+
"""
577585

578586

579587
@unique

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: 192 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,40 @@
88
import pytest
99

1010
from ethereum_test_base_types import Account, Address, Hash
11+
from ethereum_test_exceptions import BlockException
1112
from ethereum_test_forks import Fork
1213
from ethereum_test_specs import BlockchainTestFiller
1314
from ethereum_test_specs.blockchain import Block
1415
from ethereum_test_types import Alloc, Transaction
16+
from ethereum_test_vm import Bytecode
17+
from ethereum_test_vm import Opcodes as Op
1518

1619

1720
class DeploymentTestType(Enum):
1821
"""Represents the type of deployment test."""
1922

2023
DEPLOY_BEFORE_FORK = "deploy_before_fork"
24+
DEPLOY_ON_FORK_BLOCK = "deploy_on_fork_block"
2125
DEPLOY_AFTER_FORK = "deploy_after_fork"
2226

2327

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+
2445
class ContractAddressHasBalance(Enum):
2546
"""Represents whether the target deployment test has a balance before deployment."""
2647

@@ -63,29 +84,33 @@ def generate_system_contract_deploy_test(
6384
fork: Fork,
6485
tx_json_path: Path,
6586
expected_deploy_address: Address,
87+
fail_on_empty_code: bool,
6688
expected_system_contract_storage: Dict | None = None,
6789
):
6890
"""
6991
Generate a test that verifies the correct deployment of a system contract.
7092
71-
Generates four test cases:
93+
Generates following test cases:
7294
73-
| before/after fork | has balance |
74-
------------------------------------|-------------------|-------------|
75-
`deploy_before_fork-nonzero_balance`| before | True |
76-
`deploy_before_fork-zero_balance` | before | False |
77-
`deploy_after_fork-nonzero_balance` | after | True |
78-
`deploy_after_fork-zero_balance` | after | False |
95+
| before/after fork | fail on | invalid block |
96+
| | empty block | |
97+
--------------------------------------|-------------------|-------------|---------------|
98+
`deploy_before_fork-nonzero_balance` | before | False | False |
99+
`deploy_before_fork-zero_balance` | before | True | False |
100+
`deploy_on_fork_block-nonzero_balance`| on fork block | False | False |
101+
`deploy_on_fork_block-zero_balance` | on fork block | True | False |
102+
`deploy_after_fork-nonzero_balance` | after | False | False |
103+
`deploy_after_fork-zero_balance` | after | True | True |
79104
80-
where `has balance` refers to whether the contract address has a non-zero balance before
81-
deployment, or not.
105+
The `has balance` parametrization does not have an effect on the expectation of the test.
82106
83107
Args:
84108
fork (Fork): The fork to test.
85109
tx_json_path (Path): Path to the JSON file with the transaction to deploy the system
86110
contract.
87111
Providing a JSON file is useful to copy-paste the transaction from the EIP.
88112
expected_deploy_address (Address): The expected address of the deployed contract.
113+
fail_on_empty_code (bool): If True, the test is expected to fail on empty code.
89114
expected_system_contract_storage (Dict | None): The expected storage of the system
90115
contract.
91116
@@ -120,7 +145,11 @@ def decorator(func: SystemContractDeployTestFunction):
120145
"test_type",
121146
[
122147
pytest.param(DeploymentTestType.DEPLOY_BEFORE_FORK),
123-
pytest.param(DeploymentTestType.DEPLOY_AFTER_FORK),
148+
pytest.param(DeploymentTestType.DEPLOY_ON_FORK_BLOCK),
149+
pytest.param(
150+
DeploymentTestType.DEPLOY_AFTER_FORK,
151+
marks=[pytest.mark.exception_test] if fail_on_empty_code else [],
152+
),
124153
],
125154
ids=lambda x: x.name.lower(),
126155
)
@@ -148,17 +177,40 @@ def wrapper(
148177
timestamp=15_000,
149178
),
150179
]
151-
elif test_type == DeploymentTestType.DEPLOY_AFTER_FORK:
180+
elif test_type == DeploymentTestType.DEPLOY_ON_FORK_BLOCK:
152181
blocks = [
153-
Block( # Empty block on fork
154-
txs=[],
182+
Block( # Deployment on fork block
183+
txs=[deploy_tx],
155184
timestamp=15_000,
156185
),
157-
Block( # Deployment block
158-
txs=[deploy_tx],
186+
Block( # Empty block after fork
187+
txs=[],
159188
timestamp=15_001,
160189
),
161190
]
191+
elif test_type == DeploymentTestType.DEPLOY_AFTER_FORK:
192+
blocks = [
193+
Block( # Empty block on fork
194+
txs=[],
195+
timestamp=15_000,
196+
exception=BlockException.SYSTEM_CONTRACT_EMPTY
197+
if fail_on_empty_code
198+
else None,
199+
)
200+
]
201+
if not fail_on_empty_code:
202+
blocks.append(
203+
Block( # Deployment after fork block
204+
txs=[deploy_tx],
205+
timestamp=15_001,
206+
)
207+
)
208+
blocks.append(
209+
Block( # Empty block after deployment
210+
txs=[],
211+
timestamp=15_002,
212+
),
213+
)
162214
balance = 1 if has_balance == ContractAddressHasBalance.NONZERO_BALANCE else 0
163215
pre[expected_deploy_address] = Account(
164216
code=b"", # Remove the code that is automatically allocated on the fork
@@ -176,24 +228,23 @@ def wrapper(
176228
assert expected_deploy_address_int in fork_pre_allocation
177229
expected_code = fork_pre_allocation[expected_deploy_address_int]["code"]
178230
# Note: balance check is omitted; it may be modified by the underlying, decorated test
179-
if expected_system_contract_storage is None:
180-
post[expected_deploy_address] = Account(
181-
code=expected_code,
231+
account_kwargs = {
232+
"code": expected_code,
233+
"nonce": 1,
234+
}
235+
if expected_system_contract_storage:
236+
account_kwargs["storage"] = expected_system_contract_storage
237+
if test_type != DeploymentTestType.DEPLOY_AFTER_FORK or not fail_on_empty_code:
238+
post[expected_deploy_address] = Account(**account_kwargs)
239+
post[deployer_address] = Account(
182240
nonce=1,
183241
)
184-
else:
185-
post[expected_deploy_address] = Account(
186-
storage=expected_system_contract_storage,
187-
code=expected_code,
188-
nonce=1,
189-
)
190-
post[deployer_address] = Account(
191-
nonce=1,
192-
)
193242

194243
# Extra blocks (if any) returned by the decorated function to add after the
195244
# contract is deployed.
196-
blocks += list(func(fork=fork, pre=pre, post=post, test_type=test_type))
245+
if test_type != DeploymentTestType.DEPLOY_AFTER_FORK or not fail_on_empty_code:
246+
# Only fill more blocks if the deploy block does not fail.
247+
blocks += list(func(fork=fork, pre=pre, post=post, test_type=test_type))
197248

198249
blockchain_test(
199250
pre=pre,
@@ -207,3 +258,116 @@ def wrapper(
207258
return wrapper
208259

209260
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)