零知識證明在以太坊智能合約的實際應用:從 zkSNARK 到 zkSTARK 的落地指南

本文深入探討零知識證明在以太坊智能合約中的實際應用,提供完整的程式碼範例和工具鏈教學。涵蓋 Circom 電路設計、snarkjs 證明生成、Solidity 驗證合約撰寫、Tornado Cash 隱私機制分析、zkSync 和 Starknet 的 zkEVM 實作,以及新興的 zkML 應用場景。適合希望將 ZK 技術實際應用於專案的開發者。

零知識證明在以太坊智能合約的實際應用:從 zkSNARK 到 zkSTARK 的落地指南

老實說,每次看到「零知識證明」這四個字,我都有一種又愛又恨的感覺。愛是因為它真的超級優雅,用數學保證了隱私和可驗證性;恨是因為網上大部分的教學都在跟你講什麼「驗證者從來不會知道證明者的秘密」——然後給你一個童話故事版本的比喻,看完還是不會寫 code。

今天這篇文章,我不跟你談童話故事。我要直接打開 code editor,給你看真正的零知識證明在以太坊智能合約裡是怎麼運作的。從最基礎的zk-SNARK 驗證合約,到實際跑在主網上的應用——我們一個一個拆開來看。

先搞清楚:我們到底要證明什麼?

零知識證明的基本邏輯很簡單:你想要證明你知道一個秘密,但又不把秘密暴露出來。但在區塊鏈的世界裡,這個「秘密」的形式可以很多樣:

範例一:知道一個 Hash 的 Preimage

我知道一個值 x,使得 SHA256(x) = 某個已知的 hash
我想要在不透露 x 的情況下,證明我知道 x

範例二:滿足某個計算電路

我執行了某個計算,輸入是 x,輸出是 y
我想要證明這個計算是正確執行的,但不想透露 x

範例三:某個條件成立

我的銀行帳戶餘額 > 100萬
我想要證明這件事,但不透露具體數字

區塊鏈上的 ZK 應用基本都是在處理這三類問題。現在讓我們來看看怎麼用代碼實現它們。

工具生態:你要選哪套工具鍊?

在開始寫 code 之前,你得先選對武器。目前主流的 ZK 工具鍊有幾個選擇:

工具鍊證明系統語言特點知名項目
Circom + SnarkJSGroth16 / PLONKCircom成熟、社群大Tornado Cash
NoirPLONKishRust-like語法簡潔Aztec
Cairo + StarknetSTARKCairo無信任假設、量子抗性Starknet
ZoKratesGroth16DSL易上手、教學友好-

我個人比較推薦從 Circom + SnarkJS 開始,為什麼?因為文件最完整、社群最大,而且目前主網上跑的大多數項目都是這個技術棧。搞懂這套,其他的基本上都能觸類旁通。

實作一:用 Circom 做 Range Proof

Range Proof 是零知識證明最常見的應用場景之一。場景是這樣的:我想證明我的年齡在某個範圍內(例如 18-65 歲可以投票),但不透露具體數字。

第一步:設計電路(circuit)

Circom 的電路用一種 DSL 來描述,我習慣叫它「約束描述語言」。來看一個簡化的 range proof 電路:

pragma circom 2.0.0;

// 這個電路證明:a <= signal < b
// 但不透露 signal 的具體值

template RangeProof(bit_len) {
    signal input secret;        // 要證明的值
    signal input lower;         // 下界
    signal input upper;         // 上界
    signal output valid;        // 輸出:1 = 在範圍內
    
    // 計算 secret - lower (必須 >= 0)
    signal diffLower;
    diffLower <== secret - lower;
    
    // 計算 upper - secret (必須 >= 0)  
    signal diffUpper;
    diffUpper <== upper - secret;
    
    // 檢查:secret 在 [lower, upper) 範圍內
    // 我們用乘法約束來實現
    // 如果 diffLower 和 diffUpper 都 >= 0,
    // 那麼它們的乘積會 > 0(大多數情況)
    // 但這只是簡化版本,真實電路更複雜
    
    // 這裡我們用更簡單的方法:轉成二進位
    // 然後驗證範圍
    
    component lowerBits[bit_len];
    component upperBits[bit_len];
    
    var i;
    for (i = 0; i < bit_len; i++) {
        lowerBits[i] = Num2Bits(1);
        upperBits[i] = Num2Bits(1);
        
        // 這個約束確保我們能恢復信號
        // 但實際的 range proof 電路更複雜
    }
    
    // 簡化的約束:確保在範圍內
    // 實際上需要用到範圍證明的專門技術
    valid <== 1;
}

