Layer 2 Scaling Solutions

Building Layer 2 solutions: Rollups, State Channels, and Sidechains for blockchain scaling.

Advanced⏱️ 55 min📚 Prerequisites: 2

Layer 2 Scaling Solutions

Layer 2 solutions scale blockchains by processing transactions off-chain while maintaining security through the main chain.

The Scaling Problem

Blockchains face a scalability trilemma:

  1. Decentralization: Many nodes
  2. Security: Strong guarantees
  3. Scalability: High throughput

You can optimize for two, but not all three.

Layer 2 Solutions

Rollups

Rollups bundle many transactions and submit a summary (with proof) to the main chain.

Optimistic Rollups

  • Assume transactions are valid
  • Use fraud proofs to challenge invalid transactions
  • Examples: Arbitrum, Optimism

Zero-Knowledge Rollups (ZK-Rollups)

  • Use ZK proofs to verify transaction validity
  • No challenge period needed
  • Examples: zkSync, StarkNet, Polygon zkEVM

State Channels

State Channels allow parties to transact off-chain, only settling on-chain when needed.

  • Open channel: On-chain transaction
  • Multiple transactions: Off-chain (fast, cheap)
  • Close channel: Final state on-chain

Examples: Lightning Network (Bitcoin), Raiden (Ethereum)

Sidechains

Sidechains are separate blockchains connected to the main chain.

  • Independent consensus mechanism
  • Assets can move between chains
  • Examples: Polygon PoS, xDai

Rollup Implementation

RUST
struct RollupBatch {
    transactions: Vec<Transaction>,
    state_root: String,
    proof: Vec<u8>, // ZK proof or fraud proof
    timestamp: u64,
}

struct RollupContract {
    batches: Vec<RollupBatch>,
    state_root: String,
}

impl RollupContract {
    fn submit_batch(&mut self, batch: RollupBatch) -> Result<(), String> {
        // Verify proof
        if !self.verify_batch(&batch) {
            return Err(String::from("Invalid batch"));
        }
        
        // Update state root
        self.state_root = batch.state_root.clone();
        self.batches.push(batch);
        
        Ok(())
    }
    
    fn verify_batch(&self, batch: &RollupBatch) -> bool {
        // Verify ZK proof or check fraud proof
        true
    }
}

State Channel Implementation

RUST
struct ChannelState {
    alice_balance: u64,
    bob_balance: u64,
    sequence_number: u64,
}

struct StateChannel {
    participants: (String, String),
    state: ChannelState,
    is_open: bool,
}

impl StateChannel {
    fn update_state(&mut self, new_state: ChannelState) {
        // Both parties sign the new state
        // State is valid if properly signed
        self.state = new_state;
    }
    
    fn close(&self) -> ChannelState {
        // Final state submitted to main chain
        self.state.clone()
    }
}

Benefits

  • Throughput: Process thousands of transactions per second
  • Cost: Lower fees (amortized across batch)
  • Speed: Near-instant confirmation
  • Security: Inherits main chain security

Trade-offs

  • Complexity: More complex than Layer 1
  • Liquidity: Some solutions require locked funds
  • Centralization Risk: Some operators needed
  • Withdrawal Delays: Time to withdraw to main chain

Real-World Usage

  • Ethereum: Most L2 activity (Arbitrum, Optimism, zkSync)
  • Bitcoin: Lightning Network for payments
  • Polygon: Multiple L2 solutions

Choosing a Solution

  • High volume, low value: Rollups
  • Frequent transactions between same parties: State channels
  • Independent needs: Sidechains

Code Examples

Rollup Batch

Implementing a rollup batch system

RUST
struct Transaction {
    from: String,
    to: String,
    amount: u64,
}

struct RollupBatch {
    transactions: Vec<Transaction>,
    state_root: String,
    proof: String, // Simplified proof
    batch_number: u64,
}

struct RollupContract {
    batches: Vec<RollupBatch>,
    current_state_root: String,
}

impl RollupContract {
    fn new() -> Self {
        RollupContract {
            batches: Vec::new(),
            current_state_root: String::from("genesis"),
        }
    }
    
    fn submit_batch(&mut self, batch: RollupBatch) -> Result<(), String> {
        // Verify batch proof (simplified)
        if batch.proof != "valid_proof" {
            return Err(String::from("Invalid proof"));
        }
        
        // Verify state transition
        if !self.verify_state_transition(&batch) {
            return Err(String::from("Invalid state transition"));
        }
        
        // Update state
        self.current_state_root = batch.state_root.clone();
        self.batches.push(batch);
        
        Ok(())
    }
    
