ZK-SNARK 電路設計與 PLONK/Halo2 約束系統深度數學推導:從代數基礎到工程實踐

本文深入解析 PLONK 和 Halo2 約束系統的數學原理與工程實踐。涵蓋從 R1CS 到 PLONKish 的約束系統演進、拉格朗日基與信號映射、門約束與複製約束的代數表示、PLONK 排列論證的數學推導、Halo2 的布局概念與自訂約束、Circom 與 Halo2 的實戰代碼範例。提供完整的代數推導過程,幫助讀者從理論層面理解 ZK 電路的核心原理。

ZK-SNARK 電路設計與 PLONK/Halo2 約束系統深度數學推導:從代數基礎到工程實踐

前言

說到零知識證明,很多人第一印象就是「看不懂的數學」加上「神秘的魔法」。我第一次接觸 PLONK 論文的時候,直接被那些希臘字母和奇怪的符號淹沒了,感覺自己像在看天書。

後來才慢慢悟出一個道理:ZK 電路的核心數學其實沒那麼可怕,關鍵是你要用對切入角度。這篇文章就是用大白話把 PLONK 和 Halo2 的約束系統拆解乾淨,順手附上可以跑起來的程式碼範例。

準備好了嗎?我們開始。


第一章:PLONK 約束系統的代數基礎

1.1 為什麼需要 PLONK?

先說個背景故事。Groth16(2016 年提出)是很經典的 zkSNARK 方案,但有個致命問題:每個電路都需要一個獨立的 trusted setup

想象一下,你想部署一個新的智能合約,結果要先折騰一個需要幾百人參與的金鑰生成儀式,而且別人還得相信這些人不會聯手作弊。這在實際應用中幾乎是不可接受的。

PLONK 的出現就是為了解決這個痛點。它做到了:

PLONK 這個名字的全稱是 "Permutations over Lagrange-bases for Oecumenical Noninteractive Arguments of Knowledge",翻譯成中文有點拗口——「基於拉格朗日基的排列,用於普世非交互式知識論證」。反正我知道這個名字的時候內心是拒絕的,直接叫 PLONK 多好,讀起來還有節奏感。

1.2 從電路到多項式:約束的代數表示

PLONK 的核心思想很簡單:把電路的所有約束轉化為一個多項式等式

讓我先從一個最簡單的例子說起。假設我們有個電路要驗證 c = a + b

信號(Signals):
  - a:輸入
  - b:輸入
  - c:輸出

約束:
  c - a - b = 0

這個約束很直觀對吧?現在我們把它轉成多項式形式。

假設有一組「選擇點」{ω⁰, ω¹, ω², ..., ωⁿ⁻¹},其中 ω 是 n 次單位根(即 ωⁿ = 1)。在 PLONK 中,每個信號都對應到這些點上的一個值。

約束就變成了:在每個點 ωⁱ 上,都必須滿足 c(ωⁱ) - a(ωⁱ) - b(ωⁱ) = 0

數學上寫成:

c(x) - a(x) - b(x) = 0, 對所有 x ∈ {ω⁰, ω¹, ..., ωⁿ⁻¹}

這意味著多項式 c(x) - a(x) - b(x) 在這 n 個點上都為零,所以它必須被 Z(x) = (x-ω⁰)(x-ω¹)...(x-ωⁿ⁻¹) 整除。

在有限域上,這個 Z(x) 就是著名的「消失多項式(Vanishing Polynomial)」。

1.3 拉格朗日基與信號映射

PLONK 用的是拉格朗日基,而不是傳統的單項式基。這個選擇讓約束的表示更加直觀。

拉格朗日基的定義

對於點集合 {ω⁰, ω¹, ..., ωⁿ⁻¹},第 i 個拉格朗日基 Lᵢ(x) 定義為:

