Chapter 30
OP Stack Integration

Requirements: Op Integration Requirements


Contents



30.1 Overview
30.2 High-Level Workflow
30.2.1 Batch Submission To Espresso
30.2.2 Confirmation Validation
30.2.3 Batch Submission To L1
30.2.4 Flowchart
30.3 Components
30.3.1 OP Batcher
Key Management
Batcher Architecture Overview
30.3.2 Base Layer Batch Validation
Batch Inbox Contract
Batch Authentication Contract
Batching And Validation Flow
30.3.3 Derivation Pipeline
30.3.4 OP’s Espresso Streamer
Batch Validity Checks
L1 Reorg Handling Details
Initializing HotShot height
Steps
30.3.5 OP Caffeinated node
Derivation Inputs
L2 Derivation
30.4 Additional Considerations
30.4.1 Gas Cost Optimization
30.4.2 L1 Reorg Handling
30.4.3 Direct Header Verification
30.5 Future Work
30.5.1 Permissionless Batching
30.5.2 Finality Check Removal
30.5.3 Inbox Address Upgrade
30.5.4 Escape Hatch



30.1 # Overview

This document outlines the integration of Espresso’s fast confirmation layer with the OP Stack. It also introduces key components and design considerations.

30.2 # High-Level Workflow

30.2.1 # Batch Submission To Espresso

30.2.2 # Confirmation Validation

30.2.3 # Batch Submission To L1

30.2.4 # Flowchart

PIC

Here is the source file of the diagram. It may be edited and re-uploaded here.

30.3 # Components

30.3.1 # OP Batcher

# Key Management

The OP batcher maintains two sets of keys with different purposes:

Batcher Key

The key which is registered with the rollup chain config as the centralized batcher key. In this key is vested the authority to add batches to the L1, and thus the ultimate authority to determine the sequence of inputs processed by the rollup. Thus, this key also acts as a centralized sequencing key.

This key may exist outside of the TEE enclave running the batcher, although the private key will need to be passed into the enclave in order for it to function.

Ephemeral Key

A key generated inside the enclave which never leaves it. Thus, signatures from this key must originate inside the enclave. This is a way of proving some data originated from or was endorsed by the code running in the enclave. This is similar to producing a TEE attestation, but these signatures are cheaper to verify than the full TEE attestation.

The batcher must have both sets of keys in order to successfully post a batch; the former proves to the derivation pipeline that a batch is originating with the centralized sequencer, while the latter proves to the inbox contract that the batch is originating from within the TEE enclave.

# Batcher Architecture Overview

Let’s take a look at core changes to the batcher design needed to support quickly publishing blocks to Espresso as soon as they arrive from the sequencer, while maintaining the comparatively slower pace of publishing frames to L1 of the original batcher design.

For the purposes of this section, the OP batcher implementation can be conceptualized as two loops and a channel manager:

Our implementation ensures that blocks published to the L1 or DA layer are initially confirmed on Espresso. This replaces the block loading loop with two new loops:

When either loop detects a reorg on L1, L2, or Espresso, batcher state is reset to the last safe L2 block.

This way, we ensure that blocks are published to Espresso as soon as they arrive from the sequencer, while L1 transaction frequency remains unchanged compared to upstream. This design also minimizes the amount of state the batcher needs to manage, which makes handling batcher restarts or L1 reorgs easier.

30.3.2 # Base Layer Batch Validation

The base layer must verify that each batch received is coming from a modified OP batcher running in a TEE, or else prevent the rollup from processing said batch. This is what ensures that the rollup will only execute batches that have been confirmed by Espresso (since the batcher in the TEE is confirming everything with Espresso).

# Batch Inbox Contract

The vanilla OP stack does not have a batch inbox contract. Batches are sent as calldata from an EOA to an arbitrary address. The derivation pipeline reads all transactions sent to this address, filters the ones sent by the designated batch poster account, and interprets the transaction data as batches.

Our integration needs to extend the logic for filtering batches to exclude any which are not sent from a batcher running the correct code in a TEE. We will deploy a batch inbox contract at the designated inbox address, which holds the filtering logic and rejects any transaction which is not authorized to post a batch.

