Skip to main content
Understanding TeQoin’s fraud proof mechanism that ensures state validity and protects user funds without requiring trust.
TL;DR:Fraud proofs allow anyone to prove that a sequencer posted an invalid state transition. If fraud is detected, a cryptographic proof is submitted to L1, the invalid state is reverted, and the sequencer is penalized. This ensures security without trusting the sequencer.

🎯 What is a Fraud Proof?

A fraud proof is cryptographic evidence submitted to Ethereum L1 that proves the sequencer posted an invalid state transition.

The Problem Fraud Proofs Solve

Without fraud proofs:
- Must trust sequencer completely
- No way to verify state is correct
- Sequencer could steal funds
- Centralized security model

With fraud proofs:
- Don't need to trust sequencer
- Anyone can verify state
- Invalid states are proven and reverted
- Decentralized security model

How They Work (Simple)


🔍 Fraud Detection Process

Step-by-Step Detection

1

Sequencer Posts Batch

Sequencer submits batch to L1 with state root
    // L1 contract receives batch
    function submitBatch(
        bytes calldata batchData,
        bytes32 prevStateRoot,
        bytes32 newStateRoot
    ) external onlySequencer {
        batches[batchIndex] = Batch({
            data: batchData,
            prevRoot: prevStateRoot,
            newRoot: newStateRoot,
            timestamp: block.timestamp
        });
    }
Sequencer claims: “Executing these transactions on prevRoot produces newRoot”
2

Verifier Downloads Data

Independent verifier downloads batch from L1
    // Verifier node retrieves batch
    const batch = await l1Contract.getBatch(batchIndex);
    const transactions = decodeBatchData(batch.data);
    
    console.log(`Verifying batch ${batchIndex}`);
    console.log(`Transactions: ${transactions.length}`);
All data is publicly available on L1
3

Re-Execute Transactions

Verifier re-executes all transactions locally
    // Start from previous state root
    let currentState = loadState(batch.prevRoot);
    
    // Execute each transaction
    for (const tx of transactions) {
        currentState = executeTransaction(currentState, tx);
    }
    
    // Compute resulting state root
    const computedRoot = currentState.getMerkleRoot();
Verifier computes what the state should be
4

Compare State Roots

Compare computed root with sequencer’s claimed root
    if (computedRoot === batch.newRoot) {
        console.log('✅ Batch is valid');
        // No action needed
    } else {
        console.log('❌ FRAUD DETECTED!');
        console.log(`Expected: ${computedRoot}`);
        console.log(`Claimed:  ${batch.newRoot}`);
        
        // Prepare fraud proof
        generateFraudProof(batch, computedRoot);
    }
Mismatch = fraud detected!
5

Generate Fraud Proof

Create proof showing the invalid transaction
    function generateFraudProof(batch, correctRoot) {
        // Binary search to find first invalid TX
        const invalidTxIndex = findInvalidTransaction(batch);
        const invalidTx = batch.transactions[invalidTxIndex];
        
        // Build proof components
        const proof = {
            batchIndex: batch.index,
            txIndex: invalidTxIndex,
            transaction: invalidTx,
            preState: getStateBeforeTx(invalidTxIndex),
            postStateClaimed: getClaimedStateAfterTx(invalidTxIndex),
            postStateCorrect: getCorrectStateAfterTx(invalidTxIndex),
            merkleProofs: generateMerkleProofs(...)
        };
        
        return proof;
    }
6

Submit to L1

Submit fraud proof to L1 contract
    // Submit fraud proof transaction
    const tx = await l1Contract.proveFraud(
        proof.batchIndex,
        proof.txIndex,
        proof.transaction,
        proof.preState,
        proof.postStateClaimed,
        proof.postStateCorrect,
        proof.merkleProofs
    );
    
    await tx.wait();
    console.log('✅ Fraud proof submitted!');
7

L1 Verification

L1 contract verifies proof on-chain
    function proveFraud(
        uint256 batchIndex,
        uint256 txIndex,
        bytes calldata transaction,
        bytes32 preState,
        bytes32 claimedPostState,
        bytes32 correctPostState,
        bytes32[] calldata merkleProofs
    ) external {
        // 1. Verify merkle proofs
        require(verifyMerkleProofs(...), "Invalid proofs");
        
        // 2. Execute transaction on-chain
        bytes32 result = executeTransactionOnChain(preState, transaction);
        
        // 3. Check if result matches claimed state
        if (result != claimedPostState) {
            // Fraud proven!
            revertBatch(batchIndex);
            slashSequencer();
            rewardChallenger(msg.sender);
        }
    }

🧮 Fraud Proof Components

What’s in a Fraud Proof?

Essential data in a fraud proof:
    FraudProof {
        // Identification
        batchIndex: number,
        txIndex: number,
        
        // Transaction data
        transaction: {
            from: address,
            to: address,
            value: bigint,
            data: bytes,
            nonce: number,
            signature: bytes
        },
        
        // State information
        preStateRoot: bytes32,      // Before invalid TX
        claimedPostState: bytes32,   // What sequencer claimed
        correctPostState: bytes32,   // What it should be
        
        // Proof data
        merkleProofs: bytes32[],     // Prove state access
        witnesses: bytes[]           // Additional data needed
    }

💻 On-Chain Verification

How L1 Verifies Fraud Proofs

Check proof is well-formed:
    function validateProof(FraudProof calldata proof) internal view {
        // Check batch exists
        require(batches[proof.batchIndex].exists, "Batch not found");
        
        // Check TX index in range
        require(proof.txIndex < batches[proof.batchIndex].txCount, "Invalid TX index");
        
        // Check challenge period active
        require(
            block.timestamp < batches[proof.batchIndex].timestamp + CHALLENGE_PERIOD,
            "Challenge period expired"
        );
        
        // Check merkle proofs valid
        require(verifyMerkleProofs(proof), "Invalid merkle proofs");
    }
Execute transaction on L1:
    function executeTransactionOnChain(
        bytes32 preStateRoot,
        Transaction calldata tx,
        bytes32[] calldata merkleProofs
    ) internal returns (bytes32 postStateRoot) {
        // Load account states from merkle proofs
        Account memory sender = loadAccount(tx.from, preStateRoot, merkleProofs);
        Account memory recipient = loadAccount(tx.to, preStateRoot, merkleProofs);
        
        // Validate transaction
        require(sender.nonce == tx.nonce, "Invalid nonce");
        require(sender.balance >= tx.value, "Insufficient balance");
        require(ecrecover(tx.hash, tx.signature) == tx.from, "Invalid signature");
        
        // Execute state changes
        sender.balance -= tx.value;
        sender.nonce += 1;
        recipient.balance += tx.value;
        
        // If contract call, execute code
        if (recipient.code.length > 0) {
            bytes memory result = executeEVMCode(recipient.code, tx.data);
            // Apply storage changes
        }
        
        // Compute new state root
        postStateRoot = computeNewRoot(sender, recipient, merkleProofs);
    }
Only ONE transaction executed on L1 (not entire batch)
Check if fraud is proven:
    function verifyFraud(FraudProof calldata proof) external {
        // Execute transaction
        bytes32 computedPostState = executeTransactionOnChain(
            proof.preStateRoot,
            proof.transaction,
            proof.merkleProofs
        );
        
        // Compare with claimed state
        if (computedPostState != proof.claimedPostState) {
            // FRAUD PROVEN!
            
            // Revert batch
            delete batches[proof.batchIndex];
            
            // Slash sequencer
            uint256 slashAmount = sequencerStake / 2;
            sequencerStake -= slashAmount;
            
            // Reward challenger
            payable(msg.sender).transfer(slashAmount);
            
            emit FraudProven(proof.batchIndex, msg.sender, slashAmount);
        } else {
            // Fraud proof invalid
            revert("Fraud proof invalid");
        }
    }

⚖️ Economic Incentives

Game Theory Analysis

Why sequencer stays honest:
    Sequencer Stakes: $10,000,000
    Daily Revenue: $100,000
    Annual Revenue: $36,500,000
    
    If Honest:
    - Keep earning $100K/day
    - Maintain $10M stake
    - Build long-term value
    
    If Dishonest:
    - Lose $10M stake (slashed)
    - Lose all future revenue
    - Get caught within 7 days
    - Reputation destroyed
    
    Expected Value:
    Honest: $10M + $36.5M/year = infinite (ongoing)
    Dishonest: -$10M (one-time loss)
    
    Rational Choice: Stay Honest

🕐 Challenge Period Timeline

7-Day Window Explained

Why 7 days?

Time for Detection

Verifiers need time to:
  • Download batch data
  • Re-execute transactions
  • Detect any fraud

Time for Proof Generation

If fraud found, need time to:
  • Identify invalid transaction
  • Generate merkle proofs
  • Construct fraud proof

