Sequencer

The sequencer is one of the core components in the Scroll protocol: it is responsible for maintaining the L2 blockchain. The sequencer also ensures that the user and developer experience on Scroll closely resemble those on Ethereum. It achieves this by maintaining EVM and RPC behaviour that is directly inherited from Ethereum, with very few modifications.

The primary functions of Scroll's sequencer are:

  • Collect transactions from L2 and L1.
  • Validate transactions and pack them into L2 blocks.
  • Execute blocks and maintain the L2 blockchain state.
  • Pack blocks into chunks for zkEVM proving.
  • Pack chunks into batches for aggregate proving.
  • Submit batches onto L1 (Ethereum), providing Data Availability.

It is worth highlighting that bridging messages and tokens from L1 to L2 (deposits) is also the sequencer's responsibility. Messages from L2 to L1 (withdraws), on the other hand, can be executed by any user on L1.

The secondary functions of Scroll's sequencer are:

  • Offer standard Ethereum RPC APIs and some Scroll extension APIs.
  • Allow peers (follower nodes) to sync the blockchain using Ethereum's peer-to-peer protocol.

The sequencer, being a fork of go-ethereum, inherits most of its functionality from Ethereum.
This includes transaction and block data structures, EVM execution, RPC and p2p protocols.
As these were not reimplemented but were directly inherited from go-ethereum's original code, we can have a very high certainty of Scroll's compatibility with Ethereum.

The following sections will introduce the main components of the sequencer, the end-to-end sequencing workflow, and some design details.

In the remainder of this article, L1 will refer to Ethereum, while L2 will refer to Scroll.

Sequencer Components

The Scroll sequencer has two main components: l2geth and rollup-relayer.

l2geth is Scroll's fork of go-ethereum.
It is responsible for building and executing blocks and maintaining the blockchain state.
l2geth inherits most of its functionality from Ethereum, with some notable differences listed below.
l2geth has the following submodules (not exhaustive list):

  • Storage: Ledger and state storage implemented using LevelDB.
  • EVM: The state transition rules of Ethereum.
  • Worker: Responsible for creating new L2 blocks.
  • L1 SyncService: Sync and store L1 messages in l2geth local database.
  • API layer: Standard Ethereum RPC and p2p interfaces.
  • Transaction pool: mempool for L2 transactions.

rollup-relayer is responsible for splitting up the Scroll blockchain into sections that are suitable for proving and commitment on L1.
The two main units of this are chunks and batches.
A chunk is simply a contiguous section of the L2 ledger, it is a collection of L2 blocks, and chunks are the unit of zkEVM proving.
Batches, on the other hand, are collections of chunks that are submitted to L1, along with an aggregate proof.
rollup-relayer has the following submodules:

  • chunk-proposer: Collect L2 blocks, propose chunks while enforcing certain limits (see below).
  • batch-proposer: Collect chunks, propose batches while enforcing certain limits (see below).
  • relayer: Submit batches and batch proofs to L1.

Furthermore, the following two Scroll contracts are closely related to the sequencer:

  • L1MessageQueue maintains a queue of messages that the sequencer must relay to L2.
    These messages are either submitted through the bridge (L1ScrollMessenger) as deposit transactions or through EnforcedTxGateway as enforced transactions.
  • ScrollChain maintains batches and their finalization status.
    Each batch is submitted through the commitBatch function, which stores its batchHash in this contract.
    The ZK proofs are submitted through the finalizeBatchWithProof function.

Lifecycle of a transaction

We will now present the end-to-end sequencing workflow from the perspective of a transaction.
Transactions can originate either from L2 or from L1, but during sequencing these two types are processed mostly the same way.

Most Scroll transactions originate on L2:

  1. After signing the transaction, the user submits it to a Scroll node through the standard eth_sendRawTransaction API.
  2. The transaction is broadcast through the Scroll peer-to-peer network until it eventually reaches the sequencer.
    Note that submitting transactions to the sequencer directly is not allowed.
  3. The sequencer validates the transaction and stores it in its local transaction pool.

Some transactions (deposit and enforced transactions) originate on L1:

  1. The user submits the transaction on Ethereum.
    For deposit transactions, the user can use the ScrollGatewayRouter contract, one of the specific gateways like StandardERC20Gateway, or they can also submit the deposit to the L1ScrollMessenger contract directly.
    Enforced transactions are initiated through the EnforcedTxGateway contract.
  2. The transaction is validated and appended to the L1MessageQueue, which then emits a QueueTransaction event.
  3. Once the corresponding blocks on L1 are finalized, the sequencer SyncService then collects all new QueueTransaction events.
    For each QueueTransaction event, the sequencer constructs a special transaction (L1MessageTx) and stores it in its local database.