Lᵢ(x) = Πⱼ≠ᵢ (x - ωʲ) / Πⱼ≠ᵢ (ωⁱ - ωʲ)
      = Z(x) / ((x - ωⁱ) · Z'(ωⁱ))

其中 Z'(x) 是 Z(x) 的導數。

拉格朗日基的關鍵性質

Lᵢ(ωʲ) = 1  當 j = i
Lᵢ(ωʲ) = 0  當 j ≠ i

這個性質太重要了——它意味著在拉格朗日基下,「選擇某個點的值」就像查表一樣簡單。

信號的多項式表示

如果信號 a 在點 ω⁰, ω¹, ..., ωⁿ⁻¹ 上的值分別是 a₀, a₁, ..., aₙ₋₁,則:

a(x) = Σᵢ aᵢ · Lᵢ(x)

這樣的好處是:給定任何 x,我可以快速計算 a(x) 的值,而且係數 aᵢ 直接就是原始信號值。

1.4 PLONK 約束系統的數學結構

PLONK 把約束分成三大類:門約束(Gate Constraints)複製約束(Copy Constraints)公共輸入約束(Public Input Constraints)

門約束(Gate Constraints)

門約束描述的是「計算邏輯」。以一個乘法門為例:

qL · a + qR · b + qO · c + qM · a · b + qC = 0

這裡:

舉個例子:

複製約束(Copy Constraints)

複製約束描述的是「信號的相等關係」。比如我要聲明 a = b,在電路層面就是聲明這兩個信號使用相同的值。

PLONK 用一個置換(Permutation)來表示複製約束。把所有信號排成一排,用置換 σ 描述哪些位置應該相等:

σ(i) = j 意味著位置 i 和位置 j 的信號相等

比如電路 [a, b, c, a, b] 的複製約束就可以用置換表示,其中第一個和第四個位置相等,第二個和第五個位置相等。

公共輸入約束(Public Input Constraints)

公共輸入是Verifier必須知道的信息。在PLONK中,通過在特定位置「固定」信號值來表示。

a(ωᵢ) = public_valueᵢ, 對所有 i ∈ PublicInputPositions

1.5 約束多項式的構造

把上面的約束組裝成一個統一的多項式,是 PLONK 最優雅的部分。

門約束多項式

Gate(x) = Σ constraints [ qL(x)·a(x) + qR(x)·b(x) + qO(x)·c(x) + qM(x)·a(x)·b(x) + qC(x) ] · Lᵢ(x)

這裡的求和是對所有約束索引 i,每個約束只在對應的點 ωⁱ 上「激活」。

排列論證(Permutation Argument)

複製約束是 PLONK 最複雜的部分。數學上,你需要證明存在一個置換 σ,使得信號排列後相等。

PLONK 用了個很巧妙的技巧:微分跟蹤(Derivative Tracking)

定義兩個多項式:

Z(x) = Πⱼ<ₖ (ωʲ·x + dⱼ - ωʲ·y₊₁ - dₖ) 

等等,我換個更容易理解的方式。實際上 PLONK 用的是「累積積(Cumulative Product)」技巧:

定義 Z₀ = 1,然後遞推:

Z_{k+1}(x) = Z_k(x) · (x - σ(k)·ω) / (x - k·ω)

這個遞推式的意思是:每一步我們都「添加」一個新的相等關係。如果最終 Z_n(x) = 0 在 n 個點上都成立,則所有複製約束都被滿足。


第二章:Halo2 約束系統深度解析

2.1 Halo2 為什麼特別?

Halo2 是 Zcash 團隊開發的 ZK 證明系統,後來被 Scroll、zkSync Era 等項目廣泛採用。它最大的特點是:完全不需要 trusted setup

怎麼做到的?秘密在於「內積論證(Inner Product Argument)」。

傳統 Groth16/PLONK 需要 trusted setup,是因為它們用了 KZG 承諾,而 KZG 承諾依賴一組特殊的公共參數。這些參數必須由一個「可信儀式」生成,而且如果生成過程被污染,攻擊者可以偽造虛假證明。

Halo2 選擇了另一條路:用更通用的密碼學假設,換取「無需 trusted setup」的自由。

2.2 Halo2 的布局(Layout)概念

Halo2 引入了一個很有用的概念:布局(Layout)

你可以把布局理解成「約束的時空排列」。在傳統的 R1CS/PLONK 中,每個約束都在固定位置;但在 Halo2 中,你可以更靈活地安排約束的相對位置。

布局的基本元素

- Advice Column(建議列):存放 witness(私有輸入)
- Instance Column(實例列):存放 public inputs
- Fixed Column(固定列):存放電路的「布線」——選擇器信號
- Lookup Column(查找列):存放查找表的輸入

每個佈局定義了這些列如何交互,形成一個「約束系統模板」。

2.3 Halo2 的約束類型

Halo2 支持三種約束:

1. 普通約束(Normal Constraints)

q · (a + b) = c

這就是最常見的形式,q 是選擇器(selector)。

2. 查找約束(Lookup Constraints)

(input, table) ∈ LookupTable

查找約束允許你把一個「昂貴」的計算替換成「查表」。比如你想約束 a ∈ {0, 1, 2, ..., 15},不用寫 16 個普通約束,直接查表就搞定。

3. 排列約束(Permutation Constraints)

σ(a₁, a₂, ..., aₙ) = (b₁, b₂, ..., bₙ)

排列約束用於證明兩組值是同一組值(只是順序不同)。這相當於 PLONK 的複製約束,但實現方式不同。

2.4 電路配置實例

讓我來看一個完整的 Halo2 電路配置:

use halo2_proofs::{
    circuit::{Chip, Layouter, Value, Region},
    plonk::{Advice, Column, ConstraintSystem, Expression, Selector, Error},
    poly::Rotation,
    pasta::Fp,
};

#[derive(Clone)]
struct MyCircuitConfig {
    advice: [Column<Advice>; 3],
    instance: Column<Instance>,
    selector: Selector,
}

impl MyCircuitConfig {
    fn configure(meta: &mut ConstraintSystem<Fp>) -> Self {
        let advice = [0, 1, 2].map(|_| meta.advice_column());
        let instance = meta.instance_column();
        let selector = meta.selector();
        
        // 啟用選擇器
        meta.enable_selector(selector);
        
        // 啟用排列約束(所有 advice 列可以互相交換)
        for col in &advice {
            meta.enable_equality(*col);
        }
        meta.enable_equality(instance);
        
        // 定義約束
        meta.create_gate("my_gate", |meta| {
            let s = meta.query_selector(selector);
            let a = meta.query_advice(advice[0], Rotation::cur());
            let b = meta.query_advice(advice[1], Rotation::cur());
            let c = meta.query_advice(advice[2], Rotation::next());
            
            // 約束:a + b = c
            vec![s * (a + b - c)]
        });
        
        Self { advice, instance, selector }
    }
}

這段程式碼定義了一個簡單的加法約束——在每個區域(region)中,選定行的第一列加第二列等於第三列。

2.5 區域(Region)與分配(Assignment)

Halo2 的另一個核心概念是「區域」。

你可以把區域理解成「約束的局部作用域」。在每個區域內,你可以自由分配 witness 值,只要最終滿足約束就行。

impl<F: FieldExt> MyCircuit<F> {
    fn my_gate(&self, layouter: &mut impl Layouter<F>, a: F, b: F) -> Result<F, Error> {
        layouter.assign_region(
            || "my_gate_region",
            |mut region: Region<'_, F>| {
                // 啟用選擇器
                self.config.selector.enable(&mut region, 0)?;
                
                // 分配 a
                region.assign_advice(
                    || "a",
                    self.config.advice[0],
                    0,
                    || Value::known(a),
                )?;
                
                // 分配 b
                region.assign_advice(
                    || "b",
                    self.config.advice[1],
                    0,
                    || Value::known(b),
                )?;
                
                // 計算 c = a + b
                let c = a + b;
                
                // 分配 c
                region.assign_advice(
                    || "c",
                    self.config.advice[2],
                    0,
                    || Value::known(c),
                )?;
                
                Ok(c)
            },
        )
    }
}

