Skip to content

Add EIP-8272 RECENT_ROOT_CODE predeploy - #53

Merged
lightclient merged 7 commits into
ethereum:mainfrom
AnkushinDaniil:daniil/recent-root-8272
Oct 8, 2026
Merged

lightclient merged 7 commits into
ethereum:mainfrom
AnkushinDaniil:daniil/recent-root-8272

Conversation

@AnkushinDaniil

@AnkushinDaniil AnkushinDaniil commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Adds the RECENT_ROOT_CODE predeploy for EIP-8272 as a geas source, following the request from the EIP authors to maintain the runtime here with generated bytecode and tests rather than as a hex blob in the EIP text.

Since ethereum/EIPs@20fb52b9 EIP-8272 checks recent roots with a canonical VERIFY frame that calls this contract, and its RECENT_ROOT_CODE constant is TBD. This runtime implements both operations of the current text, so its bytecode can fill that constant.

Calls with nonzero value revert. Calldata length selects the operation:

write, len == 64:
  calldata    = salt(32) || root(32)
  source_id   = keccak256(msg.sender || salt)
  entry_hash  = keccak256(ENTRY_DOMAIN   || source_id || uint64_be(SLOTNUM) || root)
  storage_key = keccak256(STORAGE_DOMAIN || source_id || uint64_be(SLOTNUM mod 8192))
  storage[storage_key] = entry_hash

validation, len == n * 72, 1 <= n <= 16:
  each tuple  = source_id(32) || uint64_be(slot)(8) || root(32)
  require slot < SLOTNUM and SLOTNUM - slot <= 8191
  require storage[storage_key(source_id, slot mod 8192)] == entry_hash(source_id, slot, root)
  no return data, no state change

any other length reverts

main.eas targets amsterdam and uses the slotnum mnemonic, so geas checks the stack comments. ctor.eas is the deploy-time wrapper in the same shape as the other predeploys here, and shasum -c passes for both generated files.

Foundry has no SLOTNUM under prague, so the guard tests run the shipped runtime (every guard reverts before SLOTNUM), and the write and validation tests run test/recent_root_shim.hex, generated from main.eas by make build with slotnum replaced by number, so vm.roll picks the slot. The tests cover the EIP's reference vector for both operations, the window edge at age 8191 and 8192, current and future slots, ring overwrite, last write in a slot, every tuple being checked, 16 duplicate tuples passing and 17 reverting, a static-context write failing, and a validation call reading only the derived key and writing nothing.

@AnkushinDaniil
AnkushinDaniil force-pushed the daniil/recent-root-8272 branch from a75a4c6 to 488096e Compare August 11, 2026 05:39
@fjl

fjl commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Geas supports SLOTNUM with #pragma target "amsterdam".

geas v0.3.3 knows SLOTNUM with #pragma target "amsterdam", so the raw-byte
macro and -no-stackcheck are no longer needed. The generated bytecode is
unchanged: the same 144 bytes, checksums and the Foundry guards still pass.
@AnkushinDaniil

Copy link
Copy Markdown
Contributor Author

Done. Switched the runtime to #pragma target "amsterdam" and the slotnum mnemonic, and dropped the raw-byte macro and -no-stackcheck. It assembles to the same 144 bytes, with checksums and the Foundry guards still passing.

The prague EVM in CI has no SLOTNUM (EIP-7843, 0x4b), so the shipped runtime's
write path could not be exercised here and only the calldata guard was covered.

Add a slot-shimmed build of the same source (test/recent_root_slotnum1.eas,
SLOTNUM -> push1 0x01) and a test that runs it and asserts the on-chain SSTORE
reproduces EIP-8272's published reference vector: storage[0x5f027aa1...] ==
0x0a0d1254... for source 0x..01, salt 0, slot 1, root 2. The shim is regenerated
by make build and differs from the shipped source only by the pragma and the two
SLOTNUM sites. The shipped predeploy bytecode is unchanged.
@AnkushinDaniil

Copy link
Copy Markdown
Contributor Author

@fjl this has been idle since i addressed the geas pragma note. what's new: a write path test that runs a slot shimmed build of the same source and checks the on chain sstore reproduces eip-8272's reference vector, so the derivation is pinned, not just the calldata guard. assembles byte identical, checksums pass.

eip-8272 still marks RECENT_ROOT_CODE as TBD, so this predeploy is the gate for both clients on the canonical frame model. another look when you have a moment would help.

ilitteri added a commit to lambdaclass/ethrex that referenced this pull request Sep 11, 2026
…b0e)