The block creation and commit steps are shared for the two transaction types:

  1. Blocks: The l2geth worker periodically starts a new mining job.
    It first selects a number of L1 messages from its local database, then a number of L2 transactions from its transaction pool for inclusion.
    The transactions are executed one by one and appended to the block.
    Once the worker has collected enough transactions, it seals the block, stores it in its local database and l2geth broadcasts it into the Scroll peer-to-peer network.
  2. Chunks, batches: The rollup-relayer collects the new L2 block.
    Eventually, this block is included in a chunk.
    Eventually, that chunk is included in a batch.
  3. Commit: The rollup-relayer commits the batch by calling commitBatch on the ScrollChain contract on L1.
    The data submitted with the commit transaction contains block headers and L2 transactions.
    (The submitted data does not contain L1 transactions, since these are already available in the L1MessageQueue contract on L1.)
    The batch hash is calculated by ScrollChain and stored in its contract storage.

The finalization steps are as follows:

  1. Chunk proof: The chunk is proven by a chunk prover (zkEVM prover).
  2. Batch proof: For each chunk in the batch, the chunk proofs are aggregated into a single batch proof by the batch prover (aggregation prover).
  3. Finalization: The batch proof is submitted to L1 by the rollup-relayer, along with the updated state root and withdraw root.
    ScrollChain verifies the submitted proof using an on-chain verifier contract.
    If verification passes, the batch is marked as finalized, and L1 messages processed in this batch are deleted from L1MessageQueue.

At this point, the transactions included in the finalized batch are considered finalized.

l2geth: Summary of modifications

