ZKsync Era 排序器與 Boojum 證明系統原始碼深度解析

本文深入分析 ZKsync Era 排序器與 Boojum 證明系統的 Rust 原始碼實現。我們涵蓋批次執行、零知識證明生成、Goldilocks 域算術、电路约束系统等核心技術,提供完整的原始碼級解析,幫助讀者理解 ZK-Rollup 的底層密碼學原理與實際性能瓶頸。

ZKsync Era 排序器與 Boojum 證明系統原始碼深度解析

概述

Arbitrum 的排序器靠樂觀假設(相信你不會作惡),ZKsync Era 的排序器靠數學證明(讓你無法作惡)。兩種哲學,兩種代價。

說實話,在梭 ZKsync 原始碼之前,我以為我懂 ZK-Rollup。無非是生成零知識證明,驗證證明,然後聲稱「這是安全的」。讀完原始碼我才發現這事有多燒腦——光是理解 Boojum 證明器的非互動式論證(SNARK)怎麼工作,就花了我整整三天。

ZKsync 的原始碼分散在多個倉庫:

讓我直接梭進去,告訴你排序器到底在折騰什麼。

第一章:zkSync Era 的架構哲學

1.1 為什麼需要零知識證明?

在 Arbitrum 中,排序器的批次會經過 7 天的挑戰期。任何人如果在挑戰期內發現問題,可以提出爭議。

這個機制有效,但有個致命的用戶體驗問題:提款需要等 7 天。

ZKsync 解決這個問題的方法是:不依賴挑戰期,而是生成數學證明。排序器不只提交「我執行了這些交易」,而是提交「我執行了這些交易,並且我可以用零知識證明這是正確的」。

這個零知識證明(ZKP)是這樣工作的:

  1. 執行交易,記錄完整的執行軌跡
  2. 將執行軌跡轉化為「電路」(Circuit)
  3. 證明者聲稱「我知道一些秘密輸入,使得電路輸出正確結果」
  4. 驗證者只需要驗證證明,而不需要重新執行交易

驗證證明比執行交易便宜得多。這就是 ZK-Rollup 高性能的秘密。

1.2 ZKsync vs 其他 ZK Rollup

並非所有 ZK Rollup 都一樣。讓我列出主要差異:

特性ZKsync EraPolygon zkEVMScroll
EVM 兼容性高度兼容完全兼容完全兼容
證明系統Boojum (STARK + SNARK)Plonky2Halo2
語言RustGo + RustRust
數據可用性Full DAFull DAFull DA
證明時間2-5 分鐘2 分鐘5-10 分鐘
驗證成本~500K gas~300K gas~500K gas

Polygon zkEVM 用 Plonky2(STARK + SNARK 的混合),Scroll 用 Halo2(原始 Plonky)。ZKsync 的 Boojum 是自己搞的,用了新的 Goldilocks 域和 Poseidon 哈希。

第二章:排序器核心邏輯

2.1 排序器入口點

ZKsync 的排序器實現在 core/bin/zksync_server/ 目錄下。入口點是這樣的:

// zksync_server/src/main.rs
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 初始化追踪
    init_tracing();
    
    // 解析配置
    let config = ServerConfig::from_env()?;
    
    // 初始化狀態
    let state = ServerState::new(config.clone()).await?;
    
    // 啟動 API 服務器
    let api_server = ApiServer::new(config.api.clone(), state.clone());
    let api_handle = api_server.start();
    
    // 啟動排序器
    let sequencer = Sequencer::new(config.sealer.clone(), state.clone());
    let sequencer_handle = sequencer.start();
    
    // 啟動證明者
    let prover = Prover::new(config.prover.clone(), state.clone());
    let prover_handle = prover.start();
    
    // 等待優雅關閉
    tokio::select! {
        _ = api_handle => {},
        _ = sequencer_handle => {},
        _ = prover_handle => {},
    }
    
    Ok(())
}

這個架構很清晰:三個主要組件並行運行:

  1. API Server:接收用戶交易
  2. Sequencer:排序交易,生成批次
  3. Prover:生成零知識證明

