以太坊零知識證明完整實作指南:從密碼學基礎到 zk-SNARKs/STARKs 智能合約部署
零知識證明(Zero-Knowledge Proof,ZKP)是現代密碼學中最具革命性的技術之一,其核心特性——在不透露任何額外資訊的情況下證明陳述的正確性——為區塊鏈隱私保護和可擴展性帶來了前所未有的可能性。本文從以太坊開發者的視角出發,深入探討零知識證明的密碼學基礎、zk-SNARKs 與 zk-STARKs 的技術差異、主流實作框架(如 Circom、ZoKrates、Groth16、PLONK)的使用方法,以及如何在以太坊上部署零知識證明智能合約。我們將提供完整的程式碼範例,涵蓋從電路設計、證明生成到鏈上驗證的整個流程,同時深入分析每個環節的 Gas 消耗、安全考量與最佳實踐。
零知識證明以太坊實作程式碼完整指南:從密碼學基礎到 zk-SNARK/STARK 實戰
文章 metadata
| 欄位 | 內容 |
|---|---|
| difficulty | advanced |
| date | 2026-03-28 |
| category | privacy |
| tags | zero-knowledge-proof, zk-snark, zk-stark, privacy, ethereum, solidity, circom, cryptography |
| disclaimer | 本網站內容僅供教育與資訊目的,不構成任何投資建議或推薦。 |
說真的,每次有人跟我解釋零知識證明,我都覺得對方在背課本。「證明者可以在不透露答案的情況下證明自己知道答案」——這句話聽起來像是魔術,不像是數學。但這恰恰是 ZK 最迷人的地方:它真的運作,而且是用優雅的數學運作的。
這篇文章我想做一件事:帶你從最基礎的密碼學直覺開始,一路走到以太坊上跑 zk-SNARK 合約。不是那種「看完還是不知道怎麼做」的理論課,而是實打實的程式碼和可運作的範例。準備好燒點腦細胞了嗎?讓我們開始。
一、零知識證明的直覺:為什麼這不是魔法
1.1 經典問題:兩個山洞
想像一個山洞,入口處有一道魔法門,需要念咒語才能打開。這個咒語只有你知道。現在有個人想證明給你聽眾她知道咒語,但又不想讓你聽到咒語是什麼。怎麼辦?
她的方法是這樣的:讓你站在洞口外面,她隨機選擇左邊或右邊走進去。然後你大喊一聲「左邊!」或「右邊!」,她必須從你指定的那邊走出來。
如果她真的知道咒語,她一定可以做到。如果她不知道,她只有 50% 的機率猜對。經過 20 次這樣的測試,如果她全部答對,你可以有 99.9999% 的信心相信她真的知道咒語——但你從頭到尾都沒聽到咒語是什麼。
這就是零知識證明的核心直覺:用「挑戰-回應」的互動方式,讓證明者在不透露祕密的情況下說服驗證者。
1.2 三個必須滿足的性質
密碼學家把零知識證明的特性精確定義成三個:
1. 完整性(Completeness)
如果 statement 是真的(證明者真的知道祕密),誠實的驗證者最終會被說服。數學上:真的命題 + 誠實證明者 = 驗證通過(機率趨近 1)
2. 可靠性(Soundness)
如果 statement 是假的(證明者其實不知道祕密),任何作弊的證明者都幾乎不可能欺騙驗證者。數學上:假的命題 + 任何證明者 = 驗證通過(機率趨近 0)
3. 零知識性(Zero-Knowledge)
驗證者在過程中除了「命題為真」之外,什麼額外資訊都得不到。換句話說:如果把整個互動過程記錄下來給別人看,別人還是不知道祕密是什麼。
這三個性質缺一不可。你可以想像成密碼學的三角定律——打破任何一邊,整個系統就失效。
1.3 從互動到非互動:Fiat-Shamir 啟發式
剛才的山洞故事是「互動式」的——證明者和驗證者需要來回溝通。但在區塊鏈上,互動式證明是不可行的:你不能要求礦工/驗證者一直等著跟證明者「對話」。
Fiat-Shamir 啟發式(Fiat-Shamir Heuristic)解決了這個問題:用密碼學雜湊函數取代驗證者的隨機挑戰。
互動式:
證明者 → [聲明] → 驗證者 → [隨機挑戰] → 證明者 → [回應]
非互動式:
挑戰 = Hash(聲明, 公開參數)
回應 = 計算(聲明, 挑戰, 祕密)
這樣一來,整個過程變成一個「一次性」的數學證明,任何人都可以獨立驗證,不需要再跟證明者互動。這就是區塊鏈上 ZK 證明的運作方式。
二、密碼學基礎:建構 ZK 的大積木
2.1 橢圓曲線密碼學(ECC)
零知識證明在區塊鏈上的實作依賴於橢圓曲線密碼學。如果你對 ECC 不熟悉,這段一定要認真看。
橢圓曲線是一個滿足特定方程式的點集合:
y² = x³ + ax + b (mod p)
以太坊和大多數 ZK 系統使用的曲線叫做 BN128(又稱 ALT_BN128),它的參數是:
# BN128 曲線參數
p = 21888242871839275222246405745257275088548364400416034343698204186575808495617
# 這是一個 254 位的質數
# 曲線係數
a = 0
b = 3
# 基點 G(曲線上的一個生成點)
Gx = 1
Gy = 2
# 羣的階(基點 G 生成的循環羣的元素個數)
n = 21888242871839275222246405745257275088696311157297823662689037894645226208583
為什麼要用橢圓曲線?因為它有兩個關鍵特性:
特性 1:離散對數問題很難
給定 P = k * G(G 是基點,k 是私鑰,P 是公鑰),從 P 和 G 反推 k 在計算上是不可行的。這就是 ECC 安全性的基礎——跟 RSA 的因數分解問題類似,但金鑰長度短很多。
特性 2:點加法封閉
曲線上的任意兩點相加,還是會落在曲線上。而且加法服從交換律、結合律,有單位元素,有逆元素——這讓曲線上的點形成一個阿貝爾羣。
class BN128Point:
def __init__(self, x, y):
self.x = x % BN128.p
self.y = y % BN128.p
def __add__(self, other):
# 點加法公式
if self == INFINITY:
return other
if other == INFINITY:
return self
if self.x == other.x:
if self.y == other.y:
# 倍點公式
s = (3 * self.x * self.x) * inv(2 * self.y, BN128.p) % BN128.p
else:
return INFINITY # P + (-P) = O
else:
# 加法公式
s = (other.y - self.y) * inv(other.x - self.x, BN128.p) % BN128.p
x3 = (s * s - self.x - other.x) % BN128.p
y3 = (s * (self.x - x3) - self.y) % BN128.p
return BN128Point(x3, y3)
# 生成公鑰
def scalar_mult(k, base_point):
"""橢圓曲線標量乘法:用 k 個 base_point 相加"""
result = INFINITY
addend = base_point
while k:
if k & 1:
result = result + addend
addend = addend + addend # 點加倍
k >>= 1
return result
2.2 雙線性配對(Pairings)
配對是 ZK 證明系統的核心工具。它允許我們在兩個不同羣之間做「乘法驗證」。
數學上,配對是這樣定義的:
e: G₁ × G₂ → G_T
其中:
- G₁ 和 G₂ 是兩個橢圓曲線點羣(通常是不同的子羣)
- G_T 是目標羣(一個有限域)
- e(aP, bQ) = e(P, Q)^ab (雙線性性)
這個性質聽起來抽象,但它的實際應用非常強大:給定 e(P, Q) 和 P、Q,你沒辦法反推 a 和 b,但你可以驗證 e(aP, bQ) = e(P, Q)^ab 這個等式。
zk-SNARK 的驗證就是靠這個性質:證明者需要展示某些橢圓曲線點之間的關係,而驗證者透過配對檢查這些關係是否正確。
2.3 密碼學雜湊函數
雜湊函數在 ZK 中扮演多重角色:
- Fiat-Shamir 轉換:用來生成 pseudo-random 挑戰
- Merkle 樹:用於批次資料的承諾
- 承諾方案:用來「鎖住」一個值而不透露它
密碼學安全的雜湊函數需要滿足:
- 單向性:給定 hash,很難找到原始輸入
- 抗碰撞:很難找到兩個不同輸入有相同 hash
- 雪崩效應:輸入改一位,輸出差一半
三、zk-SNARK 詳解:現在最流行的 ZK 系統
3.1 SNARK 是什麼
SNARK = Succinct Non-interactive ARguments of Knowledge
翻譯成人話就是:簡潔的、非互動式的知識論證。
- Succinct:證明很小(幾百 bytes),驗證很快
- Non-interactive:不需要來回互動
- Arguments:計算完整性(對誠實驗證者的可靠性)
- Knowledge:證明者必須真的「知道」 witness
zk-SNARK 是 Zcash 團隊在 2016 年左右推向主流的。從那之後,它幾乎成為了以太坊 Layer 2(zkSync、Polygon zkEVM、Scroll)的底層支柱。
3.2 從電路到多項式
zk-SNARK 的核心魔法是:把任意計算問題轉換成多項式問題。
步驟 1:把計算表達成算術電路
假設要驗證:x * y = z,其中 x, y, z 是 public inputs
電路表示:
┌─────────────────────────────┐
│ input_x → [×] → output_z │
│ input_y ↗ │
└─────────────────────────────┘
步驟 2:把電路轉換成 R1CS(Rank-1 Constraint System)
R1CS 的每個約束都是這種形式:
(A·w) × (B·w) - (C·w) = 0
其中 w 是所有輸入輸出的拼接向量。
步驟 3:把 R1CS 轉換成 QAP(Quadratic Arithmetic Program)
QAP 把 n 個約束變成 3 個 n-1 次多項式 L(x), R(x), O(x)。
驗證 (A·w) × (B·w) = (C·w) 等價於驗證:
L(x) × R(x) - O(x) 在所有約束點上都能被 (x - t) 整除
這個「能被整除」的性質,可以用配對來驗證,而且驗證代價很小。
3.3 信任設置(Trusted Setup)
zk-SNARK 需要一個「信任設置」階段。這是它最大的爭議點。
信任設置的目的是生成「有毒廢料」(Toxic Waste):一組只能用一次的祕密隨機數。如果這些數字洩露,攻擊者可以偽造假的證明。
信任設置儀式(Cergeemony):
1. 參與者們各生成隨機數 s
2. 計算 s^i × G(公開的羣元素)
3. 每個人立刻「忘掉」自己的 s
如果至少有一個參與者誠實地忘掉 s,整個系統就是安全的。
這就是為什麼「多方計算」(MPC)信任設置很重要:讓數百個獨立的人在不同的地點、不同的設備上參與,理論上不可能讓所有人生成並洩露有毒廢料。
以太坊的「Power of Tau」 ceremony 是一個著名的例子:超過 100,000 人參與了設置。
3.4 Groth16:經典的 zk-SNARK 協議
Groth16 是目前最廣泛使用的 zk-SNARK 協議。它的證明只有三個羣元素(約 128 + 128 + 128 = 384 bytes),驗證只需要幾個配對運算。
class Groth16:
"""
Groth16 zk-SNARK 協議實現(概念性)
"""
# 1. 設置階段(每次電路只做一次)
@staticmethod
def setup(r1cs):
"""
輸入:R1CS 約束系統
輸出:Proving Key (pk) 和 Verification Key (vk)
"""
# 生成有毒廢料 s
tau = sample_random_scalar()
alpha = sample_random_scalar()
beta = sample_random_scalar()
gamma = sample_random_scalar()
delta = sample_random_scalar()
# 計算 CRS(Common Reference String)
# [τ^i G]_1, [τ^i G]_2, [α·τ^i G]_1, etc.
crs_1 = [pow(tau, i, n) * G1 for i in range(r1cs.num_constraints + 1)]
crs_2 = [pow(tau, i, n) * G2 for i in range(1, r1cs.num_constraints)]
pk = {
'A': compute_A_polynomial(crs_1, r1cs),
'B': compute_B_polynomial(crs_2, r1cs),
'C': compute_C_polynomial(crs_1, r1cs),
}
vk = {
'alpha': alpha * G1,
'beta': beta * G2,
'gamma': gamma * G2,
'delta': delta * G2,
'IC': compute_IC_polynomial(crs_1, r1cs), # 線性組合係數
}
# ⚠️ 這裡要立刻忘掉 tau, alpha, beta!
return pk, vk
# 2. 證明生成
@staticmethod
def prove(pk, public_inputs, private_inputs, vk):
"""
輸入:pk, public inputs, private inputs (witness)
輸出:Proof (A, B, C 三個羣元素)
"""
# 合併所有輸入
full_inputs = public_inputs + private_inputs
# 計算 A, B, C 多項式在隨機點的取值
A = evaluate_polynomial(pk['A'], full_inputs)
B = evaluate_polynomial(pk['B'], full_inputs)
C = evaluate_polynomial(pk['C'], full_inputs)
# 加上隨機種子(確保零知識性)
blinding = sample_random_scalar()
A = A + blinding * G1
# 最終證明
proof = {
'A': A,
'B': B,
'C': C,
}
return proof
# 3. 驗證
@staticmethod
def verify(vk, public_inputs, proof):
"""
輸入:vk, public inputs, proof
輸出:True/False
"""
# 計算 public input 的線性組合
gamma_AC = compute_linear_combination(vk['IC'], public_inputs)
# 配對驗證
# e(A, B) = e(alpha + gamma·AC, beta) · e(C, delta)^(-1)
lhs = pairing(proof['A'], proof['B'])
rhs = pairing(vk['alpha'] + gamma_AC, vk['beta'])
rhs = rhs * pairing(proof['C'], vk['delta']).inverse()
return lhs == rhs
3.5 PLONK 和通用的信任設置
Groth16 的問題是:每個電路都需要獨立的信任設置。這對 Eth混混來說是不可接受的——如果想在以太坊上部署新的 zkRollup,你不能要求用戶重新做一次 Power of Tau。
PLONK(Permutations over Lagrange-bases for Oecumenical Noninteractive arguments of Knowledge)解決了這個問題:它使用「通用的」(Universal)「可更新的」(Updatable)信任設置。
PLONK 的優勢:
1. 信任設置只需做一次
2. 任何電路都可以使用這個設置
3. 如果有新參與者加入,可以「更新」設置而不需要重新開始
4. 電路大小有一定限制,但透過定製電路可以繞過
PLONK 的驗證稍微複雜一點,但它的實用性使其成為 zkSync、zkEVM 等項目的選擇。
四、zk-STARK:後量子時代的選擇
4.1 STARK 與 SNARK 的核心差異
STARK = Scalable Transparent ARguments of Knowledge
跟 SNARK 相比,STARK 有兩個關鍵改進:
1. Transparent(透明):不需要信任設置
STARK 使用公開可驗證的隨機性(VDF/哈希),不需要「有毒廐料」。這消除了 SNARK 最受爭議的信任假設。
2. Quantum-resistant(抗量子):基於哈希而非橢圓曲線
STARK 的安全性完全依賴於密碼學哈希函數的安全性,而不是離散對數或因數分解。考慮到量子計算的發展,這是一個重要的長期保障。
代價是:STARK 的證明更大(幾十到幾百 KB),驗證也更慢。
SNARK vs STARK 比較:
| 證明大小 | 驗證時間 | 信任設置 | 抗量子
-------------|-----------|-----------|-----------|--------
Groth16 | ~400 B | 幾毫秒 | 需要 | 否
PLONK | ~1 KB | 幾毫秒 | 通用一次 | 否
STARK | ~100 KB | 幾十毫秒 | 不需要 | 是
4.2 STARK 的核心:FRI 協議
STARK 使用 FRI(Fast Reed-Solomon IOP)協議來達成簡潔證明。FRI 的核心思想是:
- 把多項式
f(x)在多個點上的取值編碼成一個 Reed-Solomon 碼字 - 透過「折半」遊戲,用對數級別的互動次數來「壓縮」這個碼字
- 最終驗證者只需要檢查最終的「 Commitment」是否正確
class FRI:
"""
FRI 協議簡化實現
"""
def __init__(self, domain_size, expansion_factor=4):
self.domain_size = domain_size
self.expansion_factor = expansion_factor
self.num_queries = 40 # 驗證者會查詢這麼多次
def commit(self, polynomial_coeffs):
"""
承諾階段:對多項式進行編碼
"""
# 在擴展域上計算多項式值
extended_values = self._evaluate_on_extended_domain(polynomial_coeffs)
# Merkle 根作為 commitment
merkle_root = compute_merkle_root(extended_values)
return merkle_root, extended_values
def prove_round(self, polynomial_coeffs, round_num):
"""
證明者回合:折半並發送下一層多項式
"""
# 把多項式的奇偶項分開
even_poly = extract_even_terms(polynomial_coeffs)
odd_poly = extract_odd_terms(polynomial_coeffs)
# 證明者發送一個線性組合
# B(x) = even_poly(x) + α · odd_poly(x)
# 其中 α 來自驗證者的挑戰(或 Fiat-Shamir)
# 下一層多項式
# f'(x²) = even_poly(x) + odd_poly(x) · x
next_poly = [even_poly[i] + odd_poly[i] * (2**i)
for i in range(len(even_poly))]
return next_poly, {'alpha': alpha, 'B': B_commitment}
def verify(self, commitment, queries, answers):
"""
驗證者回合:檢查查詢結果的一致性
"""
# 驗證查詢的 Merkle 證明
for i, (index, value, proof) in enumerate(queries):
assert verify_merkle_proof(
commitment, index, value, proof
)
# 驗證相鄰查詢之間的代數關係
# (這是 FRI 的核心:兩個查詢點的答案必須滿足某個約束)
return True
def verify_proof(self, commitment, proof, challenges):
"""
完整驗證流程
"""
current_commitment = commitment
for round_i, (alpha, next_proof) in enumerate(proof.rounds):
# 驗證者發送挑戰
# alpha = FiatShamir(commitment_history)
# 驗證「折半」的代數約束
assert self._check_folding_constraint(
current_commitment, alpha, next_proof
)
current_commitment = next_proof
# 最終檢查:最內層多項式是否真的是低次的
assert is_low_degree(next_proof, max_degree=1)
return True
五、零知識證明在以太坊的應用場景
5.1 Layer 2 zk-Rollup
zk-Rollup 是目前 ZK 在以太坊上最重要的應用。思路很簡單:
L2 交易處理:
1. 用戶把交易提交給 Sequencer(排序器)
2. Sequencer 批量處理交易,生成新狀態
3. 同時生成 ZK 證明:「這個狀態轉換是正確的」
4. 把證明和狀態根提交到 L1
好處:
- L1 只存證明,不存完整交易數據(節省 gas)
- L1 驗證證明比重新執行交易便宜很多
- 狀態有效性的保證是密碼學的,不是經濟遊戲
zkSync Era、Polygon zkEVM、Scroll、Starknet 都是 zk-Rollup 的實現。
5.2 zkEVM:挑戰與實現
zkEVM 是 zk-Rollup 的聖杯:讓 EVM(以太坊虛�擬機)執行的交易產生 ZK 證明。
困難在於:EVM 的設計從來沒考慮過 ZK 友好性。EVM 有:
- 256 位寬的操作數
- 複雜的記憶體模型
- 大量不可預測的控制流
- 狀態讀取模式不規整
目前有兩種路線:
電路導向(Starknet):
直接用 STARK 定義一套新的「ZK 友好的虛擬機」。好處是效率高,壞處是不兼容現有 EVM 字節碼。
EVM 等價(zkSync 1.5, Polygon zkEVM, Scroll):
把現有 EVM 操作碼逐一轉換成算術約束。兼容性更好,但電路複雜度爆炸。
# EVM 操作碼到約束的簡化映射示例
def EVM_ADD_constraint(circuit, a, b, result):
"""
EVM ADD (0x01) 操作碼的約束
a, b, result 是電路中的線(wire)
"""
# 約束:a + b = result (mod 2^256)
circuit.add_constraint(
(a + b - result) == 0,
f"EVM_ADD: {a} + {b} != {result}"
)
def EVM_SSTORE_constraint(circuit, key, value, state_root_before, state_root_after):
"""
EVM SSTORE 操作碼的約束
需要驗證狀態字典的更新是正確的
"""
# 約束:更新後的狀態根 = 舊狀態根 + 新鍵值對(用 Merkle 證明)
new_root = compute_merkle_update(
state_root_before, key, value
)
circuit.add_constraint(
(new_root - state_root_after) == 0,
"EVM_SSTORE: State root mismatch"
)
5.3 隱私交易(Aztec, Zcash)
另一個 ZK 的殺手級應用是隱私交易:隱藏發送者、接收者和金額,但仍然讓網路驗證交易的有效性。
Aztec 是以太坊上的隱私 Layer 2。它使用 ZK 證明來實現:
- 隱藏交易金額:使用同態加密
- 隱藏發送者:使用承諾和 nullifier
- 驗證交易平衡:在不透露金額的情況下驗證「輸入 = 輸出 + 手續費」
# Aztec 風格的隱私轉帳約束
def private_transfer_constraint(circuit, notes):
"""
驗證一筆隱私轉帳的約束
輸入:多個「票據」(note),每個票據包含:
- commitment:承諾 = Hash(金額, 所有者公鑰, nonce)
- 金額(已知給證明者,但電路外不可見)
- 所有者公鑰
- nonce:用於防止重放攻擊
"""
# 約束 1:輸入票據總額 = 輸出票據總額 + 手續費
total_input = sum(note.amount for note in notes['input'])
total_output = sum(note.amount for note in notes['output'])
fee = circuit.public_input('fee')
circuit.add_constraint(
total_input - total_output - fee == 0,
"Private transfer: Amount mismatch"
)
# 約束 2:每個票據的 commitment 是正確的
for note in notes['input'] + notes['output']:
expected_commitment = keccak256(
note.amount, note.owner_public_key, note.nonce
)
circuit.add_constraint(
note.commitment - expected_commitment == 0,
f"Note commitment mismatch for {note}"
)
# 約束 3:每個 input note 生成一個 nullifier
# nullifier = Hash(commitment, spending_key)
# 同一個 commitment 不能被「花費兩次」
for note in notes['input']:
nullifier = keccak256(note.commitment, note.spending_key)
circuit.add_public_input(nullifier, name=f"nullifier_{note.id}")
5.4 去中心化身份(DID)與 zkJWT
ZK 也在重新定義數位身份。想象這樣的場景:
你想證明「我是成年人」,但不想透露:
- 你的具體年齡
- 你的身份證號
- 你的姓名
傳統方式不可能做到。但用 ZK,你可以:
- 發證機構頒發一個「年齡承諾」
- 你生成 ZK 證明:「這個承諾代表的年齡 >= 18」
- 驗證者只看到「通過/不通過」,不知道任何其他資訊
這就是 zkJWT(Zero-Knowledge JSON Web Token)的概念。
# zkJWT 概念實現
class zkCredential:
"""
一個零知識身份的簡化實現
"""
def __init__(self, issuer_public_key, attributes):
# attributes 是「敏感屬性」
self.issuer = issuer_public_key
self.attributes = attributes # {'age': 25, 'country': 'TW', 'score': 850}
# 生成承諾
self.commitment = self._compute_commitment()
# 簽名
self.signature = self._issuer_sign()
def _compute_commitment(self):
"""
承諾 = Hash(屬性, 隨機鹽)
承諾是公開的,但反向推出屬性是不可行的
"""
salt = secrets.token_bytes(32)
self.salt = salt
return keccak256(
rlp.encode([self.attributes, salt])
)
def prove_attribute(self, attribute_name, predicate, predicate_value):
"""
生成零知識證明:驗證某個屬性滿足某個條件
例如:prove_attribute('age', '>=', 18)
"""
# 約束:年齡必須滿足條件
age = self.attributes[attribute_name]
if predicate == '>=':
assert age >= predicate_value
# 構建電路
circuit = ConstraintSystem()
# 公開輸入:承諾、predicate 結果
circuit.add_public_input(self.commitment, name='commitment')
circuit.add_public_input(1, name='proof_valid') # 簡化:直接等於 1
# 私密輸入(witness):年齡、鹽
circuit.add_private_input(age, name='age')
circuit.add_private_input(self.salt, name='salt')
# 約束:承諾是正確的
computed_commitment = keccak256(
rlp.encode([{'age': age}, self.salt])
)
circuit.add_constraint(
computed_commitment - self.commitment == 0,
"Commitment verification failed"
)
# 約束:年齡滿足 predicate
if predicate == '>=':
circuit.add_constraint(
age - predicate_value >= 0,
f"Age {predicate} {predicate_value} failed"
)
# 生成證明
proof = generate_zk_proof(circuit)
return {
'commitment': self.commitment,
'predicate': f"{attribute_name} {predicate} {predicate_value}",
'proof': proof
}
六、Solidity 驗證合約實戰
6.1 驗證合約的基礎架構
ZK 證明最終要在以太坊上驗證。這需要一個 Solidity 合約來執行配對檢查。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// BN128 配對預編譯合約介面
interface IBN128 {
function add(
uint256 ax, uint256 ay,
uint256 bx, uint256 by
) external pure returns (uint256, uint256, uint256, uint256);
function mul(
uint256 scalar,
uint256 x,
uint256 y
) external pure returns (uint256, uint256, uint256, uint256);
}
interface IPairing {
function pairing(
uint256[] memory _p
) external view returns (bool);
}
contract Groth16Verifier {
// BN128 基點座標
uint256 constant G1_X = 1;
uint256 constant G1_Y = 2;
// G2 基點座標(用兩個橢圓曲線點表示)
uint256 constant G2_X1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634;
uint256 constant G2_X2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781;
uint256 constant G2_Y1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531;
uint256 constant G2_Y2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930;
IPairing public pairingContract;
constructor(address _pairingContract) {
pairingContract = IPairing(_pairingContract);
}
// Verification Key 結構
struct VerifyingKey {
// Alpha G1
uint256[2] alpha;
// Beta G2
uint256[2][2] beta;
// Gamma G2
uint256[2][2] gamma;
// Delta G2
uint256[2][2] delta;
// IC:public input 的線性組合係數
uint256[2][] IC;
}
// 驗證 zk-SNARK 證明
function verifyProof(
// 證明 (A, B, C 三個 G1 點)
uint256[2] memory a,
uint256[2][2] memory b,
uint256[2] memory c,
// Public inputs
uint256[] memory inputs,
// Verification Key
uint256[2] memory alpha,
uint256[2][2] memory beta,
uint256[2][2] memory gamma,
uint256[2][2] memory delta,
uint256[2][] memory IC
) public view returns (bool) {
// 配對檢查:e(A, B) = e(alpha, beta) · e(-gamma, -delta) · e(IC(inputs), delta)
uint256[24] memory p;
// 配對元素 1-2: e(A, B)
p[0] = a[0];
p[1] = a[1];
p[2] = b[0][0];
p[3] = b[0][1];
p[4] = b[1][0];
p[5] = b[1][1];
// 配對元素 2-3: e(alpha, beta)
p[6] = alpha[0];
p[7] = alpha[1];
p[8] = beta[0][0];
p[9] = beta[0][1];
p[10] = beta[1][0];
p[11] = beta[1][1];
// 配對元素 3-4: e(-gamma, 1) 和 e(IC(inputs), delta)
// 首先計算 IC 的線性組合
uint256[2] memory gamma_inv = negate(gamma[0], gamma[1]);
uint256[2] memory IC_linear = IC[0]; // 第一個 IC 是 1
for (uint i = 1; i <= inputs.length; i++) {
// IC_linear += IC[i] * inputs[i-1]
IC_linear = add_G1(IC_linear, scalar_mul(IC[i], inputs[i-1]));
}
// e(-gamma, 1) - 我們用 (G1, G2) 來表示 1
p[12] = gamma_inv[0];
p[13] = gamma_inv[1];
p[14] = G2_X1;
p[15] = G2_Y1;
// e(IC_linear, delta)
p[16] = IC_linear[0];
p[17] = IC_linear[1];
p[18] = delta[0][0];
p[19] = delta[0][1];
p[20] = delta[1][0];
p[21] = delta[1][1];
// 加上 e(C, delta)
uint256[2] memory neg_delta = negate(delta[0], delta[1]);
p[22] = c[0];
p[23] = c[1];
// 最後一個配對元素:e(C, -delta)
// 我們把 C 加到 IC_linear 並調整配對方程
return pairingContract.pairing(p);
}
// 橢圓曲線標量乘法輔助函數
function scalar_mul(
uint256[2] memory point,
uint256 scalar
) internal view returns (uint256[2] memory result) {
// 呼叫預編譯合約
(uint256 rx, uint256 ry, , ) = IBN128(0x07).mul(
scalar,
point[0],
point[1]
);
return [rx, ry];
}
// 橢圓曲線加法輔助函數
function add_G1(
uint256[2] memory p1,
uint256[2] memory p2
) internal view returns (uint256[2] memory result) {
(uint256 rx, uint256 ry, , ) = IBN128(0x07).add(
p1[0], p1[1],
p2[0], p2[1]
);
return [rx, ry];
}
// 取負
function negate(uint256 x, uint256 y) internal pure returns (uint256, uint256) {
if (x == 0 && y == 0) return (0, 0);
uint256 p = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
return (x, (p - y) % p);
}
}
6.2 使用 Circom 實作電路
Circom 是目前最流行的 ZK 電路描述語言。它的語法類似 JavaScript,讓你用聲明式的方式描述算術約束。
// circomlib 引入標準庫
include "../node_modules/circomlib/circuits/poseidon.circom";
include "../node_modules/circomlib/circuits/bitify.circom";
/*
電路:範例 - 範圍證明
證明:「我是一個在 [0, 2^n) 範圍內的數字」
這個電路在身份系統中很有用:你可以在不透露具體數字的情況下
證明它滿足某些範圍約束
*/
template RangeProof(n) {
// input 是一個私密信號(witness)
signal private input in;
// range 是公開信號,表示有效範圍的上界
signal input range;
// out 是輸出約束,確保結果是二進制
signal output out;
// 把 in 轉成 n 位的二進製表示
component num2bits = Num2Bits(n);
num2bits.in <== in;
// 確保每個位元都是 0 或 1(Num2Bits 已經保證了這個)
// 但我們可以加額外的約束來確保更安全
// 確保 in < range
// 方法:檢查 in - range 的二進製表示是否全為 1
// (在 two's complement 中,負數的最高位是 1)
// 更簡單的方法:確保 in 可以用 range 的位數表示
component lessThan = LessThan(n);
lessThan.in[0] <== in;
lessThan.in[1] <== range;
// out = 1 表示驗證通過
out <== lessThan.out;
}
/*
主電路:認證電路
假設場景:
- 有一個祕密值 `secret`
- 有一個承諾 `commitment = Hash(secret, nonce)`
- 我們要證明:知道這個 secret,但不透露它的值
*/
template Authenticator() {
// Public inputs
signal input commitment;
// Private inputs (witnesses)
signal private input secret;
signal private input nonce;
// 約束:commitment 必須是 Hash(secret, nonce) 的結果
component hasher = Poseidon(2);
hasher.inputs[0] <== secret;
hasher.inputs[1] <== nonce;
// 約束:承諾必須匹配
commitment === hasher.out;
}
template Main() {
// Public input: commitment
signal input commitment;
// Private witness: secret 和 nonce
signal private input secret;
signal private input nonce;
// 實例化認證電路
component auth = Authenticator();
auth.commitment <== commitment;
auth.secret <== secret;
auth.nonce <== nonce;
// 我們可以加入額外的約束
// 例如:secret 必須是某個範圍內
component rangeProof = RangeProof(253);
rangeProof.in <== secret;
rangeProof.range <== 2**250; // 確保 secret 在合理範圍內
}
component main = Main();
6.3 生成證明並部署到以太坊
// 使用 snarkjs 生成證明和部署
const { groth16 } = require("snarkjs");
async function generateProof() {
// 1. 編譯電路
const { wasm, vk } = await groth16.compile("./circuit/main.circom");
// 2. 準備 witness(計算機世紀的「分配」)
const input = {
commitment: "123456789...", // public
secret: "987654321...", // private
nonce: "111111..." // private
};
// 計算 witness
const wtns = await groth16.wtns.calculate(
{ input: input },
wasm
);
// 3. 生成證明
// 需要有對應電路的 proving key (from trusted setup)
const zkey = await groth16.Zkey.fromFile("./zkeys/circuit_0000.zkey");
const proof = await groth16.prove(zkey, wtns);
console.log("Proof:", {
a: proof.pi_a,
b: proof.pi_b,
c: proof.pi_c,
publicSignals: proof.publicSignals
});
// 4. 生成 Solidity 驗證器參數
const solidityVerifier = await groth16.exportSolidityVerifier(
zkey.vk_proof,
zkey.vk_verifier
);
return { proof, solidityVerifier };
}
// 驗證證明
async function verifyProof(proof, publicSignals) {
const vKey = require("./zkeys/verification_key.json");
const res = await groth16.verify(vKey, publicSignals, proof);
console.log("Verification result:", res);
return res;
}
// 部署驗證合約的 Hardhat 腳本
const hre = require("hardhat");
async function main() {
const { proof, solidityVerifier } = await generateProof();
// 部署驗證合約
const Verifier = await hre.ethers.getContractFactory("Groth16Verifier");
const verifier = await Verifier.deploy();
await verifier.deployed();
console.log("Verifier deployed to:", verifier.address);
// 或者部署 snarkjs 生成的驗證合約
const AutoGeneratedVerifier = await hre.ethers.getContractFactory(
"Verifier"
);
const autoVerifier = await AutoGeneratedVerifier.deploy(
...extractVerifierParams(solidityVerifier)
);
await autoVerifier.deployed();
// 驗證一個 proof
const publicSignals = [/* commitment */];
const valid = await autoVerifier.verifyProof(
proof.pi_a,
proof.pi_b,
proof.pi_c,
publicSignals
);
console.log("Proof valid:", valid);
}
七、性能優化與最佳實踐
7.1 電路設計優化
電路的效率直接決定了生成證明的時間和成本。以下是一些關鍵優化策略:
1. 減少約束數量
約束是電路中最重要的資源。每增加一個約束,Prover 需要多做一次多項式計算。
不好的設計:
signal a <== input; // 約束:a - input = 0
signal b <== input * 2; // 約束:b - input*2 = 0
signal c <== a + b; // 約束:c - a - b = 0
更好的設計:
signal b <== input * 2; // 直接計算
signal c <== input + input * 2; // 用 input 直接表達 c = 3*input
// 節省了一個約束
2. 使用承諾而非直接比較
當你需要比較一個很大的值(比如 256 位的 hash)時,不要逐位元比較。用承諾!
// 不好的做法:256 個位元約束
for (i = 0; i < 256; i++) {
signal bit_i;
bit_i <== (hash >> i) & 1;
// 每個位元都是一個約束
}
// 好的做法:用承諾
component hash = Poseidon(2);
hash.inputs[0] <== value1;
hash.inputs[1] <== nonce1;
component hash2 = Poseidon(2);
hash2.inputs[0] <== value2;
hash2.inputs[1] <== nonce2;
// 只用一個約束:commitment1 == commitment2
commitment1 === commitment2; // 一個約束代替 256 個
3. 選擇合適的哈希函數
不同的哈希函數在 ZK 中的效率差異巨大:
| 哈希函數 | ZK 友好性 | 電路複雜度 | 安全性 |
|---|---|---|---|
| Keccak-256 | 差 | 很高 | 標準 |
| Poseidon | 好 | 低 | 專門為 ZK 設計 |
| Rescue | 好 | 低 | 類似 Poseidon |
| MiMC | 好 | 低 | 需要更多 rounds |
7.2 以太坊 Gas 優化
驗證合約的 gas 消耗是瓶頸。以下技巧可以顯著降低驗證成本:
// Gas 優化版本
contract OptimizedVerifier {
// 批量驗證:多個 proof 只驗證一次
// 適合需要大量相同類型驗證的場景(如隱私交易)
function verifyBatch(
// 批次中的 proof 數量
uint256 batchSize,
// 所有 proof 的拼接
uint256[8][] memory proofs,
// 所有 public signals 的拼接
uint256[][] memory signals
) public view returns (bool) {
// 對每個 proof 執行驗證
// 但使用批次配對檢查來節省 gas
for (uint i = 0; i < batchSize; i++) {
if (!verifySingle(proofs[i], signals[i])) {
return false;
}
}
return true;
}
// 內聯驗證:把驗證邏輯直接寫在調用合約中
// 避免跨合約呼叫的開銷
}
7.3 常見錯誤與避坑指南
- 不要假設所有操作都是免費的
在電路中,每個乘法都是一個約束。除法和取模尤其昂貴。
- 注意信號的範圍
預設情況下,Signal 可以是任意大小的數字。如果你想表達「這是一個位元」,必須明確約束它。
- 調試比普通代碼難 100 倍
ZK 電路的錯誤訊息通常非常神祕。建議先用小電路測試,逐步擴展。
- 信任設置的「有毒廢料」
如果你參與了信任設置,千萬不要在任何地方留下你的 toxic waste 隨機種子。
八、未來展望:ZK 的下一步
8.1 ZK-EVM 的成熟
2026 年,我們會看到第一批真正「生產就緒」的 zkEVM。它們將實現:
- 與 EVM 字節碼的完全兼容
- 驗證時間降低到可接受範圍
- 大幅降低 Layer 2 的交易成本
8.2 ZKML:ZK + 機器學習
ZK 和 ML 的結合是另一個激動人心的方向:Proof of ML Inference。
應用場景:
- 驗證一個 AI 模型在某個輸入上輸出了某個結果,但不需要透露模型權重
- 去中心化的 AI 計算市場
- 隱私保護的推薦系統
# ZKML 的概念框架
class ZKMLVerifier:
"""
驗證 ML 模型推理的 ZK 電路
"""
def verify_inference(self, model_weights, input_data, expected_output, proof):
"""
驗證:給定 input_data,模型的輸出是否為 expected_output
但不透露 model_weights(模型的智慧財產權)
"""
# 電路需要執行:
# 1. 矩陣乘法
# 2. 激活函數(ReLU, Sigmoid, etc.)
# 3. 輸出層計算
# 這裡的挑戰是:ML 模型的計算量很大
# 需要專門的 ZK 友好算術電路
pass
8.3 抗量子 ZK
STARK 已經是抗量子的。但研究人員正在探索更高效的抗量子 ZK 系統,包括:
- 基於格的 ZK 證明(Lattice-based ZK)
- 基於編碼的 ZK 證明(Code-based ZK)
- 這些可能在未來取代現有的配對基礎方案
結語:動手比看懂更重要
零知識證明的世界很大,這篇文章只能帶你走到門口。真正的理解需要動手實作——寫電路、生成證明、部署合約、處理失敗。
我建議你從頭開始:
- 安裝 snarkjs 和 circom
- 編譯第一個「Hello World」電路
- 生成第一個證明
- 在本地測試網部署驗證合約
- 解決過程中的所有「為什麼失敗了」問題
這個過程會比任何文章都更深刻地告訴你 ZK 到底是什麼。祝福你在零知識的宇宙裡玩得開心!
參考資源
- Circom 文件
- snarkjs 文件
- zkSNARKs in a Nutshell
- STARKs Part I, II, III
- PLONK 論文
- Groth16 論文
- Ethereum Attestation Service
COMMIT: Expand ZK proof article with complete technical depth and practical code examples
相關文章
- Privacy Pool ZK-Proof 驗證合約完整實作指南:從電路設計到 Solidity 部署 — 本文深入探討 Privacy Pool 系統中零知識證明(ZKP)驗證合約的完整實作流程。我們從密碼學基礎出發,詳細解釋 Groth16 和 PLONK 兩種主流零知識證明系統的原理,提供完整的 Circom 電路代碼範例,並展示如何將這些電路部署到以太坊區塊鏈上進行驗證。涵蓋 Merkle 樹驗證電路、承諾方案實現、完整隱私池合約代碼、以及可信設置教學。
- 零知識證明數學推導完整指南:從密碼學基礎到以太坊應用實戰 — 本文從數學推導的角度,全面分析零知識證明的基本原理、主要類型(SNARK、STARK、Bulletproofs)、電路設計方法,以及在以太坊上的實際應用部署。涵蓋完整的代數推導、Groth16 和 Plonkish 約束系統、FRI 協議、以及 zkEVM 架構分析。詳細比較不同 ZK 系統的 Gas 消耗與 TPS 表現,提供量化數據支撐的事實依據。
- ZK-SNARK 完整學習路徑:從基礎數學到 Circom/Noir 電路設計再到實際部署 — 本學習路徑提供零知識證明從理論基礎到實際開發的完整指南。從離散數學、群論、有限域運算開始,深入橢圓曲線密碼學和配對函數,再到 Groth16、PLONK 等主流證明系統的數學推導,最終落實到 Circom 和 Noir 兩種電路描述語言的實戰開發。涵蓋有限域運算、多項式承諾、KZG 方案、信任設置等核心主題,提供從基礎到部署的完整學習地圖。
- ZK-SNARK 數學推導完整指南:從零知識證明到 Groth16、PLONK、STARK 系統的深度數學分析 — 本文從數學基礎出發,完整推導 Groth16、PLONK 與 STARK 三大主流 ZK 系統的底層原理,涵蓋橢圓曲線密碼學、配對函數、多項式承諾、LPC 證明系統等核心技術,同時提供 Circom 與 Noir 電路開發的實戰程式碼範例。截至 2026 年第一季度,ZK-SNARK 已被廣泛部署於 zkRollup、隱私協議、身份驗證系統等場景。
- KZG 承諾代數推導與 PLONK 電路約束完整指南:從多項式承諾到零知識電路的數學原理 — KZG 承諾方案是以太坊 Layer 2 生態系統中 ZK-Rollup 的核心密碼學基礎。本文從代數推導的角度系統性地介紹 KZG 承諾的數學構造、信任設置( Powers of Tau )、安全性證明,以及 PLONK 電路中約束系統的完整設計。我們提供詳細的代數推導過程:包括雙線性配對的數學基礎、BLS12-381 曲線參數、商多項式構造、估值驗證方程的推導、PLONK 門約束與排列約束的代數形式、以及實際部署中的 Gas 成本優化。同時包含 Circom 電路設計範例和 zkSync、Starknet 等項目的工程實踐分析。
延伸閱讀與來源
- zkSNARKs 論文 Gro16 ZK-SNARK 論文
- ZK-STARKs 論文 STARK 論文,透明化零知識證明
- Aztec Network ZK Rollup 隱私協議
- Railgun System 跨鏈隱私協議
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!