Note that the derivation pipeline also ignores transactions that revert, as specified at 30.3.3, so the contract and the pipeline are consistent.

A challenge in designing this batch inbox contract is that it must maintain binary compatibility with the existing derivation pipeline, where batches are sent as raw calldata (or blob data) with no additional information. This motivates two key design decisions:

1.
The batch inbox contract will accept batches via a fallback function, so that there is no method selector or ABI encoding overhead interfering with receiving the raw batches as calldata in the format expected by the OP derivation pipeline.
2.
All auxiliary inputs (e.g. signatures proving the batch is being sent from an appropriate TEE) must be sent separately.

This motivates the deployment of an additional contract, which we call the batch authentication contract. This contract is responsible for receiving extra inputs to authenticate batches and recording which batches are eligible to be posted. The fallback function of the batch inbox contract then simply calls a method on this authentication contract to check if the batch being sent to it is eligible.

# Batch Authentication Contract

The batch authentication contract has three jobs:

1.
It allows batchers to register an ephemeral key by proving they are running in a TEE enclave. This key can later be used to sign batches, transitively proving that the batches have been generated inside an enclave. Verifying these signatures is much cheaper than verifying the TEE attestation, which only has to be done once by this method.
1                function register(address ephemeralKey, bytes calldata attestation) external;
2.
It allows batchers to attest to batches with their ephemeral keys, proving that these specific batches are being generated inside the enclave and are thus eligible to be posted:
1                function authenticate(bytes32 batchHash, bytes calldata signature) external;
3.
It allows the batch inbox contract to check if a batch being posted is eligible.
1                function isValidBatch(address batcherKey, bytes32 batchHash) external view (bool);

To accomplish these tasks, it maintains a set of registered ephemeral keys, and a mapping of batcher keys to batch hashes. Each ephemeral key in the set has been verified to live within a valid TEE enclave. The batch hash stored with each batcher key is the next batch eligible to be posted by that batcher, having been endorsed by one of the registered ephemeral keys.

1        mapping(address => bool) ephemeralKeys private; 
2        mapping(address => bytes32) authenticatedBatches private;

Each time register is called, it verifies the TEE attestation on the ephemeral key being registered and, if valid, adds it to the ephemeralKeys set. Thereafter, batches signed by this key will be accepted. authenticate updates the stored batch hash for a batcher key, after validating a signature on the hash and batcher key, recovering the signing address, and checking that this address is in the ephemeral key registry. The batcher key to update is assumed to be msg.sender; that is, authenticate must be called from the same account that later posts the batch itself. isValidBatch simply reads the stored batch hash for the given batcher and compares it to the given hash.

The reason for only storing one hash at a time for each batcher is to save on the high gas costs of persistent storage. The intention is for the batcher to always call authenticate and then immediately post the batch to the inbox contract, so there is never need to remember more than one batch at a time for a given batcher. These two calls are to be thought of as two parts of the same operation; they are separated into two separate transactions merely to comply with the calldata format expected of batch posting by the derivation pipeline.

# Batching And Validation Flow

With this contract design, the process for submitting batches to the base layer becomes slightly more complicated. The batcher must send two transactions to L1: first to the authentication contract which validates that the batch is coming from a TEE, and then the actual batch contents to the inbox contract, as shown in figure 30.3.2.0.

PIC

Figure 30.1: Batching and batch validation flow

This introduces new failure modes we must consider, as it is now possible for a failure to occur while batch posting is in an intermediate state (i.e. we have successfully authenticated a batch commitment with the authentication contract, but have not yet sent the batch contents to the inbox contract), or worse, for an L1 reorg to later revert us to such an intermediate state. Previously, batch posting happend in a single atomic L1 transaction and so such intermediate states could never occur.

However, note that if we ever find ourselves in such an intermediate state, it suffices to retry the whole operation from the start. Any state in the authentication contract that is associated with our key will be overwritten if we restart the operation, and then, barring another failure or reorg, we will succeed in sending a batch to the inbox contract. Thus, we can handle all cases by retrying the entire two-transaction operation until it succeeds, which will happen as soon as we manage to send both transactions to L1 without an RPC failure or reorg (both rare), and as long as we always send the correct commitment with a valid signature to the authentication contract just before sending a batch to the inbox contract.