// 主模板
component main {public [lower, upper]} = RangeProof(32);

我知道這段電路超級簡化,實際上真正的 range proof 電路用的是更聰明的技巧,比如「位元分解 + 約束」。但重點是讓你感受一下電路長什麼樣子。

第二步:編譯電路

# 安裝 circom
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
git clone https://github.com/iden3/circom.git
cd circom && cargo build --release

# 下載 snarkjs
npm install -g snarkjs

# 編譯電路
circom range_proof.circom --r1cs --wasm --sym

# 這會產生三個檔案:
# range_proof.r1cs - 約束系統
# range_proof_js/ - WASM 證明者工具
# range_proof.sym - 符號檔案

第三步:設置Trusted Setup

ZK-SNARK 需要一個「信任儀式」(trusted setup)。這是個有點拗口的概念——本質上是你需要生成一堆「有毒廢物」(toxic waste),然後假設所有參與者都誠實地刪除了它。聽起來不太靠譜對吧?但這就是目前主流方案的現實。

# 初始化 powers of tau
snarkjs powersoftau new bn128 12 pot12_final.ptau -v

# 貢獻隨機性(這是信任儀式的第一步)
snarkjs powersoftau contribute pot12_final.ptau pot12_contrib1.ptau --name="First contribution" -v

# 準備相位(phase 2)
snarkjs powersoftau prepare phase2 pot12_contrib1.ptau pot12_final.ptau -v

# 生成 zkey(最終的金鑰)
snarkjs groth16 setup range_proof.r1cs pot12_final.ptau pot12_0000.zkey

# 貢獻 zkey
snarkjs zkey contribute pot12_0000.zkey pot12_0001.zkey --name="Second contribution" -v

# 匯出最終 zkey
snarkjs zkey export verificationkey pot12_0001.zkey verification_key.json

實際上,這個流程在 production 環境裡會更複雜,通常會有多個獨立的貢獻者,每個人都貢獻一段隨機性。Groth16 的 trusted setup 是「通用的」(per circuit),而 PLONK 是「通用的」(universal),不需要每個電路都重做 setup。

第四步:撰寫 Solidity 驗證合約

這是最激動人心的部分!我們要寫一個合約,能驗證 ZK 證明:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "hardhat/console.sol";
import { Groth16Verifier } from "./Groth16Verifier.sol";

contract ZKRangeProof {
    Groth16Verifier public verifier;
    
    // 存儲驗證結果
    mapping(bytes32 => bool) public verifiedProofs;
    
    // 驗證金鑰的 ABI 編碼版本
    // 由 snarkjs 生成的 verifer.sol 提供
    address public verificationKey;
    
    constructor(address _verifierAddress) {
        verifier = Groth16Verifier(_verifierAddress);
    }
    
    /**
     * @notice 驗證一個範圍證明
     * @param proof ZK 證明(a, b, c 三個點)
     * @param input 公開輸入 [lower, upper, hash_of_secret]
     */
    function verifyProof(
        uint256[2] memory a,
        uint256[2][2] memory b,
        uint256[2] memory c,
        uint256[3] memory input
    ) public returns (bool) {
        // 調用生成的驗證合約
        bool valid = verifier.verifyProof(a, b, c, input);
        
        // 如果驗證成功,存儲結果
        if (valid) {
            bytes32 proofHash = keccak256(abi.encodePacked(a, b, c));
            verifiedProofs[proofHash] = true;
        }
        
        return valid;
    }
    
    /**
     * @notice 檢查某個值是否在指定範圍內(不透露值本身)
     * @dev 這是個演示函數,真實應用中前端會做更多處理
     */
    function checkRangeAndDoSomething(
        uint256 secret,
        uint256 lower,
        uint256 upper
    ) public pure returns (bool) {
        return secret >= lower && secret < upper;
    }
}

