Skip to content
On this page

L4-2: Privacy Mixer(隐私混币器)

1. 问题

以太坊交易默认是完全透明的——每笔转账的发送者、接收者和金额都永久记录在公开的账本上。虽然这种透明度对审计和安全有好处,但它严重侵犯了用户的财务隐私。任何人都可以看到你的钱包余额、交易历史和交互的 DeFi 协议。这不仅是隐私问题,也是安全问题——持有大量加密资产的人可能成为攻击目标。

隐私混币器的目标是在不损害区块链去中心化和可验证性的前提下,打破存款和取款之间的链上关联。Tornado Cash 是最著名的实现:用户存入固定面额的 ETH(如 0.1 ETH),获得一个秘密凭证;之后任何人(包括通过中继者)都可以用这个凭证提取 0.1 ETH 到一个新地址,而链上观察者无法将存款交易和取款交易关联起来。

这带来了极其复杂的密码学挑战:如何证明你知道某个存款的凭证,而不暴露是哪一个存款?答案是零知识证明(ZKP)——具体来说,通过 Merkle Tree 承诺所有存款,然后用 ZK 电路证明你知道某个叶子节点的 preimage(nullifier + secret),同时不透露叶子的位置。

2. 原因

隐私保护是以太坊生态最重要但未解决的问题之一。Tornado Cash 证明了技术可行性(在制裁前处理了超过 70 亿美元的交易量),但也引发了关于隐私工具合法性的激烈争论。从技术角度看,隐私混币器是将零知识证明从理论引入实践的完美案例——它证明了 ZKP 可以在智能合约中以合理的 Gas 成本(约 500K gas)验证复杂计算。

理解隐私混币器对开发者有多重价值:

  • 密码学实践:Merkle Tree + 承诺方案 + 零知识证明的端到端实现
  • ZKP 工程:理解 Circom 电路编写、Groth16 证明生成和链上验证的完整工作流
  • 隐私设计模式:固定面额、匿名集、nullifier 防双花——这些模式适用于所有链上隐私系统
  • 监管与哲学:理解隐私工具的技术必要性、合法用途和监管挑战

虽然 Tornado Cash 面临法律挑战,但其底层技术(ZK 证明 + Merkle 承诺)已被广泛应用于合法的隐私项目——如 zkSync 的隐私交易、Railgun(基于合规的隐私协议)、以及正在兴起的 Privacy Pool(通过 ZK 证明排除非法资金来源的混币器)。

3. 方案

PrivacyMixer.sol 实现了固定面额(0.1 ETH)的隐私混币器。虽然它使用了直接密钥揭示(而非完整 ZK 证明)作为教育简化,但其架构完整保留了 Tornado Cash 的核心设计:

3.1 核心密码学原语

承诺方案(Commitment Scheme)

  • 用户生成两个随机 32 字节值:secretnullifier
  • 计算承诺:commitment = keccak256(abi.encodePacked(secret, nullifier))
  • 存款时提交 commitment 到链上,存入 Merkle Tree
  • 秘密凭证(secretnullifier)保存在链下,由用户保管

Merkle Tree

  • 高度为 20 的二叉树,支持最多 2^20 ≈ 1,048,576 笔存款
  • 使用增量插入算法(Incremental Merkle Tree):
    • 每个层级 i 维护一个"正在填充的"子树 filledSubtrees[i]
    • 偶数索引:当前叶子是该层子树的左半部分,右半部分用预计算的 zeros[i] 填充
    • 奇数索引:当前叶子是该层子树的右半部分,与之前存储的 filledSubtrees[i] 配对哈希
    • 每次插入更新整个路径上的哈希直到 root
  • 零值预计算:zeros[0] = keccak256(bytes32(0))zeros[i] = keccak256(zeros[i-1], zeros[i-1])

Nullifier 防双花

  • nullifierHash = keccak256(abi.encodePacked(nullifier))
  • 每次取款时公开 nullifierHash 并标记为已使用
  • 同一 nullifier 不能用于两次取款——防止同一存款被多次提取

3.2 存款流程

  1. 用户链下生成 secretnullifier(各 32 随机字节)
  2. 计算 commitment = keccak256(secret, nullifier)
  3. 调用 deposit(commitment) 并发送恰好 0.1 ETH
  4. 合约将 commitment 插入 Merkle Tree
  5. 存储 commitments[commitment] = leafIndex + 1(1-indexed,0 表示未使用)
  6. 记录新的 Merkle root:roots[depositCount] = currentRoot
  7. 用户保存 (secret, nullifier, leafIndex) 作为取款凭证

3.3 取款流程

  1. 用户提供 (secret, nullifier, recipient, relayer, fee)
  2. 合约重构 commitment = keccak256(secret, nullifier) — 验证它已被存入
  3. 计算 nullifierHash = keccak256(nullifier) — 验证未被使用
  4. 验证 fee < DENOMINATION(中继费不能超过面额)
  5. 标记 nullifierSpent[nullifierHash] = true(在转账前标记,遵循 CEI 模式)
  6. 转账:recipient 获得 DENOMINATION - feerelayer 获得 fee
  7. 中继者机制使得取款者(recipient)可以不同于交易发起者——进一步增强隐私

3.4 ZK 验证器接口(IVerifier)

合约中预留了 IVerifier 接口,定义了 verifyProof() 方法。在完整的 Tornado Cash 实现中:

  • 取款操作不直接提交 secretnullifier
  • 而是提交一个 Groth16 ZK 证明,证明"我知道某个 commitment 等于 MiMC(nullifier, secret) 且在 Merkle Tree 中,同时 nullifier 哈希为公开值"
  • Verifier 合约验证证明、Merkle root 和 nullifierHash

3.5 增量 Merkle Tree 算法详解

_insertLeaf() 函数实现了高效的增量插入:

function _insertLeaf(leaf):
    index = depositCount
    currentHash = leaf
    
    for i in 0..MAX_DEPTH-1:
        if index % 2 == 0:  // 偶数索引——当前叶子在左侧
            filledSubtrees[i] = currentHash
            currentHash = hash(currentHash, zeros[i])
        else:                 // 奇数索引——当前叶子在右侧
            currentHash = hash(filledSubtrees[i], currentHash)
        index = index / 2
    
    currentRoot = currentHash

这个算法的关键是:它只需要每层存储一个"正在填充"的子树值,而不是整个树的所有节点。对于高度 20 的树,存储复杂度从 O(2^h) 降低到 O(h)。这使得合约可以在链上高效地维护百万级别的 Merkle Tree。

参考源文件:src/level4/PrivacyMixer.sol

3.6 完整的 Tornado Cash ZK 电路(概念层)

在实际的 Tornado Cash 中,取款不直接暴露 secretnullifier,而是通过 Circom ZK 电路生成证明:

text
template Mixer(levels) {
    // 私有输入(不暴露在链上)
    signal input nullifier;
    signal input secret;
    signal input pathElements[levels];   // Merkle proof
    signal input pathIndices[levels];    // 0=左, 1=右

    // 公开输入(暴露在链上,合约验证)
    signal input root;          // Merkle root
    signal input nullifierHash; // 防双花
    signal input recipient;     // 收款地址

    // Commitment = MiMC(nullifier, secret)
    component hasher = MiMC7(2, 91);
    hasher.in[0] <== nullifier;
    hasher.in[1] <== secret;

    // 验证 commitment 在 Merkle Tree 中
    signal leaf = hasher.out;
    for (var i = 0; i < levels; i++) {
        // 逐层哈希到 root
        hasher[i] = MiMC7(2, 91);
        // 根据 pathIndices 决定左右
        hasher[i].in[0] <== pathIndices[i] == 0 ? leaf : pathElements[i];
        hasher[i].in[1] <== pathIndices[i] == 1 ? leaf : pathElements[i];
        leaf <== hasher[i].out;
    }
    root === leaf;  // 最终哈希必须匹配公开输入的 root

    // nullifierHash = MiMC(nullifier)
    component nullifierHasher = MiMC7(1, 91);
    nullifierHasher.in[0] <== nullifier;
    nullifierHash === nullifierHasher.out;
}

为什么使用 MiMC 而不是 keccak256:keccak256 在 ZK 电路中的执行成本极高(需要大量约束/门电路)。MiMC(Minimal Multiplicative Complexity)是专门为 ZK 友好而设计的哈希函数——它在有限域中的实现只需要很少的乘法操作(乘法是 ZK 电路中最昂贵的约束)。Tornado Cash 电路使用 MiMC7(7 轮),每轮在 BN254 曲线上仅需约 250 个约束,而 keccak256 需要数十万个约束。

4. 遭遇的陷阱

  • 零知识证明生成的计算成本:在浏览器中生成 Groth16 证明需要 10-30 秒,对于小额取款来说用户体验很差
  • 匿名集过小:如果只有少数人使用混币器(例如仅 10 笔存款),存款和取款之间的关联很容易被统计分析推断
  • 固定面额 vs. 灵活面额:固定面额(所有存款等额)增强匿名性,但用户体验差(需要精确金额);灵活面额方便但破坏匿名集(不同金额可用于关联)
  • nullifier 丢失的风险:如果用户保存的 (secret, nullifier) 丢失,存款永久锁定在合约中——没有恢复机制
  • 增量 Merkle Tree 的存储管理roots 映射存储每个插入后的 root,取款需要检查 root 是否已知——遍历所有历史 root 的 Gas 成本随存款数量线性增长
  • MiMC 与 keccak256 的 ZK 差异PrivacyMixer.sol 使用 keccak256 作为哈希函数(教育简化),但真实 ZK 电路使用 MiMC——如果合约和电路使用不同的哈希函数,验证将失败
  • 前端隐私泄漏:即使用户使用混币器,如果他们的浏览器暴露了 IP 地址、使用了相同的 RPC 节点、或在社交媒体上透露了交易信息,隐私仍然会被破坏
  • Groth16 可信设置:Tornado Cash 的 ZK 电路需要 Powers of Tau 仪式生成的 CRS(公共参考字符串)。如果设置过程中存在恶意参与者且未正确销毁有毒废料,就可能伪造证明