30.3.3 # Derivation Pipeline

Note: If the following approach doesn’t work out, we can use Distributed Lab’s solution to ensure that the signature is signed by an AWS KMS key that can only be used inside a TEE. With this solution, we don’t need extra changes to filter batches. We don’t do this in the first place because:

The OP stack code derives batches by looking at transactions sent to a designated inbox address, but it does not check whether those transactions succeed or revert on L1. Thus, even if the Batch Inbox Contract reverts for an unauthorized batch, the derivation pipeline will still parse its calldata and treat it as a valid batch on L2. This means that without updating the derivation pipeline, the Batch Authentication Contract and Batch Inbox Contract cannot fully enforce TEE-based submission on L1 or prevent a compromised batcher key from posting data that reverts on-chain but is accepted by the pipeline.

To update the derivation pipeline to ignore transactions that revert, we need to update the L1 retrieval code where transactions in L1 blocks are scanned to extract potential batch data. L1 retrieval is implemented by one of the several data sources depending on the DA configuration. Specifically, we should update the calldata source and the blob data source. Both of them currently verify the batch transaction by isValidBatchTx, which doesn’t check the transaction status, so we need to add the check.

The transaction status is represented by the FetchReceipts function that is implemented here, so the next step is to retrieve the receipt status for each transaction, skip the transaction if it doesn’t have a success receipt status (i.e., the status is zero), and build the L2 data with the subset of successful transactions. Note that the receipts are already fetched in the derivation pipeline and cached, so we are not adding complexity to fraud proofs or significantly impacting the performance or the cost.

Depending on the data source, we should either add the above status check to DataFromEVMTransactions or to dataAndHashesFromTxs, so that only transactions that are both valid and successful are kept by the derivation pipeline.

30.3.4 # OP’s Espresso Streamer

The OP’s Espresso streamer is responsible for fetching messages needed by both the op-caff-node and the op-batcher from the Espresso query nodes. It does this by tracking which blocks have already reached quorum—i.e., when enough nodes have agreed on a block. This ensures that only finalized blocks are processed. Each message contains sequencer batches and the corresponding signature from the batcher.

# Batch Validity Checks

Whenever an L2 block advances, the streamer calls the NextBatch() function to select the next valid batch, using a given L2 parent block as a reference.

OP’s current batch validity rule assumes batches arrive in the correct order. It processes transactions as they come, simply accepting or dropping them.

In contrast, our validation process is similar to OP’s previous batch validity rule: we assume batches may arrive out of order. We re-order them based on their batch numbers before processing.

Our validation includes the following checks:

If, after reordering, a batch is found to be invalid with respect to the L2 parent state, it is immediately discarded.

# L1 Reorg Handling Details

The streamer selects the next batch deterministically by using derivation inputs, which include the hash of the L1 origin. In the original design by OP, a valid batch’s L1 origin is guaranteed to come from the canonical L1 chain. Therefore, an L1 reorg could lead to inconsistency between the derivation from Espresso and the derivation from L1. To mitigate this risk, the L1 origin must be finalized. See L1 Reorg Handling for additional details.

When NextBatch() is called, the following steps are executed:

1.
The batch undergoes a validity check against the L2 parent state. If it is invalid, it is dropped immediately.
2.
If the batch is still valid, the function checks whether its L1 origin is finalized by comparing the corresponding L1 origin block number with the current finalized L1 block number.
3.
Should the batch fail the state validity check, it is dropped and NotEnoughData is returned, prompting another iteration.

# Initializing HotShot height

On streamer start or reset due to inconsistent state we need to determine an appropriate HotShot block height to start traversing for batches in order to avoid traversing the whole HotShot chain from genesis, which can take a very long time. For use in batcher, we’re interested in batches that haven’t yet been posted to L1 and thus haven’t yet made their way through the derivation pipeline.

To describe the algorithm to pick HotShot block height, let us first name the entities involved:

Streamer queries de-caffeinated OP node for Safe L2 Block O and determines its L1 Origin block E. Streamer then queries Light Client Contract on L1 for finalized HotShot block height h at L1 height e. h is picked as starting point for traversing HotShot for unsafe batches.