snarkjs 會幫你生成一個 Groth16Verifier.sol,看起來像這樣:

// 自動生成的 Groth16 驗證合約(簡化版)
contract Groth16Verifier {
    uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
    
    struct Proof {
        uint256[2] a;
        uint256[2][2] b;
        uint256[2] c;
    }
    
    struct Vk {
        uint256[2] alpha;
        uint256[2] beta;
        uint256[2][2] gamma;
        uint256[2][2] delta;
        uint256[2][] IC;  // 線性組合係數
    }
    
    function verifyProof(
        uint256[2] memory a,
        uint256[2][2] memory b,
        uint256[2] memory c,
        uint256[3] memory input
    ) public view returns (bool) {
        // 配對檢查
        // 這是 zkSNARK 驗證的核心數學
        // 需要驗證 e(A, B) = e(alpha, beta) * e(C, delta) * product(e(IC[i], gamma))
        
        uint256[2] memory temp;
        
        // 計算配對
        // ...
        
        // 如果所有檢查通過,返回 true
        return true;
    }
}

gas 消耗方面,Groth16 驗證合約大約需要 300,000-500,000 gas。這在 L2 上是可以接受的,但在 L1 主網上就比較貴了。這就是為什麼很多項目開始轉向 PLONK 或 STARK——它們的驗證成本更低,或者說驗證者不需要信任假設。

實作二:zkSNARK 在 DeFi 的實際應用——Tornado Cash

Tornado Cash 是 ZK 在隱私領域最著名的應用了。它的原理說穿了很簡單:

  1. 存款:你把 ETH 發到一個智能合約,同時提交一個「承諾」(commitment)——這是 hash(secret, nullifier) 的值。合約存儲這個 commitment。
  2. 等待:你要等待一段時間,讓別人無法追蹤你存款和提款的時間關聯。
  3. 提款:你出示一個 ZK 證明,證明你知道某個 commitment 對應的 secret,但又不透露是哪個。

來看 Tornado Cash 的簡化版電路:

pragma circom 2.0.0;

include "../circomlib/poseidon.circom";

// 存款電路:計算 commitment
template DepositCircuit() {
    signal input secret;
    signal input nullifier;
    signal output commitment;
    signal output nullifierHash;
    
    component poseidon = Poseidon(2);
    poseidon.inputs[0] <== secret;
    poseidon.inputs[1] <== nullifier;
    
    commitment <== poseidon.out;
    nullifierHash <== poseidon.out;
}

// 提款電路:證明知道 secret 但不透露
template WithdrawalCircuit() {
    signal input secret;
    signal input nullifier;
    signal input root;
    signal input pathElements[20];  // Merkle tree path
    signal input pathIndices[20];  // 0 = left, 1 = right
    
    // 計算 commitment
    component deposit = DepositCircuit();
    deposit.secret <== secret;
    deposit.nullifier <== nullifier;
    
    // 驗證 commitment 在 Merkle tree 中
    component merkleVerifier = MerkleTreeChecker(20);
    merkleVerifier.leaf <== deposit.commitment;
    merkleVerifier.root <== root;
    merkleVerifier.pathElements <== pathElements;
    merkleVerifier.pathIndices <== pathIndices;
    
    // 這個約束確保:
    // 1. 提款者知道某個在樹中的 leaf
    // 2. 提款者知道對應的 nullifier
    // 但驗證者不知道是哪個 leaf!
}

component main {public [root]} = WithdrawalCircuit();

Merkle Tree 的技巧在這裡超級重要。它讓你只需要存儲一個 root(32 bytes),就能代表任意數量的 commitment。任何人都可以驗證某個 commitment 在樹中,但不知道它的位置——因為路徑資訊不會透露整棵樹的結構。

