Repository navigation
feat(specs, tests): deploy the EIP-8141 expiry verifier as an ordinary contract - #3729
Conversation
Two EIP-8141 text changes merged upstream on 2026-09-29 after the version the tests were pinned to: - ethereum/EIPs#12213 bounds the unpaid validation work a block producer spends on a pending frame transaction whose validation prefix does not approve. It is local producer policy, not consensus, so nothing in the specification changes. - ethereum/EIPs#12395 replaces the EIP-7623 references with EIP-7976, which already defined the uniform calldata token count the frame transaction floor uses. Cite EIP-7976 in the frame transaction intrinsic cost and gas settlement docstrings and in the gas settlement test module. The calldata floor citations shared with the Amsterdam transaction code are left unchanged. Move the reference spec pin to the blob of the EIP file at the second merge. The version checker compares the pin against the file's blob hash, so the previous commit hash could never match.
ethereum/EIPs#12387 makes the EIP-8141 expiry verifier an ordinary contract. It moves `EXPIRY_VERIFIER` to 0x81413f0cF12e9b6a49B1D0439E081c577D57FfFf, the address created by a pre-signed legacy transaction from a synthetic sender, and drops the protocol install of its runtime code at activation. Specification: `EXPIRY_VERIFIER` takes the new address and `apply_fork` no longer installs anything. The runtime code constant is removed; the EIP publishes the canonical code, and the specification carries no predeploy bytecode for other system contracts either. Testing framework: the expiry verifier joins the standard predeploy pattern, pre-allocated with nonce 1 for both state and blockchain tests. The `activation_code_installs` hook and `Alloc.with_installed_code` added by ethereum#3539 had no other user and are removed. Tests: the install test at the fork transition is replaced by a deployment test built on `generate_system_contract_deploy_test`, which runs the transaction published in the EIP before, at, and after the fork block and checks the deployed code against the pre-allocation. A missing verifier invalidates no block, as nothing in the protocol calls it. The frame-in-first-post-fork-block test is kept. Post-state pins of the verifier account are dropped along with the test-side bytecode copy: the expired-frame case already fails on a missing predeploy, and the deployment test anchors the bytecode to the EIP. The reference spec pin moves to the blob of the EIP file at the merge.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## eips/bogota/eip-8141 #3729 +/- ##
=======================================================
Coverage ? 94.91%
=======================================================
Files ? 676
Lines ? 41821
Branches ? 3942
=======================================================
Hits ? 39695
Misses ? 1471
Partials ? 655
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
spencer-tb
left a comment
There was a problem hiding this comment.
LGTM overall! EIP-8250 still uses the activation_code_installs hook on eips/bogota/eip-8250 for the nonce manager install, and #3722 and #3725 extend it.
The main suggestions keep the hook and only drop the EIP-8141 override, otherwise merging with 8250 breaks its nonce manager install. The last 2 are just docstring changes :)
| fork.fork_at( | ||
| block_number=0, timestamp=0 | ||
| ).activation_code_installs() | ||
| ), |
There was a problem hiding this comment.
| ), | |
| ).with_installed_code( | |
| fork.fork_at( | |
| block_number=0, timestamp=0 | |
| ).activation_code_installs() | |
| ), |
| """ | ||
| pass | ||
|
|
||
| # Engine API information abstract methods |
There was a problem hiding this comment.
| # Engine API information abstract methods | |
| @classmethod | |
| @abstractmethod | |
| def activation_code_installs(cls) -> Mapping: | |
| """ | |
| Return the runtime code this fork installs when it activates, keyed | |
| by address. | |
| Unlike `pre_allocation_blockchain`, which describes accounts that | |
| already exist at genesis, these installs happen at the first block | |
| of the fork: only the code is written, and the nonce, balance and | |
| storage the account had before the fork are kept. In a blockchain | |
| test that starts at a fork already including them, the installs are | |
| applied to the genesis allocation instead. | |
| """ | |
| pass | |
| # Engine API information abstract methods |
| """ | ||
| return {} | ||
|
|
||
| @classmethod |
There was a problem hiding this comment.
| @classmethod | |
| @classmethod | |
| def activation_code_installs(cls) -> Mapping: | |
| """ | |
| Return the runtime code installed when the fork activates. | |
| Frontier installs no code at activation. | |
| """ | |
| return {} | |
| @classmethod |
| block_number=0, timestamp=0 | ||
| ).activation_code_installs() | ||
| ) | ||
| if empty_accounts := pre_alloc.empty_accounts(): |
There was a problem hiding this comment.
| if empty_accounts := pre_alloc.empty_accounts(): | |
| # Code a fork installs at activation is part of genesis only when | |
| # that fork is already active there. A transition test gets it at | |
| # the fork block instead, in `generate_block_data`. | |
| pre_alloc = pre_alloc.with_installed_code( | |
| self.fork.fork_at( | |
| block_number=0, timestamp=0 | |
| ).activation_code_installs() | |
| ) | |
| if empty_accounts := pre_alloc.empty_accounts(): |
| new_installs | ||
| ) | ||
|
|
||
| transition_tool_output = t8n.evaluate( |
There was a problem hiding this comment.
| transition_tool_output = t8n.evaluate( | |
| # A fork activating at this block installs its code before the block | |
| # executes. The parent's fork tells whether this is that block, and | |
| # which installs are new rather than inherited from an earlier fork. | |
| assert env.parent_timestamp is not None, ( | |
| "parent_timestamp is required to resolve the parent's fork" | |
| ) | |
| parent_fork = self.fork.fork_at( | |
| block_number=env.number - 1, timestamp=env.parent_timestamp | |
| ) | |
| if fork != parent_fork: | |
| parent_installs = parent_fork.activation_code_installs() | |
| inherited = { | |
| Address(address): code | |
| for address, code in parent_installs.items() | |
| } | |
| new_installs = { | |
| address: code | |
| for address, code in fork.activation_code_installs().items() | |
| if inherited.get(Address(address)) != code | |
| } | |
| if new_installs: | |
| if isinstance(previous_alloc, LazyAlloc): | |
| previous_alloc = previous_alloc.materialize() | |
| previous_alloc = previous_alloc.with_installed_code( | |
| new_installs | |
| ) | |
| transition_tool_output = t8n.evaluate( |
| List, | ||
| Literal, | ||
| Mapping, | ||
| Optional, |
There was a problem hiding this comment.
| Optional, | |
| Mapping, | |
| Optional, |
| installed.migrate_state_commitment(self.state_commitment()) | ||
| return installed | ||
|
|
||
| def __getitem__( |
There was a problem hiding this comment.
| def __getitem__( | |
| def with_installed_code(self, installs: Mapping) -> "Alloc": | |
| """ | |
| Return a copy of this allocation with the runtime code in `installs` | |
| written at each address. | |
| Only the code changes: an account that already exists keeps its | |
| nonce, balance and storage, and one that does not is created with | |
| all three zero. This is how a fork installs code when it activates | |
| (EIP-8141's expiry verifier), as opposed to a predeploy that is part | |
| of the genesis allocation. | |
| """ | |
| if not installs: | |
| return self | |
| root: Dict[Address, Account | None] = dict(self.root) | |
| for address, code in installs.items(): | |
| address = Address(address) | |
| root[address] = Account.merge( | |
| root.get(address), Account(code=code) | |
| ) | |
| installed = Alloc(root) | |
| installed.migrate_state_commitment(self.state_commitment()) | |
| return installed | |
| def __getitem__( |
| transaction it is. The protocol neither installs nor special-cases it; the | ||
| EIP gives its canonical runtime code, which reverts unless called with | ||
| exactly [`EXPIRY_DATA_LENGTH`][edl] bytes of calldata holding an unsigned | ||
| big-endian expiry timestamp at or after the current block timestamp. |
There was a problem hiding this comment.
Small rewording.
| transaction it is. The protocol neither installs nor special-cases it; the | |
| EIP gives its canonical runtime code, which reverts unless called with | |
| exactly [`EXPIRY_DATA_LENGTH`][edl] bytes of calldata holding an unsigned | |
| big-endian expiry timestamp at or after the current block timestamp. | |
| transaction it is. The protocol does not install it. The EIP gives its | |
| canonical runtime code, which reverts unless called with exactly | |
| [`EXPIRY_DATA_LENGTH`][edl] bytes of calldata. |
| allocation in every EIP-8141 test, so the fork activation itself writes | ||
| no state. These tests cover what the first post-fork block accepts: a |
There was a problem hiding this comment.
When we add EIP-8250 the fork activation does write state (nonce manager install).
| allocation in every EIP-8141 test, so the fork activation itself writes | |
| no state. These tests cover what the first post-fork block accepts: a | |
| allocation in every EIP-8141 test, so EIP-8141's activation writes no | |
| state. These tests cover what the first post-fork block accepts: a |
Review follow-up for ethereum#3729. EIP-8250 installs its nonce manager through `activation_code_installs` on `eips/bogota/eip-8250`, and ethereum#3722 and ethereum#3725 build on it, so removing the hook here would break that install when the branches merge. Restore the hook, Frontier's empty implementation, both blockchain filler call sites, the Hive genesis builder, the pre-allocation group builder, and `Alloc.with_installed_code`, all as they are on the base branch. Only the EIP-8141 override stays removed: the expiry verifier is a predeploy, not an activation install. The `with_installed_code` docstring no longer names an EIP as its example. Reword the `EXPIRY_VERIFIER` docstring and the fork transition test module docstring as suggested in review.
|
Pushed an update to make the changes |
There was a problem hiding this comment.
Looks good from my side!
It seems like the CI fails for all new PRs, it is fill-pypy that fails.
edit: I think it is fixed by PR #3723 , doing a rebase should resolve this issue.
| Spec.EXPIRY_VERIFIER: Account( | ||
| nonce=0, | ||
| code=Spec.EXPIRY_VERIFIER_CODE, | ||
| ), |
There was a problem hiding this comment.
Maybe we could keep this verification, verify the nonce only is okay.
dd0cb2e
into
ethereum:eips/bogota/eip-8141
Description
Tracks the three EIP-8141 text changes merged upstream after the version the tests were pinned to (
b75cbe6, 2026-09-01).ethereum/EIPs#12213 — bound producer-side re-execution of an unapproving validation prefix. Local block-producer policy, explicitly non-consensus. No specification change.
ethereum/EIPs#12395 — replace EIP-7623 with EIP-7976. The frame transaction floor already used the EIP-7976 uniform token count; this only swaps the citations. The frame intrinsic cost and gas settlement docstrings and the gas settlement test module now cite EIP-7976. Citations shared verbatim with the Amsterdam transaction code are left unchanged.
ethereum/EIPs#12387 — deploy the expiry verifier as an ordinary contract.
EXPIRY_VERIFIERmoves to0x81413f0cF12e9b6a49B1D0439E081c577D57FfFf, the address created by the pre-signed legacy transaction published in the EIP, and the protocol no longer installs its runtime code at activation.EXPIRY_VERIFIERtakes the new address andapply_forkis a pass-through again, identical to Amsterdam's. The runtime code constant is removed: the EIP publishes the canonical code, and the specification carries no predeploy bytecode for other system contracts either.activation_code_installshook andAlloc.with_installed_codefrom feat(test-specs,tests): exercise the EIP-8141 expiry verifier install at the fork transition #3539 had no other user and are removed.test_contract_deployment.py, built ongenerate_system_contract_deploy_testwith the EIP's transaction (expiry_verifier_deploy_tx.json). It deploys before, at, and after the fork block onAmsterdamToBogotaAtTime15kand checks the deployed code against the framework's pre-allocation, so the two cannot drift apart.fail_on_empty_code=Falsebecause nothing in the protocol calls the verifier; a block without it is valid. The frame-in-first-post-fork-block transition test is kept. Post-state pins of the verifier account and the test-side bytecode copy are dropped: the expired-frame case already fails on a missing predeploy.The reference spec pin moves to the blob of the EIP file after #12387 (
d0d1d86b…);check_eip_versionspasses.Verification:
just staticclean;fill tests/bogota/eip8141_frame_transactions --until Bogotayields 735 fixtures; framework unit tests for the forks, specs, fixtures, and test-type modules pass. The EIP's deployment transaction was checked independently: recovered sender, derived address, transaction hash, and the runtime code its initcode returns all match the EIP.Related Issues or PRs
Checklist
just static<type>(<area>): <title>, where<type>and<area>come from an appropriateC-<type>, respectivelyA-<area>, label. The title should match the target squash commit message.Cute Animal Picture