Skip to content

feat(eip8272): deploy the recent root contract as an ordinary contract - #14437

Merged
AnkushinDaniil merged 51 commits into
masterfrom
daniil/eip8272-recent-root-deploy
Oct 9, 2026
Merged

AnkushinDaniil merged 51 commits into
masterfrom
daniil/eip8272-recent-root-deploy

Conversation

@AnkushinDaniil

@AnkushinDaniil AnkushinDaniil commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

based on master (#14071 and ethereum/EIPs#12443 are in).

follows ethereum/EIPs#12443: the recent root contract is no longer installed at eip-8272 activation. it is an ordinary contract created by the pre-signed nick's-method tx in the EIP, at 0x8272D9679689Ea2f307140CdF9002D27dC00Ffff. runtime code is RECENT_ROOT_CODE from ethereum/sys-asm#53. EELS: ethereum/execution-specs#3740, tracker ethereum/execution-specs#3727.

Changes

  • Eip8272Constants.RecentRootAddress moves from 0x...8272 to 0x8272D9679689Ea2f307140CdF9002D27dC00Ffff. runtime code and code hash unchanged
  • PredeployInstaller drops the 8272 entry, so activation writes nothing at the address and adds no BAL entry. this also reverts the activation-only plumbing fix(eip8272): verify recent roots with a canonical VERIFY frame #14071 added for it: the InitializesOnce flag, the parent-spec lookup on IBlockAccessListManager and the three stateless envs, and BlockErrorMessages.RecentRootPredeployNotEmpty. per the EIP, activation makes no state change, so a block can no longer be invalid because of what sits at the address
  • the frame validation tracer and the txpool recheck still require the canonical code hash at the address, so before the deploy tx lands a recent root verify frame is not admitted, matching "default code reverts, reject"
  • no native write path exists, storage is only written by the contract code
  • tests: new Execute_Eip8272DeploymentTransaction_CreatesRecentRootCodeAtItsAddress runs the exact deployment tx from the EIP (hash, recovered sender 0x14bf16d4c9842bf1EbF396e553477C66EB0a8A82, resulting code hash and nonce 1). activation tests now check the address is left untouched and out of the BAL, for an empty and a non-empty prestate

Types of changes

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)
  • Breaking change (a change that causes existing functionality not to work as expected)
  • Optimization
  • Refactoring
  • Documentation update
  • Build-related changes
  • Other: Description

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

Notes on testing

unit tests (Release, filtered to the touched areas), all green:

  • Nethermind.Consensus.Test PredeployInstaller|FrameTx|Prewarm: 147/147
  • Nethermind.Blockchain.Test BlockProcessorTests|FrameTx: 272/272
  • Nethermind.Evm.Test FrameTx|RecentRoot|Eip8272: 577/577
  • Nethermind.TxPool.Test RecentRoot|FrameTx: 298/298

EELS fixtures: spencer-tb/execution-specs#71 still installs the contract at activation at 0x...8272. i refilled it locally with the address moved and the activation install swapped for a pre-alloc, the same way ethereum/execution-specs#3729 did for the expiry verifier.

Documentation

Requires documentation update

  • Yes
  • No

Requires explanation in Release Notes

  • Yes
  • No

devnets that enable eip-8272 need the contract in genesis alloc (nonce 1, code above) or the deploy tx, since the fork no longer installs it.

EIP-8250 assigns 0x0D legacy nonce, 0x0E key count, 0x0F nonce_keys_hash,
0x10 first key. EIP-8272 no longer defines a TXPARAM index, so 0x0F belongs
to EIP-8250 on every fork and indices above 0x10 halt.
Follow EIPs 20fb52b9 and 824cbc0b: drop the recent_root_references
envelope field, RECENTROOTREFLOAD, the per-reference intrinsic gas and
the native pre-execution check. A recent root is now verified by a
VERIFY frame targeting RECENT_ROOT_ADDRESS whose data is n 72-byte
tuples, validated by the predeploy with ordinary EVM execution.

