返回 Papers
白皮书

ZK 隐私交易系统设计

- 第1章 项目概览

2,148ZK_PRIVACY_TRANSACTION_SYSTEM.md

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 框架,并做以下补强:

  1. k-anonymity within compliant set — 用户在"合规子集"内匿名(而非整个 pool);
  2. 多 ASP(Association Set Provider)共存 — 用户可选择不同 ASP,避免 single point of censorship;
  3. 链上+链下双 KYT 层 — 链上做硬性 SDN 过滤,链下做软性风险评分;
  4. 司法授权解密通道 — 通过 t-of-n threshold escrow 应对法律传票;
  5. Relayer 中心化但不可篡改 — Relayer 可中继 tx 但不能挪用资金(recipient/fee 全部 bind 到 proof)。

1.3 与 Tornado / Aztec / Privacy Pools v2 的差异

维度Tornado CashAztec Connect (旧)Privacy Pools v2 (0xbow)PriPool v0.1(本文)
隐私模型全 pool k-anon全 pool k-anonASP 内 k-anonASP 内 k-anon + 多 ASP
合规Chainalysis ASPChainalysis + TRM + 自定义
withdraw gas~360k~500k (含 Note)~520k~480k(目标)
司法授权t-of-3 threshold escrow
TokenTORNAZTEC(计划)无(v0.1)
支持币种ETH + 个别 ERC20ETH + 多 ERC20ETHETH + USDC(v0.1)
代码状态已被 fork关闭主网本文设计

1.4 系统目标(可量化)

v0.1 设计目标:

目标数值说明
Withdraw gas< 500k优于 Privacy Pools v2,近 Tornado
Proof generation (browser)< 8 secsnarkjs WebWorker,目标用户体验
Proof size (Groth16)192 bytesGroth16 标准 (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

问题:

  1. 黑名单需要主动更新(运营负担);
  2. 谁来更新?谁来仲裁?中心化点;
  3. 链上更新成本高(每加一个地址 ~50k gas);
  4. 用户被错误加入黑名单后申诉困难。

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)

优点:

  1. 默认拒绝(white-list)比默认接受(black-list)安全得多;
  2. 多 ASP 竞争 — 用户可选择 Chainalysis、TRM、Elliptic、社区 DAO 等;
  3. 监管可要求受监管 entity 只接受特定 ASP;
  4. 链上只需存一个 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 时检查 recipientrelayer
  • 更新频率: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)            -- 防双花

这个设计的产品意义在于:

  1. 用户主动选择合规 — ASP 是 opt-in(不强制),但选择"非主流 ASP"会使 anonymity set 变小,本身就是一种自我惩罚;
  2. 监管获得 visibility — 监管不知道 user A 是哪笔 deposit,但知道 user A 选择了哪个 ASP(这等于声明合规属性);
  3. 去中心化 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 Proverwitness 生成、proof 生成snarkjs (WASM), WebWorker
Relayer Server中继 tx、收 feeNode.js, ethers.js, Redis (replay protection)
PrivacyPool.sol核心存取、Merkle treeSolidity 0.8.20+
Verifier.solGroth16 verifiersnarkjs auto-generated
ComplianceOracle.solASP roots + SDN blacklistSolidity, multisig admin
RelayerRegistry.solRelayer 注册、stakingSolidity
ThresholdEscrow.sol司法授权解密Solidity + off-chain MPC
ASP Aggregator拉链上数据 + 调 KYT API + 发布 rootPython, requests, gnosis-py

3.3 信任模型 — 谁信任谁,何时信任

信任关系强度备注
用户 → ZK 电路强(密码学)假设 trusted setup ceremony 至少 1 人诚实
用户 → PrivacyPool 合约已审计 + 不可升级(无 admin key for funds)
用户 → ComplianceOracleadmin 可改 SDN list & 接受 ASP,但不能动用户资金
用户 → ASP(如 Chainalysis)用户选择ASP,可换
用户 → Relayerrecipient/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 秒。所以减约束数等于减用户等待时间

