返回 Expert 笔记
Expert Day 261

项目实施 #2 — 链上合约实现

snarkjs Verifier 集成、IncrementalBinaryTree 用法、合约层防 reentrancy/front-running

2027-01-17
Phase 4 - 综合项目 (Day 259-263)
ZKSolidity智能合约实战项目PrivacyPools

日期: 2027-01-17 方向: ZK综合项目 / 隐私交易系统 阶段: Phase 4 - 综合项目 (Day 259-263) 标签: #ZK #Solidity #智能合约 #实战项目 #PrivacyPools


今日目标

类型内容
学习snarkjs Verifier 集成、IncrementalBinaryTree 用法、合约层防 reentrancy/front-running
实操写 4 个 .sol(Verifier/PrivacyPool/ComplianceOracle/Relayer)+ Hardhat 端到端测试
产出contracts/ 目录全部 + test/PrivacyPool.test.ts + Sepolia 部署脚本

1. 合约总体架构

                ┌──────────────────────┐
   user ──────▶ │   PrivacyPool        │  (1 ETH per deposit)
   relayer ──▶  │   - deposit()        │
                │   - withdraw()       │
                │   - merkle tree      │
                │   - nullifiers       │
                └─────┬────────┬──────┘
                      │        │
                      ▼        ▼
            ┌────────────┐  ┌──────────────────┐
            │ Verifier   │  │ ComplianceOracle │
            │ (Groth16)  │  │ - ASP registry   │
            │            │  │ - asp roots      │
            └────────────┘  └──────────────────┘

                ┌──────────────────────┐
   relayer ──▶  │   Relayer Registry   │
                │   - register()       │
                │   - stake/slash      │
                │   - fee schedule     │
                └──────────────────────┘

2. 完整合约代码

2.1 contracts/Verifier.sol

snarkjs zkey export solidityverifier 自动生成。骨架如下(实际文件约 200 行 hardcoded constants):

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.20;

contract Groth16Verifier {
    // BN254 G1
    uint256 constant alphax  = 20491192805390485299153009773594534940189261866228447918068658471970481763042;
    uint256 constant alphay  = 9383485363053290200918347156157836566562967994039712273449902621266178545958;
    // ... (其他常量自动生成: betax1/2, betay1/2, gammax1/2, gammay1/2, deltax1/2, deltay1/2)
    // ... IC[0..7] (7 个 public inputs, 共 8 个 IC 点)

    function verifyProof(
        uint[2] calldata _pA,
        uint[2][2] calldata _pB,
        uint[2] calldata _pC,
        uint[7] calldata _pubSignals  // root, aspRoot, nullifierHash, recipient, relayer, fee, refund
    ) public view returns (bool) {
        assembly {
            // ... pairing check via 0x08 precompile
            // 详细见 snarkjs 自动生成
        }
    }
}

Gas 实测(Tornado 同结构):~250k

2.2 contracts/PrivacyPool.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@zk-kit/imt.sol/IncrementalBinaryTree.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "./Verifier.sol";
import "./ComplianceOracle.sol";

