Skip to main content
A deep dive into TeQoin’s sequencer - the component responsible for ordering transactions, producing blocks, and posting batches to Ethereum L1.
TL;DR:The sequencer is the heart of TeQoin L2. It receives transactions, orders them, executes them in the EVM, produces blocks every 5 seconds, and posts batches to Ethereum L1. While currently centralized for simplicity, the system is designed to be fraud-proof secure regardless of sequencer behavior.

🎯 What is the Sequencer?

The sequencer is the component that:
  • Receives transactions from users
  • Orders transactions deterministically
  • Executes transactions in the EVM
  • Produces L2 blocks
  • Posts transaction batches to L1

Role in the System

Think of the sequencer as:
  • The “block producer” for L2 (like miners/validators on L1)
  • The “coordinator” that orders transactions
  • The “publisher” that posts data to L1

🏗️ Sequencer Architecture

High-Level Components

User-facing transaction submission
    RPC Server (JSON-RPC 2.0)

    Accepts:
    - eth_sendRawTransaction
    - eth_call
    - eth_estimateGas
    - eth_getTransactionReceipt
    - etc.
    
    Returns:
    - Transaction hashes
    - Receipts
    - State queries
Endpoints:

🔄 Transaction Flow Through Sequencer

Complete Lifecycle

1

Transaction Submission

User submits transaction via RPC
    // User's wallet
    const tx = await signer.sendTransaction({
      to: '0x...',
      value: ethers.parseEther('1.0')
    });
    
    console.log('TX submitted:', tx.hash);
    // Returns immediately with TX hash
Sequencer receives:
    {
      "from": "0x...",
      "to": "0x...",
      "value": "1000000000000000000",
      "nonce": 42,
      "gasLimit": "21000",
      "gasPrice": "1000000000",
      "data": "0x",
      "signature": "0x..."
    }
2

Validation

Sequencer validates transaction
    function validateTransaction(tx) {
        // 1. Check signature
        const signer = recoverSigner(tx);
        if (signer !== tx.from) {
            throw new Error('Invalid signature');
        }
        
        // 2. Check nonce
        const expectedNonce = await getAccountNonce(tx.from);
        if (tx.nonce < expectedNonce) {
            throw new Error('Nonce too low');
        }
        
        // 3. Check balance
        const balance = await getAccountBalance(tx.from);
        const totalCost = tx.value + (tx.gasLimit * tx.gasPrice);
        if (balance < totalCost) {
            throw new Error('Insufficient funds');
        }
        
        // 4. Check gas limit
        if (tx.gasLimit < 21000) {
            throw new Error('Gas limit too low');
        }
        
        return true;
    }
3

Mempool Addition

Add to transaction pool
    // Add to appropriate queue
    if (tx.nonce === expectedNonce) {
        mempool.pending.push(tx);
        console.log('Added to pending:', tx.hash);
    } else {
        mempool.queued.push(tx);
        console.log('Added to queue:', tx.hash);
    }
    
    // Sort by gas price (if needed)
    mempool.pending.sort((a, b) => b.gasPrice - a.gasPrice);
4

Block Production (5 seconds)

Include in next block
    // Every 5 seconds
    setInterval(async () => {
        // Select transactions
        const txsToInclude = selectTransactions(mempool);
        
        // Execute in EVM
        const results = await executeTransactions(txsToInclude);
        
        // Create block
        const block = createBlock(txsToInclude, results);
        
        // Save and broadcast
        await saveBlock(block);
        broadcastBlock(block);
        
        console.log(`Block ${block.number} produced`);
    }, 5000);
User sees confirmation in ~5 seconds
5

Batching (~10 minutes)

Collect blocks into batch
    // Every 10 minutes (or when batch full)
    setInterval(async () => {
        const blocks = getUnpublishedBlocks();
        
        if (blocks.length === 0) return;
        
        const batch = {
            blockStart: blocks[0].number,
            blockEnd: blocks[blocks.length - 1].number,
            transactions: compressTransactions(blocks),
            prevStateRoot: blocks[0].parentStateRoot,
            newStateRoot: blocks[blocks.length - 1].stateRoot
        };
        
        await publishBatch(batch);
    }, 600000); // 10 minutes
