Privacy Pool 關聯性證明技術實作:從密碼學原理到 Solidity 程式碼

本文深入分析 Privacy Pool 的 Association Proof(關聯性證明)機制,從密碼學原理到實際的 ZK 電路設計(Circom),再到 Solidity 智慧合約的完整部署程式碼。我們將展示如何用零知識證明技術,在保護用戶隱私的同時提供合規所需的關聯集合證明,實現隱私與監管的平衡。

Privacy Pool 關聯性證明技術實作:從密碼學原理到 Solidity 程式碼

前言

上次寫隱私池的文章,收到不少私訊問我:「那個 association proof 到底是怎麼回事?有沒有實際能跑的程式碼?」

好問題。網上關於 Privacy Pool 的文章,十篇有九篇只會畫個示意圖、講個大概原理,真正動手的沒幾個。今天這篇,我決定把袖子捲起來,直接帶你看密碼學公式、ZK 電路設計、到最後 Solidity 的實際部署。

我假設你已經知道什麼是零知識證明、什麼是 Merkle Tree,不然這篇可能會有點吃力。如果你是新手,先去翻我之前寫的「零知識證明入門」再回來。


什麼是 Association Proof?

先說個場景

你是一個基金的經理人,想把一大筆 ETH 從冷錢包轉到交易所。但是區塊鏈是公開的——如果你直接轉,外界馬上就知道你「要出貨了」。

傳統隱私池的做法是:把 ETH 存進隱私池,攪一攪,再提出來。這樣外面的人只知道「有人存了錢、有人提了錢」,但不知道是不是你。

問題來了:監管機構這時候跳出來說「我要知道你沒有洗錢」。你怎麼證明?

Association Proof 就是用來解決這個問題的。你可以證明:「我提出來的這筆錢,來源是某個合法的存款集合(比如某幾間交易所的提款),但我不需要透露具體是哪一筆。」

這個概念是 2022 年由 Vitalik 和幾位研究者提出來的,後來成為 Privacy Pool 設計的標準範式。


密碼學原理:ZK-SNARK 電路設計

承諾與廢棄值

每筆存款進隱私池的時候,系統會做兩件事:

  1. 生成一個承諾(Commitment):把存款金額和一個秘密隨機數丟進 hash 函數,產生一個「指紋」存進 Merkle Tree
  2. 記錄一個廢棄值(Nullifier):把同一個秘密隨機數做 hash,未來提款的時候用來證明這筆存款已經用過了
// 存款時
const secret = randomBytes(31);  // 31 bytes 的隨機數
const nullifier = pedersenHash(secret);  // 廢棄值 = hash(secret)
const commitment = keccak256(secret, amount);  // 承諾 = hash(secret, amount)

// 承諾存入 Merkle Tree,廢棄值存進廢棄集合

提款證明要驗證什麼?

當你要從隱�私池提款的時候,你需要向合約證明(但不透露):

  1. 你知道某個存在於 Merkle Tree 中的承諾背後的秘密
  2. 這個承諾對應的廢棄值沒有被用過
  3. 這個承諾屬於某個「合法的關聯集合」

第三點就是 Association Proof 的核心。


電路設計:用 Circom 寫 ZK 電路

電路的輸入

// Withdraw.circom
pragma circom 2.0.0;