contract PrivacyPool is ReentrancyGuard {
    using IncrementalBinaryTreeLib for IncrementalTreeData;

    // ─── 常量 ───────────────────────────────────────
    uint256 public constant DENOMINATION = 1 ether;
    uint8   public constant LEVELS = 20;
    uint8   public constant ROOT_HISTORY_SIZE = 30;

    // ─── 不可变 ─────────────────────────────────────
    Groth16Verifier   public immutable verifier;
    ComplianceOracle  public immutable oracle;
    address           public immutable admin;       // 仅做紧急 pause(可弃权)

    // ─── Merkle tree state ─────────────────────────
    IncrementalTreeData public tree;
    bytes32[ROOT_HISTORY_SIZE] public rootHistory;
    uint8   public currentRootIndex;

    // ─── Anti-double-spend ─────────────────────────
    mapping(bytes32 => bool) public commitments;
    mapping(bytes32 => bool) public nullifiers;

    // ─── Pause ───────────────────────────────────────
    bool public paused;

    // ─── Events ───────────────────────────────────────
    event Deposit(
        bytes32 indexed commitment,
        uint32 leafIndex,
        uint256 timestamp
    );
    event Withdraw(
        address indexed recipient,
        bytes32 nullifierHash,
        bytes32 aspRoot,
        address indexed relayer,
        uint256 fee
    );
    event Paused(bool isPaused);

    // ─── Modifiers ────────────────────────────────────
    modifier whenNotPaused() {
        require(!paused, "PrivacyPool: paused");
        _;
    }

    constructor(
        address _verifier,
        address _oracle,
        address _admin
    ) {
        verifier = Groth16Verifier(_verifier);
        oracle = ComplianceOracle(_oracle);
        admin = _admin;

        // 初始化 IMT, default zero value 用 keccak("momoweb3-pp-v0.1") % p
        uint256 zero = uint256(keccak256("momoweb3-pp-v0.1")) %
            21888242871839275222246405745257275088548364400416034343698204186575808495617;
        tree.init(LEVELS, zero);
        rootHistory[0] = bytes32(tree.root);
    }

    // ─── DEPOSIT ─────────────────────────────────────
    function deposit(bytes32 _commitment)
        external
        payable
        nonReentrant
        whenNotPaused
    {
        require(msg.value == DENOMINATION, "PrivacyPool: wrong amount");
        require(!commitments[_commitment], "PrivacyPool: dup commitment");

        commitments[_commitment] = true;
        tree.insert(uint256(_commitment));

        // 记录 root 历史
        currentRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE;
        rootHistory[currentRootIndex] = bytes32(tree.root);

        emit Deposit(_commitment, uint32(tree.numberOfLeaves) - 1, block.timestamp);
    }

    function isKnownRoot(bytes32 _root) public view returns (bool) {
        if (_root == bytes32(0)) return false;
        for (uint8 i = 0; i < ROOT_HISTORY_SIZE; i++) {
            if (rootHistory[i] == _root) return true;
        }
        return false;
    }

    // ─── WITHDRAW ────────────────────────────────────
    struct WithdrawArgs {
        uint[2]    pA;
        uint[2][2] pB;
        uint[2]    pC;
        bytes32    root;
        bytes32    aspRoot;
        bytes32    nullifierHash;
        address payable recipient;
        address payable relayer;
        uint256    fee;
        uint256    refund;
    }

    function withdraw(WithdrawArgs calldata args)
        external
        payable
        nonReentrant
        whenNotPaused
    {
        // ─ 1. Sanity ──────────────────────────────────
        require(args.fee <= DENOMINATION / 5, "PrivacyPool: fee too high");
        require(args.refund == msg.value, "PrivacyPool: refund mismatch");
        require(!nullifiers[args.nullifierHash], "PrivacyPool: double spend");

        // ─ 2. Roots ────────────────────────────────────
        require(isKnownRoot(args.root), "PrivacyPool: unknown root");
        require(oracle.isKnownAspRoot(args.aspRoot), "PrivacyPool: unknown asp root");

        // ─ 3. ZK verification ────────────────────────
        uint[7] memory pubSignals = [
            uint256(args.root),
            uint256(args.aspRoot),
            uint256(args.nullifierHash),
            uint256(uint160(args.recipient)),
            uint256(uint160(args.relayer)),
            args.fee,
            args.refund
        ];
        require(
            verifier.verifyProof(args.pA, args.pB, args.pC, pubSignals),
            "PrivacyPool: invalid proof"
        );

        // ─ 4. Mark spent ─────────────────────────────
        nullifiers[args.nullifierHash] = true;

        // ─ 5. Transfer ───────────────────────────────
        // checks-effects-interactions: state already updated above
        uint256 toRecipient = DENOMINATION - args.fee;
        (bool ok1, ) = args.recipient.call{value: toRecipient + args.refund}("");
        require(ok1, "PrivacyPool: recipient transfer fail");

        if (args.fee > 0 && args.relayer != address(0)) {
            (bool ok2, ) = args.relayer.call{value: args.fee}("");
            require(ok2, "PrivacyPool: relayer transfer fail");
        }

        emit Withdraw(
            args.recipient,
            args.nullifierHash,
            args.aspRoot,
            args.relayer,
            args.fee
        );
    }

    // ─── ADMIN (emergency only) ──────────────────────
    function setPaused(bool _paused) external {
        require(msg.sender == admin, "PrivacyPool: not admin");
        paused = _paused;
        emit Paused(_paused);
    }

    // ─── View helpers ────────────────────────────────
    function currentRoot() external view returns (bytes32) {
        return rootHistory[currentRootIndex];
    }
}

