From 8d8858b57e3d1d6a1d5608d082fd60e9f3002637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= Date: Mon, 20 Jul 2026 15:23:13 +0200 Subject: [PATCH 1/4] feat(spec-specs): add EIP-7997 deterministic factory deployment Install the factory runtime code on the fork activation block, detected by the parent header being a pre-amsterdam header, when the code is not already present. The nonce and code changes are recorded in the block access list at the pre-execution index. On chains where the factory already exists this is a no-op, and no other block ever touches the account. --- src/ethereum/forks/amsterdam/fork.py | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 302e0887ed..0e02777ca0 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -80,6 +80,7 @@ incorporate_tx_into_block, increment_nonce, set_account_balance, + set_code, ) from .transactions import ( TX_MAX_GAS_LIMIT, @@ -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 @@ -842,6 +854,31 @@ 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, recording the changes in the block access list at the + pre-execution index. + + [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: + return + + 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, ...], @@ -874,6 +911,9 @@ def apply_body( """ block_output = vm.BlockOutput() + if isinstance(block_env.parent_header, PreviousHeader): + deploy_deterministic_factory(block_env) + process_unchecked_system_transaction( block_env=block_env, target_address=BEACON_ROOTS_ADDRESS, From b464cdef1eca1e3fd79bd1fe596861732f8bbb01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= Date: Thu, 23 Jul 2026 07:03:42 +0200 Subject: [PATCH 2/4] feat(spec-specs): record the factory access in the block access list The factory account appears in the activation block's access list even when the code check finds it already deployed, as an accessed account with no changes. --- src/ethereum/forks/amsterdam/fork.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 0e02777ca0..50a449efdb 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -858,23 +858,22 @@ 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, recording the changes in the block access list at the - pre-execution index. + 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: - return - - set_code( - tx_state, - DETERMINISTIC_FACTORY_ADDRESS, - DETERMINISTIC_FACTORY_CODE, - ) - if account.nonce == Uint(0): - increment_nonce(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) From e256b22d13c6912ed1666c6360f09443f8692b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= Date: Thu, 23 Jul 2026 07:35:11 +0200 Subject: [PATCH 3/4] feat(spec-specs, tests): install the EIP-7997 factory across fork transitions Move the activation gate from apply_body to execute_block, where the parent header is available. The filler marks a block whose parent belongs to an earlier fork by passing parentFork in the t8n environment, and the t8n applies the deployment for forks that define it when the parent fork does not. Add transition tests covering the install, the access-only recording, non-canonical code reset, and same-block use. --- .../src/execution_testing/specs/blockchain.py | 13 + .../test_types/block_types.py | 7 + src/ethereum/forks/amsterdam/fork.py | 6 +- .../evm_tools/loaders/fork_loader.py | 10 + .../evm_tools/t8n/__init__.py | 27 ++ src/ethereum_spec_tools/evm_tools/t8n/env.py | 2 + .../test_fork_transition.py | 263 ++++++++++++++++++ 7 files changed, 325 insertions(+), 3 deletions(-) diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 6eea651a26..46f6894a0b 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -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( diff --git a/packages/testing/src/execution_testing/test_types/block_types.py b/packages/testing/src/execution_testing/test_types/block_types.py index 56d367e2e6..2d1026dca0 100644 --- a/packages/testing/src/execution_testing/test_types/block_types.py +++ b/packages/testing/src/execution_testing/test_types/block_types.py @@ -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: diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 50a449efdb..717e0fa380 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -349,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, @@ -910,9 +913,6 @@ def apply_body( """ block_output = vm.BlockOutput() - if isinstance(block_env.parent_header, PreviousHeader): - deploy_deterministic_factory(block_env) - process_unchecked_system_transaction( block_env=block_env, target_address=BEACON_ROOTS_ADDRESS, diff --git a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py index f9ec92d6de..1e088929de 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -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.""" diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index cd449ad9f0..e0cc43e9e5 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -6,6 +6,7 @@ import fnmatch import json import os +import re from contextlib import AbstractContextManager from typing import Any, Final, TextIO, Type, TypeVar @@ -24,6 +25,7 @@ ) from ..loaders.fixture_loader import Load +from ..loaders.fork_loader import ForkLoad from ..utils import ( FatalError, find_fork, @@ -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 @@ -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"(? 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, diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index edf3763d57..740b677ae2 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -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] @@ -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) diff --git a/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py b/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py index b124a0c588..8fabab7836 100644 --- a/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py +++ b/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py @@ -17,6 +17,7 @@ compute_create2_address, ) from execution_testing.test_types.block_access_list.account_changes import ( + BalCodeChange, BalNonceChange, ) from execution_testing.test_types.block_access_list.expectations import ( @@ -104,3 +105,265 @@ def test_factory_deploys_across_transition( ), }, ) + + +@pytest.mark.valid_at_transition_to("Amsterdam") +@pytest.mark.pre_alloc_mutable +def test_factory_installed_at_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +) -> None: + """ + A chain without the factory installs it on the Amsterdam activation + block, recording the nonce and code changes in that block's access + list at the pre-execution index. Later blocks do not touch the + account, and the installed factory is immediately usable. + """ + factory = Address(Spec.FACTORY_ADDRESS) + # Zero out the injected predeploy; the genesis allocation merge + # drops the resulting empty account entirely. + pre[factory] = Account(nonce=0, balance=0, code=b"") + sender = pre.fund_eoa() + + runtime_code = Op.RETURN(0, 1) + initcode = Initcode(deploy_code=runtime_code) + salt = Hash(0) + + blockchain_test( + pre=pre, + blocks=[ + Block(timestamp=FORK_TIMESTAMP - 1), + Block( + timestamp=FORK_TIMESTAMP, + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + factory: BalAccountExpectation( + nonce_changes=[ + BalNonceChange( + block_access_index=0, post_nonce=1 + ) + ], + code_changes=[ + BalCodeChange( + block_access_index=0, + new_code=Spec.FACTORY_BYTECODE, + ) + ], + ), + } + ), + ), + Block( + timestamp=FORK_TIMESTAMP + 1, + txs=[ + Transaction( + sender=sender, + to=factory, + data=salt + bytes(initcode), + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + factory: BalAccountExpectation( + nonce_changes=[ + BalNonceChange( + block_access_index=1, post_nonce=2 + ) + ], + code_changes=[], + ), + } + ), + ), + ], + post={ + factory: Account(nonce=2, code=Spec.FACTORY_BYTECODE), + compute_create2_address(factory, salt, initcode): Account( + nonce=1, code=bytes(runtime_code) + ), + }, + ) + + +@pytest.mark.valid_at_transition_to("Amsterdam") +@pytest.mark.pre_alloc_mutable +@pytest.mark.parametrize("factory_nonce", [1, 5]) +def test_factory_access_recorded_at_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + factory_nonce: int, +) -> None: + """ + An already-deployed factory is left untouched by the activation + block, which records only the access: the account appears in the + activation block's access list with no changes, and not at all in + later blocks' access lists. + """ + factory = pre.deploy_contract( + code=Spec.FACTORY_BYTECODE, + address=Address(Spec.FACTORY_ADDRESS), + nonce=factory_nonce, + ) + sender = pre.fund_eoa() + receiver = pre.fund_eoa(amount=0) + + blockchain_test( + pre=pre, + blocks=[ + Block(timestamp=FORK_TIMESTAMP - 1), + Block( + timestamp=FORK_TIMESTAMP, + txs=[Transaction(sender=sender, to=receiver, value=1)], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + factory: BalAccountExpectation.empty(), + } + ), + ), + Block( + timestamp=FORK_TIMESTAMP + 1, + txs=[Transaction(sender=sender, to=receiver, value=1)], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + factory: None, + } + ), + ), + ], + post={ + factory: Account( + nonce=factory_nonce, code=Spec.FACTORY_BYTECODE + ), + }, + ) + + +@pytest.mark.valid_at_transition_to("Amsterdam") +@pytest.mark.pre_alloc_mutable +@pytest.mark.parametrize( + "factory_nonce,post_nonce,expected_nonce_changes", + [ + pytest.param( + 0, + 1, + [BalNonceChange(block_access_index=0, post_nonce=1)], + id="zero_nonce_set_to_one", + ), + pytest.param(7, 7, [], id="nonzero_nonce_preserved"), + ], +) +def test_factory_code_reset_at_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + factory_nonce: int, + post_nonce: int, + expected_nonce_changes: list, +) -> None: + """ + A factory account holding non-canonical code is reset to the + canonical runtime code on the activation block. A zero nonce is set + to one, a nonzero nonce and the balance are preserved, and only the + resulting changes appear in the access list. + """ + factory = pre.deploy_contract( + code=Op.STOP, + address=Address(Spec.FACTORY_ADDRESS), + nonce=factory_nonce, + balance=1000, + ) + + blockchain_test( + pre=pre, + blocks=[ + Block(timestamp=FORK_TIMESTAMP - 1), + Block( + timestamp=FORK_TIMESTAMP, + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + factory: BalAccountExpectation( + nonce_changes=expected_nonce_changes, + code_changes=[ + BalCodeChange( + block_access_index=0, + new_code=Spec.FACTORY_BYTECODE, + ) + ], + ), + } + ), + ), + ], + post={ + factory: Account( + nonce=post_nonce, + code=Spec.FACTORY_BYTECODE, + balance=1000, + ), + }, + ) + + +@pytest.mark.valid_at_transition_to("Amsterdam") +@pytest.mark.pre_alloc_mutable +def test_factory_install_merged_with_same_block_use( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +) -> None: + """ + The factory is installed and used within the activation block. The + install and the transaction-driven use merge into a single access + list entry: the code change at the pre-execution index and nonce + changes at both indices. + """ + factory = Address(Spec.FACTORY_ADDRESS) + # Zero out the injected predeploy; the genesis allocation merge + # drops the resulting empty account entirely. + pre[factory] = Account(nonce=0, balance=0, code=b"") + sender = pre.fund_eoa() + + runtime_code = Op.RETURN(0, 1) + initcode = Initcode(deploy_code=runtime_code) + salt = Hash(0) + + blockchain_test( + pre=pre, + blocks=[ + Block(timestamp=FORK_TIMESTAMP - 1), + Block( + timestamp=FORK_TIMESTAMP, + txs=[ + Transaction( + sender=sender, + to=factory, + data=salt + bytes(initcode), + ) + ], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + factory: BalAccountExpectation( + nonce_changes=[ + BalNonceChange( + block_access_index=0, post_nonce=1 + ), + BalNonceChange( + block_access_index=1, post_nonce=2 + ), + ], + code_changes=[ + BalCodeChange( + block_access_index=0, + new_code=Spec.FACTORY_BYTECODE, + ) + ], + ), + } + ), + ), + ], + post={ + factory: Account(nonce=2, code=Spec.FACTORY_BYTECODE), + compute_create2_address(factory, salt, initcode): Account( + nonce=1, code=bytes(runtime_code) + ), + }, + ) From f6e4386997cf9f9b17200f69fe013ae0b32f7da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= Date: Thu, 23 Jul 2026 07:41:03 +0200 Subject: [PATCH 4/4] docs(tests): cover the EIP-7997 transition cases in the BAL test matrix --- .../amsterdam/eip7928_block_level_access_lists/test_cases.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md index 6bcc98b121..64c2455f30 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -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 |