Skip to content

Zk Rollups

DodaTech 8 min read

In this tutorial, you'll learn zk-rollups including zero-knowledge proofs, validity proofs, zk-SNARKs, zk-STARKs, and how zkSync and StarkNet scale Ethereum with cryptographic guarantees. Why it matters: zk-rollups are the most advanced Ethereum scaling solution, offering the highest security guarantees (mathematical proof) with the lowest gas costs, processing thousands of transactions per second while maintaining Ethereum-level security. By the end, you'll understand how zk-rollups work and deploy on one.

A zk-rollup is a Layer 2 scaling solution that executes transactions off-chain and submits a validity proof (zero-knowledge proof) to Ethereum L1, providing instant finality with full cryptographic security guarantees.

How zk-Rollups Work

zk-rollups batch thousands of off-chain transactions and generate a single validity proof that is verified on Ethereum.

flowchart LR
  subgraph "Layer 2 - zk-Rollup"
    TX1[Transaction 1]
    TX2[Transaction 2]
    TXN[Transaction N]
    SEQ[Sequencer]
    PROOF[Proof Generator]
    STATE[State Tree]
  end
  
  subgraph "Layer 1 - Ethereum"
    SM[zk-Rollup Contract]
    VER[Verifier Contract]
  end
  
  TX1 --> SEQ
  TX2 --> SEQ
  TXN --> SEQ
  SEQ --> STATE
  STATE --> PROOF
  PROOF --> SM
  SM -->|Submit proof| VER
  VER -->|Verify in ms| SM
  SM -->|Update state root| SM
  SEQ -->|Transaction data| SM
  SM -->|DA: Data availability| SM
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// Simplified zk-rollup L1 contract
contract ZKRollupL1 {
    // Verifier contract (verifies zk-SNARK proofs)
    IVerifier public verifier;
    
    // Current state root (Merkle root of all accounts)
    bytes32 public stateRoot;
    
    // Queue of forced transactions (for censorship resistance)
    bytes32[] public forcedQueue;
    
    event StateUpdated(bytes32 newRoot, uint256 batchId);
    event TransactionForced(address indexed user, bytes data);
    
    constructor(address _verifier) {
        verifier = IVerifier(_verifier);
    }
    
    // Submit batch with validity proof
    function submitBatch(
        bytes32 _newRoot,
        bytes calldata _transactionsData,
        uint256[2] memory a,
        uint256[2][2] memory b,
        uint256[2] memory c,
        uint256[] memory publicInputs
    ) external {
        // Verify the zero-knowledge proof
        require(verifier.verifyProof(a, b, c, publicInputs), "Invalid proof");
        
        // Store previous state root as public input to the proof
        // Proof should include:
        // - Previous state root
        // - New state root
        // - Batch hash of transactions
        
        stateRoot = _newRoot;
        
        emit StateUpdated(_newRoot, block.number);
    }
    
    // Forced transaction (anti-censorship)
    function forceTransaction(bytes calldata _txData) external {
        forcedQueue.push(keccak256(_txData));
        emit TransactionForced(msg.sender, _txData);
    }
    
    // Withdraw to L1 (exit from rollup)
    function withdraw(
        bytes32[] calldata _proof,
        uint256 _amount,
        address _recipient
    ) external {
        // Verify Merkle proof that user has funds
        bytes32 leaf = keccak256(abi.encodePacked(msg.sender, _amount));
        require(verifyMerkleProof(_proof, stateRoot, leaf), "Invalid proof");
        
        // Transfer ETH from the rollup contract
        payable(_recipient).transfer(_amount);
    }
}

zk-SNARKs vs zk-STARKs

Two main types of zero-knowledge proofs used in rollups.

Feature zk-SNARK zk-STARK
Full Name Zero-Knowledge Succinct Non-Interactive Argument of Knowledge Zero-Knowledge Scalable Transparent Argument of Knowledge
Proof Size ~200 bytes ~100 KB
Verification Time Milliseconds Milliseconds
Trusted Setup Required Not required
Quantum Resistant No Yes
Gas Cost on L1 Lower Higher
Example zkSync Era, Loopring StarkNet, dYdX

Deploying on zkSync Era

zkSync Era is an EVM-compatible zk-rollup, meaning Solidity contracts deploy with minimal changes.

# Install zkSync Hardhat plugin
npm install -D @matterlabs/hardhat-zksync-deploy @matterlabs/hardhat-zksync-solc
// hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@matterlabs/hardhat-zksync-deploy";
import "@matterlabs/hardhat-zksync-solc";

const config: HardhatUserConfig = {
  zksolc: {
    version: "1.3.17",
    compilerSource: "binary",
    settings: {
      optimizer: { enabled: true, runs: 200 },
    },
  },
  networks: {
    zkSyncSepolia: {
      url: "https://sepolia.era.zksync.dev",
      ethNetwork: "sepolia",
      zksync: true,
    },
  },
  solidity: {
    version: "0.8.19",
  },
};

export default config;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract ZKCounter {
    uint256 public count;
    
    event CountIncremented(uint256 newCount);
    
    function increment() external {
        count++;
        emit CountIncremented(count);
    }
    
    function getCount() external view returns (uint256) {
        return count;
    }
}
// deploy.ts
import { Wallet } from "zksync-ethers";
import { Deployer } from "@matterlabs/hardhat-zksync-deploy";
import { ethers } from "hardhat";

async function main() {
  const wallet = new Wallet("YOUR_PRIVATE_KEY");
  const deployer = new Deployer(hre, wallet);
  
  const artifact = await deployer.loadArtifact("ZKCounter");
  const counter = await deployer.deploy(artifact);
  
  console.log("ZKCounter deployed to:", await counter.getAddress());
  
  // Verify
  await hre.run("verify:verify", {
    address: await counter.getAddress(),
    contract: "contracts/ZKCounter.sol:ZKCounter",
  });
}

main().catch(console.error);

Expected output:

ZKCounter deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
// Test interaction
const { expect } = require("chai");
const { Wallet, Provider } = require("zksync-ethers");

describe("ZKCounter", function () {
  it("should increment count and emit event on zkSync", async function () {
    const provider = new Provider("https://sepolia.era.zksync.dev");
    const wallet = new Wallet("PRIVATE_KEY", provider);
    
    const counter = new ethers.Contract(
      "0x5Fb...",
      ["function increment()", "function getCount() view returns (uint256)", "event CountIncremented(uint256)"],
      wallet
    );
    
    // Increment
    const tx = await counter.increment();
    const receipt = await tx.wait();
    console.log("Gas used:", receipt.gasUsed.toString());
    
    // Verify count
    const count = await counter.getCount();
    expect(count).to.equal(1);
    
    // Gas on zkSync is ~100x cheaper than L1
    console.log("Equivalent L1 gas would be ~50,000");
    console.log("Actual zkSync gas: ~300");
  });
});

Expected output:

Gas used: 320
Equivalent L1 gas would be ~50,000
Actual zkSync gas: ~300

StarkNet and Cairo

StarkNet uses a different programming language (Cairo) and offers higher throughput.

# Cairo contract example (StarkNet)
%lang starknet

from starkware.cairo.common.cairo_builtins import HashBuiltins
from starkware.starknet.common.syscalls import get_caller_address
from starkware.cairo.common.uint256 import Uint256

@storage_var
func balance(account: felt) -> (res: Uint256):
end

@external
func deposit{syscall_ptr: felt*, pedersen_ptr: HashBuiltins*, range_check_ptr}():
    let (caller) = get_caller_address()
    let (current) = balance.read(caller)
    let (amount) = Uint256(1, 0)  # Deposit 1 token
    
    let new_balance = Uint256(current.low + amount.low, current.high + amount.high)
    balance.write(caller, new_balance)
    return ()
end

@view
func get_balance{syscall_ptr: felt*, pedersen_ptr: HashBuiltins*, range_check_ptr}(
    account: felt
) -> (res: Uint256):
    let (bal) = balance.read(account)
    return (bal)
end

Expected behavior: The Cairo contract stores balances and allows deposits. StarkNet compiles Cairo to Sierra (Safe Intermediate Representation) for L2 execution and generates STARK proofs for L1 verification.

L1-L2 Interaction

zk-rollups support communication between L1 and L2.

Direction Method Delay Cost
L1 to L2 Deposit via bridge Minutes L1 gas + L2 gas
L2 to L1 Withdrawal via proof ~1 hour (finality) L1 verification gas
L1 to L2 message Governance/DAO calls Minutes L1 gas
L2 to L1 message Oracle updates Batch dependent Covered by sequencer
// L1 contract that sends a message to zkSync L2
contract L1Messenger {
    IL1Bridge public bridge;
    
    function sendMessageToL2(address _l2Contract, bytes calldata _message) external {
        // Bridge forwards the message to the zkSync mailbox
        bridge.deposit(_l2Contract, _message);
    }
}