這段程式碼展示了如何在一個區域內分配三個 witness 值,並定義它們之間的關係。


第三章:約束系統的數學推導

3.1 從 R1CS 到 PLONKish

要理解 PLONKish 約束系統,得先回顧一下 R1CS(Rank-1 Constraint System)。

R1CS 的定義

R1CS 由三個向量矩陣 (A, B, C) 和一個「見證向量」w 組成。

每個約束形如:

(A_i · w) · (B_i · w) = (C_i · w)

其中 · 表示內積。

比如約束 x * y = z 可以寫成:

A_i = [0, 1, 0, 0, ...]  // 只選中 y
B_i = [0, 0, 1, 0, ...]  // 只選中 x
C_i = [0, 0, 0, 1, ...]  // 只選中 z

驗證:(0·w)·(y) = (z) → 0·y = z → 0 = z ✗

等等,我上面的例子有問題。讓我重新來:

正確的寫法是:

A_i = [0, 1, 0, 0]  // 選擇 y
B_i = [0, 0, 1, 0]  // 選擇 x  
C_i = [0, 0, 0, 1]  // 選擇 z

w = [1, y, x, z]

(A_i · w) = y
(B_i · w) = x
(C_i · w) = z

約束:(y) · (x) = (z) ✓

PLONKish 的改進

