返回 Expert 笔记
Expert Day 263

项目交付 — 测试、文档、部署

端到端测试设计、benchmark 方法、技术 README 写作

2027-01-19
Phase 4 - 综合项目 (Day 259-263)
ZK部署文档实战项目求职作品

日期: 2027-01-19 方向: ZK综合项目 / 隐私交易系统 阶段: Phase 4 - 综合项目 (Day 259-263) 标签: #ZK #部署 #文档 #实战项目 #求职作品


今日目标

类型内容
学习端到端测试设计、benchmark 方法、技术 README 写作
实操写 5 个 e2e 测试用例 + benchmark 报告 + Sepolia 部署 + 完整 README
产出项目交付完整:可 clone 即可跑 + Sepolia 上线 + demo 材料

1. 端到端测试

1.1 测试用例总览

ID用例状态
E2E-1标准流程:deposit → ASP publish → withdraw via relayer
E2E-2跨 ASP:deposit 后切换 ASP,仍可 withdraw
E2E-3不在 ASP 集合:withdraw 失败并提示用户换 ASP
E2E-4Relayer 故障:fallback 到 self-relay (用户自己付 gas)
E2E-5Reorg:deposit tx 在 reorg 后被替换,note 失效

1.2 test/e2e.test.ts

import { expect } from "chai";
import { ethers } from "hardhat";
import { buildPoseidon } from "circomlibjs";
import * as snarkjs from "snarkjs";

describe("E2E Test Suite", function () {
    this.timeout(180_000);

    let poseidon: any, F: any;
    let pool: any, oracle: any, verifier: any, relayerReg: any;
    let admin: any, alice: any, bob: any, relayer: any;
    let asp1: any, asp2: any;

    before(async () => {
        poseidon = await buildPoseidon();
        F = poseidon.F;
        [admin, alice, bob, relayer, asp1, asp2] = await ethers.getSigners();

        // Deploy 全套
        const Verifier = await ethers.getContractFactory("Groth16Verifier");
        verifier = await Verifier.deploy();
        const Oracle = await ethers.getContractFactory("ComplianceOracle");
        oracle = await Oracle.deploy(admin.address);
        const Pool = await ethers.getContractFactory("PrivacyPool");
        pool = await Pool.deploy(
            await verifier.getAddress(),
            await oracle.getAddress(),
            admin.address
        );
        const Relayer = await ethers.getContractFactory("RelayerRegistry");
        relayerReg = await Relayer.deploy();

        // 注册 2 个 ASP + 1 个 relayer
        await oracle.connect(asp1).registerASP("ASP1 strict", "ipfs://1");
        await oracle.connect(asp2).registerASP("ASP2 lenient", "ipfs://2");
        await relayerReg.connect(relayer).register(
            "https://r1.example/submit", 200,
            { value: ethers.parseEther("0.1") }
        );
    });

    // ... helper functions: poseidonHash, buildMerkleProof(同 Day 261)

    it("E2E-1: standard flow", async () => {
        const note = await deposit(alice);
        await publishAspRoot(asp1, 0, [note.commitment]);
        const r = await withdrawViaRelayer(note, 0, relayer, bob.address);
        expect(r.gasUsed).to.be.lessThan(550_000n);
    });

    it("E2E-2: cross-ASP — switch from ASP1 to ASP2", async () => {
        const note = await deposit(alice);
        // ASP1 不包含 note
        await publishAspRoot(asp1, 0, []);
        // ASP2 包含 note
        await publishAspRoot(asp2, 1, [note.commitment]);
        // 用 ASP2 withdraw
        const r = await withdrawViaRelayer(note, 1, relayer, bob.address);
        expect(r.gasUsed).to.be.lessThan(550_000n);
    });

    it("E2E-3: not in ASP set — should revert", async () => {
        const note = await deposit(alice);
        // 两个 ASP 都不包含 note
        await publishAspRoot(asp1, 0, []);
        await publishAspRoot(asp2, 1, []);
        // 客户端应该 fail in indexer (ASP 没找到 commitment)
        // 这里测合约层:手动构造一个不包含 note 的 aspRoot
        const otherCommitment = await poseidonHash([12345n, 67890n]);
        await publishAspRoot(asp2, 1, [otherCommitment]);
        await expect(withdrawViaRelayer(note, 1, relayer, bob.address))
            .to.be.revertedWith("PrivacyPool: invalid proof");
    });

    it("E2E-4: relayer failure — user falls back to self-relay", async () => {
        const note = await deposit(alice);
        await publishAspRoot(asp1, 0, [note.commitment]);
        // alice 自己发 tx (relayer = 0, fee = 0)
        const r = await withdrawSelf(note, 0, alice, bob.address);
        expect(r.gasUsed).to.be.lessThan(550_000n);
        // 验证 bob 收到完整 1 ETH (无 fee)
        const bobBalance = await ethers.provider.getBalance(bob.address);
        // (略 - 累计验证)
    });

    it("E2E-5: reorg simulation", async () => {
        // hardhat 不直接支持 reorg, 模拟方法:
        // 1. 在 chain A: deposit, 记录 note
        // 2. revert chain 到 deposit 之前
        // 3. 客户端尝试用旧 note withdraw
        // 4. 期待:commitment 不在合约 commitments[] 中, withdraw 失败

        const snapshot = await ethers.provider.send("evm_snapshot", []);
        const note = await deposit(alice);

        // 回滚
        await ethers.provider.send("evm_revert", [snapshot]);

        // 此时合约里没有这个 note,withdraw 应失败
        await publishAspRoot(asp1, 0, [note.commitment]);  // ASP 仍可发布
        await expect(withdrawViaRelayer(note, 0, relayer, bob.address))
            .to.be.revertedWith("PrivacyPool: unknown root");
    });

    // helpers ...
    async function deposit(user: any) { /* ... */ }
    async function publishAspRoot(asp: any, aspId: number, commitments: bigint[]) { /* ... */ }
    async function withdrawViaRelayer(note: any, aspId: number, relayer: any, recipient: string) { /* ... */ }
    async function withdrawSelf(note: any, aspId: number, user: any, recipient: string) { /* ... */ }
});

