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

ZK 证明:在不泄露知识的前提下证明知识的实用技术指南

了解零知识证明的工作原理、如何在代码中生成和验证它们,以及如何针对真实的 RPC 端点衡量其成本。

TL;DR

零知识证明(ZKP)是一种密码学协议,证明者借此让验证者相信某个陈述为真,同时不泄露该真值之外的任何信息。本指南解释了证明者-验证者的思维模型、交互式证明与非交互式证明的区别,以及在发布 ZK 功能之前应当考虑的可靠性和零知识属性。随后,本指南演示了一个可运行的链下验证循环,以及一套针对你自行控制的 RPC 端点衡量链上验证成本的流程,从而让你能够将文档中描述的协议行为与你实际观测到的数据区分开来。

ZK 证明究竟能保证什么

零知识证明是两方之间的协议:证明者知道秘密见证,验证者检查关于该见证的某个声明。证明使验证者相信该陈述为真,同时不传递该陈述真值之外的任何信息。经典例子是证明你知道某个公开值的离散对数而不公开指数,或者证明一笔交易收支平衡而不泄露金额。

评估任何 ZK 方案时,有三个属性至关重要。完备性意味着拥有有效见证的诚实证明者总能说服验证者。可靠性意味着不诚实的证明者无法以可忽略的概率说服验证者相信一个虚假陈述。零知识意味着验证者除了陈述为真之外学不到任何东西,这通常通过展示一个模拟器能够生成与真实记录不可区分的记录来形式化。

本文涵盖概念、可运行的验证工作流以及一种测量方法。它不声称任何特定证明系统的具体 gas 成本、证明生成时间或网络支持;这些取决于电路、证明后端和链,你应当使用下面的流程自行测量。

  • 证明者:持有私有见证并生成证明。
  • 验证者:根据公开输入检查证明。
  • 见证:证明所涉及的秘密数据。
  • 公开输入:验证者可以看到的陈述。

假设与前提条件

代码示例假设使用 Node.js 18 或更高版本,以及带有 curl 的 shell。对于链上测量部分,你需要一个 RPC 端点,指向部署了验证者合约的网络。使用诸如 RPC_URL 的占位符并提供你自己的端点;如果你正在选择提供商,RPC 端点指南和多链 RPC 端点指南解释了应比较哪些方面,而 RPC API 服务则描述了托管访问。

你还应当知道你的项目使用哪种证明系统。zk-SNARK 通常依赖可信设置并生成较小的证明,而 zk-STARK 避免可信设置且往往生成较大的证明。验证接口各不相同,因此请将这里的代码片段视为需要适配你所用库的测试框架,而非适用于所有方案的即插即用代码。

交互式证明与非交互式证明

在交互式证明中,证明者和验证者交换多条消息。经典例子是阿里巴巴洞穴:证明者反复从验证者随机选择的隧道中走出,经过足够多轮后,验证者便相信证明者知道秘密路径。每一轮都增加可靠性,但要求验证者在线并提供新的随机性。

大多数区块链应用需要非交互式证明,因为验证者合约无法与离线证明者进行多轮对话。Fiat-Shamir 启发式通过从记录哈希中派生验证者的挑战,将交互式证明转换为单条消息。这就是非交互式 ZK 证明有时被称为 NIZK 的原因,也是你通常会在链上验证的形式。

实际后果是,你的安全性现在依赖于哈希函数表现得像随机预言机。如果挑战派生是可预测的或记录存在歧义,恶意证明者可能能够伪造证明。在实现自定义电路时,请仔细审计 Fiat-Shamir 记录的域分离。

代码中的证明者-验证者思维模型

以下 Node.js 示例展示了验证循环的形态,但并不声称是生产级证明系统。它模拟了一个对秘密进行承诺的证明者、一个检查公开承诺的验证者,以及一个应当失败的篡改检查。在集成真实方案时,请将玩具哈希替换为你所用库的 verify 调用。

使用 node zk-harness.mjs 运行它。预期结果是诚实证明通过验证,而篡改后的证明被拒绝。如果两者都通过,说明你的验证逻辑实际上并未将公开输入与证明绑定。

import { createHash } from 'node:crypto';

function commit(secret, publicInput) {
  return createHash('sha256')
    .update(`${publicInput}:${secret}`)
    .digest('hex');
}

function verify(commitment, publicInput, revealedSecret) {
  return commit(revealedSecret, publicInput) === commitment;
}

const publicInput = 'balance>=0';
const secret = 'witness-42';
const commitment = commit(secret, publicInput);

console.log('honest proof verifies:', verify(commitment, publicInput, secret));
console.log('tampered proof verifies:', verify(commitment, publicInput, 'witness-43'));
console.log('wrong public input verifies:', verify(commitment, 'balance>=100', secret));

衡量链上验证成本

验证成本是决定 ZK 功能在给定链上是否可行的关键数字。不要轻信博客文章中的 gas 数值;应针对你自己的验证者合约和你自己的 RPC 端点进行测量。流程是:部署验证者,准备有效证明及其公开输入,通过 eth_call 调用验证者的 verify 函数以确认其返回 true,然后发送真实交易并从收据中读取使用的 gas。

eth_call 步骤是免费的,可以在你花费 gas 之前捕获格式错误的证明。交易步骤则给出实际消耗的 gas。将区块号、证明系统版本和验证者字节码哈希与数值一并记录,以便测量可复现。

使用类似下表的表格记录你的运行结果。单元格有意留空,因为数值取决于你的电路和链;请从你自己的收据中填写。

  • | 运行 | 证明系统 | 公开输入 | eth_call 结果 | 使用的 Gas | 区块 |
  • | --- | --- | --- | --- | --- | --- |
  • | 1 | (你的系统) | (你的输入) | (true/false) | (来自收据) | (区块号) |
  • | 2 | (你的系统) | (你的输入) | (true/false) | (来自收据) | (区块号) |

针对验证者合约的可运行 RPC 检查

下面的 shell 片段通过 JSON-RPC 调用验证者合约。它使用带有预编码 calldata 负载的 eth_call。将 RPC_URL、VERIFIER_ADDRESS 和 CALLDATA 替换为你自己的值。预期结果是一个 JSON 响应,其 result 字段是 ABI 编码的布尔值或验证输出。

如果节点返回错误,请检查合约是否已在该链上的该地址部署,以及 calldata 选择器是否与验证者的函数签名匹配。没有 revert 的 0x 结果通常意味着调用成功但返回了空数据,这往往表明选择器不匹配。

curl -s -X POST "$RPC_URL" \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "eth_call",
    "params": [
      { "to": "VERIFIER_ADDRESS", "data": "CALLDATA" },
      "latest"
    ]
  }'

验证结果并读取收据

发送验证交易后,使用 eth_getTransactionReceipt 获取收据并读取 gasUsed 和 status。status 为 0x1 表示交易成功;0x0 表示交易回滚,但 gas 仍被消耗。将 gasUsed 值与你的 eth_call 结果进行比较:调用不消耗 gas,因此收据才是权威数字。

为了获得可复现的测量结果,请对同一证明至少运行三次并记录每份收据。差异可能来自状态变化、冷存储槽与热存储槽,以及特定区块的 gas 调度。如果你需要高吞吐量验证者的稳定延迟,专用节点可以消除共享端点可能引入的噪声邻居效应。

如果你正在为此工作负载比较提供商,请保持方法固定,只更改端点。RPC 定价页面解释了套餐维度,网络目录列出了支持的链,以便你在基准测试前确认目标链。

排查常见的 ZK 集成故障

大多数 ZK 集成错误可归为几类。证明在链下验证通过但在链上回滚通常意味着公开输入的编码方式不同,例如字段元素以不同的字节序序列化。证明总是验证通过意味着验证者没有绑定公开输入,或者 Fiat-Shamir 挑战遗漏了记录的一部分。证明从不验证通过通常指向验证密钥不匹配,或电路编译参数与部署的验证者不同。

对于 RPC 层面的故障,请区分传输错误与合约回滚。带有 revert 数据的 -32000 类错误意味着节点到达了 EVM 且合约拒绝了调用;超时或连接重置意味着端点没有响应。重试逻辑应区别对待这些情况,因为重试确定性的回滚会浪费时间和 gas。

  • 编码公开输入时检查字节序和字段模数。
  • 确认验证密钥哈希与部署的验证者匹配。
  • 记录完整的 revert 数据,而不仅仅是错误消息。
  • 将传输重试与合约层面的故障分开处理。

局限性与权衡

ZK 证明并非没有代价。证明计算开销很大,且成本随电路规模增长。验证成本较低但仍消耗 gas,具体成本取决于你的方案所需的配对运算或哈希检查。可信设置引入了一个仪式,其被攻破可能破坏可靠性,这就是一些团队尽管证明更大仍选择透明设置的原因。

还有一个需要尊重的隐私边界。ZK 证明隐藏了见证,但公开输入、交易时机和链上元数据仍可能泄露信息。如果你的威胁模型包括时序关联或输入可链接性,仅靠证明是不够的;你还需要设计周围的协议。

最后,工具成熟度参差不齐。一些证明系统拥有稳定且经过审计的验证者,而另一些则快速演进。固定库版本,并在每次升级后重新运行测量流程,因为验证者的变更可能同时改变 gas 成本和安全假设。

ZK 集成的后续步骤

首先写下你想证明的陈述、见证和公开输入。然后选择一种与你的信任模型和验证者目标链相匹配的证明系统。先构建链下测试框架,确认诚实证明通过且篡改证明失败,然后再部署验证者。

验证者上线后,针对稳定端点运行测量流程,并将结果记录在可与团队共享的表格中。如果你需要帮助为你目标链选择端点,RPC 端点指南和 RPC API 服务是合适的起点。对于高吞吐量验证,请评估专用节点并在确定套餐前查看 RPC 定价。

永远不用担心基础设施

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

开始