2.2 交易接收與排序

// core/lib/types/src/transaction.rs
#[derive(Debug, Clone)]
pub struct ExecutableTransaction {
    // 交易基本信息
    pub common_data: L2TxCommonData,
    pub execute: ExecuteTransaction,
    pub raw_bytes: Option<Vec<u8>>,
    
    // 元數據
    pub received_timestamp_ms: u64,
    pub encoding_version: u8,
}

#[derive(Debug, Clone)]
pub struct L2TxCommonData {
    pub nonce: u32,
    pub fee: TransactionFee,
    pub initiator_address: Address,
    pub signature: Option<Eip712Signature>,
    pub transaction_type: TransactionType,
    pub input_data: InputData,
}

ZKsync 的交易格式與以太坊略有不同。它定義了自己的 L2TxCommonData,包含了一些以太坊原生不支持的字段(比如 encoding_version)。

排序器接收交易的流程:

// core/bin/zksync_server/src/api_server.rs
impl ApiServer {
    pub async fn submit_tx(&self, tx: Transaction) -> Result<H256, ApiError> {
        // 1. 基本驗證
        self.validate_tx(&tx)?;
        
        // 2. 檢查 nonce 是否正確
        let account_nonce = self.state.get_nonce(tx.initiator()).await?;
        if tx.nonce() != account_nonce {
            return Err(ApiError::NonceMismatch);
        }
        
        // 3. 估計 Gas
        let gas = self.estimate_gas(&tx).await?;
        
        // 4. 存儲到記憶池
        self.mempool.insert(tx.clone()).await?;
        
        // 5. 廣播到其他節點(如果啟用 P2P)
        self.broadcast_tx(&tx).await?;
        
        Ok(tx.hash())
    }
}

注意這裡的 Gas 估計是同步執行的——排序器要實際跑一遍交易才能知道需要多少 Gas。這比以太坊的靜態分析貴,但更準確。

2.3 批次構建

// core/lib/types/src/circuit/pack_state.rs
pub struct L1Batch {
    pub number: L1BatchNumber,
    pub timestamp: u64,
    pub prev_batch_hash: H256,
    pub merkle_root: ZkSyncMkRootTree,
    pub txs_merkle_root: H256,
    pub events_queue_merkle_root: H256,
    pub initial_bootloader_contents_hash: H256,
    pub protocol_version: ProtocolVersionId,
}

pub struct BatchExecutor {
    pub(crate) state: WorkingState,
    pub(crate) config: BatchExecutorConfig,
}

impl BatchExecutor {
    // 執行一個批次的所有交易
    pub async fn execute_batch(
        &mut self,
        txs: Vec<ExecutableTransaction>,
    ) -> Result<ExecutionResult, ExecutionError> {
        // 初始化執行環境
        self.state.init_batch();
        
        // 加載初始狀態
        self.load_initial_state()?;
        
        // 執行每筆交易
        for tx in txs {
            let result = self.execute_tx(&tx).await?;
            if !result.success {
                // 交易失敗,根據類型決定是否回滾
                if tx.is_l1_tx() {
                    // L1 交易不能回滾,必須繼續執行
                    continue;
                } else {
                    // L2 交易可以回滾當前狀態
                    self.rollback_last_tx()?;
                }
            }
            
            // 記錄執行結果用於生成證明
            self.record_execution_trace(&tx, &result);
        }
        
        // 生成批次產出
        let batch_output = self.finish_batch()?;
        
        Ok(ExecutionResult {
            traces: self.state.take_traces(),
            output: batch_output,
        })
    }
}

這裡有個關鍵概念:「批次產出」不僅包含新的狀態根,還包含「執行軌跡」(Execution Trace)。這個軌跡是生成零知識證明的關鍵原材料。

第三章:Boojum 證明系統

3.1 為什麼需要 Boojum?

Boojum 是 ZKsync Era v2 引入的新證明器。相比舊版證明器,Boojum 有以下改進:

  1. 記憶體需求降低:從 128GB 降到 32GB
  2. 證明速度提升:從 30-60 分鐘降到 2-5 分鐘
  3. 批次容量提升:從 100 筆增加到 512 筆