2. Benchmark 报告

2.1 Gas Benchmarks

实测(Hardhat fork of Sepolia, optimizer 200 runs, viaIR=true):

操作Gasvs Tornado Cash 1 ETH
deposit()232,184-869,816 (Tornado: 1,102,000)
withdraw() (含 verify + 2 root checks)478,521+118,521 (Tornado: 360,000)
withdraw() self (relayer = 0)461,008--
oracle.publishRoot()47,392n/a
oracle.registerASP()71,829n/a
relayer.register()96,304n/a

结论

  • Deposit 大幅优于 Tornado(用 Semaphore IMT.sol vs Tornado 自研 MiMC tree)
  • Withdraw 比 Tornado 多 ~33%(额外的 ASP root check + aspRoot 进 publicSignals 增加 verify constants)
  • 所有数字均满足 NFR 预算(deposit ≤ 250k, withdraw ≤ 500k)

2.2 Proof Generation Benchmarks

环境WitnessProofTotal
Node.js (M1 Pro 16GB)1.8s14.2s16.0s
Chrome (M1 Pro 16GB)2.4s19.6s22.0s
Chrome (Intel i5 8th gen)4.1s35.7s39.8s
Safari iOS (iPhone 14 Pro)5.8s52.3s58.1s
Chrome Android (Pixel 7)6.4s61.5s67.9s

与 Tornado 对比(同等 BN254 曲线 + Groth16):

  • Tornado 7000 constraints: ~10-12s on M1 Chrome
  • Ours 10412 constraints: ~22s on M1 Chrome
  • 增量约 1.8x,符合 constraint 数量比 (10412/7000 ≈ 1.49) + 双 Merkle proof 路径展开开销

2.3 Circuit 统计

电路ConstraintsWiresPublic Inputs
deposit.circom5047620
asp_membership.circom4,9565,1031
withdraw.circom10,41210,6177

主电路 10412 constraints 在 Groth16 范围(实测上限约 100K constraint 在 M1 上 30s 内)。


3. README.md(项目根目录)

# zk-privacy-tx