5. 陷阱的原因

匿名集过小是混币器最根本的挑战——它是"先有鸡还是先有蛋"的问题。在只有少量存款时,任何人都可以列举所有可能的存款-取款对应关系。例如,如果有 3 笔存款和 3 笔取款,理论上只有 3! = 6 种可能的对应关系,隐私严重受损。随着存款数量增加到数千或数百万,可能的对应关系指数级增长,匿名性大幅增强。

Tornado Cash 通过固定面额(0.1 ETH, 1 ETH, 10 ETH, 100 ETH)来创造不同规模的独立匿名集。用户在同一面额池中的所有存款是无法区分的。但是,这也意味着需要根据不同金额使用不同的池子,降低了每笔金额的匿名集规模。Privacy Pools(Tornado Cash 的继承者,由 Ameen Soleimani 提出)引入了"关联集"的概念——用户可以证明他们的存款不在某个被标记的地址集合中,从而在法律合规要求下保持隐私。

ZK 证明生成的计算成本来自 Groth16 证明系统的特性。证明生成包括三个步骤:(1) witness 计算(在 JavaScript/WASM 中运行电路);(2) 证明生成(多标量乘法和配对计算);(3) 证明序列化为 Solidity calldata 格式。其中步骤 (2) 最耗时。新的证明系统(如 PLONK、Halo2、Nova)正在通过消除可信设置或支持递归证明来改善这个问题,但它们目前尚未完全取代 Groth16 在链上验证中的主导地位。

增量 Merkle Tree 的 root 检查问题:当取款人提交一个 Merkle root 时,合约需要验证这个 root 是否存在于历史 root 中。_isKnownRoot() 遍历 roots[1..depositCount],这个循环的 Gas 成本随存款数量线性增长。对于数百万存款的池子,这会导致 Gas 成本过高。解决方案是维护一个 root → bool 的 mapping,将查询从 O(n) 优化到 O(1)。

6. 如何解决陷阱

匿名集增长:通过标准化面额和跨池交互来最大化池子规模。让多个应用共享同一个混币器合约(如 Tornado Cash Nova 支持的 ETH + ERC-20 多资产池)。

证明生成优化

  • 使用 Web Worker 在后台线程生成证明,不阻塞 UI
  • 预计算 witness(在用户交互前就开始计算)
  • 探索新的证明系统——如 Leo 的在线证明生成器或 RISC Zero 的 ZK VM
  • 在 L2 上部署混币器,利用更快的区块确认时间和更低的 Gas 费来降低总体延迟

Root 查询优化

solidity
mapping(bytes32 => bool) public isKnownRoot;

function _insertLeaf(bytes32 leaf) internal {
    // ... 更新 tree ...
    isKnownRoot[currentRoot] = true; // O(1) 标记
}

function _isKnownRoot(bytes32 root) internal view returns (bool) {
    return isKnownRoot[root];
}

注意:这个 mapping 会随存款数量无限增长——2000 万存款 × 32 字节 = 约 640MB 的存储,超过以太坊的可行范围。对于超大规模,可以使用 rollup 方案——将 root 存储在 L2,只在 L1 上验证 ZK 证明。

前端隐私保护

  • 使用 Tor 或 VPN 连接 RPC 节点
  • 使用中继者(relayer)模式——交易由第三方提交,不关联到用户的 IP
  • 定时取款——在存款后等待随机时间(几天到几周)再取款
  • 不将取款直接发送到与存款有交互历史的地址
  • 使用浏览器隐私模式、清除 cookie、禁用追踪

可信设置风险缓解

  • Powers of Tau 仪式的安全性取决于至少一个参与者诚实销毁了他们的随机数据
  • 使用社区广泛验证的设置(如 Ethereum Foundation 的 Perpetual Powers of Tau)
  • 考虑使用透明设置(Transparent Setup)或无设置(Transparent)的证明系统如 STARK

7. 技术要点

技术点说明
承诺方案commitment = hash(secret, nullifier),存款时不暴露密钥
Merkle Tree高度 20,支持 1M+ 存款,增量插入算法
Nullifier 防双花nullifierHash 标记已使用,同一密钥只能取款一次
固定面额所有存款 0.1 ETH,增强匿名集统一性
中继者模式取款发起者 != 收款者,打破交易图关联
CEI 模式标记 nullifier 在转账之前,防止重入
IVerifier 接口ZK 证明验证的抽象层,可对接不同证明系统
Groth16Tornado Cash 使用的证明系统,证明小(~128 字节),验证快
MiMC 哈希ZK 友好的哈希函数,低乘法复杂度
CircomZK 电路编写 DSL,编译为 R1CS 后生成证明
增量 Merkle TreeO(h) 存储,O(h) 插入,无需存储整棵树
零值预计算zeros[i] = hash(zeros[i-1], zeros[i-1]),替换空子树
匿名集池中存款越多,隐私越强
可信设置Groth16 需要 Powers of Tau 仪式的 CRS

Built with AiAda