Skip to content
Draft
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
13 changes: 13 additions & 0 deletions packages/testing/src/execution_testing/specs/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,19 @@ def generate_block_data(
block_number=env.number, timestamp=env.timestamp
)
env = env.set_fork_requirements(fork)
parent_fork = self.fork.fork_at(
block_number=max(int(env.number) - 1, 0),
timestamp=int(previous_env.timestamp),
)
env = env.model_copy(
update={
"parent_fork": (
parent_fork.transition_tool_name()
if parent_fork is not fork
else None
)
}
)
txs = block.txs[:]
if any("gas_limit" not in tx.model_fields_set for tx in block.txs):
max_tx_gas_limit = Transaction.calculate_max_gas_limit(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ def strip_computed_fields(cls, data: Any) -> Any:
block_access_list_hash: Hash | None = Field(None)
block_access_lists: Bytes | None = Field(None)

parent_fork: str | None = Field(
None,
alias="parentFork",
description="Transition-tool name of the parent block's fork, set "
"only on a fork activation block.",
)

@computed_field # type: ignore[prop-decorator]
@cached_property
def parent_hash(self) -> Hash | None:
Expand Down
39 changes: 39 additions & 0 deletions src/ethereum/forks/amsterdam/fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
incorporate_tx_into_block,
increment_nonce,
set_account_balance,
set_code,
)
from .transactions import (
TX_MAX_GAS_LIMIT,
Expand Down Expand Up @@ -146,6 +147,17 @@
HISTORY_STORAGE_ADDRESS = hex_to_address(
"0x0000F90827F1C53a10cb7A02335B175320002935"
)
DETERMINISTIC_FACTORY_ADDRESS = hex_to_address(
"0x4e59b44847b379578588920cA78FbF26c0B4956C"
)
DETERMINISTIC_FACTORY_CODE = Bytes(
bytes.fromhex(
"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"ffe03601600081602082378035828234f58015156039578182fd5b80825250"
"50506014600cf3"
)
)
DETERMINISTIC_FACTORY_CODE_HASH = keccak256(DETERMINISTIC_FACTORY_CODE)
MAX_BLOCK_SIZE = 10_485_760
SAFETY_MARGIN = 2_097_152
MAX_RLP_BLOCK_SIZE = MAX_BLOCK_SIZE - SAFETY_MARGIN
Expand Down Expand Up @@ -337,6 +349,9 @@ def execute_block(
slot_number=block.header.slot_number,
)

if isinstance(parent_header, PreviousHeader):
deploy_deterministic_factory(block_env)

block_output = apply_body(
block_env=block_env,
transactions=block.transactions,
Expand Down Expand Up @@ -842,6 +857,30 @@ def process_unchecked_system_transaction(
return system_tx_output


def deploy_deterministic_factory(block_env: vm.BlockEnvironment) -> None:
"""
Install the [EIP-7997] deterministic deployment factory on the fork
activation block if its canonical runtime code is not already
present. The factory account is recorded in the block access list at
the pre-execution index: with its changes when installed, or as an
accessed account when the code check finds it already present.

[EIP-7997]: https://eips.ethereum.org/EIPS/eip-7997
"""
tx_state = TransactionState(parent=block_env.state)
account = get_account(tx_state, DETERMINISTIC_FACTORY_ADDRESS)
if account.code_hash != DETERMINISTIC_FACTORY_CODE_HASH:
set_code(
tx_state,
DETERMINISTIC_FACTORY_ADDRESS,
DETERMINISTIC_FACTORY_CODE,
)
if account.nonce == Uint(0):
increment_nonce(tx_state, DETERMINISTIC_FACTORY_ADDRESS)

incorporate_tx_into_block(tx_state, block_env.block_access_list_builder)


def apply_body(
block_env: vm.BlockEnvironment,
transactions: Tuple[LegacyTransaction | Bytes, ...],
Expand Down
10 changes: 10 additions & 0 deletions src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ def state_transition(self) -> Any:
"""state_transition function of the fork."""
return self._module("fork").state_transition

@property
def has_deploy_deterministic_factory(self) -> bool:
"""Check if the fork installs the EIP-7997 factory."""
return hasattr(self._module("fork"), "deploy_deterministic_factory")

@property
def deploy_deterministic_factory(self) -> Any:
"""deploy_deterministic_factory function of the fork."""
return self._module("fork").deploy_deterministic_factory

@property
def signing_hash(self) -> Any:
"""signing_hash function of the fork."""
Expand Down
27 changes: 27 additions & 0 deletions src/ethereum_spec_tools/evm_tools/t8n/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import fnmatch
import json
import os
import re
from contextlib import AbstractContextManager
from typing import Any, Final, TextIO, Type, TypeVar

Expand All @@ -24,6 +25,7 @@
)

from ..loaders.fixture_loader import Load
from ..loaders.fork_loader import ForkLoad
from ..utils import (
FatalError,
find_fork,
Expand Down Expand Up @@ -177,6 +179,7 @@ def __init__(
else:
stdin = None

self.forks = forks
fork_module, self.fork_block = find_fork(forks, self.options, stdin)

fork_criteria = None
Expand Down Expand Up @@ -380,7 +383,31 @@ def run_state_test(self) -> Any:
self.result.update(self, block_env, block_output)
self.result.rejected = self.txs.rejected_txs

def _apply_fork_activation_operations(self, block_env: Any) -> None:
"""
Apply irregular state transitions on a fork activation block.

The filler marks the activation block by passing the parent
block's fork name in the environment.
"""
if self.env.parent_fork is None:
return
if not self.fork.has_deploy_deterministic_factory:
return
parent_short_name = re.sub(
r"(?<!^)(?=[A-Z])", "_", self.env.parent_fork
).lower()
parent_short_name = re.sub("^b_p_o", "bpo", parent_short_name)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you leave a comment what this does or what this parent_short_name is? Does this standardize fork (transition?) names?

for hardfork in self.forks:
if hardfork.short_name == parent_short_name:
parent_fork = ForkLoad(hardfork)
if not parent_fork.has_deploy_deterministic_factory:
self.fork.deploy_deterministic_factory(block_env)
return

def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None:
self._apply_fork_activation_operations(block_env)

if self.fork.has_compute_requests_hash:
self.fork.process_unchecked_system_transaction(
block_env=block_env,
Expand Down
2 changes: 2 additions & 0 deletions src/ethereum_spec_tools/evm_tools/t8n/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class Env:
prev_randao: Optional[Bytes32]
parent_difficulty: Optional[Uint]
parent_timestamp: Optional[U256]
parent_fork: Optional[str]
base_fee_per_gas: Optional[Uint]
parent_gas_used: Optional[Uint]
parent_gas_limit: Optional[Uint]
Expand All @@ -68,6 +69,7 @@ def __init__(self, t8n: "T8N", stdin: Optional[Dict] = None):
self.block_gas_limit = parse_hex_or_int(data["currentGasLimit"], Uint)
self.block_number = parse_hex_or_int(data["currentNumber"], Uint)
self.block_timestamp = parse_hex_or_int(data["currentTimestamp"], U256)
self.parent_fork = data.get("parentFork")

self.read_block_difficulty(data, t8n)
self.read_base_fee_per_gas(data, t8n)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,8 @@
| `test_invalid_post_fork_block_without_bal_hash_field` | Verify clients reject an Amsterdam activation block whose header is missing `block_access_list_hash`. File: `tests/amsterdam/eip7928_block_level_access_lists/test_fork_transition.py`. | Single block at `timestamp=15_000` with a regular transfer, mutated via `rlp_modifier=Header(block_access_list_hash=Header.REMOVE_FIELD)` so the field is dropped from the header. | Block **MUST** be rejected with `BlockException.INVALID_BAL_HASH`: clients re-derive the BAL hash from execution and find no header hash to match. The engine fixture omits the `blockAccessList` param from `newPayloadV5`, which **MUST** return `-32602: Invalid params`. | ✅ Completed |
| `test_fork_transition_bal_size_constraint` | Verify the BAL size constraint (`bal_items <= gas_limit // BLOCK_ACCESS_LIST_ITEM`) applies only on/after Amsterdam. File: `tests/amsterdam/eip7928_block_level_access_lists/test_fork_transition.py`. Parametrized over `exceeds_limit_at_fork`: `at_fork_within_budget` (`gas_limit == empty_block_bal_item_count() * BLOCK_ACCESS_LIST_ITEM`) and `at_fork_over_budget` (`gas_limit` one wei below that). | Two empty blocks: pre-fork (`timestamp=14_999`) and activation block (`timestamp=15_000`). The same low `gas_limit` is used for both via `genesis_environment=Environment(gas_limit=...)`. | Pre-fork block **MUST** be accepted under both budgets (constraint not yet enforced). Activation block **MUST** be accepted at the exact budget and **MUST** be rejected with `BlockException.BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED` one item over the budget. | ✅ Completed |
| `test_bal_dirty_account_selfdestruct` | Ensure BAL does not record dirty state on a same-tx ephemeral contract whose `SELFDESTRUCT` takes effect. | A factory deploys an ephemeral whose initcode dirties balance, nonce, code, and storage; runtime `SELFDESTRUCT` routes through an intermediate oracle. | **success**: ephemeral's BAL entry contains only `storage_reads` for the demoted slots. **revert**: BAL records all four dirty fields (destruction rolled back). | ✅ Completed |
| `test_factory_installed_at_transition` | Ensure BAL records the [EIP-7997](https://eips.ethereum.org/EIPS/eip-7997) factory install on the Amsterdam activation block as an irregular state transition at `block_access_index=0`. File: `tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py`. Uses `@pytest.mark.valid_at_transition_to("Amsterdam")`. | Factory removed from genesis via a zeroed pre-alloc override. Blocks at `timestamp=14_999` (pre-fork), `15_000` (empty activation block), `15_001` (CREATE2 deployment through the factory). | Activation block BAL **MUST** include the factory with `nonce_changes=[(0, 1)]` and `code_changes=[(0, runtime code)]`. The next block **MUST** record only the tx-driven `nonce_changes=[(1, 2)]` with explicitly empty `code_changes` (the install must not repeat). Post-state: factory at nonce 2 with canonical code, CREATE2 child deployed. | ✅ Completed |
| `test_factory_access_recorded_at_transition` | Ensure the activation block records an access-only BAL entry when the factory is already deployed (the mainnet case). File: `tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py`. | Factory pre-existing with canonical code, parametrized `nonce ∈ [1, 5]`. Unrelated Alice→Bob transfers in the activation block and the block after. | Activation block BAL **MUST** include the factory as an accessed account with no changes (`BalAccountExpectation.empty()`). The following block **MUST NOT** include the factory at all. Post-state: factory unchanged. | ✅ Completed |
| `test_factory_code_reset_at_transition` | Ensure a factory account holding non-canonical code is reset on the activation block, with nonce and balance preservation rules in the BAL. File: `tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py`. | Factory pre-existing with `STOP` code and balance 1000, parametrized: `nonce=0` (set to 1) and `nonce=7` (preserved). Empty activation block. | Activation block BAL **MUST** include `code_changes=[(0, runtime code)]`. `nonce_changes` **MUST** be `[(0, 1)]` for the zero-nonce case and explicitly empty for the nonzero case. Post-state: canonical code, balance preserved. | ✅ Completed |
| `test_factory_install_merged_with_same_block_use` | Ensure the factory install and a same-block CREATE2 use merge into a single BAL entry across indices. File: `tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py`. | Factory removed from genesis. The activation block itself contains a CREATE2 deployment through the freshly installed factory. | BAL **MUST** contain one factory entry with `code_changes=[(0, runtime code)]` and `nonce_changes=[(0, 1), (1, 2)]` — the install at the pre-execution index and the tx-driven increment merged in index order. | ✅ Completed |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one more case:
This is for chains which could have deployed the factory the "normal" way but did not, and now it is inserted as irregular state transition. We then replay the create tx https://etherscan.io/tx/0xeddf9e61fb9d8f5111840daef55e5fde0041f5702856532cdbb5a02998033d26

This should be rejected due to https://eips.ethereum.org/EIPS/eip-684 (the tx is valid, the contract deployment is not)


Loading
Loading