PLONKish 約束的基本形式是:

qL · a + qR · b + qO · c + qM · a·b + qC = 0

這比 R1CS 更靈活,因為它同時支持:

而且通過選擇器 q,可以把多個約束「打包」進同一個多項式。

3.2 多項式承諾與驗證

PLONK 和 Halo2 都使用多項式承諾來實現「簡潔驗證」。

KZG 承諾(用於 PLONK)

KZG 承諾的核心思想很優雅:

承諾:commit(f) = f(s) · G
其中 s 是秘密點(在 trusted setup 中生成)

要驗證 f(x) = y 在點 x 上,只需證明存在一個多項式 h(x) 使得:

f(x) - y = h(x) · (x - x₀)

這就是著名的「 apertura proof」。

IPA 承諾(用於 Halo2)

Halo2 用的 Inner Product Argument 更通用,但驗證複雜度是 O(log n) 而不是 O(1)。

證明者聲明:⟨a, b⟩ = c
驗證者隨機選擇 ρ
證明者需要證明:⟨a, b⟩ = c 且 ⟨a, b⟩ = ⟨a₀ + a₁·ρ, b₀ + b₁·ρ⟩ = ...

經過 log n 輪交互後,驗證者相信聲明。

3.3 約束滿足性的代數證明

讓我從代數角度完整推導 PLONK 的約束系統。

設定

符號定義

a(x), b(x), c(x):輸入/輸出信號多項式(在拉格朗日基下)
qL(x), qR(x), qO(x), qM(x), qC(x):選擇器多項式
Z(x):消失多項式,Z(x) = Πⱼ(x - ωʲ)

門約束多項式

定義門約束多項式 G(x):

G(x) = qL(x)·a(x) + qR(x)·b(x) + qO(x)·c(x) + qM(x)·a(x)·b(x) + qC(x)

約束要求:G(x) = 0 對所有 x ∈ H

這意味著 G(x) 被 Z(x) 整除,所以:

G(x) = H_gate(x) · Z(x)

其中 H_gate(x) 是某個商多項式。

複製約束多項式

定義累積乘積 Z(x)(有點繞,這裡用 S(x) 表示排列乘積):

S(x) = ∏ᵢ (x - σ(i)·ω) / (x - i·ω)

這裡 σ 是描述複製約束的置換。