Install RECENT_ROOT_CODE from ethereum/sys-asm#53 at activation.
Refuse a tuple-carrying blob frame tx in a persistent blob pool, whose frameless records the sweep cannot read, and skip the sweep while no pooled tx carries tuples.
Record each pending recent_root_verify transaction's (storage_key, entry_hash)
dependencies and its first expiring current_slot. A head extending the previous
one only ages tuples out, with no storage reads; any other head rechecks every
indexed transaction, reading each distinct cell once. A head before activation
evicts every such transaction.
…nical-frame

# Conflicts:
#	src/Nethermind/Nethermind.Consensus.Test/PredeployInstallerTests.cs
#	src/Nethermind/Nethermind.Evm.Test/FrameTxProcessorTests.cs
#	src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessorBase.FrameTx.cs
…pool

# Conflicts:
#	src/Nethermind/Nethermind.Core/FrameTxValidation.cs
#	src/Nethermind/Nethermind.Evm/Tracing/FrameTxValidationTracer.cs
…DRESS runs RECENT_ROOT_CODE

The tracer only withheld the SLOTNUM and recent-root SLOAD permissions over other code,
so code needing neither (a bare STOP) was admitted. EIP-8272 requires rejection when the
code differs or the fork is inactive.
…prefix simulator

Committed tuple then self-verify, with optional expiry and deploy prologue; tuple age at
the head slot, 8190 and 8191 measured at head.slotNumber + 1; rejection for wrong code at
RECENT_ROOT_ADDRESS and for a sender reading recent-root storage through a call.
…canonical head

Head events are consumed after the chain has moved, while admission validates against the canonical head. Taking the header and its state together from IChainHeadInfoProvider.TryGetHeadState keeps a transaction admitted against a newer head from being judged at an older event's slot.
The mempool sweep checks age and commitment in RecentRootDependencyIndex, and block execution checks them in the predeploy, so only RecentRootStoreTests still called IsReferenceValid. The window, current-slot and mismatched-root cases stay pinned against the predeploy in Execute_RecentRootVerifyFrame_IsCheckedAgainstTheCommittedEntry.
@AnkushinDaniil

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @AnkushinDaniil's task in 18s —— View job


PR #14437 re-review (after 7d085ac)

  • Gather context: diff against origin/daniil/eip8272-canonical-frame, plus the new commit
  • Check the follow-up commit against the earlier Low findings
  • Re-check the production changes
  • Post summary

No Critical, High or Medium findings. 7d085ac fixes the earlier Low naming findings:

  • InstallRecentRootPredeploy is now DeployRecentRootContract.
  • Execute_RecentRootVerifyFrame_RecordsThePredeploySlotInBal is now …RecordsTheContractSlotInBal, and its local variable and assertion message were renamed to match.
  • The TxPool test case is now named …when_the_contract_code_differs.
  • In RecentRootStore.cs, the remarks on line 21 and the summary on ReferenceCell now say "contract".

A grep turns up no remaining "predeploy" wording for the recent root contract. Nothing still references InitializesOnce, GetParentSpec or RecentRootPredeployNotEmpty.

Production code is the same as in the previous review:

  • Block processing. BlockProcessor calls InstallPredeploys(spec) again and no longer throws. PredeployInstaller.Install is back to void, and the 8141 and 8250 entries behave as they did before.
  • Admission. FrameTxValidationTracer and the txpool recheck still require RecentRootCodeHash at 0x8272D967…Ffff, so a recent root verify frame is rejected until the deployment tx lands.
  • Deployment test. Execute_Eip8272DeploymentTransaction_CreatesRecentRootCodeAtItsAddress checks the EIP's tx hash, the sender, the resulting code hash and nonce 1. The fixture doesn't pre-install the contract, so the test really exercises the CREATE.

Low: none outstanding.

Mergeable: yes, once ethereum/EIPs#12443 lands and the stacked base #14071 is merged.

🤖 AI agent (Claude Code / Opus 5.5) on behalf of @AnkushinDaniil

…into daniil/eip8272-recent-root-deploy

# Conflicts:
#	src/Nethermind/Nethermind.Consensus/Processing/PredeployInstaller.cs
@AnkushinDaniil

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @AnkushinDaniil's task in 23s —— View job


PR #14437 re-review (after merge 91c416d)

  • Gather context: diff against origin/daniil/eip8272-canonical-frame, plus what the merge brought in since 7d085ac
  • Check the merged base changes against this PR
  • Look for leftover references
  • Post summary

