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

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_ANCHOR predeploy 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

StackL1 anchorL1 to L2L2 to L1
OP stackThe L1Block predeploy, written by an unsigned deposit transaction carrying L1 attributesUnsigned deposit transactions derived from OptimismPortal events, with an aliased L1 sender. The CrossDomainMessenger builds on them and exposes xDomainMessageSenderProofs against the storage root of the L2ToL1MessagePasser, included in each output root
LineaA permissioned relayer submits rolling hashes of L1 messages, checked by the settlement proofMessages claimed on L2 against the relayed hashes. sender() exposes the L1 senderA custom Merkle tree of messages, verified in the validity proof
TaikoAn 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 proofSignalService proofs against the anchored L1 state root. The Bridge exposes context()The same SignalService mechanism in the opposite direction
Orbit stackNone: each L1 message becomes its own transactionDelayed messages in the L1 Bridge become unsigned transactions with an aliased L1 sender, force-included after a delayArbOS 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.