Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Specification

Table of Contents

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:

  1. The operator builds an L2 block and proves verify_stateless_new_payload for it. The proof’s public output is the block’s StatelessValidationResult, committed as the public_input_root (see Proof statement).
  2. 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.
  3. The rollup contract reconstructs the expected public_input_root from its storage, the calldata, and the blob versioned hashes, checks the verification key hash against the EIP-8357 registry, and updates its state.
  4. 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.

FieldExpected sourceNotes
new_payload_requestsee below
witnessoffchainExecutionWitness: MPT node preimages, contract bytecodes, ancestor headers. Used by the prover, never posted to L1
chain_idimmutableThe 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):

FieldConstrainedExpected sourceNotes
execution_payloadSee below
versioned_hashesyesBLOBHASHOrdered 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_rootnocomputed onchainRepurposed 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_requestsyescalldataRoot supplied by the operator. The rollup contract accepts any requests, since they have no effect on L2 (see Genesis)

ExecutionPayload

ExecutionPayload (inside NewPayloadRequest):

FieldConstrainedExpected sourceNotes
parent_hashyesstorageHash of the previous L2 block. Chain continuity link
fee_recipientnovariousFee recipient (coinbase). Could come from calldata, or be a fixed address in storage (e.g. a DAO treasury)
state_rootyescalldataPost-state root
receipts_rootyescalldataPost-receipts root
logs_bloomyescalldataComputed during execution
prev_randaonovariousSee L1 vs L2 differences
block_numberyesstorageMust equal parent_header.number + 1
gas_limityesimmutableFixed at deployment, so the 1/1024 bound against the parent always holds. TBD: ZK gas handling
gas_usedyescalldataComputed during execution
timestampyescalldataMust be > parent_header.timestamp. The contract also requires <= block.timestamp, and at most an hour less
extra_datanovariousMax 32 bytes
base_fee_per_gasyescalldataMust match EIP-1559 formula from parent header
block_hashyescalldataComputed from header
transactionsyescalldata + blobstransactions_root in calldata, full transactions in EIP-8142 payload blobs
withdrawalsfixedconstantEmpty: L2 has no beacon chain. Without this, an operator could mint ETH on L2 through fake withdrawals
blob_gas_usedfixedconstant0: L2 does not support blob transactions, and any blob transaction would make the computed value mismatch
excess_blob_gasfixedconstant0 for L2
block_access_listyescalldata + blobsCanonical RLP bytes defined by EIP-7928. block_access_list_root in calldata, full bytes in EIP-8142 payload blobs
slot_numberfixedconstantTBD: the L2 value is not defined yet. L2 contracts read it with SLOTNUM (EIP-7843)
payload_blob_countyescalldataHeader 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 through block_hash.
  • block_access_list_root = hash_tree_root(ProgressiveByteList(block_access_list)) is a compact SSZ summary defined by this proposal for NewPayloadRequestHeader. Replacing the full block_access_list field with this root preserves the hash_tree_root of the enclosing progressive ExecutionPayload.

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’s NewPayloadRequest
  • successful_validation: whether the block is valid
  • chain_id: the chain ID the block was validated under
  • schema_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:

  1. Compute new_payload_request_root from NewPayloadRequestHeader. 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 their hash_tree_root without changing the root of the enclosing object. Consequently, hash_tree_root(NewPayloadRequestHeader) == hash_tree_root(NewPayloadRequest). NewPayloadRequestHeader was 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 progressive NewPayloadRequest (see Proof statement).

  2. Hash PublicInput: compute the hash_tree_root of the PublicInput containing new_payload_request_root (from step 1), successful_validation = true, the L2 chain_id fixed at deployment, and the schema_id that the EIP-8357 registry returns with the verification key hash. The result is the public_input_root.

The contract has access to every field needed for step 1:

LeafExpected source
Scalar header fields (parent_hash, block_number, etc.)Contract storage, immutables, and operator calldata
transactions_rootOperator calldata (constrained: proven by the L2 proof)
withdrawals_rootKnown constant (empty for L2)
block_access_list_rootOperator calldata: hash_tree_root(ProgressiveByteList(block_access_list)) (constrained by the L2 proof)
slot_numberConstant (value TBD)
versioned_hashesBLOBHASH from the transaction’s blobs
parent_beacon_block_rootComputed onchain (L1 anchor)
execution_requestsOperator 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:

CallGas
advance, one blob121k, plus 131,072 blob gas, plus 98k of state gas per block until the state root history wraps. Includes the mock dependency frame
sendMessage74k to 81k, plus 98k of state gas when the message count first reaches a new power of two
claimL2Message100k 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

  1. Proof pricing: covering an L2 proof in the mandatory L1 proof adds work for the L1 prover. EIP-8288 charges a fixed LEANSTARK_VERIFICATION_GAS per 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.

  2. 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.

  3. 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. BLOBHASH only 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.

  4. Forward compatibility: the rollup contract reconstructs new_payload_request_root with a fork-specific schema and assigns an L2 value to every field. When an L1 fork changes NewPayloadRequest or ExecutionPayload, as with the recently added slot_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’s schema_id lets the contract detect such a change, but not handle it. Supporting this without a contract upgrade at every such fork is open.

  5. 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.

  6. 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.