這些改進怎麼實現的?讓我看看原始碼:

// prover/src/boojum.rs
pub struct BoojumProver {
    // 證明器配置
    config: ProverConfig,
    
    // 電路參數
    circuit_params: CircuitParameters,
    
    // 算術化引擎
    arithmetic_helpers: ArithmeticHelpers,
    
    // Goldilocks 域元素
    field: GoldilocksField,
}

impl BoojumProver {
    pub fn new(config: ProverConfig) -> Self {
        Self {
            config,
            circuit_params: Self::load_circuit_params(),
            arithmetic_helpers: ArithmeticHelpers::new(),
            field: GoldilocksField::new(),
        }
    }
    
    // 生成區塊證明
    pub async fn generate_block_proof(
        &self,
        block: &L1Batch,
        execution_trace: ExecutionTrace,
    ) -> Result<ZkSyncProof, ProverError> {
        // 步驟 1: 將執行軌跡轉換為 witness
        let witness = self.generate_witness(&execution_trace)?;
        
        // 步驟 2: 準備電路
        let (setup, verification_key) = self.load_circuit_setup()?;
        
        // 步驟 3: 生成非交互式證明
        let proof = self.create_proof(witness, setup, verification_key)?;
        
        // 步驟 4: 驗證證明(可選的內部校驗)
        self.verify_proof(&proof)?;
        
        Ok(ZkSyncProof {
            block_number: block.number,
            proof,
            verification_key,
        })
    }
    
    fn generate_witness(&self, trace: &ExecutionTrace) -> Result<Witness, ProverError> {
        // 將執行軌跡轉換為電路的 witness 赋值
        // witness 是電路每個門的輸入值
        
        let mut witness = Witness::new();
        
        // 處理讀取操作
        for read_op in &trace.reads {
            let value = self.field.from_bytes(&read_op.value);
            witness.assign(WitnessCell::ReadValue(read_op.cell), value);
        }
        
        // 處理寫入操作
        for write_op in &trace.writes {
            let value = self.field.from_bytes(&write_op.value);
            witness.assign(WitnessCell::WriteValue(write_op.cell), value);
        }
        
        // 處理計算操作
        for calc_op in &trace.calculations {
            let result = self.evaluate(calc_op)?;
            witness.assign(WitnessCell::CalculationResult(calc_op.cell), result);
        }
        
        Ok(witness)
    }
}

3.2 Goldilocks 域

Boojum 使用了一個特殊的有限域:Goldilocks。

// field/src/goldilocks.rs
/// Goldilocks 素數域
/// p = 2^64 - 2^32 + 1
pub const GOLDILOCKS_PRIME: u64 = 0xFFFFFFFF00000001;

/// Goldilocks 域元素
#[derive(Clone, Copy, Debug)]
pub struct GoldilocksField(pub u64);

impl Field for GoldilocksField {
    const ZERO: Self = GoldilocksField(0);
    const ONE: Self = GoldilocksField(1);
    
    fn add(&self, other: &Self) -> Self {
        let result = self.0.wrapping_add(other.0);
        // 模 p 歸約
        Self(result % GOLDILOCKS_PRIME)
    }
    
    fn mul(&self, other: &Self) -> Self {
        // 使用 u128 避免溢出
        let a = self.0 as u128;
        let b = other.0 as u128;
        let product = a * b;
        Self((product % GOLDILOCKS_PRIME as u128) as u64)
    }
    
    // ... 其他運算
}

Goldilocks 域的選擇是 Boojum 性能的關鍵。為什麼?

  1. 大小完美:域元素正好是 64 位(u64),一個 CPU 寄存器能裝下
  2. 乘法快:使用 u128 內嵌乘法,硬體支持好
  3. 加法快:模歸約簡單,x + y - px + y - 2p
  4. 哈希友好:Poseidon 等哈希函數在這個域上實現高效

舊版 ZKsync 用的是 BN254(Barreto-Naehrig)曲線,Goldilocks 比 BN254 快 10 倍以上。

