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

Popular posts from this blog

Firmware Testing Strategies: Hardware-in-the-Loop, Simulation, and CI/CD Pipelines | Embedded Systems Hub

IoT Protocols Compared: MQTT, CoAP, HTTP/2, WebSocket - Performance, Overhead, and Use Cases | Embedded Systems Hub

Embedded Security: Secure Boot, ARM TrustZone, and OTA Update Integrity | Embedded Systems Hub