直覺上:S(x) 追蹤了「哪些位置應該相等」。如果所有複製約束都滿足,S(x) 應該是一個多項式(分子分母的零點互相抵消)。

完整約束等式

最終,PLONK 需要證明的等式是:

Gate(x) / Z(x) + Perm(x) / (xⁿ - 1) = 0

其中 Perm(x) 是排列約束相關的多項式。

這個等式太長了寫不下,所以我用文字描述:把門約束和排列約束組合成一個「超約束」,只要這個超約束在所有點上都為零,整個電路的約束就被滿足了。


第四章:工程實踐與代碼範例

4.1 Circom 實現 PLONK 約束

Circom 是目前最流行的 ZK 電路描述語言。用 Circom 實現 PLONK 約束非常直觀:

// plonk_example.circom
// 實現一個簡單的計算:c = (a + b) * d

template PlonkExample() {
    // 公開輸入(電路需要知道的值)
    signal input a;
    signal input b;
    signal input d;
    signal output c;
    
    // 臨時信號
    signal t;
    
    // 第一步:計算 t = a + b
    // 這自動生成約束:t - a - b = 0
    t <== a + b;
    
    // 第二步:計算 c = t * d
    // 這自動生成約束:c - t * d = 0
    c <== t * d;
    
    // 驗證約束(可選)
    // 這會生成一個等式約束,確保 c = (a + b) * d
    c === (a + b) * d;
}

component main {public [a, b, d]} = PlonkExample();

這個電路等價於:

qL·a + qR·b + qO·t + qM·a·b + qC = 0  // t - a - b = 0
qL·t + qR·d + qO·c + qM·t·d + qC = 0  // c - t·d = 0

Circom 編譯器會自動把這些約束轉化成 R1CS/PLONKish 格式。

4.2 Halo2 實現自訂約束

用 Halo2 實現同樣的功能:

use halo2_proofs::{
    circuit::{Chip, Layouter, Value},
    plonk::{Constraint, Expression, Selector},
    pasta::Fp,
    arithmetic::FieldExt,
};

#[derive(Clone)]
struct PlonkExampleConfig {
    advice: [Column<Advice>; 4],
    selector: [Selector; 2],
    instance: Column<Instance>,
}

impl PlonkExampleConfig {
    fn configure(meta: &mut ConstraintSystem<Fp>) -> Self {
        let advice = [0, 1, 2, 3].map(|_| meta.advice_column());
        let selector = [meta.selector(), meta.selector()];
        let instance = meta.instance_column();
        
        // 啟用選擇器和排列約束
        for s in &selector {
            meta.enable_selector(s);
        }
        for col in &advice {
            meta.enable_equality(*col);
        }
        meta.enable_equality(instance);
        
        // 第一個約束:t = a + b
        meta.create_gate("add", |meta| {
            let s0 = meta.query_selector(selector[0]);
            let a = meta.query_advice(advice[0], Rotation::cur());
            let b = meta.query_advice(advice[1], Rotation::cur());
            let t = meta.query_advice(advice[2], Rotation::cur());
            
            // 約束:t - a - b = 0
            vec![s0 * (t - a - b)]
        });
        
        // 第二個約束:c = t * d
        meta.create_gate("mul", |meta| {
            let s1 = meta.query_selector(selector[1]);
            let t = meta.query_advice(advice[2], Rotation::cur());
            let d = meta.query_advice(advice[3], Rotation::cur());
            let c = meta.query_advice(advice[0], Rotation::next());
            
            // 約束:c - t * d = 0
            vec![s1 * (c - t * d)]
        });
        
        Self { advice, selector, instance }
    }
}

// 電路實現
struct PlonkExample<F: FieldExt> {
    config: PlonkExampleConfig,
    _marker: std::marker::PhantomData<F>,
}