实践原则:

  1. Hash 用 Poseidon,不用 keccak/SHA。Poseidon 在 BN254 上每 hash ~250 约束,keccak ~150k 约束。
  2. Merkle tree 深度刚好够用。深度 20 = 1M 叶子(够 5-10 年用),深度 32 = 4G 叶子(浪费)。
  3. public input 越少越好。每个 public input 在 verifier 里都是一个 G1 scalar mul(~ 30 约束 + 5500 gas)。
  4. 避免动态分支。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);

几个关键点的说明:

  1. hasher.nullifierHash === nullifierHash — 这一行是 Day 227 提到的 Nova-style 漏洞防护点。如果写漏,prover 可以提交任意 nullifierHash。
  2. 两个独立的 Merkle tree 验证 — Pool tree (depth 20) 和 ASP tree (depth 16) 是不同的树。POOL 全集 ≥ ASP 子集,所以 ASP 树更小。
  3. aspId 也 bind 到 proof — 这样链上合约可以检查 aspRootaspId 对应(防止用户拿 ASP_A 的 root 但声明 ASP_B)。
  4. 平方约束(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 最新):

子电路constraintslinear constraintsnon-linear
Poseidon(2)~20050150
Poseidon(1)~15040110
Switcher422
MerkleTreeChecker(20)~4,200~1,000~3,200
MerkleTreeChecker(16)~3,400~800~2,600
Withdraw(20, 16)~9,500~2,500~7,000

优化路线:

  1. 改用 Poseidon2(2024 年新版 Poseidon):约束数减少 ~30% → 总约束 ~6,500;
  2. 改 hash 为 RescuePrime / Anemoi:约束数减少 ~40%,但 EVM gas 高(不适合 deposit on-chain hash);
  3. 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 是 被动的,不是主动的:

  1. 用户 deposit 到 PrivacyPool;
  2. ASP Aggregator 监听 Deposit event;
  3. ASP Aggregator 拉 tx.from 地址的 KYT 评分;
  4. 如果评分 < threshold(例:Chainalysis 风险分 < 5),把 commitment 加入 ASP 集合 A;
  5. 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 通过另一个独立流程更新:

  1. 数据源:Treasury OFAC public XML feed(每周 1-2 次更新);
  2. 解析:proxy/ETL 拉 SDN list,过滤出 crypto address;
  3. 多签更新:通过 ComplianceOracle.addSDN(addr, reason) 上链;
  4. 响应时间: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

  1. 法庭出 subpoena 给项目方,要求披露某 nullifierHash 对应的 depositor;
  2. 项目方提交 requestDisclosure(nullifierHash, "subpoena #12345")
  3. Custodians(team / regulator / auditor)审议;
  4. 2-of-3 批准;
  5. 项目方调用 reveal(),给出由 t-of-3 解密后的证据(depositor address + proof of mapping);
  6. 链上 emit DisclosureRevealed event(透明审计)。

关键设计

  • 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)10s6s1.2s
PriPool (~9.5k constraints)~5s~3s~0.6s
Aztec Connect Note (~50k)25s15s3s

测试机器:MacBook Pro M2 Max, 32GB RAM, Chrome 120。

优化方向

  1. rapidsnark + WebAssembly: 浏览器端可达 ~1.5s(理论);
  2. WebGPU prover: 实验性,未来可达 ~0.5s;
  3. Server-side proving: 牺牲隐私(用户必须把 secret 发给 server)—— 不推荐
  4. Note pre-computation: deposit 时预先计算部分 witness——可减 ~30% 时间;v0.2 路线。

7.2 On-chain verify gas