template Main() {
    // 公開輸入
    signal input root;                    // Merkle Tree 的根
    signal input nullifierHash;           // 廢棄值的 hash(公開)
    signal input recipient;               // 接收者地址
    signal input associationRoot;          // 關聯集合的 Merkle 根

    // 私人輸入(只有 prover 知道)
    signal input nullifier;                // 真正的廢棄值
    signal input secret;                    // 存款的秘密
    signal input amount;                   // 存款金額
    signal input pathElements[< = 20];    // Merkle  proof 路徑
    signal input pathIndices[< = 20];      // Merkle proof 索引
    signal input associationPathElements[< = 20];  // 關聯集合的路徑
    signal input associationPathIndices[< = 20];  // 關聯集合的索引

    // 步驟一:驗證承諾的有效性
    component commitmentHasher = Poseidon(3);
    commitmentHasher.inputs[0] <== nullifier;
    commitmentHasher.inputs[1] <== amount;
    commitmentHasher.inputs[2] <== 0;  // padding

    // 步驟二:驗證廢棄值匹配
    component nullifierHasher = Poseidon(1);
    nullifierHasher.inputs[0] <== nullifier;
    nullifierHasher.in === nullifierHash;  // 公開的 nullifierHash 必須匹配

    // 步驟三:驗證 Merkle 證明
    component merkleVerifier = MerkleTreeVerifier(20);
    merkleVerifier.leaf <== commitmentHasher.out;
    merkleVerifier.root <== root;
    merkleVerifier.pathElements <== pathElements;
    merkleVerifier.pathIndices <== pathIndices;
    merkleVerifier.out === 1;

    // 步驟四:驗證關聯集合成員資格
    // 我們需要證明:commitment 是某個合規存款集合的成員
    // 這個合規集合可以由交易所/銀行預先構建
    component associationVerifier = MerkleTreeVerifier(20);
    associationVerifier.leaf <== commitmentHasher.out;
    associationVerifier.root <== associationRoot;
    associationVerifier.pathElements <== associationPathElements;
    associationVerifier.pathIndices <== associationPathIndices;
    associationVerifier.out === 1;
}

component main {public [root, nullifierHash, recipient, associationRoot]} = Main();

Merkle Tree 驗證器

// MerkleTreeVerifier.circom
pragma circom 2.0.0;

template MerkleTreeVerifier(levels) {
    signal input leaf;
    signal input root;
    signal input pathElements[levels];
    signal input pathIndices[levels];
    signal output out;

    signal hash[levels + 1];
    hash[0] <== leaf;

    for (var i = 0; i < levels; i++) {
        // 根據路徑索引決定左右順序
        // pathIndices[i] = 0 表示葉子在左側
        // pathIndices[i] = 1 表示葉子在右側
        component hashFunc = Poseidon(2);
        
        // 左側 = 當前 hash,右側 = 路徑元素
        hashFunc.inputs[0] <== pathIndices[i] == 0 ? hash[i] : pathElements[i];
        hashFunc.inputs[1] <== pathIndices[i] == 0 ? pathElements[i] : hash[i];
        
        hash[i + 1] <== hashFunc.out;
    }

    // 最後的 hash 必須等於根
    hash[levels] === root;
    out <== 1;
}

Solidity 智慧合約實作

合約架構

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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@aztec/contracts/interfaces/IVerifier.sol";