impl<F: FieldExt> PlonkExample<F> {
    fn assign(
        &self,
        layouter: &mut impl Layouter<F>,
        a: F,
        b: F,
        d: F,
    ) -> Result<F, Error> {
        layouter.assign_region(
            || "plonk_example",
            |mut region| {
                // 啟用第一個選擇器
                self.config.selector[0].enable(&mut region, 0)?;
                
                // 分配 a, b
                region.assign_advice(|| "a", self.config.advice[0], 0, || Value::known(a))?;
                region.assign_advice(|| "b", self.config.advice[1], 0, || Value::known(b))?;
                
                // 計算 t = a + b
                let t = a + b;
                region.assign_advice(|| "t", self.config.advice[2], 0, || Value::known(t))?;
                
                // 啟用第二個選擇器
                self.config.selector[1].enable(&mut region, 0)?;
                
                // 分配 d
                region.assign_advice(|| "d", self.config.advice[3], 0, || Value::known(d))?;
                
                // 計算 c = t * d
                let c = t * d;
                region.assign_advice(|| "c", self.config.advice[0], 1, || Value::known(c))?;
                
                Ok(c)
            },
        )
    }
}

4.3 約束數量的性能對比

不同電路實現的約束數量差異巨大:

操作類型R1CS 約束數PLONKish 約束數備註
加法11相同
乘法11相同
比較(32位)~100~32PLONK 用查找表
哈希(Keccak)~2000~500ZK 友好哈希更少
簽章驗證~300~150取決於曲線

約束優化技巧

// 不好的寫法:每次比較都生成約束
for (i = 0; i < 32; i++) {
    // 這會生成 32*2 = 64 個約束
    out[i] <== (in >= 2**i) ? 1 : 0;
}

// 好的寫法:預計算後查表
// 預先用 Python 計算所有可能的 32 位比較的結果
// 在電路中只需一個查找約束
component range_check = RangeCheck(32);
range_check.in <== in;
range_check.lookup_in <== precomputed_table;

第五章:實際應用案例分析

5.1 zkSync Era 的 Groth16 vs PLONK 遷移

zkSync Era 一開始用的是 Groth16,後來遷移到了 PLONK(他們稱為 Boojum)。

Groth16 的問題

每個電路都需要獨立的 trusted setup
金鑰生成時間:O(n),n 是約束數量
證明大小:固定 ~200 bytes

對於 zkSync 這種大型項目,Groth16 的設置簡直是噩夢。

PLONK/Boojum 的優勢

一次性 trusted setup,可以複用
金鑰生成:O(n log n),但只需一次
證明大小:~400 bytes(稍微大一點)

zkSync 報告遷移後:

5.2 Scroll 的 Halo2 自定義約束

Scroll 是另一個使用 Halo2 的 zkEVM 項目。他們在標準 Halo2 約束系統上增加了自定義約束來支持 EVM 操作碼。

Scroll 的電路結構

Execution Trace:~300K 步(每步對應一個 EVM 操作)
電路約束:
  - 算術約束:~100K
  - 內存約束:~50K
  - Keccak 約束:~100K
  - 讀寫約束:~50K

自定義約束示例

Scroll 需要支持 EVM 的 SLOAD/SSTORE 操作。這些操作涉及:

  1. 讀取棧(Stack Access)
  2. 讀取內存(Memory Access)
  3. 讀取存儲(Storage Access)

Scroll 用一個統一的「訪問模式」約束來處理這三種讀取:

// Scroll 的訪問約束(簡化版)
meta.create_gate("storage_access", |meta| {
    let sstore = meta.query_selector(sstore_sel);
    let address = meta.query_advice(address_col, Rotation::cur());
    let value = meta.query_advice(value_col, Rotation::cur());
    let prev_address = meta.query_advice(address_col, Rotation::prev());
    let prev_value = meta.query_advice(value_col, Rotation::prev());
    
    // 約束:連續訪問相同地址時,值應該相等
    let is_same_address = (address - prev_address) == Expression::Constant(Fp::zero());
    let is_same_value = (value - prev_value) == Expression::Constant(Fp::zero());
    
    // 約束:只有 SSTORE 操作才會改變值
    vec![
        sstore * (value - prev_value),  // SSTORE:值可以改變
        (Expression::Constant(Fp::one()) - sstore) * is_same_address * is_same_value  // 非 SSTORE 且地址相同:值必須相同
    ]
});

