Circom与SnarkJS实战:构建零知识证明智能合约的完整指南

零知识证明Circom开发入门教程

引言:为什么开发者需要掌握ZK证明技术

在以太坊生态系统中,隐私与扩容一直是两大核心挑战。零知识证明(Zero-Knowledge Proof)技术的成熟,特别是zk-SNARKs的广泛应用,正在为这两个问题提供优雅的解决方案。从Zcash的隐私交易到zkSync、StarkNet等Layer2扩容方案,零知识证明已经从学术理论走向工程实践。

对于智能合约开发者而言,掌握ZK证明技术意味着能够构建真正保护用户隐私的应用,同时享受L2带来的低gas成本优势。Circom作为专门用于编写ZK电路的语言,配合SnarkJS的证明生成工具链,已成为开发ZK应用的主流选择。本文将带你从零开始,搭建完整的ZK合约开发环境,编写一个隐私转账电路,并将其部署到以太坊测试网。

第一部分:理解ZK电路的核心概念

1.1 什么是ZK电路

零知识证明电路(ZK Circuit)是一种特殊的计算电路,它将我们要证明的计算过程转化为一系列约束条件。在传统的程序中,我们编写代码让计算机执行计算;而在ZK电路中,我们编写电路来定义计算必须满足的关系。

理解电路思维是掌握Circom的关键。与传统编程不同,ZK电路具有以下特点:

确定性执行:电路中没有分支和循环,所有输入都必须有确定的值。

约束驱动:电路中的每个计算都必须能被转化为数学约束。

隐私保护:输入信号可以被标记为private,证明者无需透露这些值。

1.2 信号与约束

在Circom中,基本的构建单元是信号(signal)和约束(constraint)。信号代表电路的输入、输出和中间值,约束则定义了它们之间的关系。

circom

pragma circom 2.0.0;

// 一个简单的加法电路
template Adder() {
    // 声明输入信号
    signal input a;
    signal input b;
    
    // 声明输出信号
    signal output c;
    
    // 约束:c = a + b
    c <== a + b;
}

component main = Adder();

这段代码定义了一个最简单的加法电路。<==运算符同时完成了信号赋值和约束生成的双重工作。

1.3 公开信号与私有信号

ZK证明的核心价值在于:证明者可以隐藏某些输入(私有信号),同时让验证者确信这些隐藏值满足特定条件。

circom

template SecretAdder() {
    signal input secretValue;  // 私有输入,验证者看不到
    signal input publicOffset; // 公开输入,验证者可见
    
    signal output result;
    
    // 约束:result = secretValue + publicOffset
    result <== secretValue + publicOffset;
}

在这个例子中,secretValue是私有信号,证明者可以在生成证明时使用任意值,但最终必须确保result满足公开的约束关系。

ZK证明者-验证者交互流程示意图

第二部分:Circom开发环境搭建

2.1 系统依赖

在开始之前,确保你的系统安装了以下依赖:

bash

# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y build-essential gcc g++ make git curl

# Rust(用于编译Circom)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
rustup default stable

# Node.js(用于SnarkJS)
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
npm install -g n
n 18

2.2 安装Circom编译器

Circom是用Rust编写的编译器,负责将.circom文件编译为约束系统。

bash

# 克隆Circom仓库
git clone https://github.com/iden3/circom.git
cd circom

# 切换到稳定版本(v2.1.x)
git checkout v2.1.8

# 编译(可能需要15-30分钟)
cargo build --release
cargo install --path circom

# 将可执行文件加入PATH
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

2.3 安装SnarkJS

SnarkJS是JavaScript实现的zkSNARK工具库,提供证明生成、验证等功能。

bash

# 全局安装SnarkJS
npm install -g snarkjs

# 验证安装
snarkjs --version

2.4 创建项目结构

bash

mkdir -p zk-tutorial
cd zk-tutorial
mkdir circuits src build

# 初始化Node.js项目
npm init -y
npm install circomlib snarkjs

第三部分:编写隐私转账电路

3.1 电路需求分析

我们要实现一个隐私转账电路,满足以下需求:

  1. 余额证明:发送方的余额必须大于等于转账金额
  2. 零和约束:资金守恒,输入总额等于输出总额
  3. 范围证明:金额不能为负数,必须在合理范围内
  4. 隐私保护:实际余额和转账金额在证明中隐藏

3.2 实现余额检查电路

circom

pragma circom 2.0.0;
include "../node_modules/circomlib/circuits/comparators.circom";

// 检查余额是否大于等于转账金额
template BalanceChecker() {
    signal input balance;      // 账户余额(私有)
    signal input amount;       // 转账金额(私有)
    signal input nullifier;    // 废止向量(私有)
    
    signal output isValid;     // 验证结果(公开)
    
    // 使用LessThan验证balance >= amount
    // 即 amount < balance
    component isAmountLessThanBalance = LessThan(64);
    isAmountLessThanBalance.in[0] <== amount;
    isAmountLessThanBalance.in[1] <== balance;
    
    // 余额检查通过时为1
    isValid <== isAmountLessThanBalance.out;
}

template PrivateTransfer(n) {
    // 公共输入
    signal input merkleRoot;          // Merkle树根
    signal input recipientPublicKey;  // 接收方公钥(哈希)
    signal input commitment;          // 新承诺
    
    // 私有输入
    signal input balance;             // 当前余额
    signal input amount;              // 转账金额
    signal input nullifier;           // 废止向量
    signal input secretKey;           // 私钥
    
    // 验证余额足够
    component balanceCheck = BalanceChecker();
    balanceCheck.balance <== balance;
    balanceCheck.amount <== amount;
    balanceCheck.nullifier <== nullifier;
    
    // 金额必须为正
    component amountPositive = LessThan(64);
    amountPositive.in[0] <== 0;
    amountPositive.in[1] <== amount;
    amountPositive.out === 1;
    
    // 零和约束:balance = amount + remainder
    signal remainder;
    remainder <== balance - amount;
    
    // 计算承诺:hash(balance, secretKey)
    component commitmentCalc = Poseidon(2);
    commitmentCalc.inputs[0] <== balance;
    commitmentCalc.inputs[1] <== secretKey;
    
    // 验证承诺匹配
    commitmentCalc.out === commitment;
}

component main {public [merkleRoot, recipientPublicKey, commitment]} = PrivateTransfer(2);

3.3 编译电路

bash

# 编译电路
circom circuits/private_transfer.circom \
    --r1cs \
    --wasm \
    --sym \
    --c \
    -o build

# 查看电路信息
snarkjs r1cs info build/private_transfer.r1cs

输出应类似:

plaintext

[INFO]  snarkJS: 
Constraint system statistics:
# of wires:              4128
# of constraints:         2056
# of labels:              4228
# of outputs:             3
# of inputs (public):     3
# of inputs (private):    5

第四部分:生成和验证证明

4.1 创建Powers of Tau仪式

zkSNARK需要一个可信设置(Trusted Setup)。Powers of Tau仪式用于生成结构化参考字符串( SRS)。

bash

# 第一阶段:Powers of Tau
snarkjs powersoftau new bn128 12 build/pot12_final.ptau -v

# 贡献随机熵
snarkjs powersoftau contribute build/pot12_final.ptau \
    build/pot12_contrib1.ptau \
    --name="First contribution" \
    -e="$(head -c 64 /dev/urandom | xxd -p)"

# 提供第二个贡献(演示用)
snarkjs powersoftau contribute build/pot12_contrib1.ptau \
    build/pot12_contrib2.ptau \
    --name="Second contribution" \
    -e="another-random-entropy"

# 准备最终相位
snarkjs powersoftau prepare phase2 \
    build/pot12_contrib2.ptau \
    build/pot12_final.ptau \
    -v

4.2 生成ZKey

bash

# 初始化ZKey
snarkjs zkey new build/private_transfer.r1cs \
    build/pot12_final.ptau \
    build/transfer_0000.zkey

# 添加贡献
snarkjs zkey contribute build/transfer_0000.zkey \
    build/transfer_final.zkey \
    --name="Developer contribution" \
    -e="$(head -c 64 /dev/urandom | xxd -p)"

# 导出验证密钥
snarkjs zkey export verificationkey build/transfer_final.zkey

4.3 生成证明

javascript

// src/generate_proof.js
const { groth16 } = require("snarkjs");
const fs = require("fs");

async function generateProof() {
    // 准备输入(实际应用中从链上获取)
    const input = {
        merkleRoot: "1234567890...",
        recipientPublicKey: "abcdef1234...",
        commitment: "9988776655...",
        
        // 私有输入
        balance: 1000,
        amount: 250,
        nullifier: "1111222233334444",
        secretKey: "556677889900...",
    };
    
    // 生成证明
    const { proof, publicSignals } = await groth16.fullProve(
        input,
        "build/private_transfer_js/private_transfer.wasm",
        "build/transfer_final.zkey"
    );
    
    // 输出证明和公开信号
    const proofData = {
        proof: {
            a: proof.pi_a,
            b: proof.pi_b,
            c: proof.pi_c
        },
        pubSignals: publicSignals
    };
    
    fs.writeFileSync(
        "build/proof.json",
        JSON.stringify(proofData, null, 2)
    );
    
    console.log("Proof generated successfully!");
    console.log("Public signals:", publicSignals);
}

generateProof().then(() => {
    process.exit(0);
}).catch(err => {
    console.error(err);
    process.exit(1);
});

运行脚本:

bash

node src/generate_proof.js

4.4 验证证明

javascript

// src/verify_proof.js
const { groth16 } = require("snarkjs");
const fs = require("fs");

async function verifyProof() {
    // 加载验证密钥和证明
    const vKey = JSON.parse(
        fs.readFileSync("build/verification_key.json")
    );
    const proofData = JSON.parse(
        fs.readFileSync("build/proof.json")
    );
    
    // 验证
    const isValid = await groth16.verify(
        vKey,
        proofData.pubSignals,
        proofData.proof
    );
    
    console.log("Verification", isValid ? "PASSED ✓" : "FAILED ✗");
    return isValid;
}

verifyProof().then(result => {
    process.exit(result ? 0 : 1);
});

第五部分:在智能合约中集成ZK验证

5.1 生成Solidity验证器

SnarkJS可以自动生成以太坊智能合约形式的验证器:

bash

snarkjs zkey export solidityverifier \
    build/transfer_final.zkey \
    src/zkVerifier.sol

生成的合约结构如下:

solidity

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

contract ZKVerifier {
    // G1点加法
    function pairing(
        uint256[2] memory a,
        uint256[2] memory b,
        uint256[2] memory c,
        uint256[2] memory d,
        uint256[2] memory e,
        uint256[2] memory f,
        uint256[2] memory g,
        uint256[2] memory h
    ) public view returns (bool) {
        // 配对检查实现
    }
    
    function verifyProof(
        uint256[2] memory a,
        uint256[2][2] memory b,
        uint256[2] memory c,
        uint256[3] memory input
    ) public view returns (bool) {
        // 验证逻辑
    }
}

5.2 隐私转账合约实现

solidity

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

import "./ZKVerifier.sol";

contract PrivateTransfer {
    ZKVerifier public verifier;
    
    // 存储已使用的nullifier,防止双花
    mapping(bytes32 => bool) public usedNullifiers;
    
    // Merkle根存储(简化版,实际应使用Merkle树)
    mapping(bytes32 => bool) public validRoots;
    
    // 事件
    event Transfer(
        bytes32 indexed nullifier,
        address indexed recipient,
        uint256 amount
    );
    
    constructor(address _verifier) {
        verifier = ZKVerifier(_verifier);
    }
    
    function transfer(
        // 证明
        uint256[2] memory a,
        uint256[2][2] memory b,
        uint256[2] memory c,
        // 公开输入
        bytes32 merkleRoot,
        address recipient,
        bytes32 commitment,
        // nullifier防止双花
        bytes32 nullifier
    ) external returns (bool) {
        // 1. 验证Merkle根有效
        require(validRoots[merkleRoot], "Invalid merkle root");
        
        // 2. 防止双花
        require(!usedNullifiers[nullifier], "Nullifier already used");
        
        // 3. 验证ZK证明
        uint256[3] memory inputs = [
            uint256(merkleRoot),
            uint256(uint160(recipient)),
            uint256(commitment)
        ];
        
        require(
            verifier.verifyProof(a, b, c, inputs),
            "Invalid proof"
        );
        
        // 4. 标记nullifier已使用
        usedNullifiers[nullifier] = true;
        
        // 5. 执行转账逻辑(实际实现中需处理代币转账)
        emit Transfer(nullifier, recipient, 0);
        
        return true;
    }
    
    // 添加有效的Merkle根
    function addMerkleRoot(bytes32 root) external {
        validRoots[root] = true;
    }
}

5.3 部署和测试

javascript

// scripts/deploy.js
const hre = require("hardhat");

async function main() {
    // 部署验证器
    const Verifier = await hre.ethers.getContractFactory("ZKVerifier");
    const verifier = await Verifier.deploy();
    await verifier.deployed();
    console.log("Verifier deployed to:", verifier.address);
    
    // 部署隐私转账合约
    const PrivateTransfer = await hre.ethers.getContractFactory("PrivateTransfer");
    const transfer = await PrivateTransfer.deploy(verifier.address);
    await transfer.deployed();
    console.log("PrivateTransfer deployed to:", transfer.address);
    
    // 生成测试证明的calldata
    const { calldata } = await snarkjs.groth16.fullProve(
        testInput,
        "build/private_transfer_js/private_transfer.wasm",
        "build/transfer_final.zkey"
    );
    
    // 调用合约
    const tx = await transfer.transfer(
        calldata.a,
        calldata.b,
        calldata.c,
        merkleRoot,
        recipientAddress,
        commitment,
        nullifier,
        { gasLimit: 1000000 }
    );
    
    const receipt = await tx.wait();
    console.log("Transaction hash:", receipt.transactionHash);
}

main()
    .then(() => process.exit(0))
    .catch(err => {
        console.error(err);
        process.exit(1);
    });

第六部分:实践建议与性能优化

6.1 电路编写最佳实践

信号数量最小化:每个信号都会增加约束数量,影响证明生成速度。

circom

// 不推荐:创建过多中间变量
signal temp1 <== a * b;
signal temp2 <== temp1 * c;
signal result <== temp2 + d;

// 推荐:直接组合运算
signal result <== (a * b * c) + d;

使用预编译库:circomlib提供了大量经过优化的电路组件。

circom

// 使用Poseidon哈希(比Keccak更ZK友好)
include "../node_modules/circomlib/circuits/poseidon.circom";

// 使用MimcSponge(高效哈希)
include "../node_modules/circomlib/circuits/mimcsponge.circom";

注意位宽选择:选择合适的位宽可以减少约束数量。

circom

// 如果金额不超过2^32,直接使用32位
component amountCheck = LessThan(32);

// 不需要为64位值使用64位比较器

6.2 证明生成性能优化

使用GPU加速

bash

# 安装GPU支持(需要CUDA)
cargo build --release --features g16-bn128,gpu

批量验证:在Layer2中,聚合多个证明可以大幅降低验证成本。

solidity

// 聚合验证合约示例
contract Aggregator {
    function aggregateProofs(
        Proof[] memory proofs
    ) public view returns (bool) {
        // 批量验证多个证明
    }
}

6.3 安全注意事项

  1. 随机性:可信设置必须使用足够多的熵源,实际应用中应使用多方参与的方式。
  2. 电路约束完整性:确保所有输入都被正确约束,防止恶意输入。
  3. 定时攻击:验证函数应使用固定时间的配对操作。

总结:从这里继续你的ZK开发之旅

本文涵盖了使用Circom和SnarkJS开发零知识证明应用的核心流程:电路设计、编译、证明生成和合约集成。通过一个隐私转账电路的实战案例,你应该已经理解了ZK应用开发的基本范式。

继续探索的方向

  • 学习Noir语言:Aztec Network开发的ZK编程语言,语法更接近传统编程
  • 深入zkEVM:了解zkSync、Polygon zkEVM等zkRollup的技术实现
  • 探索zkBridge:跨链ZK证明,连接不同区块链网络
  • 研究PLONK和PLONKish:了解Groth16之外的新型证明系统

零知识证明技术正在快速发展,现在是入场的最佳时机。从本文的示例出发,你可以开始构建自己的隐私保护应用或扩容解决方案。

附录:常用资源

表格

资源链接
Circom官方文档https://docs.circom.io
SnarkJS文档https://github.com/iden3/snarkjs
circomlib库https://github.com/iden3/circomlib
Zero Knowledge FMhttps://zeroknowledge.fm/
ZK白板系列https://zkhack.dev/

本文面向具备智能合约开发基础的读者,假设你熟悉Solidity和以太坊基本概念。如需补充基础知识,建议先阅读Solidity官方文档。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注