contract PrivacyPoolWithAssociation is Ownable {
    
    // ============================================
    // 資料結構
    // ============================================
    
    // Merkle Tree 相關
    uint256 public constant TREE_DEPTH = 20;
    uint256 public constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
    
    // 存款記錄
    struct Deposit {
        bytes32 commitment;
        uint32 timestamp;
        bool isValid;
    }
    
    // ============================================
    // 狀態變數
    // ============================================
    
    // Merkle Tree 的根
    bytes32 public currentRoot;
    
    // 記錄已使用的廢棄值(防止雙花)
    mapping(bytes32 => bool) public nullifierUsed;
    
    // 記錄所有有效的根(允許歷史根也能提款)
    mapping(bytes32 => bool) public validRoots;
    
    // 關聯集合的根(合規存款池)
    mapping(bytes32 => bool) public validAssociationRoots;
    
    // 存款記錄
    mapping(bytes32 => Deposit) public deposits;
    
    // ZK 驗證器介面
    IVerifier public verifier;
    
    // ============================================
    // 事件
    // ============================================
    
    event DepositMade(
        address indexed depositor,
        bytes32 indexed commitment,
        uint256 leafIndex,
        uint32 timestamp
    );
    
    event WithdrawalMade(
        address indexed recipient,
        bytes32 indexed nullifierHash,
        bool associationProofProvided,
        uint32 timestamp
    );
    
    // ============================================
    // 初始化
    // ============================================
    
    constructor(address _verifier) {
        verifier = IVerifier(_verifier);
        currentRoot = bytes32(0);
        validRoots[currentRoot] = true;
    }
    
    // ============================================
    // 存款功能
    // ============================================
    
    /**
     * @notice 存款函數
     * @param _commitment 存款承諾 = hash(secret + amount)
     * 
     * 存款時,用戶將 ETH 存入合約,並提供一個承諾。
     * 承諾會作為葉節點加入 Merkle Tree。
     */
    function deposit(bytes32 _commitment) external payable {
        require(msg.value > 0, "Must send ETH");
        require(!deposits[_commitment].isValid, "Commitment already exists");
        
        // 將承諾添加到 Merkle Tree
        uint256 leafIndex = _insertIntoTree(_commitment);
        
        // 記錄存款
        deposits[_commitment] = Deposit({
            commitment: _commitment,
            timestamp: uint32(block.timestamp),
            isValid: true
        });
        
        emit DepositMade(
            msg.sender,
            _commitment,
            leafIndex,
            uint32(block.timestamp)
        );
    }
    
    // ============================================
    // 普通提款(不提供關聯證明)
    // ============================================
    
    /**
     * @notice 普通提款(沒有關聯證明)
     * @param _proof ZK-SNARK 證明
     * @param _root Merkle Tree 根
     * @param _nullifierHash 廢棄值的 hash
     * @param _recipient 接收者地址
     * @param _relayer 中繼者地址(幫忙墊 Gas 的第三方)
     * @param _fee 支付給中繼者的費用
     */
    function withdraw(
        bytes calldata _proof,
        bytes32 _root,
        bytes32 _nullifierHash,
        address payable _recipient,
        address payable _relayer,
        uint256 _fee
    ) external {
        // 驗證 ZK 證明
        _verifyProof(_proof, _root, _nullifierHash, _recipient, _relayer, _fee, bytes32(0));
        
        // 檢查廢棄值未使用
        require(!nullifierUsed[_nullifierHash], "Nullifier already used");
        nullifierUsed[_nullifierHash] = true;
        
        // 轉帳(扣除中繼費用)
        uint256 amount = msg.value;
        require(amount > _fee, "Insufficient amount for fee");
        
        _recipient.transfer(amount - _fee);
        if (_fee > 0) {
            _relayer.transfer(_fee);
        }
        
        emit WithdrawalMade(_recipient, _nullifierHash, false, uint32(block.timestamp));
    }
    
    // ============================================
    // 關聯證明提款(提供合規存款池證明)
    // ============================================
    
    /**
     * @notice 帶有關聯證明的提款
     * @param _proof ZK-SNARK 證明(包含關聯集合證明)
     * @param _root Merkle Tree 根
     * @param _nullifierHash 廢棄值的 hash
     * @param _recipient 接收者地址
     * @param _relayer 中繼者地址
     * @param _fee 支付給中繼者的費用
     * @param _associationRoot 關聯集合的 Merkle 根
     */
    function withdrawWithAssociationProof(
        bytes calldata _proof,
        bytes32 _root,
        bytes32 _nullifierHash,
        address payable _recipient,
        address payable _relayer,
        uint256 _fee,
        bytes32 _associationRoot
    ) external {
        // 驗證關聯集合根是否有效
        require(
            validAssociationRoots[_associationRoot],
            "Invalid association root"
        );
        
        // 驗證 ZK 證明
        _verifyProof(_proof, _root, _nullifierHash, _recipient, _relayer, _fee, _associationRoot);
        
        // 檢查廢棄值未使用
        require(!nullifierUsed[_nullifierHash], "Nullifier already used");
        nullifierUsed[_nullifierHash] = true;
        
        // 轉帳
        uint256 amount = msg.value;
        require(amount > _fee, "Insufficient amount for fee");
        
        _recipient.transfer(amount - _fee);
        if (_fee > 0) {
            _relayer.transfer(_fee);
        }
        
        emit WithdrawalMade(_recipient, _nullifierHash, true, uint32(block.timestamp));
    }
    
    // ============================================
    // 內部函數
    // ============================================
    
    function _verifyProof(
        bytes calldata _proof,
        bytes32 _root,
        bytes32 _nullifierHash,
        address _recipient,
        address _relayer,
        uint256 _fee,
        bytes32 _associationRoot
    ) internal view {
        // 驗證根是否有效(允許從歷史狀態提款)
        require(validRoots[_root], "Invalid Merkle root");
        
        // 準備驗證輸入
        uint256[8] memory inputs = [
            uint256(_root),
            uint256(_nullifierHash),
            uint256(uint160(_recipient)),
            uint256(uint160(_relayer)),
            _fee,
            uint256(_associationRoot),
            0,  // padding
            0   // padding
        ];
        
        // 調用 ZK 驗證器
        require(
            verifier.verify(_proof, inputs),
            "Invalid ZK proof"
        );
    }
    
    uint256 private _nextLeafIndex;
    
    function _insertIntoTree(bytes32 _commitment) internal returns (uint256) {
        uint256 index = _nextLeafIndex;
        _nextLeafIndex++;
        
        // 簡化版本:實際需要完整的 Merkle Tree 實現
        // 這裡我們只是更新根(實際產品需要完整實現)
        currentRoot = keccak256(abi.encodePacked(currentRoot, _commitment));
        validRoots[currentRoot] = true;
        
        return index;
    }
    
    // ============================================
    // 管理員函數
    // ============================================
    
    /**
     * @notice 添加合規的關聯集合根
     * @dev 這個根代表一組「被認可的存款來源」
     * 例如:某幾間交易所的存款可以被視為合規
     */
    function addAssociationRoot(bytes32 _root) external onlyOwner {
        validAssociationRoots[_root] = true;
    }
    
    /**
     * @notice 移除合規的關聯集合根
     */
    function removeAssociationRoot(bytes32 _root) external onlyOwner {
        validAssociationRoots[_root] = false;
    }
    
    /**
     * @notice 緊急暫停
     */
    bool public paused;
    
    function pause() external onlyOwner {
        paused = true;
    }
    
    function unpause() external onlyOwner {
        paused = false;
    }
    
    modifier whenNotPaused() {
        require(!paused, "Contract is paused");
        _;
    }
}

