How Rollups Work at the Code Level
Optimism is an optimism rollup built on top of Ethereum. What’s an optimism rollup? And how does it work at the code level? This article will explain.
We will also cover why rollups need inter-chain communication and how this communication is implemented. We will see the actual code snippets that implement the most important functionality of rollups.
Here is the outline of this article
- What’s an optimism rollup?
- A high-level overview of Optimism contracts
- Code for L1 — L2 bridge
- Code for rolling up transactions
- Code for disputes
First, what is a rollup? It’s one of the ways to make Ethereum more efficient, commonly known as L2 solutions. There are 3 L2 solution types: state channels, plasma, and rollups. I have an article coming up on “Classification of L2 solutions” soon which will cover this in detail. Here is a short summary of what a rollup and specifically, an optimism rollup is.
There is a smart contract on Ethereum (call it
RollupL1) that allows deposits/withdrawals of ETH. When your money is in
RollupL1, you can consider it to be in L2. The L2 money moves much faster than L1 money because L2 transactions (txns) are much more efficient and faster. How is this accomplished?
There is a program that lives outside of Ethereum (call it
RollupL2). It can process txns much faster because it does not have to go through Ethereum’s slow and expensive consensus mechanism. It can process a bunch of txns, combine them (roll them up) into a batch, and submit the batch to
RollupL2can be another smart contract that lives on a faster blockchain or it can be a traditional web2 server. There are pros and cons to each approach such as latency and decentralization.
By processing txns off-chain, you save from 2 axes:
- data compression: a batch takes up less space than individual txns stacked on top of each other. See this section to understand why.
- You only have to go through Ethereum’s slow and expensive consensus only once.
There is another saving axis: you don’t need to calculate the new state after each txn on Ethereum. You see, when you submit a txn on Ethereum directly, Ethereum needs to calculate the new state of accounts. This is expensive. By offloading this work to an L2, you avoid this expensive computation on Ethereum.
RollupL1 just trust the new state submitted to it by
RollupL2? Should it verify? If it verifies, it wastes the same computation so the point of rollups is lost.
Optimistic rollups get around this by blind trust: they simply trust the newly submitted state without doing any verification (they are very optimistic ✨). But, they lock up the newly submitted batch for a week (called the “challenge window”). Anyone can submit mathematical proof during this challenge window and receive a reward if they find a fraudulent state update. If the batch is not disputed during the week, it’s considered final.
The reward is financed by the deposit of those who submit the batch. If you want to submit a batch, you need to submit a deposit.
That’s how optimism rollups work at a high level. ZK-rollups work differently (read my L2 article).
Optimistic rollups need 3 functionality at a high level:
- a 2-way bridge to move money between L1 and L2
- processing transactions and rolling them up into a batch
- disputes/proof of invalid state update
Here is a diagram of Optimism smart contracts that implement the above:
Let’s now take a look at the actual code for the most important parts.
The bridge works by locking up funds on L1 and minting the equivalent on L2. To withdraw funds, the bridge burns the L2 funds and releases the locked L1 funds.
Here is the function for depositing funds:
The function is part of the L1StandardBridge contract which lives on Ethereum. It’s very simple: accept ETH (don’t automatically with the
payable keyword), encode all parameters of the function into a message, and send the message to a cross-domain messenger.
The cross-domain messenger broadcasts messages between L1 and L2. We will cover it in a bit.
There is a corresponding function for listening to these messages on L2.
L2StandardBridge contract does that. This contract lives on a separate L2 blockchain (that is faster than Ethereum).
The function just runs a few checks and mints new tokens. I should have mentioned that you can move arbitrary ERC-20 tokens using this bridge, not just ETH (ETH is just wrapped in an ERC-20 interface).
There are corresponding functions for moving funds from L2 to L1. Also done using an x-domain messenger. I will skip them for brevity.
The communication between L1 and L2 happens via an x-domain messenger contract (there is a copy on each chain). Internally, this contract just stores the message and relies on “relayers” to notify the other chain (L1 or L2) of a new message.
There is no native L1 ↔ L2 communication. There are functions such as
onNewMessageOn each side and relayers are supposed to call them using traditional web2 HTTPs.
For example, here is how L1 → L2 transactions are stored/enqueued on L1:
Relayers would notify L2 that there is a new message in the queue.
There is a sequencer on Optimism whose job is to accept L2 transactions, check for their validity, and apply a state update to its local state as a pending block. These pending blocks are periodically submitted in large batches to Ethereum (L1) for finalization.
The function accepting these batches on Ethereum is
appendSequencerBatch which is part of the
CanonicalTransactionChain contract on L1. internally,
appendSequencerBatch uses the function below to process batches.
batchesRefis a helper contract used for data storage. That’s where batches are stored.
- the function first computes the batch header and then computes its hash.
- it then computes the batch context. The batch header and context are just additional information about the batch.
- it then stores the hash and context in the storage (
Later, hash and context will be used to validate disputes.
Right now, the sequencer role, which rolls up transactions into a batch and submits them, is centralized — controlled by the Optimism org. But they have a plan to decentralize this role in the future. You can also submit your own batches to
CanonicalTransactionChain directly, without going through the sequencer, but it would be more expensive because the fixed cost of submitting the batch is paid entirely by you and is not amortized over many different transactions.
At a high level, disputes work by submitting proof that one state update is invalid and validating this proof against stored state updates (stored batch metadata: hash and context).
The contract responsible for handling disputes is
OVMFraudVerifier. This contract is part of OVM — Optimism virtual machine (similar to EVM — Ethereum virtual machine). Here is the main functions for disputes:
_postStateRoot(submitted by the verifier) is not equal to the root that was submitted by the sequencer.
- if it’s not, then we delete the batch in
_cancelStateTransitionand slash the deposit of the sequencer (in order to become a sequencer, you need to lock up a deposit. When you submit a fraudulent batch, your deposit is slashed and this money goes to the verifier as an incentive to keep the entire mechanism going ).