No Critical, High or Medium findings.

Since the last review: 91c416d only merges the base branch, which picked up master. The PR's own diff hasn't changed: the same 18 files, +93/−219.

Merge result:

  • The merge brought in the EIP-8141 change from master. It moves ExpiryVerifierAddress to 0x81413f0c…FfFf and removes it from PredeployInstaller.Predeploys, the same pattern this PR uses for 8272.
  • PredeployInstaller resolved cleanly. Its only remaining entry is the 8250 nonce manager, with a non-nullable Nonce and Math.Max(nonce, predeploy.Nonce). Install returns void and takes no parentSpec.
  • Nothing in src/Nethermind still references InitializesOnce, GetParentSpec, RecentRootPredeployNotEmpty or the old 0x…8272 address.
  • Bogota now has shorter remarks, because 8141 activation no longer puts an access-list entry in the first block. That change came from master, and nothing in this PR contradicts it.

Unchanged from the earlier reviews:

  • FrameTxValidationTracer and the TxPool recheck still require RecentRootCodeHash at the new address. So a recent root verify frame is rejected until the deployment tx lands.
  • Execute_Eip8272DeploymentTransaction_CreatesRecentRootCodeAtItsAddress checks the EIP's tx hash, the recovered sender, the resulting code hash and nonce 1.
  • The block-processing path no longer does the per-block parent header and spec lookup.

Low: nothing outstanding.

Mergeable: yes, once ethereum/EIPs#12443 lands and the stacked base #14071 is merged.

🤖 AI agent (Claude Code / Opus 5.5) on behalf of @AnkushinDaniil

Base automatically changed from daniil/eip8272-canonical-frame to master October 8, 2026 11:34
@AnkushinDaniil
AnkushinDaniil marked this pull request as ready for review October 8, 2026 11:51
@claude

claude Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @AnkushinDaniil's task in 27s —— View job


PR #14437 re-review (ready for review, after merge e40b8a1)

  • Gather context: what changed since the last review (91c416d)
  • Re-check the diff against master
  • Post summary

No Critical, High or Medium findings.

Since the last review: #14071 has been merged to master as 058bd5b, and e40b8a1 merges master into this branch. The diff against 058bd5b is the same as in the last review: the same 18 files, +93/−219. No new code was added to the PR.

Re-checked:

  • Activation: PredeployInstaller.Predeploys now holds only the 8250 nonce manager. 8272 activation writes no state and adds no BAL entry, so BlockProcessor can no longer reject a block because of what is at the address. This matches Update EIP-8272: deploy recent root contract as standard contract ethereum/EIPs#12443.
  • Revert leftovers: nothing in src/Nethermind still references InitializesOnce, GetParentSpec, RecentRootPredeployNotEmpty or the old 0x…8272 address. The ISpecProvider/IHeaderFinder constructor params are gone from BlockAccessListManager and the three stateless/witness envs.
  • Admission: FrameTxValidationTracer and the txpool recheck still require RecentRootCodeHash at 0x8272D967…Ffff, so a recent root verify frame is rejected until the deployment tx lands.
  • Performance: each block skips the parent header and spec lookup on the predeploy path.
  • Deployment test: Execute_Eip8272DeploymentTransaction_CreatesRecentRootCodeAtItsAddress checks the EIP's tx hash, the recovered sender, the resulting code hash and nonce 1. The contract is not pre-installed, so the test runs the CREATE itself.
  • Comments: the new and rewritten XML docs on RecentRootCode and in RecentRootStore are accurate. None of them just restate the code.

Low: nothing outstanding.

Mergeable: yes, once ethereum/EIPs#12443 lands. The stacked base #14071 is already on master.

🤖 AI agent (Claude Code / Opus 5.5) on behalf of @AnkushinDaniil

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

Reviewed all 18 changed files, including removal of activation-only installation and parent-spec plumbing, BAL/stateless paths, contract constants and deployment tests. The address, runtime code and deployment vector match https://eips.ethereum.org/EIPS/eip-8272; activation leaves the account and BAL untouched. No actionable findings.

Local Release validation: 39 recent-root/EIP-8272 tests and 219 BlockProcessorTests passed. No independent EELS fixture run.

…nt-root-deploy-review

# Conflicts:
#	src/Nethermind/Nethermind.Blockchain.Test/BlockProcessorTests.cs
#	src/Nethermind/Nethermind.Consensus.Test/PredeployInstallerTests.cs
#	src/Nethermind/Nethermind.Consensus/Processing/PredeployInstaller.cs
@AnkushinDaniil
AnkushinDaniil merged commit 8b4d422 into master Oct 9, 2026
940 of 942 checks passed
@AnkushinDaniil
AnkushinDaniil deleted the daniil/eip8272-recent-root-deploy branch October 9, 2026 23:08
stdevMac pushed a commit that referenced this pull request Oct 15, 2026
#14437)

* fix(eip8250): align TXPARAM keyed-nonce indices with the spec

EIP-8250 assigns 0x0D legacy nonce, 0x0E key count, 0x0F nonce_keys_hash,
0x10 first key. EIP-8272 no longer defines a TXPARAM index, so 0x0F belongs
to EIP-8250 on every fork and indices above 0x10 halt.

* test(eip8250): pin TXPARAM 0x0F halting with only EIP-8272 active

* fix(eip8272): verify recent roots with a canonical VERIFY frame

Follow EIPs 20fb52b9 and 824cbc0b: drop the recent_root_references
envelope field, RECENTROOTREFLOAD, the per-reference intrinsic gas and
the native pre-execution check. A recent root is now verified by a
VERIFY frame targeting RECENT_ROOT_ADDRESS whose data is n 72-byte
tuples, validated by the predeploy with ordinary EVM execution.

Install RECENT_ROOT_CODE from ethereum/sys-asm#53 at activation.

* fix(eip8272): report an overrun frame tx payload as truncated

* test(eip8272): pin the truncation error for an overrun frame tx payload

* feat(eip8272): public mempool rules for the recent_root_verify frame

* fix(eip8272): sweep the in-memory blob pool for stale recent roots

Refuse a tuple-carrying blob frame tx in a persistent blob pool, whose frameless records the sweep cannot read, and skip the sweep while no pooled tx carries tuples.

* fix(eip8272): count persistent-blob-pool recent-root refusals separately

* fix(eip8272): evict recent-root frame txs once RECENT_ROOT_ADDRESS stops running RECENT_ROOT_CODE

* feat(eip8272): index recent-root mempool dependencies by expiry slot

Record each pending recent_root_verify transaction's (storage_key, entry_hash)
dependencies and its first expiring current_slot. A head extending the previous
one only ages tuples out, with no storage reads; any other head rechecks every
indexed transaction, reading each distinct cell once. A head before activation
evicts every such transaction.

* fix(eip8272): read recent-root storage outside the dependency index lock

* test: drop multiple-assert scope spanning state-mutating Process calls

* test: resolve the recent-root block executor from production modules

* test: drop unused using in FrameTxBlockProductionTests

* test: fold the payload truncation message into the declared-length overrun test

* fix(eip8272): reject a recent_root_verify frame unless RECENT_ROOT_ADDRESS runs RECENT_ROOT_CODE

The tracer only withheld the SLOTNUM and recent-root SLOAD permissions over other code,
so code needing neither (a bare STOP) was admitted. EIP-8272 requires rejection when the
code differs or the fork is inactive.

* test(eip8272): pool admission of recent_root_verify through the real prefix simulator

Committed tuple then self-verify, with optional expiry and deploy prologue; tuple age at
the head slot, 8190 and 8191 measured at head.slotNumber + 1; rejection for wrong code at
RECENT_ROOT_ADDRESS and for a sender reading recent-root storage through a call.

* fix(eip8272): release the hash of an aged-out recent_root_verify transaction so a slot rollback can readmit it

* test(eip8272): resolve the recent_root_verify admission pool and simulator from the test container

* fix(eip8272): judge pending recent roots against one snapshot of the canonical head

Head events are consumed after the chain has moved, while admission validates against the canonical head. Taking the header and its state together from IChainHeadInfoProvider.TryGetHeadState keeps a transaction admitted against a newer head from being judged at an older event's slot.

* chore(eip8272): drop the unused rlp using after the master merge

* refactor(eip8272): drop the unused RecentRootStore reference check