6

L1 Publication

Post batch to Ethereum
    async function publishBatch(batch) {
        // Encode batch data
        const batchData = encodeBatch(batch);
        
        // Submit to L1
        const tx = await l1Contract.submitBatch(
            batchData,
            batch.prevStateRoot,
            batch.newStateRoot,
            {
                gasLimit: 500000,
                gasPrice: await getOptimalGasPrice()
            }
        );
        
        console.log('Batch submitted to L1:', tx.hash);
        await tx.wait();
        console.log('Batch confirmed on L1');
    }

📊 Transaction Ordering

How Sequencer Orders Transactions

First-In-First-Out ordering
    // Simple FIFO
    function selectTransactions(mempool) {
        // Return transactions in order received
        return mempool.pending.slice(0, BLOCK_SIZE_LIMIT);
    }
Characteristics:
  • ✅ Simple and predictable
  • ✅ No MEV (Maximal Extractable Value)
  • ✅ Fair to all users
  • ❌ No priority for urgent transactions
Current implementation on TeQoin

⚙️ Block Production Details

Block Structure

Block {
    // Header
    number: number,              // Block number
    timestamp: number,           // Block timestamp
    parentHash: bytes32,         // Previous block hash
    stateRoot: bytes32,          // State root after execution
    transactionsRoot: bytes32,   // Merkle root of transactions
    receiptsRoot: bytes32,       // Merkle root of receipts
    
    // Body
    transactions: Transaction[], // All transactions in block
    
    // Metadata
    gasUsed: bigint,            // Total gas used
    gasLimit: bigint,           // Block gas limit
    sequencer: address,         // Sequencer address
    l1BlockNumber: number       // L1 block reference
}

Block Production Algorithm

class BlockProducer {
    async produceBlock() {
        const startTime = Date.now();
        
        // 1. Select transactions
        const txs = this.selectTransactions();
        console.log(`Selected ${txs.length} transactions`);
        
        // 2. Execute transactions
        const executionResults = [];
        let gasUsed = 0n;
        
        for (const tx of txs) {
            try {
                const result = await this.evm.execute(tx);
                executionResults.push(result);
                gasUsed += result.gasUsed;
                
                if (gasUsed > this.BLOCK_GAS_LIMIT) {
                    console.log('Block gas limit reached');
                    break;
                }
            } catch (error) {
                console.log('TX execution failed:', tx.hash, error);
                // Include failed TX with error receipt
                executionResults.push({ error, gasUsed: tx.gasLimit });
            }
        }
        
        // 3. Update state
        const stateRoot = this.state.commit();
        
        // 4. Create block
        const block = {
            number: this.blockNumber++,
            timestamp: startTime,
            parentHash: this.lastBlockHash,
            stateRoot,
            transactionsRoot: this.merkleRoot(txs),
            receiptsRoot: this.merkleRoot(executionResults),
            transactions: txs,
            receipts: executionResults,
            gasUsed,
            gasLimit: this.BLOCK_GAS_LIMIT,
            sequencer: this.address,
            l1BlockNumber: await this.getL1BlockNumber()
        };
        
        // 5. Save and broadcast
        await this.db.saveBlock(block);
        this.broadcast('newBlock', block);
        
        const elapsed = Date.now() - startTime;
        console.log(`Block ${block.number} produced in ${elapsed}ms`);
        
        return block;
    }
}

📦 Batch Publishing

Batch Composition

Batch contents:
    Batch {
        // Range
        startBlock: number,
        endBlock: number,
        
        // Transaction data (compressed)
        transactions: bytes,
        
        // State commitments
        prevStateRoot: bytes32,
        newStateRoot: bytes32,
        
        // Metadata
        timestamp: number,
        sequencer: address,
        signature: bytes
    }
Typical batch:
  • 100-200 L2 blocks
  • 5,000-20,000 transactions
  • 500KB-2MB compressed data
