Rollups

 

 OPTIMISTIC ROLLUPS 


⭐ PART 1 — OPTIMISTIC ROLLUPS (Code-Level Architecture)

Optimistic Rollups include:

  • Optimism

  • Arbitrum (Optimistic + multi-round)

They share the same fundamental concepts.

We will break this into:

✔ 1. Roles (Sequencer, Proposer, Verifier)

✔ 2. Transaction bundling logic

✔ 3. Posting batches to L1 contract

✔ 4. Fraud proof mechanism (single-round Optimism)

✔ 5. Watcher/Challenger logic (who detects fraud?)

✔ 6. State roots & Merkle proofs code

✔ 7. Withdrawal & 7-day challenge period

✔ 8. Realistic solidity code examples


⭐ 1. Who Does What? (Roles in Optimistic Rollup)

RoleResponsibility
Sequencer (L2 operator)Collects Tx → Executes them → Creates new L2 state root
Proposer (Batch poster)Posts compressed batch to L1
Watchers / ValidatorsVerify L2 batches → Challenge if invalid
Fraud proof contract on L1Verifies fraud → Slashes proposer

In Optimism today, sequencer + proposer is centralized, watchers are permissionless.


⭐ 2. How Transactions Are Bundled (Code Logic)

A sequencer gathers many L2 transactions:

tx1 tx2 tx3 ... txn

Executes them off-chain, updating the L2 state:

state0 --tx1--> state1 state1 --tx2--> state2 ... state(n-1) --txn--> staten

The sequencer computes:

post_state_root = hash(staten)

Then it creates a calldata batch:

bytes calldata batch = abi.encodePacked( tx1, tx2, tx3, ..., txn );

Finally, it posts an L1 transaction:

rollupContract.submitBatch(batch, post_state_root);

⭐ 3. What Happens on L1? (Deposit Contract / Rollup Contract)

A minimal rollup contract looks like this:

contract Rollup { struct Batch { bytes data; // compressed transactions bytes32 stateRoot; // resulting state root uint timestamp; // used for 7-day challenge window } Batch[] public batches; function submitBatch(bytes calldata data, bytes32 postStateRoot) external { batches.push(Batch({ data: data, stateRoot: postStateRoot, timestamp: block.timestamp })); } }

This simply stores the batch, but assumes it is valid (optimistic).

No checks → trust but verify later.


⭐ 4. How Fraud Detection Works (Who detects bad batches?)

Fraud detection happens off-chain.

There are Watchers (like validators):

  • Anyone can run a full L2 node

  • They re-execute the batch posted to L1

  • They compute the resulting state root

  • If it doesn’t match → fraud

Watcher’s job:

if (recomputeState(batch.data) != batch.stateRoot) { challenge(batchIndex); }

These watchers are paid incentives.


⭐ 5. Submitting a Fraud Proof

Optimistic uses single-step fraud proof:

If a batch is invalid, the watcher submits:

rollupContract.challenge(batchIndex, proof);

Contract structure:

function challenge(uint index, FraudProof calldata proof) external { Batch storage b = batches[index]; require(block.timestamp < b.timestamp + 7 days, "Challenge window ended"); // Verify fraud proof bool valid = verifyFraudProof(b.data, b.stateRoot, proof); require(valid == false, "Proof does not show fraud"); // Slash proposer slash(b.proposer); // Delete the batch delete batches[index]; }

So L1 smart contract validates only the fraud proof, not full transactions.


⭐ 6. How Fraud Proof Works (code logic)

A fraud proof basically proves:

Given input stateRoot and transaction X, the output stateRoot is incorrect.

The challenger provides:

  • Pre-state Merkle proof

  • Post-state Merkle proof

  • Transaction data

  • Storage proofs

Simplified pseudocode:

function verifyFraudProof(bytes calldata batch, bytes32 stateRoot, FraudProof calldata proof) internal pure returns (bool) { bytes32 recomputedRoot = applyTx( proof.preStateRoot, proof.tx, proof.preStateAccountProof, proof.preStateStorageProof ); // recomputed root should equal post-state in batch return (recomputedRoot == stateRoot); }

If not equal → fraud.


⭐ 7. Withdrawals & 7-Day Challenge Window

Withdraw example:

You want to withdraw 100 USDC from L2 to L1.

Steps:

  1. You initiate withdrawal on L2

  2. Sequencer posts it in a batch

  3. L1 sees withdrawal included

  4. But you must wait 7 days

  5. If no fraud challenge → withdrawal finalized

Withdrawal code:

function withdraw(address user, uint amount) external { require(block.timestamp > withdrawalTimestamp[user] + 7 days); token.transfer(user, amount); }

If fraud is detected → withdrawal data becomes invalid → blocked.


⭐ 8. Putting It All Together (Full Code Flow)

Step 1: Sequencer executes tx on L2

-> generates new stateRoot

Step 2: Batch proposed to L1

submitBatch(bytesBatch, stateRoot);

Step 3: Watcher recomputes

if (localRoot != stateRootOnL1) { challenge(); }

Step 4: Fraud proof submitted

-> L1 verifies
-> if valid fraud: batch is deleted

Step 5: After 7 days

withdrawal executes


⭐ 9. Tiny Complete Rollup Contract (Educational)

contract MiniOptimisticRollup { struct Batch { bytes32 stateRoot; uint timestamp; address proposer; } Batch[] public batches; function submitBatch(bytes32 stateRoot) external { batches.push(Batch({ stateRoot: stateRoot, proposer: msg.sender, timestamp: block.timestamp })); } function challengeBatch(uint index, bytes32 correctRoot) external { Batch storage b = batches[index]; require(block.timestamp < b.timestamp + 7 days, "Challenge window passed"); // If roots don't match -> fraud proven require(b.stateRoot != correctRoot, "Not a fraud"); // Slash slash(b.proposer); // Invalidate batch delete batches[index]; } function slash(address proposer) internal { // slash stake or collateral } }

This is the core logic behind optimistic rollups.


⭐ 10. SUMMARY — HOW OPTIMISTIC ROLLUP WORKS IN CODE (COMPLETE)

✔ L2 executes tx off-chain → creates new state root

✔ L1 contract stores batches + state roots

✔ Anyone can validate off-chain

✔ If fraud → watcher submits fraud proof

✔ Contract verifies the proof

✔ Fraud batch is deleted

✔ Proposer slashed

✔ Withdrawals require 7-day window to allow fraud detection

No fraud = optimistic success.






⭐ 1. You said:

optimistic executes transactions off-chain and generates new state roots faster

YES — correct.

Optimistic Rollup = “We execute tx off-chain and only post final results (state roots) on L1.”

Let’s break the sequence:

  1. L2 Sequencer receives tx from users

  2. It applies them to L2 state

  3. Updates L2 state tree

  4. Produces a new state root

  5. Sends it to L1 as a batch

This happens extremely fast because:

  • No PoW / PoS consensus

  • Just one sequencer (currently centralized in Optimism)

So far, your understanding = correct.


⭐ 2. You asked:

how does a watcher calculate the CORRECT state root?
what if watcher is wrong also?

Very important.

✔ What is a watcher?

A Watcher
is any full node that:

  • Downloads the L1 batches

  • Reads the posted transactions

  • Re-executes them locally

  • Recomputes the state root

  • Compares results with the proposer’s state root

Who runs watchers?

  • Independent validators

  • MEV bots

  • L2 full nodes

  • Researchers

  • Anyone who wants to earn reward by catching fraud

  • Anyone who cares about rollup safety

Most importantly:

A watcher IS NOT a validator of Ethereum L1.

They are just “police officers” for L2.


⭐ 3. How does the watcher know the correct state root?

Because the L2 state transition function is deterministic.

Meaning:

If 100 transactions are executed in EXACT SAME ORDER,
starting from SAME PREVIOUS STATE,
everybody MUST end up at the SAME new state.

There is no randomness.

There is no consensus mechanism.

There is no choice.

EVM execution is pure deterministic computation.

⚡ Example

State Root (previous) → run tx1 → new state
Run tx2 → new state
Run tx3 → new state

Run tx100 → final state root

Anyone who replays the same transactions MUST get the same result.

If not → proposer submitted fraud.

So watchers ALWAYS know the correct truth automatically
because the EVM is deterministic.


⭐ 4. You asked:

What if the watcher is wrong?

Then the fraud proof will fail on L1.

Because the fraud proof required:

  • Correct pre-state Merkle proof

  • Correct execution inside proof contract

  • Correct post-state root

If watcher’s proof is wrong →
the fraud proof contract rejects it →
proposer is safe.

So watchers cannot “accidentally” punish an honest proposer.


⭐ 5. You asked:

as there is no consensus in L2 users also do double spending
how proposers know? If they just execute transactions?

Important topic.

✔ Doubt 1:

“There is no consensus → double spend possible?”

Not really.

Because:

L2 still has a canonical chain of transactions

produced by sequencer.

For a user to double spend, they would need to:

  • Submit two conflicting txs

  • And get both included in the same batch

  • Which the sequencer prevents (it rejects invalid txs)

The sequencer enforces:

  • nonce sequence

  • balance checks

  • signature checks

  • gas fees

  • transaction ordering

So double spend on L2 is not possible because it is prevented by the L2 EVM execution rules.

Consensus is not needed.

Consensus is only needed on L1 because many nodes must agree.
But on L2, only 1 sequencer writes the L2 chain.

As long as the L1 challenge window exists, L2 safety is preserved.


⭐ 6. You asked:

why proposer faces a loss if incorrect state root is calculated?

This is the core game-theory mechanism.

✔ The proposer must put a bond/stake on L1

This is called:

  • Bond

  • Collateral

  • Deposit

  • Stake

Example:

mapping(address => uint) public stakes;

When proposer submits batch:

stakes[msg.sender] += requiredStake;

If fraud proof succeeds:

stakes[proposer] = 0; reward(challenger);

Why is slashing needed?

Because if there is no punishment, proposer can cheat every day.

Slashing ensures:

  • It hurts to publish invalid data

  • Honest watchers get rewarded

This makes system stable.


⭐ 7. Your last question:

L1 only stores batches so when somebody reads state it reads from L2?

Correct and incomplete.

Let’s clarify.

✔ L1 stores:

  • Transaction batches (calldata)

  • Proposed state roots

  • Withdrawal entries

  • Fraud proofs

  • Canonical chain info

  • Finalized roots after 7 days

✔ L2 stores:

  • Full state (balances, contracts, storage)

  • Real-time execution

  • Mempool

  • Actual RPC node data

  • Updated state tree

✔ When users want to read their balance:

They DO NOT ask L1.

They ask L2 RPC node (Optimism RPC).

✔ When they want to withdraw to L1:

L1 uses the state root posted by L2 after 7 days to finalize withdrawals.

So:

  • L2 = realtime state

  • L1 = canonical backup + disputes

This is exactly how rollups scale Ethereum.


⭐ FINAL CLEAR PICTURE (VERY IMPORTANT)

Let me rewrite Optimistic Rollup in a perfect summary:


Execution

L2 sequencer executes tx FAST
→ computes new state root
→ sends to L1


Verification

Anyone (watcher) replays transactions
→ recomputes the same state root
→ compares with proposer result


Fraud Proof

If mismatch → watcher submits fraud proof
→ L1 contract checks
→ If fraud proven: proposer slashed, batch reverted


Finality

If 7 days pass with no challenge:
→ batch becomes FINAL
→ withdrawals unlocked


Double Spend Protection

Sequencer enforces nonce & balance checks.
No need for consensus on L2.


State Access

Users read live state from L2.
L1 stores committed data for dispute and finality.

Comments

Popular posts from this blog

Frontend-to-Blockchain Flow

Top-to-bottom map of how an Ethereum node is structured

Arbitrum vs Optimism Rollups