Layer 2 Scaling: Rollups, Validiums & State Channels - Blockchain Development Hub
Introduction
Layer 2 scaling solutions execute transactions off-chain while inheriting security from the underlying Layer 1 (Ethereum). The core innovation is separating execution from consensus and data availability.
Scaling Spectrum
Solution
Data Availability
Execution
Finality
Trust Assumptions
Optimistic Rollup
L1 (calldata/blobs)
Off-chain
~7 days (challenge)
1 honest validator
ZK Rollup
L1 (calldata/blobs)
Off-chain + ZK proof
Minutes (proof verification)
Crypto (no trust)
Validium
Off-chain (DAC)
Off-chain + ZK proof
Minutes
DAC honesty
Volition
User choice per tx
Off-chain + ZK proof
Varies
Per-tx choice
State Channel
Off-chain
Off-chain
Instant (off-chain)
Counterparty
Plasma
Off-chain (Merkle roots)
Off-chain
~7 days (exit)
1 honest operator
Rollup Fundamentals
Core Architecture
# Rollup = Off-chain execution + On-chain settlement
# 1. Sequencer: orders transactions, produces blocks
# 2. Executor: applies transactions, computes state root
# 3. Prover (ZK) / Challenger (Optimistic): validity guarantee
# 4. Settlement Contract (L1): verifies, finalizes, handles bridges
# Data flow:
User → Sequencer → Batch → L1 (calldata/blobs) → Verification → Finalized
# State commitment: Merkle root of account/storage trie
# State transition: pre_state_root + transactions → post_state_root
Data Availability (DA) Options
# Ethereum EIP-4844 (Proto-Danksharding): Blobs
# - 128 KB per blob, 6 blobs/block target (3 max pre-cancun)
# - Blob gas market separate from execution gas
# - KZG commitments for polynomial evaluation
# - 18-day retention (not permanent calldata)
# Alternative DA layers:
# - Celestia: Data availability sampling (DAS), namespaced Merkle trees
# - EigenDA: EigenLayer AVS, disperser + retrievers
# - Avail: Polygon DA, light client verification
# - Near DA: NEAR protocol as DA layer
# DA cost comparison (approximate, $/MB):
# - Ethereum calldata: ~$50-200
# - Ethereum blobs: ~$1-5
# - Celestia/EigenDA: ~$0.10-0.50
Bridge Architecture
# L1 → L2 (Deposit):
# 1. User calls L1 bridge contract: deposit(token, amount, l2Recipient)
# 2. L1 contract: burns/locks tokens, emits DepositInitiated event
# 3. L2 indexer: picks up event, credits l2Recipient on L2
# L2 → L1 (Withdrawal):
# Optimistic:
# 1. User calls L2 bridge: withdraw(token, amount, l1Recipient)
# 2. L2: burns tokens, emits WithdrawalInitiated
# 3. Wait challenge period (7 days)
# 4. User calls L1 bridge: finalizeWithdrawal(proof)
# 5. L1: verifies Merkle proof, releases tokens
# ZK:
# 1. User calls L2 bridge: withdraw(...)
# 2. L2: includes in batch, generates ZK proof
# 3. L1 verifier: verifies proof, releases tokens immediately
# Fast bridges (Hop, Across, Connext):
# - Liquidity providers front withdrawal on L1
# - User gets instant L1 funds, LP reimbursed after L2 finality
Optimistic Rollups
Architecture (Arbitrum, Optimism, Base)
# Fraud proof model: "Innocent until proven guilty"
# 1. Sequencer posts batch + state root to L1
# 2. Assumption: state root is correct
# 3. Challenge period: anyone can submit fraud proof
# 4. Fraud proof: interactive verification (bisection) or single-step (Cannon)
# 5. If valid fraud proof: roll back, slash sequencer bond
# 6. If no challenge: state root finalized
# Arbitrum Nitro (Stylus + WASM):
# - Geth-compatible EVM (revm)
# - WASM execution for custom logic (Rust, C++)
# - Interactive fraud proofs via Cannon (MIPS emulator)
# - AnyTrust: DAC for data availability (Nova)
# OP Stack (Optimism, Base, Mode, Zora):
# - Modular: consensus, execution, settlement, DA as swappable
# - Bedrock: EVM equivalence (minimal diffs from Geth)
# - Fault proofs: Cannon (single-step MIPS) + dispute game
# - Superchain: shared sequencing, interop
Fraud Proof Mechanisms
# Interactive (Arbitrum classic):
# 1. Challenger disputes output root
# 2. Bisection: narrow down to single instruction
# 3. On-chain execution of that instruction (MIPS)
# 4. Compare result: mismatch → fraud proven
# Single-step (OP Stack Cannon):
# 1. Challenger provides pre-state + witness
# 2. On-chain MIPS emulator executes 1 instruction
# 3. Verify post-state matches claimed
# 4. Requires pre-image oracle for memory access
# Bond economics:
# - Sequencer posts bond (ETH)
# - Challenger posts bond
# - Winner takes loser's bond
# - Deters spam challenges
Optimistic Rollup Implementation
// Simplified L1 settlement contract (Optimism-style)
contract OptimisticPortal {
struct OutputRoot {
bytes32 root;
uint256 timestamp;
uint256 blockNumber;
bytes32 stateRoot;
}
OutputRoot[] public outputRoots;
uint256 public constant CHALLENGE_PERIOD = 7 days;
mapping(bytes32 => bool) public challenged;
// Sequencer submits output root
function proposeOutputRoot(
bytes32 _root, uint256 _timestamp, uint256 _blockNumber, bytes32 _stateRoot
) external onlySequencer {
outputRoots.push(OutputRoot({
root: _root,
timestamp: _timestamp,
blockNumber: _blockNumber,
stateRoot: _stateRoot
}));
emit OutputRootProposed(outputRoots.length - 1, _root);
}
// Challenge output root
function challengeOutputRoot(uint256 _outputIndex, bytes calldata _proof) external {
OutputRoot storage output = outputRoots[_outputIndex];
require(!challenged[output.root], "Already challenged");
require(block.timestamp < output.timestamp + CHALLENGE_PERIOD, "Challenge period ended");
// Verify fraud proof (simplified)
// _proof contains: pre-state, witness, disputed instruction trace
bool isValid = _verifyFraudProof(output.stateRoot, _proof);
require(isValid, "Invalid fraud proof");
challenged[output.root] = true;
// Slash sequencer, reward challenger
emit OutputRootChallenged(_outputIndex);
}
// Finalize after challenge period
function finalizeOutputRoot(uint256 _outputIndex) external {
OutputRoot storage output = outputRoots[_outputIndex];
require(block.timestamp >= output.timestamp + CHALLENGE_PERIOD, "Challenge period active");
require(!challenged[output.root], "Output root was challenged");
// Mark as finalized, allow withdrawals
emit OutputRootFinalized(_outputIndex);
}
// Withdraw using finalized output root
function proveWithdrawal(
uint256 _outputIndex, bytes32 _accountProof, address _l1Token, address _to, uint256 _amount
) external {
require(!challenged[outputRoots[_outputIndex].root], "Output root challenged");
require(block.timestamp >= outputRoots[_outputIndex].timestamp + CHALLENGE_PERIOD, "Not finalized");
// Verify Merkle proof against output root
// _accountProof proves account balance in L2 state
require(_verifyAccountProof(outputRoots[_outputIndex].stateRoot, _accountProof, _to, _amount), "Invalid proof");
// Release tokens
IERC20(_l1Token).safeTransfer(_to, _amount);
emit WithdrawalProven(_outputIndex, _to, _l1Token, _amount);
}
}
ZK Rollups
Architecture (zkSync Era, Starknet, Scroll, Polygon zkEVM, Linea)
# Validity proof model: "Guilty until proven innocent"
# 1. Sequencer/Prover executes batch, generates ZK proof
# 2. Proof attests: "This state transition is valid"
# 3. L1 verifier contract checks proof
# 4. If valid: state root finalized immediately
# 5. No challenge period needed
# Proof systems:
# - zkSync Era: PLONK (Boojum), custom VM (zkEVM)
# - Starknet: STARK (Cairo VM), recursive proving
# - Scroll: Halo2 (zkEVM), EVM-equivalent
# - Polygon zkEVM: PIL/STARK (Polygon zkEVM), EVM-equivalent
# - Linea: Vortex (Gnark/PLONK), EVM-equivalent
# - Taiko: Halo2, based sequencing (L1 proposes)
# Recursive proving: aggregate many batch proofs → single proof
# Verification cost: ~200-500k gas on Ethereum
ZK-EVM Designs
# Type 1: Ethereum-equivalent (consensus + execution)
# - Identical to Ethereum: same opcodes, gas, precompiles
# - Hardest to prove, max compatibility
# - Taiko, Scroll (target)
# Type 2: EVM-equivalent (execution only)
# - Same EVM semantics, minor gas/precompile diffs
# - Scroll, Polygon zkEVM, Linea
# Type 3: EVM-compatible (bytecode compatible)
# - Most opcodes, some precompiles missing, gas diffs
# - zkSync Era (LLVM IR → custom VM)
# Type 4: High-level language (Solidity/Vyper → custom IR)
# - Compile Solidity directly to circuit-friendly IR
# - Starknet (Cairo), zkSync (Yul → Boojum)
ZK Proof Generation Pipeline
# 1. Execution trace: record every VM step (opcode, stack, memory, storage)
# 2. Arithmetization: convert trace to polynomial constraints (AIR/R1CS/Plonkish)
# 3. Commitment: commit to polynomials (KZG, FRI, Hyrax)
# 4. Prove: generate proof that constraints satisfied
# 5. Verify: L1 contract checks proof
# Parallelization:
# - Segment trace into chunks
# - Prove chunks in parallel
# - Recursively aggregate chunk proofs
# Hardware acceleration:
# - GPU: MSM (multi-scalar multiplication), NTT (number theoretic transform)
# - FPGA/ASIC: custom circuits for specific proof systems
# - Distributed proving: cluster of provers (Gevulot, Nil, =nil;)
ZK Rollup Implementation (Verifier)
// Simplified ZK rollup verifier (Groth16 / PLONK style)
contract ZKRollupVerifier {
struct VerificationKey {
// Groth16: [alpha]1, [beta]2, [gamma]2, [delta]2, [IC]1
// PLONK: [Q_M]1, [Q_L]1, [Q_R]1, [Q_O]1, [Q_C]1, [S_σ1]1, [S_σ2]1, [S_σ3]1
}
struct Proof {
// Groth16: A ∈ G1, B ∈ G2, C ∈ G1
// PLONK: a, b, c ∈ G1, z ∈ G1, t_lo, t_mid, t_hi ∈ G1, W_ζ, W_ζω ∈ G1
}
VerificationKey public vk;
bytes32 public latestStateRoot;
constructor(VerificationKey _vk) {
vk = _vk;
}
// Verify batch proof and update state root
function verifyBatch(
bytes32 _prevStateRoot,
bytes32 _newStateRoot,
bytes calldata _publicInputs, // prevRoot, newRoot, batchHash, etc.
Proof calldata _proof
) external returns (bool) {
// Verify proof using precompile (bn254 pairing for Groth16)
// For PLONK: use Gnark verifier or custom PLONK verifier
bool valid = _verifyProof(vk, _publicInputs, _proof);
require(valid, "Invalid ZK proof");
latestStateRoot = _newStateRoot;
emit BatchVerified(_prevStateRoot, _newStateRoot);
return true;
}
// Withdraw using verified state root
function withdraw(
bytes32 _accountProof, // Merkle proof against latestStateRoot
address _l1Token, address _to, uint256 _amount
) external {
require(_verifyAccountProof(latestStateRoot, _accountProof, _to, _amount), "Invalid proof");
IERC20(_l1Token).safeTransfer(_to, _amount);
emit Withdrawal(_to, _l1Token, _amount);
}
// Internal: pairing check for Groth16
function _verifyProof(
VerificationKey memory _vk,
bytes memory _publicInputs,
Proof memory _proof
) internal view returns (bool) {
// Use precompile at 0x08 (bn256Pairing) for Groth16
// Or call PLONK verifier contract
}
}
Validiums & Volitions
Validium: ZK Proof + Off-chain DA
# Same validity proofs as ZK rollup, but data off-chain
# Data Availability Committee (DAC): N parties, threshold signatures
# - If DAC honest: same security as rollup
# - If DAC malicious: can withhold data → funds frozen (not stolen)
# Use cases:
# - High-throughput apps (gaming, social, orderbook DEX)
# - Enterprise: permissioned DAC
# - Cost-sensitive: DA cost ~100x cheaper than L1
# Examples:
# - StarkEx (dYdX, Immutable X, Sorare): STARK proofs + DAC
# - zkSync Porter: validium mode for specific accounts
# - Polygon Miden: validium option
Volition: User Choice Per Transaction
# Each transaction: user chooses DA mode
# - Rollup mode: data on L1 (higher cost, max security)
# - Validium mode: data off-chain (lower cost, DAC trust)
# - Seamless: same account, same UX
# Implementation:
# - Transaction includes DA mode flag
# - Prover generates proof for both modes
# - L1 verifier checks proof + DA mode validity
# StarkNet volitions (planned): account-level DA mode
# zkSync Porter: account-level validium/rollup choice
State Channels
Architecture
# Off-chain bilateral/multilateral state updates
# 1. Open: on-chain funding transaction (multisig or contract)
# 2. Exchange: signed states (nonce, balances, app data)
# 3. Close: submit final state on-chain
# 4. Dispute: challenge period for invalid final state
# Lightning Network (Bitcoin): HTLC-based payment channels
# Raiden Network (Ethereum): similar, ERC-20 support
# Perun: generic state channel framework (Go)
# Connext: cross-chain channels (xDAI, Polygon, etc.)
# Scaling: channel networks (route payments through intermediaries)
# - Atomic multi-hop via HTLCs
# - Liquidity management: rebalance, splice
Channel Implementation
// Simplified state channel (Ethereum)
contract StateChannel {
struct Channel {
address partyA;
address partyB;
uint256 nonce;
uint256 balanceA;
uint256 balanceB;
uint256 timeout; // Challenge period
bool closed;
bytes32 finalStateHash; // Hash of final signed state
}
mapping(bytes32 => Channel) public channels;
// Open channel
function openChannel(address _partyB, uint256 _depositA, uint256 _depositB, uint256 _timeout) external payable {
require(msg.value == _depositA, "Deposit mismatch");
// Party B must also deposit (via separate call or pre-funded)
bytes32 channelId = keccak256(abi.encode(msg.sender, _partyB, block.number));
channels[channelId] = Channel({
partyA: msg.sender,
partyB: _partyB,
nonce: 0,
balanceA: _depositA,
balanceB: _depositB,
timeout: _timeout,
closed: false,
finalStateHash: bytes32(0)
});
emit ChannelOpened(channelId, msg.sender, _partyB, _depositA, _depositB);
}
// Close cooperatively (both parties sign)
function closeChannel(bytes32 _channelId, uint256 _nonce, uint256 _balanceA, uint256 _balanceB, bytes calldata _sigA, bytes calldata _sigB) external {
Channel storage ch = channels[_channelId];
require(!ch.closed, "Already closed");
require(_nonce > ch.nonce, "Stale nonce");
require(_balanceA + _balanceB == ch.balanceA + ch.balanceB, "Invalid balances");
// Verify signatures
bytes32 stateHash = keccak256(abi.encode(_channelId, _nonce, _balanceA, _balanceB));
require(_verifySig(ch.partyA, stateHash, _sigA), "Invalid sig A");
require(_verifySig(ch.partyB, stateHash, _sigB), "Invalid sig B");
ch.closed = true;
ch.finalStateHash = stateHash;
ch.nonce = _nonce;
Comments
Post a Comment