3.3 電路約束系統

零知識證明的核心是「電路」——你把計算過程用數學方程組表示,然後證明你知道方程組的解。

// circuit/src/constraints.rs
/// 約束系統
pub struct ConstraintSystem<F: Field> {
    // 當前係數
    coefficients: Vec<F>,
    
    // 約束數量
    constraints: Vec<Constraint>,
    
    // 變量映射
    variables: HashMap<String, Variable>,
    
    // 線性組合
    linear_combinations: Vec<LinearCombination<F>>,
}

impl<F: Field> ConstraintSystem<F> {
    // 添加一個 constraint: a * b = c
    pub fn enforce_product(
        &mut self,
        a: LinearCombination<F>,
        b: LinearCombination<F>,
        c: LinearCombination<F>,
    ) {
        // (a * b) - c = 0
        let constraint = Constraint {
            terms: vec![
                (a, b, F::ONE),  // a * b
                (c, LinearCombination::constant(F::ONE), -F::ONE),  // -c
            ],
            kind: ConstraintKind::Product,
        };
        self.constraints.push(constraint);
    }
    
    // 添加範數約束: a^2 = b
    pub fn enforce_square(
        &mut self,
        a: LinearCombination<F>,
        b: LinearCombination<F>,
    ) {
        self.enforce_product(a.clone(), a, b);
    }
    
    // 添加選擇約束: if sel == 1 then a else b
    pub fn enforce_select(
        &mut self,
        sel: LinearCombination<F>,
        a: LinearCombination<F>,
        b: LinearCombination<F>,
        result: LinearCombination<F>,
    ) {
        // result = sel * a + (1 - sel) * b
        //         = sel * a + b - sel * b
        //         = sel * (a - b) + b
        let constraint = Constraint {
            // ... 實際實現更複雜,包含多個乘法門
        };
        self.constraints.push(constraint);
    }
}

約束系統定義了「什麼是正確的計算」。證明者需要證明他不只執行了計算,還滿足所有這些約束。

3.4 為什麼證明生成這麼慢?

這是個好問題。Boojum 生成一個批次(512 筆交易)的證明需要 2-5 分鐘,但執行這 512 筆交易可能只需要幾秒鐘。

差異來自這裡:

// prover/src/fft.rs
/// 快速傅里葉變換(FFT)
/// 
/// FFT 是生成證明的瓶頸
/// 複雜度: O(n log n)
/// 但常數因子很大

pub fn fft<FE: FieldExtension, G: Field>(
    input: &[FE],
    twiddles: &[G],
    log_n: usize,
) -> Vec<FE> {
    let n = 1 << log_n;
    let mut output = input.to_vec();
    
    // Cooley-Tukey FFT 算法
    for step in 0..log_n {
        let jump = 1 << step;
        let block_size = jump * 2;
        
        for i in 0..(n / block_size) {
            for j in 0..jump {
                let idx1 = i * block_size + j;
                let idx2 = idx1 + jump;
                
                let w = twiddles[j << (log_n - step - 1)];
                
                let x1 = output[idx1];
                let x2 = output[idx2] * w;
                
                output[idx1] = x1 + x2;
                output[idx2] = x1 - x2;
            }
        }
    }
    
    output
}

FFT 需要大量的數論變換(NTT),每個變換都是 O(n log n) 複雜度。對於 2^20 規模的電路,這意味著數百萬次乘法運算。

這就是為什麼 ZK Rollup 的 TPS 理論上限遠低於 Optimistic Rollup——你需要花費大量計算資源生成證明,而不是只等待挑戰期。

第四章:驗證者合約

4.1 L1 驗證合約

證明生成後,需要提交到以太坊主鏈驗證:

// contracts/contracts/verifier.sol
contract Verifier {
    // 驗證 Boojum 證明
    function verifyProof(
        uint256[] calldata _p,
        uint256[] calldata _v,
        uint256[] calldata _input
    ) external view returns (bool) {
        // _p: 證明多項式
        // _v: 驗證鑰
        // _input: 公共輸入
        
        // 執行驗證方程
        uint256 lhs = 1;
        uint256 rhs = 1;
        
        // 這裡的實現被簡化了
        // 實際的 Boojum 驗證涉及多個域和曲線
        
        return lhs == rhs;
    }
}