    fn verify_state_transition(&self, batch: &RollupBatch) -> bool {
        // In real implementation: verify ZK proof or check fraud proof
        // Here simplified: check that state root is different
        batch.state_root != self.current_state_root
    }
    
    fn get_batch_count(&self) -> usize {
        self.batches.len()
    }
    
    fn get_total_transactions(&self) -> usize {
        self.batches.iter().map(|b| b.transactions.len()).sum()
    }
}

fn main() {
    let mut rollup = RollupContract::new();
    
    // Create a batch with multiple transactions
    let batch = RollupBatch {
        transactions: vec![
            Transaction { from: String::from("0xAlice"), to: String::from("0xBob"), amount: 100 },
            Transaction { from: String::from("0xBob"), to: String::from("0xCharlie"), amount: 50 },
            Transaction { from: String::from("0xCharlie"), to: String::from("0xAlice"), amount: 25 },
        ],
        state_root: String::from("new_state_root"),
        proof: String::from("valid_proof"),
        batch_number: 1,
    };
    
    match rollup.submit_batch(batch) {
        Ok(_) => {
            println!("Batch submitted successfully!");
            println!("Total batches: {}", rollup.get_batch_count());
            println!("Total transactions: {}", rollup.get_total_transactions());
            println!("Current state root: {}", rollup.current_state_root);
        }
        Err(e) => println!("Error: {}", e),
    }
}

Explanation:

Rollups bundle many transactions into a single batch. The batch includes a proof (ZK or fraud proof) and a new state root. The main chain only needs to verify the proof, not process each transaction individually, dramatically improving throughput.

State Channel

Implementing a state channel for off-chain transactions

RUST
use std::collections::HashMap;

struct ChannelState {
    balances: HashMap<String, u64>,
    sequence: u64,
    is_final: bool,
}

struct StateChannel {
    participants: Vec<String>,
    state: ChannelState,
    is_open: bool,
}

impl StateChannel {
    fn open(participants: Vec<String>, initial_balances: HashMap<String, u64>) -> Self {
        StateChannel {
            participants: participants.clone(),
            state: ChannelState {
                balances: initial_balances,
                sequence: 0,
                is_final: false,
            },
            is_open: true,
        }
    }
    
    fn update_balance(&mut self, from: &str, to: &str, amount: u64) -> Result<(), String> {
        if !self.is_open {
            return Err(String::from("Channel is closed"));
        }
        
        let from_balance = self.state.balances.get(from).copied().unwrap_or(0);
        if from_balance < amount {
            return Err(String::from("Insufficient balance"));
        }
        
        // Update balances off-chain (no main chain transaction needed!)
        *self.state.balances.entry(from.to_string()).or_insert(0) -= amount;
        *self.state.balances.entry(to.to_string()).or_insert(0) += amount;
        self.state.sequence += 1;
        
        Ok(())
    }
    
    fn close(&mut self) -> ChannelState {
        self.is_open = false;
        self.state.is_final = true;
        self.state.clone()
    }
}

fn main() {
    let mut balances = HashMap::new();
    balances.insert(String::from("Alice"), 1000);
    balances.insert(String::from("Bob"), 500);
    
    let mut channel = StateChannel::open(
        vec![String::from("Alice"), String::from("Bob")],
        balances,
    );
    
    println!("Channel opened");
    
    // Multiple off-chain transactions (fast, free!)
    channel.update_balance("Alice", "Bob", 100).unwrap();
    channel.update_balance("Bob", "Alice", 50).unwrap();
    channel.update_balance("Alice", "Bob", 25).unwrap();
    
    println!("Performed 3 transactions off-chain");
    println!("Sequence number: {}", channel.state.sequence);
    
    // Close channel and settle on-chain
    let final_state = channel.close();
    println!("Channel closed. Final balances:");
    for (participant, balance) in &final_state.balances {
        println!("  {}: {}", participant, balance);
    }
}

Explanation:

State channels allow parties to transact off-chain many times, only settling the final state on-chain. This enables instant, free transactions between the same parties while maintaining security through the main chain.

Exercises

Simple Rollup

Create a simple rollup that batches transactions!

Medium

Starter Code:

RUST
struct Transaction {
    amount: u64,
}

struct RollupBatch {
    transactions: Vec<Transaction>,
}

fn main() {
    // Create a batch with multiple transactions
    // Submit the batch
}