Appearance
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 字节值:
secret和nullifier - 计算承诺:
commitment = keccak256(abi.encodePacked(secret, nullifier)) - 存款时提交 commitment 到链上,存入 Merkle Tree
- 秘密凭证(
secret、nullifier)保存在链下,由用户保管
Merkle Tree:
- 高度为 20 的二叉树,支持最多 2^20 ≈ 1,048,576 笔存款
- 使用增量插入算法(Incremental Merkle Tree):
- 每个层级 i 维护一个"正在填充的"子树
filledSubtrees[i] - 偶数索引:当前叶子是该层子树的左半部分,右半部分用预计算的
zeros[i]填充 - 奇数索引:当前叶子是该层子树的右半部分,与之前存储的
filledSubtrees[i]配对哈希 - 每次插入更新整个路径上的哈希直到 root
- 每个层级 i 维护一个"正在填充的"子树
- 零值预计算:
zeros[0] = keccak256(bytes32(0)),zeros[i] = keccak256(zeros[i-1], zeros[i-1])
Nullifier 防双花:
nullifierHash = keccak256(abi.encodePacked(nullifier))- 每次取款时公开 nullifierHash 并标记为已使用
- 同一 nullifier 不能用于两次取款——防止同一存款被多次提取
3.2 存款流程
- 用户链下生成
secret和nullifier(各 32 随机字节) - 计算
commitment = keccak256(secret, nullifier) - 调用
deposit(commitment)并发送恰好0.1 ETH - 合约将 commitment 插入 Merkle Tree
- 存储
commitments[commitment] = leafIndex + 1(1-indexed,0 表示未使用) - 记录新的 Merkle root:
roots[depositCount] = currentRoot - 用户保存
(secret, nullifier, leafIndex)作为取款凭证
3.3 取款流程
- 用户提供
(secret, nullifier, recipient, relayer, fee) - 合约重构
commitment = keccak256(secret, nullifier)— 验证它已被存入 - 计算
nullifierHash = keccak256(nullifier)— 验证未被使用 - 验证
fee < DENOMINATION(中继费不能超过面额) - 标记
nullifierSpent[nullifierHash] = true(在转账前标记,遵循 CEI 模式) - 转账:
recipient获得DENOMINATION - fee,relayer获得fee - 中继者机制使得取款者(recipient)可以不同于交易发起者——进一步增强隐私
3.4 ZK 验证器接口(IVerifier)
合约中预留了 IVerifier 接口,定义了 verifyProof() 方法。在完整的 Tornado Cash 实现中:
- 取款操作不直接提交
secret和nullifier - 而是提交一个 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 中,取款不直接暴露 secret 和 nullifier,而是通过 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 证明验证的抽象层,可对接不同证明系统 |
| Groth16 | Tornado Cash 使用的证明系统,证明小(~128 字节),验证快 |
| MiMC 哈希 | ZK 友好的哈希函数,低乘法复杂度 |
| Circom | ZK 电路编写 DSL,编译为 R1CS 后生成证明 |
| 增量 Merkle Tree | O(h) 存储,O(h) 插入,无需存储整棵树 |
| 零值预计算 | zeros[i] = hash(zeros[i-1], zeros[i-1]),替换空子树 |
| 匿名集 | 池中存款越多,隐私越强 |
| 可信设置 | Groth16 需要 Powers of Tau 仪式的 CRS |