How data is compressed:
    function compressBatch(blocks) {
        const txs = blocks.flatMap(b => b.transactions);
        
        // Remove redundant data
        const compressed = txs.map(tx => ({
            // Keep only essential fields
            nonce: tx.nonce,
            to: tx.to,
            value: tx.value,
            data: tx.data,
            // Remove: chainId, gasPrice, gasLimit (same for all)
        }));
        
        // Use delta encoding for nonces
        for (let i = 1; i < compressed.length; i++) {
            compressed[i].nonceDelta = 
                compressed[i].nonce - compressed[i-1].nonce;
            delete compressed[i].nonce;
        }
        
        // Compress with zlib
        return zlib.compress(JSON.stringify(compressed));
    }
Compression ratio: ~15:1Size comparison:
  • Uncompressed: 180 bytes/TX
  • Compressed: ~12 bytes/TX
When batches are published:
    class BatchScheduler {
        shouldPublish() {
            const timeSinceLastBatch = 
                Date.now() - this.lastBatchTime;
            const unpublishedBlocks = 
                this.getUnpublishedBlocks().length;
            const batchSize = this.estimateBatchSize();
            
            // Publish if:
            return (
                // 10 minutes elapsed
                timeSinceLastBatch > 10 * 60 * 1000 ||
                
                // 200 blocks accumulated
                unpublishedBlocks >= 200 ||
                
                // Batch size near limit
                batchSize > 1.5 * 1024 * 1024 // 1.5MB
            );
        }
    }
Typical frequency: Every 10 minutes

🔒 Sequencer Security

Centralized Sequencer Risks

Limited powers:Order transactions
  • Choose transaction ordering within blocks
  • Potentially front-run or sandwich
  • Mitigation: FIFO ordering policy
Delay transactions
  • Hold transactions in mempool temporarily
  • Mitigation: Users can force via L1
Choose when to post batches
  • Control batch timing (within limits)
  • Mitigation: Economic incentive to post regularly
These risks are limited and can be mitigated

🌐 Decentralization Roadmap

Path to Decentralized Sequencer

1

Phase 1: Single Sequencer (Current)

Current state:
  • One centralized sequencer
  • Operated by TeQoin team
  • Fraud-proof secured
  • Good enough for launch
Trade-offs:
  • ✅ Simple and efficient
  • ✅ Fast block times
  • ✅ Low operational cost
  • ❌ Single point of failure
  • ❌ Potential censorship
2

Phase 2: Backup Sequencers

Add redundancy:
  • Multiple sequencer nodes
  • Automatic failover
  • Load balancing
Benefits:
  • ✅ Higher availability
  • ✅ No downtime for maintenance
  • ✅ Geographic distribution
3

Phase 3: Sequencer Set

Decentralized sequencer rotation:
  • Multiple independent operators
  • Round-robin or random selection
  • Stake-based participation
Implementation:
    mapping(uint256 => address) public sequencerSchedule;
    
    function getSequencer(uint256 blockNumber) 
        public view returns (address) 
    {
        uint256 index = blockNumber % sequencerSet.length;
        return sequencerSet[index];
    }
4

Phase 4: Fully Decentralized

Open participation:
  • Anyone can run sequencer
  • Stake-based selection
  • MEV-resistant ordering
  • Decentralized governance
Similar to Ethereum validators

📊 Performance Metrics

Sequencer Performance

Block Time

5 secondsFixed block interval

Throughput

1000+ TPSTransactions per second

Latency

< 100msTransaction confirmation

Block Size

30M gasMaximum per block

Batch Interval

~10 minutesL1 posting frequency

Uptime

99.9%Sequencer availability

🛠️ For Node Operators

Running a Sequencer Node

Currently Not OpenSequencer operation is currently restricted to the TeQoin team. Public sequencer participation will be enabled in Phase 3 of decentralization.
Future requirements (Phase 3+):
Hardware:
  CPU: 16+ cores
  RAM: 64GB+
  Storage: 2TB NVMe SSD
  Network: 1Gbps+

Software:
  OS: Ubuntu 22.04 LTS
  Docker: Latest
  Geth: Custom TeQoin build

Stake:
  Minimum: 10,000 ETH
  Locked for: 30 days
  Slashing: Up to 100% for fraud

📚 Further Reading

Optimistic Rollup

How the overall system works

Fraud Proofs

How invalid sequencer behavior is prevented

Security Model

Complete security analysis

Technical Overview

Overall architecture

Understand the sequencer? Continue to Security Model - the final architecture page! →