Skip to content

feat(spec-specs): add EIP-7997 deterministic factory deployment#3200

Draft
nerolation wants to merge 4 commits into
ethereum:forks/amsterdamfrom
nerolation:nerolation/eip-7997
Draft

feat(spec-specs): add EIP-7997 deterministic factory deployment#3200
nerolation wants to merge 4 commits into
ethereum:forks/amsterdamfrom
nerolation:nerolation/eip-7997

Conversation

@nerolation

Copy link
Copy Markdown
Contributor

Reopening #3198 through this pr.

This time the state access is gated by checking the PreviousHeader, and will only happen in the fork transition block.
It feels like the right solution now.

cc @marioevz @MariusVanDerWijden

@MariusVanDerWijden

MariusVanDerWijden commented Jul 21, 2026

Copy link
Copy Markdown
Member

I think thats actually what we do in geth already

@marioevz

Copy link
Copy Markdown
Member

and will only happen in the fork transition block

Does this imply that the fork transition block should contain the account code access in the block level access list and also the execution witness?

@nerolation

Copy link
Copy Markdown
Contributor Author

I've now clarified it further in the EIP. It's now saying that even with the contract being accessed at the fork transition, it won't be in the BAL. Only on chains where the contract doesn't exist we put it in the BAL.

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.
@nerolation
nerolation force-pushed the nerolation/eip-7997 branch from 1b254f7 to 8d8858b Compare July 22, 2026 13:22
@nerolation
nerolation changed the base branch from devnets/glamsterdam/7 to forks/amsterdam July 22, 2026 13:40
@kdeme

kdeme commented Jul 22, 2026

Copy link
Copy Markdown

Nimbus EL also does it like that (but the witness tracking for stateless testing triggered this issue).

Only on chains where the contract doesn't exist we put it in the BAL.

We should clarify in the EIP the behaviour for the witness also (cc @jsign )

(I realize that within execution-specs this is tracked with the same mechanism, but not all clients work like that).

@jsign

jsign commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

In EEST, the execution witness uses the same state tracker as BALs.

From the execution witness POV, at the fork transition block there will be a state lookup to check if the contract exists or not. This implies this state access should materialize into an MPT proof in the execution witness as expected (just as any other state access that happens in the block).

If we exclude this state lookup from the execution witness, then a stateless program can't know whether the contract exists, and thus can't decide what to do since that state access isn't part of the data received (i.e. remember is a stateless program that receives state information as input).

@nerolation, is there any benefit to having this exclusion rule for BALs? I feel like having an exception for a single lookup only in a fork transition might not be worth it and might even make things more risky (since sounds not natural to ignore a state access). It would force EEST to have this rule at the BAL level, not at the "state tracker" level shared with the execution witness. It is kind of saying that only after some starting point of the block execution state is tracked, instead of just doing it always.

Said differently, any state access should go through the state tracker feels like a simpler rule to me (i.e. including any extra state access in fork transitions).

@nerolation

nerolation commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Yeah I can def see this reasoning. Seems like we have to bring this to ACDT on Monday. Right now the EIP clarifies that the lookup wouldn't be covered by the BAL. but I'm happy to update this:
ethereum/EIPs#11991

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.
@nerolation

Copy link
Copy Markdown
Contributor Author

The latest commit removed the early return guard and let's the deployment/check be included independent from the factory existing or not.
That's the respective EIP change:
ethereum/EIPs@30e0e16

…nsitions

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.
@jochem-brouwer

Copy link
Copy Markdown
Member

Ref, PR (seems to) implement ethereum/EIPs#11991

@jochem-brouwer jochem-brouwer left a comment

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.

Some minor comments!

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?

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

1,
[BalNonceChange(block_access_index=0, post_nonce=1)],
id="zero_nonce_set_to_one",
),

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.

This seems to include nonce 0 and nonce 7 as starting nonce, not the implied range [0,7]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants