The Native Rollups Book
Table of Contents
- What are native rollups?
- Problem statement
- Two problems to solve
- Thesis: generalized proof verification
- Candidate designs
- How this book is organized
What are native rollups?
A native rollup is a new type of EVM-based rollup that directly makes use of Ethereum’s execution environment for its own state transitions, removing the need for complex and hard-to-maintain custom proof systems.
Problem statement
Governance risk
Today, EVM-based rollups need to make a trade-off between security and L1 equivalence. Everytime Ethereum forks, EVM-based rollups need to go through bespoke governance processes to upgrade the contracts and maintain equivalence with L1 features. A rollup governance system cannot be forced to follow Ethereum’s governance decisions, and thus is free to arbitrarily diverge from it. Because of this, the best that an EVM-based rollup that strives for L1 equivalence can do is to provide a long exit window for their users, to protect them from its governance going rogue.
Exit windows present themselves with yet another trade-off:
- They can be short and reduce the un-equivalence time for users, but reducing at the same time the cases in which the exit window is effective. All protocols that require long enough time delays (e.g. vesting contracts, staking contracts, timelocked governance) would not be protected by the exit window.
- They can be long to protect more cases, at the cost of increased un-equivalence time. It’s important to remember though that no finite (and reasonable) exit window length can protect all possible applications.
The only way to avoid governance risk today is to give up upgrades, remain immutable and accept that the rollup will increasingly diverge from L1 over time.
Bug risk
EVM-based rollups need to implement complex proof systems just to be able to support what Ethereum already provides on L1. Such proof systems, even though they are getting faster and cheaper over time, are still considered not safe to be used in production in a permissionless environment. Rollups today aim to reduce this problem by implementing multiple independent proof systems that need to agree before a state transition can be considered valid, which increases protocol costs and complexity.
Two problems to solve
Shipping native rollups requires solving two separate problems.
Reusing the L1 STF for L2s
Even when both are EVM chains, an L2 differs from L1 in important ways. It exchanges arbitrary messages with L1, accepts native token deposits from L1 and allows withdrawals back to it, does not support blob transactions but posts its own data as blobs, has no beacon chain attached to it, allows different sequencing strategies such as centralized sequencing, and may want to collect the base fee. At the same time, a native rollup is protected from bugs only if every bug in its program is also an L1 bug. Since Ethereum cannot afford to maintain two programs, L1 and L2 must run exactly the same one, and all L2-specific behavior has to live outside it, in the rollup contract.
This problem is largely solved. Reusing the L1 STF specifies how L2 blocks map onto L1’s stateless validation function, and how anchoring, messaging, deposits, fees, and forced transactions work without modifying it. Ethrex has merged a proof of concept, built on the earlier EXECUTE precompile. The remaining questions concern newer EIPs, such as Block-in-Blobs (EIP-8142), and how native rollups stay compatible as L1 upgrades.
Proof infrastructure
To verify L2 state transitions with the same infrastructure as L1, native rollups need:
- mandatory L1 execution proofs, so that L1 itself verifies its execution program through proofs;
- a way to attach proofs to transactions, either a new transaction type or a new frame mode; and
- a way to aggregate those proofs, since they are too large to include in blocks individually.
This book assumes a future L1 in which block proofs are mandatory and validators verify a single block proof instead of downloading and executing the full payload. The rest of this proof infrastructure does not exist yet, and it is the focus of Proof verification.
Thesis: generalized proof verification
Building this proof infrastructure only for native rollups is not worth it. Native rollups must reuse exactly the same STF as L1: no new transaction types, no precompiles, and no other VM customization. As of today, only Linea is trying to maintain exact L1 semantics in production.
The L1 zkEVM, however, plans to separate the program from the VM that proves it. From a verifier’s perspective, verifying a proof of the EVM program or of any other program is then essentially the same work. Allowing proofs of arbitrary programs to be verified supports every L2 STF, and every other ZK application as well: privacy protocols, ZK coprocessors, and more. These projects still maintain their own programs, but no longer their own proof verification infrastructure, which is arguably the hardest part.
In this world, a native rollup is just the minimal special case that uses the native program. The native rollups upgrade is massively de-risked, because most of its complexity moves into generalized proof verification, a feature that is useful on its own.
Candidate designs
Two proposals currently describe how proofs are delivered and aggregated:
- Proof-carrying transactions: a new transaction type with an ephemeral proof sidecar, which is discarded once the builder aggregates the proof, plus opcodes that expose the proof’s inputs. It follows the EIP-8025 specifications as closely as possible to maximize code reuse.
- EIP-8288, also known as zkzkframes: a new EIP-8141 frame mode instead of a new transaction type, with proof inputs exposed through frame introspection and proofs aggregated recursively in the mempool and by the builder.
The two are equivalent in practice, and this book prefers EIP-8288 because it builds on frame transactions. It still needs to be made concrete with respect to the existing zkEVM specifications, and it is an open question whether mempool nodes can realistically perform recursive proving. On top of EIP-8288, native rollups need one more piece: the EVM verification key registry (EIP-8357), which lets rollup contracts refer to the same program as L1.
How this book is organized
- Proof verification: the case for generalized proof verification, EIP-8288, and the EVM verification key registry.
- Reusing the L1 STF: the native rollup specification, and how L2-specific features work without modifying the L1 program.
- Beyond minimal native rollups: native rollups with extensions, and execution sharding.
Status and roadmap
Last updated: October 2026.
Table of Contents
- Where things stand
- L1 dependencies
- The case for native rollups
- Timeline
- Open question: upgrades with the zkEVM
- Next steps
Where things stand
- Reusing the L1 STF is largely specified in the Specification, and ethrex has merged a proof of concept of the earlier
EXECUTEprecompile design, already on Glamsterdam. The remaining issues are newer EIPs, such as Block-in-Blobs, and compatibility with future L1 upgrades. - Proof-carrying transactions, proposed in the Native proof verification post in May 2026, are the main alternative to EIP-8288. The book follows EIP-8288, as explained in the introduction.
- EIP-8288 (zkzkframes) was merged as a Draft on 9 September 2026. See EIP-8288 (zkzkframes).
- The reference implementation: the NativeRollup contract runs end to end on a local EIP-8141 devnet, with real L2 blocks, mock proofs, and the EIP-8357 registry in its genesis. A public explorer shows each L2 block and message, and what is still mocked.
- EIP-8357, the EVM verification key registry, was merged as a Draft on 14 October 2026, with a reference implementation in ethereum/sys-asm#56 and tests in ethereum/execution-specs#3466. See EIP-8357.
L1 dependencies
| Dependency | Needed for | Status |
|---|---|---|
| L1 stateless validation program | The program native rollups prove | In development in execution-specs projects/zkevm |
| Mandatory execution proofs | One L1 block proof that covers the L2 proofs | Optional proofs (EIP-8025) declined for Hegotá on 15 October 2026; mandatory in K* under the current strawmap ordering |
| EIP-8141 frame transactions | The transaction envelope for EIP-8288 dependencies | Scheduled for Hegotá |
Compact NewPayloadRequestHeader | Validation without full payloads, and the contract’s root computation | Removed from the optional-proof flow (consensus-specs#5076), assumed to return with mandatory proofs |
| EIP-8142 Block-in-Blobs | L2 block data in blobs | Declined for Hegotá on 21 September 2026 |
| EIP-7928 block-level access lists | Part of every proven payload | Scheduled for Glamsterdam |
| EIP-7997 deterministic factory | Deploying the EIP-8357 registry | Scheduled for Glamsterdam |
| EIP-7805 FOCIL | Forced transactions through L2 inclusion lists, once the L1 stateless validation program supports it | Scheduled for Hegotá |
| EIP-7864 binary state tree | Optional: cheaper storage proofs for messaging | Draft |
The case for native rollups
If there is no confidence that many existing rollups will become native, is the upgrade worth it? Several teams have expressed interest, including Gnosis, Celo, Linea, Taiko, and Scroll. Even so, native rollups are worth shipping only if the feature is kept minimal, so that its low complexity justifies it even if usage turns out to be low.
Top rollups such as Arbitrum and Optimism have instead expressed interest in an extensible native program, explored in Native rollups with extensions. A more ambitious direction is to use native rollups as a form of execution sharding.
Timeline
Mandatory proofs and a zkzkframes-like feature are both hard requirements. According to the strawmap, zkzkframes can go live in 2028-2029 at the earliest. The Ethereum Foundation’s protocol priorities post places mandatory proofs in K* under the current strawmap ordering, and notes that they may move back to L* if the strawmap swaps the two forks to accelerate post-quantum consensus.
Can anything ship earlier?
Probably not. The original proposal suggested shipping native rollups through re-execution instead of ZK proofs. Re-execution, however, only supports optimistic rollups with bisection games, which still require a long challenge period for withdrawals, something all rollups are trying to move away from.
Another suggested path is to ship native rollups and generalized proof verification before mandatory proofs, as a testing ground: if something fails, only the applications that chose to take that risk fail, not all of L1. Delaying mandatory proofs is not desirable, but this remains an option if there is not enough confidence to ship them at all.
Open question: upgrades with the zkEVM
How will upgrades work with the zkEVM? Suppose a bug is found: how do nodes upgrade? Today, most patches are backwards compatible until the bug is hit in production. With the zkEVM, the verification key needs to change, and old proofs can no longer be verified. What does the runbook for this scenario look like?
Next steps
- Make LeanSTARK dependencies work in EIP-8288: give them a place in the dependency digest, which is defined for LeanSPHINCS dependencies only, follow leanVM’s move to RISC-V until it can verify proofs of arbitrary programs, and allow more than one per mempool wrapper (see EIP-8288 open issues).
- Bring EIP-8357 to clients and a fork: finish its review, tests, and system contract, and propose it together with EIP-8288.
- Specify how the mandatory L1 proof binds and absorbs the EIP-8288 aggregate.
- Find a path for L2 block data now that EIP-8142 was declined for Hegotá: EIP-8142 in a later fork, or another way to bind the data to blobs.
- Replace the reference implementation’s mock proofs with zkVM proofs of the same program, once the execution and consensus specifications agree on what the proof commits to.
- Research native rollups with extensions.
Generalized proof verification
Table of Contents
This page shows what generalized proof verification would replace, and what L1 must provide for it. The case for it is made in the introduction.
How rollups verify proofs today
Each zkVM vendor provides a universal Solidity verifier contract, typically a Groth16 or Plonk check over BN254. The program’s verification key and the public values are passed alongside the proof. For SP1:
interface ISP1Verifier {
function verifyProof(
bytes32 programVKey, // program verification key
bytes calldata publicValues, // public values (inputs and/or outputs)
bytes calldata proofBytes // the proof
) external view;
}
Rollups then build their own stack around these verifiers. Taiko, for example, runs six contracts, each maintained and upgraded through a custom multisig:
- two raw zkVM verifiers, the vendor-provided SP1 Plonk and Risc0 Groth16 contracts;
- two adapters, one per zkVM, that implement Taiko’s
IVerifierinterface and whitelist the trusted program verification keys; - an SGX verifier; and
- a
ComposeVerifierdispatcher that calls several verifiers and requires a sufficient combination: SGX plus either SP1 or Risc0.
Optimistic rollups instead ship their own onchain fraud-proof VMs, such as Arbitrum’s WAVM and Optimism’s Cannon MIPS machine, plus the dispute logic around them.
Impact on existing rollups
The table below reports Solidity SLOC (non-blank, non-comment source lines) for each project’s onchain contracts, split between “core” rollup logic and the proof verification stack that generalized proof verification would retire.
| Project | Proof system | Core SLOC | Retired SLOC | % retired |
|---|---|---|---|---|
| Arbitrum | Optimistic, WASM VM | 19,034 | 8,181 | 43.0% |
| Base | Optimistic, MIPS VM | 17,426 | 8,907 | 51.1% |
| ZKsync Era | Validity, EraVM | 10,823 | 2,379 | 22.0% |
| Linea | Validity, direct EVM | 8,111 | 2,460 | 30.3% |
| Lighter | Validity, no VM (custom circuits) | 5,417 | 1,699 | 31.4% |
| Total | 60,811 | 23,626 | 38.9% |
These numbers are rough estimates. They cover only onchain Solidity code and exclude offchain provers, sequencers, and each project’s guest program. Governance surfaces (multisigs, timelocks, DAO contracts, proxy admins), partner-specific bridges, and proxy boilerplate are excluded from both columns.
What L1 must provide
Generalized proof verification needs four things from L1:
- A program-agnostic verifier. EIP-8025 only proves L1 execution. EIP-8288’s recursive circuit verifies a proof of any program against its verification key hash, on the same zkVM that L1 uses for its own execution proofs (see EIP-8288).
- Proof delivery and access from contracts. EIP-8288 lets a transaction declare a proof dependency in a frame, which contracts read through frame introspection.
- Aggregation. EIP-8288 aggregates proofs recursively in the mempool and in the builder, and the mandatory L1 block proof must cover the result.
- Program identity. A contract accepts proofs under exact verification key hashes. Applications manage the hashes they accept, as they whitelist programs today. For the EVM, L1 itself publishes the approved hash for each fork in the EIP-8357 registry.
A native rollup is the special case whose verification key hash comes from the EIP-8357 registry, and whose contract reconstructs the proof’s public output from L1 state, as described in the Specification. A rollup with a custom VM follows the same pattern with its own program and verification key.
EIP-8288 (zkzkframes)
Table of Contents
What EIP-8288 is
EIP-8288, “In-mempool signature and proof aggregation”, adds a dependency frame mode to EIP-8141 frame transactions. A dependency frame declares one or more (scheme, data_hash, verification_key_hash) triples. It executes no EVM code; each triple is a claim that must be proven valid for the block to be valid. Contracts read the declared triples with the existing EIP-8141 introspection instructions.
The proofs themselves never enter the block. Transactions travel in mempool wrappers that carry either the direct proofs or a recursive STARK over them. Mempool nodes recursively aggregate the dependencies of all their pending transactions at a fixed interval, FOCIL inclusion lists carry their own aggregate, and the builder produces one final recursive STARK over the dependencies of the included transactions. The block header commits to that STARK and to the hash of the block’s sorted, deduplicated dependency list.
The EIP was merged as a Draft in September 2026 and supports two schemes: LeanSPHINCS signatures and LeanSTARK proofs.
How a native rollup uses it
A native rollup update declares exactly one dependency:
(LEANSTARK_SCHEME, public_input_root, verification_key_hash)
public_input_rootis the root of the public output produced by proving the L2 block with L1’s stateless validation program, committed as EIP-8025’sPublicInput, as defined in the Specification.verification_key_hashis the hash of the EVM program’s verification key selected from the EIP-8357 registry, either the current entry or a pinned one.
A LeanSTARK proof is a proof of a program running on leanVM. This book assumes that L1 uses the same zkVM for its own execution proofs, so the L1 stateless validation program is proven on leanVM directly. leanVM’s planned move to RISC-V (leanVM #277) points in this direction: it would run the same guest programs that EIP-8025’s zkVMs run today, once it supports their standard target and interface, including cryptographic accelerators, and continuations for block-sized runs. Declaring a dependency means requiring it, so the native proof is always a single mandatory 1-of-1 proof.
A minimal self-paying transaction looks like this:
frame 0: VERIFY target = sender, flags = APPROVE_EXECUTION_AND_PAYMENT
frame 1: DEP_VERIFY data = (LEANSTARK_SCHEME, public_input_root, verification_key_hash)
frame 2: SENDER target = NativeRollup, data = advance(params, dependency_frame_index = 1)
The VERIFY frame runs the smart account’s authorization; the dependency frame grants no authority on its own. A sponsored transaction replaces frame 0 with an only_verify frame for the sender followed by a pay frame for the paymaster. The L2 data still travels in blobs: EIP-8288 proves correctness, not availability, and BLOBHASH returns the transaction’s versioned hashes in every frame.
The rollup contract verifies no proof itself. It reads the triple with FRAMEPARAM and FRAMEDATACOPY, checks the verification key hash against the EIP-8357 registry, and checks the data hash against the root it reconstructs from its own state. See the NativeRollup contract in the Specification.
Relation to the mandatory L1 proof
EIP-8288’s recursive STARK only proves that every declared dependency is valid. It does not prove that the L1 transactions executed correctly, that the rollup contract matched the dependency to the right transition, or that the dependency list was correctly extracted from the block. With full-payload validation, validators check those things by executing the block themselves.
This book assumes validators instead verify a mandatory L1 execution proof and do not download the full payload. That proof must therefore also bind EIP-8288’s dependency hash and recursive STARK. Since both proofs come from the same zkVM, the mandatory L1 proof can recursively verify the EIP-8288 aggregate, leaving validators a single proof.
Open issues
- LeanSTARK dependencies cannot be proven yet. leanVM’s RISC-V rewrite brings back recursion, with aggregation trees over signature proofs, but each tree verifies proofs of one fixed program, so there is still no way to express that a proof of an arbitrary program verifies under a given key. EIP-8288’s dependency digest is also defined for LeanSPHINCS dependencies only, so LeanSTARK claims still need their own list. The ethrex prototype of EIP-8288 therefore refuses LeanSTARK dependencies. Native rollups need exactly this, together with a leanVM build of the L1 stateless validation program.
- What a LeanSTARK
data_hashcommits to. EIP-8288 calls it the public inputs hash, without saying how it is derived from a program’s public values. A leanVM proof states the program and one 32-byte output, the digest of the values the guest made public, which a verifier recomputes from those values. The Specification fixesdata_hashfor native rollups as the root of EIP-8025’sPublicInput, which works if the guest makes public only that root, but the EIP needs a general rule. - Recursive proving in the mempool. Every participating mempool node produces a new recursive STARK over its pending transactions once per second, and aggregates are wrapped repeatedly with no bound on recursion depth. It is an open question whether ordinary mempool nodes can realistically do this, rather than only the builder.
- Wrapper capacity.
MAX_LEANSTARK_DEPS_PER_WRAPPER = 1, yet each node is expected to broadcast one wrapper covering all of its active transactions. As written, two native-rollup transactions cannot share a wrapper. - Frame placement. EIP-8288’s examples put the dependency frame before
VERIFY, while EIP-8141’s public mempool only recognizes four validation prefixes. The layout above places it after the prefix; neither EIP specifies this yet. - Compact validation. EIP-8288 assumes validators extract dependencies from the full transactions. How its dependency hash and recursive STARK enter the compact payload header and the mandatory L1 proof is not specified.
- Unfinished parameters.
AGGREGATED_VKis still to be defined, with no mechanism for later changes to the verifier relation. So isDEPS_HASH, the aggregation circuit’s own hash, which the block’s dependency digest also uses: BLAKE2s in today’s tooling, with BLAKE3 the leading replacement. The hash behind a LeanSTARKverification_key_hashis not named either.
EIP-8357: EVM Verification Key Registry
Table of Contents
EIP-8357 is a draft Core EIP that tells contracts which verification key L1 has approved for proving EVM execution in each fork. It is the one piece native rollups need on top of EIP-8288. This page summarizes the design; the EIP is authoritative:
- EIP: EIP-8357 (Draft)
- Reference implementation: ethereum/sys-asm#56
- Tests: ethereum/execution-specs#3466
- Discussion: Ethereum Magicians
Motivation
EIP-8288 verifies each proof dependency against an explicitly supplied verification_key_hash, a hash of the STARK verification key. It does not tell contracts which hashes L1 recognizes as its own EVM program, which one is current, or which input schema each program accepts. A native rollup that hard-coded these values would have to upgrade through its own governance at every L1 feature fork, or remain on an older EVM: exactly the governance risk native rollups are meant to remove.
EIP-8357 records these values once, in shared L1 state that every contract can read. Verifiers keep accepting exact hashes, while each rollup chooses whether to follow the current entry or pin a historical one.
How the registry works
The registry is an ordinary EVM contract at a fixed address, 0x00005e9c1447c1a05a642ec9eb76d9c125468357. Anyone can deploy it through the EIP-7997 deterministic factory with a fixed salt, so the address is bound to the exact bytecode.
Its state is:
- a mapping from each registered
verification_key_hashto theschema_idof the stateless input its program accepts, which also selects the fork rules the program executes (see Specification); and - the current
verification_key_hash.
Entries are append-only: a registered hash is never deleted, overwritten, or revoked.
Reads. Any caller sends a single 32-byte word:
- zero requests the current entry;
- a nonzero value requests that exact registered hash.
The registry returns verification_key_hash || schema_id, and reverts if no such entry exists.
Updates. Only SYSTEM_ADDRESS can update the registry, and only in the first block of a hard fork, before any transactions are processed:
- a 64-byte registration adds a new hash with its
schema_idand makes it current; - a 32-byte reactivation makes a previously registered hash current again, keeping its
schema_id.
Each new or reactivated hash is specified by its own Core EIP, which names the L1 feature fork whose EVM semantics it represents and the schema_id its program accepts. The fork that activates EIP-8357 also registers the initial hash. The system call’s gas does not count toward the block, and the block is invalid if the call fails.
How native rollups use it
A native rollup reads the registry when it advances its L2 state and uses the result in two places:
- it requires the EIP-8288 dependency in the transaction to carry the returned
verification_key_hash; and - it uses the returned
schema_idto reconstruct the proof’s public output, which commits to it (see Proof statement).
Each rollup chooses one of two policies:
- Follow current: query with zero and accept only the current hash. When a hard fork updates the registry, the rollup moves to the new EVM automatically, without changing its code or storage.
- Pin: query a stored historical hash, and keep that fork’s EVM semantics after L1 moves on. This gives the rollup its own upgrade window, at its own risk.
The full contract is shown in the Specification.
Security considerations
- The registered hash must be sound. If a hash for an incorrect program were registered as current, an attacker could prove false state transitions for every rollup following it.
- Following current means adopting each fork’s key. Proofs generated under the previous key may fail after the switch, and a rollup that cannot produce proofs under the new key may halt.
- Pinning transfers maintenance risk. Registered hashes are never revoked, even if proofs under them are later considered insecure. L1 does not maintain historical entries, and a pinned rollup must detect vulnerabilities and migrate on its own.
Specification
Table of Contents
- Design principles
- Overview
- Data layout
- Genesis
- Proof statement
- Root computation
- NativeRollup contract
- Blob encoding
- Open questions
Design principles
The core principle is to reuse as many L1 components as possible. L2 operators prove the same program as L1 provers, verify_stateless_new_payload, under the verification key that L1 approves for the current fork. The mandatory L1 block proof covers the L2 proofs, and validators only verify that block proof.
This means native rollups inherit whatever the L1 EVM supports: no custom transaction types, precompiles, or fee markets on the L2 side. Any such change would require modifying the shared program. L2-specific logic, such as L1 anchoring and the fixed fields below, lives in the rollup contract, which decides which fields come from storage, which from the operator, and which are constants.
Parts of the design depend on L1 features that are still in development, listed in L1 dependencies. The specification is written as if they were already implemented, and will change as they mature.
Overview
This design targets the future L1 in which execution proofs are mandatory and validators do not download full execution payloads. Validators sample payload data through DAS and verify the mandatory L1 block proof against a compact NewPayloadRequestHeader, which has the same SSZ hash_tree_root as the full NewPayloadRequest.
A native rollup advances as follows:
- The operator builds an L2 block and proves
verify_stateless_new_payloadfor it. The proof’s public output is the block’sStatelessValidationResult, committed as thepublic_input_root(see Proof statement). - The operator submits an EIP-8141 frame transaction that carries the L2 block data in EIP-8142 blobs, declares the EIP-8288 dependency
(LEANSTARK_SCHEME, public_input_root, verification_key_hash), and calls the rollup contract. - The rollup contract reconstructs the expected
public_input_rootfrom its storage, the calldata, and the blob versioned hashes, checks the verification key hash against the EIP-8357 registry, and updates its state. - The EIP-8288 aggregate proves the dependency, and the mandatory L1 block proof covers both the aggregate and the contract’s execution.
Data layout
Native rollups prove the same function as L1, and therefore share its block structure. The following tables describe how native rollup blocks map to the standard spec types.
Fields marked constrained are validated during execution (wrong value = proof fails). Fields marked unconstrained are free inputs chosen by the operator. Fields marked fixed have a constant value for L2.
The unconstrained fields (fee_recipient, prev_randao, parent_beacon_block_root) correspond to the PayloadAttributes that on L1 are trusted to come from the consensus layer. The EL never validates them; it accepts whatever the CL provides. Since native rollups have no CL, these become free inputs for the operator. timestamp is also CL-provided on L1 but additionally constrained by the EL (> parent_header.timestamp). The EL sets no upper bound, so the rollup contract requires timestamp <= block.timestamp, as Taiko bounds its L2 timestamps by the proposal’s L1 timestamp. Without it, anyone could halt the rollup with a valid block at the maximum uint64 timestamp, since no block could follow it. The contract also requires the timestamp to lag block.timestamp by at most MAX_TIMESTAMP_LAG, one hour, so that whoever builds a block cannot hold L2 time back, past the deadlines of L2 users. A block must reach L1 while its anchor is in the BLOCKHASH window, about 51 minutes, so the bound does not shorten the time an operator has to post it.
StatelessInput
StatelessInput: input to verify_stateless_new_payload.
| Field | Expected source | Notes |
|---|---|---|
new_payload_request | see below | |
witness | offchain | ExecutionWitness: MPT node preimages, contract bytecodes, ancestor headers. Used by the prover, never posted to L1 |
chain_id | immutable | The L2 chain ID |
The input carries no fork schedule. The guest reads it as schema_id || SSZ(StatelessInput), where the 2-byte schema_id is (fork_index << 8) | revision, and the fork byte selects the rules it executes: 0x1501 is Amsterdam, revision 1. Each L2 block is therefore executed under the rules of the fork that its verification key implements, with no activation check, and the rollup contract learns that schema_id from the EIP-8357 registry together with the key. This still needs to be specified: execution-specs’ reference program notes that a real implementation must check the payload timestamp against fork activation, which has no meaning for an L2 chain ID.
NewPayloadRequest
NewPayloadRequest (inside StatelessInput):
| Field | Constrained | Expected source | Notes |
|---|---|---|---|
execution_payload | See below | ||
versioned_hashes | yes | BLOBHASH | Ordered list of blob versioned hashes. On L1, the first payload_blob_count entries are payload blobs (EIP-8142, carrying block data) and the rest are from type-3 blob transactions. On L2, since blob transactions are not supported, the list contains only payload blob hashes, read via BLOBHASH from the transaction’s blobs |
parent_beacon_block_root | no | computed onchain | Repurposed as the L1 anchor on L2. The existing EIP-4788 system transaction inside apply_body writes this value to the beacon roots predeploy, making it available to L2 contracts. The reference contract passes the hash of a recent L1 block chosen by the operator, which must be within the BLOCKHASH window and must not move backwards. See Messaging |
execution_requests | yes | calldata | Root supplied by the operator. The rollup contract accepts any requests, since they have no effect on L2 (see Genesis) |
ExecutionPayload
ExecutionPayload (inside NewPayloadRequest):
| Field | Constrained | Expected source | Notes |
|---|---|---|---|
parent_hash | yes | storage | Hash of the previous L2 block. Chain continuity link |
fee_recipient | no | various | Fee recipient (coinbase). Could come from calldata, or be a fixed address in storage (e.g. a DAO treasury) |
state_root | yes | calldata | Post-state root |
receipts_root | yes | calldata | Post-receipts root |
logs_bloom | yes | calldata | Computed during execution |
prev_randao | no | various | See L1 vs L2 differences |
block_number | yes | storage | Must equal parent_header.number + 1 |
gas_limit | yes | immutable | Fixed at deployment, so the 1/1024 bound against the parent always holds. TBD: ZK gas handling |
gas_used | yes | calldata | Computed during execution |
timestamp | yes | calldata | Must be > parent_header.timestamp. The contract also requires <= block.timestamp, and at most an hour less |
extra_data | no | various | Max 32 bytes |
base_fee_per_gas | yes | calldata | Must match EIP-1559 formula from parent header |
block_hash | yes | calldata | Computed from header |
transactions | yes | calldata + blobs | transactions_root in calldata, full transactions in EIP-8142 payload blobs |
withdrawals | fixed | constant | Empty: L2 has no beacon chain. Without this, an operator could mint ETH on L2 through fake withdrawals |
blob_gas_used | fixed | constant | 0: L2 does not support blob transactions, and any blob transaction would make the computed value mismatch |
excess_blob_gas | fixed | constant | 0 for L2 |
block_access_list | yes | calldata + blobs | Canonical RLP bytes defined by EIP-7928. block_access_list_root in calldata, full bytes in EIP-8142 payload blobs |
slot_number | fixed | constant | TBD: the L2 value is not defined yet. L2 contracts read it with SLOTNUM (EIP-7843) |
payload_blob_count | yes | calldata | Header field added by EIP-8142: the number of payload blobs. The contract reads that many BLOBHASH values. It is not yet a leaf of the payload the contract rebuilds, since no consensus-specs type places it. The versioned hashes root already commits to the number of blobs |
The BAL has two distinct commitments. Let block_access_list = RLP.encode(BAL), as defined by EIP-7928:
block_access_list_hash = keccak256(block_access_list)is the existing EIP-7928 execution-block-header field. Stateless validation computes it from the full BAL and checks it throughblock_hash.block_access_list_root = hash_tree_root(ProgressiveByteList(block_access_list))is a compact SSZ summary defined by this proposal forNewPayloadRequestHeader. Replacing the fullblock_access_listfield with this root preserves thehash_tree_rootof the enclosing progressiveExecutionPayload.
block_access_list_root is not a new execution-block-header field and is not part of EIP-7928. It is only the root-equivalent representation needed by contracts and validators that do not download the full execution payload.
Genesis
Amsterdam treats a block as invalid if any of its request system contracts has no code: EIP-7002 withdrawal requests, EIP-7251 consolidations, and EIP-8282 builder deposits and exits. The L2 genesis must therefore deploy them, together with the EIP-4788 beacon roots contract, which stores the L1 anchor. The EIP-2935 history contract and the EIP-7997 deterministic factory are recommended for parity with L1, and preconfirmations require the history contract.
Since the request contracts exist, L2 users can create requests. Requests have no effect on L2, and the value sent with them stays locked in the contracts, so the rollup contract accepts any execution_requests whose root the L2 proof binds. Fixing them to empty would let anyone halt the rollup by forcing a transaction that creates a request.
The genesis also holds the L2 messenger with the pre-minted supply of the gas token, and no other ETH, so that L1 backs all L2 ETH. The messenger stores the address of the rollup contract, whose queue it proves messages against, while the rollup contract stores the genesis block hash. The genesis is therefore built for the address the rollup contract will have, for example from the deployer’s nonce. CREATE2 does not break the cycle, since the genesis hash is part of the init code.
Proof statement
The L2 proof shows that verify_stateless_new_payload succeeded for the L2 block. Its public output is the StatelessValidationResult:
new_payload_request_root: the root of the block’sNewPayloadRequestsuccessful_validation: whether the block is validchain_id: the chain ID the block was validated underschema_id: the input schema, and therefore the fork rules, the guest applied
The EIP-8288 dependency’s data_hash is the public_input_root: the hash_tree_root of this result as the PublicInput of EIP-8025, the same commitment L1’s own execution proofs use. PublicInput has the same four fields and serialization, but it is a progressive container (EIP-7495), so its root differs from that of execution-specs’ plain container. Likewise, Gloas makes NewPayloadRequest progressive (EIP-7688), while execution-specs merkleizes it as a plain container, and consensus-specs bounds versioned_hashes at 4,096 entries, while execution-specs makes it a progressive list. This proposal follows consensus-specs in all three cases, and execution-specs needs to align.
Why chain ID and schema ID are in the output. Neither is part of NewPayloadRequest or the block header, yet both decide whether the block is valid. Execution rejects any transaction whose chain ID differs from the input’s chain_id, so if the output only committed to new_payload_request_root, a prover could validate the L2 block under another chain’s ID and replay that chain’s transactions on the rollup. Likewise, schema_id fixes the rules the block was executed under, and the contract requires the one the registry pairs with the verification key.
Root computation
The rollup contract must reconstruct the expected public_input_root and check it against the declared dependency. This requires two steps:
-
Compute
new_payload_request_rootfromNewPayloadRequestHeader. The mandatory-proof model requires a compact header because neither validators nor the contract have the full transaction list or BAL. SSZ defines root-equivalent summaries: variable-sized payload fields can be replaced by theirhash_tree_rootwithout changing the root of the enclosing object. Consequently,hash_tree_root(NewPayloadRequestHeader) == hash_tree_root(NewPayloadRequest).NewPayloadRequestHeaderwas removed from the current optional EIP-8025 flow only because validators still receive the full payload; this proposal assumes its return for mandatory proofs. The root is that of Gloas’s progressiveNewPayloadRequest(see Proof statement). -
Hash
PublicInput: compute thehash_tree_rootof thePublicInputcontainingnew_payload_request_root(from step 1),successful_validation = true, the L2chain_idfixed at deployment, and theschema_idthat the EIP-8357 registry returns with the verification key hash. The result is thepublic_input_root.
The contract has access to every field needed for step 1:
| Leaf | Expected source |
|---|---|
Scalar header fields (parent_hash, block_number, etc.) | Contract storage, immutables, and operator calldata |
transactions_root | Operator calldata (constrained: proven by the L2 proof) |
withdrawals_root | Known constant (empty for L2) |
block_access_list_root | Operator calldata: hash_tree_root(ProgressiveByteList(block_access_list)) (constrained by the L2 proof) |
slot_number | Constant (value TBD) |
versioned_hashes | BLOBHASH from the transaction’s blobs |
parent_beacon_block_root | Computed onchain (L1 anchor) |
execution_requests | Operator calldata (constrained by the L2 proof; any requests are accepted) |
The reference implementation computes both roots in Solidity with the SHA-256 precompile, in about 21k gas without blobs and 28k with one, and is tested against roots computed with the compiled consensus-specs types.
transactions_root and block_access_list_root are constrained fields: if the operator provides a wrong value, the reconstructed new_payload_request_root does not match the L2 proof, and the mandatory L1 proof fails. This is the same trust model as state_root and receipts_root, which are also claimed by the operator and validated by the proof.
The contract does not separately receive EIP-7928’s block_access_list_hash. The L2 proof computes that Keccak commitment from the full BAL, validates it as part of the execution block header, and binds the resulting block_hash.
NativeRollup contract
The contract implements the root computation and checks the result against the EIP-8288 dependency declared in the same transaction. It verifies no proof itself: in a valid block, EIP-8288 guarantees that every declared dependency is proven.
The code below is abridged from the reference implementation, which also contains the L2 messenger. It runs on a local copy of bogota-devnet-0, the EIP-8141 devnet, with real L2 blocks, and mocks what L1 does not have yet: the EIP-8288 dependency and EIP-8142 in the L1 program. The EIP-8357 registry is in the devnet’s genesis, holding the entry its activation fork would register.
contract NativeRollup {
struct BlockParams {
// Constrained fields (validated by the L2 proof)
bytes32 stateRoot;
bytes32 receiptsRoot;
bytes logsBloom;
uint64 gasUsed;
uint64 timestamp;
uint256 baseFeePerGas;
bytes32 blockHash;
bytes32 transactionsRoot;
bytes32 blockAccessListRoot; // SSZ root of the progressive byte list containing RLP(BAL)
uint256 payloadBlobCount; // EIP-8142: number of blobs carrying L2 block data
bytes32 executionRequestsRoot; // SSZ root of ExecutionRequests; any requests are accepted
uint256 anchorBlockNumber; // L1 block whose hash is the L1 anchor
// Unconstrained fields (free operator inputs)
address feeRecipient;
bytes32 prevRandao;
bytes extraData; // at most 32 bytes
}
uint8 constant LEANSTARK_SCHEME = 0x11; // EIP-8288
address constant EVM_VK_REGISTRY = 0x00005e9c1447C1A05A642ec9eB76D9C125468357; // EIP-8357
uint64 constant L2_SLOT_NUMBER = 0; // TBD
// SSZ root of an empty progressive list: sha256 of 64 zero bytes.
bytes32 constant EMPTY_LIST_ROOT = 0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b;
uint256 constant STATE_ROOT_HISTORY = 8191;
uint256 constant MAX_TIMESTAMP_LAG = 1 hours;
// Fixed at deployment
uint64 immutable chainId;
uint64 immutable l2GasLimit;
enum VkPolicy { FollowCurrent, Pinned } // see EIP-8357
VkPolicy immutable vkPolicy;
bytes32 immutable pinnedVkHash;
address immutable l2Messenger; // L2 predeploy whose storage records L2->L1 messages
// L2 chain state tracked onchain
bytes32 public blockHash;
uint64 public blockNumber;
uint64 public anchorBlockNumber;
// State roots of the last STATE_ROOT_HISTORY L2 blocks, by block number
// modulo STATE_ROOT_HISTORY, as EIP-2935 keeps L1 block hashes (for
// L2->L1 messaging via state proofs).
mapping(uint256 => bytes32) stateRootHistory;
// L1->L2 messages, as an append-only Merkle tree of their hashes whose
// root is kept in storage, as the beacon chain deposit contract builds
// it. L2 proves the root against the anchored L1 block hash, then each
// message's path to it (see Messaging).
MessageTree l1Messages;
// L2->L1 messages already delivered, 256 flags per storage slot.
mapping(uint256 => uint256) claimedL2Messages;
// Emitted so that the message can be claimed on L2, where only its
// hash is proven.
event L1MessageSent(
uint256 indexed index, address indexed sender, address indexed to,
uint256 value, uint256 fee, uint256 gasLimit, bytes data
);
// Emitted for each L2 block, so that L2 nodes can find the transaction
// that carries it and its blobs.
event BlockAdded(uint64 indexed number, bytes32 blockHash);
// Sends msg.value - fee to `to` on L2, with a call that gets gasLimit,
// and `fee` to whoever claims the message there (see Messaging).
function sendMessage(address to, uint256 fee, uint256 gasLimit, bytes calldata data) external payable {
require(msg.value >= fee, "fee exceeds value");
require(to != l2Messenger, "message to the messenger");
uint256 index = l1Messages.count;
l1Messages.insert(keccak256(
abi.encodePacked(msg.sender, to, msg.value - fee, fee, gasLimit, keccak256(data), index)
));
emit L1MessageSent(index, msg.sender, to, msg.value - fee, fee, gasLimit, data);
}
function advance(BlockParams calldata params, uint256 dependencyFrameIndex) external {
// 1. Read the EIP-8288 dependency declared by this transaction.
(uint8 scheme, bytes32 dataHash, bytes32 vkHash) = readDependency(dependencyFrameIndex);
require(scheme == LEANSTARK_SCHEME, "not a LeanSTARK dependency");
// 2. Select the EVM verification key hash and its schema ID
// from the EIP-8357 registry. Zero selects the current entry.
(bytes32 expectedVkHash, uint16 schemaId) =
readRegistry(vkPolicy == VkPolicy.FollowCurrent ? bytes32(0) : pinnedVkHash);
require(vkHash == expectedVkHash, "wrong verification key");
// 3. Bound the L2 timestamp by L1 time (see Data layout).
require(params.timestamp <= block.timestamp, "timestamp in the future");
require(params.timestamp + MAX_TIMESTAMP_LAG >= block.timestamp, "timestamp too old");
// 4. Compute new_payload_request_root from storage, calldata,
// versioned hashes, and the L1 anchor (see Messaging), with SSZ
// hash_tree_root over the SHA-256 precompile.
bytes32 npRoot = computeNewPayloadRequestRoot(
// ExecutionPayloadHeader fields
parentHash: blockHash, // from storage
feeRecipient: params.feeRecipient,
stateRoot: params.stateRoot,
receiptsRoot: params.receiptsRoot,
logsBloom: params.logsBloom,
prevRandao: params.prevRandao,
blockNumber: blockNumber + 1, // from storage
gasLimit: l2GasLimit, // fixed at deployment
gasUsed: params.gasUsed,
timestamp: params.timestamp,
extraData: params.extraData,
baseFeePerGas: params.baseFeePerGas,
blockHash: params.blockHash,
transactionsRoot: params.transactionsRoot, // constrained (proven)
withdrawalsRoot: EMPTY_LIST_ROOT, // no withdrawals on L2
blobGasUsed: 0, // fixed for L2
excessBlobGas: 0, // fixed for L2
blockAccessListRoot: params.blockAccessListRoot, // constrained (proven)
slotNumber: L2_SLOT_NUMBER, // fixed for L2 (value TBD)
// NewPayloadRequest fields
versionedHashes: getVersionedHashes(params.payloadBlobCount),
parentBeaconBlockRoot: anchor(params.anchorBlockNumber), // L1 anchor
executionRequests: params.executionRequestsRoot // proven, not interpreted
);
// 5. Hash the EIP-8025 PublicInput and compare it with the
// declared dependency.
bytes32 publicInputRoot = SSZ.hashTreeRootPublicInput(npRoot, true, chainId, schemaId);
require(dataHash == publicInputRoot, "root mismatch");
// 6. Update onchain state.
uint64 number = blockNumber + 1;
blockHash = params.blockHash;
blockNumber = number;
anchorBlockNumber = uint64(params.anchorBlockNumber);
stateRootHistory[number % STATE_ROOT_HISTORY] = params.stateRoot;
emit BlockAdded(number, params.blockHash);
}
function stateRootAt(uint256 l2BlockNumber) public view returns (bytes32) {
require(
l2BlockNumber <= blockNumber && blockNumber - l2BlockNumber < STATE_ROOT_HISTORY,
"L2 block not in history"
);
return stateRootHistory[l2BlockNumber % STATE_ROOT_HISTORY];
}
function claimL2Message(
Message calldata m, // sender, to, value, fee, gasLimit, data, index
uint256 l2BlockNumber,
bytes[] calldata accountProof,
bytes[] calldata storageProof,
address feeRecipient
) external {
markClaimed(claimedL2Messages, m.index); // reverts if already claimed
// Messages stay in the messenger's queue, so any recent root works.
bytes32 entry = storageValue(
stateRootAt(l2BlockNumber), l2Messenger, queueSlot(m.index), accountProof, storageProof
);
require(entry == hashMessage(m), "message not queued");
// Pay out of the ETH escrowed by sendMessage: the value to the
// destination, with a call that gets m.gasLimit and can read m.sender
// from l2Sender(), then the fee to feeRecipient.
deliver(m, feeRecipient);
}
function anchor(uint256 number) internal view returns (bytes32 hash) {
// A recent L1 block chosen by the operator (see Messaging).
require(number >= anchorBlockNumber, "anchor moved backwards");
hash = blockhash(number);
require(hash != bytes32(0), "anchor not available");
}
function getVersionedHashes(uint256 count) internal view returns (bytes32[] memory) {
// Since L2 has no type-3 blob transactions, all blobs in the
// transaction are payload blobs (EIP-8142 encoded L2 block data).
bytes32[] memory hashes = new bytes32[](count);
for (uint256 i = 0; i < count; i++) {
hashes[i] = blobhash(i);
}
return hashes;
}
}
readDependency uses the EIP-8141 introspection instructions: FRAMEPARAM(0x02, i) to require the dependency frame mode, FRAMEPARAM(0x04, i) to require exactly one 96-byte triple, and FRAMEDATACOPY to read it. Requiring a single triple avoids ambiguous matching across frames. readRegistry is a STATICCALL to the EIP-8357 registry.
Replay is constrained by state: the expected root commits to the parent L2 block hash and number, the L2 chain ID, the schema ID, the L1 anchor, and the blob versioned hashes.
Gas. Measured on the local devnet, with EIP-8037 state gas:
| Call | Gas |
|---|---|
advance, one blob | 121k, plus 131,072 blob gas, plus 98k of state gas per block until the state root history wraps. Includes the mock dependency frame |
sendMessage | 74k to 81k, plus 98k of state gas when the message count first reaches a new power of two |
claimL2Message | 100k to 180k to an existing account, growing with the proof, here 1.9 to 2.6 KB. The first claim in each 256 adds 98k of state gas for its flag slot, and a new recipient 184k for its account |
See also: Messaging
Blob encoding
L2 block data is encoded into blobs following EIP-8142, which explicitly depends on EIP-7928. The operator calls execution_payload_data_to_blobs to encode the canonical RLP BAL followed by the RLP transaction list into an ordered list of blobs attached to the transaction. Native rollups do not define a separate BAL encoding.
Data availability guarantee. EIP-8288 proves correctness, not availability, so the L2 data travels in blobs. Following EIP-8142’s zkEVM path, the L2 proof derives payload blobs from its private BAL and transaction data and verifies blob/commitment consistency against the public versioned hashes. The public_input_root additionally commits, through new_payload_request_root, to those versioned hashes, transactions_root, and block_access_list_root. The contract reconstructs the same root using BLOBHASH and the two operator-provided SSZ summaries. DAS ensures the blobs are available. Missing blobs make the L1 block invalid; inconsistent blob data makes the L2 proof invalid; and incorrect summary roots make the contract’s root check fail.
EIP-8142 is not in the L1 program yet. execution-specs’ stateless program requires versioned_hashes to be the blob hashes of the payload’s blob transactions, which an L2 block never has, and its StatelessInput has no fields for the payload blobs’ KZG commitments and proofs, which the zkEVM path takes as private inputs. The design depends on both.
L1 data is enough to rebuild the L2 chain. An L2 node finds each block through the contract’s BlockAdded event, takes the header fields from the advance calldata and the BAL and transactions from the payload blobs, and re-executes the block. The header’s requests_hash and transactions trie root come from that execution, since the calldata only carries the SSZ roots of the requests and transactions.
Open questions
-
Proof pricing: covering an L2 proof in the mandatory L1 proof adds work for the L1 prover. EIP-8288 charges a fixed
LEANSTARK_VERIFICATION_GASper dependency, added to the transaction’s intrinsic gas. Whether that is adequate, or a separate proof gas market is needed, depends on the L1 zkEVM gas model. -
One L2 block per transaction, and per slot: EIP-8288’s mempool allows one STARK dependency per transaction, and the native program proves one block, so every L2 block needs its own L1 transaction. EIP-8141’s public mempool also keeps one pending frame transaction per sender, so a rollup that posts through it adds at most one block per L1 slot: the next post waits for the previous one to land. The relaxations under discussion, concurrent keyed nonces for contract senders (EIPs#12039) and expiring nonces (EIP-8266), make a sender’s transactions independent, while consecutive blocks must land in order. Proving a range of blocks with a single proof would require L1 to approve a program that validates multiple blocks.
-
Sequence-first-prove-later: the current design requires blobs and proof to be in the same transaction, so the operator must have the proof ready at data posting time. Supporting sequence-first-prove-later (post data first, prove later) would require a mechanism to reference past blobs.
BLOBHASHonly accesses blobs in the current transaction. Possible approaches include a new opcode or precompile that can attest to blob availability from past blocks (within the DAS availability window), or a contract-level registry of blob commitments. -
Forward compatibility: the rollup contract reconstructs
new_payload_request_rootwith a fork-specific schema and assigns an L2 value to every field. When an L1 fork changesNewPayloadRequestorExecutionPayload, as with the recently addedslot_number, a contract that follows the current registry entry must already know the new schema and the L2 value of each new field. The registry’sschema_idlets the contract detect such a change, but not handle it. Supporting this without a contract upgrade at every such fork is open. -
Recursive L1 execution proofs: this design assumes L1 proves each block with the stateless validation program, so native rollups reuse that program, its verification key, and its
PublicInput. EIP-8025 is moving to recursive proofs (consensus-specs#5566, with the guest drafted in consensus-specs#5534), in which one program verifies the previous proof, checks the beacon-chain lineage, and runs the stateless validation function inside itself. L1 would then no longer prove the per-block program on its own, and the key registered in EIP-8357 would belong to a program that L1 approves but does not use. Having the recursive program verify per-block proofs, instead of re-executing blocks, would restore the reuse. The recursive design is still a draft. -
Blob granularity: EIP-8142 encodes each payload into its own blobs, so every L2 block takes at least one blob, 131,072 blob gas, however small it is. On a local frames devnet, L2 blocks of 4 to 5 KB used 3 to 4% of their blob. Rollups today pack many blocks into each blob. Fewer and larger L2 blocks amortize the cost, while sharing blobs across blocks or rollups would need a different encoding than EIP-8142’s.
Messaging
Table of Contents
Native rollups exchange messages with L1 using only what the L1 program already supports: no new transaction types and no L2-only system logic. L1 to L2 messages build on an L1 anchor, while L2 to L1 messages build on the L2 state root that the rollup contract stores.
L1 anchoring
To receive messages from L1, a rollup needs some information about the L1 chain on L2, the most general being an L1 block hash. Placing such a reference on L2 is called anchoring.
Native rollups reuse the parent_beacon_block_root field of NewPayloadRequest as the L1 anchor. The L1 stateless validation function already runs the EIP-4788 system call, which writes this value to the beacon roots predeploy, so L2 contracts can read the anchor without any change to the L1 program.
The rollup contract chooses the value, and the proof binds it through new_payload_request_root, so the operator cannot pick a different one. The reference contract passes the hash of a recent L1 block, which lets L2 contracts prove any L1 state with storage proofs. A message queue commitment or any other bytes32 works too.
The anchor enters L2 state through the system call, so it must be known before the block is proven. The operator therefore picks the L1 block number, and the contract requires its hash to be within the BLOCKHASH window of the last 256 blocks and the number not to move backwards. Anchoring to the parent of the including block, blockhash(block.number - 1), would tie each proof to the exact L1 block that includes it. Taiko applies the same rules to its anchor, checked by its proofs with a 512-block window.
The trade-off is that the field no longer holds a beacon block root on L2, so a rollup cannot expose both a beacon root and a custom anchor. Most rollups do not support EIP-4788 today anyway (see L1 vs L2 differences).
Alternatives that were considered and dropped:
- A dedicated
L1_ANCHORpredeploy written by its own system call, modeled on EIP-2935. The extra system call is not part of the L1 program, so it would require a different program. - Passing arbitrary bytes as in-memory context, as in this proposal. Reading it requires an L2-only precompile that does not exist on L1.
L1 to L2 messaging
The rollup contract appends the hash of each L1 to L2 message to a Merkle tree, like the l1Messages tree of the reference contract, keeps the tree’s root in its storage, and emits the full message so that it can be claimed. On L2, a messenger contract verifies a storage proof of the root against the anchored L1 block hash, once for all the claims against that anchor. It keeps only the latest root, so its storage never grows, but a claim built against an earlier root fails once a newer one is proven, and must be rebuilt. Each claim carries the message’s path to the root: the messenger checks it, marks the message as claimed, and calls the destination. For the duration of the call, it exposes the original L1 sender so that the destination can authenticate it, as Linea’s sender() and Taiko’s context() already do. Alternatively, the sender can be passed directly to the destination contract. The rollup contract rejects messages to the L2 messenger, and the L2 messenger messages to the rollup contract: the call could make the messenger send a message in its own name, which the other side would expose as the sender.
Messages carry a fee, which the sender pays on top of the value, as on Linea and Taiko. Whoever claims a message names the fee’s recipient, so anyone can claim a message and be paid for it, while recipients can claim their own and pay no fee. On L2, a recipient without funds claims a deposit to itself before its VERIFY frame approves payment, and pays the gas with the value it just received: if the claim fails, it cannot pay, and the transaction is invalid. If one transaction could make many such claims fail, each would cost the L2 node its execution and pay nothing, which is why EIP-8141’s public mempool rejects frames like these before payment. A node can still take these claims, since a plain transfer to a sender without code depends on nothing anyone else can change. Claims of messages to contracts run arbitrary code, so their claimers, such as relayers earning the fee, pay up front.
Messages also carry a gas limit, as on OP and Taiko, since whoever claims a message chooses the claim’s gas. Without it, a claimer could give the call just enough gas for the destination to finish while a call the destination makes runs out, and the message would count as delivered. The claim fails unless the call gets the limit, after the 63/64 rule of EIP-150. Under EIP-8037, the call also draws state gas, which a frame transaction pays only out of each frame’s own budget, never out of execution gas, so the messenger also requires the frame’s remaining budget, TXPARAM(0x0C), to cover the limit. In other transactions, state gas spills into the execution gas the call gets.
A queue with a storage slot per message, each proven on its own, was dropped. EIP-8037 charges each new slot as state growth, about 98k gas per message on L1, and each claim carried a proof against L1 state of about 3 KB, which the L2 publishes in blobs. The tree follows the beacon chain deposit contract, which only creates a slot when the message count first reaches a new power of two, and a claim only carries the path up to the tree’s height, 32 hashes at most.
Native rollups do not add an unsigned transaction type that executes messages with the L1 sender as msg.sender, as the OP and Orbit stacks do. L1 has no such transaction type, so supporting it would require a different program. The cost is that destination contracts must explicitly support the messenger interface for cross-chain authentication, instead of relying on msg.sender. Many projects already work this way, and standardizing the interface across projects would reduce this cost.
L2 to L1 messaging
The rollup contract stores the state roots of recent L2 blocks, like the stateRootHistory of the reference contract, which keeps the last 8,191 as EIP-2935 does for L1 block hashes. An L1 contract proves an L2 message against one of them with a storage proof, for example against the storage of an L2 contract that records sent messages, as OP’s L2ToL1MessagePasser does. Since sent messages stay in that storage, a proof can always use a recent root. Keeping every root instead would create a storage slot on every block, which EIP-8037 charges as state growth, about half the gas of each advance. The block’s receipts_root is also bound by the proof, so a rollup could store it too and prove events instead of storage. The rollup contract delivers proven messages as the L2 messenger does, with their fee and gas limit.
A shallower interface, such as a dedicated root of L2 to L1 messages, cannot be added to the proof’s public output without changing the L1 program. EIP-7685 requests could carry such a root, but only by overloading execution-to-consensus requests with L2 to L1 semantics and adding a request type to L1. Cheaper state proofs from the planned binary state tree (EIP-7864) are expected to reduce the need for a shallower interface.
Existing rollups
| Stack | L1 anchor | L1 to L2 | L2 to L1 |
|---|---|---|---|
| OP stack | The L1Block predeploy, written by an unsigned deposit transaction carrying L1 attributes | Unsigned deposit transactions derived from OptimismPortal events, with an aliased L1 sender. The CrossDomainMessenger builds on them and exposes xDomainMessageSender | Proofs against the storage root of the L2ToL1MessagePasser, included in each output root |
| Linea | A permissioned relayer submits rolling hashes of L1 messages, checked by the settlement proof | Messages claimed on L2 against the relayed hashes. sender() exposes the L1 sender | A custom Merkle tree of messages, verified in the validity proof |
| Taiko | An anchorV4 call at the start of every block saves an L1 block number, hash, and state root chosen by the proposer, within 512 blocks of the proposal’s L1 block and never moving backwards, checked by the proof | SignalService proofs against the anchored L1 state root. The Bridge exposes context() | The same SignalService mechanism in the opposite direction |
| Orbit stack | None: each L1 message becomes its own transaction | Delayed messages in the L1 Bridge become unsigned transactions with an aliased L1 sender, force-included after a delay | ArbOS accumulates messages in a Merkle tree whose root is confirmed on L1. The Outbox executes them with Merkle proofs |
Each stack relies on transaction types or system logic that L1 does not have, or on a permissioned relayer. Native rollups get the same functionality from the reused anchor, storage proofs, and messenger contracts.
Gas token deposits
Table of Contents
Rollup users need the L2 gas token to send transactions. Native rollups cannot mint it with a special transaction type, since L1 has none. Instead, the gas token is pre-minted on L2 and released by L1 to L2 messages.
Design
A predeployed L2 contract holds a pre-minted supply of the gas token. When the messenger processes a deposit message from L1, the contract releases the corresponding amount to the recipient. Withdrawals lock tokens back into the contract and send an L2 to L1 message.
Claiming a deposit is an L2 transaction, and all L2 ETH starts in the pre-minted supply, so the claimed ETH has to pay for its own claim. EIP-8141 frame transactions allow this: a DEFAULT frame claims the message, then a VERIFY frame of the recipient approves execution and payment. EIP-8141 collects the fee from the payer only when it approves, and by then the recipient holds the deposit, so later frames can already use the rest. Deposits to contracts and other addresses that cannot approve payment are claimed by others, for the fee the message carries (see Messaging).
The claim frame runs before any frame approves payment, so a failing claim costs the operator its execution and pays nothing, and such transactions fall outside the public mempool’s validation rules. The operator therefore admits a claim only if it succeeds on its head state, and only one claim per message, so a pending claim cannot keep a message from being claimed.
Because the L2 contract only reacts to messages, the L1 side decides what backs the gas token. Custom gas tokens are therefore only a matter of L1 contract design:
- ETH: the L1 contract escrows the ETH sent with each message, as Linea does.
- ERC20: the L1 contract transfers an ERC20 into escrow instead. Tokens with non-standard decimals or transfer logic need special care.
- ETH burn: the L1 contract burns the ETH instead of escrowing it.
- NFT-gated credits: NFT holders can claim a fixed amount of gas token once per NFT.
Alternatives that were considered and dropped:
- A transaction type that mints the gas token, as in the OP and Orbit stacks. L1 has no such transaction type.
- Minting through the
withdrawalsfield of the payload. The rollup contract could fill it from its own deposit queue, but withdrawals are only processed at the end of a block, so deposits could not be used within the same block. The reference design keepswithdrawalsempty.
Existing rollups
| Stack | Deposit mechanism |
|---|---|
| OP stack | A deposit transaction type mints the gas token from TransactionDeposited event fields |
| Linea | Pre-minted tokens in the L2MessageService, unlocked by L1 to L2 messages |
| Taiko | Pre-minted tokens in the L2 Bridge, unlocked by L1 to L2 messages |
| Orbit stack | A deposit transaction type (ArbitrumDepositTx) mints the gas token |
Open questions
- Claims before EIP-8141: native rollups only have frame transactions once L1 does. Until then, nobody can pay for the first claim, since all L2 ETH starts in the pre-minted supply. Funding an account at genesis would work around it, but L1 would not back that ETH.
L2 fee market
Table of Contents
Native rollups run L1’s fee market unchanged: EIP-1559 base fees, priority fees, and the same gas schedule.
Fee collection
Priority fees go to the block’s fee_recipient, an unconstrained field of the payload (see Specification). The rollup contract can take it from the operator or fix it, for example to a DAO treasury.
The base fee is burned, as on L1. Most L2s in production instead redirect it to a dedicated address, which native rollups cannot do without changing the program. One proposal is for the program to track the fees it burns in each block, both the base fee and the blob fee:
block_output.burned_fees += (
effective_gas_fee - gas_refund_amount - transaction_fee + blob_gas_fee
)
and to expose the total in the proof’s public output. The rollup contract could then act on it; for example, when the gas token is bridged from L1, it could release the burned amount from the L1 escrow to a dedicated address. This is an L1 change: every L1 block proof would expose the value too.
Open questions
- Data availability costs: L2 transactions cannot pay an explicit L1 data fee, as on OP stack chains, since that would change the program. How operators recover the cost of posting blobs, for example through priority fees, is open.
Forced transactions
Table of Contents
Rollups with centralized sequencers need a forced transaction mechanism to preserve L1’s censorship resistance: a user must be able to submit a transaction on L1 with the guarantee that it is eventually included on L2.
Constraints
Native rollups only support the transaction types that L1 has, so a forced transaction is an ordinary signed L2 transaction. It cannot be an unsigned transaction authenticated by its L1 sender, as in the OP and Orbit stacks, and it cannot reserve space in the block or skip the base fee. Where possible, the mechanism should not interfere with the sequencer’s preconfirmations.
A queue that requires every old entry to be included does not work: a signed transaction can become invalid after submission, for example if its sender spends the balance, and the chain could not advance past it.
L2 FOCIL
FOCIL already forces transactions into L1 blocks without new transaction types. The execution layer checks the block against an inclusion list, and a listed transaction may only be missing if it could not have been appended at the end of the block: it is invalid, underpriced, or does not fit. A native rollup can reuse this check unchanged and replace the parts that build the list, the inclusion list committee and the mempool, with an L1 inbox contract. The design is described in Repurposing FOCIL as an L2 forced transaction mechanism, with a prototype contract in this repository.
The inbox works as follows:
- Submission. Anyone submits a signed L2 transaction. The contract runs the stateless checks (signature, intrinsic gas, bounds), and the nonce and balance checks against an account proof on a recent L2 state root. Without the stateful checks, the head of the queue could be filled with high-fee transactions that can never execute. Entries are ordered by
maxFeePerGas, with one entry per sender, and a full queue evicts its cheapest entry. - Inclusion list. When the rollup advances, the list is the head of the queue, up to a gas budget and while entries pay the base fee. Only entries submitted at least a threshold before the block’s anchor qualify, and a replacement counts as a new submission, so the sequencer knows every entry the list could hold when it builds the block, however late it posts it, as its preconfirmations require. Entries can still leave the queue before then, bringing later qualified entries into the list, so a sequencer that preconfirms includes every qualified entry it can.
- Enforcement. The list is an input to the L2 proof, and the rollup contract only advances if the proof reports it satisfied. On L1, satisfaction affects fork choice; here it becomes a condition for advancing.
- Clearing. Satisfaction does not say which listed transactions were included, so entries leave the queue in two ways. Anyone can prune an entry with an account proof at a newer block, showing that its nonce advanced or that its balance no longer covers it. The nonce alone is not enough, since EIP-7702 lets a balance fall without it. At settlement, the operator proves which listed transactions are in the block’s
transactions_root, and an absent entry is dropped if the block had room for it, since it must then have been invalid. An entry that did not fit stays for the next block. - Offline sequencer. A timeout removes the sequencer whitelist if the sequencer stops producing blocks.
Block stuffing remains possible but costly, as on L1: EIP-1559 raises the base fee exponentially while the listed transactions wait. This relies on every L2 block carrying the list, otherwise the operator could publish empty blocks to lower the base fee cheaply. The Specification already proves one L2 block per update.
In the prototype, a submission costs about 620k gas and a prune about 430k gas, roughly 0.0006 and 0.0004 ETH at 1 gwei. Settlement costs about 100k to 115k gas per included entry with Merkle-Patricia proofs, so a list of 32 entries takes about 3.7M gas. Against the SSZ transactions_root that the rollup contract already receives, an inclusion proof is a short sha256 branch.
What L1 must provide
The rollup reuses L1’s stateless validation program, so that program must take the inclusion list as an input, and its proven output must commit to the list and to whether it was satisfied. FOCIL is scheduled for Hegotá and specified on top of Amsterdam in eips/amsterdam/eip-7805, where satisfaction is computed separately from block validity and consumed by fork choice. The zkEVM program does not include it yet, and neither EIP-8025 nor the consensus specs say how a proof commits to it. L1 needs the same once attesters verify proofs instead of executing payloads.
Open questions
- Account proofs. Submitting and pruning require account proofs, which today require a full node, prohibitive for most users of an L2. Block-level access lists carry storage diffs but not storage roots, so tracking them is not enough to build account proofs. EIP-8268 would add storage roots to them, so that nodes tracking only accounts, as in VOPS, could serve these proofs.
- Pruning incentives. A prune costs about 430k gas and benefits everyone waiting in the queue. Submitters could post a small bond that refunds whoever prunes their entry.
Preconfirmations
Table of Contents
The Specification describes a permissionless rollup, in which anyone with a proof can post the next block. A rollup can instead customize it with a centralized sequencer, which confirms transactions long before their block reaches L1: the sequencer shares each block as soon as it builds it, and users act on it. These preconfirmations are only as good as the sequencer’s word. The OP stack gossips blocks that the sequencer signs, and its nodes expect that the sequencer will not equivocate. Arbitrum’s sequencer feed and Taiko’s preconfirmations work the same way, and none of them can penalize a broken promise. Lighter plans a pre-commitment scheme that binds the data its sequencer broadcasts to the state it later commits onchain.
A native rollup can back preconfirmations with a bond that L1 slashes. The rollup contract receives the hash of every L2 block, and the proof binds it, so a preconfirmation can be a signature over the hash that will be posted, and a broken one is a mismatch between two hashes.
Commitments
The sequencer signs the block’s number, hash, and anchor block number, for the rollup contract’s address and L1 chain ID. The hash fixes everything advance rebuilds: the payload fields, the versioned hashes, since EIP-8142’s encoding is deterministic, the anchor and the execution requests. With the signature, each user receives the header and proofs of their transaction and its receipt against it, and checks that the anchor block number is that of the anchor the header holds, which the slashing below relies on.
The hash does not fix the L1 context in which the block is posted:
- Who posts. In the Specification, anyone with a proof can call
advance, so another party could post a different block at the same height. A sequenced rollup takes blocks only from the sequencer, except after the forced transactions timeout, which must be longer than the posting deadline. - Deadline. A block must reach L1 while its anchor is in the
BLOCKHASHwindow, 256 L1 blocks or about 51 minutes, and its timestamp is at most an hour behind L1 time. This plays the role of the OP stack’s sequencing window, and bounds the proving latency, while users only wait for the preconfirmation. - L1 reorgs. If L1 reorgs the anchor block out, the blocks anchored to it cannot be posted. The sequencer anchors a few slots behind the L1 head, trading deposit latency for safety.
- L1 forks. The EIP-8357 registry switches the verification key at an L1 fork, after which the contract rejects blocks proven under the previous key, so the sequencer posts its preconfirmed blocks before the fork.
- Forced transactions. The block’s anchor, not the time of posting, fixes which forced transactions it must include (see L2 FOCIL).
Slashing
The sequencer bonds collateral in the rollup contract, which slashes all of it for:
- Equivocation: two signatures for the same height with different hashes.
- Divergence: a signature for block
Nwith hashX, while the rollup’s blockNhas another hash.
Divergence includes a preconfirmed block that was not posted before its deadline, whether the sequencer let it expire on purpose or failed to post it in time: with about 51 minutes to post, the sequencer carries the risk of L1 congestion and censorship. The only exception is a block whose anchor an L1 reorg removed, which L1’s EIP-2935 history contract shows for 8,191 blocks.
The rollup contract only stores the hash of its latest block, but the L2’s own EIP-2935 history contract keeps the hashes of the last 8,191 L2 blocks in L2 state. A slasher proves the hash of block N with a storage proof against a recent state root, as L2 to L1 messages are proven, which makes the history contract a requirement of the genesis.
The reference implementation’s SequencedNativeRollup implements this customization, and the demo runs it: its sequencer builds and preconfirms a block every 4 seconds, empty if no transaction waits, and posts each once a stand-in proving time has passed.
The OP stack could only enforce such a bond through its output roots, which include the latest L2 block hash but are proposed for some blocks only and final after the dispute window. A native rollup posts and proves every block hash as it goes. The bond is only as credible as the contract that holds it: a native rollup follows L1 forks without upgrades, so its contract can be immutable.
Partial blocks
EIP-8288 allows one proof dependency per transaction, and EIP-8142 takes at least one blob per block, so frequent small blocks are expensive, and a rollup posting through the public mempool adds at most one per L1 slot (see the Specification’s open questions). The sequencer can instead build longer blocks and preconfirm them as they grow, as the OP stack’s Flashblocks do: it signs each prefix of the block’s transaction list together with the parent hash. A prefix and its parent fix the result of every transaction in it, so these are execution preconfirmations, and they are slashable: the header of the posted block, proven through the history contract, holds its transactions root, against which a slasher proves the transactions of the prefix. Intermediate state roots are not slashable, since the header only holds the final one.
Open questions
- Bond size. A fault slashes the bond once, however many preconfirmations it breaks, so the bond caps the value that preconfirmations can protect at any time. Larger transfers wait for the block to be posted.
- Liveness faults. Slashing the whole bond for a block that expired by accident may deter sequencers. A smaller penalty for blocks posted after their deadline, as based preconfirmations give liveness faults, would need the contract to record when each block was posted, and must still exceed what a sequencer gains by letting a block expire on purpose.
- Recipients. Whether the slashed bond pays the users holding broken preconfirmations or whoever proves the fault is open.
L1 vs L2 differences
Table of Contents
A native rollup runs the L1 program unchanged, so its differences from L1 come only from the values the rollup contract puts in the payload (see Specification). This page covers the ones visible to L2 users.
Blob-carrying transactions
Rollups have no consensus layer that handles blobs, so they cannot support blob-carrying transactions. The rollup contract fixes blob_gas_used and excess_blob_gas to zero, so any block containing a blob transaction fails validation. BLOBHASH and the point evaluation precompile are unchanged, and behave as in an L1 block without blob transactions.
RANDAO
prev_randao is an unconstrained field: the rollup contract takes it from the operator or fixes it. Existing rollups differ: Orbit stack chains return the constant 1, OP stack chains return the value from the latest L1 block synced on L2, Linea returns 2, Scroll returns 0, and ZKsync returns 2500000000000000.
Slot number
slot_number is a fixed constant for L2, whose value is still to be defined (see Specification), so the SLOTNUM opcode (EIP-7843) returns the same value in every block.
Beacon roots storage
Native rollups repurpose parent_beacon_block_root as the L1 anchor, so the EIP-4788 predeploy stores anchors rather than beacon block roots. Of the rollups surveyed, only the OP stack supports EIP-4788, while the Orbit stack, Taiko, Linea, Scroll, and ZKsync Era do not.
Native rollups with extensions
Top rollups such as Arbitrum and Optimism have expressed interest in an extensible native program: most execution comes from the native program, but the project is free to add precompiles, opcodes, transaction types, and so on.
No research has been done on this yet. One idea to explore is to take the native program’s verification key hash from the EIP-8357 registry as an input, and express the rollup’s program as a delta on top of it. The native part would then follow L1 upgrades automatically, while the rollup maintains only its extensions. Those extensions do not inherit the property that an L2 bug is also an L1 bug, so they carry their own bug and governance risk.
Execution sharding
A more ambitious idea is to use native rollups explicitly as a form of execution sharding.
Even once verification is no longer the bottleneck, the next bottleneck is serial execution when generating the block witness, and one way to address it is parallel block building. This is what rollups already do: separate state and a separate block-building pipeline that communicates asynchronously with L1 through messaging. Native rollups would allow multiple instances that are identical to L1, trustless and fully proven, as the original sharding vision intended.
Two talks cover this in more detail:
- Ethereum’s roadmap to 10M TPS (TOKEN2049 Singapore 2025): a higher-level talk on the intuitions behind parallel block building.
- Execution sharding through native rollups (ETHDenver 2026): a lower-level talk comparing sharding through native rollups with the live sharding implementations of Polkadot and Near.