實作三:PLONK 的實際應用——zkSync Era

zkSync Era(現在也叫 ZKsync Era)用的是 PLONK 證明系統,它比 Groth16 有幾個優勢:

  1. 通用 Trusted Setup:只需要一次 setup,就能驗證任意電路(只要不超過設定上限)
  2. 透明度:不需要多個參與者做 trusted setup,可以用「powers of tau」公開儀式

zkSync 的 ZK 電路是用 Zinc(他們自己的 DSL)或 Yul 寫的。來看一個簡化的 transfer 電路概念:

// 簡化的 zkSync transfer 電路概念
// 實際代碼要複雜得多

const FIELD_SIZE: u64 = 21888242871839275222246405745257275088696311157297823662689037894645226208583;

fn transfer_circuit(
    // 公開輸入
    root_before: Field,
    root_after: Field,
    pub_token_id: Field,
    pub_amount: Field,
    
    // 私密輸入(只有證明者知道)
    account_nonce: Field,
    account_balance: Field,
    signature: [u8; 64],  // ECDSA 簽章
    merkle_proof: [Field; 10],
) -> bool {
    // 1. 驗證 Merkle 證明
    let leaf = hash(account_nonce, account_balance, pub_token_id);
    let computed_root = compute_merkle_root(leaf, merkle_proof);
    assert(computed_root == root_before);
    
    // 2. 驗證餘額足夠
    assert(account_balance >= pub_amount);
    
    // 3. 驗證簽章
    let message = hash(root_before, pub_token_id, pub_amount);
    let pubkey = recover_pubkey(message, signature);
    let account_id = hash(pubkey);
    assert(hash(account_id) == leaf);  // 驗證帳戶擁有者
    
    // 4. 計算新狀態
    let new_balance = account_balance - pub_amount;
    let new_leaf = hash(account_nonce + 1, new_balance, pub_token_id);
    
    // 5. 輸出新的 Merkle root
    // 這個會是 root_after
    
    true
}

zkSync 的厲害之處在於它把整個 EVM 執行都搬到了 ZK 電路裡。也就是說,你可以用 Solidity 寫任何合約,zkSync 會生成一個 ZK 電路來證明這個合約的執行是正確的。這個技術叫做「zkEVM」,是個非常複雜的工程——但它讓你不需要學新的 DSL 就能寫隱私應用。

實作四:zkSTARK 在 Starknet 的應用

STARK 是 ZK-SNARK 的「無需信任」版本。它不需要 trusted setup,但代價是證明大小更大、驗證成本更高。

Cairo 是 Starknet 的智慧合約語言。用 Cairo 寫的合約會被編譯成 STARK 證明,然後提交到以太坊主網驗證。

%builtins range_check

from starkware.cairo. Common.cairo_builtins import HashBuiltin

// 簡化的轉帳函數
func transfer{
    syscall_ptr: felt*,
    pedersen_ptr: HashBuiltin*,
    range_check_ptr
}(
    from_account: felt,
    to_account: felt,
    amount: felt
) -> (success: felt) {
    alloc_locals;
    
    // 讀取發送者餘額
    let (from_balance) = balances.read(from_account);
    
    // 確保餘額足夠(這是個約束)
    assert 1 = from_balance - amount;
    
    // 扣除發送者餘額
    balances.write(from_account, from_balance - amount);
    
    // 增加接收者餘額
    let (to_balance) = balances.read(to_account);
    balances.write(to_account, to_balance + amount);
    
    return (success=1);
}

Cairo 的語法有點像 Python + Rust 的混合體。它的厲害之處在於每一行 Cairo 代碼都對應一個 STARK 約束——編譯器會自動處理所有的 ZK 魔法。

混合應用:zkML——讓鏈上 AI 成真

現在有個超酷的新方向:zkML(零知識機器學習)。場景是這樣的:

  1. 你在鏈外訓練了一個 ML 模型
  2. 你想在不透露模型權重的情況下,證明模型對某個輸入的輸出是正確的

這在預言機、遊戲、保險等場景都有應用。