關聯集合的構建方法

什麼是關聯集合?

關聯集合(Association Set)就是一堆「被認可為合法的存款」的集合。你可以把它想像成一個「白名單」。

舉例來說:

用戶提款時,可以選擇證明自己的存款來自這個合規集合,但又不需要透露具體是哪一筆。

怎麼構建關聯集合?

// 假設我們有一組合規的存款承諾
const compliantCommitments = [
    commitment1,  // 來自 Coinbase 的存款
    commitment2,  // 來自 Coinbase 的存款
    commitment3,  // 來自 Binance 的存款
    // ...
];

// 將這些承諾插入 Merkle Tree
const associationTree = new MerkleTree(TREE_DEPTH);
compliantCommitments.forEach(commitment => {
    associationTree.insert(commitment);
});

// 計算並發布根
const associationRoot = associationTree.getRoot();

// 將根提交到 Privacy Pool 合約
await privacyPool.addAssociationRoot(associationRoot);

實務考量

安全性注意事項

  1. 廢棄值必須妥善保管:廢棄值(secret)一旦洩漏,他人可以偽造你的提款證明
  2. 隨機數必須夠強:使用夠長的隨機數(至少 31 bytes),並且使用密碼學安全的隨機數生成器
  3. ZKP 電路必須經過審計:ZK 電路的漏洞可能導致資金被盜

隱私與合規的平衡

Association Proof 提供了一種優雅的平衡方式:

這兩種模式可以在同一個系統中共存,用戶根據需要選擇。

Gas 成本考量

ZK 驗證在鏈上的成本相當高。以目前以太坊主網為例:

所以實務上,很多人會選擇在 Layer 2(如zkSync、StarkNet)上部署 Privacy Pool,以大幅降低成本。


結論

Association Proof 是 Privacy Pool 設計中一個很聰明的概念。它不是在「隱私」和「合規」之間二選一,而是提供了一個連續的光譜——你可以選擇不同程度的隱私,同時滿足監管要求。

我個人認為這種設計會是未來合規隱私技術的主流方向。完全封閉的隱私系統遲早會被監管機構盯上,而完全透明的系統又喪失了區塊鏈最大的價值。Association Proof 在兩者之間找到了一個不錯的平衡點。

如果你對這個主題有興趣,建議你去讀讀 Vitalik 的原始文章 "Privacy Pools as a means to balance privacy and regulatory compliance",網上可以找到。

有任何問題,歡迎留言討論。


參考資料

本網站內容僅供教育與資訊目的,不構成任何技術建議或投資建議。在部署任何隱私相關合約前,請進行完整的安全審計並諮詢專業人士意見。

資料截止日期:2026年3月

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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