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

什么是零知识证明?ZK 证明系统实用指南

零知识证明技术详解:证明者-验证者模型、完备性、可靠性、零知识性、交互式与非交互式协议,以及如何端到端验证 ZK 证明。

TL;DR

零知识证明(ZKP)是一种密码学协议,证明者向验证者证明某个陈述为真,而不泄露除该陈述真实性之外的任何信息。每个有用的 ZKP 都必须满足三个性质:完备性、可靠性和零知识性。本指南解释了心智模型,对比了交互式与非交互式证明,演示了一个可运行的 Schnorr 风格验证流程,并展示了如何在区块链 RPC 端点上诊断证明验证。

零知识证明的定义与范围

零知识证明是两方之间的协议。证明者持有秘密见证,并希望说服验证者某个公开陈述为真。验证者只了解到该陈述为真,而不知道见证、不知道用于推导它的输入,也不知道任何能让验证者向第三方复现该证明的旁信息。

经典框架来自 Goldwasser、Micali 和 Rackoff 于 1985 年发表的论文《交互式证明系统的知识复杂性》。关键细微之处在于,ZKP 不仅仅是隐藏数据的证明;它是一种除陈述真实性之外不泄露任何知识的证明。这一区别将 ZKP 与加密、哈希或简单承诺区分开来。

本文涵盖协议机制、必须测试的性质以及可复现的验证工作流。它不推荐特定的 ZK 证明服务或特定的 RPC 提供商;关于端点选择,在了解验证者需要什么之后,请使用 RPC 助手。

  • 陈述:被证明的公开声明,例如“我知道 x 使得 g^x = y”。
  • 见证:证明者知道的秘密输入,例如 x。
  • 证明:验证者检查的消息或产物。
  • 验证者:输出接受或拒绝的算法。

每个 ZK 证明必须满足的三个性质

一个协议只有同时满足三个性质,才算是零知识证明。分别测试每个性质是评估 ZK 系统最可靠的方法。

完备性意味着拥有有效见证的诚实证明者总能说服诚实验证者。如果你的实现拒绝了有效证明,则完备性被破坏。可靠性意味着不诚实的证明者无法说服验证者相信一个错误陈述,除非概率可忽略。零知识性意味着验证者可以在没有见证的情况下模拟转录,因此证明除陈述真实性外不泄露任何信息。

第四个性质简洁性并非 ZKP 所必需,但它是许多区块链 ZK 系统实用的原因:证明很小,且相对于其所证明的计算,验证速度很快。简洁非交互式知识论证(SNARK)和透明变体(STARK)是你将遇到的常见家族。

  • 完备性:有效见证总被接受。
  • 可靠性:无效见证以压倒性概率被拒绝。
  • 零知识性:转录可在没有见证的情况下模拟。
  • 简洁性(可选):证明短,验证成本低。

交互式与非交互式 ZK 证明

在交互式证明中,证明者和验证者交换多条消息。验证者发送随机挑战,证明者以只有持有见证的人才能做到的方式回应。阿里巴巴洞穴和色盲朋友的例子说明了这一点:重复降低了作弊证明者猜对的概率。

在非交互式证明中,证明者生成单个证明,任何人都可以在无需进一步通信的情况下验证。Fiat-Shamir 启发式通过用陈述和承诺的哈希替换验证者的随机挑战,将许多交互式证明转换为非交互式证明。这是区块链上使用的形式,因为智能合约无法与证明者进行实时来回交互。

对于链上验证,验证者通常是预编译合约或验证者合约,检查配对或基于哈希的证明。证明作为 calldata 提交,合约返回 true 或 false。这意味着你的验证工作流依赖于链的 RPC 端点返回一致的状态和日志。

演练的前提条件与假设

以下示例假设你拥有 Node.js 18 或更高版本、Python 3.10 或更高版本,以及访问 EVM 兼容 RPC 端点的权限。将 RPC_URL 替换为你自己的端点;不要在生产环境中硬编码公共端点。如果你需要托管端点,请通过 RPC 服务 比较选项,并在确定提供商之前查看 RPC 定价。

Schnorr 示例是教学实现,不是生产级密码学。它使用小素数域,以便你可以在本地运行。真实系统使用椭圆曲线或多项式承诺方案以及经过审计的库。重点是直接观察完备性、可靠性和零知识行为。

pip install ecdsa requests
# or for the JS example
npm init -y && npm install ethers

心智模型:承诺、挑战与响应

大多数 ZK 协议遵循称为 sigma 协议的三步模式。证明者首先发送一个承诺,它隐藏见证但将证明者绑定到该见证。验证者发送随机挑战。证明者发送一个响应,满足只有见证持有者才能满足的方程。验证者检查该方程。

在 Schnorr 协议中,陈述是证明者知道 x 使得 y = g^x mod p。证明者选择随机 r,发送 t = g^r。验证者发送挑战 c。证明者发送 s = r + c*x mod (p-1)。验证者检查 g^s = t * y^c mod p。如果证明者不知道 x,那么对于随机 c 产生满足方程的 s 与求解离散对数一样困难。

零知识性成立,因为验证者可以通过先选择 s 和 c 并计算 t = g^s * y^(-c) 来模拟转录。由于模拟转录与真实转录不可区分,验证者学不到关于 x 的任何信息。

Python 中可运行的 Schnorr 验证

