以太坊零知識證明完整實作指南:從密碼學基礎到 zk-SNARKs/STARKs 智能合約部署

零知識證明(Zero-Knowledge Proof,ZKP)是現代密碼學中最具革命性的技術之一,其核心特性——在不透露任何額外資訊的情況下證明陳述的正確性——為區塊鏈隱私保護和可擴展性帶來了前所未有的可能性。本文從以太坊開發者的視角出發,深入探討零知識證明的密碼學基礎、zk-SNARKs 與 zk-STARKs 的技術差異、主流實作框架(如 Circom、ZoKrates、Groth16、PLONK)的使用方法,以及如何在以太坊上部署零知識證明智能合約。我們將提供完整的程式碼範例,涵蓋從電路設計、證明生成到鏈上驗證的整個流程,同時深入分析每個環節的 Gas 消耗、安全考量與最佳實踐。

零知識證明以太坊實作程式碼完整指南:從密碼學基礎到 zk-SNARK/STARK 實戰


文章 metadata

欄位內容
difficultyadvanced
date2026-03-28
categoryprivacy
tagszero-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)PQ,你沒辦法反推 ab,但你可以驗證 e(aP, bQ) = e(P, Q)^ab 這個等式。

zk-SNARK 的驗證就是靠這個性質:證明者需要展示某些橢圓曲線點之間的關係,而驗證者透過配對檢查這些關係是否正確。

2.3 密碼學雜湊函數

雜湊函數在 ZK 中扮演多重角色:

  1. Fiat-Shamir 轉換:用來生成 pseudo-random 挑戰
  2. Merkle 樹:用於批次資料的承諾
  3. 承諾方案:用來「鎖住」一個值而不透露它

密碼學安全的雜湊函數需要滿足:

三、zk-SNARK 詳解:現在最流行的 ZK 系統

3.1 SNARK 是什麼

SNARK = Succinct Non-interactive ARguments of Knowledge

翻譯成人話就是:簡潔的、非互動式的知識論證

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 的核心思想是:

  1. 把多項式 f(x) 在多個點上的取值編碼成一個 Reed-Solomon 碼字
  2. 透過「折半」遊戲,用對數級別的互動次數來「壓縮」這個碼字
  3. 最終驗證者只需要檢查最終的「 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 有:

目前有兩種路線:

電路導向(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 證明來實現:

# 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,你可以:

  1. 發證機構頒發一個「年齡承諾」
  2. 你生成 ZK 證明:「這個承諾代表的年齡 >= 18」
  3. 驗證者只看到「通過/不通過」,不知道任何其他資訊

這就是 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 常見錯誤與避坑指南

  1. 不要假設所有操作都是免費的

在電路中,每個乘法都是一個約束。除法和取模尤其昂貴。

  1. 注意信號的範圍

預設情況下,Signal 可以是任意大小的數字。如果你想表達「這是一個位元」,必須明確約束它。

  1. 調試比普通代碼難 100 倍

ZK 電路的錯誤訊息通常非常神祕。建議先用小電路測試,逐步擴展。

  1. 信任設置的「有毒廢料」

如果你參與了信任設置,千萬不要在任何地方留下你的 toxic waste 隨機種子。

八、未來展望:ZK 的下一步

8.1 ZK-EVM 的成熟

2026 年,我們會看到第一批真正「生產就緒」的 zkEVM。它們將實現:

8.2 ZKML:ZK + 機器學習

ZK 和 ML 的結合是另一個激動人心的方向:Proof of ML Inference

應用場景:

# 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 系統,包括:

結語:動手比看懂更重要

零知識證明的世界很大,這篇文章只能帶你走到門口。真正的理解需要動手實作——寫電路、生成證明、部署合約、處理失敗。

我建議你從頭開始:

  1. 安裝 snarkjs 和 circom
  2. 編譯第一個「Hello World」電路
  3. 生成第一個證明
  4. 在本地測試網部署驗證合約
  5. 解決過程中的所有「為什麼失敗了」問題

這個過程會比任何文章都更深刻地告訴你 ZK 到底是什麼。祝福你在零知識的宇宙裡玩得開心!


參考資源

COMMIT: Expand ZK proof article with complete technical depth and practical code examples

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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