协议Verify gas总 withdraw gas备注
Tornado Cash~230k~360k2 inputs (nullifier, root) bound
Privacy Pools v2~280k~520k+ ASP root + ASP id
PriPool v0.1~290k~480k优化了 SDN check(mapping 查询)
Aztec Connect~340k~500kMulti-asset notes
Theoretical Groth16 floor~200k~280k2 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 CashAztec Connect (旧)Aztec Network (新)Privacy Pools v2PriPool v0.1
隐私模型全 pool k-anon全 pool + Notes私有 state + private fnsASP 内 k-anonASP 内 k-anon (multi-ASP)
架构L1 mixerL2 (rollup) + NotesL2 (Aztec L2) + NoirL1 pool + ASPL1/L2 pool + multi-ASP + escrow
币种ETH + 个别 ERC20ETH + 多 ERC20ETH + ERC20 (private)ETHETH + USDC
denomination0.1/1/10/100 ETH任意 (通过 Note)任意任意1 ETH (v0.1)
proof systemGroth16Plonk + Halo2Honk + Goblin PlonkGroth16Groth16
circuit DSLCircomNoirNoirCircomCircom
trusted setupMPC ceremony (1100+)Universal (PLONK)UniversalMPC ceremonyMPC ceremony (50+)
withdraw gas~360k~500kvaries (L2)~520k~480k
proof gen time10s (browser)30s~5s (recursive)~10s~5s
anonymity set~200k cumulative~50k notesgrowing~5kTBD
TVL (2026-12)~$80M$0 (closed)~$80M (early)~$15M$0 (design)
合规opt-in compliance hooksChainalysis ASPmulti-ASP + SDN + escrow
judicial disclosure通过 selective disclosuret-of-3 ThresholdEscrow
关闭历史OFAC 制裁 (2022-08, 撤销 2025-03)主动关闭 (2024-03)activeactiveN/A
token economicsTORN (governance)(planned)无 (v0.1)
法律地位仍在调查(Roman Storm 案)关闭compliance-friendlyOFAC 认可多管辖区配置
代码0x12D66f... (Tornado v1)archivedaztec-protocol/aztec-packages0xbow.iothis 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 + 合规工程化" ——不是密码学创新,是产品工程创新。

具体差异:

  1. 多 ASP 市场 vs 单 ASP(去 single point of censorship);
  2. 双层 SDN 过滤 vs 仅 ASP(应对 OFAC 直接制裁);
  3. 司法授权解密 vs 无(应对 subpoena/court order);
  4. 多司法管辖区配置 vs 单一 instance(让美国/欧盟/日本等独立部署);
  5. 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 常见漏洞清单

#漏洞类型描述实例
1Under-constrained signalsignal 未充分约束,prover 可填任意值Nova Tornado fork: nullifierHash 未 enforce
2Aliasing不同 witness 产生相同 commitmenthash function 选择不当
3Generator manipulation攻击者用特殊点绕过 pairingbn254 subgroup attack
4Trusted setup compromisetoxic waste 泄露 → 伪造任何 proofTornado v0 (no ceremony) 不安全
5Public input mismatch链上检查的 input 与电路输入不一致input ordering 错误
6Field overflowinput > FIELD_SIZE未检查 commitment ∈ F
7Path indices not constrained as bitspathIndices 不是 0/1 → 树验证可绕过没用 Switcher 强约束
8Hash function choice用 keccak in-circuit 太贵,但 Poseidon 不在所有 hash 域安全选择 Poseidon for SNARK
9Replay across chains同 nullifierHash 在多链复用跨链桥需加 chainId
10Non-determinism in witnesswitness 计算依赖外部状态 → testnet/mainnet 不一致block.timestamp in circuit

9.2 Solidity 常见漏洞清单