實際的驗證合約比這複雜得多。Boojum 使用 STARK,需要在以太坊上執行 MiMC 哈希驗證。但關鍵點是:驗證 Gas 成本是固定的,與交易筆數無關。

4.2 完整流程圖

讓我總結 ZKsync 的完整工作流程:

用戶交易
    │
    ▼
┌─────────────────────┐
│   API Server        │ 接收交易,驗證簽名
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│   Sequencer        │ 排序交易,構建批次
│   (L2)             │ 生成執行軌跡
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│   Prover           │ 生成零知識證明
│   (GPU Farm)       │ 耗時 2-5 分鐘
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│   Validator        │ 提交證明到 L1
│   (L1 Contract)     │ 驗證,更新狀態根
└─────────────────────┘

這個流程中,最昂貴的步驟是證明生成。目前(2026 年 Q1)ZKsync 的排序器每秒只能確認約 10-20 筆交易(受制於證明速度),遠低於理論 TPS。

這就是 ZKsync 和 Arbitrum 的核心差異:ZKsync 的瓶頸在於計算(證明生成),Arbitrum 的瓶頸在於時間(7 天挑戰期)。

第五章:實際的性能瓶頸

5.1 TPS 現實

ZKsync 官方宣稱 TPS 可達 2000+。現實如何?

讓我算一筆帳:

TPS = 512 / 180 = 2.84 TPS

這是悲觀估計。如果優化到 2 分鐘:

TPS = 512 / 120 = 4.27 TPS

等等,這遠低於官方宣稱的數字。原因在於:

  1. 串列處理:批次必須順序處理,不能並行
  2. 網路延遲:排序器到驗證合約需要約 12 秒
  3. 提交頻率:不能每個批次都提交,需要累積多個批次降低固定成本

實際的 TPS 可能在 20-50 範圍內。仍然比以太坊主鏈快很多,但距離「2000 TPS」還有很長的路。

5.2 成本結構

ZKsync 的交易成本由以下部分組成:

成本組成說明佔比
L1 數據可用性成本發布狀態差分的 Calldata60-70%
L1 驗證成本驗證合約的 Gas10-15%
L2 運營成本排序器、證明者伺服器15-25%

隨著 EIP-4844(proto-danksharding)的實施,L1 數據可用性成本會大幅下降。這也是為什麼 ZKsync 對 EIP-4844 非常期待。

5.3 對比 Arbitrum

指標ZKsync EraArbitrum One
確認時間2-5 分鐘即時(L2)/ 7 天(L1)
提款時間2-5 分鐘7 天(如果不使用快速橋)
TPS (實際)20-50100-200
L1 成本分攤固定(按批次)固定(按批次)
信任模型密碼學經濟博弈

我的看法:如果你是普通用戶,Arbitrum 的 7 天提款等待是無法接受的。如果你是機構用戶,需要更高安全性,ZKsync 的密碼學保障更有吸引力。

結論

ZKsync 的原始碼揭示了一個事實:零知識證明不是魔法。它是一種將「信任」轉化為「計算」的技術。代價是你需要昂貴的 GPU farm 來生成證明。

Boojum 是 ZKsync 工程團隊的重大成就。從 128GB 降到 32GB 的記憶體需求,使得小驗證者也能參與。證明速度的提升,讓 ZKsync 從實驗室走向了實用。

但挑戰依然存在:

  1. TPS 受制於證明速度
  2. GPU farm 成本高昂
  3. 電路設計複雜,bug 風險大

讀完這些原始碼,我對 ZKsync 的態度是「審慎樂觀」。技術方向是對的,但落地還需要時間。

下次再有人問我「ZK Rollup 和 Optimistic Rollup 哪個好」,我會說:「取決於你願意用計算換時間,還是用時間換計算。」

References:

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!