Time for Submission

Need buffer for:
  • Network congestion
  • Gas price spikes
  • Submission delays

Geographic Distribution

Allow for:
  • Different time zones
  • Global verifier network
  • Redundancy in monitoring

🛡️ Security Guarantees

What Fraud Proofs Ensure

1

No Fund Theft

Guarantee: Sequencer cannot steal user fundsWhy:
  • Any fraudulent withdrawal would change state root
  • Verifiers would detect mismatch
  • Fraud proof submitted
  • Invalid state reverted
  • Funds returned to rightful owners
2

No Censorship

Guarantee: Users can always force transactionsWhy:
  • Users can submit transactions directly to L1
  • L1 forces sequencer to include them
  • If sequencer censors, state root won’t match
  • Fraud proof submitted
  • Censoring sequencer slashed
3

State Validity

Guarantee: All state transitions are validWhy:
  • Every batch must include transaction data on L1
  • Anyone can re-execute and verify
  • Invalid states can be proven wrong
  • Economic incentive ensures verification
4

Data Availability

Guarantee: Users can always recover their fundsWhy:
  • All transaction data on L1
  • Even if sequencer disappears
  • Users can reconstruct L2 state
  • Submit forced withdrawal via L1

🔬 Advanced Topics

More efficient proof system:Instead of proving entire batch invalid in one step:
    Round 1: Challenger claims batch invalid
    Round 2: Sequencer provides execution trace
    Round 3: Binary search to narrow down invalid step
    Round 4: Execute single EVM opcode on-chain
    
    Example:
    Batch has 10,000 transactions
    
    Without interactive proofs:
    - Execute all 10,000 on L1 (expensive!)
    
    With interactive proofs:
    - Binary search: log2(10,000) ≈ 14 rounds
    - Execute 1 opcode on L1 (very cheap!)
    
    Gas savings: 10,000x
Trade-offs:
  • Pro: Much cheaper to verify
  • Pro: Scales to larger batches
  • Con: Takes multiple rounds (hours)
  • Con: More complex protocol
Prevent spam attacks:Problem: Attacker submits fake fraud proofs to waste gasSolution: Require bond to challenge
    uint256 constant CHALLENGE_BOND = 1 ether;
    
    function submitFraudProof(FraudProof calldata proof) external payable {
        require(msg.value >= CHALLENGE_BOND, "Insufficient bond");
        
        // Verify proof
        if (verifyFraud(proof)) {
            // Fraud proven - return bond + reward
            payable(msg.sender).transfer(msg.value + sequencerSlash);
        } else {
            // Invalid proof - lose bond
            // Bond goes to sequencer
        }
    }
Result:
  • Valid challenges are profitable
  • Invalid challenges lose money
  • Spam attacks economically irrational
Only prove fraud when necessary:Normal operation:
    Sequencer posts batches
    → Verifiers monitor silently
    → No fraud → No proofs submitted
    → Efficient operation
Only when fraud detected:
    Fraud detected
    → Proof generated
    → Submitted to L1
    → Gas cost incurred
Benefits:
  • Zero overhead in normal case (99.99% of time)
  • Only pay for fraud proof when needed
  • Scales indefinitely
This is why optimistic rollups are “optimistic”

📊 Fraud Proof Statistics

Real-World Data

Fraud Attempts

0Zero fraud attempts on TeQoin (and Optimism, Arbitrum)

Fraud Proofs Submitted

0No fraud proofs needed in production

Verification Efficiency

99.99%Silent verification, no on-chain cost
Why no fraud?
  1. Economic deterrent is strong
    • Sequencer would lose everything
    • No rational incentive to commit fraud
  2. Monitoring is active
    • Multiple independent verifiers
    • Automated fraud detection
    • Would be caught immediately
  3. Penalty is severe
    • Complete loss of stake
    • Loss of sequencer position
    • Permanent reputation damage
Result: Fraud proofs work by deterring fraud, not just detecting it.

🎓 For Developers

Running a Verifier Node

# Clone verifier software
git clone https://github.com/TeQoin/verifier
cd verifier

# Install dependencies
npm install

# Configure
cp .env.example .env
# Edit .env with your settings

# Run verifier
npm start

📚 Further Reading

Optimistic Rollup

How optimistic rollups work

Security Model

Complete security analysis

Challenge Period

Why withdrawals take 7 days

Sequencer Design

Block production system

Understand fraud proofs? Continue to Sequencer Design