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 的出現就是為了解決這個痛點。它做到了:
- 通用可信設置(Universal Trusted Setup):一次性設置,可以複用於任何電路
- 後量子安全(理論上):安全性基於啞元和配對
- 約束數量優化:比早期方案更高效
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
這裡:
- a, b 是輸入信號
- c 是輸出信號
- qL, qR, qO, qM, qC 是「選擇器係數(Selector Coefficients)」
- 不同選擇器值定義不同類型的門
舉個例子:
- 加法門:qL=1, qR=1, qO=-1, qM=0, qC=0 → a + b - c = 0
- 乘法門:qL=0, qR=0, qO=-1, qM=1, qC=0 → a·b - c = 0
- 常量乘法:qL=k, qR=0, qO=-1, qM=0, qC=0 → k·a - c = 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 更靈活,因為它同時支持:
- 線性組合(qL·a + qR·b)
- 乘積(a·b)
- 常數偏移(qC)
而且通過選擇器 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 的約束系統。
設定:
- 有限域:𝔽ₚ,其中 p 是大質數
- 電路深度:n(2 的冪)
- n 次單位根集合:H = {ω⁰, ω¹, ..., ωⁿ⁻¹}
- 拉格朗日基:{L₀, L₁, ..., Lₙ₋₁}
符號定義:
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 約束數 | 備註 |
|---|---|---|---|
| 加法 | 1 | 1 | 相同 |
| 乘法 | 1 | 1 | 相同 |
| 比較(32位) | ~100 | ~32 | PLONK 用查找表 |
| 哈希(Keccak) | ~2000 | ~500 | ZK 友好哈希更少 |
| 簽章驗證 | ~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 報告遷移後:
- 電路規模:~2.5 倍約束(因為要兼容更多操作)
- 證明速度:提升 ~50%(感謝 Rust 的並行優化)
- 驗證成本:基本持平
5.2 Scroll 的 Halo2 自定義約束
Scroll 是另一個使用 Halo2 的 zkEVM 項目。他們在標準 Halo2 約束系統上增加了自定義約束來支持 EVM 操作碼。
Scroll 的電路結構:
Execution Trace:~300K 步(每步對應一個 EVM 操作)
電路約束:
- 算術約束:~100K
- 內存約束:~50K
- Keccak 約束:~100K
- 讀寫約束:~50K
自定義約束示例:
Scroll 需要支持 EVM 的 SLOAD/SSTORE 操作。這些操作涉及:
- 讀取棧(Stack Access)
- 讀取內存(Memory Access)
- 讀取存儲(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 ETH | 17 | ~4,000 | |
| 1 ETH | 20 | ~5,000 | 最常用 |
| 10 ETH | 23 | ~6,000 | |
| 100 ETH | 26 | ~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 追求最大的靈活性。
實際應用中,選擇哪個系統取決於你的需求:
- 需要快速部署?選 PLONK(有現成的工具鏈)
- 需要高度自訂約束?選 Halo2
- 對 trusted setup 有顧慮?選 Halo2
不管怎麼說,這兩套系統都在快速演進。zkML、zkEVM、zkBridge... 每一個應用場景都在推動約束系統的邊界。
如果你想深入學習,我建議:
- 先從 Circom 入手(門檻最低)
- 讀懂 PLONK 論文(數學優雅)
- 動手實現一個小電路(理論結合實踐)
- 探索 Halo2 的自訂約束(挑戰極限)
ZK 的世界很大,慢慢探索。
參考文獻
- PLONK 原始論文
- Gabizon, Williamson & CNeo, "PLONK: Permutations over Lagrange-bases for Oecumenical Noninteractive Arguments of Knowledge" (2019)
- eprint: https://eprint.iacr.org/2019/953
- Halo2 設計文檔
- Zcash, "Halo2 Design Documentation"
- https://zcash.github.io/halo2/
- PLONKish 約束系統教程
- Vitalik Buterin, "Understanding PLONK" (部落格文章)
- https://vitalik.ca/general/2019/09/22/plonk.html
- Circom 文檔
- iden3, "Circom Language Documentation"
- https://docs.circom.io/
- zkSync Era (Boojum) 技術報告
- Matter Labs, "Boojum: zkSNARK Prover without Trusted Setup"
- https://github.com/matter-labs/boojum
- Scroll zkEVM 架構
- Scroll Team, "Scroll zkEVM Architecture"
- https://scroll.io/blog/zkEVMArchitecture
- Tornado Cash 電路設計
- Tornado Cash, "Circuit Design"
- https://github.com/tornadocash/tornado-core
聲明:本文涉及的所有代碼僅供教育目的。實際部署 ZK 電路前,請進行完整的安全審計。
最後更新日期:2026 年 3 月 28 日
相關文章
- HALO2 與 PLONK 家族零知識證明系統深度工程實作指南:折疊方案、遞迴證明與電路設計實務 — HALO2 由 Electric Coin Company(Zcash 團隊)開發,其創新性地採用了「折疊方案」(Folding Scheme)來實現無需可信設置的遞迴證明組合。PLONK 及其衍生協議(UltraPLONK、TurboPLONK、Plonky2)則以其通用性和效率聞名。本文深入分析 HALO2 協議的核心原理、PLONK 家族的數學推導、以及實際電路設計的工程細節。涵蓋 IVC 增量可驗證計算、PLONK 置換論證與門約束系統、折疊方案數學推導、IPA 內積論證、HALO2 電路結構與約束定義、Rust HALO2 電路開發實務、PLONK 變體分析(UltraPLONK 查找表、Plonky2 Goldilocks 域優化)、以及 zkSync、Aztec 等項目的實際應用案例。同時探討電路安全性陷阱、驗證器安全最佳實踐與形式化驗證方法。
- KZG 承諾、PLONK 與 HALO2 電路設計數學推導完整指南:從多項式承諾到零知識電路的工程實踐 — 本文深入探討零知識證明的三大核心技術支柱:KZG 承諾的密碼學基礎、PLONK 證明系統的電路設計原理,以及 HALO2 的遞歸證明機制。提供完整的數學推導過程,包括有限域運算、橢圓曲線配對、多項式承諾、批量開放大學等核心概念的詳細證明。同時涵蓋電路設計實務,包含約束系統、查找表優化、遞歸證明組合等工程實踐。為 L2 開發者、安全研究者和密碼學研究者提供全面的理論與實作指南。
- PLONK/Halo2 電路約束推導完整指南:從代數電路到零知識證明系統 — PLONK 與 Halo2 是當今區塊鏈領域最重要的零知識證明系統之一。本文從電路約束推導的視角出發,完整解析 PLONK 的約束系統、證明流程與 Halo2 的工程實現。讀者將理解為何 PLONK 能夠在「通用性」與「效率」之間取得優異平衡,以及如何在實際項目中設計與優化零知識電路。
- PLONK 與 Halo2 約束系統步驟式推導完整指南:以橢圓曲線密碼學為基礎的零知識證明原理 — PLONK 和 Halo2 是目前最被廣泛採用的通用零知識證明系統。本文以步驟式推導的方式,從最基礎的密碼學假設開始,逐步構建出完整的約束系統理論框架。涵蓋橢圓曲線離散對數問題的直覺解釋、從電路到約束系統的轉換、PLONK 的排列論證、Halo2 的查找約束、以及在 zkML 中的實際應用。提供完整的數學推導過程和實作範例。
- 零知識證明系統數學推導完整指南:從電路約束到 Kate 承諾的深度工程實踐 — 本文從電路約束系統的視角出發,系統性地推導 PLONK 和 Halo2 兩種主流 ZK 證明系統的核心算法。涵蓋算術電路的表示與約束、R1CS 約束系統的矩陣表示、多項式承諾與 Kate 承諾的代數結構、PLONK 置換論證的推導過程、Halo2 區域配置與 Accumulator 機制、以及 zkEVM 的約束系統設計。透過完整的數學推導與 Solidity 驗證合約實作,幫助讀者深入理解零知識證明在以太坊 Layer2 擴容中的工程實踐。
延伸閱讀與來源
- Ethereum.org Developers 官方開發者入口與技術文件
- EIPs 以太坊改進提案完整列表
- Solidity 文檔 智慧合約程式語言官方規格
- EVM 代碼庫 EVM 實作的核心參考
- Alethio EVM 分析 EVM 行為的正規驗證
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!