Considering there is no requirement that batches appear on HotShot chain in-order, this approach requires proof that picking h will nonetheless result in no unsafe batches - i.e. batches with height o - being skipped.

We can prove this by contradiction. Assume the opposite: let h′≤ h be the height of a HotShot block that contains batch Owith height o′≥ o. Let us also define Eu as L1 block at which transaction updating Light Client Contract state was included. Consider cross-dependencies for the chains involved:

Thus we encounter a dependency cycle where E references itself through a chain of blocks that include each other either directly, by hash or by chain of hashes, making blocks involved impossible to construct. Our initial assumption is incorrect and we can guarantee no batches will be skipped by the streamer when traversing HotShot chain.

# Steps

To start a Caff node and process messages:

If there is resubmission to HotShot, the streamer might get more than one batch with the same L2 block number, and the streamer will only keep one and skip later batches for the same number.

30.3.5 # OP Caffeinated node

OP node can derive the state from any stream of batches. Depending on how it’s set up, it should be able to derive from the sequencer feed (preconfirmations), from the L1 inbox (the real finalized state), or, if it’s a ”Caffeinated node (Caff node)”, should have an option to derive from Espresso.

This page documents the technical design of the Caff node component of the OP stack integration . This node can be used by anyone who wants to derive the finalized state of the OP rollup as soon as it is confirmed by Espresso (before L1), including solvers in intent-based bridges to listen to blocks that have been finalized by HotShot. Therefore the page covers the process of reading L2 derivation inputs from Espresso to derive the L2 chain. It uses an op-espresso-streamer to fetch finalized messages and then runs them through the state transition function of OP to verify their validity.

# Derivation Inputs

L2 derivation inputs refers to data we need for Caff node to construct Payload Attributes . All of them are derived using Espresso as the source of truth. Some come directly from Espresso, while others are obtained from L1 using Espresso’s reference. Below, we provide the detailed information.

L2 derivation inputs include:

# L2 Derivation

When a Caff node gets everything it needs to build a target L2 block, it will follow the L2 derivation pipeline:

30.4 # Additional Considerations

30.4.1 # Gas Cost Optimization

It costs around 63 million gas to validate an attestation with no prior verified certificates. To mitigate costs, we should have a mechanism where the batcher generates a key at startup and sends an attestation confirming the key’s origin in an enclave. Once verified, the contract can add the public key to a valid set, reducing per-batch posting costs.

30.4.2 # L1 Reorg Handling

L1 reorgs can impact batch consistency and state integrity. To avoid issues due to reorgs, the sequencer should have the flexibility to derive the state from either the latest or the finalized block on L1. Depending on whether such flexibility is supported, the implementations differ as follows.

30.4.3 # Direct Header Verification

Light client is expensive and because we do not run it frequently (around every 10 minutes), it adds lag between sequencing and L1 finality. It is better to verify the header directly from the query service, by either downloading a chain of QCs or a set of Schnorr signatures.

With the query service, we may start from the majority rule for simplicity, then switch to Merkle proof verification. Arbitrum Nitro and OP integration teams may collaborate on this–one team prototypes it and shares the outcomes between teams.

Note this does not mean we cannot use the light client at all. We may still use it for operations such as fetching the finalized state from HotShot, which is more complicated to do through the query service.

30.5 # Future Work

30.5.1 # Permissionless Batching

In the initial version, we support permissioned batcher. In the long term, as described in the rollup integration page, we should be able to run multiple batchers.

To support this, we need to update the derivation pipeline to include a sequencer signature check and replace the batcher signature sent to Espresso with the sequencer signature. Note that the sequencer signature is not the same as the batcher signature. Since we run the batcher inside a TEE, we cannot consider the sequencer and the batcher as one party.

30.5.2 # Finality Check Removal

We may remove the L1 finality checks from the Caff node and the OP batcher for chains that don’t enable the sequencer to derive the finalized state. See L1 Reorg Handling for more reasoning.

30.5.3 # Inbox Address Upgrade

30.5.4 # Escape Hatch

We may add an escape hatch mechanism to provide flexibility when HotShot is unable to provide finality. The batcher will call IsHotshotLive before posting batches, and if HotShot is unavailable, the batcher can be configured to either wait for HotShot to go live or bypass consistency checks.