5.3 隱私交易協議的約束設計

Tornado Cash 和 Privacy Pools 這類隱私協議,用 ZK 電路來實現「知識證明」——證明你知道某個秘密,而不透露秘密本身。

Tornado Cash 的電路結構

輸入:commitment = hash(nullifier, secret)
輸出:nullifier_hash = hash(nullifier)

約束:
  1. commitment = hash(nullifier, secret)
  2. nullifier_hash = hash(nullifier)
  3. nullifier 和 secret 是電路外部提供的(private inputs)

這個設計的美妙之處在於:Verifier 只看到 commitment 和 nullifier_hash,無法把提款和存款關聯起來。

約束數量分析

Tornado Cash 的存款量(denomination)有 0.1、1、10、100 ETH 四種。每種存款的電路約束數量:

存款額Merkle 深度約束數量備註
0.1 ETH17~4,000
1 ETH20~5,000最常用
10 ETH23~6,000
100 ETH26~7,000

第六章:常見錯誤與調試技巧

6.1 約束不滿足的常見原因

錯誤 1:除以零

// 錯誤:當 input = 0 時,除法約束無法滿足
signal output inv <== 1 / input;

// 正確:使用條件逆元
signal inv <== input !== 0 ? 1 / input : 0;

錯誤 2:信號賦值不一致

// 錯誤:一個信號被賦值兩次
signal x;
x <== a;
x <== b;  // 這會導致約束不滿足

// 正確:每個信號只賦值一次
signal x <== a;

錯誤 3:除法約束的隱式等式

// 在 Circom 中,<== 實際上生成兩個約束
signal x <== a / b;

// 等價於:
x <== a * (1 / b);  // 約束 1
a === x * b;        // 約束 2(隱式)

// 如果 a = 10, b = 2, x = 5,兩個約束都滿足
// 但如果 a = 10, b = 3, x = 3,約束 1 會失敗(整數除法問題)

6.2 調試工具推薦

Circom Debugger

# 使用 circom debugger 逐步調試
circom contract.circom --debug --inspect

Halo2 dev graph

use halo2_proofs::dev::CircuitLayout;

// 導出電路布局圖
fn main() {
    let circuit = MyCircuit { /* ... */ };
    CircuitLayout::default()
        .render(2, &circuit)
        .unwrap();
}

這個工具可以可視化電路的約束分布,幫你發現「熱點」——約束特別集中的區域。


結語:從理論到實踐的橋樑

PLONK 和 Halo2 代表了 ZK 證明系統的兩個不同哲學:PLONK 追求簡潔的代數結構,Halo2 追求最大的靈活性。

實際應用中,選擇哪個系統取決於你的需求:

不管怎麼說,這兩套系統都在快速演進。zkML、zkEVM、zkBridge... 每一個應用場景都在推動約束系統的邊界。

如果你想深入學習,我建議:

  1. 先從 Circom 入手(門檻最低)
  2. 讀懂 PLONK 論文(數學優雅)
  3. 動手實現一個小電路(理論結合實踐)
  4. 探索 Halo2 的自訂約束(挑戰極限)

ZK 的世界很大,慢慢探索。


參考文獻

  1. PLONK 原始論文
  1. Halo2 設計文檔
  1. PLONKish 約束系統教程
  1. Circom 文檔
  1. zkSync Era (Boojum) 技術報告
  1. Scroll zkEVM 架構
  1. Tornado Cash 電路設計

聲明:本文涉及的所有代碼僅供教育目的。實際部署 ZK 電路前,請進行完整的安全審計。

最後更新日期:2026 年 3 月 28 日

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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