下面的脚本在小素数域上实现了 Schnorr 协议的单轮。运行它以查看有效证明被接受,篡改的证明被拒绝。这是用于理解数学的诊断工具,不是生产库。

预期结果:第一次验证打印 True,篡改的证明打印 False。如果两者都打印 True,则你的挑战或响应方程有误。如果两者都打印 False,则你的模运算或生成元有误。

import random

p = 23          # small prime for demonstration only
g = 5           # generator of a subgroup
x = 6           # secret witness
y = pow(g, x, p)

r = random.randint(1, p - 2)
t = pow(g, r, p)
c = random.randint(1, p - 2)
s = (r + c * x) % (p - 1)

# Verifier checks g^s == t * y^c mod p
valid = pow(g, s, p) == (t * pow(y, c, p)) % p
print('valid proof accepted:', valid)

# Tamper with the response
tampered = (s + 1) % (p - 1)
print('tampered proof accepted:', pow(g, tampered, p) == (t * pow(y, c, p)) % p)

通过 RPC 端点验证 ZK 证明

链上 ZK 验证者暴露一个视图函数,例如 verifyProof(bytes proof, uint256[] inputs) returns (bool)。你可以使用 eth_call 调用它而无需花费 gas。下面的示例使用 ethers.js 和占位符验证者地址。将 VERIFIER_ADDRESS、PROOF_HEX 和 INPUTS 替换为你的证明系统中的值。

预期结果:对于有效证明,调用返回 true;对于无效证明,返回 false 或回滚。如果调用回滚,请检查 ABI、输入数组长度以及验证者是否期望不同的证明编码。

const { ethers } = require('ethers');

const provider = new ethers.JsonRpcProvider('RPC_URL');
const abi = ['function verifyProof(bytes proof, uint256[] inputs) view returns (bool)'];
const verifier = new ethers.Contract('VERIFIER_ADDRESS', abi, provider);

async function main() {
  const ok = await verifier.verifyProof('PROOF_HEX', ['INPUT_1', 'INPUT_2']);
  console.log('proof valid:', ok);
}

main().catch(console.error);

比较验证者行为的结果表流程

当你测试多种证明编码或多个 RPC 端点时,将结果记录在表格中,以便无需猜测即可比较。不要填写你未测量的数字。使用以下列,并将每行至少运行三次,以捕捉间歇性端点行为。

测试用例证明编码端点eth_call 结果延迟(毫秒)回滚原因
有效证明压缩RPC_URL_A
有效证明未压缩RPC_URL_A
篡改证明压缩RPC_URL_A
有效证明压缩RPC_URL_B

该表格是测量模板,不是已发布的结果集。你的延迟和回滚原因将取决于你的端点、链和验证者合约。如果你运行自己的节点,请参阅专用节点指南以了解隔离和资源规划。

排查常见的 ZK 验证失败

大多数验证失败属于少数几类。在假设密码学有误之前,请按顺序逐一排查。

编码不匹配。 SNARK 证明通常序列化为两个 32 字节配对点加两个标量。如果你的验证者期望不同的顺序或字节序,有效证明会回滚。将你的证明者输出格式与验证者的 ABI 文档进行比较。

公共输入不匹配。 验证者检查证明是否承诺了你传入的相同公共输入。如果输入数组顺序与电路的信号顺序不同,即使证明有效,验证也会失败。状态过期。 如果证明引用了区块高度或 Merkle 根,请针对同一区块进行验证。当可复现性重要时,使用带有显式区块标签的 eth_call。

端点不一致。 滞后的 RPC 节点可能返回较旧的验证者状态或无法解码日志。在提交错误之前,跨端点比较结果并检查链的网络配置。

  • 检查证明字节顺序和域元素编码。
  • 确认公共输入顺序与电路匹配。
  • 固定区块标签以实现可复现验证。
  • 在得出证明无效的结论之前,至少比较两个 RPC 端点。

ZK 证明的局限性与权衡

零知识证明功能强大但并非没有代价。证明通常比验证昂贵得多,因此 ZK 系统将成本从验证者转移到证明者。对于区块链使用,这意味着链下证明基础设施和链上验证合约。

信任假设因方案而异。SNARK 通常需要可信设置仪式;STARK 避免了这一点但产生更大的证明。透明设置降低了仪式风险,但增加了证明大小和验证 gas。递归证明可以将许多陈述压缩为一个,但增加了电路复杂性和调试难度。

最后,零知识并不意味着默认匿名。ZK 证明可以隐藏见证,同时仍然链接到公开身份或地址。隐私取决于陈述和公共输入所揭示的内容,而不仅仅是证明系统本身。

下一步:从理解到集成

一旦你能够在本地和通过 eth_call 验证证明,下一步就是将证明集成到你的应用中。从小型电路开始,生成证明,并在迁移到托管端点之前针对本地节点进行验证。关于端点选择,请使用 RPC 助手 来匹配你的验证者的链和延迟需求。

如果你正在评估 ZK rollup 或隐私应用的基础设施,请查看 RPC 定价 和 网络支持 以确认你需要的链和方法覆盖范围。对于运行自己验证者基础设施的团队,专用节点 提供了与共享公共端点的隔离。

合理的学习路径是:在本地实现 Schnorr,使用测试验证者验证 SNARK 证明,然后使用上面的结果表测量跨端点的验证延迟。在每一步都牢记三个性质,因为一个快速但不可靠的证明比没有证明更糟糕。

永远不用担心基础设施

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

开始