Skip to content

feat(specs, tests): deploy the EIP-8141 expiry verifier as an ordinary contract - #3729

Merged
spencer-tb merged 3 commits into
ethereum:eips/bogota/eip-8141from
gurukamath:eip-8141/spec-updates
Oct 7, 2026
Merged

spencer-tb merged 3 commits into
ethereum:eips/bogota/eip-8141from
gurukamath:eip-8141/spec-updates

Conversation

@gurukamath

@gurukamath gurukamath commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

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_VERIFIER moves to 0x81413f0cF12e9b6a49B1D0439E081c577D57FfFf, the address created by the pre-signed legacy transaction published in the EIP, and the protocol no longer installs its runtime code at activation.

  • Specification: EXPIRY_VERIFIER takes the new address and apply_fork is 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.
  • Testing framework: the 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 from feat(test-specs,tests): exercise the EIP-8141 expiry verifier install at the fork transition #3539 had no other user and are removed.
  • Tests: the install-at-transition test is replaced by test_contract_deployment.py, built on generate_system_contract_deploy_test with the EIP's transaction (expiry_verifier_deploy_tx.json). It deploys before, at, and after the fork block on AmsterdamToBogotaAtTime15k and checks the deployed code against the framework's pre-allocation, so the two cannot drift apart. fail_on_empty_code=False because 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_versions passes.

Verification: just static clean; fill tests/bogota/eip8141_frame_transactions --until Bogota yields 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

  • Ran fast static checks to avoid CI fails, see Code Standards & Verifying Changes: just static
  • PR title has the form <type>(<area>): <title>, where <type> and <area> come from an appropriate C-<type>, respectively A-<area>, label. The title should match the target squash commit message.

Cute Animal Picture

Cute Animals - 1 of 1

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

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (eips/bogota/eip-8141@d6c9865). Learn more about missing BASE report.

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           
Flag Coverage Δ
unittests 94.91% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@spencer-tb spencer-tb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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()
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
),
).with_installed_code(
fork.fork_at(
block_number=0, timestamp=0
).activation_code_installs()
),

"""
pass

# Engine API information abstract methods

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
# 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
@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():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
Optional,
Mapping,
Optional,

installed.migrate_state_commitment(self.state_commitment())
return installed

def __getitem__(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
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__(

Comment on lines +66 to +69
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Small rewording.

Suggested change
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.

Comment on lines +5 to +6
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When we add EIP-8250 the fork activation does write state (nonce manager install).

Suggested change
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.
@gurukamath

Copy link
Copy Markdown
Contributor Author

Pushed an update to make the changes

@LouisTsai-Csie LouisTsai-Csie left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines -105 to -108
Spec.EXPIRY_VERIFIER: Account(
nonce=0,
code=Spec.EXPIRY_VERIFIER_CODE,
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe we could keep this verification, verify the nonce only is okay.

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.

3 participants