From 20fb52b9b2c370b6964c59e4f998f63a14803122 Mon Sep 17 00:00:00 2001 From: soispoke <66172107+soispoke@users.noreply.github.com> Date: Fri, 4 Sep 2026 09:24:10 +0200 Subject: [PATCH 1/7] Update EIP-8272: verify recent roots with a canonical frame Merged by EIP-Bot. --- EIPS/eip-8272.md | 365 ++++++++++++++++++++++++++--------------------- 1 file changed, 203 insertions(+), 162 deletions(-) diff --git a/EIPS/eip-8272.md b/EIPS/eip-8272.md index 264de9b2cde308..e80518b56e265a 100644 --- a/EIPS/eip-8272.md +++ b/EIPS/eip-8272.md @@ -1,67 +1,58 @@ --- eip: 8272 title: Recent Roots for Frame Transactions -description: Frame transactions can declare verified recent roots +description: Frame transactions can verify recent roots through a canonical frame author: Thomas Thiery (@soispoke), Vitalik Buterin (@vbuterin), Toni Wahrstätter (@nerolation) discussions-to: https://ethereum-magicians.org/t/eip-8272-recent-roots-for-frame-transactions/28621 status: Draft type: Standards Track category: Core created: 2026-05-15 -requires: 7623, 7843, 8141 +requires: 7843, 8141 --- ## Abstract -[EIP-8141](./eip-8141.md) frame transactions can reference recent roots without reading mutable storage during validation. This helps privacy applications validate spends using proofs against recent commitment tree roots named in the signed transaction envelope. A root source writes roots to a system contract, with each root keyed by `(source_id, slot)`, where `source_id` derives from the writer address and a salt. A frame transaction may declare recent root references of the form: +[EIP-8141](./eip-8141.md) frame transactions can verify recent roots through a canonical `VERIFY` frame. The frame carries one to sixteen `(source_id, slot, root)` tuples and calls a recent root contract that checks each tuple against its storage. Root sources write to the same contract. Each root is keyed by `(source_id, slot)`, where `source_id` is derived from the writer address and a salt. -```text -(source_id, slot, root) -``` - -Before frame execution, clients check each reference against the transaction pre-state. The check succeeds only if the named root is stored for the named source and slot, and the slot is still recent. Validation code can then read the verified reference through transaction introspection. +The frame runs before account validation in the public mempool. A failed check invalidates the transaction, while account validation can read the verified tuples through existing frame introspection. This requires no change to the `FrameTx` envelope and no new opcode. ## Motivation -[EIP-8141](./eip-8141.md) validation must not read arbitrary storage controlled by another account or application in the public mempool. Some validation rules still need to depend on recent application state, such as privacy tree roots, wallet authorization roots, or account validation roots. +Public mempool validation under [EIP-8141](./eip-8141.md) cannot read arbitrary storage controlled by another account or application. Some validation rules still need recent application state, such as privacy tree roots, wallet authorization roots, or account validation roots. -Recent root references let a transaction explicitly name recent roots in its signed transaction envelope. Each reference maps to one system-contract storage key and can be checked before validation code runs. +The recent root contract stores each entry under a key determined by its source and slot. Except after a chain reorganization, that entry cannot change while a transaction may reference it. The canonical frame lets clients check and index these dependencies without changing the transaction envelope. -Privacy applications, for example, keep a tree of commitments and prove spends against a recent tree root. With this EIP, the application writes roots by slot, and spend transactions reference one of those roots directly instead of reading the application's changing tree state during validation. +Privacy applications, for example, keep a tree of commitments and prove spends against a recent tree root. The application writes roots by slot, and a spend transaction verifies one of those roots before its account validation frame runs. ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. -This specification is a delta against [EIP-8141](./eip-8141.md). Terms not defined here, including `FrameTx`, `FRAME_TX_TYPE`, `VERIFY`, `EXPIRY_VERIFIER`, frame modes, and `TXPARAM`, have the meanings defined in [EIP-8141](./eip-8141.md). +This specification extends [EIP-8141](./eip-8141.md). Terms not defined here, including `FrameTx`, `VERIFY`, `expiry_verify`, the validation prefix, `MAX_VERIFY_GAS`, `FRAMEPARAM`, `FRAMEDATALOAD`, and `FRAMEDATACOPY`, have the meanings defined in EIP-8141. ### Constants -| Name | Value | -| ------------------------------------ | -------------------------------------------------------------------------------: | -| `FORK_TIMESTAMP` | `TBD` | -| `RECENT_ROOT_ADDRESS` | `0x0000000000000000000000000000000000008272` | -| `RECENT_ROOT_CODE` | `TBD` | -| `RECENT_ROOT_LENGTH` | `8192` | -| `RECENT_ROOT_USABLE_WINDOW` | `8191` | -| `MAX_RECENT_ROOT_REFERENCES` | `16` | -| `RECENT_ROOT_ENTRY_DOMAIN` | `keccak256("RECENT_ROOT_ENTRY")` | -| `RECENT_ROOT_STORAGE_DOMAIN` | `keccak256("RECENT_ROOT_STORAGE")` | -| `RECENT_ROOT_REFERENCE_ADDRESS_GAS` | `ACCESS_LIST_ADDRESS_COST` | -| `RECENT_ROOT_REFERENCE_GAS` | `ACCESS_LIST_STORAGE_KEY_COST + 2 * KECCAK256_BASE_GAS + 7 * KECCAK256_WORD_GAS` | -| `TXPARAM_RECENT_ROOT_REFERENCE_COUNT` | `0x11` | -| `RECENTROOTREFLOAD` | `0xB6` | -| `RECENTROOTREFLOAD_GAS` | `3` | - -All concatenations below use fixed-length encodings. Domains are 32 bytes. Addresses are 20 bytes. Slots and indices are unsigned 64-bit big-endian integers. Roots, salts, source identifiers, entry hashes, and storage keys are 32 bytes. +| Name | Value | +| --- | ---: | +| `RECENT_ROOT_ADDRESS` | `0x0000000000000000000000000000000000008272` | +| `RECENT_ROOT_CODE` | `TBD` | +| `RECENT_ROOT_LENGTH` | `8192` | +| `RECENT_ROOT_USABLE_WINDOW` | `8191` | +| `MAX_RECENT_ROOT_REFERENCES` | `16` | +| `RECENT_ROOT_TUPLE_BYTES` | `72` | +| `RECENT_ROOT_ENTRY_DOMAIN` | `keccak256("RECENT_ROOT_ENTRY")` | +| `RECENT_ROOT_STORAGE_DOMAIN` | `keccak256("RECENT_ROOT_STORAGE")` | + +All concatenations below use encodings with fixed lengths. Domains are 32 bytes. Addresses are 20 bytes. Slots and indices are unsigned integers encoded in eight bytes, with the most significant byte first. Roots, salts, source identifiers, entry hashes, and storage keys are 32 bytes. ### Current slot -For block validation, `current_slot` is the consensus slot of the beacon block that contains the execution payload being validated. +During block execution, `current_slot` is the consensus slot of the beacon block containing the execution payload. -Execution clients MUST obtain `current_slot` from the [EIP-7843](./eip-7843.md) `slotNumber` field. Clients MUST NOT derive `current_slot` from `block.timestamp` using a fixed slot duration. +Execution clients MUST obtain `current_slot` from the [EIP-7843](./eip-7843.md) `slotNumber` field. Clients MUST NOT derive it from `block.timestamp` using a fixed slot duration. -For transaction pool handling, `current_slot` is the node's current slot at receipt, recheck, or eviction time. It is local policy, not block validity. +During public mempool validation, `current_slot` is one greater than the `slotNumber` in the latest canonical execution header. The simulated `SLOTNUM` value MUST equal this slot. This value deliberately does not advance during empty slots. Before including a transaction in a payload, a client MUST revalidate it with `SLOTNUM` set to that payload's `slotNumber`. References MUST target slots strictly before `current_slot`. A root written during slot `S` becomes referenceable beginning in slot `S + 1`. @@ -75,7 +66,7 @@ source_id = keccak256(source_address || salt) where `source_address` is an address and `salt` is a `bytes32` value. -The source address MAY be an externally owned account or a contract, and MAY use multiple root sources by using different salts. Applications using a root source are responsible for controlling who can write to it and how salts are allocated. +The source address MAY be an externally owned account or a contract, and MAY use multiple root sources through different salts. Applications using a root source are responsible for controlling who can write to it and how salts are allocated. ### Entry and storage keys @@ -114,7 +105,11 @@ Each root source uses at most `RECENT_ROOT_LENGTH` storage keys. The global stor At activation, clients MUST create or update the account at `RECENT_ROOT_ADDRESS` as specified in [Activation](#activation). -The contract accepts one write operation with 64 bytes of calldata: +Calls with nonzero value MUST revert. Calldata selects one of two operations by length. + +#### Write operation + +The write operation has exactly 64 bytes of calldata: ```text salt: bytes32 @@ -123,15 +118,7 @@ root: bytes32 Bytes `0..31` are `salt`. Bytes `32..63` are `root`. -Calls MUST revert unless calldata is exactly 64 bytes and call value is zero. - -In static context, the write MUST fail and storage MUST remain unchanged. - -The source address for a successful write is `msg.sender` of the call to `RECENT_ROOT_ADDRESS`. - -Only a direct call to `RECENT_ROOT_ADDRESS` can write recent-root storage. `DELEGATECALL` and `CALLCODE` MUST NOT write recent-root storage. - -When a successful call is made during slot `S`, the contract computes: +The source address is `msg.sender` of the call to `RECENT_ROOT_ADDRESS`. When a successful call is made during slot `S`, the contract computes: ```text source_address = msg.sender @@ -156,59 +143,37 @@ and sets: storage[storage_key] = entry_hash ``` -The call follows normal EVM execution and gas accounting. A successful call returns zero bytes. The contract exposes no read operation. +In static context, the write MUST fail and storage MUST remain unchanged. -Each `(source_id, S)` has at most one referenceable root on the canonical chain. Multiple writes by the same source address and salt during slot `S` target the same storage key. If multiple writes are included, the final write in canonical block execution order overwrites earlier writes. Only that final root is referenceable beginning in slot `S + 1`. +Only execution whose storage context is `RECENT_ROOT_ADDRESS` writes recent root storage. `DELEGATECALL` and `CALLCODE` use the invoking account's storage context, while [EIP-7702](./eip-7702.md) delegated code uses the delegating account's storage context. None modifies recent root storage unless that context is `RECENT_ROOT_ADDRESS`. -### Transaction payload +Each `(source_id, S)` has at most one referenceable root on the canonical chain. Multiple writes by the same source address and salt during slot `S` target the same storage key. The final write in canonical block execution order overwrites earlier writes, and only that root is referenceable beginning in slot `S + 1`. -This EIP inserts `recent_root_references` immediately after `blob_versioned_hashes` in the [EIP-8141](./eip-8141.md) `FRAME_TX_TYPE` payload. No other payload field is changed. Applied to the [EIP-8141](./eip-8141.md) payload, the result is: +A successful write returns no data or logs. -```text -[chain_id, nonce, sender, frames, signatures, - max_priority_fee_per_gas, max_fee_per_gas, - max_fee_per_blob_gas, blob_versioned_hashes, - recent_root_references] -``` +#### Validation operation -where: +The validation operation has `n * RECENT_ROOT_TUPLE_BYTES` bytes of calldata, where: ```text -recent_root_references = [[source_id, slot, root], ...] +1 <= n <= MAX_RECENT_ROOT_REFERENCES ``` -`recent_root_references` MUST be an RLP list. Each item MUST be an RLP list of exactly three elements. `source_id` MUST be a byte string of length 32. `slot` MUST be a canonical RLP integer satisfying `slot < 2**64`. `root` MUST be a byte string of length 32. Consensus treats `root` as opaque bytes; applications define what it commits to. The number of references MUST NOT exceed `MAX_RECENT_ROOT_REFERENCES`. +The calldata is the concatenation of `n` tuples without a selector or length prefix: -The `slot` field is a slot number, not a timestamp or block number. - -The frame layout and frame execution rules are otherwise unchanged. - -The [EIP-8141](./eip-8141.md) signature hash procedure applies after this field is inserted. The signing payload contains `recent_root_references` immediately after `blob_versioned_hashes`. - -Frame data, including VERIFY frame data, MUST NOT add, remove, or modify the transaction's recent root reference set. - -### Static validity - -Decoders MUST reject a frame transaction to which this EIP applies if any of the following is true: - -* the payload does not match the [EIP-8141](./eip-8141.md) payload schema after inserting exactly one `recent_root_references` field immediately after `blob_versioned_hashes`; -* `recent_root_references` is not an RLP list; -* `len(recent_root_references) > MAX_RECENT_ROOT_REFERENCES`; -* any reference is not an RLP list of exactly three elements; -* any `source_id` is not a byte string of length 32; -* any `slot` is not a canonical RLP integer or satisfies `slot >= 2**64`; -* any `root` is not a byte string of length 32. - -### Reference validity - -Within block execution, recent root references are checked after the [EIP-8141](./eip-8141.md) nonce check and before frame execution, against the transaction pre-state. The transaction pre-state includes all prior transactions in the same block. +```text +source_id: bytes32 +slot: uint64_be +root: bytes32 +``` -Competing blocks at the same slot may have different recent-root state, and a reference is valid only in a block whose transaction pre-state contains the referenced entry. `current_slot` is the consensus slot of that block. +The contract MUST revert for empty calldata, for a length that is not a multiple of `RECENT_ROOT_TUPLE_BYTES`, or for more than `MAX_RECENT_ROOT_REFERENCES` tuples. The 64-byte write encoding and every valid validation encoding are disjoint. -A recent root reference `(source_id, slot, root)` is valid only if: +For each tuple, the contract MUST check: ```text -1 <= current_slot - slot <= RECENT_ROOT_USABLE_WINDOW +slot < current_slot +current_slot - slot <= RECENT_ROOT_USABLE_WINDOW i = slot mod RECENT_ROOT_LENGTH entry_hash = keccak256( RECENT_ROOT_ENTRY_DOMAIN || @@ -221,158 +186,234 @@ storage_key = keccak256( source_id || uint64_be(i) ) -RECENT_ROOT_ADDRESS[storage_key] == entry_hash +storage[storage_key] == entry_hash ``` -If any reference is invalid, the transaction is invalid and no frame is executed. A block containing such a transaction is invalid. - -Duplicate references are valid. They are checked, charged, and preserved independently. Accessed address and storage-key sets deduplicate normally. +The call MUST revert if any tuple fails. Duplicate tuples are valid and are checked independently. If every tuple passes, the call MUST succeed with no return data, logs, or state changes. -Each valid reference MUST add `RECENT_ROOT_ADDRESS` and its `storage_key` to the transaction's accessed address and storage-key sets. This affects warm/cold gas accounting only. +The validation operation MUST read the current slot with `SLOTNUM`, MUST NOT call another account, and MUST read only the recent root storage keys derived from its calldata. -### Access lists +All calls use ordinary EVM execution and gas accounting. -Recent root writes are ordinary writes to `RECENT_ROOT_ADDRESS[storage_key]`. +### Recent root verifier frame -### Gas accounting - -This EIP adds the data cost and reference checks introduced by `recent_root_references` to the [EIP-8141](./eip-8141.md) gas quantities. Define: +A **recent root verifier frame** is a frame with all of the following properties: ```text -recent_root_reference_intrinsic_gas = - 0 - if len(recent_root_references) == 0 - RECENT_ROOT_REFERENCE_ADDRESS_GAS - + len(recent_root_references) * RECENT_ROOT_REFERENCE_GAS - otherwise +frame.mode == VERIFY +frame.target == RECENT_ROOT_ADDRESS +frame.flags == 0 +frame.value == 0 +frame.limits.state == 0 +RECENT_ROOT_TUPLE_BYTES <= len(frame.data) +len(frame.data) <= MAX_RECENT_ROOT_REFERENCES * RECENT_ROOT_TUPLE_BYTES +len(frame.data) mod RECENT_ROOT_TUPLE_BYTES == 0 ``` -and: +For public mempool classification, this frame is named `recent_root_verify`. + +The frame executes the validation operation through the `STATICCALL` semantics of EIP-8141. If the contract reverts or the frame halts exceptionally, EIP-8141 makes the transaction invalid. + +The frame consumes ordinary EVM gas. Its target access and storage reads follow the normal warm and cold access rules, and its data follows the normal EIP-8141 transaction data pricing. This EIP adds no special intrinsic gas, warming rule, or exemption from block gas. + +EIP-8141's canonical signature hash already covers the complete frame list, including each frame's data. This EIP does not change the transaction payload or signature hash. + +### Application introspection + +Applications read a recent root verifier frame through EIP-8141's existing `FRAMEPARAM`, `FRAMEDATALOAD`, and `FRAMEDATACOPY` instructions. This EIP adds no transaction parameter or opcode. + +Application validation can identify a completed recent root verifier frame through `FRAMEPARAM` by checking `resolved_target == RECENT_ROOT_ADDRESS`, `mode == VERIFY`, `flags == 0`, `limits.state == 0`, a valid recent root data length, and `status == 1`. + +In a transaction eligible for the public mempool, the recent root verifier frame is frame `0`, or frame `1` when an `expiry_verify` frame is present. + +### Public mempool handling + +EIP-8141's public mempool policy is extended to permit the `current_slot` value defined above and the code and storage reads required by a `recent_root_verify` frame, subject to the rules below. + +A transaction eligible for the public mempool MUST contain no more than one recent root verifier frame. If present, it MUST appear immediately after an optional `expiry_verify` frame and before every other frame: ```text -recent_root_calldata = rlp(recent_root_references) -recent_root_calldata_cost = calldata_cost(recent_root_calldata) -recent_root_calldata_tokens = tokens_in(recent_root_calldata) +[expiry_verify?] [recent_root_verify?] [deploy?] [account validation] ... ``` -Apply the following additions: +For the purpose of matching EIP-8141's four recognized validation prefix shapes, clients MUST skip both optional leading protocol verifier frames. The EIP-8141 rule that `deploy` is first applies after these frames are skipped. -* add `recent_root_calldata_cost + recent_root_reference_intrinsic_gas` to `standard_gas_limit`; -* add `recent_root_reference_intrinsic_gas` to `calldata_floor_gas`; -* add `recent_root_calldata_tokens` to `calldata_tokens`. +Clients MUST check this frame shape and data encoding before reading sender state. -The [EIP-8141](./eip-8141.md) definitions of `max_gas`, `gas_used`, and fee settlement remain unchanged and use these updated quantities. +A recent root verifier frame is subject to EIP-8141's generic validation trace and opcode rules with exactly two additional permissions. These permissions apply only while this frame executes at the top level and the runtime code at `RECENT_ROOT_ADDRESS` exactly equals `RECENT_ROOT_CODE`: -`RECENT_ROOT_REFERENCE_GAS` covers one declared storage key and the two Keccak computations used to derive `storage_key` and `entry_hash`. +1. `SLOTNUM` MAY execute inside `RECENT_ROOT_CODE`. +2. `SLOAD` MAY read `RECENT_ROOT_ADDRESS[storage_key]` for storage keys derived from the frame's tuples as specified above. -### TXPARAM and RECENTROOTREFLOAD +No nested call receives either permission. No other opcode, call, storage read, or storage write exception is added. Clients MUST reject the transaction from the public mempool if the code at `RECENT_ROOT_ADDRESS` does not equal `RECENT_ROOT_CODE`. -One new `TXPARAM` index and one new opcode are added: +The frame MUST execute successfully. During public mempool validation, a client MAY evaluate this frame directly instead of executing `RECENT_ROOT_CODE`, even when other frames in the validation prefix require simulation. This direct evaluation MUST produce the same frame receipt and gas use as EVM execution. It MUST also apply the same warm account and storage key updates and rollbacks as EVM execution. A client MUST reject the transaction if EVM execution would revert or halt exceptionally, including when `frame.limits.execution` is insufficient. -[EIP-8141](./eip-8141.md) assigns opcodes `0xB4` to `SIGPARAM` and `0xB5` to `SIGDATACOPY`. `RECENTROOTREFLOAD` uses `0xB6` to avoid those collisions. +For EIP-8141's public mempool checks on declared limits and total validation work, clients MUST exclude the recent root verifier frame: -| Name | Value | Return value | -| ------------------------------------ | -----: | ---------------------------- | -| `TXPARAM_RECENT_ROOT_REFERENCE_COUNT` | `0x11` | `len(recent_root_references)` | +```text +signature_verification_cost + + sum( + frame.limits.execution + for frame in validation_prefix + if frame is not recent_root_verify + ) + <= MAX_VERIFY_GAS +``` -`TXPARAM(TXPARAM_RECENT_ROOT_REFERENCE_COUNT)` costs the standard `TXPARAM` gas. +This exclusion changes only public mempool admission accounting. The frame's declared execution limit remains part of the EIP-8141 transaction maximum cost and block execution gas reservation, and the payer is charged for the frame's actual gas use under the ordinary EIP-8141 rules. -`RECENTROOTREFLOAD` pops two stack items: +Clients MUST reject a transaction from the public mempool if any tuple has `slot >= current_slot`. + +Clients MUST evict a pending transaction if any tuple has `slot >= current_slot`. Otherwise, they MUST evict it when any tuple reaches: ```text -field -index +current_slot - slot >= RECENT_ROOT_LENGTH ``` -where `field` is the top stack item and `index` is the second stack item. It pushes one word from `recent_root_references[index]`: +To avoid admitting a transaction that expires almost immediately, clients SHOULD NOT admit a transaction whose oldest reference is within a margin chosen by the node. -| `field` | Return value | -| ------: | ------------------------------------------ | -| `0` | `source_id` | -| `1` | `slot`, as a zero-extended 256-bit integer | -| `2` | `root` | +On admission, clients MUST record each distinct `(storage_key, entry_hash)` dependency and the first `current_slot` at which it expires. Clients SHOULD index pending transactions by these values so that a slot advance or chain reorganization does not require scanning unrelated transactions. Clients MUST also track the code and activation status of `RECENT_ROOT_ADDRESS` as dependencies. These records MUST be removed when the transaction leaves the public mempool. When one transaction replaces another, clients MUST replace these records atomically with the transaction. -`RECENTROOTREFLOAD` costs `RECENTROOTREFLOAD_GAS`. It MUST exceptional-halt if `index >= len(recent_root_references)` or `field > 2`. +When any recorded dependency may have changed, clients MUST recheck the complete age and storage predicates for each affected transaction and evict transactions that no longer satisfy them. Reorganization handling MUST consider state changes on both the removed and added branches, including a referenced entry removed by rollback without a replacement write. A change to the code or activation status of `RECENT_ROOT_ADDRESS` requires revalidation of every pending transaction containing a recent root verifier frame. The ordinary EIP-8141 revalidation rules continue to apply to all other dependencies. -`RECENTROOTREFLOAD` reads only fields from the signed transaction envelope. It does not read recent root contract storage. It MAY be used in any frame mode, including VERIFY frames. +### Activation -### Public mempool handling +This EIP MUST activate at or after EIP-8141 and EIP-7843. -Recent root storage is not exposed to validation code through EVM execution. During public mempool validation, recent root state may affect a transaction only through pre-execution reference checks and declared roots exposed by introspection. +For the first block in which this EIP is active, clients MUST initialize `RECENT_ROOT_ADDRESS` against the parent state before executing any transaction. -Nodes SHOULD admit a transaction to the public mempool only if all declared recent root references are valid against the node's current head. +If `RECENT_ROOT_ADDRESS` does not exist, clients MUST create it with balance 0, nonce 1, code `RECENT_ROOT_CODE`, and empty storage. -Nodes SHOULD NOT admit a transaction while any reference has `slot >= current_slot`. Nodes SHOULD evict a transaction with any reference where `current_slot - slot >= RECENT_ROOT_LENGTH`, so that expired transactions do not accumulate. +If `RECENT_ROOT_ADDRESS` already exists with empty code and empty storage, clients MUST set its code to `RECENT_ROOT_CODE`, set its nonce to `max(existing_nonce, 1)`, preserve its balance, and leave storage empty. -To avoid admitting a transaction that expires almost immediately, nodes SHOULD NOT admit a transaction whose oldest reference is within a small margin of expiry, i.e. where `current_slot - slot >= RECENT_ROOT_LENGTH - m` for a node-chosen margin `m` of a few slots. +The fork configuration MUST choose a `RECENT_ROOT_ADDRESS` with empty code and empty storage in the parent state of the first active block. If this condition is false, the first active block is invalid. -To keep expiry and reorg eviction bounded without restricting application concurrency, public mempool nodes SHOULD index pending transactions by declared reference and by expiry slot, so that the transactions affected by a slot advance or a reorg can be found and evicted efficiently. +For all other blocks, clients MUST NOT run this initialization. Clients MUST handle reorganizations across activation by applying or undoing this transition according to the canonical chain. -Nodes SHOULD recheck pending transactions with recent root references when the head changes, when the node's current slot advances, or after any reorg that may affect referenced entries. +## Rationale -### Activation +[EIP-8141](./eip-8141.md) public mempool validation normally rejects reads from shared mutable storage. Recent root storage is a narrow exception because a valid entry cannot be overwritten during its usable window. A write in the current slot uses the only ring buffer index that could collide with a root exactly `RECENT_ROOT_LENGTH` slots old, and that older root is already expired. -This EIP MUST activate at or after [EIP-8141](./eip-8141.md). +### Canonical frame instead of an envelope field -If `timestamp < FORK_TIMESTAMP`, clients MUST NOT insert `recent_root_references` and MUST NOT apply recent root logic. +The canonical frame reuses EIP-8141's transaction encoding, signature hash, execution semantics, gas accounting, and frame introspection. An envelope field would require a new transaction schema, native checks before frame execution, special gas and warming rules, and a new introspection opcode. -If `timestamp >= FORK_TIMESTAMP`, clients MUST insert `recent_root_references` and MUST apply recent root logic. +A contract and an informal transaction pool convention are not enough. EIP-8141 has a closed public mempool validation prefix grammar and normally bans both `SLOTNUM` and reads from storage outside `tx.sender`. The protocol must define the frame's shape, position, work bound, allowed storage reads, and revalidation rules. -For every block `B` with `B.timestamp >= FORK_TIMESTAMP` whose parent has `parent.timestamp < FORK_TIMESTAMP`, clients MUST initialize `RECENT_ROOT_ADDRESS` against `B`'s parent state before executing any transaction in `B`. +### Full tuples in frame data -If `RECENT_ROOT_ADDRESS` does not exist, clients MUST create it with balance 0, nonce 1, code `RECENT_ROOT_CODE`, and empty storage. +Including `(source_id, slot, root)` in the frame lets clients derive every storage dependency without executing application validation. After a reorganization, a client can identify and evict transactions that reference an orphaned root. -If `RECENT_ROOT_ADDRESS` already exists with empty code and empty storage, clients MUST set its code to `RECENT_ROOT_CODE`, set its nonce to `max(existing_nonce, 1)`, preserve its balance, and leave storage empty. +The packed encoding uses 72 bytes per tuple and has no redundant count or selector. Its valid lengths do not collide with the write operation, which uses 64 bytes. -The fork configuration MUST choose a `RECENT_ROOT_ADDRESS` with empty code and empty storage in the parent state of the first post-fork payload. If this condition is false at activation, the payload is invalid. +The stored entry commits to `source_id`, `slot`, and `root`. This prevents a root from another source or a stale occupant of the same ring buffer index from satisfying the tuple. -For all other blocks, clients MUST NOT run this initialization. Clients MUST handle reorgs across the fork boundary by applying or undoing this transition according to the canonical chain. +### Fixed early position -Pre-fork frame transactions and authorizations bound to the pre-fork canonical signature hash do not survive the boundary and MUST be evicted from mempools and regenerated. +The recent root verifier frame runs after the optional expiry check and before deployment or account validation. This order rejects invalid or orphaned roots before more expensive application validation runs and lets every dependent validation frame inspect a successful frame. -## Rationale +The fixed position affects public mempool eligibility, not block validity. Privately submitted transactions remain subject to ordinary EIP-8141 execution, including its rule that a failed `VERIFY` frame invalidates the transaction. -[EIP-8141](./eip-8141.md) validation needs inputs that are known before validation starts. General reads from storage controlled by another account or application are unsafe for the public mempool because one mutable cell can invalidate many pending transactions. +### Public mempool validation budget -Recent root references provide a narrow exception. The root is declared in the signed transaction envelope, checked by clients before validation code runs, and exposed to validation code only through introspection. Recent root references are intentionally narrow: each reference names one recent `bytes32` root and one system-contract storage key. +Checking sixteen distinct roots requires sixteen cold storage reads in the worst case. Counting this work toward `MAX_VERIFY_GAS` would leave less gas for the application's own validation. -### Entry binding +`MAX_VERIFY_GAS` excludes the recent root verifier frame. A transaction eligible for the public mempool can contain only one such frame, its data can contain at most sixteen tuples, and its target code must equal `RECENT_ROOT_CODE`. These rules limit the work without a second gas counter. -Each stored entry commits to the root source, slot, and root. This prevents an old root at the same array index, or a root from another root source, from satisfying a reference. +Direct evaluation does not change the frame's execution gas or fees. The frame's declared execution limit still contributes to the transaction's maximum cost and block gas reservation, and the payer still pays for the gas used. ### Window choice -References are limited to slots strictly before `current_slot`. During slot `S`, writes update index `S mod RECENT_ROOT_LENGTH`, but references to `S` are invalid and references old enough to share that index are expired. Current-slot writes therefore cannot invalidate currently valid references. +References are limited to slots strictly before `current_slot`. During slot `S`, writes update index `S mod RECENT_ROOT_LENGTH`, but references to `S` are invalid and references old enough to share that index are expired. Writes in the current slot cannot invalidate valid references. `RECENT_ROOT_LENGTH = 8192` gives `RECENT_ROOT_USABLE_WINDOW = 8191`, because the current slot is not referenceable. +For public mempool simulation, the latest canonical header slot plus one is the earliest possible slot for the next payload. A reference close to expiry may become stale if slots are skipped, so builders revalidate it against the exact payload slot before inclusion. + ### Implicit source creation -No creation transaction is required. A root source is created implicitly when a source address first writes with a new `(source_address, salt)` pair. Each root source has a bounded rolling window. Aggregate storage grows linearly with the number of written root sources. State growth is paid incrementally by the writes that create storage entries. +No creation transaction is required. A root source is created when a source address first writes with a new `(source_address, salt)` pair. Each root source has a bounded rolling window. Aggregate storage grows linearly with the number of written root sources, and writes that create storage entries pay the ordinary state growth cost. -### Bounded validation work +## Backwards Compatibility -For validation checks, each declared reference names exactly one storage key under `RECENT_ROOT_ADDRESS`. +This EIP does not change the EIP-8141 transaction payload or signature hash and does not modify other transaction types. + +The account at `RECENT_ROOT_ADDRESS` must have empty code and storage before activation. Calls to that address change behavior after activation because `RECENT_ROOT_CODE` is installed there. + +References to slots before activation are not satisfiable because recent root storage is empty at activation. + +## Test Cases + +Implementations should cover at least the following cases. Rejection from the public mempool does not by itself make a transaction invalid in a block. The contract and EIP-8141 columns state any separate block execution or static validity result. + +| Case | Contract or EIP-8141 result | Public mempool result | +| --- | --- | --- | +| One valid tuple from the previous slot | success | accept if all other EIP-8141 rules pass | +| Sixteen valid tuples with distinct cold storage keys | success when `limits.execution` is sufficient | accept if all other EIP-8141 rules pass | +| Duplicate valid tuples | success | accept | +| Wrong root, source, or slot | revert | reject | +| Tuple for the current or a future slot | revert | reject | +| Ages `8191` and `8192` slots | success, then revert | accept, then reject | +| Empty validation data, data of length 71 or 73 bytes, or data with seventeen tuples | revert | reject | +| Nonzero `VERIFY` value | statically invalid under EIP-8141 | reject | +| `flags == 1` | contract executes normally | reject; not `recent_root_verify` | +| `flags` contains `ATOMIC_BATCH_FLAG` | statically invalid under EIP-8141 | reject | +| Nonzero state gas limit | contract executes normally | reject; not `recent_root_verify` | +| More than one matching frame | each frame executes normally | reject | +| Matching frame after deployment or account validation | executes normally | reject | +| `limits.execution` is one gas less than the recent root verifier frame requires | halts exceptionally | reject | +| Generic declared limit plus signature cost equals `MAX_VERIFY_GAS`, with a valid recent root verifier frame | ordinary execution | accept | +| Generic declared limit plus signature cost exceeds `MAX_VERIFY_GAS` by one | ordinary execution if sufficiently funded | reject | +| Direct evaluation instead of EVM execution | identical frame receipt, gas use, and warm access sets | same admission result | +| Root removed by a reorganization | revert after revalidation | evict | +| Activation installs `RECENT_ROOT_CODE` with empty storage | subsequent writes and validation use `RECENT_ROOT_CODE` | revalidate; reject until every referenced entry exists | +| Activation finds empty code and storage with an existing balance and nonce | preserve balance and set nonce to `max(existing_nonce, 1)` | revalidate | +| Activation finds nonempty code or storage | first active block is invalid | not applicable | +| A later block follows successful activation | do not rerun initialization or clear storage | retain and revalidate normally | +| Reorganization crosses back before activation | this EIP is inactive | evict | + +The case with sixteen tuples must include the cold target account access charged at frame entry by EIP-8141, not only the contract's internal execution. + +Each of EIP-8141's four recognized validation prefix shapes must be tested with no protocol verifier, with `recent_root_verify`, with `expiry_verify`, and with `expiry_verify` followed by `recent_root_verify`. Reversed or duplicate protocol verifiers must be rejected from the public mempool. + +### Reference vector + +The following vector uses `current_slot = 2`: + +| Name | Value | +| --- | --- | +| `source_address` | `0x0000000000000000000000000000000000000001` | +| `salt` | `0x0000000000000000000000000000000000000000000000000000000000000000` | +| `source_id` | `0xb9382d35273c75a50631a3e84d3c75ec9266e2b18c35a627e16cdbf26a18ca85` | +| `slot` | `1`, encoded as `0x0000000000000001` | +| `root` | `0x0000000000000000000000000000000000000000000000000000000000000002` | +| `entry_hash` | `0x0a0d1254c851be5a133b4c9a9e300f5602fc0f43dbe65aa6a66930d4ca0a51b8` | +| `storage_key` | `0x5f027aa1cbe2df279bf6518edd4b44ea5409fd800189ec35224e10ab05e574c3` | + +The 72-byte validation calldata is: -`MAX_RECENT_ROOT_REFERENCES = 16` bounds pre-execution reference checks while covering the expected root set for privacy, wallet authorization, and historical state root use cases. +```text +0xb9382d35273c75a50631a3e84d3c75ec9266e2b18c35a627e16cdbf26a18ca8500000000000000010000000000000000000000000000000000000000000000000000000000000002 +``` -## Backwards Compatibility +With `storage[storage_key] = entry_hash`, the validation operation succeeds. -This EIP does not modify [EIP-7702](./eip-7702.md) or other transaction types. An [EIP-7702](./eip-7702.md)-delegated EOA may be a source address; `source_id` is derived from `msg.sender` of the write call. +## Security Considerations -References to slots before this EIP's activation are not satisfiable because recent root storage is empty at activation. +Consensus treats `root` as an opaque `bytes32`. Applications define what it commits to and should bind the expected `(source_id, slot, root)` tuple to the statement being authorized. Before treating frame data as verified, application validation should check the completed frame's defining fields and success status as described in [Application introspection](#application-introspection). A successful recent root verifier frame proves only that the tuple is currently stored and recent. -## Security Considerations +EIP-8141's canonical signature hash covers the frame data, but EIP-8141 also permits validation based on an explicit 32-byte signature message. Validation logic that does not authorize the canonical signature hash must bind the recent root verifier frame and every other relevant frame itself. -Consensus treats `root` as an opaque `bytes32`. Applications define what it commits to and MUST bind the expected `source_id`, slot window, and root in their validation logic. A privacy proof using root `R` should include `(source_id, slot, R)` or an application-specific commitment to those fields as a public input, and validation logic MUST check the same tuple through `RECENTROOTREFLOAD`. +For each `(source_id, slot)`, only the last root written in canonical execution order is referenceable. An application that needs multiple roots from the same source and slot should write an aggregate commitment. -Each `(source_id, slot)` has one referenceable root on the canonical chain. The referenceable root is last-write-wins according to canonical execution order and is finalized with the containing block. An application that needs multiple roots from the same slot SHOULD write an aggregate commitment. +This EIP does not guarantee inclusion of root writes. Applications that rely on timely publication need their own publication path, redundant root sources, or an inclusion policy for write transactions. -This EIP does not guarantee inclusion of root writes. Applications that rely on timely root publication need their own publication path, redundant root sources, or an inclusion policy for write transactions. +The same `(source_address, salt)` pair produces the same `source_id` on different chains, but each chain maintains its own recent root state. Proofs, bridge messages, and attestations produced off chain that carry recent root references must bind the intended chain domain outside the tuple. -The same `(source_address, salt)` pair produces the same `source_id` on different chains, but each chain maintains independent recent root state. Proofs, bridge messages, and offchain attestations that carry recent root references MUST bind the intended chain domain outside the tuple. +Recent roots create persistent storage under `RECENT_ROOT_ADDRESS`. Existing root sources overwrite at most `RECENT_ROOT_LENGTH` cells, while new root sources create additional cells. A future proposal may add a source registration cost or a surcharge for the first write if ordinary state growth pricing is insufficient. -Recent roots create ordinary persistent storage under `RECENT_ROOT_ADDRESS`. Existing root sources overwrite at most `RECENT_ROOT_LENGTH` cells, while new root sources create additional cells. The natural pricing point for aggregate state growth is source creation, not recurring writes. Future versions MAY add a one-time source registration cost or first-write surcharge for each new `source_id`. +The recent root verifier frame can add up to sixteen root checks beyond `MAX_VERIFY_GAS`. An invalid final tuple or a frame with slightly too little execution gas can force almost all of this work before the transaction is rejected, without paying an inclusion fee. Implementations should account for this work in limits on invalid transactions from each peer and in limits on total transaction pool work. A reorganization or synchronized expiry may evict many transactions that use one popular root, but dependency indexing lets clients identify affected transactions without rerunning unrelated validation prefixes. ## Copyright From 65d177d431638639d35897d6e869301a5ea8a02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Fri, 4 Sep 2026 13:16:08 +0200 Subject: [PATCH 2/7] Update EIP-8037: restore a frame's state-gas on rollback Merged by EIP-Bot. --- EIPS/eip-8037.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/EIPS/eip-8037.md b/EIPS/eip-8037.md index bf81d7db709b5f..80e29ecea5e8b6 100644 --- a/EIPS/eip-8037.md +++ b/EIPS/eip-8037.md @@ -102,6 +102,7 @@ This means that the `state_gas_reservoir` holds gas that exceeds [EIP-7825](./ei - `evm_execution_gas_used` encodes the total execution-gas used by the transaction, and it is initialized as `evm_execution_gas_used = 0`. - `evm_state_gas_used` encodes the total state-gas used by the transaction, and it is initialized as `evm_state_gas_used = 0`. - `state_gas_from_gas_left` encodes how much of the current frame's `gas_left` has been spent on state-gas charges (i.e., the state-gas not covered by the reservoir). Like `gas_left`, it is frame-local: it is initialized to `0` at the start of each call frame and is used to refill state-gas in last-in, first-out (LIFO) order. +- `state_gas_baseline` records the value of `state_gas_reservoir` at the start of the state changes a frame's rollback undoes, which for a call frame is its entry. Like `gas_left`, it is frame-local. The gas counters operate as follows: @@ -148,7 +149,7 @@ State-gas accounting for `CALL*` and `CREATE`/`CREATE2` operations is performed The code-deposit portion (`L × CPSB`) is unaffected by this rule and is charged at code-deposit time as usual. -The same conditional rule applies to top-level contract-creation transactions. Under [EIP-2780](./eip-2780.md) the `STATE_BYTES_PER_NEW_ACCOUNT × CPSB` account-creation charge is not part of intrinsic gas and is not reserved up front; it is applied as a runtime charge in the pre-execution phase, when transaction setup accesses the deployment address (an access [EIP-7928](./eip-7928.md) always records), only if the destination does not already exist. Like any other state charge, it draws from `state_gas_reservoir` first and from `gas_left` once the reservoir is exhausted. If the transaction reverts or halts (including an address collision), any charged portion is refilled by the rules below, and the unspent reservoir is returned to the sender by the normal end-of-transaction settlement. +The same conditional rule applies to top-level contract-creation transactions. Under [EIP-2780](./eip-2780.md) the `STATE_BYTES_PER_NEW_ACCOUNT × CPSB` account-creation charge is not part of intrinsic gas and is not reserved up front; it is applied as a runtime charge in the pre-execution phase, when transaction setup accesses the deployment address (an access [EIP-7928](./eip-7928.md) always records), only if the destination does not already exist. Like any other state charge, it draws from `state_gas_reservoir` first and from `gas_left` once the reservoir is exhausted. If the transaction reverts or halts (including an address collision), any charged portion is undone by the frame's rollback, and the unspent reservoir is returned to the sender by the normal end-of-transaction settlement. Charges for account creation with `SELFDESTRUCT` are charged at the point where it executes. @@ -160,15 +161,25 @@ For an account that existed before the transaction, `SELFDESTRUCT` only transfer ##### Gas accounting for halts and reverts -A call frame's state changes are rolled back on both reverts and exceptional halts, so in both cases the state-gas charged within the frame is refilled in LIFO order (see the reservoir model above). The two cases differ only in how the frame's `gas_left` is treated, exactly as in existing EVM semantics. +A call frame's state changes are rolled back on both reverts and exceptional halts. In both cases the frame's state-gas is restored to its baseline rather than refilled: + +```python +net_state_gas = state_gas_baseline - state_gas_reservoir + state_gas_from_gas_left +evm_state_gas_used -= net_state_gas +gas_left += state_gas_from_gas_left +state_gas_from_gas_left = 0 +state_gas_reservoir = state_gas_baseline +``` + +`net_state_gas` is negative for a frame that refilled more than it charged, so `evm_state_gas_used` increases: the rollback also restores the state whose removal the refill credited. The restore assigns `state_gas_reservoir` where a refill credits it, so it is applied before any refill its parent makes for the failed operation. The baseline and frame entry coincide for every frame but the top-level one, whose rollback leaves the [EIP-7702](./eip-7702.md) authorizations applied in the pre-execution phase in place ([EIP-2780](./eip-2780.md)): its baseline sits after their charges and before the account-creation charge for the transaction's target, which the rollback does undo. The two cases differ only in how the frame's `gas_left` is treated, exactly as in existing EVM semantics. When a child frame **succeeds**, its remaining `gas_left` is returned to the parent and its `state_gas_from_gas_left` is added to the parent's `state_gas_from_gas_left` (the state-gas it funded from `gas_left` persists and is now backed by the merged `gas_left`). -When a child frame **reverts**, all of its state changes are rolled back and the state-gas charged within the frame is refilled: credited to the child's `gas_left` up to `state_gas_from_gas_left`, with any remainder credited to `state_gas_reservoir`. The child's resulting `gas_left` (including the refilled portion) is then returned to the parent. `evm_state_gas_used` is decreased by the refilled amount, while `evm_execution_gas_used` is updated according to the execution gas consumed by the child frame before the revert. +When a child frame **reverts**, all of its state changes are rolled back, its state-gas is restored as above, and its resulting `gas_left` (including the credited `state_gas_from_gas_left`) is returned to the parent. `evm_execution_gas_used` is updated according to the execution gas consumed by the child frame before the revert. -When a child frame **halts exceptionally**, the same refill is applied, but the child's `gas_left` is consumed (set to zero) rather than returned. Because the portion refilled to `gas_left` is part of the consumed `gas_left`, only the portion refilled to `state_gas_reservoir` survives — which is exactly the reservoir's value at the start of the child frame. No separate reservoir-reset rule is therefore required: refilling in LIFO order makes the reservoir whole automatically, while the `gas_left` the parent forwarded to the child is consumed in full as usual. +When a child frame **halts exceptionally**, the same restore is applied, but the child's `gas_left` is consumed (set to zero) rather than returned, so the credited `state_gas_from_gas_left` is consumed with it. -The same rules apply when the top-level call frame reverts or halts, treating the frame as a child of the transaction boundary. In particular, for address collisions in contract-creation transactions, `gas_left` is consumed in full and `evm_execution_gas_used` is incremented by the initial `gas_left`; any account-creation state-gas charged for the destination (possible only for a storage-only collision, since a destination with nonce, code, or balance is existent) is refilled by the rules above, and the `state_gas_reservoir` is returned to the sender by the normal end-of-transaction settlement. +The same rules apply when the top-level call frame reverts or halts, treating the frame as a child of the transaction boundary. In particular, for address collisions in contract-creation transactions, `gas_left` is consumed in full and `evm_execution_gas_used` is incremented by the initial `gas_left`; any account-creation state-gas charged for the destination (possible only for a storage-only collision, since a destination with nonce, code, or balance is existent) is undone by the frame's rollback, and the `state_gas_reservoir` is returned to the sender by the normal end-of-transaction settlement. ##### Gas accounting for [EIP-7702](./eip-7702.md) authorizations @@ -374,7 +385,7 @@ To solve this issue, we only apply this gas limit to execution gas, not state ga However, we cannot statically enforce an execution gas consumption of `TX_MAX_GAS_LIMIT`, while still allowing a higher state gas consumption, because transactions only have a single gas limit parameter, `tx.gas`. This is solved through a **reservoir model**: at transaction start, `evm_gas` is split into `gas_left` (capped at `TX_MAX_GAS_LIMIT - intrinsic_gas`) and a `state_gas_reservoir` (the overflow). State gas charges draw from the reservoir first, then from `gas_left` when the reservoir is empty. Execution gas charges draw from `gas_left` only. Exceeding the execution gas budget behaves identically to running out of gas — no special error is needed. -State-gas refills are applied in last-in, first-out (LIFO) order: because charges draw from the reservoir first and from `gas_left` last, refills credit `gas_left` first (up to the gas it borrowed, tracked by the frame-local `state_gas_from_gas_left` counter), then the reservoir. Undoing a state creation therefore restores the exact pools the charge drew from, so the two pools never drift into one another — `gas_left` is never inflated beyond `TX_MAX_GAS_LIMIT - intrinsic_gas`, and execution gas is never permanently stranded in the state-only reservoir. This LIFO refill also subsumes the exceptional-halt case: refilling into a child's `gas_left` and then consuming that `gas_left` leaves the reservoir at exactly its start-of-frame value, so no dedicated reservoir-reset rule is needed (see [Gas accounting for halts and reverts](#gas-accounting-for-halts-and-reverts)). +State-gas refills are applied in last-in, first-out (LIFO) order: because charges draw from the reservoir first and from `gas_left` last, refills credit `gas_left` first (up to the gas it borrowed, tracked by the frame-local `state_gas_from_gas_left` counter), then the reservoir. Undoing a state creation therefore restores the exact pools the charge drew from, so the two pools never drift into one another — `gas_left` is never inflated beyond `TX_MAX_GAS_LIMIT - intrinsic_gas`, and execution gas is never permanently stranded in the state-only reservoir. LIFO order alone does not make a frame's rollback correct: because the original value in the `SSTORE` table above is the value at transaction start rather than at frame entry, a frame may credit a refill it never funded. Rollback is therefore specified as a restore to the frame's baseline (see [Gas accounting for halts and reverts](#gas-accounting-for-halts-and-reverts)). #### Higher throughput From 58596a9911b945fc855caa80a904c5b3f4c4f254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Fri, 4 Sep 2026 13:21:02 +0200 Subject: [PATCH 3/7] Update EIP-8037: remove the duplicated calldata floor note Merged by EIP-Bot. --- EIPS/eip-8037.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/EIPS/eip-8037.md b/EIPS/eip-8037.md index 80e29ecea5e8b6..03dda821afa592 100644 --- a/EIPS/eip-8037.md +++ b/EIPS/eip-8037.md @@ -220,8 +220,6 @@ Since all state-gas charges are now applied as runtime or execution-time charges The [EIP-7623](./eip-7623.md) calldata floor participates in block accounting through the `max` term, applied to the execution-gas dimension: calldata is an execution-gas resource, and the floor's bound on worst-case block size holds only if a data-heavy transaction contributes at least the floor to the block's gas accounting. The floor is compared against the transaction's execution-gas portion alone — not the transaction total — so execution gas may absorb the floor exactly as under [EIP-7623](./eip-7623.md), while state-gas spending cannot (see [Calldata floor in block accounting](#calldata-floor-in-block-accounting)). -Note: `tx_gas_used` applies the [EIP-7623](./eip-7623.md) calldata floor after refunds, so the floor can effectively negate part of a refund when `calldata_floor_gas_cost > tx_gas_used_after_refund`. The receipt `cumulative_gas_used` uses this post-floor value. - The block header `gas_used` field is set to: ```python From c406bdb2ffbf2fbf9d85698e97b3b2e8b6e79112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Fri, 4 Sep 2026 13:22:38 +0200 Subject: [PATCH 4/7] Update EIP-8037: correct the general state-gas charge timing Merged by EIP-Bot. --- EIPS/eip-8037.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-8037.md b/EIPS/eip-8037.md index 03dda821afa592..0e3ee284334054 100644 --- a/EIPS/eip-8037.md +++ b/EIPS/eip-8037.md @@ -187,7 +187,7 @@ Gas charges for [EIP-7702](./eip-7702.md) authorizations follow the specificatio #### Pre-state and post-state gas validation -[EIP-7928](./eip-7928.md) defines two-phase gas validation for state-accessing opcodes: pre-state costs (determinable without state access) and post-state costs (requiring state access). Under this EIP, the execution-gas portion of these opcodes follows the [EIP-7928](./eip-7928.md) rules unchanged and is charged at opcode time against `gas_left`. The state-gas portion is also charged at the opcode level; it is computed and deducted at the end of the opcode execution, drawing first from `state_gas_reservoir` and then from `gas_left`. +[EIP-7928](./eip-7928.md) defines two-phase gas validation for state-accessing opcodes: pre-state costs (determinable without state access) and post-state costs (requiring state access). Under this EIP, the execution-gas portion of these opcodes follows the [EIP-7928](./eip-7928.md) rules unchanged and is charged at opcode time against `gas_left`. The state-gas portion is also charged at the opcode level, with the timing given for each opcode above. For `SSTORE`, the `GAS_CALL_STIPEND` pre-state check ([EIP-7928](./eip-7928.md)) applies to `gas_left` only, excluding the `state_gas_reservoir`. From fe87e741f2ebf1d73a808fe5f245481740a77736 Mon Sep 17 00:00:00 2001 From: soispoke <66172107+soispoke@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:40:52 +0200 Subject: [PATCH 5/7] Update EIP-8272: Add lightclient as a co-author Merged by EIP-Bot. --- EIPS/eip-8272.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-8272.md b/EIPS/eip-8272.md index e80518b56e265a..dcc2f4d847b91b 100644 --- a/EIPS/eip-8272.md +++ b/EIPS/eip-8272.md @@ -1,8 +1,8 @@ --- eip: 8272 title: Recent Roots for Frame Transactions -description: Frame transactions can verify recent roots through a canonical frame -author: Thomas Thiery (@soispoke), Vitalik Buterin (@vbuterin), Toni Wahrstätter (@nerolation) +description: Frame transactions can declare verified recent roots +author: Thomas Thiery (@soispoke), Vitalik Buterin (@vbuterin), Toni Wahrstätter (@nerolation), lightclient (@lightclient) discussions-to: https://ethereum-magicians.org/t/eip-8272-recent-roots-for-frame-transactions/28621 status: Draft type: Standards Track From 3ff5cff17a11ab31273ee14ebea0df8bbc4f4aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Fri, 4 Sep 2026 13:46:11 +0200 Subject: [PATCH 6/7] Update EIP-8037: return state-gas to gas_left when a child merges Merged by EIP-Bot. --- EIPS/eip-8037.md | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-8037.md b/EIPS/eip-8037.md index 0e3ee284334054..7737c6eb3c3f03 100644 --- a/EIPS/eip-8037.md +++ b/EIPS/eip-8037.md @@ -101,7 +101,7 @@ This means that the `state_gas_reservoir` holds gas that exceeds [EIP-7825](./ei - `evm_execution_gas_used` encodes the total execution-gas used by the transaction, and it is initialized as `evm_execution_gas_used = 0`. - `evm_state_gas_used` encodes the total state-gas used by the transaction, and it is initialized as `evm_state_gas_used = 0`. -- `state_gas_from_gas_left` encodes how much of the current frame's `gas_left` has been spent on state-gas charges (i.e., the state-gas not covered by the reservoir). Like `gas_left`, it is frame-local: it is initialized to `0` at the start of each call frame and is used to refill state-gas in last-in, first-out (LIFO) order. +- `state_gas_from_gas_left` encodes how much of the current frame's `gas_left` has been drawn for state-gas charges and not yet credited back (i.e., the state-gas not covered by the reservoir). Like `gas_left`, it is frame-local: it is initialized to `0` at the start of each call frame and is used to refill state-gas in last-in, first-out (LIFO) order. - `state_gas_baseline` records the value of `state_gas_reservoir` at the start of the state changes a frame's rollback undoes, which for a call frame is its entry. Like `gas_left`, it is frame-local. The gas counters operate as follows: @@ -109,6 +109,7 @@ The gas counters operate as follows: - Execution-gas charges deduct from `gas_left` only and increment `evm_execution_gas_used`. - State-gas charges deduct from `state_gas_reservoir` first. When the reservoir is exhausted, the remainder deducts from `gas_left` and increments `state_gas_from_gas_left` by that remainder. State-gas charges increment `evm_state_gas_used`. - If a state creation is undone during execution, the corresponding amount of state-gas is **refilled in last-in, first-out (LIFO) order**: because charges deduct from `state_gas_reservoir` first and from `gas_left` last, refills credit the pool charged last (`gas_left`) first. It is credited to `gas_left` first (decrementing `state_gas_from_gas_left` by the same amount) up to the current value of `state_gas_from_gas_left`, and any remainder is credited to `state_gas_reservoir`. State-gas refills decrement `evm_state_gas_used`. Throughout this specification, "refilling state-gas" refers to this LIFO operation. +- `state_gas_reservoir` is passed to a child frame in full: the 63/64 rule applies to `gas_left` only. The parent keeps none of it while the child runs. On the child's exit it becomes the parent's again. Note: a refill in the child may have raised it above the amount passed in. - The `GAS` opcode returns `gas_left` only (excluding the reservoir). - State-gas is metered at the end of all state-mutating opcodes, call-frame boundaries (in case of exceptional halts and reverts) and at the end of the transaction. @@ -173,7 +174,20 @@ state_gas_reservoir = state_gas_baseline `net_state_gas` is negative for a frame that refilled more than it charged, so `evm_state_gas_used` increases: the rollback also restores the state whose removal the refill credited. The restore assigns `state_gas_reservoir` where a refill credits it, so it is applied before any refill its parent makes for the failed operation. The baseline and frame entry coincide for every frame but the top-level one, whose rollback leaves the [EIP-7702](./eip-7702.md) authorizations applied in the pre-execution phase in place ([EIP-2780](./eip-2780.md)): its baseline sits after their charges and before the account-creation charge for the transaction's target, which the rollback does undo. The two cases differ only in how the frame's `gas_left` is treated, exactly as in existing EVM semantics. -When a child frame **succeeds**, its remaining `gas_left` is returned to the parent and its `state_gas_from_gas_left` is added to the parent's `state_gas_from_gas_left` (the state-gas it funded from `gas_left` persists and is now backed by the merged `gas_left`). +When a child frame **succeeds**, its remaining `gas_left` is returned to the parent and its `state_gas_from_gas_left` is added to the parent's `state_gas_from_gas_left` (the state-gas it funded from `gas_left` persists and is now backed by the merged `gas_left`). Then, with the child's `state_gas_reservoir` merged in, the frame returns state-gas to `gas_left`, up to the amount it has outstanding: + +```python +d = min(state_gas_reservoir, state_gas_from_gas_left) +gas_left += d +state_gas_reservoir -= d +state_gas_from_gas_left -= d +``` + +Note: this undoes no state creation, so `evm_state_gas_used` is unchanged. It only moves gas between the two pools. + +This step is needed because a refill may happen in a different frame than the matching charge. The original value in the `SSTORE` table above is the value at transaction start, not at frame entry. A frame may therefore clear a slot allocated by an earlier frame. Its `state_gas_from_gas_left` may be smaller than the refill, and the rest goes to `state_gas_reservoir`, although the charge drew from `gas_left`. The step moves it up on every merge, to the first frame with an outstanding `state_gas_from_gas_left`. If it reaches the top-level frame, it stays in `state_gas_reservoir` and is returned to the sender by the end-of-transaction settlement. + +The step applies to successful children only. This requires that a frame that reverts or halts exceptionally returns no state-gas to its parent; the restore to the frame's baseline above provides that. Otherwise the parent receives state-gas for a slot the rollback restored, and a later merge moves it to `gas_left`. When a child frame **reverts**, all of its state changes are rolled back, its state-gas is restored as above, and its resulting `gas_left` (including the credited `state_gas_from_gas_left`) is returned to the parent. `evm_execution_gas_used` is updated according to the execution gas consumed by the child frame before the revert. @@ -383,7 +397,7 @@ To solve this issue, we only apply this gas limit to execution gas, not state ga However, we cannot statically enforce an execution gas consumption of `TX_MAX_GAS_LIMIT`, while still allowing a higher state gas consumption, because transactions only have a single gas limit parameter, `tx.gas`. This is solved through a **reservoir model**: at transaction start, `evm_gas` is split into `gas_left` (capped at `TX_MAX_GAS_LIMIT - intrinsic_gas`) and a `state_gas_reservoir` (the overflow). State gas charges draw from the reservoir first, then from `gas_left` when the reservoir is empty. Execution gas charges draw from `gas_left` only. Exceeding the execution gas budget behaves identically to running out of gas — no special error is needed. -State-gas refills are applied in last-in, first-out (LIFO) order: because charges draw from the reservoir first and from `gas_left` last, refills credit `gas_left` first (up to the gas it borrowed, tracked by the frame-local `state_gas_from_gas_left` counter), then the reservoir. Undoing a state creation therefore restores the exact pools the charge drew from, so the two pools never drift into one another — `gas_left` is never inflated beyond `TX_MAX_GAS_LIMIT - intrinsic_gas`, and execution gas is never permanently stranded in the state-only reservoir. LIFO order alone does not make a frame's rollback correct: because the original value in the `SSTORE` table above is the value at transaction start rather than at frame entry, a frame may credit a refill it never funded. Rollback is therefore specified as a restore to the frame's baseline (see [Gas accounting for halts and reverts](#gas-accounting-for-halts-and-reverts)). +State-gas refills are applied in last-in, first-out (LIFO) order: because charges draw from the reservoir first and from `gas_left` last, refills credit `gas_left` first (up to the gas it borrowed, tracked by the frame-local `state_gas_from_gas_left` counter), then the reservoir. Undoing a state creation therefore restores the exact pools the charge drew from, as long as both happen in the same frame. Because the original value in the `SSTORE` table above is the value at transaction start rather than at frame entry, a refill may land in a frame other than the one that was charged; the counter is frame-local, so the gas then stays in the reservoir while the `gas_left` that funded the charge stays reduced. Returning it to `gas_left` on the merge keeps the two pools from drifting into one another: `gas_left` is never inflated beyond `TX_MAX_GAS_LIMIT - intrinsic_gas`, and state-gas is not left in the reservoir while a frame is still owed it. The same mismatch is why LIFO order alone does not make a frame's rollback correct: a frame may credit a refill it never funded, so rollback is specified as a restore to the frame's baseline (see [Gas accounting for halts and reverts](#gas-accounting-for-halts-and-reverts)). #### Higher throughput @@ -431,6 +445,8 @@ Wallet developers and node operators MUST update gas estimation handling to acco Users can maintain their usual workflows without modification, as wallet and RPC updates will handle these changes. +Contracts and tools measuring work as the difference between two `GAS` reads can get a negative result. This EIP makes it possible both within a frame and across a call (see [`gas_left` can increase](#gas_left-can-increase)). Contracts already deployed with such metering cannot be fixed. + ### Deterministic deployment factories Some well-known deterministic deployment factories are deployed via pre-signed transactions with a hardcoded gas limit. The classic Nick's method factory, for instance, uses a pre-signed transaction with a 100,000 gas limit. Under this EIP, the state creation cost of deploying these factories increases beyond their hardcoded gas limit, so the original pre-signed deployment transactions will no longer execute successfully. The same applies to other deterministic deployer contracts such as `SingletonFactory`, `Create2Deployer`, `ImmutableCreate2Factory`, and `CreateX`. @@ -463,6 +479,16 @@ One potential concern is the cost of creating a new account (`STATE_BYTES_PER_NE Optimal block construction becomes more complex, since builders must now balance resource usage across multiple dimensions rather than a single gas metric. Sophisticated builders may gain an advantage by applying advanced optimization techniques, raising concerns about further builder centralization. However, practical heuristics (e.g., greedily filling blocks until one resource dimension saturates) remain effective for most use cases. These heuristics limit centralization pressure by keeping block construction feasible for local and less sophisticated builders. +### `gas_left` can increase + +Refills credit `gas_left` before `state_gas_reservoir`, so a frame's `gas_left` can be higher after an operation than before it. A contract measuring work as the difference between two `GAS` reads then gets a negative value. This reverts under checked arithmetic and wraps under unchecked arithmetic. + +A negative result requires the charge to have drawn from `gas_left`, i.e. the reservoir to be empty. That is the case for every transaction with `tx.gas` up to `TX_MAX_GAS_LIMIT`. + +This is already possible in a single frame. Allocating a storage slot that was zero at transaction start and clearing it again refills `STATE_BYTES_PER_STORAGE_SET × CPSB` to `gas_left`. `SSTORE(k, 1); GAS; SSTORE(k, 0); GAS` shows it: the two reads differ by the refill less the cost of the clear. Reads around both stores do not, because the charge and the refill cancel between them. + +Returning state-gas on a merge extends this across a call boundary, which no call does today. Metering a sub-call this way is the more common pattern. The amount is not limited to one slot: a merge returns up to the whole `state_gas_from_gas_left` the frame has accumulated. The bound is that `gas_left + state_gas_from_gas_left` never exceeds the frame's `gas_left` at entry. + ### Interaction with [ERC-4337](./eip-4337.md) bundles The `GAS` opcode returns `gas_left` only and cannot observe `state_gas_reservoir`. Bundling protocols that meter sub-call consumption via `gasleft()` deltas (e.g., the [ERC-4337](./eip-4337.md) `EntryPoint`) therefore cannot accurately attribute state-gas usage when it is funded by the reservoir. Because all user operations in a bundle share the transaction's reservoir, one user operation's state-gas refunds can subsidize another's state-gas charges without appearing in either's `gasleft()` delta. Bundlers and `EntryPoint` implementations MUST account for state-gas charges and refunds explicitly rather than relying on `gasleft()` differences alone. From 052029f3625328d6f51dec8e62a7090201e66f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Fri, 4 Sep 2026 14:01:07 +0200 Subject: [PATCH 7/7] Update EIP-8037: remove unused evm_execution_gas_used counter Merged by EIP-Bot. --- EIPS/eip-8037.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-8037.md b/EIPS/eip-8037.md index 7737c6eb3c3f03..e9b489e9b680ec 100644 --- a/EIPS/eip-8037.md +++ b/EIPS/eip-8037.md @@ -99,14 +99,13 @@ Because [EIP-2780](./eip-2780.md) makes intrinsic gas state-independent, no stat This means that the `state_gas_reservoir` holds gas that exceeds [EIP-7825](./eip-7825.md)'s budget. Additionally, three new counters are introduced: -- `evm_execution_gas_used` encodes the total execution-gas used by the transaction, and it is initialized as `evm_execution_gas_used = 0`. - `evm_state_gas_used` encodes the total state-gas used by the transaction, and it is initialized as `evm_state_gas_used = 0`. - `state_gas_from_gas_left` encodes how much of the current frame's `gas_left` has been drawn for state-gas charges and not yet credited back (i.e., the state-gas not covered by the reservoir). Like `gas_left`, it is frame-local: it is initialized to `0` at the start of each call frame and is used to refill state-gas in last-in, first-out (LIFO) order. - `state_gas_baseline` records the value of `state_gas_reservoir` at the start of the state changes a frame's rollback undoes, which for a call frame is its entry. Like `gas_left`, it is frame-local. The gas counters operate as follows: -- Execution-gas charges deduct from `gas_left` only and increment `evm_execution_gas_used`. +- Execution-gas charges deduct from `gas_left` only. - State-gas charges deduct from `state_gas_reservoir` first. When the reservoir is exhausted, the remainder deducts from `gas_left` and increments `state_gas_from_gas_left` by that remainder. State-gas charges increment `evm_state_gas_used`. - If a state creation is undone during execution, the corresponding amount of state-gas is **refilled in last-in, first-out (LIFO) order**: because charges deduct from `state_gas_reservoir` first and from `gas_left` last, refills credit the pool charged last (`gas_left`) first. It is credited to `gas_left` first (decrementing `state_gas_from_gas_left` by the same amount) up to the current value of `state_gas_from_gas_left`, and any remainder is credited to `state_gas_reservoir`. State-gas refills decrement `evm_state_gas_used`. Throughout this specification, "refilling state-gas" refers to this LIFO operation. - `state_gas_reservoir` is passed to a child frame in full: the 63/64 rule applies to `gas_left` only. The parent keeps none of it while the child runs. On the child's exit it becomes the parent's again. Note: a refill in the child may have raised it above the amount passed in. @@ -189,11 +188,11 @@ This step is needed because a refill may happen in a different frame than the ma The step applies to successful children only. This requires that a frame that reverts or halts exceptionally returns no state-gas to its parent; the restore to the frame's baseline above provides that. Otherwise the parent receives state-gas for a slot the rollback restored, and a later merge moves it to `gas_left`. -When a child frame **reverts**, all of its state changes are rolled back, its state-gas is restored as above, and its resulting `gas_left` (including the credited `state_gas_from_gas_left`) is returned to the parent. `evm_execution_gas_used` is updated according to the execution gas consumed by the child frame before the revert. +When a child frame **reverts**, all of its state changes are rolled back, its state-gas is restored as above, and its resulting `gas_left` (including the credited `state_gas_from_gas_left`) is returned to the parent. When a child frame **halts exceptionally**, the same restore is applied, but the child's `gas_left` is consumed (set to zero) rather than returned, so the credited `state_gas_from_gas_left` is consumed with it. -The same rules apply when the top-level call frame reverts or halts, treating the frame as a child of the transaction boundary. In particular, for address collisions in contract-creation transactions, `gas_left` is consumed in full and `evm_execution_gas_used` is incremented by the initial `gas_left`; any account-creation state-gas charged for the destination (possible only for a storage-only collision, since a destination with nonce, code, or balance is existent) is undone by the frame's rollback, and the `state_gas_reservoir` is returned to the sender by the normal end-of-transaction settlement. +The same rules apply when the top-level call frame reverts or halts, treating the frame as a child of the transaction boundary. In particular, for address collisions in contract-creation transactions, `gas_left` is consumed in full; any account-creation state-gas charged for the destination (possible only for a storage-only collision, since a destination with nonce, code, or balance is existent) is undone by the frame's rollback, and the `state_gas_reservoir` is returned to the sender by the normal end-of-transaction settlement. ##### Gas accounting for [EIP-7702](./eip-7702.md) authorizations