零知识协议让证明者在不泄露背后见证数据的情况下,使验证者相信某个陈述为真。本文区分协议的保证(完备性、可靠性、零知识性)与围绕证明系统、设置和链上验证的工程选择。随后介绍一个可运行的本地证明与验证循环,以及一套可复现的证明生成与验证延迟测量流程。在验证层选择任何 RPC 或节点提供商之前,可用本文设计、测试和排查 ZK 集成问题。
范围与直接答案
零知识协议是一种方法,一方(证明者)使另一方(验证者)相信某个陈述为真,而不传递该陈述真实性之外的任何信息。在实践中,大多数工程讨论指的是零知识知识证明:证明者展示自己知道满足某个公开关系的秘密见证,例如知道公钥对应的私钥,或知道哈希到已发布承诺的输入。
重要的工程区分在于协议的数学保证与其周边系统。协议定义什么是有效证明。系统定义证明如何生成、在哪里验证、存在哪些设置假设,以及工作流增加多少延迟和成本。本文聚焦第二部分:如何构建、测试和测量零知识工作流,而不把任何单一库或链视为唯一正确答案。
- 完备性:拥有有效见证的诚实证明者能够说服验证者。
- 可靠性:没有有效见证的证明者无法说服验证者,除非概率可忽略。
- 零知识性:验证者除陈述真实性外学不到任何东西,形式化为存在能产生不可区分记录的模拟器。
前提条件与假设
以下示例假设本地开发环境使用 Node.js 20 或更高版本以及 Python 3.10 或更高版本。它们使用占位符 RPC_URL 表示你控制的任何 JSON-RPC 端点,并且不假设特定证明系统。目标是建立一个可复现的循环,你可以将其适配到 Groth16、PLONK、STARKs,或 snarkjs、arkworks 等库,或 zkVM 工具链。
你还应具备运行验证器合约或原生验证器二进制文件的方式。如果计划在链上验证,需要一个有资金的开发账户和测试网络。关于端点选择和网络可用性,请使用 OnFinality API 服务和受支持网络目录,而不是硬编码公共端点。
- 编码陈述和见证的电路或程序。
- 可信设置或透明设置产物,取决于证明系统。
- 根据公开输入检查证明的验证器。
- 记录墙钟时间和资源使用情况的测量工具。
心智模型:陈述、见证与公开输入
每个零知识协议都有三个组成部分。陈述是你声称的内容,例如“我知道一个原像 x 使得 H(x) = y”。见证是秘密 x。公开输入是验证者看到的值,例如 y 和电路标识符。证明是一个短对象,将公开输入与见证的存在绑定。
这种分离很重要,因为大多数集成错误来自公开输入不匹配,而非密码学损坏。如果验证者收到的哈希、Merkle 根或域分隔符与证明者使用的不一致,即使证明本身对证明者的输入有效,验证也会失败。将公开输入视为 API 契约:确定性地序列化它们,记录其顺序,并独立于证明进行测试。
- 证明者:接收见证和公开输入,输出证明。
- 验证者:接收证明和公开输入,输出接受或拒绝。
- 设置:生成证明密钥和验证密钥;其信任假设取决于证明系统。
- 记录:交互式协议可通过 Fiat-Shamir 变换变为非交互式,该变换将记录哈希为挑战。
交互式与非交互式协议
交互式协议在证明者和验证者之间交换消息,通常由验证者选择挑战。它们适用于教学以及双方都在线的场景,例如多方计算和某些认证流程。非交互式证明将交互压缩为单条消息,这使得链上验证变得实用。
Fiat-Shamir 启发式用记录的哈希替换验证者的随机挑战。这是一个真实的工程决策,而非形式问题:哈希必须覆盖影响陈述的每个元素,包括电路标识符和所有公开输入。遗漏字段可能导致证明可延展性或域混淆。审查 ZK 集成时,先检查记录定义,再检查曲线算术。
可运行的本地证明与验证循环
以下 Node.js 脚本是一个测试工具,而非密码学实现。它定义了证明者和验证者接口,运行确定性测试向量,并记录时间。将占位函数替换为所选库的调用。结构才是关键:生成证明、验证证明,并断言被篡改的公开输入会被拒绝。
使用 node zk-harness.mjs 运行。如果验证失败或篡改输入被接受,脚本以非零状态退出,因此适合 CI。
// zk-harness.mjs
import { performance } from 'node:perf_hooks';
// Replace these with your library's real functions.
async function prove(witness, publicInputs) {
// Example shape only. Do not use as cryptography.
return { proof: 'PROOF_PLACEHOLDER', publicInputs };
}
async function verify(proof, publicInputs) {
// Example shape only. Replace with real verifier.
return proof.publicInputs.hash === publicInputs.hash;
}
const witness = { secret: 'WITNESS_PLACEHOLDER' };
const publicInputs = { hash: 'PUBLIC_HASH_PLACEHOLDER', circuitId: 'demo-v1' };
const t0 = performance.now();
const proof = await prove(witness, publicInputs);
const t1 = performance.now();
const ok = await verify(proof, publicInputs);
const t2 = performance.now();
console.log(JSON.stringify({
proveMs: t1 - t0,
verifyMs: t2 - t1,
accepted: ok
}, null, 2));
if (!ok) process.exit(1);
const tampered = { ...publicInputs, hash: 'TAMPERED_HASH' };
const tamperedOk = await verify(proof, tampered);
if (tamperedOk) {
console.error('Verifier accepted tampered public inputs');
process.exit(1);
}
console.log('Tampered input rejected as expected');通过 JSON-RPC 验证证明
如果验证器是合约,证明和公开输入被编码到交易或 eth_call 中。先使用 eth_call 检查验证器的返回值,而不消耗 gas。下面的 curl 示例使用占位符 RPC_URL 和占位符 calldata;使用库的 ABI 编码器生成 calldata。
对于生产验证流量,选择具有可预测延迟和足够速率限制的端点。OnFinality 的 RPC 定价页面解释了套餐维度,当验证对延迟敏感或流量高时,专用节点选项是合适的。
curl -s "$RPC_URL" \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_call",
"params": [
{
"to": "VERIFIER_CONTRACT_ADDRESS",
"data": "0xENCODED_PROOF_AND_PUBLIC_INPUTS"
},
"latest"
]
}'预期验证结果与失败特征
正确的验证器对有效证明返回 true 或非零状态,对无效证明返回 false 或回滚。在本地测试工具中,预期输出是一个包含 proveMs 和 verifyMs 值以及 accepted: true 的 JSON 对象,随后是篡改输入被拒绝的消息。在 RPC 示例中,成功的 eth_call 返回十六进制编码的布尔值或状态值;回滚通常意味着证明或公开输入与验证器的期望不匹配。
不要将单次成功运行视为正确性的证据。至少对三种情况运行测试工具:有效证明、更改一个公开输入的证明,以及为不同电路标识符生成的证明。第三种情况能捕获第二种情况遗漏的域分隔错误。
- 验证器无原因回滚:通常是 calldata 格式错误或 ABI 顺序错误。
- 验证器返回 false:公开输入不匹配或证明是为不同密钥生成的。
- 证明生成成功但验证失败:检查设置产物和电路版本。
- 间歇性失败:如果验证在链上,检查 nonce 管理和交易排序。
可复现的测量流程
只有可复现的测量才有用。记录证明系统、电路大小、库版本、硬件,以及证明者在浏览器、服务器还是原生二进制中运行。预热后每个案例至少运行 30 次,报告中位数和 p95,而非单次运行。保持见证和公开输入在多次运行中固定,以便测量系统而非输入方差。
下表是模板。用你自己的测量值填充;不要复制其他项目的数字。交付物是流程,而非声称的基准。
- 预热:丢弃 5 次运行。
- 测量运行:每个案例 30 次。
- 报告:中位数、p95、最小值、最大值和标准差。
- 环境:CPU 型号、RAM、操作系统、库提交和证明系统参数。
结果表模板
| 案例 | 证明系统 | 电路约束数 | 证明中位数 (ms) | 证明 p95 (ms) | 验证中位数 (ms) | 验证 p95 (ms) | 接受 |
|---|---|---|---|---|---|---|---|
| 有效证明 | YOUR_SYSTEM | YOUR_COUNT | MEASURED | MEASURED | MEASURED | MEASURED | true |
| 篡改输入 | YOUR_SYSTEM | YOUR_COUNT | n/a | n/a | MEASURED | MEASURED | false |
| 错误电路 ID | YOUR_SYSTEM | YOUR_COUNT | MEASURED | MEASURED | MEASURED | MEASURED | false |
如果在链上验证,添加第二个表记录 gas 使用量和确认延迟。对于给定的验证器和输入大小,gas 是确定性的,而确认延迟取决于网络和端点。分别测量两者,以便正确归因回归。
故障排查与局限性
最常见的集成失败不是密码学破解。它们是公开输入不匹配、过时的设置产物、非确定性序列化,以及针对错误电路版本部署的验证器合约。在清单中将电路、证明密钥和验证器地址固定在一起,当其中任何一项更改而没有相应版本升级时,让 CI 失败。
零知识协议也有实际局限性。证明生成可能消耗大量 CPU 和内存,可信设置引入必须管理的假设,透明设置以更大的证明或更慢的验证换取设置信任。零知识不隐藏元数据,如时间、IP 地址或交易图模式。如果你的威胁模型包括这些,需要额外的隐私技术。
- 在清单中固定电路、密钥和验证器版本。
- 在 CI 中测试篡改输入和错误域分隔符。
- 分别测量证明生成和验证。
- 记录设置假设以及谁可以影响它们。
- 不要假设零知识意味着匿名。
生产集成的后续步骤
从本地测试工具开始,然后使用受控 RPC 端点将验证移至测试网络。比较不同端点配置的验证延迟,并将结果记录在你自己的表中。如果验证量增长,评估专用节点,以免证明检查与无关流量竞争。关于端点和提供商选择,RPC Assistant 关于区块链节点托管和多链 RPC 端点的指南是此方法的有用补充。
最后,将零知识协议视为更大可验证管道中的一个组件。证明的可信度取决于电路、设置、验证器部署以及馈送公开输入的数据。独立审查每一层,并将测量工具保留在版本控制中,以便未来更改可以与已知基线进行比较。