Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
集成与开发阅读约 12 分钟

零知识证明(ZKP):实用实现与验证指南

通过可运行的代码和诊断流程,了解零知识证明在实践中如何工作,从电路设计到链上验证。

TL;DR

零知识证明(ZKP)允许证明者在不泄露底层秘密的情况下,让验证者相信某个陈述为真。本文首先解释核心心智模型,然后逐步介绍可复现的实现工作流:定义电路、在链下生成证明,并在链上或后端服务中验证。你将找到用于证明生成和验证的可运行代码、常见故障的诊断流程,以及关于何时 ZKP 是正确工具、何时更简单的承诺方案就足够的指导。重点在于方法论和验证,而非推荐特定的证明系统或端点提供商。

ZKP 实现的范围与假设

本指南假设你是一名熟悉基本密码学(哈希函数、公钥签名)的开发者,并且可以访问区块链 RPC 端点以进行链上验证。你不需要具备算术电路的先验经验。目标是从抽象定义——证明者让验证者相信某个陈述为真而不揭示原因——过渡到可测试和可度量的工作流水线。

我们将使用 Circom 和 snarkjs 的简化 zk-SNARK 工作流,它们广泛用于以太坊兼容的验证器。相同的心智模型适用于其他证明系统(STARKs、Bulletproofs、Halo2),尽管工具和证明大小不同。所有代码使用诸如 RPC_URL 和 PRIVATE_KEY 的占位符;切勿提交真实密钥。

  • 已安装 Node.js 18+ 和 npm
  • 基本熟悉 JavaScript 和命令行工具
  • 目标链的 RPC 端点(使用 /en/api-service 比较选项)
  • 拥有测试网账户和测试 ETH 用于合约部署

心智模型:证明者、验证者和见证

零知识证明有三个角色:证明者、验证者和陈述。陈述是公开声明,例如“我知道一个秘密输入,其哈希等于这个公开值”。见证是秘密输入。证明者运行计算生成证明;验证者根据公开陈述检查证明。证明必须满足完备性(真陈述被接受)、可靠性(假陈述被拒绝)和零知识性(证明不泄露关于见证的任何信息)。

在区块链场景中,验证者通常是智能合约。合约不重新执行计算;它只检查简洁证明。这就是 ZKP 实现可验证计算的原因:你可以证明链下计算正确完成,而无需每个节点重复执行。权衡是生成证明计算成本高,而验证成本低。

从陈述到电路:定义约束

电路是有限域上的一组算术约束。每个约束是见证必须满足的方程。例如,要证明知道原像 x 使得 hash(x) = h,你会将哈希函数实现为电路,并约束其输出等于公开的 h。证明者提供 x;电路内部检查哈希。

电路设计是大多数 ZKP 项目投入精力的地方。约束成本高昂:更多约束意味着更大的证明密钥和更慢的证明生成。你应该通过使用高效哈希函数(例如,在电路内使用 Poseidon 而不是 SHA-256)和避免不必要的范围检查来最小化约束。公开输入是验证者看到的值;其他一切都是私有的。

// example.circom
pragma circom 2.0.0;

include "circomlib/circuits/poseidon.circom";

template PreimageProof() {
    signal input preimage;
    signal input hash;
    component hasher = Poseidon(1);
    hasher.inputs[0] <== preimage;
    hash === hasher.out;
}

component main = PreimageProof();

编译电路并生成可信设置

编写电路后,将其编译为 Rank-1 约束系统(R1CS)和 WebAssembly 见证生成器。然后运行可信设置以生成证明密钥和验证密钥。对于开发,本地 Powers of Tau 仪式就足够了;对于生产,你必须使用多方仪式或透明证明系统,以避免单点有毒废物。

以下命令假设你已全局安装 circom 和 snarkjs。输出文件是 circuit.r1cs、circuit.wasm 和 circuit.zkey。确保 .zkey 文件安全;生成证明需要它。

circom example.circom --r1cs --wasm --sym
snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v
snarkjs groth16 setup example.r1cs pot12_final.ptau circuit_0000.zkey
snarkjs zkey contribute circuit_0000.zkey circuit_final.zkey --name="My contribution" -v
snarkjs zkey export verificationkey circuit_final.zkey verification_key.json

使用 JavaScript 在链下生成证明

证明生成接收见证(私有输入)和公开输入,运行电路,并输出证明和公开信号。以下代码在 Node.js 中使用 snarkjs。它从编译的 WASM 读取见证计算器,计算见证,并生成 Groth16 证明。证明是一个 JSON 对象,可以发送到验证器合约或后端服务。

注意,证明生成可能需要几秒到几分钟,具体取决于电路大小。对于大型电路,你可能需要专用证明服务或具有足够内存的机器。示例使用小型电路进行演示。

const snarkjs = require("snarkjs");
const fs = require("fs");

async function generateProof() {
  const wasm = "example_js/example.wasm";
  const zkey = "circuit_final.zkey";
  const input = { preimage: 12345, hash: "0x..." }; // replace with actual Poseidon hash
  const { proof, publicSignals } = await snarkjs.groth16.fullProve(input, wasm, zkey);
  fs.writeFileSync("proof.json", JSON.stringify(proof, null, 2));
  fs.writeFileSync("public.json", JSON.stringify(publicSignals, null, 2));
  console.log("Proof generated");
}

