返回 Expert 笔记
Expert Day 260

项目实施 #1 — Circom 电路实现

复合电路设计模式、双 Merkle inclusion、under-constrained 检测

2027-01-16
Phase 4 - 综合项目 (Day 259-263)
ZKCircom电路开发实战项目PrivacyPools

日期: 2027-01-16 方向: ZK综合项目 / 隐私交易系统 阶段: Phase 4 - 综合项目 (Day 259-263) 标签: #ZK #Circom #电路开发 #实战项目 #PrivacyPools


今日目标

类型内容
学习复合电路设计模式、双 Merkle inclusion、under-constrained 检测
实操写 4 个 .circom(deposit/withdraw/asp_membership + lib),可编译,配单元测试
产出circuits/ 目录完整代码 + circom_tester 测试 + ptau 选择文档

1. 电路总体设计回顾

回顾 Day 259 的 PRD:withdraw 电路是核心,需要同时证明 4 件事:

  1. knowledge: 我知道 (n, s) 使得 commitment $c = \text{Poseidon}(n, s)$
  2. pool inclusion: $c \in$ PoolMerkleTree (root 公开)
  3. ASP inclusion: $c \in$ ASPMerkleTree (aspRoot 公开)
  4. nullifier: $h = \text{Poseidon}(n)$ 公开作双花防护

并且要把 recipient/relayer/fee/refund 绑定到 proof(这是 Tornado 早期 audit 中的关键点:如果 prover 可以替换 recipient,relayer 可窃取资金)。


2. 文件结构

circuits/
├── lib/
│   ├── merkle.circom                # MerkleTreeChecker (Day 224 复用)
│   └── num2bits_strict.circom       # 严格版 Num2Bits
├── deposit.circom                   # commitment 生成(sanity)
├── asp_membership.circom            # 子电路(独立可audit)
├── withdraw.circom                  # 主电路
└── tests/
    ├── deposit.test.js
    ├── withdraw.test.js
    └── asp_membership.test.js

3. 完整代码

3.1 lib/merkle.circom

复用 Day 224 的 MerkleTreeChecker,这里完整复制以保证 Day 260 文件自包含。

pragma circom 2.1.6;

include "circomlib/circuits/poseidon.circom";
include "circomlib/circuits/mux1.circom";

// 单层 Merkle hash:
// 给定当前节点 cur 和 sibling,返回父节点 hash
// pathIndex == 0 → cur 在左, sibling 在右
// pathIndex == 1 → cur 在右, sibling 在左
template HashLevel() {
    signal input cur;
    signal input sibling;
    signal input pathIndex;     // 0 or 1
    signal output parent;

    // 强约束 pathIndex 必须为 0 或 1
    pathIndex * (pathIndex - 1) === 0;

    component mux1 = MultiMux1(2);
    mux1.c[0][0] <== cur;
    mux1.c[0][1] <== sibling;
    mux1.c[1][0] <== sibling;
    mux1.c[1][1] <== cur;
    mux1.s <== pathIndex;

    component h = Poseidon(2);
    h.inputs[0] <== mux1.out[0];
    h.inputs[1] <== mux1.out[1];
    parent <== h.out;
}

// 完整 Merkle proof 验证
template MerkleTreeChecker(LEVELS) {
    signal input leaf;
    signal input root;
    signal input pathElements[LEVELS];
    signal input pathIndices[LEVELS];

    component levels[LEVELS];
    signal cur[LEVELS + 1];
    cur[0] <== leaf;

    for (var i = 0; i < LEVELS; i++) {
        levels[i] = HashLevel();
        levels[i].cur <== cur[i];
        levels[i].sibling <== pathElements[i];
        levels[i].pathIndex <== pathIndices[i];
        cur[i + 1] <== levels[i].parent;
    }

    cur[LEVELS] === root;
}

约束估算:每层 Poseidon(2) ≈ 240 constraints + 1 mux + 1 boolean,共 ~245。20 层 ≈ 4900 constraints

3.2 lib/num2bits_strict.circom

pragma circom 2.1.6;

// 严格的 Num2Bits:保证 bits 加起来 ≤ 2^n - 1
// 用于把 amount/fee 转 bits 后 range check
template Num2BitsStrict(n) {
    signal input in;
    signal output out[n];

    var lc = 0;
    var pow = 1;
    for (var i = 0; i < n; i++) {
        out[i] <-- (in >> i) & 1;
        out[i] * (out[i] - 1) === 0;
        lc += out[i] * pow;
        pow *= 2;
    }
    lc === in;
}