EIP-8272 at 824cbc0b0e (PRs #12281 and #12302) no longer touches the envelope: the
recent roots a frame transaction proves travel as a canonical VERIFY frame to the
predeploy at 0x…8272, one to sixteen packed 72-byte (source_id, slot, root) tuples in
its data, which the contract's new validation operation checks under STATICCALL
semantics. The envelope field, its RLP and signature-hash coverage, the per-reference
intrinsic gas and TXPARAM 0x11 are gone; the type-0x06 envelope is back to eight
fields and both golden vectors (the Rust tests and frametx.py) are regenerated from the
two encoders and agree.

The frame is recognised by shape, its tuples parsed from the data, and its public
mempool position enforced with the structural rules: first, or second behind an expiry
verifier, at most one, its limits.execution counted toward MAX_VERIFY_GAS. Admission
checks the tuples natively before any EVM work — RECENT_ROOT_CODE present at the
predeploy, each slot before current_slot and inside the usable window, each entry hash
committed — with current_slot = head slotNumber + 1, and the post-block revalidation
re-runs the same check as the transaction's recent-root dependency. The prefix
simulation runs the frame's code with the two permissions the spec grants it and no
other: SLOTNUM, and SLOAD of the predeploy's own storage, only while that frame executes
the predeploy at the top level. The block context it simulates against is the head's
state at the next slot (prospective_header), at the mempool, the revalidation and the
simulate RPC alike; the EIP-8369 Profile 2 replay keeps judging at the block's own slot.

RECENT_ROOT_CODE becomes the 345-byte two-operation runtime: the write operation byte for
byte from ethereum/sys-asm#53, plus the validation operation the spec leaves as TBD,
assembled from scripts/hegota-testnet/recent_root/main.eas and pinned by hash and by its
four domain immediates. The hash layout is unchanged, so entries written before this
change stay valid.

Tests cover the write-then-validate path through the VM across the window's edges,
malformed data, static writes, admission of a committed tuple and rejection of
uncommitted, too-new, expired, misplaced and code-less cases, the gas accounting, and
the in-block versus prospective judgment slot. The devnet verification script exercises
the frame end to end.
EIP-8272 now verifies recent roots through a canonical VERIFY frame that calls
this contract, so the runtime gains the validation operation: n 72-byte tuples,
1 <= n <= 16, each checked for slot < SLOTNUM, age <= 8191 and a stored entry
hash. Any other length than 64 or n * 72 reverts.

The test shim is now generated from main.eas with SLOTNUM read from NUMBER, so
tests choose the slot with vm.roll.
AnkushinDaniil added a commit to NethermindEth/nethermind that referenced this pull request Sep 30, 2026
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.
@soispoke

soispoke commented Oct 7, 2026

Copy link
Copy Markdown

@ilitteri could you confirm this PR matches what ethrex uses?

@soispoke

soispoke commented Oct 7, 2026

Copy link
Copy Markdown

lgtm!

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

AnkushinDaniil added a commit to NethermindEth/nethermind that referenced this pull request Oct 8, 2026
* 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

* test(txpool): keep resubmission outside multiple-assert scopes
EIP-8272 now pins RECENT_ROOT_ADDRESS to 0x8272D9679689Ea2f307140CdF9002D27dC00Ffff.
Bump forge-std to v1.17.0 for vm.rollSlot and run the EVM as amsterdam so SLOTNUM executes. Amsterdam's state repricing makes a fresh-slot write in the 2935 test cost 125575 gas instead of 43655, which pushes the ring buffer and fee tests past the default 2^30 test gas limit, so lift the limit. Solidity still compiles for prague since amsterdam is experimental in solc.
With the suite running under amsterdam, SLOTNUM executes and vm.rollSlot sets the slot, so the NUMBER shim generated with sed isn't needed. The guard tests now sit next to a call that succeeds; under prague every path ended at SLOTNUM, so a missing guard still failed the call.

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

No changes to the asm, but updated foundry and set the vanity address prefix and suffix to match the eip.

@lightclient
lightclient merged commit f808a29 into ethereum:main Oct 8, 2026
2 checks passed
ilitteri added a commit to lambdaclass/ethrex that referenced this pull request Oct 9, 2026
A recent root verifier frame is a VERIFY frame to RECENT_ROOT_ADDRESS (0x…8272)
with flags 0, value 0 and `limits.state` 0, whose data is 1 to 16 packed
`source_id || be64(slot) || root` tuples. It runs the contract's validation
operation, so a tuple that is not stored, names the current slot or later, or is
more than 8,191 slots old reverts the frame and invalidates the transaction.

RECENT_ROOT_CODE is the 320-byte runtime proposed in ethereum/sys-asm#53
(bytecode/recent_root/main.hex at 0ea4d71517), since the EIP still lists it as TBD.
Calldata of exactly 64 bytes writes `salt || root` for the caller at the current
slot. Valid tuple data validates. Anything else, or any value, reverts. The contract
is installed at Hegota activation with the other system contracts, outside the block
access list. An address that already holds code or storage makes the activation
block invalid.

In the public mempool, shape matching skips the verifier frame when it is first, or
second behind an expiry verifier. A matching frame anywhere else is rejected. Its
`limits.execution` counts toward MAX_VERIFY_GAS. A transaction carrying the frame is
rejected unless the code at the address is exactly RECENT_ROOT_CODE. The simulation
runs that code at the head slot plus one, and grants SLOTNUM and SLOAD of the
contract's own storage only at the top level of that frame. Post-block revalidation
re-simulates pending transactions, so tuples that age out or are reorged away evict
them.

The VM tests run both contract operations through the EVM, including the EIP's
reference vector, the window edges, malformed data and static writes. They also
check that admission and execution agree on the frame's gas and on FRAMEPARAM reads
of it.
AnkushinDaniil added a commit to NethermindEth/nethermind that referenced this pull request Oct 9, 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
AnkushinDaniil added a commit to NethermindEth/nethermind that referenced this pull request Oct 12, 2026
* 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

* refactor(consensus): remove the now-empty predeploy installer

EIP-8141, EIP-8250 and EIP-8272 system contracts are now deployed by ordinary
transactions, so the installer has no entries left and its block-processing
hook never runs.
stdevMac pushed a commit to NethermindEth/nethermind that referenced this pull request Oct 15, 2026
* 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

* test(txpool): keep resubmission outside multiple-assert scopes
stdevMac pushed a commit to NethermindEth/nethermind 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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants