Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
网络与协议指南阅读约 11 分钟

零知识证明协议:ZKP 如何工作以及如何验证

一份关于零知识证明协议的实用指南:证明者-验证者模型、交互式与非交互式证明、可运行的验证工作流,以及如何衡量证明与验证成本。

TL;DR

零知识证明协议是一种过程:证明者在不泄露背后秘密见证的情况下,使验证者相信某个陈述为真。该协议必须满足三个性质:完备性、可靠性和零知识性。本文解释了证明者-验证者的心智模型,对比了交互式与非交互式设计,并演示了一个可运行的验证工作流,你可以将其适配到自己的电路或证明系统。文章还展示了如何衡量证明生成与验证成本,以便你基于证据而非假设做出基础设施决策。

零知识证明协议究竟保证了什么

零知识证明(ZKP)协议是一种密码学交互,其中一方(证明者)使另一方(验证者)相信某个陈述为真,而不传达除该陈述真实性之外的任何信息。该陈述通常与一个称为见证的秘密输入相关联。例如,陈述可能是“我知道与此公钥对应的私钥”,而见证就是私钥本身。

三个性质定义了一个可靠的协议。完备性意味着拥有有效见证的诚实证明者总能说服诚实的验证者。可靠性意味着不诚实的证明者无法说服验证者相信一个虚假陈述,除非以可忽略的概率。零知识性意味着验证者除了陈述的真实性之外,对见证一无所知,通常通过证明模拟记录与真实记录不可区分来形式化。

这些保证是概率性的,而非绝对的。大多数协议通过重复或通过单个证明(其可靠性误差在密码学上可忽略)将验证者的信心任意接近确定性。当你评估一个用于生产的证明系统时,这一区别很重要:你是在选择一个具体的安全参数,而不是一个二元属性。

  • 完备性:有效见证总能说服验证者。
  • 可靠性:虚假陈述以压倒性概率被拒绝。
  • 零知识性:记录不泄露关于见证的任何信息。
  • 安全参数:设定可靠性误差的重复次数或域大小。

构建前的假设与前提条件

在实现或集成 ZKP 协议之前,确认四件事。第一,精确定义陈述:验证者看到哪些公共输入,哪些私有输入必须保持隐藏?第二,选择与你的约束相匹配的证明系统家族,例如透明设置与可信设置,以及简洁证明与非简洁证明。第三,决定证明是否必须是非交互式的,因为大多数链上和异步系统无法支持实时来回交互。第四,确定证明将在哪里生成和验证,因为证明者成本和验证者成本通常是不对称的。

你还需要一个带有 ZK 库或电路编译器的开发环境、一种序列化证明和验证密钥的方法,以及一个验证者运行时。如果你的验证者在区块链上运行,验证者合约的 gas 成本以及你用来提交和确认交易的 RPC 端点将成为协议操作范围的一部分。关于端点选择,RPC API 服务和 RPC 定价页面描述了托管访问的结构,而网络目录列出了支持的链。

证明者-验证者心智模型

将协议视为两个角色之间的契约。证明者持有见证 w 和公共陈述 x。关系 R(x, w) 定义了陈述何时为真。证明者运行证明算法,输出证明 π。验证者在给定 (x, π) 以及在某些系统中的验证密钥的情况下,运行验证算法并输出接受或拒绝。

关键设计点是验证者永远不需要 w。它只需要足够的公共结构来检查 π 只能由知道有效 w 的人产生。在非交互式系统中,这通常通过从证明者承诺的哈希派生挑战来实现,这种技术称为 Fiat-Shamir 变换。该变换将交互式挑战-响应协议转换为一条消息,任何人都可以在之后验证。

一个有用的类比是带有防篡改印章的密封信封。验证者可以确认印章有效且内容满足公共规则,但无法读取内容。这个类比在可靠性方面不成立:真实协议依赖于计算困难性假设,而非物理印章,因此安全论证必须对照你的库所使用的具体假设进行检查。

交互式与非交互式证明协议

交互式协议要求验证者在多轮中向证明者发送新的随机挑战。它们在概念上简单且通常高效,但要求双方在线并同步。非交互式协议产生单个证明,可由不同方在之后验证,无需进一步通信。这就是非交互式证明在区块链和可验证计算用例中占主导地位的原因。

这种权衡并非没有代价。非交互式证明通常依赖于公共参考字符串或随机预言机模型,设置仪式或哈希假设成为你信任模型的一部分。透明设置避免了可信仪式,但可能增加证明大小或验证时间。当你比较证明系统时,应在相同的陈述和相同的安全参数上进行比较,而不是基于营销宣传。

  • 交互式:多轮、新的验证者挑战、双方在线。
  • 非交互式:单条证明消息、可异步验证、通常通过 Fiat-Shamir。
  • 可信设置:证明更小,但需要仪式且其记录必须被审计。
  • 透明设置:无需仪式,但证明可能更大或验证更慢。

可运行工作流:生成并验证证明

以下 Python 草图使用占位库接口展示了证明工作流的结构。将 zklib 替换为你实际的证明系统库,并根据你的陈述调整电路定义。目标是使数据流明确:公共输入和见证输入,证明和验证密钥输出,验证者仅检查公共数据。

在隔离环境中运行此代码,并记录证明大小、证明时间和验证时间。这三个数字是后续所有基础设施决策的输入。

import time
import zklib  # placeholder for your ZK library

# 1. Define the relation R(x, w)
circuit = zklib.Circuit("knowledge_of_preimage")

# 2. Public input and private witness
public_input = {"commitment": "0xabc..."}
witness = {"secret": "0xdef..."}

# 3. Setup (skip if your system uses a universal or transparent setup)
params = zklib.setup(circuit)

# 4. Prove
start = time.perf_counter()
proof = zklib.prove(circuit, params, public_input, witness)
prove_ms = (time.perf_counter() - start) * 1000

# 5. Verify
start = time.perf_counter()
ok = zklib.verify(circuit, params.verification_key, public_input, proof)
verify_ms = (time.perf_counter() - start) * 1000

print({"accepted": ok, "prove_ms": round(prove_ms, 2), "verify_ms": round(verify_ms, 2), "proof_bytes": len(proof)})
assert ok is True

通过 RPC 端点验证证明

如果你的验证者是智能合约,证明在交易中提交,合约的验证函数返回布尔值或回滚。RPC 端点是传输层,而非信任锚:合约强制执行可靠性,而端点决定你的提交是否被可靠地包含和确认。使用占位端点,切勿在源代码管理中硬编码凭据。

下面的 curl 示例使用 eth_call 从合约读取验证者结果。它假设你的合约暴露了一个接受公共输入和证明字节的视图函数。将函数选择器和编码替换为你验证者的 ABI。

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

预期验证结果及其解读

成功的验证会从验证函数返回 true,或者如果验证改变状态,则返回状态为 0x1 的交易收据。失败的验证返回 false 或回滚并报错。不要将成功的 RPC 响应视为证明有效性:只要节点应答,JSON-RPC 调用就成功了,即使解码结果为 false。始终解码返回值并对其进行断言。

为了可复现的检查,对同一证明至少运行两个独立的验证者实现,例如你的本地库和链上合约。如果它们不一致,错误几乎总是出在编码、公共输入顺序或验证密钥上,而非密码学本身。将确切的公共输入、证明字节和验证密钥哈希与结果一起记录,以便检查可以重复。

  • 解码返回值;不要从 HTTP 200 推断有效性。
  • 对同一证明比较本地和链上验证。
  • 将公共输入、证明哈希和验证密钥哈希与每个结果一起存储。
  • 在任何电路或库升级后重新运行。

衡量证明者与验证者成本

证明系统具有不对称成本。证明通常是昂贵的步骤,而验证被设计为廉价且相对于电路大小恒定或对数增长。在你实际使用的硬件和运行时上测量两者。每次测量至少运行 30 次,丢弃预热运行,并报告中位数和 p95 而非单个样本。

使用如下结果表。不要用借来的数字填充它;从你自己的运行中填充。该表是让你在平等基础上比较证明系统、电路大小和托管选项的工件。

  • | 配置 | 电路大小 | 证明时间中位数 | 证明时间 p95 | 验证时间中位数 | 证明大小 |
  • | 本地库,陈述 A | 你的值 | 你的值 | 你的值 | 你的值 | 你的值 |
  • | 链上验证者,陈述 A | 你的值 | 不适用 | 不适用 | 你的值 | 你的值 |
  • | 本地库,陈述 B | 你的值 | 你的值 | 你的值 | 你的值 | 你的值 |

排查常见协议故障

ZKP 集成中的大多数故障并非密码学破解。它们是证明者和验证者对公共输入的看法不一致、验证密钥过期,或库与合约之间的编码差异。一个在本地验证通过但在链上失败的证明,是逐字段比较序列化字节的强烈信号。

如果证明意外缓慢,检查电路是否比预期大、证明密钥是否在每次运行时重新生成,以及进程是否受内存限制。如果验证意外昂贵,检查验证者是否对每个公共输入进行椭圆曲线配对,以及公共输入是否可以批处理。如果证明间歇性被拒绝,检查输入顺序是否非确定性以及挑战派生是否依赖于可变状态。

  • 本地通过、链上失败:比较公共输入编码和验证密钥哈希。
  • 间歇性拒绝:检查输入顺序和挑战派生。
  • 证明缓慢:确认电路大小并重用证明密钥。
  • 验证成本高:计算配对次数并考虑批处理公共输入。
  • RPC 超时:在日志中将传输故障与验证者拒绝分开。

局限性、权衡与操作边界

零知识证明默认并不会使系统私有。它们隐藏见证,但公共输入、时序和交易元数据仍可能泄露信息。它们也不保证陈述本身有意义:对错误陈述的有效证明仍然是有效证明。像定义电路一样仔细地定义陈述及其公共输入。

在操作上,证明是计算密集型的,通常受益于专用硬件,而验证是轻量级的,可以在适度的基础设施上运行。如果你的验证者在链上,RPC 层成为可靠性依赖:丢失的交易或滞后的节点可能看起来像验证失败。对于高吞吐量的验证者工作负载,专用节点可以减少共享端点的可变性,而学习库涵盖了相关基础设施主题,如节点托管和端点可靠性。

生产级 ZKP 集成的后续步骤

首先在规范文档中冻结陈述、公共输入和安全参数。然后构建本地证明者-验证者测试框架,添加链上验证者,并在代表性电路上运行上述测量表。只有在这些结果稳定后,才应选择托管和端点拓扑。

对于端点决策本身,保留此处的方法论,并使用 RPC Assistant 指南获取具体的提供商和网络选择。合理的顺序是:本地验证协议、衡量成本、然后通过 API 服务或专用基础设施选择托管 RPC 访问,并在网络目录中确认支持的网络。在每次电路、库或验证者升级后重新运行验证和成本检查,以便协议的保证始终与证据挂钩。

永远不用担心基础设施

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

开始