3.3 deposit.circom(sanity check 电路)

注意:Privacy Pools 风格 deposit 不需要 ZK proof(用户只需提交 commitment,链上验证 msg.value),但我们仍写一个 deposit 电路用于:

  1. 客户端生成 commitment 时本地 sanity check
  2. v0.2 如果支持自定义面额,可以增加 amount 的 range proof
pragma circom 2.1.6;

include "circomlib/circuits/poseidon.circom";

// Deposit sanity:
// 用户提供 (nullifier, secret),电路输出 commitment
// 客户端用此电路 verify 自己 note.json 没有损坏
template Deposit() {
    // private
    signal input nullifier;
    signal input secret;

    // public
    signal output commitment;

    component h = Poseidon(2);
    h.inputs[0] <== nullifier;
    h.inputs[1] <== secret;
    commitment <== h.out;

    // 强制 nullifier 和 secret 在 252 bits 内
    // (Poseidon 输入是 BN254 field element, < 2^254)
    component nbits = Num2Bits(252);
    nbits.in <== nullifier;
    component sbits = Num2Bits(252);
    sbits.in <== secret;
}

component main = Deposit();

约束估算:~500 constraints (Poseidon 2-input ~240 + 2× Num2Bits(252) ~252×2 = 504)。

3.4 asp_membership.circom(独立子电路)

为什么独立成电路?

  • Audit 友好:ASP membership 是合规层关键,单独审 ~3000 constraints 比审主电路 ~10000 constraints 容易。
  • 未来灵活性:v0.2 可能要支持"用户先 deposit,后 attach 到 ASP",那时这个子电路独立可用。
  • 教学:分层电路设计是 ZK 工程最佳实践。
pragma circom 2.1.6;

include "./lib/merkle.circom";
include "circomlib/circuits/poseidon.circom";

// ASP Membership Proof:
// 证明 commitment c 在 ASP 的 Merkle tree 中
// 公开 aspRoot, 私密 c, aspPath
//
// 注:commitment 在主电路里同样要在 pool tree 中。
// 这个子电路只关心 ASP tree,与 pool tree 独立。
template ASPMembership(LEVELS) {
    // public
    signal input aspRoot;

    // private
    signal input commitment;
    signal input aspPathElements[LEVELS];
    signal input aspPathIndices[LEVELS];

    component aspChecker = MerkleTreeChecker(LEVELS);
    aspChecker.leaf <== commitment;
    aspChecker.root <== aspRoot;

    for (var i = 0; i < LEVELS; i++) {
        aspChecker.pathElements[i] <== aspPathElements[i];
        aspChecker.pathIndices[i] <== aspPathIndices[i];
    }
}

component main {public [aspRoot]} = ASPMembership(20);

约束估算:~4900 constraints(主要来自 20 层 Merkle)。

3.5 withdraw.circom(主电路)

pragma circom 2.1.6;

include "circomlib/circuits/poseidon.circom";
include "./lib/merkle.circom";
include "./lib/num2bits_strict.circom";

// CommitmentHasher:
// 复用 Day 227,输出 commitment 和 nullifierHash
template CommitmentHasher() {
    signal input nullifier;
    signal input secret;
    signal output commitment;
    signal output nullifierHash;

    component cHash = Poseidon(2);
    cHash.inputs[0] <== nullifier;
    cHash.inputs[1] <== secret;
    commitment <== cHash.out;

    component nHash = Poseidon(1);
    nHash.inputs[0] <== nullifier;
    nullifierHash <== nHash.out;
}