关键设计点

  • isKnownRoot:保留最近 30 个 roots,允许用户用旧 root 做 proof(隐私必需 — 如果只能用最新 root,frontrunning 一次 deposit 就会让正在生成的 proof 失效)
  • refund 字段:允许 relayer 给 recipient 一些 ETH 作为 gas(适合从未交互过的全新地址)。refund == msg.value 保证 relayer 自己出钱
  • fee ≤ DENOMINATION / 5:硬性上限 20%,防止 relayer 通过用户错误配置榨取
  • ReentrancyGuard:transfer 用 .call(兼容 EIP-1884),必须防 reentrant
  • Checks-Effects-Interactions:先 mark nullifier,再 transfer

2.3 contracts/ComplianceOracle.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract ComplianceOracle {
    address public admin;

    // ─── ASP Registry ─────────────────────────────
    struct ASP {
        address provider;
        string  name;          // 如 "Chainalysis no-SDN"
        string  policyURI;     // IPFS or HTTPS link to policy doc
        bool    active;
        uint256 registeredAt;
    }
    mapping(uint256 => ASP) public asps;          // aspId => ASP
    uint256 public nextAspId;

    // ─── ASP Roots ────────────────────────────────
    // aspId => merkle root => true
    mapping(uint256 => mapping(bytes32 => bool)) public aspRootHistory;
    // 记录所有曾经被发布过的 root(用于 isKnownAspRoot 快速查)
    mapping(bytes32 => bool) public globalAspRoots;
    mapping(bytes32 => uint256) public rootToAspId;

    // ─── Events ───────────────────────────────────
    event ASPRegistered(uint256 indexed aspId, address indexed provider, string name);
    event ASPDeactivated(uint256 indexed aspId);
    event ASPRootUpdated(uint256 indexed aspId, bytes32 root, uint256 timestamp);

    constructor(address _admin) {
        admin = _admin;
    }

    // ─── ASP 注册(任何人都可注册,admin 可 deactivate)───
    function registerASP(
        string calldata name,
        string calldata policyURI
    ) external returns (uint256 aspId) {
        aspId = nextAspId++;
        asps[aspId] = ASP({
            provider: msg.sender,
            name: name,
            policyURI: policyURI,
            active: true,
            registeredAt: block.timestamp
        });
        emit ASPRegistered(aspId, msg.sender, name);
    }

    // ─── 发布新 root ────────────────────────────────
    function publishRoot(uint256 aspId, bytes32 root) external {
        ASP storage asp = asps[aspId];
        require(asp.provider == msg.sender, "Oracle: not provider");
        require(asp.active, "Oracle: asp inactive");
        require(!globalAspRoots[root], "Oracle: root already published");

        aspRootHistory[aspId][root] = true;
        globalAspRoots[root] = true;
        rootToAspId[root] = aspId;

        emit ASPRootUpdated(aspId, root, block.timestamp);
    }

    // ─── PrivacyPool 调用 ────────────────────────────
    function isKnownAspRoot(bytes32 root) external view returns (bool) {
        return globalAspRoots[root];
    }

    // ─── Admin: 紧急下线 ASP ───────────────────────
    function deactivateASP(uint256 aspId) external {
        require(msg.sender == admin, "Oracle: not admin");
        require(asps[aspId].active, "Oracle: already inactive");
        asps[aspId].active = false;
        emit ASPDeactivated(aspId);
    }

    // ─── View ─────────────────────────────────────────
    function getASP(uint256 aspId) external view returns (
        address provider,
        string memory name,
        string memory policyURI,
        bool active,
        uint256 registeredAt
    ) {
        ASP storage a = asps[aspId];
        return (a.provider, a.name, a.policyURI, a.active, a.registeredAt);
    }
}

设计点

  • 任何人都可 registerASP,但 admin 可 deactivate(ASP 服务质量自治)
  • globalAspRoots 是 PrivacyPool 唯一查询入口(O(1)),不用走 aspId 维度
  • 一个 root 只能发布一次(防 replay);被 deactivate 的 ASP 历史 root 仍有效(避免 user 的 in-flight proof 失败)

