引言:为什么开发者需要掌握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满足公开的约束关系。
第二部分: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 电路需求分析
我们要实现一个隐私转账电路,满足以下需求:
余额证明 :发送方的余额必须大于等于转账金额
零和约束 :资金守恒,输入总额等于输出总额
范围证明 :金额不能为负数,必须在合理范围内
隐私保护 :实际余额和转账金额在证明中隐藏
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 安全注意事项
随机性 :可信设置必须使用足够多的熵源,实际应用中应使用多方参与的方式。
电路约束完整性 :确保所有输入都被正确约束,防止恶意输入。
定时攻击 :验证函数应使用固定时间的配对操作。
总结:从这里继续你的ZK开发之旅
本文涵盖了使用Circom和SnarkJS开发零知识证明应用的核心流程:电路设计、编译、证明生成和合约集成。通过一个隐私转账电路的实战案例,你应该已经理解了ZK应用开发的基本范式。
继续探索的方向 :
学习Noir语言 :Aztec Network开发的ZK编程语言,语法更接近传统编程
深入zkEVM :了解zkSync、Polygon zkEVM等zkRollup的技术实现
探索zkBridge :跨链ZK证明,连接不同区块链网络
研究PLONK和PLONKish :了解Groth16之外的新型证明系统
零知识证明技术正在快速发展,现在是入场的最佳时机。从本文的示例出发,你可以开始构建自己的隐私保护应用或扩容解决方案。
附录:常用资源
表格
本文面向具备智能合约开发基础的读者,假设你熟悉Solidity和以太坊基本概念。如需补充基础知识,建议先阅读Solidity官方文档。