generateProof().catch(console.error);

使用 Solidity 验证器在链上验证证明

snarkjs 可以从验证密钥导出 Solidity 验证器合约。合约暴露一个 verifyProof 函数,接收证明和公开信号并返回布尔值。将此合约部署到目标链并从应用程序调用它。验证 gas 成本通常为几十万 gas,具体取决于证明系统和公开输入的数量。

以下脚本使用 ethers.js 调用验证器合约。将 RPC_URL、PRIVATE_KEY 和 VERIFIER_ADDRESS 替换为你的值。证明和公开信号从上一阶段生成的文件中读取。

const { ethers } = require("ethers");
const fs = require("fs");

async function verifyOnChain() {
  const provider = new ethers.JsonRpcProvider("RPC_URL");
  const wallet = new ethers.Wallet("PRIVATE_KEY", provider);
  const verifierAddress = "VERIFIER_ADDRESS";
  const abi = ["function verifyProof(uint256[2] a, uint256[2][2] b, uint256[2] c, uint256[] publicSignals) public view returns (bool)"];
  const verifier = new ethers.Contract(verifierAddress, abi, wallet);
  const proof = JSON.parse(fs.readFileSync("proof.json"));
  const publicSignals = JSON.parse(fs.readFileSync("public.json"));
  const isValid = await verifier.verifyProof(proof.pi_a, proof.pi_b, proof.pi_c, publicSignals);
  console.log("Proof valid:", isValid);
}

verifyOnChain().catch(console.error);

预期验证结果及其解读

当证明有效时,验证器合约返回 true,交易(如果你提交)成功。如果证明无效,合约返回 false 或回滚。你应该测试两种情况:使用正确见证生成证明,然后尝试用篡改的公开信号进行验证。后者应该失败,确认可靠性。

对于链下验证,snarkjs 提供了一个 verify 命令,根据验证密钥检查证明。这在部署前调试时很有用。有效证明的预期输出是“OK”,无效证明则报错。

snarkjs groth16 verify verification_key.json public.json proof.json

常见 ZKP 故障的诊断流程

大多数 ZKP 故障分为三类:见证生成错误、证明生成错误和验证不匹配。首先检查见证是否满足所有约束。直接运行见证计算器并检查输出。如果见证正确,确保证明密钥与电路匹配;.zkey 和 .r1cs 之间的不匹配将导致证明生成失败或产生无效证明。

如果链上验证失败但链下验证成功,检查公开信号是否正确编码为 Solidity 验证器。Groth16 证明使用椭圆曲线点,必须作为 uint256 数组传递;常见错误是交换坐标顺序或使用十进制字符串而不是十六进制。还要验证验证器合约是从相同的验证密钥导出的。

  • 见证错误:约束未满足——检查输入类型和域模数。
  • 证明生成错误:缺少 .zkey 或电路错误——重新生成密钥。
  • 验证不匹配:公开信号顺序或编码——与 snarkjs 输出比较。
  • gas 估算失败:证明格式与验证器不兼容——重新导出合约。

ZKP 的局限性与权衡

ZKP 功能强大但并非免费。证明时间和内存随电路大小增长;复杂计算(例如,大型 Merkle 证明或 zkEVM)可能需要专用硬件。可信设置引入信任假设,除非你使用透明系统。链上证明验证成本低,但证明本身必须传输和存储,这会消耗 calldata gas。

零知识证明也不能解决数据可用性问题:如果见证不可用,证明可能有效但底层数据无法恢复。对于许多应用,更简单的承诺方案(例如,哈希)就足够且成本低得多。当你需要证明私有数据的属性而不泄露它,或需要对繁重计算进行简洁验证时,使用 ZKP。

下一步:将 ZKP 集成到你的技术栈

一旦你有了可用的证明流水线,下一步是将其与应用程序的后端和智能合约集成。考虑证明生成的位置(客户端、服务器端或证明服务)以及如何处理故障。对于链上验证,你需要可靠的 RPC 端点;你可以浏览 /en/api-service 了解托管选项,或 /en/dedicated-node 了解专用基础设施。如果你要部署到特定网络,请查看 /en/networks 了解支持的链。

对于生产使用,审计你的电路和验证器合约,并考虑对约束进行形式化验证。在目标硬件和链上测量证明时间和验证 gas;这些数字差异很大。你可以使用下面的结果表流程记录自己的测量结果。

  • 在代表性硬件上测量电路的证明时间。
  • 记录目标链上每个证明的验证 gas 成本。
  • 使用无效见证进行测试以确认可靠性。
  • 记录可信设置仪式和密钥管理。

ZKP 流水线的可复现测量流程

要比较证明系统或电路设计,运行受控基准测试。使用固定的电路和见证,在同一台机器上测量以下内容:见证生成时间、证明生成时间、证明大小和链上验证 gas。每个测量至少运行五次并报告中位数。不要依赖其他团队发布的基准;你的约束和硬件会有所不同。

创建如下所示的结果表,并用你自己的测量结果填写。该表是模板;不要编造数字。对于 gas 测量,使用本地分叉或测试网,并记录 verifyProof 交易使用的 gas。对于证明大小,记录证明 JSON 的字节长度。

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始