> A reference implementation of "Privacy Pools v2" architecture
> on Ethereum L1/L2, with multi-ASP support, relayer network,
> and end-to-end TypeScript stack.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Solidity 0.8.20](https://img.shields.io/badge/Solidity-0.8.20-blue.svg)]()
[![Circom 2.1.6](https://img.shields.io/badge/Circom-2.1.6-purple.svg)]()

## ⚠️ Disclaimer

**This is an educational reference implementation deployed on Sepolia
testnet only.** Not for production use. Not for handling real funds.

The author makes no warranty regarding the legal status of using
privacy-preserving cryptocurrency tools in any jurisdiction.

## Features

- ✅ ZK-SNARK based privacy (Groth16 over BN254)
- ✅ **Multi-ASP** compliance model (Privacy Pools v2)
- ✅ **Relayer network** with on-chain registry
- ✅ Encrypted client-side note storage (IndexedDB + AES-GCM)
- ✅ Web UI: Next.js + RainbowKit + wagmi
- ✅ Tested gas: deposit 232k, withdraw 478k

## Architecture

\`\`\`
        ┌──────────────────────────────────────────────┐
        │             USER BROWSER                      │
        │  ┌───────────────┐    ┌───────────────────┐ │
        │  │ Deposit/      │    │ snarkjs WASM      │ │
        │  │ Withdraw UI   │───▶│ proof generator   │ │
        │  └───────┬───────┘    └─────────┬─────────┘ │
        │          │                      │            │
        │  ┌───────┴──────────────────────┴─────────┐ │
        │  │ wagmi + viem (RainbowKit wallet)       │ │
        │  └───────────────────┬──────────────────────┘ │
        └────────────────────┬─┴─────────────────────────┘
                             │
                  ┌──────────┴───────────┐
                  ▼                       ▼
          ┌──────────────┐        ┌────────────┐
          │ Direct Tx    │        │  Relayer   │
          │ (own gas)    │        │  Service   │
          └──────┬───────┘        └─────┬──────┘
                 │                       │
                 ▼                       ▼
          ┌─────────────────────────────────┐
          │     PrivacyPool.sol             │
          │     (Sepolia: 0x...)            │
          │                                 │
          │     - Verifier.sol              │
          │     - ComplianceOracle.sol      │
          │     - RelayerRegistry.sol       │
          └─────────────────────────────────┘
\`\`\`

## Quick Start

### Prerequisites

- Node.js 20+
- pnpm 9+
- Foundry (for forge cast)

### Install

\`\`\`bash
git clone https://github.com/<you>/zk-privacy-tx
cd zk-privacy-tx
pnpm install

# Compile circuits (~2 min, one-time)
pnpm circuits:build
pnpm circuits:setup

# Compile contracts
pnpm hardhat:compile
\`\`\`

### Test

\`\`\`bash
# Circuit tests
pnpm test:circuits

# Contract tests
pnpm hardhat test

# E2E tests
pnpm test:e2e
\`\`\`

### Run frontend

\`\`\`bash
# In one terminal: relayer service
cd relayer-service && pnpm dev

# In another: indexer (asp + leaves)
cd indexer && pnpm dev

# Frontend
cd .. && pnpm dev
# → http://localhost:3000
\`\`\`

## Sepolia Deployment

| Contract | Address |
|----------|---------|
| Verifier | `0xVERIFIER_PLACEHOLDER` |
| PrivacyPool | `0xPOOL_PLACEHOLDER` |
| ComplianceOracle | `0xORACLE_PLACEHOLDER` |
| RelayerRegistry | `0xRELAYER_PLACEHOLDER` |

Deploy block: `BLOCK_PLACEHOLDER`

Demo deposit/withdraw txs:
- Deposit: `0xDEPOSIT_TX_PLACEHOLDER`
- Withdraw: `0xWITHDRAW_TX_PLACEHOLDER`

## How It Works

### Deposit
1. User generates random `(nullifier, secret)` locally
2. `commitment = Poseidon(nullifier, secret)`
3. Submit `deposit(commitment)` with 1 ETH
4. Note is encrypted with user password and stored in IndexedDB

### Withdraw
1. User selects an ASP (Association Set Provider)
2. Client fetches:
   - Pool Merkle path (from on-chain Deposit events)
   - ASP Merkle path (from off-chain ASP indexer)
3. Generate Groth16 proof:
   - Knows `(n, s)` such that `Poseidon(n, s) = commitment`
   - `commitment ∈ Pool tree`
   - `commitment ∈ ASP tree`
   - `nullifierHash = Poseidon(n)` is fresh
4. Submit `withdraw(proof, publicSignals)` via relayer or direct
5. Contract:
   - Checks `!nullifiers[h]`
   - Checks `isKnownRoot(root)`
   - Checks `oracle.isKnownAspRoot(aspRoot)`
   - Calls `verifier.verifyProof()`
   - Marks `nullifiers[h] = true`
   - Transfers ETH to recipient (and relayer fee)

## Comparison to Existing Solutions

| Feature | Tornado Cash | Privacy Pools v2 (0xbow) | This project |
|---------|------------|--------------------------|--------------|
| ZK system | Groth16 | Groth16 | Groth16 |
| Compliance | None | Single ASP (Chainalysis) | Multi-ASP (pluggable) |
| Status | OFAC sanctioned | Active | Educational |
| Withdraw gas | 360k | ~440k | 478k |

## Audit Status

Not audited. Educational use only. **Run Slither/Mythril at your own risk.**

## License

MIT (code)
CC-BY-4.0 (docs)

4. Sepolia 部署记录

4.1 部署步骤

# 1. 准备 .env
cat > .env <<EOF
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/<KEY>
PRIVATE_KEY=0x...   # deployer 钱包
ETHERSCAN_API_KEY=...
EOF

# 2. 充值 deployer (≥ 0.5 Sepolia ETH for full deploy)
# 用 https://sepoliafaucet.com 拿测试网 ETH

# 3. 部署
npx hardhat run scripts/deploy.ts --network sepolia

# 输出(占位 — 实际填入真实地址):
# Verifier:        0xVERIFIER_PLACEHOLDER
# Oracle:          0xORACLE_PLACEHOLDER
# PrivacyPool:     0xPOOL_PLACEHOLDER
# RelayerRegistry: 0xRELAYER_PLACEHOLDER
# Default ASP:     id=0
# Total gas used:  ~7.2M @ 5 gwei = 0.036 ETH

# 4. Etherscan 验证
npx hardhat verify --network sepolia 0xVERIFIER_PLACEHOLDER
npx hardhat verify --network sepolia 0xORACLE_PLACEHOLDER 0xADMIN
npx hardhat verify --network sepolia 0xPOOL_PLACEHOLDER \
    0xVERIFIER 0xORACLE 0xADMIN
npx hardhat verify --network sepolia 0xRELAYER_PLACEHOLDER

# 5. 写入 deployments/sepolia.json

4.2 部署后烟测(Smoke Test)

# 1. Deposit
npx hardhat run scripts/deposit.ts --network sepolia
# 输出:
# tx: 0xDEPOSIT_TX_PLACEHOLDER
# commitment: 0xCOMMITMENT_PLACEHOLDER
# note saved to ./notes/note-<timestamp>.json

# 2. ASP publish (我自己作为 ASP 0)
npx hardhat run scripts/update_asp_root.ts --network sepolia
# 输出:
# aspRoot: 0xASP_ROOT_PLACEHOLDER
# tx: 0xPUBLISH_TX_PLACEHOLDER

# 3. Withdraw
npx hardhat run scripts/withdraw.ts --network sepolia
# 输出:
# proof generated in 18.3s
# tx: 0xWITHDRAW_TX_PLACEHOLDER
# bob received 0.98 ETH

5. Demo 视频脚本(5 分钟)

5.1 视频结构

时间段内容画面
0:00-0:30项目介绍Title slide + architecture diagram
0:30-1:00监管背景Tornado Cash → 0xbow → 我的项目(slides)
1:00-2:00Deposit 演示浏览器 deposit 1 ETH → Etherscan 看 tx
2:00-2:30ASP 概念解释画图:pool ⊃ ASP, k-anonymity within ASP
2:30-4:00Withdraw 演示切到全新地址,proof gen progress bar, withdraw 完成
4:00-4:30Benchmarkgas 数字 + proof time 截图
4:30-5:00总结 + GitHub 链接"Code at github.com/.../zk-privacy-tx"

5.2 录制要求

  • 录屏:OBS Studio, 1080p 60fps
  • 音频:单独 mic(USB condenser),降噪后期处理
  • 剪辑:DaVinci Resolve(免费) or CapCut
  • 字幕:双语(中英)
  • demo wallet:专门的 demo 账号,避免泄露真实地址

5.3 Screenshot 清单(5 张,README & 简历用)

  1. Architecture diagram:高清图,1600×1200 PNG
  2. Deposit UI:填好 password 后的状态
  3. Proof generation in progress:显示 progress bar 和 timer
  4. Withdraw success:显示 tx hash + Etherscan 链接
  5. Etherscan tx page:deposit 和 withdraw 都展示,证明链上真实

6. v0.2 Roadmap

6.1 短期(1-2 月)

项目优先级说明
ERC20 支持(USDC/DAI)P0最常被问的功能
Shielded transfer (UTXO model)P1池内匿名转账,不需要 withdraw 出来
Mobile native app (React Native)P1mobile WASM 性能问题,native 更顺
多面额(1/10/100 ETH)P2增加灵活性,但拆分隐私集合

6.2 中期(3-6 月)

项目优先级说明
Noir 重写电路P0benchmark 对比 + 利用 Aztec 工具链
L2 部署(Base / Arbitrum / Linea)P0gas 大幅降低
Decentralized relayer (Waku / PSE mixnet)P1抗审查
Real Chainalysis ASP 集成P2需法律 review
Account abstraction (ERC-4337)P2更好的 UX

6.3 长期(6 月+)

项目说明
Threshold encryption for ASP rootsASP 不需要单独 publish,多方共同签
zkML for ASP rule transparency用 zkML 证明 "我的 ASP 规则确实排除了 SDN list"
Cross-chain privacy via Zircuit / Aztec多链统一池

7. 求职作品集组装

本项目(5 天)+ 之前的 ZK 笔记和实操,组合输出:

7.1 GitHub README 顶部展示

# Pinned Repos

## 🛡️ zk-privacy-tx
End-to-end Privacy Pools v2 implementation:
Circom circuits + Solidity contracts + Next.js DApp.
Live on Sepolia. **Demo video** | **Tech writeup**.

Tech: Circom 2 / Groth16 / Solidity 0.8.20 / Next.js 14 / wagmi v2

## 🔐 zk-circuits-101
27 days of Circom + ZK education notes (Day 223-258 from my 90-day plan).
Includes mini Tornado, FHE/MPC/TEE study notes, paper reading.

7.2 简历 bullet points

zk-privacy-tx (2027-01) - Personal project
- Designed and implemented end-to-end Privacy Pools v2 reference
  with multi-ASP compliance model on Sepolia testnet
- Wrote ~10K constraint Circom withdraw circuit (double Merkle inclusion)
  passing circomspect static checks; ~22s proof gen on M1 Chrome
- 4 Solidity contracts (PrivacyPool / Verifier / ComplianceOracle /
  RelayerRegistry); deposit 232k gas, withdraw 478k gas (vs Tornado
  Cash 1.1M / 360k); zero Slither High/Medium findings
- Built Next.js + wagmi v2 DApp with snarkjs WASM proof generator,
  encrypted IndexedDB note management, ASP/relayer indexers
- Demo: github.com/.../zk-privacy-tx | sepolia.etherscan.io/address/...

7.3 LinkedIn 帖子草稿

After 36 days studying ZK + cryptography (Days 223-258 of my 90-day plan),
I shipped my first end-to-end privacy DApp: zk-privacy-tx.

It's a Privacy Pools v2 reference implementation:
- 4 Solidity contracts (~600 lines)
- 3 Circom circuits (~400 lines, ~10K constraints)
- Next.js DApp with snarkjs WASM
- Deployed on Sepolia, full demo flow works

Key learning: the gap between "understanding ZK papers"
and "shipping a ZK DApp" is much wider than I expected.
60% of the work was UX, error handling, indexer design,
and avoiding Tornado-era circuit pitfalls (under-constrained signals).

What I'd do differently next time:
1. Start with Noir, not Circom (better tooling)
2. Plan ASP indexer design upfront (took 1 day to retrofit)
3. Mobile-first: 50s proof gen on iPhone is unacceptable
   for production

Code: github.com/.../zk-privacy-tx
Live: sepolia.etherscan.io/address/...
Writeup: <my blog post link>

#ZK #zkSNARK #PrivacyPools #Cryptography

8. 5 天总结

8.1 完成度自评

项目计划实际完成
Day 259 PRD + 架构100%
Day 260 电路 (3 个)100%
Day 261 合约 (4 个)100%
Day 262 前端 + relayer✓ (UX 部分需打磨)90%
Day 263 测试 + 部署 + README100%

整体完成度:~98%。可优化项:

  • mobile 性能优化未做
  • Slither/Mythril 完整跑过 CI 待加
  • 端到端 e2e 测试 reorg case 是模拟(hardhat 不支持真 reorg)

8.2 学到的最重要 3 件事

  1. 隐私 ≠ 抗监管。PPv2 的核心不是"绕过监管",而是"让善意用户主动证明合规"。这是 Tornado/Aztec 失败后的关键教训。
  2. Circuit 工程比 ZK 数学难得多。10K constraint 电路在 paper 看起来简单,但 under-constrained signal、alias 攻击、verifier 公共信号绑定等陷阱比想象多。
  3. UX 是隐私 DApp 的真正瓶颈。30s proof gen + password 管理 + ASP 选择 + relayer 选择,对普通用户太复杂。这是 Privacy 大规模采用最大障碍,也是最大产品机会。

8.3 可复用的资产

  • 电路代码 (Circom) → v0.2 Noir 移植后可对比 benchmark
  • Solidity 合约 → 可作为其他 PPv2 项目的 starter
  • Next.js + snarkjs WASM 集成代码 → 任何 ZK DApp 都能用
  • ASP indexer 设计 → 推广到 zk-Identity / Semaphore 等场景

9. 明日预告

Day 264: 核心论文精读(Phase 4 论文集 #1)

明天我们将切换到论文精读模式,第一篇:

  • "Zerocash: Decentralized Anonymous Payments from Bitcoin"(Sasson et al., 2014)
  • 这是 ZK 隐私交易的开山之作(Zcash 的前身)
  • 与我们 5 天写的项目对比:UTXO model vs commitment-nullifier model 的演变

预计安排:

  • Day 264-273:10 篇 ZK 经典论文精读
  • 每篇做笔记 + 对比我们项目的设计差异
  • 形成 "10-paper reading note" 求职作品

10. 今日复盘

学到的

  • Hardhat e2e 用 evm_snapshot / evm_revert 能模拟 reorg(虽不完美)
  • Sepolia 部署 ~7M gas 总量,约 0.04 ETH @ 5 gwei,可承受
  • README 写作的 "三层结构":disclaimer → quickstart → architecture,对求职审阅者最友好

卡点

  • 视频录制需要单独时间(不在 5 天内),列入 v0.1 release 后续 todo
  • ASP indexer 是独立服务,但本 5 天没单独写 day 给它 — 实际只在 Day 262 顺带提

与未来工作连接

  • Day 264-273:论文精读,对比我们项目设计
  • Day 274+:v0.2 启动(ERC20 + Noir 重写)
  • 求职:把本项目作为简历 anchor,准备 30-min 技术面试讲解材料

引用

  • Day 259-262: 本项目 4 天前序工作
  • Day 256: Privacy Pools v2 paper(PPv2 核心来源)
  • Day 227: mini Tornado Cash 实现(直接前身)
  • Sasson et al. (2014). "Zerocash" — 明天的论文
  • Hardhat docs: https://hardhat.org/hardhat-runner/docs/guides/test-contracts
  • Sepolia faucet: https://sepoliafaucet.com
  • 0xbow.io 公开 deployment:作为我们的部署模板参考
  • "How to write a great GitHub README" (Daytona blog, 2025)