2.4 contracts/Relayer.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract RelayerRegistry {
    struct Relayer {
        address operator;
        string  endpoint;       // 如 "https://relayer1.example/submit"
        uint256 stake;
        uint256 feeBps;         // 收取的 fee (basis points, 1% = 100)
        bool    active;
    }

    uint256 public constant MIN_STAKE = 0.1 ether;
    uint256 public constant MAX_FEE_BPS = 500;     // 5%

    mapping(address => Relayer) public relayers;
    address[] public relayerList;

    event RelayerRegistered(address indexed operator, string endpoint, uint256 stake);
    event RelayerWithdrawn(address indexed operator);

    function register(string calldata endpoint, uint256 feeBps) external payable {
        require(msg.value >= MIN_STAKE, "Relayer: stake too low");
        require(feeBps <= MAX_FEE_BPS, "Relayer: fee too high");
        require(relayers[msg.sender].operator == address(0), "Relayer: already registered");

        relayers[msg.sender] = Relayer({
            operator: msg.sender,
            endpoint: endpoint,
            stake: msg.value,
            feeBps: feeBps,
            active: true
        });
        relayerList.push(msg.sender);
        emit RelayerRegistered(msg.sender, endpoint, msg.value);
    }

    function withdrawStake() external {
        Relayer storage r = relayers[msg.sender];
        require(r.operator == msg.sender, "Relayer: not registered");
        require(r.active, "Relayer: already withdrawn");

        r.active = false;
        uint256 amt = r.stake;
        r.stake = 0;
        (bool ok, ) = msg.sender.call{value: amt}("");
        require(ok, "Relayer: withdraw fail");
        emit RelayerWithdrawn(msg.sender);
    }

    function isActive(address operator) external view returns (bool) {
        return relayers[operator].active;
    }

    function listActiveRelayers() external view returns (address[] memory) {
        uint256 cnt;
        for (uint256 i = 0; i < relayerList.length; i++) {
            if (relayers[relayerList[i]].active) cnt++;
        }
        address[] memory out = new address[](cnt);
        uint256 j;
        for (uint256 i = 0; i < relayerList.length; i++) {
            if (relayers[relayerList[i]].active) {
                out[j++] = relayerList[i];
            }
        }
        return out;
    }
}

设计点

  • v0.1 不做 slash 机制(保持简单),仅作为"白名单 + endpoint 发现"
  • MIN_STAKE = 0.1 ETH 防 sybil
  • 客户端从 listActiveRelayers() 拉列表,按 feeBps 排序展示

3. Hardhat 测试

3.1 test/PrivacyPool.test.ts

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