// L2 contract receiving messages from L1
contract L2Receiver {
    address public immutable L1_MESSENGER;
    
    modifier onlyL1Messenger() {
        require(msg.sender == L1_MESSENGER, "Not L1 messenger");
        _;
    }
    
    function onMessageReceived(bytes calldata _data) external onlyL1Messenger {
        // Process the message from L1
    }
}

Common Errors and Misunderstandings

1. Confusing zk-Rollups with Optimistic Rollups

zk-rollups use validity proofs (instant finality, math-based security). Optimistic rollups use fraud proofs (7-day delay, game-theoretic security). zk-rollups have higher development complexity but better capital efficiency.

2. Assuming Full EVM Equivalence

zkSync Era is EVM-compatible but not identical. Some opcodes differ, and gas costs vary. Not all Solidity code compiles without modification. StarkNet uses Cairo, a different language entirely.

3. Underestimating Proof Generation Cost

Generating zero-knowledge proofs requires significant computation (GPUs). Proof generation time and cost affect how frequently batches are submitted.

4. Forgetting Withdrawal Delay

While zk-rollups have instant L2-to-L2 transfers and fast L1-to-L2, L2-to-L1 withdrawals require waiting for proof generation and L1 finality (~1 hour).

5. Ignoring Data Availability Costs

zk-rollups still post transaction data to L1 (for data availability). While cheaper than L1 execution, this is still a significant cost. EIP-4844 (blobs) reduces this further.

Practice Questions

  1. What is a validity proof in zk-rollups? A cryptographic proof (zk-SNARK or zk-STARK) that mathematically proves all transactions in a batch were executed correctly. The proof is verified on L1 in milliseconds.

  2. How do zk-rollups differ from optimistic rollups? zk-rollups provide instant finality via validity proofs. Optimistic rollups assume validity and have a 7-day challenge period. zk-rollups are more capital efficient but harder to build.

  3. What is the role of a sequencer in a zk-rollup? The sequencer receives L2 transactions, orders them, executes them, produces a new state root, and generates the validity proof. It submits the proof and batch data to L1.

  4. Why do zk-rollups need data availability on L1? So anyone can reconstruct the L2 state from L1 data. This ensures users can always withdraw their funds, even if the sequencer disappears.

  5. What is the difference between zk-SNARKs and zk-STARKs? SNARKs have smaller proofs (~200 bytes) but require a trusted setup. STARKs have larger proofs (~100 KB) but are transparent (no trusted setup) and quantum-resistant.

Challenge

Deploy a complete ERC-20 token bridge between Ethereum Sepolia and zkSync Sepolia using Solidity contracts on both sides. Implement deposit (L1 to L2), execute L2 mint via zkSync's L1->L2 messaging, and withdrawal (L2 to L1) with Merkle proof verification. Test the full round trip using Hardhat with the zkSync plugin.

Real-World Task

Build a multi-asset portfolio dashboard using React that tracks user balances across Ethereum L1, zkSync Era, Arbitrum, and Polygon. Display gas costs for common operations on each network, show cross-chain transfer capabilities with estimated time and cost, and provide a bridge interface using official bridge contracts for each rollup.

Frequently Asked Questions

Are zk-rollups the future of Ethereum scaling?

Most experts believe zk-rollups will dominate scaling due to their superior security and capital efficiency. Both Vitalik Buterin and StarkWare predict zk-rollups will eventually surpass optimistic rollups for most applications, though optimistic rollups remain important for general EVM compatibility.

How much does it cost to transact on a zk-rollup?

Transaction costs vary: zkSync Era (~$0.02-0.10), StarkNet (~$0.01-0.05), Loopring (~$0.005-0.02). These are 10-100x cheaper than Ethereum L1 ($1-50). Costs depend on batch frequency, L1 gas prices, and proof generation costs.

Can I deploy any Solidity contract on zkSync?

zkSync Era is EVM-compatible but not identical. Most Solidity contracts deploy without changes, but some opcodes behave differently and some precompiles are not supported. Check the zkSync documentation for specific limitations before deploying complex contracts.

Next Steps

After mastering zk-rollups, explore Wallet Development for managing assets across rollups, then dive into DeFi Protocols deployed on L2 networks.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro