项目实施 #2 — 链上合约实现
snarkjs Verifier 集成、IncrementalBinaryTree 用法、合约层防 reentrancy/front-running
日期: 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 池 | 增量 |
|---|---|---|---|
| deposit | 230k | 1,100k | -870k (用 IMT.sol 库) |
| withdraw | 480k | 360k | +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 生成 proofclient/merkle.ts:本地 reconstruct Merkle path(订阅 Deposit events)client/relayer.ts:HTTP POST 到 relayer servicecomponents/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
引用
- Day 227: mini Tornado MiniTornado.sol — 本合约直接演化
- Day 256: Privacy Pools v2 paper — ASP oracle 设计来源
- Semaphore IncrementalBinaryTree.sol: https://github.com/semaphore-protocol/semaphore
- snarkjs Solidity Verifier 模板:snarkjs/templates/verifier_groth16.sol.ejs
- OpenZeppelin ReentrancyGuard: https://docs.openzeppelin.com/contracts/4.x/api/security
- Tornado Cash audit (ABDK 2019, Trail of Bits 2020) — Reentrancy / front-running 分析