l2geth is a fork of [go-ethereum](https://github.com/ethereum/go-ethereum).
As such, it inherits most of Ethereum's behaviours.
However, we needed to make some breaking changes to l2geth to enable more efficient proving.
This section provides a non-exhaustive list of the modifications, along with their rationale.

zktrie

  • Ethereum uses the MPT (Merkle-Partricia Trie) as its state storage data structure.
    This trie's structure and the fact that it uses keccak hash would make it prohibitively expensive for ZK circuits.
    l2geth instead uses zktrie: a binary trie with Poseidon hash for its state storage.

Opcodes:

  • The BASEFEE opcode are disabled and behaves the same way as INVALID.
    The reason is that Scroll diabled EIP-1559.
  • The SELFDESTRUCT opcode is disabled and behaves the same way as INVALID.
    The reason is that this opcode would be prohibitively expensive to prove, and it is already deprecated in Ethereum.
  • DIFFICULTY/PREVRANDAO returns 0 since l2geth does not run PoW or PoS.
  • BLOCKHASH returns keccak(chainId || height).
    The reason is that the full block hash cannot be verified by the zkEVM with reasonable cost, since one would need to provide all the receipts and the bloom filter to verify these fields.
    As a result, BLOCKHASH will be unique for each block but it will not correspond to the actual L2 blockhash and it cannot be used as a source of randomness.
  • The Shanghai EIPs: EIP-3860 (Limit and meter initcode), EIP-3651 (warm coinbase), PUSH0 are enabled on Scroll.
    That means that you can safely target the latest EVM version when compiling your contracts.

Precompiles

  • The hashing precompile sha256, ripemd-160, and blake2f cannot be verified by the current zkEVM version and thus they are disabled.
    Calling these precompiles will revert.
    We plan to support these precompiles in the future in the form of a network upgrade.
  • modexp works but only supports inputs of 32 bytes (uint256) or shorter.
    ecPairing works but only allows up to 4 inputs.
    This limitations make proving these precompiles much more manageable.
    We believe that the majority of existing use cases fit into these limits.

Fees

  • All fees collected on L2 are sent to a Scroll-maintained L2 fee vault contract.
  • L1 fee: In addition to the L2 gas fee that covers L2 block space and execution costs, we also collect an L1 fee that covers the costs of committing the transaction to L1.
    This fee is proportional to the size of the RLP-encoded transaction.
    The actual cost depends on the current settings stored in the L1GasOracle contract on L2.
    This fee is deducted from the sender balance directly (and not from the gas allowance).

L1MessageTx

  • We added a new transaction type L1MessageTx.
  • We also added DB interfaces for storing such transactions and related metadata.
  • Finally, we implemented SyncService that monitors finalized blocks on L1 and collects L1 messages from these.

StateAccount

  • Add PoseidonCodeHash to StateAccount.
    The zkEVM uses this code hash to verify the contract code when it's loaded.
  • Maintain KeccakCodeHash in StateAccount.
    The zkEVM only needs to verify this one (when the contract is created).
    The EXTCODEHASH opcode returns the KeccakCodeHash, maintaining compatibility with Ethereum.
  • Add CodeSize to StateAccount.
    The EXTCODESIZE opcode returns CodeSize.
    This way, we can verify that the correct result is returned without loading the contract code into the zkEVM.

New validation rules

  • We added a number of new block validity rules to l2geth that correspond to the "chunk and batch constraints" outlined above: Number of l2 transactions, block payload size, circuit row consumption.
  • Other rules are related to L1 messages.
    L1 messages in a block must form a contiguous block at the front of the block.
    L1 messages in a block must be included following their order in L1MessageQueue, i.e.
    they must be included with increasing QueueIndex.
    L1 messages in a block must match those in L1MessageQueue.
  • Most of these rules are considered both during block creation (worker.go) and block validation (block_validation.go).
    As a result, if the sequencer violates any of these rules, follower nodes will reject its blocks.

Other changes

  • Disable EIP1559.
  • Offer a trace api scroll_traceBlockByNumberOrHash to provers.

l2geth: Skipping & circuit capacity checker

With Scroll's current zkEVM circuits, it is possible to construct transactions and blocks that cannot be proven because they "do not fit into the zkEVM".
We call this proof overflow.
To avoid this, we implemented a circuit capacity checker module as part of l2geth.
The circuit capacity checker is used both during block creation (worker.go) and block validation (block_validation.go).

During block creation, if the next transaction would lead to proof overflow, then we seal the block and leave the transaction for the next block.
If a single transaction leads to proof overflow, l2geth discards it.
We call this mechanism skipping.

Skipping L2 transactions means simply discarding them.
In this case, the user needs to submit another transaction with the same nonce that does not lead to proof overflow in order to proceed.

Skipping L1 messages is a more explicit process.
Both the ScrollChain contract on L1 and the zkEVM verifies that each L1 transaction in L1MessageQueue was either included and executed, or skipped.
While the batch encoding does not contain L1 transactions, it does include a skip bitmap that indicates to ScrollChain which L1 messages were skipped.
If an L1 deposit message is skipped, the user can get their tokens refunded on L1.
Note: Proof overflow and thus skipping suggests malicious activity; for normal deposit transactions, this will never happen.

As the unit of zkEVM proving is the chunk, proof overflow must be avoided for chunks as well.
This is achieved by incorporating the circuit capacity checker into the rollup-relayer to make sure that it never proposes unprovable chunks.

rollup-relayer: Chunk and batch constraints

Compared to l2geth, the rollup-relayer is a much more straightforward and simple component.
It collects L2 blocks, creates chunks and batches, and submits these to L1.
In this section we outline the main limits that rollup-relayer must enforce during chunk and batch creation.

Constraints on chunks:

  • No proof overflow: Using the circuit capacity checker, we make sure to include as many blocks in a chunk that it would still not lead to proof overflow.

Constraints on batches:

  • L1 payload: Ethereum has a hardcoded 128KB limit on transaction payload size.
  • L1 commit gas: The gas cost of committing a batch is proportional to the number of blocks and transactions in it.
    We must make sure that this cost does not exceed the L1 block gas limit.
  • Number of chunks: rollup-relayer includes a number of chunks that is optimal for the aggregation prover.

Notes on decentralizing the sequencer

As it stands, Scroll's current sequencer is centralized.
In this section, we consider 4 threats related to centralized sequencers: arbitrary state transitions, censorship risk, regulatory risk, liveness risk.

A centralized sequencer cannot execute arbitrary state transitions.
If, for example, the sequencer tried to move funds from a user's account to its own account, that would constitute an invalid state transition according to the EVM's rules.
While the sequencer could certainly create such a block, it could not be proven in the zkEVM and thus the funds could never be withdrawn to L1.

A centralized sequencer could choose to censor transactions from certain senders, rendering their funds on L2 frozen.
We added the enforced transaction mechanism to mitigate this risk.
Through this mechanism, users can submit transaction on L1 directly and the sequencer is forced to include such messages in its blocks.
Unfortunately, the problem of proof overflow forces us to allow the sequencer to skip such messages.
This kind of censorship is very explicit and easy to detect.
Scroll is working actively to eliminate the proof overflow problem through upgrades to our proving system.
Once proof overflow does not necessitate skipping anymore, the enforced transaction mechanism can fully solve censorship.

Since the centralized sequencer must be deployed in some specific jurisdiction, it is subject to regulatory risk.
Regulators can force Scroll to enforce KYC requirements, or potentially event take over the sequencer.
While this is certainly a possibility, it is possible in such scenarios to quickly migrate the sequencer to other jurisdictions.
The long-term solution to this risk is decentralizing the sequencer.

If the centralized sequencer is down for any reason, the L2 ledger cannot progress and users cannot withdraw their funds.
The long-term solution to this risk is decentralizing the sequencer.

Decentralizing the sequencer helps prevent both malicious acts from the sequencer, as well as Single-Point-of-Failure issues.
Scroll is actively working on upgrading the protocol to allow multiple sequencers.
We will share the progress with the community shortly.

Select a repo