# 用 Python + zkML 庫定義一個簡單的神經網路
import tensorflow as tf

# 定義模型
model = tf.keras.Sequential([
    tf.keras.layers.Dense(10, activation='relu', input_shape=(5,)),
    tf.keras.layers.Dense(1)
])

# 量化模型(ZK 電路只支援整數運算)
quantized_weights = quantize_weights(model.get_weights())

# 編譯成 ZK 電路
# 這部分需要用 circomlib 或其他工具

zkML 的挑戰在於:神經網路充滿了浮點數運算,而 ZK 電路只能處理有限域的整數。你需要:

  1. 量化:把所有浮點數轉成定點整數
  2. 約束:確保量化誤差不會破壞正確性
  3. 優化:減少電路規模,因爲推理可能需要數百萬個約束

常見坑與最佳實踐

坑一:Trusted Setup 的安全假設

Groth16 的 trusted setup 意味著如果所有參與者都是誠實的,你的證明就是安全的。但現實是:

建議:選擇有公開、經過審計的 trusted setup 的項目。

坑二:電路設計失誤

ZK 電路有個特性:如果約束數量不夠,你可能會繞過某些檢查。

// 錯誤示例:忘記加約束
template BadCircuit() {
    signal input secret;
    signal output result;
    
    // 問題:如果不約束 secret,攻擊者可以任意輸入
    result <== 1;  // 永遠輸出 1,但完全沒用到 secret!
}

// 正確做法:確保 secret 被約束
template GoodCircuit() {
    signal input secret;
    signal output result;
    signal mid;
    
    // 約束 secret 必須在某個範圍內(或其他約束)
    secret * 1 === mid;  // 這個約束把 secret 加入電路
    
    result <== mid;
}

建議:找專業的安全公司審計你的電路。Certik、Trail of Bits 都有 ZK 審計服務。

坑三:隨機性不足

ZK 證明需要「高品質的隨機性」來防止攻擊。如果你的隨機數生成有漏洞,攻擊者可能構造假證明。

// 錯誤做法:使用可預測的隨機數
uint256 badRandom = uint256(keccak256(block.timestamp));

// 正確做法:使用多個 entropy 來源
uint256 goodRandom = uint256(keccak256(
    abi.encodePacked(
        block.timestamp,
        msg.sender,
        tx.origin,
        gasleft()
    )
));

坑四:Gas 成本失控

驗證一個 ZK 證明的 gas 成本可能高達幾十萬。這在 L1 上可能還行,但在複雜應用裡就會成為瓶頸。

優化策略

  1. 把驗證搬到 L2(如 zkSync、Starknet)
  2. 使用聚合證明(Bunzz、Polyhedra 等項目在做這個)
  3. 選擇驗證成本更低的證明系統(PLONK、STARK)

結語:零知識的世界才剛開始

寫到這裡,我已經帶你看了 ZK 證明在以太坊上的主要應用場景和實作方式。從簡單的 range proof,到複雜的 zkEVM,每個應用都在挑戰著密碼學和工程的邊界。

但說實在的,2026 年的今天,ZK 技術還遠遠沒有成熟。大部分應用還停留在概念驗證階段,真正跑在主網上的實用案例屈指可數。電路設計工具不夠好用、驗證成本太高、Trusted Setup 的安全假設不夠好——這些都是亟需解決的問題。

好消息是,每天都有新的突破:新的證明系統、新的硬體加速、新的工具鏈。如果你想在這個領域深耕,現在是最好的時機。

下次當你看到「某某 DeFi 協議宣稱使用零知識證明保護隱私」的時候,你可以問問自己:他們用的是哪套工具鏈?trusted setup 是否靠譜?驗證成本是否可控?電路是否經過審計?這才是真正判斷一個 ZK 項目靠不靠譜的方法。


延伸閱讀


免責聲明:本網站內容僅供教育與資訊目的。零知識證明技術涉及複雜的密碼學原理,實際應用需要專業的安全審計。在部署任何基於 ZK 的智能合約前,請諮詢合格的密碼學家和安全專家。

數據截止日期:2026-03-27

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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