The mempool sweep checks age and commitment in RecentRootDependencyIndex, and block execution checks them in the predeploy, so only RecentRootStoreTests still called IsReferenceValid. The window, current-slot and mismatched-root cases stay pinned against the predeploy in Execute_RecentRootVerifyFrame_IsCheckedAgainstTheCommittedEntry.

* docs(eip8272): rewrap the recent-root sweep remarks and name their subject

* fix(eip8272): initialise the recent root predeploy only over an empty account

* fix(eip8272): drop the recent-root dependency of a transaction the pool no longer holds

* docs(eip8272): drop comments describing the removed envelope field

* fix(eip8272): reject a chain spec activating EIP-8272 before EIP-8141 or EIP-7843

* perf(txpool): recheck recent-root dependencies before taking the head lock

* test(eip8272): exempt EIP-8272 from the single-transition chain parameters check

EIP-8272 cannot activate without EIP-8141 and EIP-7843, so a chain spec that sets only its timestamp is rejected. Its activation is covered by ChainSpecBasedSpecProviderTests.

* fix(eip8272): reject a block when foreign code sits at the predeploy address

* fix(eip8272): reject any non-empty predeploy account on the activation block

EIP-8272 Activation requires RECENT_ROOT_ADDRESS to have empty code and
empty storage in the parent state of the first active block, otherwise
that block is invalid. The installer accepted an account already holding
the canonical RECENT_ROOT_CODE as initialised, so a chain could activate
over pre-seeded canonical code (and any storage under it).

The installer now takes the parent block's release spec. When the
predeploy is not active in the parent (the activation transition), any
code, canonical included, or any storage makes the block invalid. When
it is already active in the parent, an account with code or storage is
left untouched and never re-initialised, so later blocks keep the
installed code and the ring-buffer storage.

The parent spec is resolved by IBlockAccessListManager.GetParentSpec
from the parent header via IHeaderFinder and ISpecProvider, which
BlockAccessListManager now takes as optional constructor dependencies
(resolved by DI in production; passed explicitly by the stateless,
witness-capturing and witness-generating environments). Both the normal
and BAL system-contract handlers forward it into PredeployInstaller.

An empty account (no code, no storage) is still installed when the
parent already has EIP-8272 active, which keeps chains that enable the
EIP from genesis working: block 1's parent is genesis, whose spec is
already active, and the genesis allocation need not carry the code.
On a chain whose activation is a real transition this case cannot
arise after the activation block, since that block installs the code.

Tests: the activation block is rejected over canonical code, foreign
code, and storage without code; a block after activation keeps the
installed code and storage and records no BAL change; the installer
rejects every non-empty shape when the parent is inactive and leaves it
unchanged when the parent is active; the genesis-active install over an
empty account keeps passing.

* refactor(eip8272): read recent root tuples with range slices

RecentRootStore.ReadTuple now slices the source id, slot and root with C# range syntax instead of Span.Slice, as asked in review. Same offsets and lengths; no behaviour change.

* refactor(eip8272): drop the chainspec activation-order guard

ChainSpecBasedSpecProvider rejected a chainspec scheduling EIP-8272 before EIP-8141 or EIP-7843. Per review, the provider does not enforce activation ordering for any other EIP, so the check and the test that existed only for it are removed. No other test depended on the exception.

* test(eip8272): drop the removed recent root reference list from the rpc view cases

* feat(eip8272): deploy the recent root contract as an ordinary contract

Follows ethereum/EIPs#12443: the recent root contract is no longer installed
at EIP-8272 activation. It is created by the pre-signed keyless deployment
transaction at 0x8272D9679689Ea2f307140CdF9002D27dC00Ffff.

- RecentRootAddress moves to the deployed address; code and code hash unchanged
- PredeployInstaller drops the recent root entry and the once-only
  initialisation path, so the parent-spec plumbing and the
  RecentRootPredeployNotEmpty block error go too
- a test replays the EIP's deployment transaction and checks hash, sender,
  code hash and nonce at the derived address

* test(eip8272): call the recent root contract a contract, not a predeploy

* test(txpool): keep resubmission outside multiple-assert scopes

* test(eip8272): share the activation tests with the expiry verifier
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants