Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ Test fixtures for use by clients are available for each release on the [Github r

- 🐞 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)).

#### Tools

- 🔀 `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)).
- ✨ 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)).

#### Exceptions

- ✨ 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)).

### 🧪 Test Cases

- ✨ [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)).
Expand All @@ -29,10 +38,13 @@ Test fixtures for use by clients are available for each release on the [Github r
- ✨ [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)).
- ✨ [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)).
- ✨ [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)).
- ✨ [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)).
- ✨ [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)).

### 📋 Misc

- 🐞 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)).
- 🔀 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)).

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

Expand Down
6 changes: 3 additions & 3 deletions eels_resolutions.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
"same_as": "EELSMaster"
},
"Prague": {
"git_url": "https://github.com/gurukamath/execution-specs.git",
"branch": "7702-to-precompile",
"commit": "bbc469729ab095300b07de8fe323c3f9fac31857"
"git_url": "https://github.com/marioevz/execution-specs.git",
"branch": "forks/prague",
"commit": "bb0eb750d643ced0ebf5dec732cdd23558d0b7f2"
}
}
3 changes: 3 additions & 0 deletions src/ethereum_clis/clis/execution_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from typing import ClassVar, Dict, List, Optional

from ethereum_test_exceptions import (
BlockException,
EOFException,
ExceptionBase,
ExceptionMapper,
Expand Down Expand Up @@ -159,6 +160,8 @@ class ExecutionSpecsExceptionMapper(ExceptionMapper):
TransactionException.TYPE_3_TX_CONTRACT_CREATION: "ction: ",
TransactionException.NONCE_IS_MAX: "tion: ",
TransactionException.GAS_ALLOWANCE_EXCEEDED: "ion: ",
BlockException.SYSTEM_CONTRACT_EMPTY: "System contract address",
BlockException.SYSTEM_CONTRACT_CALL_FAILED: "call failed:",
# TODO EVMONE needs to differentiate when the section is missing in the header or body
EOFException.MISSING_STOP_OPCODE: "err: no_terminating_instruction",
EOFException.MISSING_CODE_HEADER: "err: code_section_missing",
Expand Down
8 changes: 8 additions & 0 deletions src/ethereum_test_exceptions/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,14 @@ class BlockException(ExceptionBase):
"""
Trying to import a block after paris fork that has difficulty != 0.
"""
SYSTEM_CONTRACT_EMPTY = auto()
"""
A system contract address contains no code at the end of fork activation block.
"""
SYSTEM_CONTRACT_CALL_FAILED = auto()
"""
A system contract call at the end of block execution (from the system address) fails.
"""


@unique
Expand Down
7 changes: 6 additions & 1 deletion src/ethereum_test_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@
Yul,
YulCompiler,
)
from .utility.generators import DeploymentTestType, generate_system_contract_deploy_test
from .utility.generators import (
DeploymentTestType,
generate_system_contract_deploy_test,
generate_system_contract_error_test,
)
from .utility.pytest import extend_with_defaults

__all__ = (
Expand Down Expand Up @@ -157,6 +161,7 @@
"compute_eofcreate_address",
"extend_with_defaults",
"generate_system_contract_deploy_test",
"generate_system_contract_error_test",
"keccak256",
"vm",
)
220 changes: 192 additions & 28 deletions src/ethereum_test_tools/utility/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,40 @@
import pytest

from ethereum_test_base_types import Account, Address, Hash
from ethereum_test_exceptions import BlockException
from ethereum_test_forks import Fork
from ethereum_test_specs import BlockchainTestFiller
from ethereum_test_specs.blockchain import Block
from ethereum_test_types import Alloc, Transaction
from ethereum_test_vm import Bytecode
from ethereum_test_vm import Opcodes as Op


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

DEPLOY_BEFORE_FORK = "deploy_before_fork"
DEPLOY_ON_FORK_BLOCK = "deploy_on_fork_block"
DEPLOY_AFTER_FORK = "deploy_after_fork"


class SystemContractTestType(Enum):
"""Represents the type of system contract test."""

GAS_LIMIT = "system_contract_reaches_gas_limit"
OUT_OF_GAS_ERROR = "system_contract_out_of_gas"
REVERT_ERROR = "system_contract_reverts"
EXCEPTION_ERROR = "system_contract_throws"

def param(self):
"""Return the parameter for the test."""
return pytest.param(
self,
id=self.value,
marks=pytest.mark.exception_test if self != SystemContractTestType.GAS_LIMIT else [],
)


class ContractAddressHasBalance(Enum):
"""Represents whether the target deployment test has a balance before deployment."""

Expand Down Expand Up @@ -63,29 +84,33 @@ def generate_system_contract_deploy_test(
fork: Fork,
tx_json_path: Path,
expected_deploy_address: Address,
fail_on_empty_code: bool,
expected_system_contract_storage: Dict | None = None,
):
"""
Generate a test that verifies the correct deployment of a system contract.

Generates four test cases:
Generates following test cases:

| before/after fork | has balance |
------------------------------------|-------------------|-------------|
`deploy_before_fork-nonzero_balance`| before | True |
`deploy_before_fork-zero_balance` | before | False |
`deploy_after_fork-nonzero_balance` | after | True |
`deploy_after_fork-zero_balance` | after | False |
| before/after fork | fail on | invalid block |
| | empty block | |
--------------------------------------|-------------------|-------------|---------------|
`deploy_before_fork-nonzero_balance` | before | False | False |
`deploy_before_fork-zero_balance` | before | True | False |
`deploy_on_fork_block-nonzero_balance`| on fork block | False | False |
`deploy_on_fork_block-zero_balance` | on fork block | True | False |
`deploy_after_fork-nonzero_balance` | after | False | False |
`deploy_after_fork-zero_balance` | after | True | True |

where `has balance` refers to whether the contract address has a non-zero balance before
deployment, or not.
The `has balance` parametrization does not have an effect on the expectation of the test.

Args:
fork (Fork): The fork to test.
tx_json_path (Path): Path to the JSON file with the transaction to deploy the system
contract.
Providing a JSON file is useful to copy-paste the transaction from the EIP.
expected_deploy_address (Address): The expected address of the deployed contract.
fail_on_empty_code (bool): If True, the test is expected to fail on empty code.
expected_system_contract_storage (Dict | None): The expected storage of the system
contract.

Expand Down Expand Up @@ -120,7 +145,11 @@ def decorator(func: SystemContractDeployTestFunction):
"test_type",
[
pytest.param(DeploymentTestType.DEPLOY_BEFORE_FORK),
pytest.param(DeploymentTestType.DEPLOY_AFTER_FORK),
pytest.param(DeploymentTestType.DEPLOY_ON_FORK_BLOCK),
pytest.param(
DeploymentTestType.DEPLOY_AFTER_FORK,
marks=[pytest.mark.exception_test] if fail_on_empty_code else [],
),
],
ids=lambda x: x.name.lower(),
)
Expand Down Expand Up @@ -148,17 +177,40 @@ def wrapper(
timestamp=15_000,
),
]
elif test_type == DeploymentTestType.DEPLOY_AFTER_FORK:
elif test_type == DeploymentTestType.DEPLOY_ON_FORK_BLOCK:
blocks = [
Block( # Empty block on fork
txs=[],
Block( # Deployment on fork block
txs=[deploy_tx],
timestamp=15_000,
),
Block( # Deployment block
txs=[deploy_tx],
Block( # Empty block after fork
txs=[],
timestamp=15_001,
),
]
elif test_type == DeploymentTestType.DEPLOY_AFTER_FORK:
blocks = [
Block( # Empty block on fork
txs=[],
timestamp=15_000,
exception=BlockException.SYSTEM_CONTRACT_EMPTY
if fail_on_empty_code
else None,
)
]
if not fail_on_empty_code:
blocks.append(
Block( # Deployment after fork block
txs=[deploy_tx],
timestamp=15_001,
)
)
blocks.append(
Block( # Empty block after deployment
txs=[],
timestamp=15_002,
),
)
balance = 1 if has_balance == ContractAddressHasBalance.NONZERO_BALANCE else 0
pre[expected_deploy_address] = Account(
code=b"", # Remove the code that is automatically allocated on the fork
Expand All @@ -176,24 +228,23 @@ def wrapper(
assert expected_deploy_address_int in fork_pre_allocation
expected_code = fork_pre_allocation[expected_deploy_address_int]["code"]
# Note: balance check is omitted; it may be modified by the underlying, decorated test
if expected_system_contract_storage is None:
post[expected_deploy_address] = Account(
code=expected_code,
account_kwargs = {
"code": expected_code,
"nonce": 1,
}
if expected_system_contract_storage:
account_kwargs["storage"] = expected_system_contract_storage
if test_type != DeploymentTestType.DEPLOY_AFTER_FORK or not fail_on_empty_code:
post[expected_deploy_address] = Account(**account_kwargs)
post[deployer_address] = Account(
nonce=1,
)
else:
post[expected_deploy_address] = Account(
storage=expected_system_contract_storage,
code=expected_code,
nonce=1,
)
post[deployer_address] = Account(
nonce=1,
)

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

blockchain_test(
pre=pre,
Expand All @@ -207,3 +258,116 @@ def wrapper(
return wrapper

return decorator


def generate_system_contract_error_test(
*,
max_gas_limit: int,
):
"""
Generate a test that verifies the correct behavior when a system contract fails execution.

Parametrizations required:
- system_contract (Address): The address of the system contract to deploy.
- valid_from (Fork): The fork from which the test is valid.

Args:
max_gas_limit (int): The maximum gas limit for the system transaction.

"""

def decorator(func: SystemContractDeployTestFunction):
@pytest.mark.parametrize("test_type", [v.param() for v in SystemContractTestType])
@pytest.mark.execute(pytest.mark.skip(reason="modifies pre-alloc"))
def wrapper(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
test_type: SystemContractTestType,
system_contract: Address,
fork: Fork,
):
modified_system_contract_code = Bytecode()

# Depending on the test case, we need to modify the system contract code accordingly.
if (
test_type == SystemContractTestType.GAS_LIMIT
or test_type == SystemContractTestType.OUT_OF_GAS_ERROR
):
# Run code so that it reaches the gas limit.
gas_costs = fork.gas_costs()
# The code works by storing N values to storage, and N is calculated based on the
# gas costs for the given fork.
# This code will only work once, so if the system contract is re-executed
# in a subsequent block, it will consume less gas.
gas_used_per_storage = (
gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD + (gas_costs.G_VERY_LOW * 2)
)
modified_system_contract_code += sum(
Op.SSTORE(i, 1) for i in range(max_gas_limit // gas_used_per_storage)
)
# If the gas limit is not divisible by the gas used per storage, we need to add
# some NO-OP (JUMPDEST) to the code that each consume 1 gas.
assert gas_costs.G_JUMPDEST == 1, (
f"JUMPDEST gas cost should be 1, but got {gas_costs.G_JUMPDEST}. "
"Generator `generate_system_contract_error_test` needs to be updated."
)
modified_system_contract_code += sum(
Op.JUMPDEST for _ in range(max_gas_limit % gas_used_per_storage)
)

if test_type == SystemContractTestType.OUT_OF_GAS_ERROR:
# If the test type is OUT_OF_GAS_ERROR, we need to add a JUMPDEST to the code
# to ensure that we go over the limit by one gas.
modified_system_contract_code += Op.JUMPDEST
modified_system_contract_code += Op.STOP
elif test_type == SystemContractTestType.REVERT_ERROR:
# Run a simple revert.
modified_system_contract_code = Op.REVERT(0, 0)
elif test_type == SystemContractTestType.EXCEPTION_ERROR:
# Run a simple exception.
modified_system_contract_code = Op.INVALID()
else:
raise ValueError(f"Invalid test type: {test_type}")

pre[system_contract] = Account(
code=modified_system_contract_code,
nonce=1,
balance=0,
)

# Simple test transaction to verify the block failed to modify the state.
value_receiver = pre.fund_eoa(amount=0)
test_tx = Transaction(
to=value_receiver,
value=1,
gas_limit=100_000,
sender=pre.fund_eoa(),
)
post = Alloc()
post[value_receiver] = (
Account.NONEXISTENT
if test_type != SystemContractTestType.GAS_LIMIT
else Account(
balance=1,
)
)

blockchain_test(
pre=pre,
blocks=[
Block( # Deployment block
txs=[test_tx],
exception=BlockException.SYSTEM_CONTRACT_CALL_FAILED
if test_type != SystemContractTestType.GAS_LIMIT
else None,
)
],
post=post,
)

wrapper.__name__ = func.__name__ # type: ignore
wrapper.__doc__ = func.__doc__ # type: ignore

return wrapper

return decorator
Loading
Loading