owned this note
owned this note
Published
Linked with GitHub
# 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](https://github.com/ethereum/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](https://github.com/ethereum/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`](https://github.com/scroll-tech/go-ethereum) is Scroll's fork of [go-ethereum](https://github.com/ethereum/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:
4. **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.
5. **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.
6. **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:
7. **Chunk proof:** The chunk is proven by a chunk prover (zkEVM prover).
8. **Batch proof:** For each chunk in the batch, the chunk proofs are aggregated into a single batch proof by the batch prover (aggregation prover).
9. **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.