// 主 Withdraw 电路
template Withdraw(LEVELS) {
    // ─────────────────────────────────────────
    // PUBLIC INPUTS
    // ─────────────────────────────────────────
    signal input root;             // pool merkle root (history allowed)
    signal input aspRoot;          // ASP merkle root (latest)
    signal input nullifierHash;    // Poseidon(nullifier)
    signal input recipient;        // payout address (not constrained, only bound)
    signal input relayer;          // relayer address
    signal input fee;              // fee paid to relayer (≤ denomination)
    signal input refund;           // refund (eth for new account, optional)

    // ─────────────────────────────────────────
    // PRIVATE INPUTS
    // ─────────────────────────────────────────
    signal input nullifier;
    signal input secret;
    signal input pathElements[LEVELS];
    signal input pathIndices[LEVELS];
    signal input aspPathElements[LEVELS];
    signal input aspPathIndices[LEVELS];

    // ─────────────────────────────────────────
    // 1. Recompute commitment + nullifierHash
    // ─────────────────────────────────────────
    component hasher = CommitmentHasher();
    hasher.nullifier <== nullifier;
    hasher.secret <== secret;

    // 强约束 nullifier hash 等于 public nullifierHash
    hasher.nullifierHash === nullifierHash;

    // ─────────────────────────────────────────
    // 2. Verify pool Merkle inclusion
    // ─────────────────────────────────────────
    component poolTree = MerkleTreeChecker(LEVELS);
    poolTree.leaf <== hasher.commitment;
    poolTree.root <== root;
    for (var i = 0; i < LEVELS; i++) {
        poolTree.pathElements[i] <== pathElements[i];
        poolTree.pathIndices[i]  <== pathIndices[i];
    }

    // ─────────────────────────────────────────
    // 3. Verify ASP Merkle inclusion (NEW vs Tornado)
    // ─────────────────────────────────────────
    component aspTree = MerkleTreeChecker(LEVELS);
    aspTree.leaf <== hasher.commitment;
    aspTree.root <== aspRoot;
    for (var i = 0; i < LEVELS; i++) {
        aspTree.pathElements[i] <== aspPathElements[i];
        aspTree.pathIndices[i]  <== aspPathIndices[i];
    }

    // ─────────────────────────────────────────
    // 4. Bind public-but-not-computed signals
    //    These signals don't participate in any
    //    computation, but we MUST add a quadratic
    //    constraint per signal so prover cannot
    //    substitute a different value (e.g.,
    //    a malicious relayer changing recipient).
    // ─────────────────────────────────────────
    signal recipientSquare;
    recipientSquare <== recipient * recipient;

    signal relayerSquare;
    relayerSquare <== relayer * relayer;

    signal feeSquare;
    feeSquare <== fee * fee;

    signal refundSquare;
    refundSquare <== refund * refund;

    // ─────────────────────────────────────────
    // 5. Range check fee (≤ 2^248, way more than 1 ETH)
    //    Prevents overflow attacks
    // ─────────────────────────────────────────
    component feeBits = Num2BitsStrict(248);
    feeBits.in <== fee;
}

component main {public [
    root, aspRoot, nullifierHash,
    recipient, relayer, fee, refund
]} = Withdraw(20);

约束估算

  • CommitmentHasher:~360 (Poseidon(2)+Poseidon(1))
  • 2× MerkleTreeChecker(20):~9800
  • 4× quadratic constraints:~4
  • Num2BitsStrict(248):~248
  • 总计:~10412 constraints

比 Tornado 原版 (~7000) 多 ~3000,主要是 ASP tree。预计 proof time +5s(~15-20s on M1)。

3.6 重要的 under-constrained 防护

陷阱 1:忘记约束 pathIndex 是 boolean

// 错误(Tornado 早期 bug):
mux1.s <== pathIndex;      // 没约束 pathIndex ∈ {0,1}

// 正确:
pathIndex * (pathIndex - 1) === 0;
mux1.s <== pathIndex;

如果 prover 提交 pathIndex = 2,mux1 输出未定义,可能产生伪造 proof。我们的 HashLevel 已加约束。

陷阱 2:public signal 未参与运算被替换

// 错误:
signal input recipient;    // 只是 public 没有任何约束
// 这种情况 prover 可以提交任何 recipient,因为它不影响 R1CS

// 正确:
signal recipientSquare;
recipientSquare <== recipient * recipient;  // 现在 recipient 进入约束

陷阱 3:alias 攻击

如果用 Num2Bits(254) 但 BN254 field 的最大值是 p - 1(一个 254-bit 数),那么 n = pn = 0 在 field 中相等,但 Num2Bits 会输出不同的 bits。我们用 Num2BitsStrict 限制到 252 bits 避免此风险。


4. 单元测试

4.1 tests/withdraw.test.js

const { wasm: wasm_tester } = require("circom_tester");
const { buildPoseidon } = require("circomlibjs");
const { randomBytes } = require("crypto");

let poseidon, F;
let circuit;

beforeAll(async () => {
    poseidon = await buildPoseidon();
    F = poseidon.F;
    circuit = await wasm_tester("./circuits/withdraw.circom");
}, 120_000);