#漏洞类型描述
11Reentrancy in withdrawrecipient.call 前未更新状态
12Front-running on deposit允许 commitment 被覆盖
13Integer overflowleafIndex / fee 可溢出
14Unchecked external callhasher.poseidon 可能 revert
15Unauthorized adminonlyOwner 缺失或 admin key 被盗
16Storage collisionproxy upgrade 未对齐 storage layout
17Unverified Verifier未确认 verifier 是 snarkjs 自动生成、未篡改
18Gas griefingmalicious recipient 在 fallback 中消耗 gas
19Tree underflownextLeafIndex == 0 时减 1 → 巨大数
20Clone-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 BitsSolidity + Circom 全栈$150k - $250k
OpenZeppelinSolidity + 经济安全$100k - $200k
ChainSecurityEVM 形式化分析$80k - $150k
VeridiseZK circuit specialty$80k - $150k
SpearbitCrowdsourced expert$50k - $100k
Code4rena比赛式审计$50k - $150k (prize pool)

v0.1 建议路线

  1. 内部 review(团队自查 + AI 辅助 like Slither / Mythril)— 4 周;
  2. Veridise ZK 电路专项审计 — 6 周,$100k;
  3. Trail of Bits 全栈审计 — 8 周,$200k;
  4. Code4rena 公开赛事 — 4 周,$80k 奖金;
  5. 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 同时运行。

关键设计

  1. 每条链独立部署 PrivacyPool(不是 cross-chain pool);
  2. ComplianceOracle 共享(同一组 ASP roots,多链 sync);
  3. Cross-chain Relayer 网络(用户在 L1 deposit,可在 L2 withdraw);
  4. 引入 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 个月

目标

  1. 加入英国 FCA Sandbox / 新加坡 MAS Project Guardian / 日本 FSA 白名单;
  2. 引入治理 token(PRI),DAO 管理 ComplianceOracle;
  3. 可配置 instance:us / eu / jp / permissionless;
  4. 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 名公开身份的贡献者
BeaconDrand 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 EquilibriumButerin, Illum, Nadler, Schär, Soleimani2023SSRN/arxiv
Tornado Cash Privacy SolutionPertsev, Storm, Semenov2019tornado.cash whitepaper
Aztec Protocol whitepaperWilliamson, Andrews, Cooper2020aztec.network
Plonk: Permutations over Lagrange-basesGabizon, Williamson, Ciobotaru2019eprint 2019/953
Groth16: On the Size of Pairing-based Non-interactive ArgumentsJens Groth2016eprint 2016/260
Halo: Recursive Proof Composition without a Trusted SetupBowe, Grigg, Hopwood2019eprint 2019/1021
zk-SNARKs: Backbone of Crypto-economic PrivacyDragan Boscovic2022arxiv 2202.01024

工具

工具用途链接
CircomZK DSLgithub.com/iden3/circom
snarkjsJS prover/verifiergithub.com/iden3/snarkjs
circomlibhelper templatesgithub.com/iden3/circomlib
circomlibjsJS helpers (Poseidon etc.)github.com/iden3/circomlibjs
rapidsnarkNative fast provergithub.com/iden3/rapidsnark
@semaphore-protocol/identityIdentity gengithub.com/semaphore-protocol
IncrementalMerkleTreeOff-chain treegithub.com/privacy-scaling-explorations/zk-kit
HardhatSolidity dev envhardhat.org
FoundrySolidity dev env (faster)github.com/foundry-rs
SlitherSolidity static analyzergithub.com/crytic/slither
TenderlyTx simulation + monitoringtenderly.co
FortaReal-time on-chain monitoringforta.network

审计报告

协议审计备注
Tornado Cash v1ABDK Consulting2019, 公开
Aztec ConnectTrail of Bits + ConsenSys Diligence2021
Privacy Pools v2 (0xbow)ChainSecurity2024
SemaphoreVeridise2023
zkSync EraOpenZeppelin + Spearbit2023
Scroll zkEVMTrail of Bits + KALOS2023

法律资源

资源内容
Coin Center Privacy Pool briefPrivacy 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 CVCsUS 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.12027-01-19初始发布

联系:本文为作者 Phase 4 学习产出,非商业项目。如有学术或工程讨论意向,请通过 GitHub issue / Twitter 联络。


End of Document