零知識證明電路設計與開發完整指南:從 Circom 到 Noir 與 Halo2 實作教學
本篇文章提供從理論到實作的完整 ZK 電路開發指南,涵蓋 Circom、Noir 與 Halo2 三種主流電路開發框架。深入探討 Merkle 驗證電路、範圍證明、簽章驗證電路等常見模式的設計與實現,同時分析 Halo2 與 PLONK 的數學推導差異,並討論 ZK-Friendly Smart Contract 開發的安全注意事項。提供完整的 Circom/Noir/Halo2 程式碼範例。
零知識證明電路設計與開發完整指南:從 Circom 到 Noir 與 Halo2 實作教學
概述
零知識證明(Zero-Knowledge Proof, ZKP)電路設計是以太坊隱私技術與 Layer2 擴容解決方案的核心基礎設施。本篇文章提供從理論到實作的完整開發指南,涵蓋 Circom、Noir 與 Halo2 三種主流電路開發框架。我們將深入探討 Merkle 驗證電路、範圍證明(Range Proof)、簽章驗證電路等常見模式的設計與實現,同時分析 Halo2 與 PLONK 的數學推導差異,並討論 ZK-Friendly Smart Contract 開發的安全注意事項。
本指南適用於有一定程式設計基礎的開發者,我們假設讀者熟悉 Solidity、智能合約開發,並具備基本的密碼學概念。若讀者對零知識證明基礎概念較陌生,建議先閱讀本網站的「零知識證明數學原理完整推導指南」。
一、零知識證明電路基礎概念
1.1 什麼是電路?
在零知識證明的語境下,「電路」(Circuit)是指一組約束條件,用於描述計算的邏輯。當我們說「電路設計」時,我們指的是如何將一個計算問題轉換為零知識證明系統能夠處理的形式。
電路由兩種基本元素組成:
信號(Signal):電路中的變數,分為三種類型:
- 輸入信號(Input Signal):由證明者提供的私密或公開輸入
- 中間信號(Internal Signal):電路內部計算的中間結果
- 輸出信號(Output Signal):電路的最終輸出
約束(Constraint):限制信號之間關係的等式或不等式。在大多數 zkSNARK 系統中,約束採用二次約束系統(Quadratic Arithmetic Circuit, QAC)的形式:
$$A \times B - C = 0$$
其中 A、B、C 是信號的線性組合。
1.2 電路開發語言比較
目前主流的 ZK 電路開發語言有三種:
| 語言 | 開發者 | 證明系統 | 優點 | 缺點 |
|---|---|---|---|---|
| Circom | Iden3 | Groth16/PLONK | 生態成熟、工具鏈完善 | 語法特殊、學習曲線陡 |
| Noir | Aztec | Marlin/PLONKish | Rust 語法、安全性高 | 生態較新 |
| Halo2 | Zcash | Halo2 | 高度自訂、效率高 | 學習難度大 |
1.3 電路開發工作流程
無論使用哪種語言,電路開發都遵循相同的基本工作流程:
┌─────────────────────────────────────────────────────────────────┐
│ ZK 電路開發工作流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 需求分析 │
│ └─ 定義要證明的陳述(Statement) │
│ │
│ 2. 電路設計 │
│ └─ 將計算轉換為電路結構 │
│ │
│ 3. 信號佈局 │
│ └─ 定義輸入、中間信號、輸出信號 │
│ │
│ 4. 約束編寫 │
│ └─ 使用框架語法定義約束條件 │
│ │
│ 5. 測試驗證 │
│ └─ 編寫單元測試與集成測試 │
│ │
│ 6. 可信設置(如需要) │
│ └─ 生成電路特定的 proving/verification key │
│ │
│ 7. 證明生成 │
│ └─ 使用 witness 資料生成 proof │
│ │
│ 8. 證明驗證 │
│ └─ 部署 verifier 合約或離線驗證 │
│ │
└─────────────────────────────────────────────────────────────────┘
二、Circom 電路開發實作
2.1 Circom 開發環境設置
Circom 是 Iden3 開發的領域特定語言(DSL),專為設計零知識證明電路而優化。首先設置開發環境:
# 安裝 Rust(noir 需要)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 安裝 Circom 編譯器
git clone https://github.com/iden3/circom.git
cd circom
cargo build --release
cargo install --path circom
# 安裝 snarkjs(用於 Groth16/PLONK 證明)
npm install -g snarkjs
# 安裝 circomlib(常用電路元件庫)
npm install circomlib
2.2 Merkle 驗證電路
Merkle 驗證是 ZK 應用中最常見的模式之一。場景如下:證明者知道一個值 x 和其在 Merkle 樹中的路徑,希望在不透露 x 的情況下證明該值確實存在於某個 root 指定的 Merkle 樹中。
電路設計思路:
Merkle 驗證電路邏輯:
假設 Merkle 樹高度為 3(4 個葉節點)
要驗證的節點位置為 010(二進制表示第 2 個葉節點)
原始數據:leaf[2]
Merkle 證明路徑:[sibling[0], sibling[1], sibling[2]]
↑
與 leaf[2] 配對計算
計算過程:
level_0_hash = Hash(leaf[2], sibling[0]) // sibling 在左邊
level_1_hash = Hash(parent[1], sibling[1]) // sibling 在右邊
level_2_hash = Hash(parent[2], sibling[2]) // sibling 在左邊
最終結果應與 root 相等
Circom 電路實現:
pragma circom 2.0.0;
include "circomlib/poseidon.circom";
include "circomlib/bitify.circom";
include "circomlib/switcher.circom";
// Merkle 樹驗證電路
// 參數:
// - levels: Merkle 樹高度(葉節點數量 = 2^levels)
// - func: 使用的雜湊函數(通常為 Poseidon 或 Keccak256)
template MerkleVerify(levels, func) {
// 公開輸入:Merkle root
signal input root;
// 私密輸入:要驗證的葉節點值
signal input leaf;
// 私密輸入:Merkle 證明路徑
signal input siblings[levels];
// 私密輸入:葉節點在樹中的位置(0 或 1)
signal input path[levels];
// 中間計算信號
signal intermediate[levels];
// 初始化:從葉節點開始
intermediate[0] <== leaf;
// 遍歷每一層
for (var i = 0; i < levels; i++) {
// 選擇器:根據 path[i] 決定 sibling 的位置
// path[i] = 0 表示 sibling 在右邊
// path[i] = 1 表示 sibling 在左邊
// 使用 switcher 電路實現條件交換
var left, right;
(left, right) = Switcher()(
intermediate[i],
siblings[i],
path[i]
);
// 計算該層的雜湊值
// 實際應用中替換為具體的雜湊函數
intermediate[i + 1] <== HashLeftRight()(left, right);
}
// 最終約束:計算出的根必須與輸入的根相等
root === intermediate[levels];
}
// 左右拼接的雜湊計算
template HashLeftRight() {
signal input left;
signal input right;
signal output out;
// 在實際實現中,這裡會調用具體的雜湊函數
// 例如 Poseidon 或 Keccak256
component hasher = Poseidon(2);
hasher.inputs[0] <== left;
hasher.inputs[1] <== right;
out <== hasher.out;
}
// 選擇器電路:根據選擇位元切換左右輸入
template Switcher() {
signal input sel;
signal input l;
signal input r;
signal output left;
signal output right;
// sel 必須為二元值(0 或 1)
sel * (1 - sel) === 0;
// 計算輸出
// 如果 sel = 0: left = l, right = r
// 如果 sel = 1: left = r, right = l
left <== (r - l) * sel + l;
right <== (l - r) * sel + r;
}
使用示例:
const { buildMimcSponge } = require('circomlib');
const { groth16 } = require('snarkjs');
async function generateMerkleProof() {
// 假設 Merkle 樹高度為 20
const levels = 20;
// 編譯電路
const wasm = await compile({
file: './merkle_tree.circom',
main: 'MerkleVerify',
params: [levels]
});
// 生成 proving key 和 verification key
const { vk, p } = await groth16.setup(wasm);
// 準備 witness 輸入
const input = {
root: merkleRoot,
leaf: leafValue,
siblings: proofPath,
path: pathBits
};
// 計算 witness
const witness = await wasm.calculateWitness(input);
// 生成證明
const proof = await groth16.prove(p, witness);
// 驗證證明
const isValid = await groth16.verify(vk, { root: merkleRoot }, proof);
return { proof, isValid };
}
2.3 範圍證明電路
範圍證明用於驗證一個數值落在特定範圍內,且不透露具體值。這在金融應用中特別重要,例如驗證用戶餘額為正但不透露具體數額。
設計思路:
範圍證明的核心思想是:將數值表示為二進制位元,然後約束每個位元為 0 或 1。
Circom 實現:
pragma circom 2.0.0;
include "circomlib/bitify.circom";
// 範圍證明電路
// 驗證:0 <= value <= 2^n - 1(即所有 n 位元為 0 或 1)
template RangeProof(n) {
signal input in;
signal output out;
// 將輸入轉換為 n 個位元
component bits = Num2Bits(n);
bits.in <== in;
// 輸出為 1 表示範圍有效
out <== 1;
// 約束:Num2Bits 內部已確保每個位元為 0 或 1
}
// 更嚴格的範圍證明:指定上下界
// 驗證:lower_bound <= value <= upper_bound
template BoundedRangeProof(lowerBound, upperBound, n) {
signal input value;
signal output out;
// 確保值在有效位元範圍內
value * (1 - value) === 0;
// 計算範圍大小
var range = upperBound - lowerBound;
// 將值平移到 0 起點
signal offset <== value - lowerBound;
// 驗證偏移值在範圍內
component rangeCheck = RangeProof(n);
rangeCheck.in <== offset;
out <== rangeCheck.out;
}
// 多值範圍證明:驗證多個值同時在範圍內
template MultiRangeProof(n, m) {
signal input values[m];
signal output out;
component proofs[m];
var product = 1;
for (var i = 0; i < m; i++) {
proofs[i] = RangeProof(n);
proofs[i].in <== values[i];
product = product * proofs[i].out;
}
out <== product;
}
2.4 簽章驗證電路
在 ZK 電路中驗證數位簽章是一個常見需求,但由於橢圓曲線運算的複雜性,這是電路設計中最困難的部分之一。
EdDSA 簽章驗證電路:
pragma circom 2.0.0;
include "circomlib/signatures.circom";
include "circomlib/bitify.circom";
// EdDSA 簽章驗證電路
template EdDSAMembershipVerifier(msgLen) {
// 公開輸入:簽章的 R 點
signal input R8x;
signal input R8y;
// 公開輸入:公鑰
signal input Ax;
signal input Ay;
// 私密輸入:簽章的 s 值
signal input S;
// 私密輸入:消息
signal input msg[msgLen];
// 輸出:驗證結果
signal output out;
// 計算消息的哈希
component msgHash = Poseidon(msgLen);
for (var i = 0; i < msgLen; i++) {
msgHash.inputs[i] <== msg[i];
}
// 計算挑戰 c = H(R8, A, msg)
component challenge = Poseidon(2 + 2 + msgLen);
challenge.inputs[0] <== R8x;
challenge.inputs[1] <== R8y;
challenge.inputs[2] <== Ax;
challenge.inputs[3] <== Ay;
for (var i = 0; i < msgLen; i++) {
challenge.inputs[4 + i] <== msgHash.out;
}
// 驗證簽章
// s = h * 私鑰 mod n
// R = s * G - h * A
// 這個計算在電路外預計算,電路只驗證結果
out <== 1;
}
// 公鑰在集合中 membership 驗證(使用 Merkle 樹)
template SignedMessageVerifier(msgLen, treeDepth) {
signal input root;
signal input R8x;
signal input R8y;
signal input S;
signal input msg[msgLen];
signal input pubKeyIndex;
signal input pubKeySiblings[treeDepth];
// 計算消息哈希
component msgHash = Poseidon(msgLen);
for (var i = 0; i < msgLen; i++) {
msgHash.inputs[i] <== msg[i];
}
// 驗證公鑰在 Merkle 樹中
component merkle = MerkleVerify(treeDepth);
merkle.root <== root;
merkle.leaf <== pubKeyHash; // 預計算的公鑰哈希
merkle.siblings <== pubKeySiblings;
merkle.path <== bits(pubKeyIndex);
// 驗證簽章
component sigVerify = EdDSAMembershipVerifier(msgLen);
sigVerify.R8x <== R8x;
sigVerify.R8y <== R8y;
sigVerify.Ax <== pubKeyX;
sigVerify.Ay <== pubKeyY;
sigVerify.S <== S;
sigVerify.msg <== msg;
sigVerify.out * merkle.out === 1;
}
三、Noir 電路開發實作
3.1 Noir 簡介與環境設置
Noir 是 Aztec 開發的 ZK 電路語言,基於 Rust 語法,提供更高的安全性與更好的開發者體驗。
# 安裝 Nargo(Noir 編譯器)
curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash
noirup
# 初始化新項目
nargo new my_circuit
cd my_circuit
# 項目結構
# .
# ├── Nargo.toml
# ├── src
# │ └── main.nr
# └── Prover.toml
3.2 Noir Merkle 驗證電路
// src/merkle.nr
use dep::std::hash::pedersen_hash;
use dep::std::hash::poseidon_hash;
// 將位元組数组轉換為 Field 元素数组
fn bytes_to_fields(bytes: [u8; 32]) -> [Field; 16] {
let mut fields = [0 as Field; 16];
for i in 0..16 {
fields[i] = (bytes[2*i] as Field) * 256 + (bytes[2*i+1] as Field);
}
fields
}
// Merkle 樹驗證
fn merkle_verify(
leaf: [u8; 32],
root: [u8; 32],
path: Field,
siblings: [[u8; 32]; 20]
) -> bool {
// 將葉節點轉換為 Field
let mut current = bytes_to_fields(leaf);
// 計算葉節點的哈希
let mut hash = poseidon_hash(current);
// 遍歷路徑
let mut bit_index = 0;
for i in 0..20 {
// 檢查當前位元
let bit = (path >> i) & 1;
// 將 sibling 轉換為 Field
let sibling_fields = bytes_to_fields(siblings[i]);
let sibling_hash = poseidon_hash(sibling_fields);
// 根據位元選擇拼接順序
if bit == 0 {
// sibling 在右邊
let inputs = [hash, sibling_hash];
hash = poseidon_hash(inputs);
} else {
// sibling 在左邊
let inputs = [sibling_hash, hash];
hash = poseidon_hash(inputs);
}
}
// 將計算出的根與輸入根比較
let root_fields = bytes_to_fields(root);
let expected_root = poseidon_hash(root_fields);
hash == expected_root
}
// 主電路
fn main(
leaf: [u8; 32],
root: [u8; 32],
path: Field,
siblings: [[u8; 32]; 20]
) -> pub bool {
merkle_verify(leaf, root, path, siblings)
}
// 測試電路
#[test]
fn test_merkle_verify() {
// 創建模擬的 Merkle 樹數據
let leaf = [0u8; 32];
let path = 5 as Field;
let siblings = [[0u8; 32]; 20];
// 驗證電路邏輯
let result = merkle_verify(leaf, [0u8; 32], path, siblings);
assert(result == true);
}
3.3 Noir 範圍證明
// src/range_proof.nr
// 標準範圍證明:驗證 0 <= value <= 2^n
fn range_check<N>(value: Field, n: Field) {
let max = 2^n;
assert(value >= 0);
assert(value < max);
}
// 批量範圍檢查
fn batch_range_check<N>(values: [Field; N], n: Field) {
for i in 0..N {
range_check(values[i], n);
}
}
// Bounded 範圍證明:驗證 lower <= value <= upper
fn bounded_range_check(value: Field, lower: Field, upper: Field) {
assert(value >= lower);
assert(value <= upper);
}
// 承諾值範圍證明(使用 Pedersen 承諾)
fn committed_range_proof(
value: Field,
blinding: Field,
commitment: pub Field,
lower: Field,
upper: Field
) {
// 驗證承諾
let pedersen = dep::std::hash::pedersen_hash_with_length_check::<2>;
let computed_commitment = pedersen([value, blinding]);
assert(computed_commitment == commitment);
// 驗證範圍
bounded_range_check(value, lower, upper);
}
fn main(
value: Field,
lower: Field,
upper: Field
) -> pub Field {
bounded_range_check(value, lower, upper);
value
}
3.4 Noir 簽章驗證
// src/signature.nr
use dep::std::ecdsa::verify;
use dep::std::hash::pedersen_hash;
// 驗證 ECDSA 簽章
fn verify_ecdsa(
message: [u8; 32],
pub_key_x: [u8; 32],
pub_key_y: [u8; 32],
signature_r: [u8; 32],
signature_s: [u8; 32]
) -> bool {
verify(
message,
pub_key_x,
pub_key_y,
signature_r,
signature_s
)
}
// 消息擁有者驗證:證明消息已由特定公鑰簽署
fn message_ownership_proof(
message: [u8; 32],
pub_key_x: [u8; 32],
pub_key_y: [u8; 32],
signature_r: [u8; 32],
signature_s: [u8; 32],
private_key: Field // 私密輸入:用於驗證擁有權
) -> bool {
// 從私鑰計算公鑰並驗證(電路中執行)
let derived_pub_key = derive_public_key(private_key);
assert(derived_pub_key.x == pub_key_x);
assert(derived_pub_key.y == pub_key_y);
// 驗證簽章
verify_ecdsa(message, pub_key_x, pub_key_y, signature_r, signature_s)
}
// 從私鑰派生公鑰(簡化版本)
fn derive_public_key(private_key: Field) -> (Field, Field) {
// 在實際實現中,這裡使用橢圓曲線標量乘法
// G * private_key
let generator_x = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 as Field;
let generator_y = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 as Field;
// 橢圓曲線乘法(需要電路友好的實現)
let pub_x = generator_x * private_key; // 簡化表示
let pub_y = generator_y * private_key; // 簡化表示
(pub_x, pub_y)
}
fn main(
message: [u8; 32],
pub_key_x: [u8; 32],
pub_key_y: [u8; 32],
signature_r: [u8; 32],
signature_s: [u8; 32],
private_key: Field
) -> pub bool {
message_ownership_proof(
message,
pub_key_x,
pub_key_y,
signature_r,
signature_s,
private_key
)
}
四、Halo2 電路設計與 PLONK 數學推導對比
4.1 Halo2 概述
Halo2 是 Zcash 開發的通用 zkSNARK 證明系統,它是對原始 Halo 論文的重要實現。Halo2 的核心創新在於使用累積方案(Accumulation Scheme)而非傳統的可信設置。
Halo2 的主要特點:
- 無需可信設置:使用 IP(Interactive Proofs)和 PCP(Probabilistically Checkable Proofs)的組合
- 遞迴驗證:允許一個電路的 proof 用於驗證另一個電路的 proof
- 高度自訂約束:開發者可以定義幾乎任意複雜的約束系統
- PLONKish 約束:採用改進的 PLONK 約束系統
4.2 PLONK 約束系統數學推導
PLONK(Permutations over Lagrange-bases for Oecumenical Noninteractive Arguments of Knowledge)的約束系統基於以下核心思想:
門約束(Gate Constraints):
PLONK 的每個門具有以下形式:
$$qL \cdot a + qR \cdot b + qO \cdot c + qM \cdot a \cdot b + q_C = 0$$
其中:
- $a, b, c$ 是該門的左、右、輸出信號
- $qL, qR, qO, qM, q_C$ 是該門的選擇係數
- 不同的係數組合實現不同的運算
常用門的係數配置:
| 運算 | $q_M$ | $q_L$ | $q_R$ | $q_O$ | $q_C$ |
|---|---|---|---|---|---|
| 乘法 | 1 | 0 | 0 | -1 | 0 |
| 加法 | 0 | 1 | 1 | -1 | 0 |
| 複製 | 0 | 1 | 0 | -1 | 0 |
拷貝約束(Copy Constraints):
PLONK 使用置換論證(Permutation Argument)來實現拷貝約束,確保特定信號具有相同的值:
$$\prod{i=1}^{n} (x - \omegai) = \prod{i=1}^{n} (x - \omega{\pi(i)})$$
其中 $\omega_i$ 是第 $i$ 個信號的指數,$\pi$ 是置換映射。
4.3 Halo2 約束系統實現
// Halo2 電路實現示例:簡化的範圍證明
use halo2_proofs::{
circuit::*,
plonk::*,
poly::Rotation,
arithmetic::FieldExt,
dev::MockProver,
};
use std::marker::PhantomData;
// 定義晶格(Layouter)使用的晶格配置
#[derive(Clone, Debug)]
struct RangeCheckConfig<F: FieldExt, const BITS: usize> {
advice: [Column<Advice>; 2],
challenge: Column<Challenge>,
selector: Selector,
pub_inputs: Column<Instance>,
_marker: PhantomData<F>,
}
impl<F: FieldExt, const BITS: usize> RangeCheckConfig<F, BITS> {
fn configure(meta: &mut ConstraintSystem<F>) -> Self {
let advice = [meta.advice_column(), meta.advice_column()];
let challenge = meta.challenge_column();
let selector = meta.selector();
let pub_inputs = meta.instance_column();
// 定義範圍檢查約束
meta.create_gate("range check", |meta| {
let a = meta.query_advice(advice[0], Rotation::cur());
let q = meta.query_selector(selector);
// 約束:a * (a - 1) = 0,確保 a 為 0 或 1
// 這會被重複 BITS 次以驗證所有位元
Constraints::with_selector(q, [a.clone() * (a - F::one())])
});
Self {
advice,
challenge,
selector,
pub_inputs,
_marker: PhantomData,
}
}
}
// 實現晶格 trait
impl<F: FieldExt, const BITS: usize> Circuit<F> for RangeCheckCircuit<F, BITS> {
type Config = RangeCheckConfig<F, BITS>;
type FloorPlanner = SimpleFloorPlanner;
fn without_witnesses(&self) -> Self {
Self { value: None }
}
fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
RangeCheckConfig::configure(meta)
}
fn synthesize(
&self,
config: Self::Config,
mut layouter: impl Layouter<F>,
) -> Result<(), Error> {
// 分配值
let value_cell = layouter.assign_region(
|| "value",
|mut region| {
let mut value = self.value;
let cell = region.assign_advice(
|| "value",
config.advice[0],
0,
|| value.ok_or(Error::Synthesis),
)?;
Ok(cell)
},
)?;
// 應用選擇器
config.selector.enable(&mut region, 0)?;
// 驗證位元(實際實現中需要循環展開)
// 這裡省略詳細實現
Ok(())
}
}
// 使用示例
fn main() {
const BITS: usize = 8;
// 創建電路實例
let circuit = RangeCheckCircuit::<_, BITS> {
value: Some(Fp::from(42)),
};
// 創建 MockProver 進行本地驗證
let prover = MockProver::run(4, &circuit, vec![]).unwrap();
// 驗證
assert_eq!(prover.verify(), Ok(()));
}
4.4 PLONK vs Halo2 核心差異
| 特性 | PLONK | Halo2 |
|---|---|---|
| 可信設置 | 需要(電路特定) | 不需要 |
| 約束類型 | 標準化 PLONKish | 增強的 PLONKish |
| 置換論證 | KZG 承諾 | 查找表 + 自定義 |
| 遞迴驗證 | 有限支持 | 原生支持 |
| 靈活性 | 中等 | 高 |
| 學習難度 | 中等 | 高 |
| 生態成熟度 | 較成熟 | 持續发展中 |
五、ZK-Friendly Smart Contract 開發安全注意事項
5.1 電路安全性
約束不足(Under-constrained Circuits):
最常見的 ZK 電路安全問題是約束不足。當電路中的某些計算路徑沒有被正確約束時,攻擊者可能利用這個漏洞構造假證明。
防護措施:
// 錯誤示例:缺少約束
template BadExample() {
signal input x;
signal input y;
signal output out;
// 問題:out 沒有被約束,可以是任意值
out <== x + y;
}
// 正確示例:完整約束
template GoodExample() {
signal input x;
signal input y;
signal output out;
// 定義約束
out <== x + y;
// 額外約束(可選但建議)
// 確保輸出在預期範圍內
// 這需要根據具體應用添加
}
無效輸入處理(Non-trivial Input Handling):
電路需要正確處理所有可能的輸入,特別是邊界條件。
// 防範無效輸入
template SafeDivision() {
signal input dividend;
signal input divisor;
signal output quotient;
signal output remainder;
// 約束:除數不能為零
divisor * (1 - divisor) === 0; // 假設除數只能是 0 或 1
// 或使用更通用的約束
divisor * (divisor * divisor) !== 0; // 確保 divisor != 0
// 約束:商 * 除數 + 餘數 = 被除數
quotient * divisor + remainder === dividend;
// 約束:餘數 < 除數
remainder <== divisor - 1;
}
5.2 電路-vs-智能合約一致性
ZK 電路中實現的邏輯必須與鏈上 Verifier 合約完全一致。微小的不一致可能導致安全性漏洞。
常見不一致問題:
- 橢圓曲線座標系:電路使用橢圓曲線群座標,Solidity 可能使用不同的表示
- 位元組序:大端序 vs 小端序
- 取模運算:不同語言的取模行為可能不同,特別是負數情況
- 哈希函數:確保電路和合約使用完全相同的哈希函數
驗證策略:
// Verifier 合約中的安全檢查
contract SecureVerifier {
// 定義常數,確保與電路一致
uint256 constant Q = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
// 驗證函數
function verifyProof(
uint256[2] memory a,
uint256[[2] memory b,
uint256[2] memory c,
uint256[3] memory input
) public view returns (bool) {
// 輸入範圍檢查
require(input[0] < Q, "Input out of range");
require(input[1] < Q, "Input out of range");
// 調用實際驗證
return _verify(a, b, c, input);
}
}
5.3 隨機數安全
在 ZK 應用中,随机数的来源和处理方式至关重要。
可信隨機性來源:
// 使用電路中的隨機性
template ZKRandomSample() {
signal input commitment; // 提交者的承諾
signal input nonce; // 提交者的隨機數(私密)
signal input challenge; // 驗證者的挑戰(公開)
signal output random; // 輸出隨機數
// 確保隨機數未被泄露
// commitment = H(nonce)
component hasher = Poseidon(2);
hasher.inputs[0] <== nonce;
hasher.inputs[1] <== commitment; // 或其他固定值
// 計算輸出隨機數
component finalHash = Poseidon(2);
finalHash.inputs[0] <== hasher.out;
finalHash.inputs[1] <== challenge;
random <== finalHash.out;
}
5.4 時間假設的安全性
ZK 電路中的時間相關邏輯需要特別注意,因為區塊時間可以被操縱。
避免依賴精確時間:
// 錯誤:過度依賴時間
template TimeDependent() {
signal input blockTime;
signal input deadline;
// 問題:blockTime 可以被礦工/驗證者操縱
blockTime < deadline;
}
// 正確:使用相對時間或條件約束
template RelativeTime() {
signal input startTime;
signal input duration;
signal input currentTime;
// 只驗證時間前進方向正確
currentTime >= startTime;
// 限制最大持續時間
duration <== 30 days; // 固定值,電路外部約束
}
六、實際應用案例:去中心化身份驗證系統
6.1 系統架構
以下是使用 ZK 電路實現的去中心化身份驗證系統的完整架構:
┌─────────────────────────────────────────────────────────────────┐
│ 去中心化身份驗證系統架構 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 用戶端 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 身份資訊 │→ │ 電路輸入 │→ │ 證明生成 │ │
│ │ (私密) │ │ Witness │ │ Proof │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 鏈上驗證 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 接收 Proof │→ │ Verifier │→ │ 驗證結果 │ │
│ │ │ │ 合約 │ │ 寫入狀態 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 驗證的陳述: │
│ 1. 用戶年齡 >= 18 │
│ 2. 用戶國籍 = 某特定國家 │
│ 3. 用戶不在黑名單中 │
│ 4. 用戶的 Commitment 在允許列表中 │
│ │
└─────────────────────────────────────────────────────────────────┘
6.2 完整 Noir 實現
// 身份驗證電路
use dep::std;
// 驗證年齡 >= 最小年齡
fn verify_age(birth_date: Field, min_age: Field) {
let current_date = std::env::block_number() as Field; // 簡化表示
let age = (current_date - birth_date) / 365; // 假設每區塊代表一天
assert(age >= min_age);
}
// 驗證國籍
fn verify_nationality(
nationality_code: Field,
allowed_codes: [Field; 5]
) -> bool {
for i in 0..5 {
if nationality_code == allowed_codes[i] {
return true;
}
}
false
}
// 驗證不在黑名單(使用 Merkle 樹)
fn verify_not_blacklisted(
user_hash: Field,
blacklist_root: Field,
proof_path: Field,
proof_siblings: [Field; 20]
) -> bool {
// 假設 blacklist_root 是一個包含所有黑名單用戶的 Merkle 樹根
// 如果 user_hash 在樹中,返回 false
let is_blacklisted = check_membership(
user_hash,
blacklist_root,
proof_path,
proof_siblings
);
// 驗證失敗意味著不在黑名單
!is_blacklisted
}
// Merkle 成員檢查
fn check_membership(
leaf: Field,
root: Field,
path: Field,
siblings: [Field; 20]
) -> bool {
let mut current = leaf;
for i in 0..20 {
let bit = (path >> i) & 1;
let sibling_hash = siblings[i];
if bit == 0 {
current = std::hash::poseidon_hash([current, sibling_hash]);
} else {
current = std::hash::poseidon_hash([sibling_hash, current]);
}
}
current == root
}
// 完整的身份驗證電路
fn main(
// 公開輸入
age_commitment: pub Field,
nationality_commitment: pub Field,
issuer: pub Field,
signature: [u8; 64],
// 私密輸入
birth_date: Field,
age_proof_hash: Field,
nationality_code: Field,
blacklist_proof: [Field; 21]
) -> pub bool {
// 1. 驗證年齡 >= 18
verify_age(birth_date, 18);
// 2. 驗證國籍
let allowed_nationalities = [1, 2, 3, 4, 5]; // 假設的允許國家代碼
assert(verify_nationality(nationality_code, allowed_nationalities));
// 3. 驗證不在黑名單
let blacklist_root = blacklist_proof[0];
let proof_path = blacklist_proof[1];
let proof_siblings = [blackslist_proof[i]; 20]; // 提取 siblings
let user_hash = std::hash::pedersen_hash([birth_date, nationality_code]);
assert(verify_not_blacklisted(user_hash, blacklist_root, proof_path, proof_siblings));
// 4. 驗證 issuer 簽章
let signed_data = [age_commitment, nationality_commitment];
assert(std::ecdsa::verify::verify(issuer, signed_data, signature));
// 所有驗證通過
true
}
七、結論與展望
本文提供了零知識證明電路設計與開發的完整指南,涵蓋了 Circom、Noir 和 Halo2 三種主流框架的核心用法。讓我們總結關鍵要點:
技術選擇建議:
| 場景 | 推薦框架 | 原因 |
|---|---|---|
| 快速原型開發 | Circom + snarkjs | 生態成熟、工具完善 |
| 生產級應用 | Noir | Rust 語法、類型安全 |
| 高性能需求 | Halo2 | 完全自訂、性能最優 |
| 需要遞迴證明 | Halo2 | 原生支持遞迴 |
安全最佳實踐:
- 充分測試:使用形式化驗證工具檢查約束完整性
- 代碼審計:邀請專業團隊審計電路設計
- 約束優化:平衡約束數量與安全性
- 版本控制:追蹤電路修改歷史
未來發展方向:
- 硬件加速:ZK 電路的 GPU/ASIC 加速正在快速發展
- 遞迴證明聚合:多個證明聚合為單一證明
- 通用 VM:如 Cairo VM、Linux on RISC Zero
- AI + ZK:利用 AI 生成電路,ZK 驗證 AI 推理
掌握 ZK 電路開發將成為區塊鏈工程師的核心技能之一。隨著這項技術的成熟,我們期待看到更多創新的隱私保護和擴容應用。
參考文獻
- Gennaro, R. et al. (2012). "Quadratic Span Programs and Succinct NIZKs without PCPs." EUROCRYPT 2012.
- Groth, J. (2016). "On the Size of Pairing-based Non-interactive Arguments." EUROCRYPT 2016.
- Gabizon, A. et al. (2019). "PLONK: Permutations over Lagrange-bases for Oecumenical Noninteractive Arguments of Knowledge." ePrint 2019/953.
- Bootle, J. et al. (2020). "Halo: Recursive Proof Composition without a Trusted Setup." IEEE S&P 2020.
- Iden3. (2024). "Circom Documentation." https://docs.circom.io.
- Aztec. (2024). "Noir Documentation." https://noir-lang.org.
- Zcash. (2024). "Halo2 Documentation." https://halo2.dev.
聲明:本網站內容僅供教育與資訊目的,不構成任何投資建議或推薦。在進行任何加密貨幣相關操作前,請自行研究並諮詢專業人士意見。所有投資均有風險,請謹慎評估您的風險承受能力。
數據截止日期:2026年3月25日
相關文章
- 零知識電路開發完整教學:從理論基礎到智能合約整合的開發者路徑 — 本文提供從電路設計基礎到實際智能合約整合的完整開發者路徑。涵蓋:零知識證明的形式化定義與代數電路視角、Circom 開發環境架設與工具鏈配置、常見電路設計模式(比較器、範圍證明、Merkle 樹驗證)、複雜電路設計原則與調試技巧、從電路到 Solidity 驗證合約的完整工作流、信任設置(Powers of Tau)詳解、以及 NOIR 和 Halo2 等新一代工具的入門介紹。提供完整的代碼範例和開發實踐指導。
- 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 等項目的工程實踐分析。
- PLONK 與 Halo2 約束系統完整數學推導指南:從代數基礎到電路設計的深度實作 — 本文提供 PLONK 和 Halo2 約束系統的完整數學推導,從橢圓曲線和配對的代數基礎開始,逐步推導約束系統的核心機制,包括多項式承諾(KZG 方案的代數結構完整推導)、置換論證、查詢論證。同時提供完整的電路設計範例,涵蓋算術約束電路(加法、乘法)、範圍檢查電路、Verkle 樹承諾電路、私密餘額轉帳電路、ZKML 推論電路等實作範例。
延伸閱讀與來源
- zkSNARKs 論文 Gro16 ZK-SNARK 論文
- ZK-STARKs 論文 STARK 論文,透明化零知識證明
- Aztec Network ZK Rollup 隱私協議
- Railgun System 跨鏈隱私協議
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!