EIP-8272 Recent roots for frame transactions
Hegota Considered for Inclusion · Account abstraction · EIP status Draft · created 2026-05-15
Lets EIP-8141 frame transactions check, during public mempool validation, a Merkle root that an app recorded recently. One standard contract stores per-slot roots, and a dedicated VERIFY frame checks them.
Why it is needed
When an EIP-8141 frame transaction is validated in the public mempool, it cannot read storage managed by other accounts or apps. Shared storage can be changed by other transactions at any time, so a node cannot trust and retain a validation result that depends on it. Yet some validations require recent app state. A privacy app maintains a commitment tree and proves a spend against a recent tree root. Wallet permission roots and account validation roots are in the same situation. This EIP introduces a standard contract that stores roots keyed by the writing entity and the slot. Barring a reorg, that entry does not change while a transaction references it, so nodes can safely check and index it. It creates no new transaction envelope and no new opcode.
What changes
Today
- Public mempool validation can read only the sending account's own storage and cannot see state managed by other apps.
- A transaction that must check a recent tree root, such as one from a privacy app, has no way to be validated within the public mempool rules.
- There is no way to tell from the transaction alone which app state it depends on.
After EIP-8272
- An app records a per-slot root in the standard contract, and the transaction declares that root as a (source_id, slot, root) tuple.
- A dedicated VERIFY frame checks the root before account validation. If it is wrong, the transaction itself is invalid.
- Nodes index storage dependencies from the frame data alone and evict only the affected transactions on a reorg or expiry.
- The transaction envelope, signature hash, and opcodes stay unchanged.
Who is affected
- Privacy and wallet app developers: You need a path to write roots into the contract for each slot, and the validation code must use FRAMEPARAM to confirm the previous frame succeeded before reading the tuple. Inclusion of the root write is not guaranteed, so a publishing policy is also needed.
- Client teams: Mempool validation must add the frame shape check, SLOTNUM and SLOAD exceptions, dependency indexing, and revalidation on reorg. It may evaluate directly instead of running the EVM, but the result must be the same.
- Users: Transactions that depend on app state, such as privacy transactions, can be sent through the public mempool. The transaction format does not change, so there is nothing extra to do.
- Block builders and proposers: Immediately before inclusion, the transaction must be revalidated with that payload's slotNumber. A reference close to expiry can become invalid if an empty slot intervenes.
In depth
Current slot and the referenceable window
The time reference in this EIP is the consensus layer slot, not the block timestamp. During block execution, current_slot is the slot of the beacon block that contains that execution payload, and the execution client must read it from the slotNumber field of EIP-7843. Deriving it backwards by applying a fixed slot length to block.timestamp is prohibited. In public mempool validation, current_slot is the slotNumber of the latest canonical execution header plus 1, and the SLOTNUM value in simulation must be the same. This value is deliberately not incremented when empty slots pass. Instead, the transaction must be revalidated with that payload's slotNumber before it is put into the payload. A reference may only point to a slot strictly earlier than current_slot, and a root written in slot S can be referenced from S + 1. The window size follows from this. A write in slot S updates cell S mod RECENT_ROOT_LENGTH, and the old root sharing that cell is exactly 8192 slots old and therefore already expired. So a write in the current slot cannot break a valid reference, and with RECENT_ROOT_LENGTH = 8192 the referenceable window is RECENT_ROOT_USABLE_WINDOW = 8191, which is the range excluding the current slot. A reference close to expiry becomes invalid if slots are skipped, so the builder revalidates with the exact payload slot.
Root sources and storage key calculation
The entity that writes a root is identified by source_id = keccak256(source_address || salt). source_address can be an externally owned account or a contract, and varying the salt lets one address hold multiple sources. Who may write and how salts are allocated are the app's responsibility. The stored value is entry_hash = keccak256(RECENT_ROOT_ENTRY_DOMAIN || source_id || uint64_be(slot) || root), and the storage location is storage_key = keccak256(RECENT_ROOT_STORAGE_DOMAIN || source_id || uint64_be(i)), where i = slot mod RECENT_ROOT_LENGTH. Each source effectively has a ring buffer entries: bytes32[RECENT_ROOT_LENGTH] of length 8192, and every cell starts at 0. Slots and indexes are 8-byte big-endian, addresses are 20 bytes, and domains, roots, salts, source_ids, hashes, and keys are concatenated as fixed 32-byte lengths. Because the stored value is a hash that contains source_id, slot, and root together, a check cannot be passed with another source's root or with an old root left in the same cell. There is no separate source creation transaction. A source comes into existence on the first write with a new (source_address, salt), and a write that creates a new storage cell usually pays the state growth cost. Total storage grows in proportion to the number of source_ids written, and each source writes at most RECENT_ROOT_LENGTH keys.
Contract deployment and write operation
The contract is deployed like an ordinary contract at RECENT_ROOT_ADDRESS (0x8272D9679689Ea2f307140CdF9002D27dC00Ffff), and the protocol does not install it at activation. The deployment transaction is fixed at type 0x0, nonce 0, gas 0x13d620, gasPrice 0xe8d4a51000, and is sent by the synthetic sender 0x14bf16d4c9842bf1EbF396e553477C66EB0a8A82, derived backwards from its signature. A call with a nonzero value reverts, and the two operations, write and verify, are selected by calldata length. A write is exactly 64 bytes, with the first 32 bytes as the salt and the last 32 bytes as the root. The calling msg.sender becomes source_address, and a call that succeeds in slot S stores entry_hash in cell i = S mod RECENT_ROOT_LENGTH. In a static context it fails and storage is unchanged. DELEGATECALL and CALLCODE write the calling account's storage, and EIP-7702 delegated code writes the delegating account's storage, so unless the storage context is RECENT_ROOT_ADDRESS none of them can change the recent root storage. If the same source address and salt write multiple times in one slot, they target the same key, so only the last value in canonical block execution order remains, and only that value can be referenced from S + 1. A successful write has no return value and no logs.
Verification operation and the recent root verifier frame
The calldata of the verification operation is n * RECENT_ROOT_TUPLE_BYTES (72) bytes, with 1 <= n <= MAX_RECENT_ROOT_REFERENCES (16). Empty calldata, a length that is not a multiple of 72, and more than 16 tuples all revert, and none overlaps with the 64-byte write encoding. For each tuple it checks slot < current_slot, current_slot - slot <= RECENT_ROOT_USABLE_WINDOW (8191), and storage[storage_key] == entry_hash, and if any one fails the whole call reverts. Duplicate tuples are each checked separately, and if all pass, it succeeds with no return value, logs, or state changes. The current slot is read with SLOTNUM, no other account is called, and only storage keys derived from calldata are read. The recent root verifier frame that carries this operation has mode == VERIFY, target == RECENT_ROOT_ADDRESS, flags == 0, value == 0, limits.state == 0, and a data length that is a multiple of 72 between 72 and 16 * 72 inclusive. In public mempool classification it is called recent_root_verify. It executes with EIP-8141 STATICCALL semantics, and if the contract reverts or the frame halts exceptionally, the transaction becomes invalid. Gas is ordinary EVM gas, target access and storage reads follow the usual warm and cold rules, and data follows the EIP-8141 transaction data fee. By using a standard frame instead of an envelope field, EIP-8141's encoding, signature hash, execution, gas, and frame lookup are reused as they are, and the transaction payload and signature hash do not change either.
Place in the public mempool and the allowed scope
Transactions targeting the public mempool may include at most one recent_root_verify frame, and the slots are fixed in the order [expiry_verify?] [recent_root_verify?] [deploy?] [account verification] ... That is, it is either frame 0, or frame 1 if an expiry_verify frame is present. When matching the four validation prefix shapes of EIP-8141, apply the rule that deploy must come first after skipping the two optional protocol verification frames that precede it. The frame shape and data encoding must be checked before the sender state is read. There are only two additional allowances. Only when this frame executes at the top level and the runtime code at RECENT_ROOT_ADDRESS is exactly equal to RECENT_ROOT_CODE may SLOTNUM be executed inside that code, and SLOAD may be used only for a storage_key derived from the tuple. Nested calls are not allowed, and if the code differs the transaction is rejected. A client may evaluate the frame directly instead of executing RECENT_ROOT_CODE, but the frame receipt, gas usage, and the updates and rollbacks of warm accounts and storage keys must match EVM execution, and it must reject the transaction in the same way if execution halts with an exception because limits.execution is insufficient. The limits.execution of this frame counts toward MAX_VERIFY_GAS. The application's account verification code uses FRAMEPARAM to check resolved_target == RECENT_ROOT_ADDRESS, mode == VERIFY, flags == 0, limits.state == 0, the correct data length, and status == 1, and then reads the tuples with FRAMEDATALOAD and FRAMEDATACOPY.
Expiry, dependency tracking, and reorg handling
If any tuple has slot >= current_slot, the node does not accept the transaction and also evicts it if it was already pending. Otherwise, a transaction is evicted once any tuple reaches current_slot - slot >= RECENT_ROOT_LENGTH. To avoid accepting transactions that would expire immediately, it is advisable not to accept a transaction whose oldest reference falls within a margin chosen by the node. On acceptance, record the first current_slot at which each distinct (storage_key, entry_hash) dependency expires, and index by this value so that slot changes and reorgs do not require scanning unrelated transactions. The code and active status of RECENT_ROOT_ADDRESS are also tracked as dependencies. When a transaction leaves the mempool, its records are removed, and when a transaction is replaced, its records are replaced atomically with it. If a dependency may have changed, re-check all age and storage conditions of the affected transactions and evict those that no longer satisfy them. During a reorg, look at the state changes on both the removed branch and the added branch, including the case where an entry disappears through rollback and there is no replacement write. If the contract code or active status changes, revalidate every pending transaction that contains this frame. This is why the full (source_id, slot, root) is placed in the frame data. A client can extract all storage dependencies without running application validation, and can immediately pick out transactions that reference roots orphaned after a reorg. The 72-byte packed encoding has no count or selector, and its valid length does not overlap with a 64-byte write.
Security considerations
The consensus layer treats root only as an opaque bytes32. What that value commits to is decided by the application, and what it approves must bind the expected (source_id, slot, root) tuple. A successful verify frame proves only that the tuple is currently stored and recent, so application verification must check the defined fields and success status of the completed frame before trusting the frame data. EIP-8141 also allows verification against an explicit 32-byte signature message instead of the standard signature hash, in which case the verification logic must bind the recent root frame and the related frames on its own. For the same (source_id, slot), only the last root in canonical execution order remains, so an application that needs multiple roots in one slot must use an aggregate commitment. This EIP does not guarantee the inclusion of root-writing transactions. An application that must publish on time needs its own publication path, redundant root sources, and an inclusion policy for write transactions. The same (source_address, salt) yields the same source_id on other chains, so proofs, bridge messages, and attestations created off-chain must bind the chain domain outside the tuple. Storage remains permanently under RECENT_ROOT_ADDRESS. Existing sources overwrite at most RECENT_ROOT_LENGTH entries, but new sources add entries, so the EIP notes that if the usual state growth fee proves insufficient, a source registration cost or a first-write surcharge could be proposed later. A single popular root can cause many transactions to be evicted at once through a reorg or simultaneous expiry, but thanks to the dependency index there is no need to re-run the validation prefix of unrelated transactions.
Activation, test cases, status, and fork decisions
This EIP must be activated at the same time as EIP-8141 and EIP-7843 or later, and activation itself does not change state. Before the deployment transaction is included, RECENT_ROOT_ADDRESS has no code, so the EIP-8141 default code in VERIFY mode runs, reverts because flags is 0, and the frame fails. A reference to a slot before deployment cannot be satisfied because the storage is empty. The test cases cover the boundary between ages 8191 and 8192 slots, 71-byte, 73-byte, and 17-tuple data, frames with flags == 1 or non-zero state gas (the contract runs normally but is not classified as recent_root_verify, so the mempool rejects it), a case that exactly fits MAX_VERIFY_GAS and one that exceeds it by 1, equivalence between direct evaluation and EVM execution, and cases where a root or the deployment transaction disappears through a reorg. The sixteen-tuple case must include the cold target account access cost that EIP-8141 charges on frame entry. A reference vector based on current_slot = 2 and slot 1 is also in the source. The source status is Draft, so detailed rules may change. In core developer calls, EIP-8272 was accepted as a Hegotá candidate (PFI) at ACDE #240 on 07.02, raised to Considered for Inclusion (CFI) for Hegotá at ACDE #246 on 09.24 together with EIP-7906, 8250, 7668, 8253, 3298, 8131, and others, and confirmed for inclusion in bogota-devnet-1 along with EIP-8141, 8250, 8272, and 7906 at ACDT #100 on 10.12.