describe("PrivacyPool E2E", function () {
    this.timeout(120_000);

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

    const WASM_PATH = "./build/withdraw_js/withdraw.wasm";
    const ZKEY_PATH = "./build/withdraw_final.zkey";

    before(async () => {
        poseidon = await buildPoseidon();
        F = poseidon.F;
        [admin, alice, bob, relayer, asp] = 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();

        // Relayer 注册
        await relayerReg.connect(relayer).register(
            "https://localhost:3001/submit",
            200,  // 2% fee
            { value: ethers.parseEther("0.1") }
        );
    });

    function poseidonHash(inputs: bigint[]): bigint {
        return BigInt(F.toString(poseidon(inputs)));
    }

    function buildMerkleProof(leaf: bigint, leaves: bigint[], LEVELS = 20) {
        const layer = [...leaves];
        while (layer.length < 2 ** LEVELS) {
            // 用 zero default value 与合约一致
            layer.push(BigInt(ethers.keccak256(ethers.toUtf8Bytes("momoweb3-pp-v0.1")))
                % BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495617"));
        }
        const idx = leaves.indexOf(leaf);
        const pathElements: bigint[] = [];
        const pathIndices: number[] = [];
        let i = idx, cur = [...layer];
        for (let lvl = 0; lvl < LEVELS; lvl++) {
            const isRight = i % 2;
            pathElements.push(cur[i ^ 1]);
            pathIndices.push(isRight);
            const next = [];
            for (let j = 0; j < cur.length; j += 2) {
                next.push(poseidonHash([cur[j], cur[j+1]]));
            }
            cur = next;
            i = Math.floor(i / 2);
        }
        return { root: cur[0], pathElements, pathIndices };
    }

    it("Test 1: register ASP", async () => {
        await oracle.connect(asp).registerASP("Test ASP", "ipfs://test");
        const aspData = await oracle.getASP(0);
        expect(aspData.provider).to.equal(asp.address);
        expect(aspData.active).to.be.true;
    });

    it("Test 2: deposit", async () => {
        const nullifier = ethers.toBigInt(ethers.randomBytes(31));
        const secret = ethers.toBigInt(ethers.randomBytes(31));
        const commitment = poseidonHash([nullifier, secret]);

        const tx = await pool.connect(alice).deposit(
            ethers.zeroPadValue(ethers.toBeHex(commitment), 32),
            { value: ethers.parseEther("1") }
        );
        const r = await tx.wait();
        console.log("deposit gas =", r.gasUsed.toString());
        expect(Number(r.gasUsed)).to.be.lessThan(300_000);

        // 保存 note 给后续用
        (this as any).note = { nullifier, secret, commitment };
    });

    it("Test 3: ASP publishes root including this commitment", async () => {
        const note = (this as any).note;
        const commitment = note.commitment;

        // ASP 把 commitment 加入它的 set
        const aspProof = buildMerkleProof(commitment, [commitment]);
        await oracle.connect(asp).publishRoot(
            0,  // aspId
            ethers.zeroPadValue(ethers.toBeHex(aspProof.root), 32)
        );
        (this as any).aspProof = aspProof;
    });

    it("Test 4: withdraw via relayer", async () => {
        const note = (this as any).note;
        const aspProof = (this as any).aspProof;

        // pool tree(合约里的)— 用 indexer 拉,这里用本地 reconstruct
        const poolProof = buildMerkleProof(note.commitment, [note.commitment]);

        const nullifierHash = poseidonHash([note.nullifier]);

        const input = {
            root: poolProof.root,
            aspRoot: aspProof.root,
            nullifierHash,
            recipient: BigInt(bob.address),
            relayer: BigInt(relayer.address),
            fee: ethers.parseEther("0.02"),  // 2%
            refund: 0n,
            nullifier: note.nullifier,
            secret: note.secret,
            pathElements: poolProof.pathElements,
            pathIndices: poolProof.pathIndices,
            aspPathElements: aspProof.pathElements,
            aspPathIndices: aspProof.pathIndices,
        };

        // 生成 proof
        const t0 = Date.now();
        const { proof, publicSignals } = await snarkjs.groth16.fullProve(
            input, WASM_PATH, ZKEY_PATH
        );
        console.log("proof gen ms =", Date.now() - t0);

        const bobBalanceBefore = await ethers.provider.getBalance(bob.address);
        const relayerBalanceBefore = await ethers.provider.getBalance(relayer.address);

        const tx = await pool.connect(relayer).withdraw({
            pA: [proof.pi_a[0], proof.pi_a[1]],
            pB: [
                [proof.pi_b[0][1], proof.pi_b[0][0]],
                [proof.pi_b[1][1], proof.pi_b[1][0]]
            ],
            pC: [proof.pi_c[0], proof.pi_c[1]],
            root: ethers.zeroPadValue(ethers.toBeHex(BigInt(publicSignals[0])), 32),
            aspRoot: ethers.zeroPadValue(ethers.toBeHex(BigInt(publicSignals[1])), 32),
            nullifierHash: ethers.zeroPadValue(ethers.toBeHex(BigInt(publicSignals[2])), 32),
            recipient: bob.address,
            relayer: relayer.address,
            fee: ethers.parseEther("0.02"),
            refund: 0
        });
        const r = await tx.wait();
        console.log("withdraw gas =", r.gasUsed.toString());
        expect(Number(r.gasUsed)).to.be.lessThan(550_000);

        const bobBalanceAfter = await ethers.provider.getBalance(bob.address);
        // Bob 收到 0.98 ETH (1 - 0.02 fee)
        expect(bobBalanceAfter - bobBalanceBefore).to.equal(ethers.parseEther("0.98"));
    });

    it("Test 5: double-spend should revert", async () => {
        // 直接 reuse Test 4 的 nullifierHash
        // ... (略,与 Test 4 相同 input,期望 revert "double spend")
    });

    it("Test 6: unknown asp root should revert", async () => {
        // 提交一个未注册的 aspRoot
        // ... (略)
    });

    it("Test 7: tampered recipient should fail verifyProof", async () => {
        // 生成 proof 后,把 args.recipient 改成 alice,
        // verifyProof 应返回 false
        // 这是电路 binding 的端到端验证
        // ... (略)
    });
});

3.2 hardhat.config.ts

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import * as dotenv from "dotenv";
dotenv.config();

const config: HardhatUserConfig = {
    solidity: {
        version: "0.8.20",
        settings: {
            optimizer: { enabled: true, runs: 200 },
            viaIR: true   // 提升 verifier inline assembly 优化
        }
    },
    networks: {
        sepolia: {
            url: process.env.SEPOLIA_RPC_URL!,
            accounts: [process.env.PRIVATE_KEY!]
        },
        hardhat: {
            allowUnlimitedContractSize: true   // Verifier ~24KB
        }
    },
    etherscan: {
        apiKey: process.env.ETHERSCAN_API_KEY
    }
};
export default config;

4. Gas 分析

4.1 实测预期(Hardhat local)

操作估算 gas预算状态
deposit()230k≤ 250k
withdraw() (含 verify)480k≤ 500k
registerASP()70k≤ 80k
publishRoot()45k≤ 50k
RelayerRegistry.register()95k≤ 100k

4.2 vs Tornado Cash mainnet 对比

操作我们Tornado Cash 1 ETH 池增量
deposit230k1,100k-870k (用 IMT.sol 库)
withdraw480k360k+120k (双 Merkle root check + ASP oracle call)

注解:deposit 大幅降低主因是 Semaphore IncrementalBinaryTree.sol 比 Tornado 自研 MiMC tree 高效。withdraw 增量是 PPv2 设计代价。


5. 安全考量

5.1 Reentrancy

withdraw().call transfer ETH,必须防 reentrancy。已加 nonReentrant。 另外 checks-effects-interactions:先 nullifiers[h] = true 再 transfer。

5.2 Front-running deposit

场景:Alice 看到 mempool 中 Bob 即将 deposit commitment C1,自己抢先 deposit C1。 问题:Alice 没有 C1 的 (n, s),无法 withdraw。但 Bob 也无法(commitments[c] = true 已 set)。 结果:DoS Bob,Alice 损失 1 ETH。 结论:这个攻击对攻击者无利可图,不是优先修复对象。

Tornado 历史上无此类攻击报告。

5.3 Front-running withdraw

场景:Alice 提交 withdraw tx 到 mempool,Eve 看到 (proof, pubSignals),复制 tx 但替换 recipient = Eve。 防护:电路把 recipient 通过 recipientSquare <== recipient * recipient 绑定到 R1CS。如果 Eve 替换 publicSignals[3],verifyProof 会 fail。

测试:Test 7 验证此场景。

5.4 Merkle root 历史保留窗口

场景:Alice 在 t0 拉 root R0 和 path P0,开始生成 proof(耗时 30s)。在 t0+10s 有人 deposit,root 更新为 R1。Alice 在 t0+30s 提交 (R0, P0),合约必须接受。 防护isKnownRoot 保留 30 个历史 roots,覆盖 ~10 分钟 deposit 频率(取决于 deposit 速率)。 风险:如果 deposit 极频繁(如每秒 1 次),30 个 roots 不够。v0.2 可调到 100。

5.5 ASP 失效

场景:ASP X 被 admin deactivate,但已发布的 roots 仍在 globalAspRoots。Alice 用 X 的旧 root 仍可 withdraw。 评估:这是设计意图。deactivate 的语义是"未来不再接受新 root",不是"作废历史"。如果作废历史,会影响 in-flight proof。 v0.2 改进:admin 可以 mark 某个具体 root 为 frozen(例如发现 SDN 漏入)。


6. 部署脚本

6.1 scripts/deploy.ts

import { ethers } from "hardhat";

async function main() {
    const [deployer] = await ethers.getSigners();
    console.log("Deploying with:", deployer.address);

    // 1. Verifier
    const Verifier = await ethers.getContractFactory("Groth16Verifier");
    const verifier = await Verifier.deploy();
    await verifier.waitForDeployment();
    console.log("Verifier:", await verifier.getAddress());

    // 2. ComplianceOracle
    const Oracle = await ethers.getContractFactory("ComplianceOracle");
    const oracle = await Oracle.deploy(deployer.address);
    await oracle.waitForDeployment();
    console.log("Oracle:", await oracle.getAddress());

    // 3. PrivacyPool
    const Pool = await ethers.getContractFactory("PrivacyPool");
    const pool = await Pool.deploy(
        await verifier.getAddress(),
        await oracle.getAddress(),
        deployer.address
    );
    await pool.waitForDeployment();
    console.log("PrivacyPool:", await pool.getAddress());

    // 4. RelayerRegistry
    const Relayer = await ethers.getContractFactory("RelayerRegistry");
    const relayer = await Relayer.deploy();
    await relayer.waitForDeployment();
    console.log("RelayerRegistry:", await relayer.getAddress());

    // 5. 注册一个 default ASP(用 deployer 测试)
    const tx = await oracle.registerASP(
        "DevTest ASP (no filter)",
        "ipfs://QmDevTest"
    );
    await tx.wait();
    console.log("Default ASP registered as aspId=0");

    // 输出地址 JSON
    const addresses = {
        verifier: await verifier.getAddress(),
        oracle: await oracle.getAddress(),
        pool: await pool.getAddress(),
        relayer: await relayer.getAddress(),
        deployedAt: new Date().toISOString()
    };
    require("fs").writeFileSync(
        "./deployments/sepolia.json",
        JSON.stringify(addresses, null, 2)
    );
}

main().catch((e) => { console.error(e); process.exit(1); });

6.2 部署命令

# 编译
npx hardhat compile

# 测试
npx hardhat test

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

# Etherscan 验证
npx hardhat verify --network sepolia <PrivacyPool address> \
    <verifier> <oracle> <admin>

7. 防 Slither 警告

slither contracts/

# 期待:
# - 0 High severity
# - 0 Medium severity
# - 一些 Informational(如 "unused parameter" — verifier 自动生成)

# 重点检查项:
# - reentrancy: 已用 ReentrancyGuard ✓
# - low-level call: 已 require(ok) ✓
# - timestamp dependence: 仅 used in event, 无 logic 依赖 ✓
# - tx.origin: 无使用 ✓

8. ABI 输出(前端用)

# 生成 frontend 用的 ABI
npx hardhat compile
mkdir -p ../frontend/src/abi
cp artifacts/contracts/PrivacyPool.sol/PrivacyPool.json ../frontend/src/abi/
cp artifacts/contracts/ComplianceOracle.sol/ComplianceOracle.json ../frontend/src/abi/
cp artifacts/contracts/Relayer.sol/RelayerRegistry.json ../frontend/src/abi/

9. 明日预告

Day 262: 前端集成

明天我们将:

  • 用 Next.js + RainbowKit + wagmi 写 DApp 框架
  • client/proof.ts:调用 snarkjs WASM 生成 proof
  • client/merkle.ts:本地 reconstruct Merkle path(订阅 Deposit events)
  • client/relayer.ts:HTTP POST 到 relayer service
  • components/DepositForm.tsx + WithdrawForm.tsx
  • 加密 note 用 password + AES-GCM 存 IndexedDB

预计代码量:~600 行 TypeScript + React


10. 今日复盘

学到的

  • IncrementalBinaryTree.sol 的 gas 优势(vs 自实现 ~5x)
  • ZK verifier 在合约层的标准 ABI(4 个 array param + public signals array)
  • ASP root 双层映射(aspId → root + global root)的查询效率取舍

卡点

  • Hardhat 测试中 snarkjs.groth16.fullProve 在 Mocha worker 中偶尔卡住(解决:this.timeout(120_000)
  • viaIR: true 增加编译时间 3 倍,但 verifier inline assembly 必需

与未来工作连接

  • Day 263 端到端测试:把 Test 5/6/7 写完
  • 求职作品:合约部分可以贴 Etherscan verified link

引用