function rand254() {
    return BigInt("0x" + randomBytes(31).toString("hex"));
}

function poseidonHash(inputs) {
    return BigInt(F.toString(poseidon(inputs.map(BigInt))));
}

// 构造一个 LEVELS 层 Merkle tree, 返回 root 和 path
function buildMerkleProof(leaf, leafIndex, leaves, LEVELS = 20) {
    // 用 zero subtree 填充
    let zeros = [BigInt(0)];
    for (let i = 1; i < LEVELS; i++) {
        zeros.push(poseidonHash([zeros[i-1], zeros[i-1]]));
    }

    let layer = [...leaves];
    while (layer.length < 2 ** LEVELS) layer.push(BigInt(0));

    const pathElements = [];
    const pathIndices = [];
    let idx = leafIndex;

    for (let i = 0; i < LEVELS; i++) {
        const isRight = idx % 2;
        const sibling = layer[idx ^ 1];
        pathElements.push(sibling);
        pathIndices.push(isRight);

        // 上一层
        const next = [];
        for (let j = 0; j < layer.length; j += 2) {
            next.push(poseidonHash([layer[j], layer[j+1]]));
        }
        layer = next;
        idx = Math.floor(idx / 2);
    }

    return { root: layer[0], pathElements, pathIndices };
}

describe("withdraw circuit", () => {
    test("happy path: valid proof", async () => {
        const nullifier = rand254();
        const secret = rand254();
        const commitment = poseidonHash([nullifier, secret]);
        const nullifierHash = poseidonHash([nullifier]);

        // pool tree with 1 leaf
        const poolProof = buildMerkleProof(commitment, 0, [commitment]);
        // ASP tree with same leaf
        const aspProof = buildMerkleProof(commitment, 0, [commitment]);

        const input = {
            root: poolProof.root,
            aspRoot: aspProof.root,
            nullifierHash,
            recipient: BigInt("0x" + "ab".repeat(20)),
            relayer: BigInt("0x" + "cd".repeat(20)),
            fee: BigInt("100000000000000000"),  // 0.1 ETH
            refund: BigInt(0),

            nullifier, secret,
            pathElements: poolProof.pathElements,
            pathIndices: poolProof.pathIndices,
            aspPathElements: aspProof.pathElements,
            aspPathIndices: aspProof.pathIndices,
        };

        const witness = await circuit.calculateWitness(input);
        await circuit.checkConstraints(witness);
    }, 60_000);

    test("invalid: wrong nullifierHash should fail", async () => {
        const nullifier = rand254();
        const secret = rand254();
        const commitment = poseidonHash([nullifier, secret]);
        const wrongNH = rand254();

        const poolProof = buildMerkleProof(commitment, 0, [commitment]);
        const aspProof = buildMerkleProof(commitment, 0, [commitment]);

        const input = {
            root: poolProof.root,
            aspRoot: aspProof.root,
            nullifierHash: wrongNH,
            recipient: BigInt(0), relayer: BigInt(0),
            fee: BigInt(0), refund: BigInt(0),
            nullifier, secret,
            pathElements: poolProof.pathElements,
            pathIndices: poolProof.pathIndices,
            aspPathElements: aspProof.pathElements,
            aspPathIndices: aspProof.pathIndices,
        };

        await expect(circuit.calculateWitness(input))
            .rejects.toThrow();
    }, 60_000);

    test("invalid: commitment not in ASP tree should fail", async () => {
        const nullifier = rand254();
        const secret = rand254();
        const commitment = poseidonHash([nullifier, secret]);
        const nullifierHash = poseidonHash([nullifier]);

        const poolProof = buildMerkleProof(commitment, 0, [commitment]);
        // ASP tree contains a DIFFERENT commitment
        const otherCommitment = rand254();
        const aspProof = buildMerkleProof(otherCommitment, 0, [otherCommitment]);

        const input = {
            root: poolProof.root,
            aspRoot: aspProof.root,
            nullifierHash,
            recipient: BigInt(0), relayer: BigInt(0),
            fee: BigInt(0), refund: BigInt(0),
            nullifier, secret,
            pathElements: poolProof.pathElements,
            pathIndices: poolProof.pathIndices,
            aspPathElements: aspProof.pathElements,
            aspPathIndices: aspProof.pathIndices,
        };

        await expect(circuit.calculateWitness(input))
            .rejects.toThrow();
    }, 60_000);

    test("invalid: tampered recipient should be unprovable", async () => {
        // 此测试验证 recipient binding:
        // 我们生成 proof 后,verifier 只接受 (proof, publicSignals)
        // 如果有人改 publicSignals 中的 recipient,verifier 应拒绝
        // 这是合约层 + 电路层共同保证的
        // 单元测试这里只 cover 电路层:
        // 如果 prover 用 input.recipient = X 生成 witness,
        // 然后试图把 publicSignals[recipient] 替换为 Y,
        // 那么 verify(proof, publicSignals_with_Y) 必然失败(因为 R1CS 已经 commit)
        //
        // 此测试在 Day 261 集成测试中完成
        expect(true).toBe(true);
    });
});

4.2 tests/asp_membership.test.js

const { wasm: wasm_tester } = require("circom_tester");
const { buildPoseidon } = require("circomlibjs");

let poseidon, F, circuit;

beforeAll(async () => {
    poseidon = await buildPoseidon();
    F = poseidon.F;
    circuit = await wasm_tester("./circuits/asp_membership.circom");
}, 120_000);

describe("asp_membership circuit", () => {
    test("commitment in ASP tree", async () => {
        // 类似 withdraw test, 但只关心 ASP tree
        // 省略 buildMerkleProof helper,复用上面
        // ... (略)
    });

    test("commitment NOT in ASP tree → fail", async () => {
        // ... (略)
    });
});

4.3 tests/deposit.test.js

const { wasm: wasm_tester } = require("circom_tester");
const { buildPoseidon } = require("circomlibjs");

describe("deposit circuit", () => {
    let circuit, poseidon, F;
    beforeAll(async () => {
        poseidon = await buildPoseidon();
        F = poseidon.F;
        circuit = await wasm_tester("./circuits/deposit.circom");
    }, 120_000);

    test("commitment correctly computed", async () => {
        const nullifier = BigInt("0x123abc");
        const secret = BigInt("0xdef456");
        const expected = BigInt(F.toString(poseidon([nullifier, secret])));

        const w = await circuit.calculateWitness({ nullifier, secret });
        await circuit.checkConstraints(w);

        // witness[1] is first output (commitment) under default circom layout
        // 实际取值可用 circuit.getDecoratedOutput
        expect(w[1].toString()).toBe(expected.toString());
    });

    test("nullifier > 2^252 should fail", async () => {
        // 大于 2^252
        const nullifier = (BigInt(1) << BigInt(253));
        const secret = BigInt(1);
        await expect(circuit.calculateWitness({ nullifier, secret }))
            .rejects.toThrow();
    });
});

5. 编译与 setup

5.1 编译命令

# 安装依赖
npm install -g circom@2.1.6 snarkjs@0.7.x
npm install circomlib circomlibjs circom_tester

# 编译 withdraw 电路 (主)
circom circuits/withdraw.circom \
    --r1cs --wasm --sym \
    -o build/ \
    -l node_modules

# 输出:
# build/withdraw.r1cs
# build/withdraw_js/withdraw.wasm
# build/withdraw.sym

5.2 Trusted setup ceremony 选择

选项大小适合 constraints来源
pot12_final.ptau9 MB≤ 4096Hermez
pot15_final.ptau73 MB≤ 32768Hermez
pot17_final.ptau290 MB≤ 131072Hermez

我们的 withdraw 电路 ~10412 constraints,用 pot15_final.ptau(覆盖到 2^15 = 32768)。

# 下载(已被广泛验证的 Hermez 仪式产物)
wget https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_15.ptau \
    -O build/pot15_final.ptau

# Phase 2: 电路特定 setup
snarkjs groth16 setup \
    build/withdraw.r1cs \
    build/pot15_final.ptau \
    build/withdraw_0000.zkey

# 贡献随机性(生产中应多人贡献,dev 阶段一人即可)
snarkjs zkey contribute \
    build/withdraw_0000.zkey \
    build/withdraw_final.zkey \
    --name="momoweb3 dev contribution" \
    -v -e="$(openssl rand -base64 32)"

# 导出 verification key + Solidity verifier
snarkjs zkey export verificationkey \
    build/withdraw_final.zkey \
    build/withdraw_vkey.json

snarkjs zkey export solidityverifier \
    build/withdraw_final.zkey \
    contracts/Verifier.sol

生成的 Verifier.sol 是 Day 261 直接使用的。

5.3 编译时间预算

步骤M1 笔记本时间备注
circom withdraw.circom~30sR1CS 生成
groth16 setup~60sconstraint 越多越慢
zkey contribute~30s
export solidityverifier<5s
TOTAL~2分钟一次性

6. 关键陷阱与最佳实践

6.1 Under-constrained 检测工具

# circomspect (静态分析)
npm install -g circomspect
circomspect circuits/withdraw.circom

# 期待输出:no warnings
# 如果出现 "signal X is not constrained", 立即修复

6.2 R1CS 大小检查

snarkjs r1cs info build/withdraw.r1cs

# 期待:
# # of Wires: ~10500
# # of Constraints: ~10412
# # of Private Inputs: ~84
# # of Public Inputs: 7

如果 constraints 比预期多 50%,说明有 redundant 约束(如重复 hash)。

6.3 与 Tornado 历史 bug 对比

Tornado 早期 bug我们的防护
pathIndex 未约束 booleanpathIndex * (pathIndex - 1) === 0 已在 HashLevel
recipient 等 public signal 未参与 R1CSrecipientSquare <== recipient * recipient 强制约束
Poseidon parameter 不匹配circomlib/poseidon.circom,与 Solidity Poseidon precompile 一致
nullifier alias 攻击Num2BitsStrict(252) 限制

7. 性能 benchmark

实测(M1 MacBook Pro, 16GB):

# Witness 生成
time node build/withdraw_js/generate_witness.js \
    build/withdraw_js/withdraw.wasm \
    input.json witness.wtns

# 实际:~2-3秒

# Proof 生成
time snarkjs groth16 prove \
    build/withdraw_final.zkey \
    witness.wtns \
    proof.json public.json

# 实际:~12-18秒(CPU-bound)

在浏览器中(snarkjs WASM),M1 笔记本:~20-25秒。Mobile 浏览器(iPhone 14):~40-60秒。


8. 输出 publicSignals 格式

[
    "12345...678",   // root
    "98765...432",   // aspRoot
    "11223...344",   // nullifierHash
    "11111...111",   // recipient (uint160 padded)
    "22222...222",   // relayer
    "100000000000000000",  // fee
    "0"              // refund
]

合约 verifyProof() 接收 uint[7] 作 publicSignals。Day 261 我们写合约时按此顺序消费。


9. 明日预告

Day 261: Solidity 合约实现

明天我们将:

  • 用 snarkjs 生成的 Verifier.sol
  • PrivacyPool.sol:deposit/withdraw + IncrementalBinaryTree state
  • ComplianceOracle.sol:ASP registry + root publishing
  • Relayer.sol:relayer 注册 + stake
  • Hardhat 完整测试(mock 一个 deposit→withdraw 流程)

预计代码量:~600 行 Solidity + ~300 行 TypeScript test


10. 今日复盘

学到的

  • 复合电路(双 Merkle proof)的 constraint 累积接近 10000,已是 Groth16 实用上限的 1/10(大约 100K 是甜区)
  • "binding 但不计算"的 public signal 必须用 quadratic constraint,否则 prover 可替换(这是 ZK 工程头号陷阱)
  • 子电路独立化不只是 modularity,更是 audit 友好(PPv2 的 ASP membership 可独立审计)

卡点

  • 测试用例 4(recipient 替换攻击)实际只能在端到端(合约 + Verifier.sol)层面验证,单元测试 cover 不到
  • circom_tester 的 mocha 集成有时慢(~60s/test),考虑 Day 263 用 jest.config 优化

与未来工作连接

  • Day 264 论文精读:可对比 Aztec Noir 写同电路,记录 LOC 与可读性差异
  • 求职作品:电路代码可以贴 GitHub README,简历加"自实现 Privacy Pools v2 电路"

引用

  • Day 223: Circom 入门(Num2Bits / signal / template)
  • Day 224: MerkleTreeChecker 实现
  • Day 225-226: Poseidon hash + Groth16 setup
  • Day 227: mini Tornado withdraw.circom(本电路的直接前身)
  • Day 256: Privacy Pools v2 论文 — 双 Merkle inclusion 设计来源
  • Tornado Cash circuits/withdraw.circom (commit a1bbfd1, 2020-04)
  • Privacy Pools v2 reference implementation (0xbow.io, 2024)
  • circom 2.x manual: https://docs.circom.io
  • snarkjs README: https://github.com/iden3/snarkjs
  • Hermez Powers of Tau ceremony (2020): https://blog.hermez.io/hermez-cryptographic-setup/