ZK 隐私交易系统设计
- 第1章 项目概览
ZK隐私交易系统 v0.1:架构与实现 (ZK Privacy Transaction System v0.1)
版本 v0.1 | 2027-01-19 适用读者:ZK协议工程师 / 隐私L2架构师 / 合规科技团队 / 隐私支付产品负责人 摘要:一个基于 Circom + Groth16 的合规友好型 ZK 隐私交易系统的完整设计与实现。本文给出从电路、合约、合规层、Relayer 网络到部署运维的端到端方案,并提供与 Tornado Cash / Aztec / Privacy Pools v2 的量化对比。
目录
- 第1章 项目概览
- 第2章 隐私+合规的设计哲学
- 第3章 系统架构(完整大图)
- 第4章 核心 ZK 电路(Circom 代码完整呈现)
- 第5章 链上合约设计
- 第6章 合规层设计(ASP+KYT 集成)
- 第7章 性能基准
- 第8章 与 Tornado / Aztec / Privacy Pools v2 对比
- 第9章 安全审计 checklist
- 第10章 部署与运维
- 第11章 路线图
- 附录 A 完整源码索引
- 附录 B 关键安全考量
- 附录 C 推荐阅读
第1章 项目概览
1.1 问题陈述 — 为什么 Tornado Cash 不够
Tornado Cash 在 2022 年 8 月达到约 $200M TVL,是当时最成功的链上隐私混币器(mixer)。它在工程上几乎完美:
- 使用 commitment-nullifier 范式,链上不可关联 deposit/withdraw;
- 使用 incremental Merkle tree,deposit gas 约 1.1M,withdraw gas 约 360k;
- Trusted setup 由 ~1100 人参与的 MPC ceremony 完成。
但它在产品/合规层面犯了一个不可弥补的错误:它没有任何机制把"善意用户"和"恶意用户"区分开。所有人共用同一个 anonymity set,监管无法识别 OFAC SDN 地址、朝鲜 Lazarus 资金等高风险来源。
后果是历史性的:
- 2022-08,OFAC 把 Tornado Cash 合约地址列入 SDN list(首次链上代码被制裁);
- 2022-08,开发者 Alexey Pertsev 在荷兰被捕,2024 年判刑 5 年 4 月;
- 2023-08,Roman Storm 在美被起诉 conspiracy to operate unlicensed money transmitter;
- 2024-01,GAO 报告称 ~$1.54B 经过 Tornado Cash,约 7.6% 与朝鲜 Lazarus 相关;
- 2024-11,第五巡回法院 (Van Loon v. Treasury) 判 OFAC 越权,2025-03 制裁正式撤销。
核心教训:纯密码学意义上的 privacy(无差别匿名)在 2025 年后的监管环境中已经不可行。任何严肃的隐私交易产品,必须从 day-1 就把"合规子集"(compliance subset)作为协议的一阶设计目标,而不是事后补丁。
1.2 设计哲学 — 隐私+合规的最小可行方案
本系统(暂称 PriPool v0.1)的设计哲学是:
隐私是公民权利,合规是社会契约。在两者之间寻找最小可行均衡。
具体到协议层,我们采纳 Vitalik 等人 2023 年提出的 Privacy Pools v2 框架,并做以下补强:
- k-anonymity within compliant set — 用户在"合规子集"内匿名(而非整个 pool);
- 多 ASP(Association Set Provider)共存 — 用户可选择不同 ASP,避免 single point of censorship;
- 链上+链下双 KYT 层 — 链上做硬性 SDN 过滤,链下做软性风险评分;
- 司法授权解密通道 — 通过 t-of-n threshold escrow 应对法律传票;
- Relayer 中心化但不可篡改 — Relayer 可中继 tx 但不能挪用资金(recipient/fee 全部 bind 到 proof)。
1.3 与 Tornado / Aztec / Privacy Pools v2 的差异
| 维度 | Tornado Cash | Aztec Connect (旧) | Privacy Pools v2 (0xbow) | PriPool v0.1(本文) |
|---|---|---|---|---|
| 隐私模型 | 全 pool k-anon | 全 pool k-anon | ASP 内 k-anon | ASP 内 k-anon + 多 ASP |
| 合规 | 无 | 无 | Chainalysis ASP | Chainalysis + TRM + 自定义 |
| withdraw gas | ~360k | ~500k (含 Note) | ~520k | ~480k(目标) |
| 司法授权 | 无 | 无 | 无 | t-of-3 threshold escrow |
| Token | TORN | AZTEC(计划) | 无 | 无(v0.1) |
| 支持币种 | ETH + 个别 ERC20 | ETH + 多 ERC20 | ETH | ETH + USDC(v0.1) |
| 代码状态 | 已被 fork | 关闭 | 主网 | 本文设计 |
1.4 系统目标(可量化)
v0.1 设计目标:
| 目标 | 数值 | 说明 |
|---|---|---|
| Withdraw gas | < 500k | 优于 Privacy Pools v2,近 Tornado |
| Proof generation (browser) | < 8 sec | snarkjs WebWorker,目标用户体验 |
| Proof size (Groth16) | 192 bytes | Groth16 标准 (3×G1+G2 → 256+512+256=1024 bit on bn254 → ~128 bytes calldata; 实际 calldata ~ 256 bytes) |
| Anonymity set 增速 | > 100 deposits/day | 上线后 30 天内 |
| ASP 更新频率 | 每 6 小时 | Chainalysis 数据延迟 |
| 合规拒绝率 | < 0.1% | 正常用户的"误伤率" |
| Pool TVL(6 月) | $20M | 略超 0xbow 当前 |
关键洞察
Tornado Cash 教训不是"隐私不可做",而是"无差别隐私不可做"。 PriPool v0.1 的核心创新点不在密码学(沿用 Privacy Pools v2),而在产品工程:把 ASP、KYT、threshold escrow、Relayer 网络做成 内置且可组合 的协议组件,让 PM/合规/法务团队可以根据司法管辖区做配置,而不是改电路。
第2章 隐私+合规的设计哲学
2.1 三难问题(隐私 / 合规 / 分散化)
类似 blockchain trilemma,隐私交易系统也存在一个三难:
完全隐私(unlinkable)
●
/ \
/ \
/ \
/ \
/ \
/ \
/ \
/ \
分散化 ●─────────────────────● 合规(监管可见性)
(no single (KYT/AML/audit)
operator)
经典三角:
- 完全隐私 + 分散化 → 无合规 = Tornado Cash → 被制裁
- 完全隐私 + 合规 → 中心化 = 银行 + Zcash shielded but with bank holding key
- 合规 + 分散化 → 半隐私 = Privacy Pools v2 / 本文方案
PriPool v0.1 选择的位置是 「半隐私 + 半分散化 + 强合规」:
- 隐私:anonymity set 内 k-anonymous(k 取决于 ASP 大小);
- 合规:链上 SDN 黑名单 + 链下 KYT API + 司法授权通道;
- 分散化:多 ASP、多 Relayer,但 admin key 由多签(短期)→ DAO(长期)持有。
2.2 ASP(Association Set Provider)模型 vs 黑名单模型
黑名单模型(Tornado Cash 在 2022 年仓促加上的方案):
合约维护 blacklist[address]
deposit/withdraw 时检查 from/to ∉ blacklist
问题:
- 黑名单需要主动更新(运营负担);
- 谁来更新?谁来仲裁?中心化点;
- 链上更新成本高(每加一个地址 ~50k gas);
- 用户被错误加入黑名单后申诉困难。
ASP(白名单/合规子集)模型(Privacy Pools v2):
ASP 维护一个 commitment 子集 A ⊆ P 的 Merkle root
用户 withdraw 时证明:
1. 我知道某 commitment c 的 secret
2. c ∈ P (pool root)
3. c ∈ A (ASP root)
优点:
- 默认拒绝(white-list)比默认接受(black-list)安全得多;
- 多 ASP 竞争 — 用户可选择 Chainalysis、TRM、Elliptic、社区 DAO 等;
- 监管可要求受监管 entity 只接受特定 ASP;
- 链上只需存一个 root(每次更新 ~30k gas,远低于黑名单)。
2.3 KYT 层的设计 — 链上 vs 链下
KYT (Know Your Transaction) 是合规栈的核心。我们采用 双层架构:
链上 KYT(硬性,~immediate):
- OFAC SDN list 直接编入 ComplianceOracle 合约;
- deposit/withdraw recipient/relayer 必须不在 SDN;
- 实现方式:bytes32 indexed mapping,deposit 时检查
msg.sender,withdraw 时检查recipient、relayer; - 更新频率:Treasury OFAC 公告后 24 小时内(多签更新)。
链下 KYT(软性,~6h delay):
- ASP(Chainalysis/TRM)每 6 小时拉取链上新增 deposits,运行风险评分;
- 评分 < 阈值的 commitment 加入"合规集合 A";
- 评分 ≥ 阈值的 commitment 不加入,但可继续在 pool 里(用户可选择不同 ASP 重试);
- 输出:A 的 Merkle root,由 ASP 签名后提交到 ComplianceOracle。
2.4 Vitalik 的 Privacy Pools v2 框架解读
论文《Blockchain Privacy and Regulatory Compliance: Towards a Practical Equilibrium》(Buterin, Illum, Nadler, Schär, Soleimani, 2023-09) 的核心论点:
不必在"全开放"和"全封闭"之间二选一。可以用 ZK 让用户主动证明自己属于某个合规子集,从而获得"合规子集内的 k-anonymity"。
形式化:
设 deposits 集合 P = {c₁, ..., cₙ}。ASP 发布 A ⊆ P 的 Merkle root R_A。 用户 withdraw 时给出 ZK proof:
∃ (s, n, c, π_P, π_A):
c = Poseidon(n, s) -- 知道 commitment 的 pre-image
Verify(R_P, c, π_P) = true -- c 在 pool 中
Verify(R_A, c, π_A) = true -- c 在 ASP 合规集合中
nullifierHash = Poseidon(n) -- 防双花
这个设计的产品意义在于:
- 用户主动选择合规 — ASP 是 opt-in(不强制),但选择"非主流 ASP"会使 anonymity set 变小,本身就是一种自我惩罚;
- 监管获得 visibility — 监管不知道 user A 是哪笔 deposit,但知道 user A 选择了哪个 ASP(这等于声明合规属性);
- 去中心化 ASP 市场 — 多 ASP 竞争避免单点审查(Chainalysis 拒绝某用户,TRM 可能接受)。
关键洞察
PriPool v0.1 不是"加了合规的 Tornado",而是"用合规重新定义 anonymity"。 在 Tornado 模型中 anonymity set = 全部 deposits,监管视角下"合规用户"和"恶意用户"无法区分。在 ASP 模型中,anonymity set 是用户主动选择并被 ASP 验证过的合规子集,监管不需要看到具体身份,只需要看到"用户在某个被认证的子集内",这对 AML/CFT 已经够用了。真正的产品突破不在密码学,在于把"主动声明合规属性"做成无摩擦的用户行为。
第3章 系统架构(完整大图)
3.1 整体架构图
┌──────────────────────────────────────┐
│ User (Browser) │
│ ┌────────────────────────────────┐ │
│ │ Frontend (Next.js + RainbowKit)│ │
│ │ - deposit form │ │
│ │ - withdraw flow │ │
│ │ - ASP selector │ │
│ └────────────┬───────────────────┘ │
│ ┌────────────┴───────────────────┐ │
│ │ ZK Prover (snarkjs WebWorker) │ │
│ │ - witness gen │ │
│ │ - Groth16 proof gen │ │
│ └────────────┬───────────────────┘ │
└───────────────┼──────────────────────┘
│
┌───────────────────────┼─────────────────────────┐
│ │ │
│ deposit (raw tx) │ withdraw │ withdraw
│ │ (via Relayer) │ (direct, recipient pays gas)
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────────┐ ┌─────────────────┐
│ Ethereum Node │ │ Relayer Network │ │ Ethereum Node │
│ (user's RPC) │ │ ┌─────────────────┐ │ │ (user's RPC) │
└────────┬─────────┘ │ │ Relayer Server │ │ └────────┬────────┘
│ │ │ - validate proof│ │ │
│ │ │ - simulate gas │ │ │
│ │ │ - submit tx │ │ │
│ │ └────────┬────────┘ │ │
│ └───────────┼──────────┘ │
│ │ │
▼ ▼ ▼
┌────────────────────────────────────────────────────────────┐
│ Smart Contract Layer (L1 / L2) │
│ ┌────────────────┐ ┌────────────────┐ ┌──────────────┐ │
│ │ PrivacyPool │──│ ComplianceOracle│──│ Verifier │ │
│ │ - tree state │ │ - SDN blacklist │ │ (Groth16) │ │
│ │ - nullifiers │ │ - ASP roots map │ │ │ │
│ └───────┬────────┘ └────────┬────────┘ └──────────────┘ │
│ │ │ │
│ ┌───────┴────────┐ ┌────────┴────────┐ │
│ │ Relayer │ │ ThresholdEscrow │ │
│ │ Registry │ │ (judicial key) │ │
│ └────────────────┘ └─────────────────┘ │
└──────────────────────────┬──────────────────────────────────┘
│
▼ events
┌─────────────────────────────────────────────────┐
│ Off-chain Compliance Stack │
│ ┌───────────────┐ ┌──────────────────────┐ │
│ │ Chainalysis │ │ TRM Labs / Elliptic │ │
│ │ KYT API │ │ KYT API │ │
│ └───────┬───────┘ └──────────┬───────────┘ │
│ │ │ │
│ ┌───────┴───────────────────────┴───────────┐ │
│ │ ASP Aggregator Service │ │
│ │ - poll Deposit events │ │
│ │ - score commitments │ │
│ │ - build A_root, sign, submit on-chain │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
3.2 模块边界
| 模块 | 职责 | 技术栈 |
|---|---|---|
| Frontend | 用户界面、note 管理、ASP 选择 | Next.js, wagmi, RainbowKit, IndexedDB |
| ZK Prover | witness 生成、proof 生成 | snarkjs (WASM), WebWorker |
| Relayer Server | 中继 tx、收 fee | Node.js, ethers.js, Redis (replay protection) |
| PrivacyPool.sol | 核心存取、Merkle tree | Solidity 0.8.20+ |
| Verifier.sol | Groth16 verifier | snarkjs auto-generated |
| ComplianceOracle.sol | ASP roots + SDN blacklist | Solidity, multisig admin |
| RelayerRegistry.sol | Relayer 注册、staking | Solidity |
| ThresholdEscrow.sol | 司法授权解密 | Solidity + off-chain MPC |
| ASP Aggregator | 拉链上数据 + 调 KYT API + 发布 root | Python, requests, gnosis-py |
3.3 信任模型 — 谁信任谁,何时信任
| 信任关系 | 强度 | 备注 |
|---|---|---|
| 用户 → ZK 电路 | 强(密码学) | 假设 trusted setup ceremony 至少 1 人诚实 |
| 用户 → PrivacyPool 合约 | 强 | 已审计 + 不可升级(无 admin key for funds) |
| 用户 → ComplianceOracle | 中 | admin 可改 SDN list & 接受 ASP,但不能动用户资金 |
| 用户 → ASP(如 Chainalysis) | 中 | 用户选择ASP,可换 |
| 用户 → Relayer | 弱 | recipient/fee bound in proof,relayer 至多拒绝转发 |
| 监管 → ASP | 强 | 监管要求受监管 entity 只接受特定 ASP |
| 监管 → ThresholdEscrow | 中 | 需要法庭 order 后由 t-of-3 解密 |
关键设计原则:用户资金安全只依赖 PrivacyPool + Verifier + 电路,其他都是辅助。
3.4 数据流
Deposit flow
1. User: 浏览器随机生成 (n, s) ∈ F²
2. User: 计算 c = Poseidon(n, s)
3. User: 把 (n, s, c) 加密存到本地(IndexedDB) + 下载 note 文件备份
4. User: 钱包 sign tx → PrivacyPool.deposit(c) + 1 ETH
5. Pool: 检查 msg.sender ∉ SDN,c 未被使用
6. Pool: insert(c) into Merkle tree, root_new = recompute
7. Pool: emit Deposit(c, leafIndex, timestamp)
8. ASP Aggregator: 监听 event, 拉 KYT 评分
9. ASP: 6h 后发布新 ASP root R_A^{t+1}
Withdraw flow
1. User: 选择 ASP(默认 Chainalysis "no SDN")
2. User: 从 ComplianceOracle 读 R_A 和 ASP 的 Merkle path
3. User: 从 PrivacyPool 读 R_P 和自己 commitment 的 Merkle path
4. User: ZK Prover 生成 proof π
5. User: 选择 (a) 自己发 tx,或 (b) 通过 Relayer
6a. (Direct): User → PrivacyPool.withdraw(π, ...)
6b. (Relayer): User POST proof to Relayer → Relayer → PrivacyPool.withdraw
7. Pool: verifier.verifyProof(π) 通过
8. Pool: 检查 nullifierHash 未使用、root 在历史 30 个内、recipient ∉ SDN
9. Pool: nullifierHashes[h] = true
10. Pool: transfer(recipient, denomination - fee), if fee>0 transfer(relayer, fee)
11. Pool: emit Withdraw(...)
Compliance check flow
1. ASP Aggregator (cron, every 6h):
a. fetch new Deposit events since last run
b. for each commitment c:
- find depositor address from tx.from
- call Chainalysis KYT API: risk_score(address)
- if score < threshold: include c in A_new
c. compute Merkle root R_A
d. multisig sign R_A
e. submit ComplianceOracle.updateASPRoot(asp_id, R_A, signature)
2. ComplianceOracle: verify multisig, store R_A, emit ASPUpdated(asp_id, R_A)
3. Frontend: subscribe to ASPUpdated events, refresh user UI
3.5 安全边界划分
┌───────────────────────────────────────────────────────────┐
│ Trust Boundary 1: User's machine │
│ - private keys, secrets (n, s), notes │
│ - threat: malware, browser extension, physical access │
└───────────────────────────────────────────────────────────┘
│ HTTPS + signed tx
┌───────────────────────────────────────────────────────────┐
│ Trust Boundary 2: Relayer (optional) │
│ - sees proof + recipient (but recipient is bound) │
│ - threat: censoring, slow broadcast │
└───────────────────────────────────────────────────────────┘
│ tx
┌───────────────────────────────────────────────────────────┐
│ Trust Boundary 3: Ethereum L1 │
│ - PrivacyPool, ComplianceOracle, Verifier │
│ - threat: smart contract bugs, validator MEV │
└───────────────────────────────────────────────────────────┘
│ events
┌───────────────────────────────────────────────────────────┐
│ Trust Boundary 4: ASP Aggregator (off-chain) │
│ - reads public events, calls KYT APIs │
│ - threat: API outage, key compromise │
└───────────────────────────────────────────────────────────┘
│ multisig tx
▼
ComplianceOracle
关键洞察
架构图最大的"反直觉"在于 Relayer 是可信度最低的组件,但它对用户体验最关键。如果用 EOA 直接 withdraw,recipient 必须有 ETH 付 gas,这反向暴露了 recipient(如果 recipient 之前没有 ETH,那么 withdraw 后立刻有钱 = 强关联信号)。Relayer 解决这个问题,但也带来 censorship 风险。所以 Relayer 必须是去中心化网络(v0.2 路线)+ 经济激励(fee 机制)+ 强约束(recipient bound in proof)。
第4章 核心 ZK 电路(Circom 代码完整呈现)
4.1 电路设计原则 — minimize constraints
ZK 电路成本与约束数(constraints)成线性关系。一个 Groth16 prover 在 25k 约束下 prove 时间约 10 秒(浏览器),50k 约束下约 20 秒。所以减约束数等于减用户等待时间。
实践原则:
- Hash 用 Poseidon,不用 keccak/SHA。Poseidon 在 BN254 上每 hash ~250 约束,keccak ~150k 约束。
- Merkle tree 深度刚好够用。深度 20 = 1M 叶子(够 5-10 年用),深度 32 = 4G 叶子(浪费)。
- public input 越少越好。每个 public input 在 verifier 里都是一个 G1 scalar mul(~ 30 约束 + 5500 gas)。
- 避免动态分支。Circom 是 declarative 的,
if在编译期展开,不能"少做"任何分支。
PriPool v0.1 电路约束预算:
| 子电路 | 约束数 |
|---|---|
| CommitmentHasher | ~250 |
| MerkleTreeChecker (depth 20) | ~5,000 |
| ASPMembershipChecker (depth 16) | ~4,000 |
| public input binding (squares) | ~4 |
| 总计 (withdraw) | ~9,500 |
对比 Tornado Cash:~25,000(更深 tree + 不同 hash)。PriPool 更轻是因为 Pool tree 复用了 ASP membership 子树结构。
4.2 deposit.circom — commitment 生成
deposit 端的电路只是辅助生成 witness,链上 deposit 不需要 ZK proof(因为没有隐私需求 — 用户公开提交 c)。但为了 client SDK 一致,我们仍写 circom:
pragma circom 2.1.6;
include "circomlib/circuits/poseidon.circom";
template Deposit() {
signal input nullifier; // private, 254-bit
signal input secret; // private, 254-bit
signal output commitment; // public
component h = Poseidon(2);
h.inputs[0] <== nullifier;
h.inputs[1] <== secret;
commitment <== h.out;
}
component main {public [commitment]} = Deposit();
这个电路实际上只是"hash 检查器",无需 trusted setup(直接 evaluate 即可)。生产环境我们直接在 JS/TS 里调 circomlibjs.poseidon 计算。
4.3 withdraw.circom — 核心提现电路
pragma circom 2.1.6;
include "circomlib/circuits/poseidon.circom";
include "circomlib/circuits/bitify.circom";
include "./merkle.circom";
// ─── Helper: 计算 commitment 与 nullifierHash ─────────────
template CommitmentHasher() {
signal input nullifier;
signal input secret;
signal output commitment;
signal output nullifierHash;
component cH = Poseidon(2);
cH.inputs[0] <== nullifier;
cH.inputs[1] <== secret;
commitment <== cH.out;
component nH = Poseidon(1);
nH.inputs[0] <== nullifier;
nullifierHash <== nH.out;
}
// ─── 主电路:Pool inclusion + ASP inclusion + nullifier ──
template Withdraw(POOL_LEVELS, ASP_LEVELS) {
// ── public ──
signal input poolRoot; // pool merkle root
signal input aspRoot; // ASP membership root
signal input nullifierHash; // = Poseidon(nullifier)
signal input recipient; // 接收地址(160-bit)
signal input relayer; // relayer 地址(可为 0)
signal input fee; // 给 relayer 的 fee
signal input refund; // optional ETH refund
signal input aspId; // 选择的 ASP ID
// ── private ──
signal input nullifier;
signal input secret;
signal input poolPathElements[POOL_LEVELS];
signal input poolPathIndices[POOL_LEVELS];
signal input aspPathElements[ASP_LEVELS];
signal input aspPathIndices[ASP_LEVELS];
// 1. 重算 commitment 和 nullifierHash
component hasher = CommitmentHasher();
hasher.nullifier <== nullifier;
hasher.secret <== secret;
hasher.nullifierHash === nullifierHash; // 强约束
// 2. 验证 commitment 在 Pool 树中
component poolTree = MerkleTreeChecker(POOL_LEVELS);
poolTree.leaf <== hasher.commitment;
poolTree.root <== poolRoot;
for (var i = 0; i < POOL_LEVELS; i++) {
poolTree.pathElements[i] <== poolPathElements[i];
poolTree.pathIndices[i] <== poolPathIndices[i];
}
// 3. 验证 commitment 在 ASP 合规子集中
component aspTree = MerkleTreeChecker(ASP_LEVELS);
aspTree.leaf <== hasher.commitment;
aspTree.root <== aspRoot;
for (var i = 0; i < ASP_LEVELS; i++) {
aspTree.pathElements[i] <== aspPathElements[i];
aspTree.pathIndices[i] <== aspPathIndices[i];
}
// 4. 把 recipient/relayer/fee/refund/aspId bind 到 proof
// (用平方约束确保它们参与了 proof 生成)
signal rSquare;
signal relayerSquare;
signal feeSquare;
signal refundSquare;
signal aspSquare;
rSquare <== recipient * recipient;
relayerSquare <== relayer * relayer;
feeSquare <== fee * fee;
refundSquare <== refund * refund;
aspSquare <== aspId * aspId;
}
component main {public [
poolRoot, aspRoot, nullifierHash,
recipient, relayer, fee, refund, aspId
]} = Withdraw(20, 16);
几个关键点的说明:
hasher.nullifierHash === nullifierHash— 这一行是 Day 227 提到的 Nova-style 漏洞防护点。如果写漏,prover 可以提交任意 nullifierHash。- 两个独立的 Merkle tree 验证 — Pool tree (depth 20) 和 ASP tree (depth 16) 是不同的树。POOL 全集 ≥ ASP 子集,所以 ASP 树更小。
aspId也 bind 到 proof — 这样链上合约可以检查aspRoot与aspId对应(防止用户拿 ASP_A 的 root 但声明 ASP_B)。- 平方约束(
x * x) — 这是把"无运算的 public input"绑定到 proof 的标准 trick。Tornado Cash 也是这么做的。
4.4 asp_membership.circom — ASP 合规 membership 证明
ASP membership 实际上和 pool membership 是同样结构(Merkle proof),只是输入不同的 root 和 path。我们没必要单独写一个 circuit,直接把它合并到 withdraw.circom 里就是上面 4.3 的设计。
但如果要支持 v0.2 的"独立 ASP attestation"(用户把 commitment 放进 ASP 的过程),可以单独写:
pragma circom 2.1.6;
include "circomlib/circuits/poseidon.circom";
include "./merkle.circom";
template ASPAttest(ASP_LEVELS) {
signal input aspRoot; // public
signal input commitment; // public
signal input pathElements[ASP_LEVELS];
signal input pathIndices[ASP_LEVELS];
component tree = MerkleTreeChecker(ASP_LEVELS);
tree.leaf <== commitment;
tree.root <== aspRoot;
for (var i = 0; i < ASP_LEVELS; i++) {
tree.pathElements[i] <== pathElements[i];
tree.pathIndices[i] <== pathIndices[i];
}
}
component main {public [aspRoot, commitment]} = ASPAttest(16);
用例:用户向某个监管机构出具"我的 commitment 在合规子集 A 内"的 ZK 证明(用于 KYC/MTL 报告),但不暴露任何其他信息。
4.5 子电路:Poseidon / MerkleTreeChecker / Num2Bits
merkle.circom:
pragma circom 2.1.6;
include "circomlib/circuits/poseidon.circom";
include "circomlib/circuits/switcher.circom";
template MerkleTreeChecker(LEVELS) {
signal input leaf;
signal input root;
signal input pathElements[LEVELS];
signal input pathIndices[LEVELS];
component selectors[LEVELS];
component hashers[LEVELS];
signal levelHashes[LEVELS + 1];
levelHashes[0] <== leaf;
for (var i = 0; i < LEVELS; i++) {
selectors[i] = Switcher();
selectors[i].sel <== pathIndices[i];
selectors[i].L <== levelHashes[i];
selectors[i].R <== pathElements[i];
hashers[i] = Poseidon(2);
hashers[i].inputs[0] <== selectors[i].outL;
hashers[i].inputs[1] <== selectors[i].outR;
levelHashes[i+1] <== hashers[i].out;
}
root === levelHashes[LEVELS];
}
Switcher:
sel = 0: (outL, outR) = (L, R)
sel = 1: (outL, outR) = (R, L)
约束:sel * (1 - sel) === 0 (sel 必须是 bit)
4.6 约束计数与优化
实测(Circom 2.1.6 + circomlib 最新):
| 子电路 | constraints | linear constraints | non-linear |
|---|---|---|---|
| Poseidon(2) | ~200 | 50 | 150 |
| Poseidon(1) | ~150 | 40 | 110 |
| Switcher | 4 | 2 | 2 |
| MerkleTreeChecker(20) | ~4,200 | ~1,000 | ~3,200 |
| MerkleTreeChecker(16) | ~3,400 | ~800 | ~2,600 |
| Withdraw(20, 16) | ~9,500 | ~2,500 | ~7,000 |
优化路线:
- 改用 Poseidon2(2024 年新版 Poseidon):约束数减少 ~30% → 总约束 ~6,500;
- 改 hash 为 RescuePrime / Anemoi:约束数减少 ~40%,但 EVM gas 高(不适合 deposit on-chain hash);
- batch withdraw(一次证明多笔提现):proof 时间增加 ~3×,但单笔摊薄到 ~3秒;v0.2 路线。
4.7 Trusted Setup 考量
Groth16 需要电路特定的 trusted setup(Phase 2)。流程:
Phase 1(universal Powers of Tau):
- 复用 Hermez 的
powersOfTau28_hez_final_15.ptau(支持 ≤ 2^15 = 32k 约束); - 该 ptau 由 Hermez ceremony 完成(数百人参与),社区公认安全。
Phase 2(circuit-specific):
# 1. 编译电路
circom withdraw.circom --r1cs --wasm --sym
# 2. setup
snarkjs groth16 setup withdraw.r1cs powersOfTau28_hez_final_15.ptau withdraw_0000.zkey
# 3. ceremony (target: 50+ contributors)
snarkjs zkey contribute withdraw_0000.zkey withdraw_0001.zkey \
--name="contributor1" -v -e="random entropy 1"
# 重复 N 次...
# 4. apply random beacon
snarkjs zkey beacon withdraw_NNNN.zkey withdraw_final.zkey \
0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20 \
10 -n="Final Beacon"
# 5. export verifier
snarkjs zkey export solidityverifier withdraw_final.zkey Verifier.sol
ceremony 治理:
- 公开邀请 50+ 贡献者,包括团队、审计公司、知名 ZK 研究员、社区代表;
- 每个贡献者公开发表 attestation(含 GPG 签名 + transcript hash);
- 最后用 Drand 或 Ethereum block hash 作为 beacon(防止最后一个贡献者作弊);
- 如果未来需要升级电路,必须新做一次 ceremony(Groth16 setup 不能复用)。
关键洞察
Tornado Cash v1 的 trusted setup 是 ZK 工程史上最被忽视的成功:1100+ 参与者,没有人证实可以重建 toxic waste,至今没有伪造 proof 事件。但这也是 Groth16 路线的"最后障碍"——每改一次电路就要重做一次 ceremony。所以 v0.1 不要追求电路完美,而应该把 ceremony 做扎实(50+ 高质量贡献者比 1000+ 散户贡献者更有价值),让电路在 12-18 个月内不变。如果未来要支持多币种、多 denomination,应该用 KZG/PLONK(universal setup)而不是 Groth16,这是 v0.3 路线的重要决策点。
第5章 链上合约设计
5.1 PrivacyPool.sol — 核心存取合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./Verifier.sol";
import "./ComplianceOracle.sol";
interface IPoseidon {
function poseidon(uint256[2] calldata) external pure returns (uint256);
}
contract PrivacyPool {
// ─── Constants ─────────────────────────────────────────
uint256 public constant DENOMINATION = 1 ether;
uint32 public constant LEVELS = 20;
uint32 public constant ROOT_HISTORY_SIZE = 30;
uint256 public constant FIELD_SIZE =
21888242871839275222246405745257275088548364400416034343698204186575808495617;
// ─── External contracts ────────────────────────────────
Groth16Verifier public immutable verifier;
ComplianceOracle public immutable oracle;
IPoseidon public immutable hasher;
// ─── Merkle tree state ─────────────────────────────────
mapping(uint256 => bytes32) public filledSubtrees;
mapping(uint256 => bytes32) public roots;
uint32 public currentRootIndex;
uint32 public nextLeafIndex;
// ─── Anti-replay ───────────────────────────────────────
mapping(bytes32 => bool) public commitments;
mapping(bytes32 => bool) public nullifierHashes;
// ─── Events ────────────────────────────────────────────
event Deposit(
bytes32 indexed commitment,
uint32 leafIndex,
uint256 timestamp,
address depositor
);
event Withdraw(
address recipient,
bytes32 nullifierHash,
address indexed relayer,
uint256 fee,
bytes32 aspId
);
// ─── Constructor ───────────────────────────────────────
constructor(address _verifier, address _oracle, address _hasher) {
verifier = Groth16Verifier(_verifier);
oracle = ComplianceOracle(_oracle);
hasher = IPoseidon(_hasher);
bytes32 zero = bytes32(uint256(keccak256("PriPool-zero")) % FIELD_SIZE);
for (uint32 i = 0; i < LEVELS; i++) {
filledSubtrees[i] = zero;
zero = bytes32(hasher.poseidon([uint256(zero), uint256(zero)]));
}
roots[0] = zero;
}
// ─── DEPOSIT ───────────────────────────────────────────
function deposit(bytes32 commitment) external payable {
require(msg.value == DENOMINATION, "PriPool: wrong amount");
require(!commitments[commitment], "PriPool: duplicate commitment");
require(!oracle.isSDN(msg.sender), "PriPool: depositor sanctioned");
require(nextLeafIndex < 2**LEVELS, "PriPool: tree full");
uint32 idx = nextLeafIndex;
bytes32 cur = commitment;
uint32 currentIndex = idx;
for (uint32 i = 0; i < LEVELS; i++) {
bytes32 left;
bytes32 right;
if (currentIndex % 2 == 0) {
left = cur;
right = filledSubtrees[i];
} else {
left = filledSubtrees[i];
right = cur;
filledSubtrees[i] = cur;
}
cur = bytes32(hasher.poseidon([uint256(left), uint256(right)]));
currentIndex /= 2;
}
currentRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE;
roots[currentRootIndex] = cur;
nextLeafIndex = idx + 1;
commitments[commitment] = true;
emit Deposit(commitment, idx, block.timestamp, msg.sender);
}
function isKnownPoolRoot(bytes32 root) public view returns (bool) {
if (root == bytes32(0)) return false;
for (uint32 i = 0; i < ROOT_HISTORY_SIZE; i++) {
if (roots[i] == root) return true;
}
return false;
}
// ─── WITHDRAW ──────────────────────────────────────────
struct WithdrawProof {
uint[2] pA;
uint[2][2] pB;
uint[2] pC;
}
struct WithdrawArgs {
bytes32 poolRoot;
bytes32 aspRoot;
bytes32 aspId;
bytes32 nullifierHash;
address payable recipient;
address payable relayer;
uint256 fee;
uint256 refund;
}
function withdraw(WithdrawProof calldata p, WithdrawArgs calldata a) external {
require(a.fee <= DENOMINATION / 2, "PriPool: fee too high");
require(!nullifierHashes[a.nullifierHash], "PriPool: double spend");
require(isKnownPoolRoot(a.poolRoot), "PriPool: unknown pool root");
require(
oracle.isKnownASPRoot(a.aspId, a.aspRoot),
"PriPool: unknown ASP root"
);
require(!oracle.isSDN(a.recipient), "PriPool: recipient sanctioned");
if (a.relayer != address(0)) {
require(!oracle.isSDN(a.relayer), "PriPool: relayer sanctioned");
require(oracle.isRegisteredRelayer(a.relayer), "PriPool: relayer not registered");
}
uint[8] memory pub = [
uint256(a.poolRoot),
uint256(a.aspRoot),
uint256(a.nullifierHash),
uint256(uint160(a.recipient)),
uint256(uint160(a.relayer)),
a.fee,
a.refund,
uint256(a.aspId)
];
require(verifier.verifyProof(p.pA, p.pB, p.pC, pub), "PriPool: invalid proof");
nullifierHashes[a.nullifierHash] = true;
(bool ok, ) = a.recipient.call{value: DENOMINATION - a.fee}("");
require(ok, "PriPool: recipient transfer failed");
if (a.fee > 0) {
(ok, ) = a.relayer.call{value: a.fee}("");
require(ok, "PriPool: relayer transfer failed");
}
emit Withdraw(a.recipient, a.nullifierHash, a.relayer, a.fee, a.aspId);
}
}
5.2 Verifier.sol — Groth16 verifier
由 snarkjs zkey export solidityverifier 自动生成,关键签名:
contract Groth16Verifier {
// Generated by snarkjs
function verifyProof(
uint[2] calldata _pA,
uint[2][2] calldata _pB,
uint[2] calldata _pC,
uint[8] calldata _pubSignals
) public view returns (bool);
}
实现细节(重点):
- 用 EVM precompile
0x06(bn256Add, 150 gas)和0x07(bn256ScalarMul, 6000 gas)和0x08(bn256Pairing, 45000 + 34000n gas); - 8 个 public input 意味着 8 次 scalar mul + 1 次 pairing;
- 实测 verify gas: ~280k(Tornado 是 ~230k,多出来的 ~50k 是 ASP 相关的两个 input + bigger pairing input)。
5.3 ComplianceOracle.sol — ASP 集合管理 + 黑名单
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract ComplianceOracle is AccessControl {
bytes32 public constant ASP_PROVIDER_ROLE = keccak256("ASP_PROVIDER_ROLE");
bytes32 public constant SDN_UPDATER_ROLE = keccak256("SDN_UPDATER_ROLE");
// ASP_id (bytes32) → root → bool
mapping(bytes32 => mapping(bytes32 => bool)) public aspRoots;
mapping(bytes32 => bytes32) public latestASPRoot;
mapping(bytes32 => string) public aspMetadata; // e.g., "chainalysis-no-sdn"
// SDN list (硬性,OFAC + 内部黑名单)
mapping(address => bool) public sdnList;
// Relayer registry
mapping(address => bool) public registeredRelayers;
event ASPUpdated(bytes32 indexed aspId, bytes32 newRoot, uint256 timestamp);
event SDNAdded(address indexed addr, string reason);
event SDNRemoved(address indexed addr);
event RelayerRegistered(address indexed relayer, uint256 stake);
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(SDN_UPDATER_ROLE, admin);
}
// ─── ASP management ────────────────────────────────────
function registerASP(bytes32 aspId, address provider, string calldata meta)
external onlyRole(DEFAULT_ADMIN_ROLE)
{
_grantRole(keccak256(abi.encode(ASP_PROVIDER_ROLE, aspId)), provider);
aspMetadata[aspId] = meta;
}
function updateASPRoot(bytes32 aspId, bytes32 newRoot) external {
require(
hasRole(keccak256(abi.encode(ASP_PROVIDER_ROLE, aspId)), msg.sender),
"Oracle: not ASP provider"
);
aspRoots[aspId][newRoot] = true;
latestASPRoot[aspId] = newRoot;
emit ASPUpdated(aspId, newRoot, block.timestamp);
}
function isKnownASPRoot(bytes32 aspId, bytes32 root) external view returns (bool) {
return aspRoots[aspId][root];
}
// ─── SDN management ────────────────────────────────────
function addSDN(address addr, string calldata reason) external onlyRole(SDN_UPDATER_ROLE) {
sdnList[addr] = true;
emit SDNAdded(addr, reason);
}
function removeSDN(address addr) external onlyRole(SDN_UPDATER_ROLE) {
sdnList[addr] = false;
emit SDNRemoved(addr);
}
function isSDN(address addr) external view returns (bool) {
return sdnList[addr];
}
// ─── Relayer registry (lite) ───────────────────────────
function registerRelayer() external payable {
require(msg.value >= 1 ether, "Oracle: stake too low");
registeredRelayers[msg.sender] = true;
emit RelayerRegistered(msg.sender, msg.value);
}
function isRegisteredRelayer(address r) external view returns (bool) {
return registeredRelayers[r];
}
}
5.4 Relayer.sol — Relayer 注册 + gas refund
实际生产中,Relayer 注册可以并入 ComplianceOracle(如上 §5.3),独立合约只在 v0.2 引入 staking + slashing 时才需要:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract RelayerRegistryV2 {
struct RelayerInfo {
uint256 stake;
uint64 registeredAt;
uint64 unbondingAt; // 0 if active
bytes32 endpointHash; // hash of HTTPS endpoint URL
uint32 successCount;
uint32 failureCount;
}
mapping(address => RelayerInfo) public relayers;
uint256 public constant MIN_STAKE = 5 ether;
uint256 public constant UNBONDING_PERIOD = 7 days;
event Registered(address indexed relayer, bytes32 endpoint, uint256 stake);
event UnbondingStarted(address indexed relayer, uint256 unlockAt);
event Withdrawn(address indexed relayer, uint256 amount);
event Slashed(address indexed relayer, uint256 amount, string reason);
function register(bytes32 endpointHash) external payable {
require(msg.value >= MIN_STAKE, "Relayer: stake too low");
require(relayers[msg.sender].registeredAt == 0, "Relayer: already registered");
relayers[msg.sender] = RelayerInfo({
stake: msg.value,
registeredAt: uint64(block.timestamp),
unbondingAt: 0,
endpointHash: endpointHash,
successCount: 0,
failureCount: 0
});
emit Registered(msg.sender, endpointHash, msg.value);
}
function startUnbonding() external {
require(relayers[msg.sender].registeredAt != 0, "Relayer: not registered");
relayers[msg.sender].unbondingAt = uint64(block.timestamp + UNBONDING_PERIOD);
emit UnbondingStarted(msg.sender, relayers[msg.sender].unbondingAt);
}
function withdrawStake() external {
RelayerInfo storage r = relayers[msg.sender];
require(r.unbondingAt != 0 && block.timestamp >= r.unbondingAt, "Relayer: not ready");
uint256 amt = r.stake;
delete relayers[msg.sender];
(bool ok, ) = msg.sender.call{value: amt}("");
require(ok, "Relayer: withdraw failed");
emit Withdrawn(msg.sender, amt);
}
}
5.5 安全考量
Reentrancy
withdraw()用 checks-effects-interactions 模式:先设置nullifierHashes[h] = true,再recipient.call;- 即使
recipient是恶意合约也无法重入(nullifierHash 已被使用)。
Front-running
- 任何人都可以看到 mempool 中的 withdraw tx,但无法修改
recipient(已 bind in proof); - 攻击者无法用相同 proof 提交(因为 fee/refund/relayer 也 bind,只要任一不同就 verify 失败);
- 防 sandwich:Privacy Pool 不涉及 swap,无 sandwich 风险。
Merkle root 历史窗口
- 30 个 root,平均出块时间 12s,每次 deposit 更新一个 root → 大约 360s 窗口;
- 如果 deposit 频率高(如每秒一笔),窗口会更短 → 用户必须快速 withdraw;
- 改进:用
IncrementalMerkleTree+ 更长的 history(如 100 个 root)。
Commitment collision
- Poseidon 是 SNARK-friendly hash,254-bit 输出,碰撞概率 ~2^-127;
- 但
<= FIELD_SIZE检查必须做:commitment 必须在 BN254 的 scalar field 内(否则 verifier 会 reject)。
Oracle key compromise
- ComplianceOracle admin 是多签(v0.1:3-of-5,v1.0:DAO governance);
- 如果 admin key 被盗,攻击者可以:
- 加任意地址到 SDN(DoS);
- 接受恶意 ASP root(让恶意 commitments 看起来合规);
- 但攻击者不能直接动 PrivacyPool 资金(pool 没有 admin key);
- 应急:social recovery + 7 天 timelock。
5.6 升级路径(Diamond / Proxy)
v0.1 不做合约升级——这是有意的设计决策:
- 用户最关心的是"我的资金不被偷",所以 PrivacyPool 不可升级;
- 如果电路要升级,部署新 PrivacyPool 合约 + 新 Verifier,旧用户继续用旧合约 withdraw(窗口 ≥ 6 个月);
- ComplianceOracle 用 OZ TransparentUpgradeableProxy(可升级),因为 SDN list / ASP 协议会演化。
v1.0 可考虑 Diamond Pattern:
- 把 deposit / withdraw / merkle tree 拆成 facets;
- 用户资金锁在 storage 中,facet 升级不影响存量;
- 风险:增加复杂度,需要额外审计。
关键洞察
可升级合约是隐私协议的隐性敌人。Tornado Cash v1 故意不可升级,这是它能够熬过 OFAC 制裁的关键——OFAC 不能让团队"加监管 backdoor",因为根本没有 backdoor。PriPool v0.1 的核心合约(PrivacyPool + Verifier)必须不可升级,所有合规逻辑放在 ComplianceOracle(可升级),让用户清楚知道:"你的资金安全 = 不可升级的合约 + 已审计电路;你的合规体验 = 可升级的 oracle"。这种两层架构是隐私+合规并存的工程关键。
第6章 合规层设计(ASP + KYT 集成)
6.1 ASP membership 设计 — 用户如何加入合规集合
ASP membership 是 被动的,不是主动的:
- 用户 deposit 到 PrivacyPool;
- ASP Aggregator 监听 Deposit event;
- ASP Aggregator 拉
tx.from地址的 KYT 评分; - 如果评分 < threshold(例:Chainalysis 风险分 < 5),把 commitment 加入 ASP 集合 A;
- 6 小时后,新的 ASP root 提交到链上。
用户不需要做任何额外操作就被加入。如果没有被加入:
- 用户可以在 frontend 看到"等待 ASP review"状态;
- 如果 ASP_A 拒绝,用户可以选择 ASP_B(不同 KYT provider);
- 如果所有 ASP 都拒绝,用户的资金仍在 pool 里,但无法 withdraw(因为没 ASP membership)。
重要:用户资金不会被 lock 死。v0.1 的 fallback 是 6 个月后开放"社区 ASP"(人工审核 + DAO 投票),处理被算法误伤的用户。
6.2 KYT 集成 — Chainalysis / TRM Labs API 对接
ASP Aggregator 后端架构:
# asp_aggregator/main.py
import asyncio
from web3 import Web3
from datetime import datetime
class ASPAggregator:
def __init__(self, asp_id: str, kyt_client, signer, oracle_addr):
self.asp_id = asp_id # e.g., "chainalysis-no-sdn"
self.kyt = kyt_client # Chainalysis or TRM client
self.signer = signer # multisig signer
self.oracle = w3.eth.contract(address=oracle_addr, abi=ORACLE_ABI)
self.commitments_in_asp = [] # ordered list
async def run(self):
last_block = self._load_state()
while True:
current = w3.eth.block_number
events = self.pool.events.Deposit().getLogs(
fromBlock=last_block, toBlock=current
)
for ev in events:
c = ev.args.commitment
depositor = ev.args.depositor
score = await self.kyt.get_risk_score(depositor)
if score < self.threshold():
self.commitments_in_asp.append(c)
else:
self._log_excluded(c, depositor, score)
# Build Merkle tree
new_root = self._build_merkle_root(self.commitments_in_asp)
# Submit on-chain
tx = self.oracle.functions.updateASPRoot(
self.asp_id, new_root
).build_transaction({"from": self.signer.address, ...})
signed = self.signer.sign_transaction(tx)
w3.eth.send_raw_transaction(signed.rawTransaction)
self._save_state(current)
last_block = current + 1
await asyncio.sleep(6 * 3600) # 6h cron
def threshold(self):
# Chainalysis risk score: 0 (safe) to 10 (sanctioned)
# 我们排除 score >= 5 (sanctioned exposure detected)
return 5
Chainalysis API 调用(real):
import requests
class ChainalysisClient:
BASE = "https://api.chainalysis.com/api/risk/v2"
def __init__(self, token):
self.token = token
def get_risk_score(self, address: str) -> int:
r = requests.post(
f"{self.BASE}/entities/{address}",
headers={"Token": self.token},
json={"address": address}
)
r.raise_for_status()
data = r.json()
return data.get("risk", {}).get("score", 0)
TRM Labs API:
class TRMLabsClient:
BASE = "https://api.trmlabs.com/public/v2/screening"
def get_risk_score(self, address: str) -> int:
r = requests.post(
f"{self.BASE}/addresses",
headers={"Authorization": f"Bearer {self.token}"},
json={"chain": "ethereum", "address": address}
)
return r.json()["addressRiskIndicators"]["riskIndicators"]["score"]
6.3 OFAC sanctioned 地址过滤
OFAC SDN list 通过另一个独立流程更新:
- 数据源:Treasury OFAC public XML feed(每周 1-2 次更新);
- 解析:proxy/ETL 拉 SDN list,过滤出 crypto address;
- 多签更新:通过
ComplianceOracle.addSDN(addr, reason)上链; - 响应时间:OFAC 公告后 24 小时内必须上链(合规要求)。
代码(简化):
def update_sdn_list():
sdn_xml = requests.get("https://www.treasury.gov/ofac/downloads/sdn.xml")
addrs = parse_crypto_addresses(sdn_xml.text)
on_chain_sdn = oracle.functions.getSDNList().call()
# add new
for addr in addrs:
if addr not in on_chain_sdn:
multisig.queueTransaction(
oracle.functions.addSDN(addr, "OFAC SDN").build_transaction(...)
)
# remove rescinded
for addr in on_chain_sdn:
if addr not in addrs:
multisig.queueTransaction(
oracle.functions.removeSDN(addr).build_transaction(...)
)
反思 1:硬编码 SDN list 在合约里有道德争议——它把美国法律逻辑写进了链上协议。v0.1 的妥协:默认开启 OFAC SDN 检查,但合约设计支持"按司法管辖区部署不同 instance"。例如:
pripool-us(默认 OFAC SDN);pripool-eu(默认 EU consolidated list);pripool-jp(默认 JFSA list);pripool-permissionless(无 SDN 检查,仅 ASP 层过滤);
反思 2:硬编码 SDN list 仍然不能完全防 OFAC 制裁本身。法律层面,OFAC 可能仍然制裁"协议"而非"工具"。我们做这一层只是为了:
- 司法可辩性("我们尽了合理努力");
- 用户教育(前端清楚告知"美国用户请使用 pripool-us");
- 监管对话(与 FinCEN / OFAC 协商时有谈判筹码)。
6.4 监管查询接口 — 司法机关如何在不破坏隐私的前提下查证
ThresholdEscrow.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract ThresholdEscrow {
address[] public custodians; // 3 个:team, regulator, auditor
uint8 public threshold = 2; // 2-of-3
struct DisclosureRequest {
bytes32 nullifierHash; // target tx
string reason; // legal basis (e.g., "subpoena #12345")
uint256 createdAt;
mapping(address => bool) approvals;
uint8 approvalCount;
bool revealed;
bytes encryptedKey; // encrypted by t-of-n
}
mapping(uint256 => DisclosureRequest) public requests;
uint256 public requestCount;
event DisclosureRequested(uint256 indexed id, bytes32 nullifierHash, string reason);
event DisclosureApproved(uint256 indexed id, address custodian);
event DisclosureRevealed(uint256 indexed id, bytes evidence);
function requestDisclosure(bytes32 nullifierHash, string calldata reason) external {
// anyone can request; custodians decide
uint256 id = requestCount++;
DisclosureRequest storage req = requests[id];
req.nullifierHash = nullifierHash;
req.reason = reason;
req.createdAt = block.timestamp;
emit DisclosureRequested(id, nullifierHash, reason);
}
function approve(uint256 id) external {
require(_isCustodian(msg.sender), "not custodian");
require(!requests[id].approvals[msg.sender], "already approved");
requests[id].approvals[msg.sender] = true;
requests[id].approvalCount++;
emit DisclosureApproved(id, msg.sender);
}
function reveal(uint256 id, bytes calldata evidence) external {
require(_isCustodian(msg.sender), "not custodian");
require(requests[id].approvalCount >= threshold, "below threshold");
requests[id].revealed = true;
emit DisclosureRevealed(id, evidence);
}
function _isCustodian(address a) internal view returns (bool) {
for (uint i = 0; i < custodians.length; i++) {
if (custodians[i] == a) return true;
}
return false;
}
}
off-chain flow:
- 法庭出 subpoena 给项目方,要求披露某 nullifierHash 对应的 depositor;
- 项目方提交
requestDisclosure(nullifierHash, "subpoena #12345"); - Custodians(team / regulator / auditor)审议;
- 2-of-3 批准;
- 项目方调用
reveal(),给出由 t-of-3 解密后的证据(depositor address + proof of mapping); - 链上 emit
DisclosureRevealedevent(透明审计)。
关键设计:
evidence不在合约逻辑中验证,仅作为 audit log;- 实际解密在链下用 MPC(threshold ECDSA / Shamir SSS)完成;
- 项目方必须保留每个 deposit 的 (depositor → commitment) 加密映射(otherwise 没东西可披露);
- 加密 key 由 t-of-3 持有,单独任何一方不能解密。
6.5 与 Privacy Pools v2 的具体差异
| 维度 | Privacy Pools v2 (0xbow) | PriPool v0.1 |
|---|---|---|
| ASP 数量 | 1 (Chainalysis) | 多(Chainalysis + TRM + community) |
| ASP 更新频率 | ~daily | 每 6 小时 |
| 链上 SDN 黑名单 | 无(依赖 ASP) | 有(双层过滤) |
| 司法授权解密 | 无 | t-of-3 ThresholdEscrow |
| Relayer 网络 | 单一 | 注册 + staking |
| 司法管辖区配置 | 单一全球版本 | 多 instance(us/eu/jp/permissionless) |
| 合约可升级 | Pool 不可升级,Oracle 可 | 同 |
| Token | 无 | 无(v1.0 引入治理 token) |
关键洞察
合规层设计是"产品工程"而不是"密码学问题"。0xbow 的 Privacy Pools v2 在密码学上做对了 90%,但合规层只做了 60%——单 ASP、无司法接口、无司法管辖区配置。PriPool v0.1 的最大差异化在合规栈的工程化:把 ASP 做成市场(多 provider 竞争)、把 SDN 做成双层(链上硬过滤 + 链下软过滤)、把司法授权做成协议组件(t-of-3 ThresholdEscrow)。这些都不是 paper-worthy 的创新,但它们让 PM 和法务可以在不改电路的情况下应对监管变化。真正能上线的产品,差异化在合规工程,不在 ZK 创新。
第7章 性能基准
7.1 Proof generation time
| 环境 | snarkjs (WASM, browser) | snarkjs (CLI, native) | rapidsnark (native) |
|---|---|---|---|
| Tornado Cash (~25k constraints) | 10s | 6s | 1.2s |
| PriPool (~9.5k constraints) | ~5s | ~3s | ~0.6s |
| Aztec Connect Note (~50k) | 25s | 15s | 3s |
测试机器:MacBook Pro M2 Max, 32GB RAM, Chrome 120。
优化方向:
- rapidsnark + WebAssembly: 浏览器端可达 ~1.5s(理论);
- WebGPU prover: 实验性,未来可达 ~0.5s;
- Server-side proving: 牺牲隐私(用户必须把 secret 发给 server)—— 不推荐;
- Note pre-computation: deposit 时预先计算部分 witness——可减 ~30% 时间;v0.2 路线。
7.2 On-chain verify gas
| 协议 | Verify gas | 总 withdraw gas | 备注 |
|---|---|---|---|
| Tornado Cash | ~230k | ~360k | 2 inputs (nullifier, root) bound |
| Privacy Pools v2 | ~280k | ~520k | + ASP root + ASP id |
| PriPool v0.1 | ~290k | ~480k | 优化了 SDN check(mapping 查询) |
| Aztec Connect | ~340k | ~500k | Multi-asset notes |
| Theoretical Groth16 floor | ~200k | ~280k | 2 inputs minimum |
差异分析:
- PriPool 比 Privacy Pools v2 少 40k,主要在去掉了 v2 的"ASP version check"(PriPool 用 nested mapping 直接查);
- 比 Tornado 多 ~120k,主要是 ASP 相关的两个 input + 合规检查。
7.3 Throughput estimate
L1 Ethereum 视角:
deposit gas = 1.1M
Each block (12s, 30M gas) supports ~27 deposits
→ ~9,720 deposits/hour theoretical max
→ Realistic: 100-500 deposits/day (current Tornado/PriPool scale)
L2 视角(Linea / Scroll / Base):
deposit gas same on L2
But L2 block 2-3s, 100M gas → 90 deposits/block → much higher TPS
+ Calldata cheap (after EIP-4844) → withdraw gas drops to ~150k
→ Realistic on L2: 50,000+ deposits/day
v0.1 部署目标:Sepolia → Base mainnet(以低 gas 起步),Ethereum L1(针对高净值 deposit)。
7.4 Storage growth — Merkle tree 的链上成本
每个 deposit 需要更新:
- 1 个 commitment slot (
commitments[c]): 20k gas (新 slot); - ~20 个 filledSubtrees slots(不全更新,平均 ~1.5): 30k gas;
- 1 个 root slot: 5k gas (重写已有);
总 storage cost / deposit ≈ ~55k gas (storage only) + ~600k (Poseidon hash) + ~50k (overhead) = ~700k 以上。我们引用的 1.1M 上限主要来自老合约的低效,新版优化后应可压到 800k。
长期 storage:
- Tree depth 20 → 1M deposits 后才 full;
- 假设 100 deposits/day → 27 年才 full;
- 但 historical roots 只保留 30 个 → 不会无限增长。
7.5 性能优化路径
v0.2: GPU prover
- 部署 cloud-based GPU prover(A100,~50ms proof time);
- 用户隐私牺牲 vs 性能:trusted server 持有 secret 是 dealbreaker;
- 改进:用 TEE(Intel SGX)让 server 可证明"我没看 secret"。
v0.3: Recursive proofs
- 一个 proof 包含 N 笔提现(recursive composition);
- 单笔 verify gas 从 290k → 50k;
- 但 proof generation 时间增加 ~3×;
- 适合 batch settlement 场景。
v1.0: PLONK + KZG (universal setup)
- 改 Groth16 为 PLONK:proof 大 5×(800 bytes vs 192)但 setup 通用;
- 电路升级不需要重做 ceremony;
- 代价:verify gas 从 290k → 350k。
关键洞察
性能 trade-off 的真相是"用户 pay for circuit complexity":每加一个 public input(如 ASP root)就增加 ~30k gas verify cost;每加 1000 约束就增加 ~200ms prove time。PriPool v0.1 的 9.5k 约束是精打细算的结果:去掉所有"为未来留空间"的 placeholder,砍掉非必要的 hash chain。实际产品建议:先用 Groth16 + Circom 跑通,等 TVL 达到 $50M 再考虑迁 PLONK——电路不变是最大的安全保证,迁移本身就是风险。
第8章 与 Tornado / Aztec / Privacy Pools v2 对比
8.1 5列对比表
| 维度 | Tornado Cash | Aztec Connect (旧) | Aztec Network (新) | Privacy Pools v2 | PriPool v0.1 |
|---|---|---|---|---|---|
| 隐私模型 | 全 pool k-anon | 全 pool + Notes | 私有 state + private fns | ASP 内 k-anon | ASP 内 k-anon (multi-ASP) |
| 架构 | L1 mixer | L2 (rollup) + Notes | L2 (Aztec L2) + Noir | L1 pool + ASP | L1/L2 pool + multi-ASP + escrow |
| 币种 | ETH + 个别 ERC20 | ETH + 多 ERC20 | ETH + ERC20 (private) | ETH | ETH + USDC |
| denomination | 0.1/1/10/100 ETH | 任意 (通过 Note) | 任意 | 任意 | 1 ETH (v0.1) |
| proof system | Groth16 | Plonk + Halo2 | Honk + Goblin Plonk | Groth16 | Groth16 |
| circuit DSL | Circom | Noir | Noir | Circom | Circom |
| trusted setup | MPC ceremony (1100+) | Universal (PLONK) | Universal | MPC ceremony | MPC ceremony (50+) |
| withdraw gas | ~360k | ~500k | varies (L2) | ~520k | ~480k |
| proof gen time | 10s (browser) | 30s | ~5s (recursive) | ~10s | ~5s |
| anonymity set | ~200k cumulative | ~50k notes | growing | ~5k | TBD |
| TVL (2026-12) | ~$80M | $0 (closed) | ~$80M (early) | ~$15M | $0 (design) |
| 合规 | 无 | 无 | opt-in compliance hooks | Chainalysis ASP | multi-ASP + SDN + escrow |
| judicial disclosure | 无 | 无 | 通过 selective disclosure | 无 | t-of-3 ThresholdEscrow |
| 关闭历史 | OFAC 制裁 (2022-08, 撤销 2025-03) | 主动关闭 (2024-03) | active | active | N/A |
| token economics | TORN (governance) | 无 | (planned) | 无 | 无 (v0.1) |
| 法律地位 | 仍在调查(Roman Storm 案) | 关闭 | compliance-friendly | OFAC 认可 | 多管辖区配置 |
| 代码 | 0x12D66f... (Tornado v1) | archived | aztec-protocol/aztec-packages | 0xbow.io | this paper |
8.2 各自的取舍分析
Tornado Cash
优点:
- 工程极简(< 500 LoC Solidity);
- 密码学正确(Poseidon、Merkle、Groth16);
- ~25k 约束已经是 mixer 的极限优化。
致命缺陷:
- 0 合规层 → 被 OFAC 制裁;
- denomination 固定(0.1/1/10/100 ETH),用户体验差;
- 无 Relayer 经济激励 → 后期 Relayer 跑路。
结论:Tornado v1 是隐私协议的"教科书 + 反面教材"。技术正确,产品错误。
Aztec Connect (旧)
优点:
- 比 Tornado 灵活:Notes 模型支持任意金额;
- L2 设计降低 gas(理论 $0.5/withdraw)。
致命缺陷:
- 法律风险无法解决 → 团队主动关闭;
- Note 概念用户难懂;
- 流动性碎片化(每个币种独立 anonymity set)。
结论:Aztec Connect 比 Tornado 更先进,但主动选择关闭比被监管制裁更体面——这是 Web3 团队的法律智慧。
Aztec Network (新)
优点:
- 完整的 private smart contract 平台;
- compliance-by-design(合约可定义 hooks);
- Noir 比 Circom 友好(Rust-like,类型系统强)。
风险:
- L2 + 全新 VM = 攻击面大;
- 早期 TVL 小,anonymity set 弱;
- 监管对 L2 隐私 chain 的态度未明。
结论:Aztec 是隐私协议的"长期方向"——通用 private computing。但短期 (2026-2028) 仍是 niche product。
Privacy Pools v2 (0xbow)
优点:
- Vitalik 学术加持;
- Chainalysis 集成 → 监管对话顺畅;
- 完全沿用 Tornado 的密码学,工程风险低。
缺陷:
- 单 ASP(Chainalysis)—— single point of censorship;
- 无司法授权通道(subpoena 无解);
- 单一 instance(无司法管辖区配置);
- TVL 小($15M)—— anonymity set 弱。
结论:0xbow 是"概念验证",证明 Privacy Pools v2 paper 可行。但要做生产级产品,需要 PriPool 这样的合规栈工程化。
8.3 我们的差异化定位
PriPool v0.1 的差异化定位是:
"Privacy Pools v2 + 合规工程化" ——不是密码学创新,是产品工程创新。
具体差异:
- 多 ASP 市场 vs 单 ASP(去 single point of censorship);
- 双层 SDN 过滤 vs 仅 ASP(应对 OFAC 直接制裁);
- 司法授权解密 vs 无(应对 subpoena/court order);
- 多司法管辖区配置 vs 单一 instance(让美国/欧盟/日本等独立部署);
- Relayer 网络(v0.2 staking) vs 无 Relayer 网络。
目标用户:
- 高净值个人(HNWI)做合规隐私转账;
- DAO treasury 做隐私 grant 发放;
- 受监管 entity(broker-dealer / VASP)做客户隐私服务;
- 不是黑灰产、不是隐私至上主义者(这些用户应该用 Monero / Zcash)。
关键洞察
隐私协议的"产品-市场契合"在 2025 年后已经彻底改变。在 2020-2022,隐私协议的目标是"反审查、反监视",TVL 增长靠"投机 + 黑灰产"。在 2025 年后的监管环境,这些 PMF 都死了。新的 PMF 是"合规友好 + 商业刚需":HNWI 不想竞争对手看到自己的 swap,DAO 不想员工知道彼此的薪酬,broker 不想给客户暴露在区块浏览器。PriPool v0.1 不和 Tornado 竞争"完全隐私"市场(已死),而是和银行内部转账系统竞争"商业隐私"市场(待开发)——这是 Privacy Pools v2 路线的真正机会。
第9章 安全审计 checklist
9.1 ZK 常见漏洞清单
| # | 漏洞类型 | 描述 | 实例 |
|---|---|---|---|
| 1 | Under-constrained signal | signal 未充分约束,prover 可填任意值 | Nova Tornado fork: nullifierHash 未 enforce |
| 2 | Aliasing | 不同 witness 产生相同 commitment | hash function 选择不当 |
| 3 | Generator manipulation | 攻击者用特殊点绕过 pairing | bn254 subgroup attack |
| 4 | Trusted setup compromise | toxic waste 泄露 → 伪造任何 proof | Tornado v0 (no ceremony) 不安全 |
| 5 | Public input mismatch | 链上检查的 input 与电路输入不一致 | input ordering 错误 |
| 6 | Field overflow | input > FIELD_SIZE | 未检查 commitment ∈ F |
| 7 | Path indices not constrained as bits | pathIndices 不是 0/1 → 树验证可绕过 | 没用 Switcher 强约束 |
| 8 | Hash function choice | 用 keccak in-circuit 太贵,但 Poseidon 不在所有 hash 域安全 | 选择 Poseidon for SNARK |
| 9 | Replay across chains | 同 nullifierHash 在多链复用 | 跨链桥需加 chainId |
| 10 | Non-determinism in witness | witness 计算依赖外部状态 → testnet/mainnet 不一致 | block.timestamp in circuit |
9.2 Solidity 常见漏洞清单
| # | 漏洞类型 | 描述 |
|---|---|---|
| 11 | Reentrancy in withdraw | recipient.call 前未更新状态 |
| 12 | Front-running on deposit | 允许 commitment 被覆盖 |
| 13 | Integer overflow | leafIndex / fee 可溢出 |
| 14 | Unchecked external call | hasher.poseidon 可能 revert |
| 15 | Unauthorized admin | onlyOwner 缺失或 admin key 被盗 |
| 16 | Storage collision | proxy upgrade 未对齐 storage layout |
| 17 | Unverified Verifier | 未确认 verifier 是 snarkjs 自动生成、未篡改 |
| 18 | Gas griefing | malicious recipient 在 fallback 中消耗 gas |
| 19 | Tree underflow | nextLeafIndex == 0 时减 1 → 巨大数 |
| 20 | Clone-and-call attack | 不同 instance 共享 nullifier mapping |
9.3 自查 checklist (30+ items)
电路(Circom):
- 所有 public input 都在电路中至少出现一次(ideally with
===或*约束); -
<--仅用于辅助赋值,每处都有===跟进; - hash function 输入数量与 R1CS 输出匹配;
- Merkle tree depth 与 Solidity 端常量一致;
- pathIndices 通过 Switcher / Num2Bits 强约束为 bit;
- commitment / nullifierHash 检查
< FIELD_SIZE; - 每个 public input 有 squared binding(如适用)。
Setup ceremony:
- Phase 1 用社区认可的 ptau(如 Hermez 或 Perpetual Powers of Tau);
- Phase 2 ≥ 50 contributors,全部公开 attestation;
- Final beacon 使用 verifiable randomness(Drand / Ethereum block hash);
- 所有中间 zkey 公开,可由第三方重 verify;
- toxic waste 销毁过程录屏存档。
合约(Solidity):
- PrivacyPool 不可升级(无 admin key for funds);
- withdraw 使用 checks-effects-interactions;
- 所有外部 call 用
.call{value: x}("")+ check return; - reentrancy guard(即使逻辑无重入,加一层保险);
- commitment / nullifierHash 检查
< FIELD_SIZE(链上重复检查); - root history 大小够(≥ 30 个);
- Verifier.sol 与 zkey 哈希对齐(CI 中验证);
- ComplianceOracle 用 multisig(≥ 3-of-5);
- SDN list 更新有 timelock(24h);
- Relayer 注册有 staking 防垃圾。
前端 / SDK:
- 用户 secret 仅在 client 生成(never sent to server);
- secret 加密存储(IndexedDB + passphrase);
- note 文件下载有清晰提示(备份重要性);
- proof 生成在 WebWorker(不阻塞 UI);
- withdraw 前再次检查 nullifierHash 未使用(防止用户重复提交);
- frontend 不请求用户 secret 或 nullifier 给 server。
合规:
- OFAC SDN 数据源每周自动同步;
- ASP API key 单独 vault(Azure KV / AWS Secrets Manager);
- ASP 多签 sign 后才更新 root;
- disclosure request 全部链上 emit event(透明审计);
- 合规事件日志保留 ≥ 7 年(监管要求)。
9.4 推荐的审计公司
| 公司 | 强项 | 大致预算 (5k LoC) |
|---|---|---|
| Trail of Bits | Solidity + Circom 全栈 | $150k - $250k |
| OpenZeppelin | Solidity + 经济安全 | $100k - $200k |
| ChainSecurity | EVM 形式化分析 | $80k - $150k |
| Veridise | ZK circuit specialty | $80k - $150k |
| Spearbit | Crowdsourced expert | $50k - $100k |
| Code4rena | 比赛式审计 | $50k - $150k (prize pool) |
v0.1 建议路线:
- 内部 review(团队自查 + AI 辅助 like Slither / Mythril)— 4 周;
- Veridise ZK 电路专项审计 — 6 周,$100k;
- Trail of Bits 全栈审计 — 8 周,$200k;
- Code4rena 公开赛事 — 4 周,$80k 奖金;
- Bug bounty 持续运营 — Immunefi,$500k bounty pool。
关键洞察
ZK 协议的安全审计成本 = 普通 DeFi 的 2-3 倍。原因:(1) ZK 漏洞极其隐蔽,under-constrained 不会让 verify 失败但会让攻击者赚钱;(2) 审计师需要既懂 Solidity 又懂 Circom,全球能做这事的工程师不到 100 人;(3) trusted setup ceremony 本身需要审计。给 PM 的现实建议:预算 $400k-$800k 做安全(含两轮独立审计 + 公开 contest),低于这个数的项目基本会死在主网(Euler / Nova / Curve 都是反例)。隐私协议作为 high-value target,安全预算应该是 v0.1 总预算的 30-40%。
第10章 部署与运维
10.1 Sepolia testnet 部署
部署顺序:
# 1. Deploy Poseidon precompiled hasher
npx hardhat run scripts/deploy_poseidon.ts --network sepolia
# → POSEIDON_ADDR
# 2. Deploy Verifier (auto-generated from snarkjs)
npx hardhat run scripts/deploy_verifier.ts --network sepolia
# → VERIFIER_ADDR
# 3. Deploy ComplianceOracle with multisig admin
npx hardhat run scripts/deploy_oracle.ts --network sepolia \
--admin 0xMULTISIG
# → ORACLE_ADDR
# 4. Deploy PrivacyPool
npx hardhat run scripts/deploy_pool.ts --network sepolia \
--verifier $VERIFIER_ADDR \
--oracle $ORACLE_ADDR \
--hasher $POSEIDON_ADDR
# → POOL_ADDR
# 5. Register ASP providers
npx hardhat run scripts/register_asp.ts --network sepolia \
--asp-id chainalysis-no-sdn \
--provider $CHAINALYSIS_AGGREGATOR_ADDR
# 6. Verify all on Etherscan
npx hardhat verify $POOL_ADDR $VERIFIER_ADDR $ORACLE_ADDR $POSEIDON_ADDR --network sepolia
Sepolia testing 阶段(≥ 8 周):
- 公开测试:邀请 100+ 用户做 deposit / withdraw;
- Bug bounty:$50k pool;
- 性能监控:Prove 时间、gas 实测分布;
- ASP 集成测试:Chainalysis sandbox 集成;
- 司法 disclosure 模拟:跑完整 t-of-3 流程。
10.2 主网上线 checklist
┌─────────────────────────────────────────────────────────┐
│ 主网上线前 30 天 │
├─────────────────────────────────────────────────────────┤
│ [ ] 全部审计报告关闭 (high/critical 都已修复) │
│ [ ] Trusted setup ceremony 完成 + transcript 公开 │
│ [ ] Sepolia 8 周无重大 bug │
│ [ ] Bug bounty 公开 ≥ 4 周无 critical 报告 │
│ [ ] 监管对话:FinCEN / OFAC / 当地 regulator │
│ [ ] 法务 sign-off:MTL / MSB licensure 评估 │
│ [ ] 多签设置:3-of-5 团队 + 2 外部 custodian │
│ [ ] Relayer 网络:≥ 5 个独立 relayer 准备 │
│ [ ] ASP 网络:≥ 2 个 ASP provider 接入 │
│ [ ] 应急响应:on-call rotation 24/7 │
│ [ ] 监控:Tenderly + Forta + 自定义告警 │
│ [ ] 文档:用户 guide / API doc / FAQ │
│ [ ] 前端:CDN + DDoS 防护 (Cloudflare) │
│ [ ] 法律 disclaimer:明确司法管辖区限制 │
└─────────────────────────────────────────────────────────┘
10.3 Relayer 网络初始化
v0.1 引导阶段:
- 团队运营 3 个 Relayer(不同 region:US-East, EU, Singapore);
- 公开邀请 5 个独立运营者(社区 / partner);
- 每个 Relayer 必须 stake ≥ 5 ETH;
- 每个 Relayer 每月分润:fee × 90%(10% 给协议 treasury)。
Relayer server 架构:
┌──────────────────────┐
│ User (Browser) │
│ - generate proof │
└──────────┬───────────┘
│ POST /relay (https)
▼
┌──────────────────────┐
│ Cloudflare WAF │
│ - rate limit │
│ - DDoS │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Relayer API Server │
│ - validate proof │
│ - simulate gas │
│ - check fee │
│ - submit to mempool │
└──────────┬───────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Geth/Reth │ │ Flashbots │ │ Mev-Boost │
│ (private │ │ (priority │ │ (bundle │
│ mempool) │ │ tx) │ │ protect) │
└────────────┘ └────────────┘ └────────────┘
Relayer 经济模型:
- 用户支付 fee(在 proof 中 bind);
- Relayer 拿到 fee - gas cost 利润;
- 失败 tx 不收费(用户体验保证);
- 健康度由 ComplianceOracle 跟踪(successCount / failureCount)。
10.4 监控与告警
链上监控(Tenderly + Forta):
# tenderly_alerts.yaml
- name: "Large withdraw"
condition: "Withdraw event with fee > 0.1 ETH"
severity: warning
actions: [slack, pagerduty]
- name: "SDN address attempted"
condition: "Tx revert with reason 'depositor sanctioned'"
severity: warning
actions: [slack]
- name: "Verifier failure"
condition: "Tx revert with reason 'invalid proof'"
severity: high
actions: [slack, pagerduty]
threshold: "3 in 1 hour"
- name: "ASP root not updated"
condition: "ComplianceOracle.ASPUpdated event not emitted in 12h"
severity: high
actions: [pagerduty]
- name: "Relayer failure spike"
condition: "Relayer.failureCount increase by 10 in 1h"
severity: warning
actions: [slack]
链下监控(Datadog / Grafana):
- ASP Aggregator 健康(last run timestamp、KYT API success rate);
- Relayer server 健康(CPU、memory、tx submit rate);
- Frontend 错误率(Sentry);
- 用户 funnel(deposit → withdraw 比例 / 时间分布)。
10.5 应急响应
Pause 机制(不在 v0.1 设计——故意的):
- PrivacyPool 不可 pause,因为 pause 等于 admin 可锁用户资金;
- 紧急情况下唯一手段:ComplianceOracle 拒绝所有 ASP root,但用户可以用历史 root 继续 withdraw(30 root 窗口);
- 长期:v0.2 可考虑 7-day-timelock pause(用户有窗口 withdraw 后才生效)。
Migration path:
- 部署新 PrivacyPool v2(修复 bug);
- 旧 v1 仍可 withdraw(不 pause);
- 前端引导新用户 deposit 到 v2;
- 老用户 6 个月窗口 withdraw 老 v1;
- 6 个月后 v1 frontend 关闭,但合约仍在链上(用户可手动调用)。
Incident response runbook:
T+0: 检测到事件(Forta 告警 / 用户报告)
T+15m: on-call engineer 确认
T+30m: CEO + CTO + 法务 conf call
T+1h: 决定行动方案(pause oracle / disclose / migrate)
T+2h: 内部公告(Slack / status page)
T+4h: 公开公告(Twitter / Discord / blog)
T+12h: 详细 post-mortem 草稿
T+24h: 公开 post-mortem
T+1w: 修复 PR + 第三方 review
T+2w: 修复部署
T+1m: 复盘会议
关键洞察
运维(ops)是隐私协议最被低估的工程领域。Tornado Cash 在 2022 年被 OFAC 制裁后,前端 IPFS 站点被运营者主动关闭——这本身是合规动作,但用户被锁在合约里 6 个月才能找到 alternative frontend。PriPool v0.1 的运维设计核心:(1) 前端是开源的多 mirror(不是单一站点),(2) 合约不可 pause(用户资金不依赖团队存活),(3) on-chain disclosure 透明(任何司法授权都 emit event)。这些都不是密码学问题,是工程纪律和法务设计——PM 必须在 day-1 就把"如果团队消失 / 被起诉 / 服务器下线"作为正常运营场景规划。
第11章 路线图
11.1 v0.2 — 多链支持
时间:v0.1 主网后 6 个月
目标:支持 Ethereum L1 + Base + Arbitrum + Linea 同时运行。
关键设计:
- 每条链独立部署 PrivacyPool(不是 cross-chain pool);
- ComplianceOracle 共享(同一组 ASP roots,多链 sync);
- Cross-chain Relayer 网络(用户在 L1 deposit,可在 L2 withdraw);
- 引入 chainId 到 nullifierHash(防跨链 replay):
nullifierHash <== Poseidon(nullifier, chainId)
挑战:
- ASP root sync 延迟(L1 → L2 通常 ~10 分钟);
- Cross-chain proof 复用:要么重新 prove,要么用 recursive;
- L2 安全模型差异(rollup 可能 reorg)。
11.2 v0.3 — Native cross-chain privacy
时间:v0.2 之后 6 个月
目标:用户在 L1 deposit ETH,直接在 L2 withdraw 不同代币(USDC)。
关键技术:
- LayerZero / Chainlink CCIP 跨链消息;
- 在跨链消息中嵌入 ZK proof;
- swap 在 L2 用 Uniswap pool 完成;
- 用户视角:单一 deposit → 单一 withdraw,跨链对用户透明。
风险:
- 跨链桥本身的安全风险(LayerZero / CCIP 没破过,但是攻击面);
- Swap 滑点对隐私的副作用(amount 变化使 anonymity set 变小)。
11.3 v1.0 — 完整合规接入 + 监管沙盒
时间:v0.3 之后 12 个月
目标:
- 加入英国 FCA Sandbox / 新加坡 MAS Project Guardian / 日本 FSA 白名单;
- 引入治理 token(PRI),DAO 管理 ComplianceOracle;
- 可配置 instance:us / eu / jp / permissionless;
- Native fiat on-ramp(与受监管 entity 集成)。
关键里程碑:
- ≥ 3 个司法管辖区监管对话进入正式阶段;
- TVL ≥ $200M(足以做 anonymity set);
- Integration with Coinbase / Kraken / Binance 等 CEX。
11.4 5 个未解决问题
Q1: ASP captures by single provider
如果 99% 用户都选 Chainalysis ASP,事实上变成 single point of censorship。
v0.1 缓解:默认 UI 推荐 ASP 多选(用户看到 3 个选项,无法默认)。
长期:未解。可能需要"ASP 联邦"——多个 ASP 的并集自动作为 mega-set。
Q2: Quantum threat to BN254
bn254 曲线在 ~2030 年量子计算机出现后可能被破。
长期方案:迁移到 bn381 (PLONK over bls12-381) 或 STARK;电路重写。
Q3: Privacy 与 trade flow analysis
即使 transaction 隐私,用户的整体交易模式(deposit 时间、金额分布)仍可被关联分析。
缓解:denomination 标准化、deposit 延迟随机化;但根本上无解。
Q4: Relayer 中心化
v0.1 是 5 个 Relayer。如果都被强制下线,用户必须自己发 tx,暴露 EOA。
v0.2 计划:去中心化 Relayer 网络 + on-chain MEV protection(如 Skip Protocol)。
Q5: 法律人格
合约不是 person,但合约的开发者是。开发者的法律风险无法用密码学解决。
最佳实践:开发者实体 → DAO(Cayman Islands / Switzerland)→ 全部代码 open source → 不接受 commercial fee → 最大化 "First Amendment" / "speech" 抗辩。
关键洞察
路线图最重要的不是"做什么",是"不做什么"。v0.1 路线图明确不做 token、不做 cross-chain、不做 multi-asset、不做 governance——这些都是后期的事。为什么?因为隐私协议的早期 risk-reward 极不对称:一次 critical bug 等于整个项目死亡(参考 Nova、Euler、Mango),所以 v0.1 必须把表面积压到最小。PM 应该把 v0.1 当作"演示版",目标是证明合规栈工作——TVL 不是目标,跑通"deposit → ASP review → judicial disclosure → withdraw"完整循环才是目标。早期克制是隐私协议的最大美德。
附录 A 完整源码索引
A.1 Circom 文件结构
circuits/
├── deposit.circom # commitment hasher (helper)
├── withdraw.circom # main proof circuit
├── asp_membership.circom # standalone ASP attestation
├── lib/
│ ├── merkle.circom # MerkleTreeChecker
│ ├── poseidon.circom # (re-export from circomlib)
│ └── switcher.circom # (re-export from circomlib)
└── build/
├── withdraw.r1cs
├── withdraw.wasm
├── withdraw.sym
└── withdraw_final.zkey
A.2 Solidity 合约文件结构
contracts/
├── PrivacyPool.sol # core deposit/withdraw
├── Verifier.sol # auto-generated from snarkjs
├── ComplianceOracle.sol # ASP roots + SDN + relayer registry
├── RelayerRegistryV2.sol # (v0.2 only) staking + slashing
├── ThresholdEscrow.sol # judicial disclosure
├── interfaces/
│ ├── IPoseidon.sol
│ ├── IGroth16Verifier.sol
│ └── IComplianceOracle.sol
└── libraries/
├── MerkleTreeWithHistory.sol
└── SafeTransfer.sol
A.3 Frontend TypeScript 文件结构
frontend/
├── pages/
│ ├── index.tsx # landing
│ ├── deposit.tsx
│ └── withdraw.tsx
├── lib/
│ ├── crypto/
│ │ ├── identity.ts # gen (n, s)
│ │ ├── commitment.ts # Poseidon hash
│ │ └── note.ts # encrypted note format
│ ├── zk/
│ │ ├── prover.ts # snarkjs WebWorker wrapper
│ │ ├── tree.ts # local Merkle tree builder
│ │ └── circuit.ts # circuit constants
│ ├── chain/
│ │ ├── pool.ts # PrivacyPool calls
│ │ ├── oracle.ts # ComplianceOracle reads
│ │ └── relayer.ts # POST to Relayer API
│ └── compliance/
│ ├── asp.ts # ASP selector logic
│ └── sdn.ts # SDN check
├── workers/
│ └── prover.worker.ts # Web Worker for proof gen
└── components/
├── DepositForm.tsx
├── WithdrawForm.tsx
├── ASPSelector.tsx
└── NoteBackup.tsx
A.4 Backend / off-chain stack
backend/
├── asp_aggregator/
│ ├── main.py # cron: poll events, score, submit root
│ ├── chainalysis.py # KYT client
│ ├── trm_labs.py # KYT client
│ └── merkle.py # Merkle tree builder
├── sdn_updater/
│ ├── main.py # OFAC XML parser + multisig submit
│ └── ofac.py # SDN list fetcher
├── relayer_server/
│ ├── api.ts # Express / Fastify endpoints
│ ├── validator.ts # proof + gas validation
│ └── submitter.ts # tx submitter (Flashbots)
└── threshold_escrow/
├── coordinator.py # MPC coordinator
└── shares.py # Shamir SSS for keys
附录 B 关键安全考量
B.1 ZK setup ceremony 要求
| 要求 | 说明 |
|---|---|
| Phase 1 ptau | 复用 Hermez Powers of Tau (≤ 2^15) |
| Phase 2 contributors | ≥ 50 名公开身份的贡献者 |
| Beacon | Drand round 或 Ethereum block hash (chosen 7 days in advance) |
| Transcript | 全部公开在 GitHub + IPFS pin |
| Audit | 由独立 ZK 审计公司 verify ceremony |
| Timeline | ≥ 4 周 (50 contributors × ~30 min each + verification) |
B.2 Relayer 中心化风险
| 风险 | 缓解 |
|---|---|
| 单一 Relayer 拒绝服务 | 用户可选其他 Relayer |
| 所有 Relayer 被强制下线 | 用户可自行发 tx (失去隐私但不失去资金) |
| Relayer 操纵 nonce 延迟广播 | 用户可设 timeout 后切换 Relayer |
| Relayer 与 ASP 串谋 | 多 Relayer + 多 ASP 降低概率 |
B.3 Merkle tree 状态管理
| 场景 | 处理 |
|---|---|
| 用户离线 ≥ 30 个 deposit | 当前 root 已不可用,用户必须 wait for new deposit 或重新生成 path |
| Pool tree 接近 full (1M deposits) | 部署新 v2 pool,老 pool 继续 withdraw |
| Filled subtrees corruption | 用户可从 events log 重建 tree(开源 library) |
| Off-chain index out of sync | 用户从 events log re-sync(参考 tornado-cli) |
附录 C 推荐阅读
论文
| 标题 | 作者 | 年 | 链接 |
|---|---|---|---|
| Blockchain Privacy and Regulatory Compliance: Towards a Practical Equilibrium | Buterin, Illum, Nadler, Schär, Soleimani | 2023 | SSRN/arxiv |
| Tornado Cash Privacy Solution | Pertsev, Storm, Semenov | 2019 | tornado.cash whitepaper |
| Aztec Protocol whitepaper | Williamson, Andrews, Cooper | 2020 | aztec.network |
| Plonk: Permutations over Lagrange-bases | Gabizon, Williamson, Ciobotaru | 2019 | eprint 2019/953 |
| Groth16: On the Size of Pairing-based Non-interactive Arguments | Jens Groth | 2016 | eprint 2016/260 |
| Halo: Recursive Proof Composition without a Trusted Setup | Bowe, Grigg, Hopwood | 2019 | eprint 2019/1021 |
| zk-SNARKs: Backbone of Crypto-economic Privacy | Dragan Boscovic | 2022 | arxiv 2202.01024 |
工具
| 工具 | 用途 | 链接 |
|---|---|---|
| Circom | ZK DSL | github.com/iden3/circom |
| snarkjs | JS prover/verifier | github.com/iden3/snarkjs |
| circomlib | helper templates | github.com/iden3/circomlib |
| circomlibjs | JS helpers (Poseidon etc.) | github.com/iden3/circomlibjs |
| rapidsnark | Native fast prover | github.com/iden3/rapidsnark |
| @semaphore-protocol/identity | Identity gen | github.com/semaphore-protocol |
| IncrementalMerkleTree | Off-chain tree | github.com/privacy-scaling-explorations/zk-kit |
| Hardhat | Solidity dev env | hardhat.org |
| Foundry | Solidity dev env (faster) | github.com/foundry-rs |
| Slither | Solidity static analyzer | github.com/crytic/slither |
| Tenderly | Tx simulation + monitoring | tenderly.co |
| Forta | Real-time on-chain monitoring | forta.network |
审计报告
| 协议 | 审计 | 备注 |
|---|---|---|
| Tornado Cash v1 | ABDK Consulting | 2019, 公开 |
| Aztec Connect | Trail of Bits + ConsenSys Diligence | 2021 |
| Privacy Pools v2 (0xbow) | ChainSecurity | 2024 |
| Semaphore | Veridise | 2023 |
| zkSync Era | OpenZeppelin + Spearbit | 2023 |
| Scroll zkEVM | Trail of Bits + KALOS | 2023 |
法律资源
| 资源 | 内容 |
|---|---|
| Coin Center Privacy Pool brief | Privacy as protected speech |
| Van Loon v. Treasury (5th Cir, 2024) | OFAC over-reach on Tornado |
| FATF Crypto Travel Rule (2019) | KYT requirements |
| EU MiCA Regulation (2024) | EU crypto framework |
| FinCEN Guidance on Anonymous CVCs | US AML requirements |
致谢与免责声明
本文是作者在 Phase 4(ZK 工程实战)的综合产出,参考了:
- Vitalik Buterin et al. 2023 Privacy Pools v2 论文;
- Tornado Cash v1 开源代码;
- Aztec Network 文档与 Noir 教程;
- Semaphore / WorldID / Sismo 协议设计;
- Privacy & Scaling Explorations (PSE) 团队的 ZK research。
本文是教学性质的设计文档,不是生产代码。任何在主网部署的隐私协议都需要:
- 完整的 trusted setup ceremony(≥ 50 contributors);
- ≥ 2 家独立审计公司的报告;
- 法律 sign-off(针对每个司法管辖区);
- 持续 bug bounty + monitoring。
作者不对任何基于本文设计的实现负责。隐私协议的开发涉及法律风险(参见 Tornado Cash 案例),请咨询专业律师。
版本历史
| 版本 | 日期 | 变更 |
|---|---|---|
| v0.1 | 2027-01-19 | 初始发布 |
联系:本文为作者 Phase 4 学习产出,非商业项目。如有学术或工程讨论意向,请通过 GitHub issue / Twitter 联络。
End of Document