UCB-Web3-零知识证明笔记-全-

UCB Web3 零知识证明笔记(全)

001:课程概述

在本节课中,我们将要学习零知识证明的基本概念。我们将从经典证明开始,逐步引入交互式证明和零知识证明的核心思想,并通过具体例子理解其工作原理。

经典证明与NP证明

上一节我们介绍了课程主题,本节中我们来看看证明的传统定义。当我们谈论经典证明时,通常会想到一系列公理和推导步骤,最终得出定理。然而,在计算机科学中,我们更关注高效可验证的证明,即NP证明。

在NP证明模型中,存在一个证明者和一个验证者。证明者向验证者发送一个证明字符串。验证者是一个多项式时间算法,它读取证明字符串并决定是否接受该证明。

以下是NP证明需要满足的两个核心条件:

  • 完备性:如果陈述为真,那么诚实的证明者可以提供一个证明字符串,使验证者总是接受。
  • 可靠性:如果陈述为假,那么任何证明者(即使是恶意的)都无法提供一个能使验证者接受的证明字符串。

我们可以用以下公式来描述一个NP语言L:存在一个多项式时间验证算法V,使得对于所有输入x:

  • 如果 x ∈ L,则存在一个字符串w(见证),使得 V(x, w) = 1(接受)。
  • 如果 x ∉ L,则对于所有字符串w,都有 V(x, w) = 0(拒绝)。

示例:证明一个数n是两个大素数的乘积。证明者可以直接发送这两个素数因子p和q。验证者只需检查 p * q == n 以及 p 和 q 是否为素数即可。

然而,这种证明方式会向验证者泄露额外的信息(即因子本身)。我们能否在不泄露这些信息的情况下证明陈述的正确性?这就是零知识证明要解决的问题。

交互式证明与零知识证明简介

上一节我们介绍了NP证明,本节中我们来看看如何超越NP证明模型。零知识证明的核心目标是:证明者能让验证者相信某个陈述为真,但不会向验证者泄露任何超出该陈述真实性以外的信息

为了实现这一目标,我们需要对证明模型进行两项关键修改:

  1. 交互:证明不再是一个单向的消息,而是证明者和验证者之间进行多轮问答。
  2. 随机性:验证者被允许抛掷随机硬币,并根据结果向证明者提出不可预测的问题。

由于引入了随机性,我们允许验证者有极小的概率出错(例如,接受一个错误的陈述)。但这个错误概率必须是可以忽略不计的。

一个直观的例子(非数学):假设证明者不是色盲,而验证者是色盲。证明者想向验证者证明一张纸有两种颜色,而不是单一颜色。

  1. 验证者私下抛硬币。如果是正面,他将纸翻转;如果是反面,则不动。
  2. 验证者将纸交给证明者看。
  3. 证明者根据纸张是否被翻转,猜出验证者抛硬币的结果并告知验证者。
  4. 验证者对比结果。如果一致,则接受“纸有两种颜色”的陈述;否则拒绝。

分析

  • 如果纸确实有两种颜色,证明者总能分辨是否被翻转,从而总是猜对,验证者总是接受。
  • 如果纸只有一种颜色,证明者无法分辨,只能随机猜测,猜对的概率最多是1/2。
  • 通过重复这个过程k次,验证者错误接受(即纸只有一种颜色但证明者每次都猜对)的概率将降至 (1/2)^k,这是一个可忽略的小概率。

这个例子展示了如何通过交互和随机性,在不传递“颜色信息”本身(零知识)的情况下,传递“存在两种颜色”这一知识。

第一个数学示例:二次剩余

现在,让我们看一个数学上的零知识证明例子。问题是:证明者想向验证者证明一个数y是模n下的二次剩余,即存在某个x,使得 y ≡ x^2 (mod n),但不泄露x的值

以下是协议步骤:

  1. 证明者随机选择一个数r(满足1 ≤ r < n 且 gcd(r, n)=1),计算 s ≡ r^2 mod n,并将s发送给验证者。
  2. 验证者随机抛一枚硬币,得到一个比特b ∈ {0, 1}。如果b=0,他要求证明者提供s的平方根(即r);如果b=1,他要求提供sy的平方根(即rx mod n)。
  3. 证明者根据b的值,发回相应的答案z(r 或 r*x)。
  4. 验证者检查收到的z是否满足:z^2 ≡ s * y^b mod n。如果满足,则接受;否则拒绝。

分析

  • 完备性:如果y确实是二次剩余(证明者知道x),那么无论b是0还是1,证明者都能正确回答,验证者总是接受。
  • 可靠性:如果y不是二次剩余,那么对于证明者发送的某个s,他不可能同时知道s和s*y的平方根。因此,当验证者随机选择b时,证明者只有1/2的概率能回答验证者选择的那一个问题。重复k轮,验证者被欺骗的概率仅为(1/2)^k。
  • 零知识性:验证者在每次交互中,要么得到一个随机数r,要么得到一个随机数r*x。由于r是随机选择的,这两个值看起来都是随机的,没有泄露关于x的信息。形式上,可以构造一个模拟器,在不接触真实证明者的情况下,生成与真实交互不可区分的对话记录。

这个协议不仅证明了y是二次剩余,而且是一个知识证明——它证明了证明者确实知道x。因为如果有一个提取器能够以“回退”的方式运行证明者两次(第一次问b=0,第二次问b=1),他就能从两次回答中计算出x = (r*x) / r。

定义:交互式证明与零知识

上一节我们通过例子建立了直观理解,本节中我们正式定义相关概念。

一个针对语言L的交互式证明系统由一对交互式算法(P, V)构成,其中验证者V是概率多项式时间算法。它满足:

  • 完备性:对于所有 x ∈ L,Pr[〈P, V〉(x) 接受] = 1。
  • 可靠性:对于所有 x ∉ L 和所有(计算能力无限制的)证明者算法P, Pr[〈P, V〉(x) 接受] ≤ ε(|x|),其中ε是一个可忽略函数。

所有拥有交互式证明的语言类记为IP。

一个交互式证明系统是零知识的,如果对于任意(可能恶意的)概率多项式时间验证者V*,都存在一个概率多项式时间模拟器Sim,使得对于所有 x ∈ L,以下两个分布是计算不可区分的:

  • 真实视图 View_{V}[〈P, V〉(x)]:V*在与真实证明者P交互后看到的全部信息(包括其随机硬币和收到的消息)。
  • 模拟视图 Sim(x):模拟器仅凭输入x(而没有证明者的秘密知识)生成的输出。

模拟范式是零知识定义的核心:它意味着验证者从交互中学到的一切,都可以由他自己在本地模拟生成,因此交互没有给他带来任何新的计算能力。

零知识还有更强的变体:完美零知识(模拟分布与真实分布完全相同)和统计零知识(两个分布统计上接近)。

图同构的零知识证明

另一个重要的例子是图同构问题。给定两个图G0和G1,证明者想向验证者证明它们同构(即存在一个顶点的一一映射,保持边的关系),但不泄露具体的同构映射

协议与二次剩余协议结构相似:

  1. 证明者随机选择一个置换π,并计算图H = π(Gb)(即随机地将其中一个图进行置换),将H发送给验证者。
  2. 验证者随机选择b ∈ {0, 1},要求证明者提供从Gb到H的同构映射。
  3. 证明者发回相应的映射(如果b=0,则发π;如果b=1,则发π ∘ φ,其中φ是G0到G1的已知同构)。
  4. 验证者检查收到的映射是否正确。如果正确,则接受;否则拒绝。

分析:完备性和可靠性与二次剩余例子类似。这也是一个完美零知识证明,并且是知识证明(提取器可以通过两次询问提取出同构映射φ)。

应用:身份认证与协议设计

零知识证明有两个直接而重要的应用:

  1. 身份认证(如Fiat-Shamir方案):用户可以将一个秘密(如大数n的平方根x)作为私钥,公开y = x^2 mod n作为公钥。登录时,用户通过零知识证明向服务器证明自己知道x,而无需传输x本身。这避免了密码泄露的风险。
  2. 协议设计工具:在复杂的安全协议中,可以要求参与方在发送每条消息时,附上一个零知识证明,证明其发送的消息是根据协议规则、其私有输入和随机性正确计算得出的。这可以强制参与者诚实执行协议,同时不泄露其私有状态,从而简化安全分析。

所有NP问题都有零知识证明吗?

一个强大的结论是:在单向函数存在的假设下,每一个NP语言都存在一个(计算性的)零知识交互式证明

证明思路基于NP完全性:

  1. 首先,为一个特定的NP完全问题(如图三着色问题)构造一个零知识证明。
  2. 然后,对于任何NP语言L,将实例x通过NP完全归约转换为一个图G_x。
  3. 最后,对G_x运行图三着色问题的零知识证明。由于归约是公开的,这等价于对原始陈述x ∈ L的零知识证明。

图三着色问题的零知识证明协议概要

  • 证明者拥有一个三着色方案(用三种颜色给图顶点着色,使得任意边两端颜色不同)。他首先随机置换颜色标签,然后对每个顶点的颜色进行承诺(将其“锁在信封里”),将所有承诺发送给验证者。
  • 验证者随机选择一条边,要求证明者打开该边两个端点的颜色承诺。
  • 证明者打开这两个承诺,验证者检查两个颜色是否不同。
  • 重复以上步骤多次。如果验证者每次检查都通过,则接受。

关键点

  • 完备性:如果图可三着色,诚实的证明者总能通过检查。
  • 可靠性:如果图不可三着色,那么至少有一条边两端颜色相同或无效。每轮验证者随机选中这条“坏边”的概率至少为1/|E|。重复足够多次后,证明者侥幸过关的概率可忽略。
  • 零知识性:模拟器可以“投机”地提前猜验证者会问哪条边,然后只为这条边的两个端点生成不同的随机颜色承诺,为其他顶点生成任意承诺。由于承诺的隐藏性,模拟视图与真实视图计算不可区分。这里用到了承诺方案这一密码学原语,而承诺方案可以从单向函数构造。

交互式证明的强大能力

交互式证明的概念本身,即使不考虑零知识,也极大地拓展了我们对“可证明性”的认识。

  • NP vs IP:NP证明可以看作交互式证明的特例(只有一轮,且验证者确定性地计算)。但交互式证明更强大。例如,图非同构问题(证明两个图不同构)没有已知的短NP证明,但却存在简单的交互式证明协议。
  • IP = PSPACE:一个里程碑式的结果表明,交互式证明所能证明的语言类IP,恰好等于PSPACE(多项式空间可解决的问题类)。这包含了大量比NP更难的问题。
  • 多证明者交互式证明:如果允许验证者同时与多个互不通信的证明者交互,能力会进一步增强,并导致了PCP定理的发现。PCP定理说,任何NP证明都可以被改写成一种特殊形式,使得验证者只需随机检查其中常数个比特,就能以高概率判断证明的正确性。
  • 非交互式证明:在随机预言机模型下,通过Fiat-Shamir变换,可以将某些特定结构的交互式证明(验证者只发送随机挑战)转化为非交互式证明,即证明者生成一个单一的证明字符串,无需与验证者实时交互。这在区块链等应用中至关重要。

总结

本节课中我们一起学习了零知识证明的基础。我们从经典证明和NP证明出发,指出了其可能泄露多余信息的局限性。为了克服这一点,我们引入了交互随机性,定义了交互式证明系统,并进一步通过模拟范式定义了零知识性——即验证者学不到任何超出陈述真实性的信息。

我们通过二次剩余图同构的例子,具体阐述了零知识证明协议如何工作,并分析了其完备性、可靠性和零知识性。我们还看到了零知识证明在身份认证安全协议设计中的强大应用。

最后,我们了解到一个深刻的结论:在合理的密码学假设下,所有NP问题都有零知识证明。这通过将任意NP问题归约到图三着色问题并为其构造零知识证明来实现。交互式证明的概念本身也深刻影响了计算复杂性理论,揭示了交互和随机性如何极大地扩展了可高效验证的陈述范围。

002:现代SNARK构造概述 🚀

在本节课中,我们将要学习现代SNARK(简洁非交互式知识论证)的基本概念、应用场景以及其核心构造范式。我们将从SNARK的定义出发,探讨其为何在区块链和隐私计算领域至关重要,并最终理解构建高效SNARK的通用框架。

什么是SNARK? 🤔

在上一讲中,我们介绍了交互式零知识协议。从本讲开始,我们将专注于非交互式协议及其应用。我们将聚焦于SNARK,即“简洁非交互式知识论证”。

SNARK本质上是一个简洁的证明,用于证明某个陈述为真。一个典型的陈述可能是:“我知道一个消息M,使得其SHA-256哈希值为0”。证明者希望向验证者证明他知道这样的M。

一个平凡的证明是直接发送消息M。但如果M很大(例如1GB),那么这个证明就非常庞大,验证者也需要重新计算哈希,工作量巨大。而SNARK的证明总是非常简短,并且验证速度极快。这听起来有违直觉:即使消息长达1GB,证明者也能用一个极短的证明来说服验证者,且验证工作量极小。这就是SNARK的魔力。

我们还可以为SNARK增加零知识属性,使其成为ZK-SNARK。这意味着证明不仅简短、验证快速,而且不会泄露关于秘密消息M的任何新信息。

SNARK的商业应用与驱动力 💼

目前,SNARK领域存在巨大的商业兴趣。许多公司正在开发SNARK软件、使用SNARK或构建SNARK硬件加速器。这种兴趣源于一个可以追溯到上世纪80年代末、90年代初的核心理念:一个“缓慢而昂贵”的计算机可以验证一群“不可靠且运行不受信任软件”的超级计算机的工作。

如今,这个“缓慢而昂贵的计算机”的典型例子就是Layer 1区块链。区块链本身可以被视为一台计算机,但其运行速度相对较慢,且操作成本高昂。这催生了SNARK的几类关键应用。

区块链驱动的应用(无需隐私)

  1. 计算外包与ZK Rollup:这是扩展区块链的关键技术。一个链下服务可以处理一批交易(例如100或1000笔),并向L1链证明这批交易中的所有交易都是有效的且被正确处理。L1链无需逐一验证这些交易,只需验证一个非常简短、快速的SNARK证明即可。这可以将L1链的吞吐量提升数百倍。
  2. 区块链互操作性(跨链桥):目标是将资产从一个链转移到另一个链。目标链需要确信源链确实锁定了该资产。实现方式是向目标链提供一个SNARK证明,证明源链的共识协议确实同意该资产已被锁定。目标链只需验证这个简短的证明,而无需运行源链的整个共识验证过程。

在这两类应用中,证明的非交互性至关重要。证明将被大量区块链验证者验证,因此不能要求证明者与每个验证者进行交互式证明。非交互式证明允许所有验证者在无需与证明者交互的情况下独立验证证明。

需要隐私的应用(使用ZK-SNARK)

  1. 公链上的隐私交易:在公链上处理交易数据被加密或提交(而非公开)的隐私交易。需要附上零知识证明来论证交易是有效的(例如,由发起者正确签名、没有创造或损失资金)。多个项目正使用ZK-SNARK实现此目的。
  2. 合规性证明:交易数据在链上被加密或提交,不为公众所见。交易发起者需要证明该交易符合当地的银行法规。这同样通过附加零知识证明来实现。
  3. 交易所偿付能力证明:交易所希望证明其资产大于对客户的负债。它需要在不泄露具体资产数额和客户负债细节的情况下,零知识地证明资产大于负债。

非区块链应用示例:对抗虚假信息 🛡️

一个与区块链无关的优雅应用是打击虚假信息。新闻文章中常嵌入图片,但图片可能与文章描述的事件完全无关。行业正在开发技术解决方案,例如C2PA(内容来源和真实性)标准。

C2PA旨在为新闻文章中的图片提供真实的来源证明。其构想是在每台相机中嵌入一个制造商设置的、无法提取的密钥。每次拍照时,相机会用该密钥对图片及其元数据(如时间、地点)进行签名。

然而,新闻机构在发布图片前通常会进行处理(如下采样、裁剪、灰度化),这破坏了原始签名,导致读者设备无法验证。

SNARK为此提供了完美的解决方案
编辑软件会生成一个ZK-SNARK证明 π,证明以下陈述:

“我知道一个原始图像及其有效的C2PA签名,并且我发送给你的编辑后图片,是经过授权操作(如裁剪、缩放)处理原始图像后的结果,且你收到的图片元数据与原始图像元数据一致。”

读者的设备只需验证这个简短的SNARK证明(约1KB,验证时间约10毫秒)。如果验证通过,则向用户显示元数据,证明图片的真实性。对于新闻机构,为一张高分辨率图片生成此类证明只需几分钟,且证明生成是高度可并行化的。

这个应用同样要求证明是非交互式的,因为新闻机构是向所有读者广播同一个证明,而非与每个读者交互。

为何现在成为可能?⚡

这些应用在五到十年前是无法实现的。现在的突破在于新一代SNARK系统支持非常快速的证明者。证明生成时间与计算规模呈线性或拟线性关系。正是证明者效率的大幅提升,使得我们能够为大型计算生成证明。

这得益于一系列关于多项式性质证明的优雅思想,是代数和多项式理论的巧妙应用。我们将在后续课程中深入探讨。

精确定义SNARK 🔬

要精确定义SNARK,我们首先需要固定一个计算模型:算术电路

算术电路

我们首先定义一个有限域 F = {0, 1, ..., p-1},其中 p 是一个大素数。在该域上可以进行模 p 的加法和乘法运算。

一个算术电路是一个函数,它接受 n 个域元素作为输入,并产生一个域元素作为输出。电路由标量输入、变量输入以及加法、减法、乘法门组成。我们可以将算术电路视为计算一个多变元多项式,但它更是一个评估该多项式的“配方”。我们用 |C| 表示电路 C 中的门数量。

算术电路非常强大,可以表示任何多项式时间计算。例如:

  • 哈希函数电路:计算 H - SHA256(M)。如果 SHA256(M) = H,则输出0。实现SHA-256大约需要20,000个算术门。
  • 数字签名验证电路:输入公钥、消息和签名,如果签名有效则输出0。

我们区分两种电路:

  1. 非结构化电路:门之间的连线可以是任意的。
  2. 结构化电路:电路由层层重复的相同操作构成,可以看作是一个虚拟机的重复执行步骤。

从NARK到SNARK

首先定义NARK(非交互式知识论证)。它应用于一个算术电路 C(x, w),其中 x 是公开陈述,w 是秘密证据。目标是证明者向验证者证明他知道一个 w 使得 C(x, w) = 0

一个预处理NARK包含三个算法 (S, P, V)

  • S (Setup):以电路 C 的描述为输入,输出证明者参数 PP 和验证者参数 VP
  • P (Prove):以 PP, x, w 为输入,生成证明 π
  • V (Verify):以 VP, x, π 为输入,输出接受或拒绝。

NARK需满足两个属性:

  1. 完备性:如果证明者确实知道有效的 w,那么验证者总是会接受其生成的证明。
  2. 知识可靠性:如果验证者接受了来自证明者的证明,那么证明者“确实知道”一个使得 C(x, w)=0w。这里的“知道”意味着存在一个提取器,能够从成功的证明者那里提取出有效的 w

SNARK(简洁NARK) 在NARK的基础上增加了简洁性要求:

  • 证明长度:必须亚线性于证据 w 的长度(不能直接发送 w)。
  • 验证时间:必须亚线性于电路 C 的大小(验证者不能重新运行整个电路)。

实际上,我们通常要求更强的简洁性:

  • 强简洁性:证明长度和验证时间至多是对数级于电路大小 |C|,甚至最好是常数级(与电路大小无关)。

这引出了一个关键点:验证者甚至没有时间读取整个电路 C。这就是为什么需要预处理步骤。算法 S 会读取整个电路并生成一个“摘要”(即验证者参数 VP),其大小至多对数级于 |C|,但仍允许验证者验证关于电路的计算。

ZK-SNARK 则是一个同时满足零知识属性的SNARK。

平凡的SNARK(即发送 w 作为证明)不满足上述任何一点:证明可能很长、验证可能需要重算复杂电路、并且泄露了秘密 w

预处理(Setup)的类型

预处理步骤生成证明者和验证者参数,其中验证者参数是电路的摘要。该步骤需要使用随机数 r

  1. 每电路可信设置:为每个需要证明的电路单独运行。随机数 r 必须对证明者保密,通常通过“可信设置仪式”完成,之后销毁生成 r 的机器。
  2. 通用可信设置:分为两个算法。
    • S_init:一次性运行,生成全局参数 GP。同样需要保密 r 并销毁机器。
    • S_index:确定性算法,以电路 CGP 为输入,生成 PPVP。任何人都可以运行并验证。
      优点:一次可信设置可用于无数个电路。
  3. 透明设置:最佳方案。设置过程无需任何秘密数据,任何人都可以验证其正确运行,无需可信仪式。

现有SNARK系统示例(部分)

系统 证明大小 验证时间 设置类型 特点
Groth16 (2016) ~200 字节 ~1.5-2 毫秒 每电路可信设置 证明和验证均为常数
Plonk / Marlin (2019) ~400 字节 稍长,但仍为常数 通用可信设置 一次设置,多电路使用
Bulletproofs ~1.5 KB (对数级) 线性于电路大小 透明 证明短,但验证慢
STARKs ~100 KB (对数级) 对数级 透明 透明设置,证明较大

所有现代SNARK系统的证明者时间都几乎是电路大小的线性时间,这正是推动SNARK革命的关键。

知识可靠性的形式化定义

知识可靠性意味着:如果验证者以不可忽略的概率接受了敌手证明者生成的证明,那么必然存在一个高效的提取器 E,能够通过与敌手交互,提取出一个有效的证据 w

具体定义涉及一个分为两阶段的敌手 (A0, A1)

  1. A0 接收全局参数,输出想要证明的电路 C、陈述 x 以及一些内部状态。
  2. 运行 S_index 为电路 C 生成参数。
  3. A1 接收证明者参数、陈述 x 和状态,生成一个证明 π
    如果 V 以概率 ε 接受这个证明,那么存在提取器 E,它可以通过与 A1 交互,以大约 ε 的概率提取出一个满足 C(x, w)=0w

构建SNARK的通用框架 🏗️

现代SNARK的构建通常遵循一个通用范式,包含两个核心组件:

  1. 功能性承诺方案:一个密码学对象。
  2. 交互式预言机证明:一个信息论对象。

将两者结合,并通过Fiat-Shamir变换转化为非交互式,即可得到针对一般电路的SNARK。

1. 功能性承诺方案

首先回顾普通承诺方案。包含两个算法:

  • Commit(m, r) -> com:用随机数 r 承诺消息 m,生成承诺 com
  • Verify(com, m, r) -> accept/reject:验证打开。

需满足绑定性(不能对同一承诺打开为两个不同消息)和隐藏性(承诺不泄露消息)。

功能性承诺方案 则允许我们对一个函数 f(来自某个函数族 F)进行承诺。证明者承诺到函数 f,发送承诺 com_f。后来,对于验证者选择的任意输入 x,证明者可以输出 y = f(x) 以及一个证明 π,说服验证者“所承诺的函数 fx 点的值确实是 y,且 f 属于函数族 F”。

验证者只知道 com_f, x, yπ,而不知道 f 的具体内容。证明 π 本身是一个小型的SNARK,证明关系:f(x)=y, f ∈ F, 且 Commit(f, r) = com_f

重要的功能性承诺类型

  1. 多项式承诺:承诺到单变元多项式 f ∈ F_{≤d}[X](次数 ≤ d)。之后可打开其在任意点 u 的值 f(u)=v
  2. 多线性承诺:承诺到多线性多项式(每个变元次数 ≤ 1)。
  3. 向量承诺:承诺到一个向量 u,之后可打开其任意位置 i 的值 u_i。Merkle树就是一种向量承诺。
  4. 内积承诺:承诺到向量 u,之后可证明对于任意向量 v,内积 <u, v> 等于某个值。

这些类型可以相互构造。多项式承诺是构建SNARK最核心的组件之一。

多项式承诺方案示例

  • KZG10:使用双线性群,需要可信设置。证明和验证均为常数时间/大小,与多项式次数 d 无关。是目前最广泛使用的方案。
  • Dory:无需可信设置,但较慢。
  • 基于FRI:仅使用哈希函数,透明设置,但证明较大(约100KB)。
  • Bulletproofs:使用椭圆曲线,透明设置,证明大小为对数级,但验证时间为线性级
  • Dark:基于未知阶群,透明设置,但较慢。

平凡的多项式承诺方案(直接哈希所有系数)不是有效的多项式承诺,因为其“打开证明”(即发送所有系数)的大小和验证时间都是线性于次数 d 的。

多项式承诺的核心观察:零测试与相等测试

一个关键观察是:对于一个非零的 d 次单变元多项式 f,在一个随机点 r 上,f(r)=0 的概率至多是 d/pp 是域的大小)。如果 p 很大(如 2^256),而 d 相对较小(如 2^40),那么这个概率可以忽略不计。

这意味着,如果我们在随机点 r 上发现 f(r)=0,那么我们就可以以极高的置信度断定 f 是恒等于零的多项式。这是构建SNARK的基石。

由此可以推导出多项式相等测试协议

  1. 验证者有两个多项式 fg 的承诺。
  2. 验证者随机选择点 r,发送给证明者。
  3. 证明者计算 y_f = f(r), y_g = g(r),并生成相应的评估证明 π_f, π_g 发送给验证者。
  4. 验证者验证两个评估证明,并检查 y_f == y_g
    如果 fg 是两个不同的多项式,它们在随机点 r 上相等的概率至多是 d/p。因此,如果验证通过,验证者可以相信 f ≡ g

这个协议是交互式的。我们可以使用 Fiat-Shamir 变换 将其转化为非交互式。核心思想是:让证明者自己用哈希函数模拟验证者的随机挑战。具体来说,证明者计算 r = Hash(com_f, com_g),然后生成响应。验证者用同样的方式计算 r 并进行验证。在随机预言机模型下,可以证明这能产生一个知识可靠的SNARK。

2. 交互式预言机证明

IOP是一种证明系统,它能够将针对特定函数族(如多项式)的功能性承诺方案,“提升”为针对任意电路的SNARK。

一个IOP用于证明电路 C(x,w)=0。其结构如下:

  1. 预处理:算法生成验证者参数 VP,其中包含一些函数(作为“预言机”)。在后续编译为实际SNARK时,这些预言机将被功能性承诺替代。
  2. 交互协议:进行 t 轮。
    • 在每一轮,证明者发送一个函数(的预言机)给验证者。
    • 验证者回复一些随机数据。
  3. 验证:最后,验证者利用其拥有的所有预言机(来自 VP 和证明者),在它选择的点上进行查询,并根据查询结果决定是否接受证明。

IOP本身是信息论对象,其安全性(知识可靠性、零知识性)可以无条件证明。提取器在IOP层面被给予所有函数的明文(而不是承诺),因此可以更容易地提取证据 w

IOP示例:集合包含证明

假设电路要证明:公开集合 X 是秘密集合 W 的子集。

  1. 证明者构造多项式 f(Z),以 W 中所有元素为根。
  2. 证明者构造多项式 g(Z),以 X 中所有元素为根(验证者也可自行构造 g)。
  3. 如果 X ⊆ W,则 g 整除 f,商 q(Z) = f(Z)/g(Z) 是一个多项式。
  4. 证明者发送 fq 的预言机给验证者。
  5. 验证者随机选择 r,查询得到 w = f(r), q' = q(r),并自行计算 v = g(r)
  6. 验证者检查 v * q' == w
    根据Schwartz-Zippel引理,如果等式在随机点成立,则 f ≡ g * q 以极高概率成立,从而 g 整除 f,即 X ⊆ W。提取器可以通过求 f 的根来得到 W

组合框架:IOP动物园 🐘

构建SNARK的通用范式是:

  1. 选择一个功能性承诺方案(如多项式承诺)。
  2. 设计一个与之兼容的IOP(如多项式IOP)。
  3. 将IOP中的“预言机”替换为该功能性承诺方案的“承诺”。
  4. 将交互式协议通过Fiat-Shamir变换转化为非交互式。

这就形成了所谓的“IOP动物园”:

  • 多项式承诺 + 多项式IOP → SNARK。例如:Sonic, Marlin, Plonk。
  • 多线性承诺 + 多线性IOP → SNARK。例如:Spartan, Clover, HyperPlonk。
  • 向量承诺 + 向量IOP → SNARK。例如:STARK, Breakdown, Orion。

现代SNARK大多采用这种模块化方式构建,将IOP构造与承诺方案构造分离,提高了设计的清晰度和灵活性。

SNARK实践流程 🛠️

在实际应用中,开发者不会直接编写算术电路。典型流程如下:

  1. 领域特定语言:开发者使用像Cairo、ZoKrates、Circom这样的DSL编写程序逻辑。
  2. 编译器:将DSL程序编译成SNARK友好的格式,如算术电路、R1CS约束系统,或EVM字节码。
  3. SNARK后端证明器:以SNARK友好格式、公开陈述 x 和证据 w 作为输入,运行高效的证明生成算法,输出最终证明 π。这是计算最密集的环节,也是大量优化工作的焦点。

总结 📚

在本节课中,我们一起学习了:

  1. SNARK的核心概念:它是一种能生成简短、快速验证、并可选择具备零知识性的非交互式证明系统。
  2. SNARK的广泛应用:从区块链扩容(Rollup)、跨链桥到隐私交易、合规证明,乃至非区块链领域的图像真实性验证。
  3. SNARK的精确定义:基于算术电路模型,满足完备性、知识可靠性和简洁性(强简洁性要求证明和验证时间至多对数级)。
  4. 预处理设置的类型:分为每电路可信设置、通用可信设置和理想的透明设置。
  5. 构建SNARK的通用范式:其核心是结合功能性承诺方案(密码学组件,如多项式承诺)和交互式预言机证明(信息论组件),并通过Fiat-Shamir变换实现非交互性。
  6. 关键技术与观察:多项式零测试/相等测试是SNARK可行性的基石,Schwartz-Zippel引理将其推广到多变元情况。
  7. 实践流程:开发者通过高级DSL编程,经编译器生成中间表示,最后由高效的SNARK后端生成证明。

在接下来的课程中,我们将深入探讨具体的SNARK DSL、多项式承诺方案(如KZG)的构造,以及第一个高效的多线性IOP示例,从而构建出我们第一个完整的现实世界SNARK系统。

003:编程零知识证明

概述

在本节课中,我们将要学习如何将应用想法转化为零知识证明系统能够理解的格式。我们将探讨三种主要方法:使用硬件描述语言(如Circom)、使用高级语言中的库(如Arkworks)以及使用专门的编程语言及其编译器(如ZoKrates)。每种方法都有其优缺点,我们将通过实例来理解它们的工作原理。


从想法到算术电路

上一节我们介绍了零知识证明的基本概念,本节中我们来看看如何将应用逻辑转化为证明系统能处理的形式。

零知识证明系统不理解高级编程语言(如C、Python),它们理解的是算术电路约束系统。因此,我们需要将想法转化为这种格式。

什么是算术电路?

算术电路类似于布尔电路,但其中的逻辑门(如AND、OR)被替换为加法乘法门。所有运算都在一个素数域上进行,即整数模一个大素数 p

例如,设 p = 5

  • 加法:4 + 5 = 9 mod 5 = 4
  • 乘法:4 * 4 = 16 mod 5 = 1

算术电路可以看作是一个有向无环图

  • 节点:输入、加法/乘法门、常数、等式检查。
  • :连接节点的导线,承载数值。

另一种表示:R1CS(秩1约束系统)

R1CS是另一种广泛使用的谓词表示方法。它定义如下:

  • 公开输入 x 表示为 L 个域元素 x1, ..., xL
  • 私有输入 w 表示为 M 个域元素 w1, ..., wM
  • 谓词本身是 n 个约束的集合,每个约束形式为 alpha * beta = gamma,其中 alphabetagamma 都是变量的仿射组合(线性组合加上常数)。

例如,约束 w2 * (w3 - w2 - 1) = x1 是有效的R1CS约束,因为 alpha = w2beta = (w3 - w2 - 1)gamma = x1

R1CS也可以用矩阵来定义:给定三个矩阵 ABC,谓词成立当且仅当 (A * z) ⊙ (B * z) = C * z,其中 z = [1, x, w] 表示逐元素乘法。


方法一:使用硬件描述语言(HDL)—— Circom

上一节我们了解了算术电路和R1CS,本节中我们来看看如何使用Circom这种硬件描述语言来直接构建它们。

Circom是一种用于描述R1CS的HDL。与编程语言不同,HDL的核心对象是导线子电路,核心操作是连接导线创建子电路

Circom 基础语法

以下是Circom的一些核心概念和操作:

  1. 定义模板(电路):使用 template 关键字。
  2. 声明信号(变量):使用 signal 关键字声明输入和输出。
  3. 赋值与约束
    • <-:为信号赋值(见证计算)。
    • ===:创建R1CS约束。
    • <==:同时执行赋值和创建约束(简写)。

以下是一个简单的乘法电路示例:

template Multiply() {
    signal input x;
    signal input y;
    signal output z;

    z <== x * y;
}

component main {public [x]} = Multiply();

Circom 高级特性

Circom支持模板参数、数组和循环,这些在编译时确定,有助于构建参数化电路。

以下是一个检查重复平方的电路示例:

template RepeatedSquare(n) {
    signal input x;
    signal output y;
    signal xs[n+1];

    xs[0] <== x;
    for (var i = 0; i < n; i++) {
        xs[i+1] <== xs[i] * xs[i];
    }
    y <== xs[n];
}

![](https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/ucb-web3/img/7e675554079f9a478746f31cbb2d4710_1.png)

component main {public [x]} = RepeatedSquare(1000);

Circom 教程:实现数独行约束检查器

现在,让我们通过一个简化的数独行约束检查器来实践Circom编程。我们只检查每一行没有重复数字。

以下是实现步骤的核心代码逻辑:

  1. 构建“不相等”组件:检查两个输入是否不相等。
  2. 构建“互异”组件:检查一个数组内的所有元素是否互不相同。
  3. 构建主电路:将数独谜题(公开)和解(私有)作为输入,对每一行应用“互异”组件,并确保解中的数字在1-9范围内且与谜题中已给出的数字匹配。

(注:完整的Circom代码涉及循环、组件实例化和约束连接,具体实现可参考课程提供的示例代码库。)

编译并运行此Circom程序后,可以生成相应的R1CS,并用于后续的零知识证明生成与验证。


方法二:使用库方法 —— Arkworks

上一节我们使用Circom直接描述电路,本节中我们来看看如何在高级语言(如Rust)中使用库来构建约束系统。

这种方法的核心是约束系统对象,它维护着R1CS的矩阵 ABC 以及变量的赋值状态。主要操作包括:

  • cs.add_var(...):创建新变量。
  • LinearCombination::zero()lc.add(...):创建线性组合。
  • cs.enforce_constraint(...):添加约束(lc_a * lc_b == lc_c)。

库方法的优势

通过利用宿主语言(如Rust)的特性(结构体、枚举、运算符重载等),我们可以创建更高级、更易用的抽象。

例如,我们可以定义一个 Boolean 结构体,并重载 & 运算符,使得编写 a & b 就能自动生成对应的“与”门约束和见证计算,大大提升了开发体验和代码可读性。

Arkworks 教程:实现数独检查器

让我们使用Arkworks的R1CS标准库来实现数独检查器。该库提供了许多常用原语(如整数、布尔值)的约束版本。

以下是实现的核心思路:

  1. 定义类型:利用Rust泛型定义适用于任意大小 N 和域的数独谜题与解的类型。
  2. 检查行唯一性:遍历每一行,对于行中的每个元素,检查它是否不等于该行中它之前的所有元素。这可以通过Rust的迭代器和切片语法简洁表达。
  3. 检查谜题与解匹配:同时遍历谜题和解的每个对应单元格。断言:解中的数字必须在1-9范围内;并且,如果谜题中某单元格非零,则解中对应单元格必须与之相等。
  4. 测试:编写测试用例,验证代码能接受有效解并拒绝无效解。

(注:具体代码涉及Arkworks特定的API调用,如分配变量、创建约束等,可参考课程示例。)

这种方法生成的约束系统可以轻松接入Groth16、Marlin等支持R1CS的零知识证明后端。


方法三:使用专用编程语言 —— ZoKrates

上一节我们使用库在宿主语言中显式构建约束,本节中我们来看看如何通过更符合直觉的编程语言来自动生成约束。

ZoKrates就是这样一种高级语言,它允许开发者像编写普通程序一样编写零知识证明的逻辑,然后由编译器将其编译为R1CS。

ZoKrates 语言特性

ZoKrates支持许多现代语言特性:

  • 自定义类型:通过 struct 定义。
  • 主函数:程序入口 main,可以指定输入是公开还是私有。
  • 变量与断言:使用 assert 关键字来施加约束,条件可以是任何布尔表达式。
  • 泛型、数组、循环、条件表达式

一个简单的ZoKrates程序示例如下:

def main(field x, private field y0, private field y1) -> bool:
    assert(x == y0 * y1)
    return true

ZoKrates 的权衡

优点是开发体验好,语法直观,易于学习。缺点是控制力减弱,所有私有输入必须在 main 函数开始时提供,无法在程序执行过程中动态计算见证值,优化也更多地依赖编译器。

ZoKrates 教程:实现数独检查器

在ZoKrates中实现数独检查器的逻辑与Arkworks版本非常相似,但语法更简洁。

以下是核心步骤:

  1. 定义类型:使用二维数组定义谜题和解。
  2. 检查行唯一性:使用嵌套的 for 循环遍历每一行和每个元素,内层循环检查当前元素不等于该行中之前的所有元素,使用 assert 进行约束。
  3. 检查匹配与范围:遍历每个单元格,使用 assert 确保解的数字在1-9之间,并且满足“谜题单元格为0或等于解单元格”的条件。
  4. 编译与证明:使用ZoKrates命令行工具进行编译、设置、计算见证、生成证明和验证。

(注:具体命令和文件操作可参考课程演示。)


总结与对比

本节课中我们一起学习了三种编程零知识证明的方法。现在我们来总结和对比它们。

三种方法的定位

我们可以从两个维度来理解这些工具:

  1. 描述对象:是描述电路合成还是描述程序
  2. 语法形式:是独立语言还是嵌入在宿主语言中。
  • Circom (HDL):独立语言,描述电路合成。
  • Arkworks (库):嵌入在Rust中,描述电路合成。
  • ZoKrates (PL):独立语言,描述程序。

优缺点分析

以下是每种方法的优缺点:

  • Circom

    • 优点:语法优雅,对约束有直接、清晰的控制。
    • 缺点:学习曲线较陡(HDL思维),抽象能力有限(无用户自定义类型等)。
  • Arkworks (库方法)

    • 优点:在保持对约束直接控制的同时,能利用宿主语言(Rust)的全部表达能力进行抽象。
    • 缺点:需要掌握宿主语言,通常需要手动优化约束数量。
  • ZoKrates (编程语言)

    • 优点:最容易学习,语义符合程序员直觉,语法可以设计得非常优雅。
    • 缺点:放弃了一些控制权,中间见证由编译器自动生成,优化可能更困难。

生态与未来

除了我们介绍的三种,生态中还有许多其他工具,例如Noir、Leo、Cairo等。尽管这些工具在输入表示和语法上各不相同,但它们最终都生成少数几种约束系统(如R1CS、AIR)。这意味着它们共享许多底层技术,例如布尔值表示、整数运算、控制流编码等。

未来的方向可能是创建更统一的基础设施或库,来封装这些通用技术,使得构建新的ZK编程语言更加容易。

核心结论:无论选择哪种方法,其共同目标都是将高级想法转化为约束系统,这是生成零知识证明的关键第一步。选择哪种工具取决于你对控制力、开发效率和语言熟悉度的权衡。

004:交互式证明

在本节课中,我们将学习零知识证明中的一个核心概念:交互式证明。我们将了解什么是交互式证明,它与我们最终目标——简洁非交互式知识论证(SNARK)——有何区别与联系,并学习一个名为“和校验协议”的经典交互式证明。最后,我们将看到如何利用这些工具构建一个用于电路可满足性问题的SNARK。

概述:什么是SNARK?

首先,让我们回顾一下SNARK的定义。SNARK是“简洁非交互式知识论证”的缩写。它本质上是一个关于某个陈述为真的简洁证明。

  • 简洁:意味着证明本身很短,并且验证速度很快。
  • 非交互式:意味着证明者和验证者之间不需要进行多轮消息交换。证明可以是一个独立的字符串,例如发布到区块链上供任何人验证。
  • 知识论证:意味着如果证明者所声称的陈述不成立,那么任何计算能力有限的证明者都无法伪造出一个能说服验证者的证明。

一个简单的应用示例是:证明者声称知道一个消息 M,使得其哈希值 hash(M) = 0。一个平凡的证明系统是直接发送消息 M 本身。但如果 M 很大(例如一个1GB的视频文件),这个证明既不短,验证(重新计算哈希)也不快。SNARK的目标就是提供比这种平凡证明系统更优的验证成本。

交互式证明 vs. SNARK

在深入交互式证明之前,我们需要明确它与SNARK的三个主要区别。

  1. 交互性:交互式证明要求证明者和验证者进行多轮对话(挑战与响应)。而SNARK是非交互式的。
  2. 健全性类型:交互式证明要求统计健全性。这意味着,无论证明者拥有多么强大的计算能力(即使是无限算力),只要其初始声明是假的,验证者几乎总能发现并拒绝。SNARK是知识论证,它只要求对多项式时间的作弊证明者保持健全性。如果作弊者能破解某些密码学假设(如找到哈希碰撞),则可能伪造证明。
  3. 知识性:SNARK要求知识健全性,即证明必须能表明证明者“知道”某个证据(例如,知道那个哈希为0的消息 M)。而交互式证明通常只要求标准健全性,即证明陈述为真,但不一定要求证明者“知道”证据本身。

有些场景下标准健全性有意义而知识健全性无意义(例如,证明一个复杂程序的输出是42,这里没有“证据”的概念)。而在密码学应用中(如证明知道一个私钥),知识健全性才是关键。

对于交互式证明,一个关键限制是:只有实际参与交互、生成随机挑战的验证者才会被说服。旁观者无法确信挑战是真正随机的。这限制了其在区块链等需要公开验证的场景中的应用。

幸运的是,对于“公开掷币”协议,我们可以通过 Fiat-Shamir 变换 将其转化为非交互式。该变换的核心思想是:将验证者生成的随机挑战,替换为对证明者已发送消息的哈希值。这样,证明者可以自行模拟出挑战,从而生成一个无需交互、可公开验证的证明字符串。本课程后续构建的SNARK都会应用此变换。

从交互式证明到SNARK的蓝图

我们的目标是构建一个用于电路可满足性问题的SNARK。即,证明者声称知道一个证据 W,使得电路 C 在输入 W 后输出特定值(例如0)。

平凡证明是直接发送 W 并让验证者重新计算电路。但这不满足“简洁”要求:W 可能很长,且电路计算可能很慢。

交互式证明可以帮助解决验证速度问题:证明者可以发送 W,然后通过一个交互式证明来说服验证者 C(W)=0 成立,而验证者无需亲自计算整个电路。

但这仍未解决证明长度问题,因为 W 本身还是被完整发送了。

最终的解决方案结合了密码学承诺:

  1. 证明者不再直接发送 W,而是使用一个密码学承诺方案,对 W 生成一个简短的承诺。
  2. 然后,证明者和验证者运行一个交互式论证(基于交互式证明改造而来),证明被承诺的 W 满足 C(W)=0
  3. 在此过程中,证明者会根据需要,仅揭示关于被承诺的 W 的少量信息,足以让验证者完成交互式协议中的检查。
  4. 最后,应用 Fiat-Shamir 变换将整个流程变为非交互式,从而得到一个SNARK。

这里用到的承诺方案通常是函数承诺,例如多项式承诺、多线性多项式承诺或向量承诺。它们允许证明者先承诺一个函数(如多项式),后续再应验证者请求,揭示该函数在特定点的取值,并提供一个小证明证实该取值与承诺一致。

Merkle树:一个经典的向量承诺

让我们看一个具体的向量承诺例子:Merkle树。它用于承诺一个向量 U = (u1, u2, ..., ud)

  • 承诺阶段:将向量元素作为叶子节点,两两哈希,层层向上,最终得到一个根哈希值。这个根哈希就是承诺。
  • 揭示阶段:当验证者请求向量中第 i 个元素 ui 时,证明者发送 ui 以及从 ui 到根哈希路径上所有节点的兄弟节点哈希值。这被称为认证路径。
  • 验证阶段:验证者利用 ui 和收到的兄弟哈希,沿着路径重新计算哈希,最终看是否得到与之前承诺一致的根哈希。

Merkle树的绑定安全性依赖于底层哈希函数的抗碰撞性。认证路径的大小和验证时间都是 O(log d)

我们可以尝试用Merkle树来构造一个多项式承诺:将一个度数为 d 的多项式 F 在所有域元素上的取值作为一个向量进行Merkle承诺。当验证者请求 F(r) 时,就打开相应的叶子节点。

但这存在两个问题:

  1. 证明者效率低:证明者需要计算并存储多项式在所有域元素(可能非常多)上的取值。
  2. 无法保证多项式结构:Merkle树只承诺了一组值,验证者无法确信这组值来自一个低次多项式。

后续课程将介绍如何解决这些问题,构建真正高效的多项式承诺方案。

核心工具:多线性扩展与和校验协议

在介绍具体的交互式证明协议前,需要两个技术工具。

多线性扩展

对于一个定义在布尔超立方 {0,1}^L 上的函数 f,其多线性扩展 是一个定义在更大域 F^L 上的多线性多项式(每个变量的次数最多为1),并且在 {0,1}^L 上与 f 完全一致。

多线性扩展具有“距离放大”的特性:如果两个布尔函数 fg 哪怕只在一点上不同,它们的多线性扩展 也将在几乎整个 F^L 上都不同。这使得通过少量点值检查来探测布尔函数间的差异成为可能。

给定 f 在所有 2^L 个布尔输入上的取值,存在 O(2^L) 时间的算法可以计算其多线性扩展 在任何点 r ∈ F^L 上的值。

和校验协议

和校验协议是一个经典的交互式证明,用于解决以下问题:

  • 验证者拥有对一个 L 变元多项式 g 的预言访问(可以请求 g 在任意点的值)。
  • 验证者想计算 H = Σ_{b∈{0,1}^L} g(b),即 g 在所有布尔输入上的取值之和。
  • 直接计算需要 2^L 次查询和加法,代价高昂。

和校验协议的目标是让验证者将此繁重计算外包给证明者,而验证者只需做 O(L) 次本地计算和1次g 的查询。

协议过程(简化描述)

  1. 证明者声明总和 C1 = H
  2. 第1轮:证明者发送一个单变量多项式 s1(x1),声称它等于 h1(x1) = Σ_{b2,...,bL∈{0,1}} g(x1, b2, ..., bL)。注意 h1 虽然定义涉及巨大求和,但其本身是低次多项式。
  3. 验证者检查 C1 = s1(0) + s1(1)。然后随机选取点 r1,并验证 s1(r1) = h1(r1)。但验证者不知道 h1(r1)
  4. 于是问题归约到验证一个新声明:s1(r1) = Σ_{b2,...,bL∈{0,1}} g(r1, b2, ..., bL)。这变成了一个关于 L-1 个变量的类似求和问题。
  5. 双方递归地进行上述过程,每一轮绑定一个变量到一个随机值。
  6. 最后一轮(第L轮):证明者发送多项式 sL(xL),声称它等于 g(r1, r2, ..., r{L-1}, xL)
  7. 验证者随机选取 rL,并直接通过一次预言查询获得 g(r1, ..., rL) 的值,检查是否等于 sL(rL)

如果证明者初始声明错误,验证者以极高概率拒绝。通信复杂度为 O(L·d),其中 dg 在每个变量上的次数。验证者时间为 O(L·d) 加上一次 g 的查询。

应用示例:计算图中的三角形数量

我们用一个具体问题展示和校验协议的威力:计算一个 n 个顶点图的三角形数量。输入是图的邻接矩阵 An×n 矩阵,A[i][j]=1 表示有边)。

直接计算需要 O(n^3) 或基于矩阵乘法的 O(n^2.373) 时间。我们将给出一个验证者时间为 O(n^2) 的交互式证明。

关键步骤

  1. 将邻接矩阵 A 视为一个定义在 {0,1}^{log n} × {0,1}^{log n} 上的函数。令 Ã 为其多线性扩展。
  2. 定义一个新的三变量组多项式 g(X, Y, Z) = Ã(X,Y) · Ã(Y,Z) · Ã(X,Z),其中 X, Y, Z 各为 log n 维变量。
  3. 可以验证,三角形数量等于 Σ_{x,y,z ∈ {0,1}^{log n}} g(x, y, z)
  4. 对多项式 g 应用和校验协议。
    • 协议轮数:3 log n
    • 每轮通信:g 在每个变量上次数为2,所以每轮证明者发送一个二次多项式(3个系数)。
    • 验证者最终需要求值 g(r1, r2, r3) = Ã(r1,r2) · Ã(r2,r3) · Ã(r1,r3)
    • 根据之前结论,验证者可以在 O(n^2) 时间内计算 Ã 在这三个点上的值(因为需要读取整个矩阵来模拟预言)。
  5. 因此,验证者总时间为 O(n^2),远优于自己计算三角形数量。

构建SNARK for Circuit Satisfiability

现在,我们整合所有概念,勾勒一个基于和校验协议的电路可满足性SNARK框架。电路 CS 个门,证明者声称知道证据 W 使得 C(W) = y

核心思想:证明者不直接发送证据 W,而是承诺于整个电路计算的正确执行轨迹 T。轨迹 T 是一个长度为 S 的向量,记录了当电路以 W 为输入运行时,每个门的输出值。

步骤1:将轨迹编码为多项式

  1. 为每个门分配一个 log S 比特的标签。
  2. 将轨迹 T 视为一个定义在布尔超立方 {0,1}^{log S} 上的函数:T(门标签) = 该门输出值
  3. 证明者计算 T 的多线性扩展 H,并(通过多项式承诺)将其发送给验证者。

步骤2:构造校验多项式
我们需要验证 H 确实扩展了一个正确的轨迹。正确性意味着每个门的赋值与其输入门的赋值符合电路逻辑(加法门输出等于输入和,乘法门输出等于输入积)。

  1. 定义两个“布线谓词”的多线性扩展 add̃mul̃。例如,add̃(a,b,c)=1 当且仅当门 a 是加法门且其两个输入来自门 bc
  2. 构造一个三变量组多项式 G_H(a,b,c),其设计使得:G_H 在所有布尔输入 (a,b,c) ∈ {0,1}^{3 log S} 上等于0,当且仅当 H 扩展了一个正确轨迹。
    G_H 的具体形式混合了 add̃, mul̃H 在点 a, b, c 的取值,用于编码门之间的约束关系。
  3. 关键性质:验证者要计算 G_H 在某点 (r1, r2, r3) 的值,只需要知道 H(r1), H(r2), H(r3) 以及 add̃(r1,r2,r3)mul̃(r1,r2,r3) 的值。

步骤3:使用和校验协议验证
现在问题转化为:验证 G_H 在所有布尔输入上为零。

  1. 一个技巧是验证 Σ_{(a,b,c) ∈ {0,1}^{3 log S}} (G_H(a,b,c))^2 = 0。在整数域上,该和为零当且仅当 G_H 在所有布尔输入上为零。
  2. 对多项式 (G_H)^2 应用和校验协议。
    • 协议轮数:3 log S
    • 验证者最终需要求值 (G_H(r))^2 在某随机点 r 的值。这需要知道 G_H(r),进而需要知道 H 在三个点以及布线谓词在某点的值。
    • H 的点值可通过多项式承诺的“打开”功能获得。布线谓词的点值可以预先计算并承诺,或由证明者提供并证明。
  3. 如果和校验协议通过,则验证者以极高概率相信 G_H 在布尔超立方上为零,即 H 扩展了一个正确轨迹,从而证明者确实知道一个有效的证据 W

步骤4:转化为SNARK

  1. 上述过程是一个交互式论证。
  2. 应用 Fiat-Shamir 变换,将验证者的随机挑战替换为证明者消息的哈希,从而获得一个非交互式论证。
  3. 结合多项式承诺方案来承诺多项式 H,最终得到一个完整的SNARK。

在这个SNARK中,证明者复杂度主要来自多项式承诺方案的操作(如承诺和生成打开证明)以及计算和校验协议中的消息。验证者复杂度则是对数级的多项式处理加上少量的多项式承诺验证和点值计算。

总结

本节课我们一起学习了交互式证明及其在构建SNARK中的核心作用。

  1. 我们首先明确了SNARK(简洁非交互式知识论证)的目标与特性。
  2. 我们对比了交互式证明与SNARK在交互性、健全性类型和知识性上的关键区别。
  3. 我们了解了如何利用密码学承诺(如Merkle树、多项式承诺)与Fiat-Shamir变换,将交互式证明转化为非交互式SNARK的蓝图。
  4. 我们学习了多线性扩展这一重要工具,它能将布尔函数“放大”到更大域上,便于检测微小差异。
  5. 我们深入探讨了经典的和校验协议,它允许验证者将大量求和工作外包,自己仅做少量计算和一次查询。
  6. 我们看到了和校验协议的一个应用实例:高效验证图中三角形数量的计算。
  7. 最后,我们勾勒了如何利用和校验协议构建一个用于电路可满足性问题的SNARK框架,其核心是将电路执行轨迹编码为多项式,并利用和校验协议验证该多项式满足电路约束。

交互式证明是构建现代SNARK的基石之一。理解其原理,特别是和校验协议,是深入掌握零知识证明技术的关键一步。在接下来的课程中,我们将看到更多基于这些思想的复杂协议和优化。

005:Plonk SNARK

概述

在本节课中,我们将学习如何构建一个广泛使用的SNARK系统——Plonk。我们将以模块化的方式逐步展开,确保在课程结束时,您能理解构成该SNARK的所有组件。事实上,我们将看到的许多组件本身就非常有趣且实用。

首先,让我们回顾一下SNARK的典型构建方式。我们从一个多项式承诺方案开始,将其与一个多项式交互式预言证明结合,从而得到我们想要的通用电路SNARK。

为了实现这一点,让我先提醒您什么是多项式承诺方案,然后我们将看一个具体的多项式承诺方案构造。

多项式承诺方案

一个多项式承诺方案允许承诺者对一个定义在有限域 F_p 上的特定多项式 F 进行承诺,且被承诺多项式的次数受限于某个上界 D

承诺者承诺多项式 F 后,可以在其选择的任意点打开该多项式。具体来说,对于有限域中的公开值 uv,承诺者可以说服验证者,被承诺的多项式满足 F(u) = v,并且该多项式的次数至多为 D。在这个证明中,验证者只知道次数的上界、对多项式的承诺以及值 uv

对于一个多项式承诺方案,我们希望证明大小和验证者时间至多是 D 的对数级别。例如,简单地将多项式发送给验证者是不可接受的,因为这会导致证明大小与 D 呈线性关系,验证时间也会是线性的,因为验证者必须评估多项式。我们的目标是构建非常简短且验证速度极快的评估证明。

一个非常广泛使用的多项式承诺方案叫做 KZG 多项式承诺方案,以开发者 Catez、Verruche 和 Goldberg 的名字命名。接下来,我们解释这个方案的工作原理。

首先,我们固定一个有限循环群 G。这个群包含 p 个元素,我们将其表示为 0, g, 2g, 3g, ..., (p-1)g。该群有一个加法规则,允许我们将两个元素相加。例如,给定元素 2g3g,我们可以将它们相加得到元素 5g。请记住,这个加法是在模 p 下进行的。

现在我们有了这个阶为 p 的群 G,让我们看看承诺方案实际上是如何工作的。

首先,我们有一个设置过程来生成一些全局参数。设置过程将接受一个安全参数 λ,并采样一个随机的域元素 τ,然后按如下方式构造全局参数:全局参数的第一个元素是生成元 g,我们将其表示为 h_0。第二个元素是 τ * g,我们称之为 h_1,然后是 τ^2 * g, τ^3 * g, ..., τ^D * g,我们将其表示为 h_D。因此,全局参数总共包含 D+1 个群元素。

重要的是,生成全局参数后,运行设置过程的实体必须删除这个域元素 τ。这非常关键。如果 τ 泄露,那么就可以破坏承诺方案,即可以产生不正确的评估证明。因此,必须确保没有人知道 τ 的值。我们需要信任运行设置过程的实体删除 τ。在下一讲中,我们将讨论运行此设置过程的方法,以确保没有人知道 τ 是什么。我们通过在许多参与方之间运行设置过程来实现这一点,只要其中一方是诚实的,就没有人知道 τ,从而确保全局参数是安全的。

现在,假设某个可信方运行了设置过程,生成了全局参数,然后删除了值 τ

要对多项式 F 进行承诺,我们使用一个承诺字符串,它本身是一个群元素。因此,comm(F) 只是群中的一个元素,定义为被承诺多项式 Fτ 处的取值乘以生成元 g,这基本上给了我们一个群元素。

现在您可能会想,我刚刚告诉您 τ 已被删除,所以没有人知道 τ 的值。那么承诺者如何在不了解 τ 的情况下计算 F(τ) * g 呢?答案是承诺者实际上将使用全局参数来承诺多项式 F

让我们看看这是如何完成的。我们将多项式 F 以系数形式写出。承诺多项式 F 的方法基本上是计算 f_0 * h_0 + f_1 * h_1 + ... + f_D * h_D。展开后,您会发现我们只是将全局参数的各种元素的值代入。例如,h_2 被替换为 τ^2 * g,一旦我们将 g 从括号中提出,就得到 F(τ) * g。实际上,我们在这里计算的承诺字符串就是 F(τ) * g,它作为对多项式的承诺。

我想指出的一点是,这是一个绑定性承诺,承诺者现在被绑定到多项式 F,但它不是一个隐藏性承诺,因为多项式 F 的某些信息被泄露了。具体来说,我们泄露了 F(τ) * g。这对我们的目的来说是可以接受的,一个绑定的但不隐藏的承诺就足够了。

现在我们知道如何承诺一个多项式了。下一个问题是如何进行评估证明。这是这里有趣的地方。

我在这里再次写下了承诺算法:对多项式 F 的承诺就是 F(τ) * g。问题是,我们如何进行评估证明?这里,证明者拥有全局参数、多项式 F 以及 uv。另一方面,验证者只有对多项式 F 的承诺,它没有明文的多项式 f。证明者希望说服验证者,被承诺的多项式满足 F(u) = v

让我向您展示这是如何完成的技巧。首先,您注意到如果 F(u) = v,这意味着 u 必须是多项式 F - v 的一个根。如果我们评估多项式 F - v 在点 u 处,会得到 0,这意味着 u 是多项式 F - v 的一个根。我们用 F_hat 表示这个多项式。

现在,uF_hat 的一个根意味着什么?这意味着线性多项式 x - u 必须整除 F_hat。如果 uF_hat 的一个根,那么 F_hat 必须将线性多项式 x - u 作为其因子之一。但这意味着存在某个商多项式 Q,使得 Q * (x - u) = F - v。我们得到了这个多项式等式,当且仅当 F(u) = v。我们将反复使用这个事实。

使用这个事实,评估证明的工作方式如下:证明者将计算商多项式 Q,并实际上承诺这个商多项式 Q。因此,它将计算对应于这个商多项式的承诺的单个群元素,实际上,这个单个承诺将构成我们的评估证明 π。因此,证明者向验证者发送一个群元素,这个群元素作为对多项式 Q 的承诺。这已经足以说服验证者 F(u) = v

这里有趣的一点是,证明大小远优于 D 的对数级别;证明大小是常数大小,即群 G 中的单个元素。因此,我们拥有如此简短的评估证明是相当了不起的。这就是为什么 KZG 多项式承诺方案如此受欢迎。

现在验证者如何验证这个证明?验证者将简单地检查这个多项式等式在点 τ 处成立。让我们将 τ 代入这个关系式,我们得到 x - u 变为 τ - u 乘以对多项式 Q 的承诺,应该等于对多项式 F 的承诺减去对常数多项式 v 的承诺。验证者验证这个多项式等式实际成立的方式是验证它在随机点 τ 处成立。我们知道,如果两个多项式在一个随机点处一致,那么只要它们有界次数,这两个多项式极大概率是相等的。

这里,验证者检查这个关系在点 τ 处成立,这意味着这个多项式等式实际上成立。

再次,当您查看这个等式时,您可能会想,验证者实际上如何检查这个等式成立?验证者不知道 τ,那么验证者如何实际检查这个关系成立?答案是,为了做到这一点,验证者使用一种称为配对的代数工具。我暂时不解释这一点,实际上我们将在下一讲中更详细地了解这是如何工作的。但有趣的是,这个配对允许验证者检查下面这个等式是否按需成立,而无需实际知道值 τ。事实证明,它只需要全局参数中的 h_0h_1,因此即使全局参数可能相当大,验证者也只需要 GP 中的前两个元素来验证评估证明。

我们看到证明实际上是常数大小,事实上,验证证明所花费的时间也与次数 D 无关。这是可能实现的,这有点了不起,但这是使 SNARK 成为可能的基本事实。

现在我应该指出,计算商多项式对证明者来说可能是一个相当大的计算。事实上,如果多项式 F 的次数很高,比如多项式的次数是十亿或更多,那么现在证明者必须操作次数为十亿的多项式来构造这个商多项式并承诺它。

这就是我想说的关于 KZG 构造以及我们如何进行评估证明的全部内容。您可能想知道为什么这是安全的,特别是为什么这个评估证明能说服验证者 F(u) = v。我们将在下一讲中看到为什么这是安全的。

KZG 多项式承诺方案实际上具有许多非常有趣的特性,我们将在整个讲座中使用。

首先,将承诺方案推广到不仅仅承诺单变量多项式实际上并不困难。事实上,我们可以承诺 k 变量多项式。

两个、三个、四个、五个变量等的多项式,这实际上是我们上一讲所需要的,但我们在这里不会使用它,我们只承诺单变量多项式。

有趣的是,KZG 还支持非常高效的批量证明。假设我对许多多项式进行了承诺,在这种情况下,我承诺了 n 个不同的多项式。假设证明者希望说服验证者,这些多项式中的每一个在多个点处都评估为特定值。对于所有的 ij,证明者希望说服验证者 F_i 在点 u_{i,j} 处等于 v_{i,j}。假设它有五个多项式和十个点,它希望说服验证者这五个多项式在这十个点中的每一个都评估为特定值。在这种情况下,它将是五个乘以十个,即五十个打开证明。了不起的是,我们实际上可以将所有这些证明批量处理成一个单一的证明。因此,即使表面上看起来这需要五十个评估证明,实际上,KZG 具有这样的特性:所有这些证明都可以批量处理成一个群元素。因此,只需要一个群元素就足以证明所有这些多项式在所有不同点的所有这些打开。

我们也将大量使用这个特性。

我想提到的 KZG 的另一个特性是它还支持线性时间承诺。证明者承诺一个多项式的时间实际上只是多项式次数的线性时间。让我们看看为什么这是真的。

首先,如果我们有多项式以所谓的系数表示形式写出,即我们实际上将多项式 f 写成这些单项式的和,这是描述多项式的标准方式,那么计算对多项式 f 的承诺,正如我们所见,可以在线性时间内完成,它基本上是群 G 中的线性数量的乘法。因此,这将在多项式次数上花费线性时间。

然而,还有另一种表示多项式的方式,实际上我们将使用这种方式,即所谓的点值表示。什么是点值表示?这里我们固定一堆输入,例如 a_0a_D,并给出多项式在这些点 a_0a_D 处的评估值。您可以看到,这些 D+1 对实际上定义了一个多项式。例如,需要三个点来确定一个抛物线,所以当 D=2 时,这将需要三个点。一般来说,如果我们有一个次数为 D 的多项式,我们需要它在 D+1 个点处的评估值来完全确定该多项式。

问题是,如果我们有多项式的点值表示,我们如何快速承诺该多项式?简单的答案可能是:让我们取给定的点值表示,将其转换为系数表示 f_0f_D,然后像之前看到的那样,在线性时间内承诺系数表示 f_0f_D。问题在于,从点值表示转换到系数表示实际上需要 D log D 的时间,使用一种称为数论变换的算法,它与快速傅里叶变换密切相关。我们不会讨论这个。您只需要知道,从点值表示到系数表示需要 D log D 的时间。但我们希望以 D 的线性时间,即真正线性于 D 的时间来承诺一个多项式。这个 log D 有点碍事,当多项式的次数相当大时,如果多项式的次数是 2^20 或 2^30,log D 就像一个 20 或 30 的因子,会减慢承诺多项式的时间。我们希望节省这个时间,简单地拥有一个在 D 上线性运行的承诺算法。

问题是,如何做到这一点?事实证明,我们可以做得相当漂亮。再次假设我们有多项式的点值表示。我们可以做的是查看所谓的拉格朗日插值,这是一种从其点值表示插值多项式的方法。拉格朗日插值的工作方式基本上是,我们将 F 在某个点 τ 处的值计算为一些拉格朗日多项式的和。这些是 λ_i,这些拉格朗日多项式将在 τ 处评估,乘以 F(a_i),这些是给我们的值。

现在,拉格朗日多项式不依赖于多项式的值。它们只依赖于点 a_0a_D。实际上,我可以向您展示这些拉格朗日多项式是什么。让我们写出它们的表达式。这里,λ_i 在点 τ 处,我写出了它的表达式。您可以看到,τ 只出现在分子中,其他所有内容都是标量常数。因此,您可以看到这些多项式是由这个方程定义的,它们只依赖于我们评估多项式的点,而不依赖于多项式的实际值。

这被称为拉格朗日插值。如果您以前没有见过拉格朗日插值,我建议您阅读一下,因为这实际上是一项非常重要的技术。它在操作多项式时经常出现。

那么,这如何帮助我们呢?我们将要做的是将全局参数转换为所谓的拉格朗日形式。这基本上只是一个线性变换,任何人都可以执行这个变换,从全局参数的原始格式转换到拉格朗日形式。这不涉及任何秘密,任何人都可以做到,这只是一个线性变换,然后任何人都可以自行计算。但是,这个拉格朗日形式是什么?基本上,我们不是让全局参数为 g, τg, τ^2g, τ^3g, ..., τ^Dg,而是要用拉格朗日系数替换它。因此,h_0_hat 将是 λ_0(τ) * gh_1_hat 将是 λ_1(τ) * g,直到 λ_D(τ) * g。这基本上将成为新的全局参数。再次强调,这与原始全局参数完全等价。您可以通过线性变换在旧全局参数和新全局参数之间来回转换。这是一个相当简单的变换。

但这样做的好处是,现在即使我们被给予点值形式的多项式,我们也可以在线性时间内承诺该多项式,因为根据拉格朗日插值公式,我们知道要计算 F 在点 τ 处的值,我们只需取拉格朗日系数的线性组合。具体来说,要计算 F(τ) * g,我们可以简单地取我们被给予的多项式值的线性组合乘以修改后的全局参数中的群元素。现在您可以看到,多项式值和全局参数元素之间的简单内积给出了对多项式 F 的承诺。再次,您看到这在次数 D 上是线性时间运行的。这比 D log D 好得多。当 D 像 2^20 或 2^30 时,这个算法将比简单方法快 20 到 30 倍。

因此,通常当人们编写全局参数时,他们以拉格朗日基的形式编写,而不是我们之前看到的标准方式。

我想告诉您的关于 KZG 承诺的最后一点是另一个有趣的特性,值得记住,那就是我们可以相对快速地一次生成许多证明。再次假设证明者有某个多项式 f,让我们固定有限域的某个子集,我们将其表示为 Ω,假设这是我们的有限域 F_p 的一个大小为 D 的子集。假设证明者需要为多项式 f 为所有 a ∈ Ω 构造评估证明。因此,它需要为每个 a ∈ Ω 一个 KZG 评估证明 π_a。简单的方法,您会一次生成一个评估证明。每个评估证明需要 D 的时间来生成。因此,如果您必须生成 D 个证明,简单生成这些 D 个证明将需要 D^2 的时间。

2020 年有一个非常聪明的算法,称为 FK 算法。它表明,如果 Ω 恰好是有限域的一个乘法子群,那么实际上我们可以在 D log D 的时间内生成所有 D 个证明。这比 D^2 好得多。如果 Ω 只是大小为 D 的有限域的任意子集,事实证明我们仍然可以更快地完成,但现在需要 D log^2 D 的时间,仍然比 D^2 好得多。顺便说一下,这种差异是当我们选择子集 Ω 时,我们实际上将其选择为有限域的乘法子群的原因之一,我们稍后会看到。

最后,我想提一下,正如我们所见,KZG 多项式承诺方案存在一些问题。首先,KZG 承诺方案需要一个可信设置来生成全局参数。具体来说,我们必须确保没有人知道秘密值 τ。如果有一个单一的实体生成参数,它必须在完成后擦除 τ。通常人们做的是使用分布式协议生成全局参数,我们将在下一讲中看到,这样只要协议中有一方是诚实的,就没有人知道值 τ,全局参数就是安全的。

KZG 的另一个问题是全局参数的大小与 D 的大小呈线性关系。因此,如果 D 像十亿,那么全局参数的大小将是许多 GB,相当大。因此,一个自然的问题是,我们是否可以在与 KZG 相同的假设下构建多项式承诺方案,但不需要可信设置,并且全局参数要小得多。事实证明,这是可以做到的。有一个叫做 Dory 的承诺方案,同样非常漂亮。我在这里放了一篇论文的链接,以防您想了解 Dory 的工作原理。

首先,没有可信设置。因此,我们不必信任任何人在生成参数时忘记任何秘密信息。对多项式的承诺仍然是一个与次数 D 无关的单个群元素,就像在 KZG 中一样。然而,现在评估证明更大了。在 KZG 中,评估证明是常数大小,它是一个群元素。在这里,评估证明将是次数 D 的对数级别。因此,它们比 KZG 证明要大得多。此外,对于验证者检查证明,验证者现在必须工作与 log D 成正比的时间,而在 KZG 中,验证者只需要工作与次数 D 无关的常数时间。

因此,知道这个方案存在是好的,但由于证明更大且验证时间更长,这些证明并不像 KZG 承诺方案那样被广泛使用,在大多数应用中,人们最终使用 KZG 承诺方案。

现在我想提一下,多项式承诺方案有许多应用。它们不仅用于构建 SNARK。让我向您展示一个非常简单的应用。这些多项式承诺方案为我们提供了 Merkle 树的直接替代品。

如果您记得,Merkle 树允许我们承诺一个向量。如果我有一个向量 u_1u_K,我可以使用 Merkle 树承诺这个向量,然后,我可以使用一个非常简短的证明说服验证者 u_5 等于特定值,这就是 Merkle 树让我们做的。我们可以承诺一个大的向量,然后只在一点打开该向量,并快速说服验证者该点被正确打开。

事实证明,多项式承诺方案实际上让我们做同样的事情,但更好。让我们看看如何做到。首先,我们承诺一个向量 u_1u_K 的方式如下:我们将插值一个多项式 f,使得对于所有 i 从 1 到 Kf(i) = u_i。这定义了一个次数为 K-1 的多项式,因为它由 K 个点定义。我们继续承诺这个多项式,并将该承诺发送给验证者 Alice。

使用 KZG 承诺方案,这将是一个发送给 Alice 的单个群元素。现在,假设 Alice 希望证明者说服她向量的第二个元素是值 a,第四个元素是值 b。证明者可以做的就是产生一个评估证明,证明 f(2) = af(4) = b,并将此评估证明发送给 Alice,Alice 将检查证明并接受或拒绝该证明。

我告诉过您关于批量证明的事情。KZG 承诺的有趣之处在于,简单来说,我们需要两个评估证明来证明这两个陈述,但实际上,使用批量证明,这两个陈述可以用一个群元素来证明。因此,证明者所要做的就是发送一个群元素,验证者将相信 u_2 = au_4 = b。这实际上比 Merkle 证明短得多,如果您记得,如果您需要在 L 个点打开,您将必须提供 L 个 Merkle 证明。每个 Merkle 证明的大小将是 log K。因此,您将不得不发送 L * log K 个哈希值,而使用 KZG 承诺方案在 L 个点打开向量,您只需要发送一个与 LK 都无关的单个群元素。这非常了不起,比 Merkle 证明短得多。

如果您想了解更多关于使用多项式承诺方案的向量承诺,我建议您查找术语“Verkle 树”。这是一种使用这些多项式承诺来承诺向量的方式,比 Merkle 树更好。

好的,这把我们带到了...

证明承诺多项式的性质

欢迎回来。在上一节中,我们看到了如何使用 KZG 承诺方案承诺一个有界次数的多项式。在本节中,我们将看到如何证明一个被承诺多项式的性质。这些证明小工具实际上使用了非常巧妙的代数技巧。让我们开始吧。

首先,证明承诺多项式的性质意味着什么?在我们的设置中,证明者将拥有一些多项式,比如 FG,以明文形式可用。验证者将只有对这些多项式的承诺。我将使用框中的多项式来表示对多项式的承诺。这可以是对多项式的 KZG 承诺或使用任何其他承诺方案的承诺。

现在,证明者试图做的是,它希望说服验证者多项式 fG 满足某些性质,我们将看到许多关于这意味着什么的例子。因此,我们将这些证明系统呈现为交互式预言证明。我的意思是,验证者将向证明者发送一些随机消息,证明者将发回一些可能依赖于验证者数据的多项式,最后验证者将在 F_p 中的一些随机点查询一些被承诺的多项式,然后它将决定接受或拒绝。

在描述这些证明系统时,我只说验证者在某个点查询 FG,但在您的脑海中,您应该已经将其编译成一个真实的证明系统,其中验证者在某个点查询 fg 意味着什么?这意味着验证者将向证明者发送一些点,证明者将用多项式在这些点处的评估值以及一个评估证明来响应,以证明证明者正确评估了被承诺多项式在请求的点处。

让我们做一个简单的例子,通过回忆这个整个领域的基本构建块之一来做到这一点,即基本上如何测试两个被承诺的多项式彼此相等,我们称之为多项式相等性测试,我们用于此的基本工具是我们在之前的讲座中已经看到的。让我非常简要地提醒您那是什么。假设我们在一个大素数域上工作,并且假设我们的多项式有界次数,比如次数小于 2^40,这样次数相对于域的大小可以忽略不计。那么,如果我们想测试两个多项式 fG 相等,我们使用这个基本事实:如果我们在有限域中选择一个随机点,并测试这两个多项式在这个随机点处相等,如果值相等,那么这两个多项式也极大概率相等。事实上,正如您可能记得的,我们看到犯错的概率最多是 D / p,这是可以忽略不计的。这为我们提供了一种非常简单的方法来测试两个被承诺的多项式是否彼此相等。同样,我们选择一个随机点,并检查这两个多项式在随机点处是否相等。

因此,再次强调,证明者将以明文形式拥有多项式 fG。这些是有界次数的多项式,以次数 D 为界。验证者将拥有对这些多项式 fG 的承诺。同样,这些框中的 fg 表示对多项式的承诺。

现在,交互式预言证明的工作方式非常简单。基本上,验证者将在有限域中选择一个随机点,它将向证明者发送一个随机点,并要求证明者评估多项式 fG 在这个随机点 r 处。因此,我将此描述为查询 fGr 处。验证者然后将学习 f(r)G(r),如果两者相等,它将接受,并说“是的,这两个被承诺的多项式确实是同一个多项式”。

再次强调,只是为了确保这一点非常清楚。当我们将其编译为实际的证明系统时,验证者将发送一个随机点。证明者将评估多项式 fG 在随机点处,并将发回评估值 yy',以及评估证明,以证明评估是相对于被承诺多项式正确完成的。验证者将验证,事实上,这两个评估值彼此相等,并且评估证明是有效的,如果是这样,它将接受。

但再次强调,作为简写,当我们描述这些证明小工具时,我只说验证者查询多项式 FG 在点 r 处,然后检查两个评估值相同,在您的脑海中,我希望您将其编译成一个证明系统,其中评估由证明者完成,证明者发回评估值和证明,证明评估是相对于被承诺多项式正确完成的。

当然,您注意到这是一个公开掷币协议。验证者在这里所做的只是发送随机域元素,它不保留任何秘密。因此,我们可以使用 Fiat-Shamir 变换将其编译成非交互式协议。

好的,我们得到了一个非常简单的协议来测试两个被承诺的多项式是否彼此相等。

关于这个非常简单的协议,我想再提一点,那就是当我们使用 KZG 承诺时,如果您记得我们描述 KZG 承诺的方式,我们将其描述为确定性承诺方案,这意味着如果两个多项式彼此相等,它们的承诺也将彼此相等。因此,您应该问,为什么我们要描述这样一个复杂的协议,如果验证者可以自行测试 F 是否等于 G?它简单地测试它拥有的两个承诺是否彼此相等,如果是,它就已经自行知道这两个多项式相等了。嗯,事实证明,如果我们做一些稍微复杂的事情,就需要这个协议。特别是,也许证明者有一堆多项式 F, G1, G2, G3,而验证者只有对这些多项式的承诺。证明者想要向验证者证明的是,实际上 F 恰好等于所有三个多项式 G1 * G2 * G3 的乘积。现在验证者不能仅仅检查承诺的相等性,因为它没有对三个多项式乘积的承诺,它只有对这些多项式各自的承诺。因此,我们将做的是,验证者将在随机点查询所有四个多项式,然后如果 f(r) = G1(r) * G2(r) * G3(r),它将接受相等性为真。是的,验证者在这里需要做一点工作,基本上必须计算 G1(r) * G2(r) * G3(r) 的乘积,并测试其是否等于 f(r)

事实上,只要 3D / p 可以忽略不计,这个协议就是完备且可靠的。3D 的原因是因为多项式 G1 * G2 * G3 的次数是 3D,因此可靠性误差,即验证者犯错的概率,最多为 3D / p,当然,如果 p 足够大,这将是一个可以忽略的值。

好的,现在我们理解了多项式相等性测试是如何工作的,让我们将其投入使用,我们将从三个非常重要的单变量证明小工具开始。为了呈现这些小工具,让我们固定有限域的某个子集,我将其称为子集 Ω,假设这是我们的有限域 F_p 的一个大小为 K 的子集。

现在,假设我有一个次数为 D 的特定多项式,多项式的次数,比如说,远大于 Ω 的大小。假设验证者对这个多项式 f 有一个承诺。

证明者希望向验证者证明的关于这个多项式的事情如下。

第一个是所谓的零测试。什么是零测试?在零测试中,证明者希望说服验证者,实际上多项式 f 在集合 Ω 上恒等于 0。现在我想强调,这并不意味着多项式 fΩ 之外处处为 0。多项式 F 可以在 Ω 之外取任意值,但在集合 Ω 上,多项式必须为 0。因此,F 不是零多项式。它恰好只在集合 Ω 上为 0。您注意到,因为多项式的次数可能远大于集合 Ω 的大小,多项式 fΩ 之外非零而在 Ω 上完全为 0 是完全合理的。好的,我们将其称为零测试。这是证明系统中一个非常重要的构建块,我将在一分钟内向您展示如何进行零测试。

证明者可能希望说服验证者的下一件事是所谓的和校验,即 f 在集合 Ω 上的评估值之和。您看到这里我们对 Ω 中的所有元素求和,并求和 f 在这些元素上的评估值,该和恰好等于 0。因此,同样,和校验是一个非常重要的证明小工具,将在课程中反复出现,目标是让证明者说服验证者评估值之和恰好等于 0。类似地,还有一个积校验,除了使用乘积外,其他相同。因此,不是证明和等于 0,现在证明者希望说服验证者,如果您取多项式 FΩ 的点上的所有评估值的乘积,该乘积评估为 1。

好的,这些是证明者希望说服验证者的三件基本事情,我们将为不同的多项式应用这三个不同的任务。

好的,我想向您展示这些协议是如何工作的。因此,首先,我们必须引入一个非常重要的概念,称为消失多项式。好的,让 Ω 再次成为我们有限域的子集,它的大小为 k

集合 Ω 的消失多项式,我们表示为 Z_Ω,基本上是一个在集合 Ω 上处处为 0 的多项式。是的,该多项式定义为 ∏_{a ∈ Ω} (x - a),显然,如果您代入集合 Ω 中的任何值,这个消失多项式将评估为 0。在 Ω 之外,当然,多项式可以评估为任意值,但在集合 Ω 上,这个多项式将始终评估为 0。现在,当然,消失多项式 Z_Ω 的次数恰好是集合 Ω 的大小,在我们的情况下恰好是 K。因此,请记住,这是一个次数为 K 的多项式,在 Ω 上处处为 0。

现在有一个特定的集合 Ω 对我们来说非常非常重要,那就是集合 Ω,它恰好是有限域的一个乘法子群。我是什么意思?让我们固定有限域中的某个元素 ω。并假设 ω 是一个本原 K 次单位根。这意味着基本上 ω^K = 1,但 ω 的幂次小于 K 时不等于 1。因此,我们可以将 Ω 设置为 ω 的所有幂次。是的,所以 1, ω, ω^2, ..., ω^{K-1}。这将是有限域的一个大小为 K 的子集。

在这种情况下,您可能记得高中代数,在 K 阶的所有本原单位根处为 0 的多项式就是多项式 x^K - 1。事实上,很容易看出,如果您代入这些点中的任何一个,那么 x^K - 1 将评估为 0。关于这个多项式的有趣之处,以及我们喜欢这个特定集合 Ω 的原因是,如果您想在点 r 处评估消失多项式,那么评估它基本上只需要计算 r^K 并减去一。计算 r^K 可以使用重复平方算法仅用大约 log K 次域操作完成。这表明,如果我们使用这个特定的集合 Ω,那么它的消失多项式可以在集合大小的对数时间内评估。再次强调,这将非常重要,因为我们的验证者将不得不自行评估 Z_Ω,我们希望该评估超级快。

好的,现在我们理解了什么是消失多项式,让我们描述我们的第一个证明系统,即零测试。

这里的设置是,证明者有一个次数为 D 的多项式 F,验证者对这个多项式 F 有一个承诺,证明者希望说服验证者的是,这个多项式在整个集合 Ω 上恰好为 0。

好的,我们将使用一个非常简单的引理来做到这一点,即如果 F 实际上在整个集合 Ω 上为 0,这意味着 Ω 的所有元素都是多项式 F 的根,这意味着实际上 F 必须能被 Ω 的消失多项式 Z_Ω 整除,在我们的情况下是 x^K - 1。事实上,这是一个当且仅当的条件。是的,如果 F 为 0,那么它是可整除的;如果 F 实际上可整除,那么它必然在 Ω 上处处为 0。因此,这个引理实际上就是我们将要使用的。

因此,证明系统的工作方式如下。基本上,证明者将计算一个商多项式,即 f 除以 Z_Ω。现在您意识到,如果 f 实际上在 Ω 上为 0,那么除以消失多项式将得到一个多项式。这个除法的结果将是一个多项式,因为 F 可被消失多项式整除。如果 FΩ 上不为 0,那么这个除法不会得到一个多项式。是的,它将导致某个有理函数,该函数将有一个分母,不会是一个干净的多项式。

因此,证明者接下来要做的是,它将向验证者发送对这个商 Q 的承诺。我想强调的是,证明者能够承诺这个商多项式 Q 的唯一原因是,如果 Q 恰好是一个多项式。KZG 承诺方案只允许我们承诺有界次数 D 的多项式,我们不能使用 KZG 承诺一般的有理函数。因此,证明者能够承诺商多项式的事实,正是我们将用来证明 F 可被消失多项式整除的。

但验证者不知道证明者实际上承诺了正确的商。证明者可能承诺了垃圾。因此,验证者必须检查这个商确实是 f 除以消失多项式。因此,我们使用我们通常的多项式相等性测试来做到这一点。验证者将在有限域中选择一个随机点 r,它将在点 r 处查询多项式 Q 和多项式 f,然后它将学习 Q(r)f(r),然后如果 f(r) = Q(r) * Z_Ω(r),它将接受。现在,如果左边和右边在一个随机点处彼此相等,正如我们所说,这意味着它们极大概率作为多项式相等。因此,这意味着实际上 f = Q * Z_Ω,这再次意味着,实际上 f 可被消失多项式整除,因此验证者可以安全地得出结论,f 在整个集合 Ω 上为 0。

这里有趣的是,您注意到验证者从证明者那里获得了值 F(r)Q(r),以及证明这些评估是正确完成的证明,但它必须自行评估消失多项式,这就是为什么我们希望消失多项式是高效可计算的,以便验证者可以非常快地完成此操作。当我们使用这个特定的 Ω 时,我们说这可以在 k 的对数时间内完成。

正如我们刚刚论证的,基本上只要 D / p 可以忽略不计,这个协议就是完备且可靠的。我们需要 D / p 可以忽略不计的原因是,当 D / p 可以忽略不计时,我们知道如果两个多项式在一个随机点处一致,那么这两个多项式极大概率作为多项式相等。

在运行时间方面,验证者唯一要做的就是验证两个打开证明,并且必须自行评估消失多项式,正如我们所说,这需要 log K 的时间。因此,验证者的工作基本上是 O(log K) 加上两次多项式查询。您可能记得在上一节中,我们说过查询可以批量处理成一个。因此,实际上,这甚至可以减少到一次多项式查询。证明者必须做什么?

006:基于配对和离散对数的多项式承诺

在本节课中,我们将学习基于双线性配对和离散对数的多项式承诺方案。多项式承诺是构建高效零知识证明系统的核心组件。我们将首先介绍必要的数学和密码学背景,然后深入讲解经典的KZG多项式承诺方案及其变体,最后探讨无需可信设置(如Bulletproofs)的方案。

背景知识:群、离散对数与双线性配对

上一节我们介绍了多项式承诺的基本概念,本节中我们来看看构建这些方案所需的数学基础。

群是一个集合及其上的一个运算,满足四个性质。我们以整数集和加法运算为例:

  • 封闭性:对于集合中的任意两个元素a和b,a+b的结果仍在集合中。
  • 结合律:对于任意a, b, c,有(a+b)+c = a+(b+c)。
  • 单位元:存在一个元素e(对于整数加法是0),使得对任意a,有e+a = a+e = a。
  • 逆元:对于任意元素a,存在一个元素b(对于整数a是-a),使得a+b = e。

密码学中常用的群是模素数p的乘法群,集合为{1, 2, ..., p-1},运算为模p乘法。

生成元与离散对数

对于一个群,如果存在一个元素g,使得通过计算g的不同幂次(g^1, g^2, ...)可以遍历群中的所有元素,则称g为生成元。此时,群可以表示为{g^1, g^2, ..., g^(p-1)}。

离散对数问题定义为:给定群元素y,找到x使得g^x = y。这是一个公认的计算困难问题,构成了许多密码学协议的基础。

计算性Diffie-Hellman假设是另一个相关假设:给定gx和gy,无法高效计算g^(xy)。该假设强于离散对数假设。

双线性配对

双线性配对涉及一个基群G(生成元为g)、一个目标群G_T和一个配对运算e。配对运算满足以下性质:
给定输入gx和gy,输出e(g^x, g^y) = e(g, g)^(xy)。

配对的核心能力是验证指数上的乘积关系,而无需计算它。例如,给定g^x, gy和声称的g(xy),可以通过检查e(g^x, gy)是否等于e(g(xy), g)来验证。

一个经典应用是BLS签名方案:

  • 私钥为x,公钥为g^x。
  • 对消息m的签名为σ = H(m)^x,其中H是哈希到群G的函数。
  • 验证时,检查e(H(m), g^x)是否等于e(σ, g)。如果签名正确,则等式成立,因为e(H(m), g^x) = e(H(m)^x, g) = e(σ, g)。

KZG多项式承诺方案

有了双线性配对的背景,我们现在可以深入讲解KZG多项式承诺方案。

KZG方案由Kate、Zaverucha和Goldberg于2010年提出。它包含四个算法:密钥生成、承诺、求值证明生成和验证。

方案构造

该方案工作在一个双线性群上,支持度数≤d的单变量多项式。

1. 密钥生成

  • 随机选择一个秘密值τ。
  • 计算全局参数GP = (g, g^τ, g2), ..., gd))。
  • 关键步骤:必须彻底删除τ。这个需要可信方执行并丢弃τ的步骤称为可信设置。如果τ泄露,任何人都可以生成假证明。

2. 承诺
对于多项式f(x) = f0 + f1x + ... + fdx^d,承诺计算为:
comm_f = g^(f(τ)) = g^f0 * (g^τ)^f1 * (g^(τ^2))^f2 * ... * (g^(τ^d))^fd
证明者可以使用全局参数GP计算此承诺,而无需知道τ。

3. 求值证明生成
当验证者查询点u时,证明者需要证明v = f(u)。核心是利用多项式等式:
f(x) - f(u) = (x - u) * q(x)
其中q(x)是商多项式,次数为d-1。
证明者计算q(x),然后生成证明:
π = g^(q(τ))
同样,这可以使用GP计算。

4. 验证
验证者需要检查多项式等式在秘密点τ处成立,即检查:
g^(f(τ) - v) = g^((τ - u) * q(τ))
虽然无法直接计算右边,但可以利用配对来验证指数上的关系。验证者检查以下等式:
e(comm_f / g^v, g) = e(g^τ / g^u, π)
如果证明正确,根据配对性质,该等式成立。

安全性分析

KZG方案的正确性由多项式等式保证。其可靠性基于一个称为q-强双线性Diffie-Hellman假设的变体。简化的安全证明思路如下:

  1. 假设敌手能生成一个错误的求值v* (≠ f(u))和一个能通过验证的假证明π*。
  2. 根据验证等式,可得 e(comm_f / g^v*, g) = e(g^(τ-u), π*)
  3. 将comm_f写作g^(f(τ)),并将v*分解为f(u) + δ(其中δ ≠ 0)。
  4. 利用多项式等式,将左边重写为 e(g, g)^((τ-u)*q(τ) + δ)
  5. 通过代数变换,最终可以导出 e(g, g)^(δ/(τ-u)) 可由敌手计算得出,这便打破了q-强双线性DH假设,与假设矛盾。

在证明中,我们默认了敌手“知道”多项式f,使得comm_f = g^(f(τ))。这需要引入指数知识假设来保证,但会带来开销(承诺和验证规模翻倍)。实践中,常在通用群模型下证明安全性,该模型捕获了所有已知的对离散对数问题的攻击,从而允许我们使用单元素承诺和单配对验证。

方案特性与扩展

KZG方案的主要特性如下:

  • 需要可信设置
  • 承诺和证明大小恒定(各1个群元素)。
  • 验证时间恒定(1个配对运算)。
  • 承诺和证明生成复杂度为O(d)次群指数运算。

为了缓解可信设置问题,可以采用仪式:多个参与者依次贡献随机性来生成最终参数,只要至少一个参与者诚实删除其秘密,整体秘密τ就是安全的。

KZG方案有多个重要变体:

  • 多变量扩展:基于多项式等式 f(x⃗) - f(u⃗) = Σ (xi - ui)*qi(x⃗)。证明包含k个元素,验证需要k个配对。
  • 零知识化:通过在承诺和证明中引入随机掩码,使验证者无法从承诺中获取任何关于多项式的信息。
  • 批量打开:对于单个多项式的多个求值点,可以构造一个单个证明来同时证明所有求值。其核心是构造一个插值多项式h(x)使得h(ui)=f(ui),然后证明f(x)-h(x)能被Π (x-ui)整除。此技术可进一步扩展至多个多项式在多个点的批量打开。

结合之前课程的多项式IOP(如Plonk),KZG承诺可以用于构建高效通用的zk-SNARK方案。

基于离散对数的透明多项式承诺

KZG方案性能优异,但需要可信设置。本节我们探讨无需可信设置的方案,以Bulletproofs为例。

Bulletproofs多项式承诺

Bulletproofs的核心思想是递归缩减:将一个关于高次多项式求值的声明,逐步缩减为一个关于低次(最终为常数)多项式求值的声明。

1. 设置与承诺

  • 透明设置:全局参数GP是d+1个随机的群元素(g0, g1, ..., gd),无秘密。
  • 承诺:对于多项式f(x)(系数为f0,...,fd),承诺为 comm_f = g0^f0 * g1^f1 * ... * gd^fd。这是一个Pedersen向量承诺。

2. 递归缩减协议(单轮)
假设初始多项式次数为3(系数f0, f1, f2, f3),求值点为u,声称值为v。

  • 证明者
    1. 将多项式分为“左半部”系数(f0, f1)和“右半部”系数(f2, f3)。
    2. 计算左半部在u的求值 vL = f0 + f1*u,右半部求值 vR = f2 + f3*u。注意 v = vL + vR * u^2
    3. 计算两个交叉承诺:
      • L = g2^f0 * g3^f1 (用“右半部”的基对“左半部”系数承诺)
      • R = g0^f2 * g1^f3 (用“左半部”的基对“右半部”系数承诺)
    4. 发送(vL, vR, L, R)给验证者。
  • 验证者
    1. 检查 v = vL + vR * u^2。若不成立则拒绝。
    2. 发送一个随机挑战数r给证明者。
  • 缩减
    双方共同将原实例缩减为一个新实例:
    • 新多项式f'(x) = (r*f0 + f2) + (r*f1 + f3)*x。次数减半。
    • 新求值点:仍为u。
    • 新声称值v' = r*vL + vR
    • 新承诺:验证者计算 comm_f' = L^r * comm_f * R^(r^-1)
    • 新基:验证者更新全局参数为 (g0' = g0^(r^-1) * g2, g1' = g1^(r^-1) * g3)。可以验证,comm_f' 正是用新基 (g0', g1') 对新多项式 f' 的Pedersen承诺。

3. 递归与完成
上述过程递归进行log(d)轮,直到多项式次数为常数(例如0或1)。在最后一轮,证明者直接发送这个常数多项式(即其系数),验证者用最终的承诺基验证其求值是否正确。若所有轮次验证通过,则原始求值声明成立。

方案特性与后续改进

Bulletproofs方案特性:

  • 透明设置,无需信任。
  • 承诺大小恒定(1个群元素)。
  • 证明大小O(log d)(每轮发送O(1)个元素)。
  • 验证者时间O(d)(主要开销在于每轮更新基向量,这是线性操作)。

为了改进Bulletproofs的线性验证时间,后续研究提出了多种方案:

  • Halo:通过将多项式系数表示为二维矩阵,进行行承诺和列缩减,将证明大小和验证时间降至O(√d),并可在两者之间灵活权衡。
  • Dory:通过内配对乘积论证,将验证者更新基向量的线性计算工作委托给证明者,并高效验证该委托的正确性,从而将验证时间降至O(log d),同时证明大小保持O(log d)。
  • Dark:在未知阶群中实现了O(log d)的证明大小和验证时间,其构造思想与上述Bulletproofs递归缩减类似。

总结

本节课中我们一起学习了基于双线性配对和离散对数的多项式承诺方案。

我们首先介绍了KZG方案,它基于双线性配对,具有证明和承诺大小恒定、验证快速的优点,但需要可信设置。我们还讨论了其多变量、零知识、批量打开等变体。

为了消除可信设置,我们研究了基于离散对数的透明方案。以Bulletproofs为例,它通过递归缩减将问题规模不断减半,实现了对数级的证明大小,但验证时间是线性的。后续的Halo、Dory和Dark等方案从不同角度进行了优化,最终实现了对数级的证明大小和验证时间,且无需可信设置。

这些多项式承诺方案是构建现代高效零知识证明系统的基石,与不同的多项式IOP结合,可以构造出功能强大且性能各异的zk-SNARK。

007:基于纠错码的多项式承诺 📜

在本节课中,我们将学习一种新型的多项式承诺方案,它基于纠错码构建。这类方案具有透明设置、证明生成速度快且可能具备后量子安全性等优点,但通常伴随较大的证明体积。我们将从纠错码的基础知识开始,逐步构建出完整的承诺方案,并探讨如何利用扩展图构造线性时间可编码的纠错码。

背景知识:纠错码 📡

上一节我们介绍了基于双线性配对和离散对数问题的多项式承诺方案。本节中,我们来看看基于纠错码的方案。首先,我们需要理解一些关于纠错码的基础概念。

纠错码是信息论和编码理论中一个被深入研究的课题,用于在网络传输中纠正错误。一个纠错码将一个大小为 K 的消息编码为一个大小为 N 的码字,其中 N 严格大于 K。纠错码的一个重要概念是最小距离。

两个码字之间的距离是它们之间不同位置的数量,这被称为汉明距离。取任意两个码字之间距离的最小值,称为 Δ,这就是该纠错码的最小距离。NKΔ 是码的三个重要参数,我们称其为 [N, K, Δ] 码。

以下是关于纠错码的一些关键术语:

  • 码率:定义为 K/N,表示码字中有意义信息的比例,我们希望它尽可能接近 1。
  • 相对距离:定义为 Δ/N,表示任意两个码字之间不同位置的比例,对于纠错目的,我们希望它尽可能大。
  • 线性码:最常见的码类型,要求任何码字的线性组合仍然是码字。这意味着编码算法可以表示为消息 M 与一个大小为 K × N 的生成矩阵 G 之间的向量-矩阵乘法:C = M * G。对于线性码,最小距离等于任何非零码字的最小非零元素数量(即码字的重量)。

一个经典的线性码构造是里德-所罗门码。它定义在一个大小为 P 的有限域上。算法将大小为 K 的消息编码为大小为 N 的码字。方法是将消息视为一个唯一的 K-1 次多项式(可以看作是在 K 个固定公共点上的多项式插值),然后将该多项式在另外 N 个预定义的公共点(例如单位根)上进行求值。里德-所罗门码的距离是 N - K + 1,这是一个非常好的性质。

基于线性码的多项式承诺方案 🧩

有了纠错码的背景知识,我们现在可以探讨如何利用线性码来构建多项式承诺方案。本节介绍的方法源自2017年的Ligero和Bünz等人论文,该方案具有平方根大小的证明和平方根的验证成本。

该多项式承诺方案的核心思想是识别多项式求值中的矩阵结构。假设多项式的系数数量 D 是某个整数的完全平方数。我们可以将系数排列成一个 √D × √D 的矩阵 F。那么,多项式在点 u 的求值 F(u) 可以分解为两个步骤:首先计算向量 a(由 u 导出)与矩阵 F 的乘积,得到一个中间向量;然后计算该中间向量与另一个由 u 导出的向量 b 的内积。

通过这种观察,我们可以将多项式承诺问题简化为一个向量-矩阵乘积的论证问题。如果有一个证明大小为 O(√D) 的方案来证明第一步计算正确,那么验证者就可以本地完成第二步内积计算,从而得到一个总证明大小为 O(√D) 的多项式承诺方案。

因此,接下来的重点是如何设计一个方案,在不直接向验证者发送矩阵 F 的情况下,测试这个向量-矩阵乘积的计算。思路是使用线性纠错码来编码由多项式系数定义的矩阵 F

方案构建

以下是方案的具体步骤:

1. 承诺阶段

  • 使用一个线性码对矩阵 F 的每一行进行编码。原始矩阵维度是 √D × √D,编码后得到一个维度为 √D × N 的编码矩阵,其中 N 是码字长度。我们通常使用恒定码率的线性码,因此 N 渐进等于 √D
  • 然后,我们使用默克尔树按列对这个编码后的矩阵进行承诺。将每一列视为默克尔树的一个叶子节点,计算出的树根哈希值就是多项式的承诺。密钥生成非常简单,只需要从一个哈希函数族中采样一个哈希函数,无需可信设置。

2. 评估与验证阶段
评估算法大致分为两步:邻近性测试和一致性测试。

  • 邻近性测试:主要目标是测试承诺的矩阵是否确实由按行编码的码字组成。步骤如下:

    1. 验证者发送一个长度为 √D 的随机向量 r
    2. 证明者计算 r 与承诺矩阵的乘积,得到一个长度为 N 的向量 w,并发送给验证者。
    3. 验证者随机选择 t 个列索引,要求证明者打开这些列(即提供列值以及对应的默克尔树路径证明)。
    4. 验证者进行三项检查:(a) 向量 w 是否是该线性码的一个有效码字;(b) 打开的列是否与默克尔树承诺一致;(c) 对于每个打开的列,计算 r 与该列的内积,结果是否等于 w 向量在对应位置的值。
      如果所有检查通过,则以压倒性概率可以断定承诺的矩阵接近一个正确编码的矩阵。
  • 一致性测试:在邻近性测试之后,目的是测试由求值点 u 导出的向量与原始系数矩阵 F 的乘积是否等于证明者声称的结果 m(一个长度为 √D 的向量)。步骤如下:

    1. 证明者发送消息 m,它应该是 u 与矩阵 F 的乘积结果。
    2. 验证者使用相同的线性码将 m 编码为一个码字 w‘
    3. 验证者使用在邻近性测试中打开的相同 t 个列。
    4. 验证者检查:对于每个打开的列,计算 u 与该列的内积,结果是否等于 w‘ 向量在对应位置的值。
      如果检查通过,则说明 u * F = m 成立。

最后,验证者本地计算 m 与向量 b(由 u 导出的第二部分)的内积,即可得到最终的多项式求值结果 F(u)

方案特性与权衡

基于线性码的多项式承诺方案具有以下特性:

  • 优点:透明设置(无信任假设),全局参数小;证明生成速度快(仅涉及域加法和乘法,无需群指数运算);可能是后量子安全的;与域无关(可在任意域上工作)。
  • 缺点:证明体积通常较大(在原始方案中为 O(√D),可达数十MB);由于缺乏代数结构,证明难以聚合。

一些后续研究,如Breakdown和Orion论文,通过证明组合等技术进一步减少了证明体积,例如从 O(√D) 降低到 O(log² D),但证明体积仍然相对较大。因此,在选择方案时,需要在证明生成速度、安全假设和证明体积之间进行权衡。

基于扩展图的线性时间可编码纠错码 ⚡

在最后一节,我们将探讨如何利用扩展图构造具有恒定相对距离的线性时间可编码纠错码。这类码被用于构建具有线性证明者时间的SNARK方案。

这种码由Daniel Spielman在1996年提出,后来由Druk和Ishai在2014年推广到有限域。它依赖于一种称为扩展图的经典对象。扩展图具有良好的扩展性,即图的任何子集都会连接到许多邻居节点。

我们使用一种称为损失扩展图的二分图。在二分图中,左侧节点集代表消息符号,右侧节点集用于编码。左侧每个节点有恒定度数 g 的边连接到右侧。损失扩展图要求对于左侧任何大小不超过某个阈值的子集 S,其邻居集合的大小至少为 (1 - β) * g * |S|,这几乎是最大可能的扩展。

简单的编码想法是将消息放在左侧节点,然后对右侧每个节点,将其所有邻居左侧节点的值求和,作为编码输出。这是一个线性码,且编码时间是线性的。然而,这种简单求和方法不能直接获得良好的相对距离。

递归编码算法

实际的编码算法更为复杂,是一个递归过程,旨在构建一个码率为 1/4 的码:

  1. 系统部分:将原始消息 m(长度 K)直接复制为码字的第一部分。
  2. 第一次扩展:将消息 m 通过一个损失扩展图(左侧大小 K,右侧大小 K/2),得到向量 m1
  3. 递归编码:假设我们已有一种能将长度为 K/2 的消息编码为长度为 2K 的码字 c1 的好码(具有恒定相对距离 δ)。我们对 m1 应用这个编码,得到 c1,作为码字的第二部分。
  4. 第二次扩展:将 c1(长度 2K)通过另一个损失扩展图(左侧大小 2K,右侧大小 K),得到向量 c2,作为码字的第三部分。
    最终的码字是 (m, c1, c2),总长度为 4K。对于递归步骤中所需的“好码”,我们递归地应用相同的算法,直到消息规模变为常数,此时可以使用任何好码(如里德-所罗门码)。

通过分析可以证明,这样构造的码具有恒定的相对距离。证明思路是分情况讨论消息和中间码字的重量,并利用损失扩展图的扩展性质来保证只要输入非零,编码过程中至少会产生一定比例的非零输出。

构造与优化

在实践中,我们需要找到可用的损失扩展图。存在确定性的显式构造,但具体效率不高。另一种方法是随机采样图,因为随机图以高概率是好的扩展图,但失败概率是 1/poly,并非可忽略。

在Breakdown和Orion等后续工作中,提出了改进:

  • Breakdown:使用带随机权重的边进行加权求和,而不是简单求和,这显著提高了码的距离。
  • Orion:提出了一种测试算法,可以将随机采样到坏扩展图的失败概率从 1/poly 降低到可忽略的程度。

总结 📝

本节课我们一起学习了基于纠错码的多项式承诺方案。我们从纠错码的基础知识入手,介绍了如何利用线性码和默克尔树来构造承诺方案,其核心是邻近性测试和一致性测试两个交互步骤。这类方案提供了透明设置和快速证明生成的优点,但代价是较大的证明体积。

此外,我们还深入探讨了基于扩展图的线性时间可编码纠错码的构造,这是实现线性时间证明者的关键工具。通过递归地使用损失扩展图,可以构造出具有恒定相对距离且编码时间为线性的纠错码。

这些基于纠错码的构造为构建后量子安全、高效且无需信任设置的零知识证明系统提供了重要的技术路径。在下一讲中,我们将讨论FRI协议和STARK构造。

008:基于 FRI 的多项式承诺与 Fiat-Shamir 变换

在本节课中,我们将要学习基于 FRI 的多项式承诺方案,并深入探讨 Fiat-Shamir 变换的具体细节及其安全性考量。

概述:多项式 IOP 与多项式承诺

大多数 SNARK 的设计都结合了两个核心组件:一个称为多项式承诺方案的密码学协议,以及一个称为多项式交互式预言证明的交互式协议。你可以将几乎任何多项式 IOP 与任何多项式承诺方案组合起来,得到一个简洁的交互式论证,然后应用 Fiat-Shamir 变换使其变为非交互式,从而获得一个 SNARK。

到目前为止,我们已经了解了多种不同的多项式 IOP 和多项式承诺方案,它们各自都有独特的性能权衡。将不同的多项式 IOP 与多项式承诺方案组合,会继承相应的优缺点。

上一节我们回顾了多项式 IOP 和多项式承诺的基本概念,本节中我们来看看基于 FRI 的具体实现。

多项式 IOP 与多项式承诺回顾

多项式 IOP 的一个常见特例是,证明者首先向验证者指定一个多项式 H。这个多项式 H 通常非常大,例如,如果它是一个单变量多项式,其度数可能与证明者声称知道满足赋值的电路规模一样大。因此,我们不希望验证者必须读取该多项式的完整描述。

在多项式 IOP 中,验证者只被允许在单个自选的点上评估 H。在验证者评估 H 之后,证明者和验证者执行一个标准的交互式证明。除了指定多项式 H 的第一个特殊消息外,证明者的其他所有消息都很短,并由验证者完整读取。最终,验证者决定是否接受证明者的声明。

多项式承诺方案是一种密码学协议,它本质上正是将多项式 IOP 转化为简洁交互式论证所需的东西。具体来说,它允许证明者在不显式发送这个大多项式 H 的描述给验证者的情况下,模拟多项式 IOP。相反,证明者会向验证者发送对 H 的一个简洁密码学承诺。之后,多项式承诺方案必须支持验证者选择一个输入 X,并要求证明者评估所承诺的多项式 H 在 X 处的值,然后说服验证者证明者返回的评估值确实等于所承诺多项式在 X 处的评估值。我们称之为多项式承诺方案的评估证明。

这样,验证者只需接收一个哈希值或密码学群元素来承诺多项式,之后接收请求的评估值及其正确性证明,从而有效地以简洁的方式运行多项式 IOP。

现有方案的性能概览

以下是已知多项式 IOP 和多项式承诺方案的简要分类,以及它们组合后的一些流行 SNARK 的优缺点。

多项式 IOP 分类

多项式 IOP 主要分为三类:

  1. 基于求和检查协议:例如 Spartan、Breakdown、Orion。这些通常具有最快的证明者速度。
  2. 基于多变量求和检查协议:例如 Aurora。
  3. 恒定轮次多项式 IOP:例如 Marlin 和 Plonk。这些通常验证成本更低,但证明者成本更高。

多项式承诺方案分类

多项式承诺方案主要分为三类:

  1. 基于配对友好群和可信设置:例如 KZG 承诺。优点是评估证明和承诺本身是恒定大小的群元素。缺点是需要可信设置,且非后量子安全。
  2. 基于离散对数问题,无需可信设置:例如 IPA、Bulletproofs、Dory。优点是透明的,但非后量子安全。
  3. 仅基于哈希:例如 FRI、Ligero、Orion。优点是透明且可能是后量子安全的。FRI 在此类别中拥有最短的评估证明。

这三类方案的验证成本依次递增:KZG 为常数,Bulletproofs 等为对数,FRI 等为多对数。

流行 SNARK 示例

以下是当前一些流行 SNARK 的简要总结:

透明 SNARK

  • Halo2 (使用 Bulletproofs):结合了 Plonk IOP 和 Bulletproofs 承诺。优点是证明最短,缺点是在非摊销设置下验证者速度慢。
  • 基于 FRI 的 SNARK:例如 STARKs、Fractal、Aurora。优点是透明且可能是后量子安全的,证明在同类中最短(但仍为数百KB),并且可以在更小的域上工作以获得更快的算术运算。
  • 基于求和检查的快速证明者 SNARK:例如 Spartan、Breakdown、Orion。优点是证明者速度最快,缺点是证明通常更大。

非透明 SNARK

  • Groth16:基于线性 PCP,验证成本最佳(证明仅3个群元素,验证只需少量配对操作)。缺点是需要电路特定的可信设置,证明者较慢,且非后量子安全。
  • Marlin 和 Plonk (使用 KZG):优点是可信设置是电路无关的。缺点是证明比 Groth16 大,证明者比 Groth16 慢,但能应用于更广泛的电路类别。

设计空间非常丰富,理解所有可能的性能权衡需要掌握许多不同的思想和技术。

基于 FRI 的多项式承诺

现在,让我们深入探讨基于 FRI 的多项式承诺方案。

问题定义与初始尝试

我们考虑一个在素数域 F_p 上的单变量多项式 Q,其次数小于 k。在一个单变量多项式承诺方案中,我们希望证明者能够承诺 Q,之后根据验证者选择的输入 r 揭示 Q(r),并证明该评估值与所承诺的多项式一致。

在之前的讲座中,我们曾尝试让证明者 Merkle 承诺 Q 在所有域元素上的评估。但这有两个问题:

  1. 证明者时间与域大小成正比,而不是与多项式度数 k 成正比,效率低下。
  2. Merkle 树无法向验证者保证所承诺的函数(其评估值是树的叶子)的次数最多为 k-1。

FRI 的改进:在单位根子集上承诺

FRI 通过让证明者仅承诺 Q 在一个精心选择的子集 Ω 上的评估来解决第一个问题。这个集合 Ω 的大小为 ρ^{-1} * k,其中 ρ^{-1} 是一个常数,称为 FRI 膨胀因子(ρ 称为 Reed-Solomon 码的速率)。

由于证明者只对大小为 ρ^{-1} * k 的集合 Ω 进行评估和 Merkle 承诺,因此在证明者时间和验证成本之间存在很强的权衡:膨胀因子越大,证明者时间越长(因为需要评估更多的点),但验证成本(包括证明长度和验证时间)越低。

具体来说,验证成本(哈希评估次数)约为:
(λ / log(ρ^{-1})) * log²(k)
其中 λ 是安全参数。每个 FRI 验证者查询提供大约 log(ρ^{-1}) 比特的安全性,因此需要大约 λ / log(ρ^{-1}) 次查询来达到 λ 比特安全。

集合 Ω 由域中所有 n 次单位根组成,其中 n = ρ^{-1} * k,且 n 是2的幂。单位根具有很好的代数性质,例如,如果 ω 是一个本原 n 次单位根,那么所有 n 次单位根就是 ω 的幂:{1, ω, ω², ..., ωⁿ⁻¹}。这些单位根在乘法下构成一个子群。

FRI 的低度测试:折叠阶段

Merkle 承诺本身不保证向量具有低度结构。FRI 的核心思想是让验证者通过交互式“低度测试”来检查所承诺的向量是否接近一个低度多项式。

FRI 的低度测试包括两个阶段:折叠阶段和查询阶段。

在折叠阶段,证明者和验证者进行对数轮(log k 轮)交互。在每一轮中,验证者发送一个随机域元素 r,证明者则“折叠”当前承诺的向量。折叠过程将向量长度减半,并且如果证明者是诚实的,折叠后向量所对应多项式的度数也会减半。

折叠的数学原理是将多项式 Q 拆分为偶次部分 Q_e 和奇次部分 Q_o,使得 Q(x) = Q_e(x²) + x * Q_o(x²)。然后,折叠后的多项式定义为 Q_fold(z) = Q_e(z) + r * Q_o(z),其中 z = x²。可以证明,这等价于将 Q 在单位根 x 和 -x 处的评估值进行特定线性组合,得到在 z 处的评估值。

经过 log k 轮折叠后,如果证明者是诚实的,最终向量应该对应一个常数多项式(度数为0)。

FRI 的低度测试:查询阶段

在折叠阶段之后是查询阶段。验证者会从每个折叠轮次产生的承诺向量中随机查询多个位置(数量约为 λ / log(ρ^{-1}))。对于每个查询,证明者需要返回该位置的评估值以及相应的 Merkle 认证路径。验证者检查这些值是否满足折叠过程中定义的线性关系。

如果证明者在任何一轮折叠中作弊(没有正确执行折叠),那么在查询阶段,验证者有很大概率会发现不一致。

安全性分析

FRI 的安全性基于以下关键主张:如果一个作弊的证明者承诺的多项式 Q 距离最近的低度(k-1)多项式 H 的相对汉明距离为 δ,那么它通过所有 FRI 验证者检查的概率最多约为 (k / |F|) + (1 - δ)^t,其中 t 是查询次数。

第一项 k/|F| 是证明者在折叠阶段“幸运地”遇到能异常降低度数的随机挑战 r 的概率。第二项 (1-δ)^t 是即使没有幸运折叠,所有 t 次查询都未能检测到折叠不一致的概率。

需要注意的是,已知的最佳攻击表明,对于任何 Q(无论其距离低度多项式多远),存在一种攻击策略能以至少 ρ^t 的概率通过验证。这表明如果 FRI 的猜想安全性成立,那么它是紧的。

从 FRI 到多项式承诺方案

单纯的 FRI 低度测试只能让验证者相信所承诺的向量“接近”某个低度多项式,而不是完全等于一个特定的低度多项式。为了构建完整的多项式承诺方案,我们需要解决两个问题:

  1. 验证者可能请求评估的点 r 不在单位根集合 Ω 中。
  2. 验证者只知道 Q 接近某个低度多项式 H,但不知道 H 具体是什么。

解决方案利用了与 KZG 承诺类似的一个事实:对于度数小于 d 的多项式 Q,Q(r) = v 当且仅当存在一个度数小于 d 的多项式 W,使得 (Q(x) - v) = W(x) * (x - r)

在承诺方案中,当验证者请求 Q(r) 的评估值 v 时,证明者声称 Q(r) = v。验证者可以要求证明者对函数 (Q(x) - v) / (x - r) 应用 FRI 低度测试,以证明其度数小于 d-1。如果通过测试,则能以高概率保证所声称的 v 等于“最接近 Q 的低度多项式 H”在 r 处的值 H(r)

实际上,目前部署的 FRI 通常作为一种“列表多项式承诺方案”使用,它只将证明者绑定到一小组低度多项式,但这对于 SNARK 的安全性来说已经足够。

Fiat-Shamir 变换与具体安全性

几乎所有 SNARK 都使用 Fiat-Shamir 变换将交互式论证转化为非交互式。本节将详细讨论其工作原理和安全性考量。

Fiat-Shamir 变换原理

以一个三轮交互协议(证明者发送 α,验证者回复随机挑战 β,证明者最后发送 γ)为例。Fiat-Shamir 变换将挑战 β 替换为对证明者第一条消息 α(以及公开输入 x)的哈希值:β = Hash(α, x)。哈希函数被建模为随机预言机。

重要提示:为了确保适应性安全(即使对手能自适应地选择输入 x),必须将公开输入 x 包含在哈希计算中。忽略这一点是一个常见错误。

研磨攻击

应用 Fiat-Shamir 变换后,一个始终可用的攻击是“研磨攻击”。作弊的证明者可以反复尝试不同的第一条消息 α,直到找到一个能哈希产生“幸运”挑战 β 的 α(所谓幸运,是指对于这个 β,作弊者能轻易计算出使验证者接受的响应 γ)。

对于一个具有 2^(-λ) 声错误(即 λ 比特安全性)的交互式协议,研磨攻击在尝试 2^b 次哈希后成功的概率约为 2^(-λ + b)。这与许多其他密码原语(如碰撞抵抗哈希函数的生日攻击)的攻击成功率随计算量增长的速度不同,研磨攻击的成功率增长更快。

交互式 vs 非交互式环境下的安全级别

在交互式设置中,作弊者必须与验证者实际交互才能知道挑战 β,从而知道攻击是否成功。如果协议具有 λ 比特安全性,作弊者平均需要与验证者交互约 2^λ 次才能成功一次,这很容易被检测和阻止。

在非交互式设置中,作弊者可以本地进行研磨攻击,无需与验证者交互。因此,要达到相同的实际安全水平,非交互式协议需要比交互式协议高得多的安全参数 λ。例如,60比特安全性在交互式设置中可能足够,但在非交互式设置中则远远不够,因为 2^60 次哈希计算在现代硬件上是可行的。

多轮协议与轮次逐轮可靠性

将 Fiat-Shamir 应用于多轮交互式协议可能导致灾难性的安全性损失。一个经典的例子是:将一个声错误为 1/2 的协议顺序重复 λ 轮,可以得到声错误为 2^(-λ) 的协议。但对其应用 Fiat-Shamir 后,作弊者可以“逐轮研磨”,依次为每一轮找到幸运消息,总成本仅为 O(λ) 次哈希,从而使非交互式协议完全不安全。

为了避免这种安全性损失,需要证明原始的交互式协议满足“轮次逐轮可靠性”。这意味着作弊者不能通过在许多轮中分别获得一点点“幸运”来累积成功,而必须在一轮中就获得极大的幸运。已知像求和检查协议这样的多轮协议是满足轮次逐轮可靠性的。

一个重要说明:FRI 是一个对数轮次的协议,目前通常以非交互式方式部署(通过 Fiat-Shamir)。据我所知,尚未有公开证明表明 FRI 满足轮次逐轮可靠性。这是一个当前分析中的空白,协议设计者应意识到这一点,因为如果不满足该性质,应用 Fiat-Shamir 可能导致实际安全性远低于预期。

总结

在本节课中,我们一起学习了:

  1. 基于 FRI 的多项式承诺方案:它通过让证明者在单位根子集上进行 Merkle 承诺,并结合一个交互式的“折叠-查询”低度测试,实现了透明且可能后量子安全的承诺方案。其性能在证明者时间、证明大小和验证时间之间存在可调的权衡。
  2. Fiat-Shamir 变换的深入细节:我们回顾了其工作原理,重点讨论了研磨攻击及其对安全性的影响,比较了交互式与非交互式设置下所需安全级别的差异,并强调了将 Fiat-Shamir 应用于多轮协议时,验证轮次逐轮可靠性的重要性。

理解这些细节对于设计和评估使用 FRI 和 Fiat-Shamir 的密码协议至关重要。下一讲将涵盖基于线性 PCP 的 SNARK,例如 Groth16。

009:基于线性 PCP 的 SNARKs

在本节课中,我们将要学习基于线性 PCP(概率可检查证明)和 QAP(二次算术程序)构造 SNARKs 的技术。这种技术是构建最早一批高效 SNARK 实现的基础,其特点是证明尺寸极短,验证速度极快。

概述

我们将首先回顾 SNARKs 的发展脉络,然后深入探讨二次算术程序(QAP)的核心构造。接着,我们将学习如何利用双线性配对将 QAP 编译成一个具有恒定尺寸证明的 SNARK。最后,我们将介绍该框架的一些重要变体,包括对 R1CS 约束系统的支持、Gro16 方案的优化以及如何实现零知识性。

第一部分:二次算术程序(QAP)

上一节我们回顾了 SNARKs 的发展历程,本节中我们来看看如何将电路可满足性问题转化为一个多项式方程,即二次算术程序。

计算轨迹与选择子多项式

我们的目标是证明一个算术电路 C 存在一个满足的赋值 w,使得 C(w) = y。为了做到这一点,我们首先定义计算的“轨迹”。

  • 轨迹定义:对于 QAP,我们将轨迹定义为电路中所有乘法门的输入和输出值组成的向量。加法门的输出值不直接包含在轨迹中,但会通过后续的多项式定义被隐式处理。

假设电路有 m 个这样的值(包括输入和乘法门输出),n 个乘法门。我们将轨迹表示为一个向量 c = (c1, c2, ..., cm)。

接下来,我们为电路中的每个乘法门 j (j=1,...,n) 定义一个公开的“选择子多项式”集合。这些多项式编码了电路中各条线如何连接到乘法门。

以下是定义这些多项式的步骤:

  1. 左输入选择子多项式 L_i(x):对于轨迹中的每个值 c_i,我们定义一个多项式 L_i(x)。该多项式在点集 {ω^1, ω^2, ..., ω^n}(其中 ω 是 n 次单位根)上的取值规则为:如果 c_i 是第 j 个乘法门的左输入,则 L_i(ω^j) = 1,否则为 0。然后,我们通过插值法得到唯一的 n-1 次首一多项式 L_i(x)。

  2. 右输入选择子多项式 R_i(x):类似地,定义 R_i(x)。如果 c_i 是第 j 个乘法门的右输入,则 R_i(ω^j) = 1,否则为 0。通过插值得到 R_i(x)。

  3. 输出选择子多项式 O_i(x):定义 O_i(x)。如果 c_i 是第 j 个乘法门的输出,则 O_i(ω^j) = 1,否则为 0。通过插值得到 O_i(x)。

这些多项式被称为“选择子多项式”,因为当我们在特定点(如 ω^j)计算它们的线性组合时,可以“选择”出对应乘法门的输入或输出值。

主多项式与 QAP 方程

基于选择子多项式和轨迹向量,我们定义三个“主”多项式:

  • L(x) = Σ_{i=1}^{m} c_i * L_i(x)
  • R(x) = Σ_{i=1}^{m} c_i * R_i(x)
  • O(x) = Σ_{i=1}^{m} c_i * O_i(x)

这些多项式的关键性质在于:

  • L(ω^j) 恰好等于第 j 个乘法门的左输入值。
  • R(ω^j) 恰好等于第 j 个乘法门的右输入值。
  • O(ω^j) 恰好等于第 j 个乘法门的输出值。

现在,我们定义主多项式 P(x)
P(x) = L(x) * R(x) - O(x)

如果轨迹 c 是电路的正确赋值,那么对于每一个乘法门 j,都有 左输入 * 右输入 - 输出 = 0。这意味着:
P(ω^j) = 0, 对于所有 j = 1, ..., n

这个条件等价于:多项式 P(x) 在点集 {ω^1, ..., ω^n} 上取值为零。这进一步等价于:存在一个多项式 Q(x),使得:
P(x) = V(x) * Q(x)
其中 V(x) = Π_{j=1}^{n} (x - ω^j) 称为消失多项式,它在给定点集上为零。

因此,电路可满足性问题被转化为一个多项式方程问题:证明者需要证明他知道一个轨迹向量 c 和一个商多项式 Q(x),使得上述等式成立。

小结

在本节中,我们学习了如何将电路可满足性问题编码为二次算术程序。核心思想是利用选择子多项式将电路结构信息公开化,并将计算正确性条件转化为一个多项式可除性条件:P(x) 必须能被消失多项式 V(x) 整除。这为后续使用密码学工具进行高效证明奠定了基础。

第二部分:从 QAP 到 SNARK

上一节我们介绍了如何构造 QAP,本节中我们来看看如何利用密码学工具,特别是双线性配对,将 QAP 编译成一个具有恒定尺寸证明的 SNARK。

线性 PCP 模型

在深入构造之前,我们先理解一下线性 PCP 模型。它是经典 PCP 模型的推广:

  • 证明者:生成一个由向量 c 定义的线性函数作为预言机。对于查询向量 q,预言机返回内积 <c, q>
  • 验证者:向该预言机提交多个线性查询(即向量),并根据返回的标量值验证陈述的正确性。

QAP 可以看作线性 PCP 的一个实例。验证者可以通过向由 c 和 Q(x) 的系数构成的预言机提交特定的线性查询,来随机检查等式 P(x) = V(x) * Q(x) 在某个随机点是否成立。

然而,我们不会直接使用这个交互式模型,而是使用密码学原语将其编译成非交互式证明。

使用双线性配对的 SNARK 构造

我们使用双线性配对群 (G1, G2, G_T),其阶为 p,生成元为 g。双线性配对 e: G1 × G2 -> G_T 满足 e(g^a, g^b) = e(g, g)^{ab}。

构造的核心是在一个秘密点 τ(在可信设置阶段生成后必须删除)上“在指数中”计算 QAP 多项式。

以下是构造步骤:

  1. 可信设置(电路特定)

    • 生成随机数 τ, α, β。
    • 计算并发布证明密钥(给证明者):
      • 对于所有 i ∈ [m](秘密导线索引):g^{L_i(τ)}, g^{α * L_i(τ)}, g^{R_i(τ)}, g^{α * R_i(τ)}, g^{O_i(τ)}, g^{α * O_i(τ)}
      • 对于所有 i ∈ [m](秘密导线索引):g^{β * (L_i(τ) + R_i(τ) + O_i(τ))}
      • g^{τ}, g^{τ^2}, ..., g^{τ^{n-1}} (用于计算商多项式)
      • g^{β}, g^{α}
    • 计算并发布验证密钥(给验证者):
      • g^{V(τ)} (消失多项式在 τ 处的值)
      • 对于所有 i ∈ [m](公开输入/输出导线索引):g^{L_i(τ)}, g^{R_i(τ)}, g^{O_i(τ)}
      • g^{β}, g^{α}, e(g, g)^{αβ}
  2. 证明生成
    证明者使用轨迹 c 和计算出的商多项式 Q(x) 的系数,利用证明密钥计算以下群元素作为证明 π:

    • π_L = g^{ Σ_{i∈I_mid} c_i * L_i(τ) } (使用 g^{L_i(τ)} 计算)
    • π_R = g^{ Σ_{i∈I_mid} c_i * R_i(τ) } (使用 g^{R_i(τ)} 计算)
    • π_O = g^{ Σ_{i∈I_mid} c_i * O_i(τ) } (使用 g^{O_i(τ)} 计算)
    • π_Q = g^{ Q(τ) } (使用 g^{τ^k} 计算)
    • π_C = g^{ Σ_{i∈I_mid} c_i * β * (L_i(τ) + R_i(τ) + O_i(τ)) } (使用 g^{β*(L_i+R_i+O_i)(τ)} 计算,用于一致性检查)
  3. 验证
    验证者执行以下步骤:
    a. 重构完整承诺:利用公开输入/输出值及其对应的验证密钥中的项,计算完整的左、右、输出多项式承诺:
    * π_L' = π_L * Π_{i∈I_io} (g^{L_i(τ)})^{c_i}
    * π_R' = π_R * Π_{i∈I_io} (g^{R_i(τ)})^{c_i}
    * π_O' = π_O * Π_{i∈I_io} (g^{O_i(τ)})^{c_i}
    b. 检查 QAP 主方程:验证以下配对等式:
    * e(π_L', π_R') / e(π_O', g) = e(g^{V(τ)}, π_Q)
    这个等式在指数中对应检查:L(τ)*R(τ) - O(τ) = V(τ)*Q(τ)
    c. 检查系数一致性:验证证明者在计算 π_L, π_R, π_O 时使用了相同的系数 c
    * e(π_L * π_R * π_O, g^{β}) = e(π_C, g)
    如果使用不同的系数,证明者将无法通过此检查。

安全性考虑与优化

上述构造(基于 PGHR13/Pinocchio)解决了几个关键问题:

  1. 知识性:通过知识指数假设(KOE)或通用群模型(GGM),确保证明者必须按照预设形式使用证明密钥来生成证明。
  2. 系数一致性:通过额外的项 g^{β*(L_i+R_i+O_i)(τ)} 和证明元素 π_C,确保 π_L, π_R, π_O 由相同的轨迹向量 c 计算而来。
  3. 公开输入/输出:通过将公开值相关的计算移交给验证者,支持电路的公开部分。

该 SNARK 的特性包括:

  • 证明尺寸:恒定(Pinocchio 中为 5 个群元素,约数百字节)。
  • 验证时间:近乎恒定(主要成本是几次双线性配对运算,小于 1 毫秒)。
  • 证明时间:与电路大小成线性关系,涉及 FFT 和线性次数的群指数运算。
  • 主要缺点:需要针对每个电路进行可信设置。

小结

在本节中,我们学习了如何利用双线性配对将 QAP 编译成一个高效的 SNARK。核心思想是在一个秘密点 τ 的指数上进行多项式计算和验证。通过精心设计的可信设置和验证方程,我们能够获得一个证明尺寸极小、验证速度极快的非交互式论证系统。

第三部分:变体与扩展

上一节我们介绍了基于 QAP 的经典 SNARK 构造,本节中我们来看看该框架的一些重要变体和扩展。

支持 R1CS 约束系统

算术电路是 R1CS 的一个特例。R1CS 提供了更通用的约束形式:
(A · c) ⊙ (B · c) = (C · c)
其中 A, B, C 是公开的 m×n 矩阵,c 是轨迹向量,⊙ 表示逐分量乘法。

将 QAP 推广到支持 R1CS 非常简单:只需将选择子多项式 L_i(x), R_i(x), O_i(x) 在点 ω^j 的取值从 {0, 1} 扩展到任意公开的域元素(即矩阵 A, B, C 的第 j 行第 i 列的值)。插值和后续的 QAP 构造过程完全不变。

这种矩阵视角将 SNARK 的构造分解为两个核心子问题:

  1. 线性检查:证明一个承诺向量与一个公开矩阵的乘积等于另一个承诺向量。
  2. 哈达玛积检查:证明两个承诺向量的逐分量乘积等于第三个承诺向量。

这个视角也被许多其他 SNARK 系统(如 Bulletproofs, Marlin, Spartan)所采用。

Groth16 优化:三元素证明

Groth16 方案在 Pinocchio 的基础上进一步优化,将证明尺寸减少到仅 3 个群元素,这是目前已知的最短证明之一。

核心思想是巧妙地重组证明元素和验证方程。简化的直觉如下:

  • 将原本的 π_L 和 π_R 与随机掩码 α, β 结合。
  • 将 π_O, π_Q 和用于一致性检查的项合并到一个新的证明元素 π 中。
  • 通过调整可信设置和验证方程,最终只需要 (π_A, π_B, π_C) 三个群元素,并通过一个配对等式完成验证:
    e(π_A, π_B) = e(π_C, g) * e(一些公开的验证密钥项)

具体的构造涉及更复杂的代数变换,但其根源仍然是 QAP 和线性 PCP 框架。

实现零知识性

原始的 QAP SNARK 证明是确定性的,可能会泄露关于见证的部分信息。为了实现零知识性,需要在证明中加入随机性。

基本思路是在指数中添加一个随机倍数 δ 的消失多项式 V(τ):

  • 例如,将 π_L 修改为 g^{ Σ c_i*L_i(τ) + δ_L * V(τ) }
  • 由于 V(τ) 是公开验证密钥的一部分,且在主方程检查中会被消去(因为配对等式的右边包含 e(g^{V(τ)}, π_Q)),这种随机化不会破坏验证的有效性。
  • 同时,随机数 δ 使得证明在统计上隐藏了真实的轨迹向量 c,从而实现了零知识。

无论是 Pinocchio 还是 Groth16,都可以通过类似的方法(添加适当的随机化项)来获得零知识版本。

总结

在本节课中,我们一起学习了基于线性 PCP 和二次算术程序构造 SNARKs 的技术。

我们首先深入探讨了 QAP 的构造,它将电路可满足性问题转化为一个多项式可除性问题。接着,我们学习了如何利用双线性配对,通过电路特定的可信设置,将 QAP 编译成一个证明尺寸恒定、验证高效的 SNARK。最后,我们介绍了该框架的重要变体:支持更通用的 R1CS 约束系统、实现超短证明的 Groth16 方案,以及如何为这些构造添加零知识性。

这种基于线性 PCP 的 SNARK 是最早实现并广泛应用的技术之一,其极短的证明和快速的验证使其在许多隐私计算和区块链场景中具有独特优势,尽管其电路特定的可信设置是一个需要考虑的权衡点。

010:递归的 SNARKs 🌀

在本节课中,我们将要学习一个引人入胜的概念:递归的 SNARK。首先,我们会探讨递归有什么用处,然后我们将了解如何构建高效的递归 SNARK。

概述:什么是递归 SNARK?

首先,我们快速回顾一下构成一个 SNARK 的三个算法。一个预处理 SNARK 是一个由三个算法组成的元组:S(设置)、P(证明)和 V(验证)。

  • 设置算法 S:接收一个电路,对其进行预处理,输出证明者的公共参数和验证者的公共参数。
  • 证明算法 P:使用证明者参数、公开陈述 X 和见证 W 来生成一个证明 π。
  • 验证算法 V:使用验证者参数、公开陈述 X 和证明 π 来决定是接受还是拒绝该证明。

在之前的课程中,我们看到了几种 SNARK 构造。例如,我们研究了 Groth16 和 PlonK SNARK。特别是使用 KZG 多项式承诺方案的 PlonK。这些构造能产生很短的证明,但证明者的生成时间不是线性的,实际上是 O(N log N),其中 N 是计算的大小。我们称之为拟线性时间证明者。

我们还研究了其他 SNARK 构造,特别是基于 FRI 的构造以及基于编码的构造(如 Brakedown、Orion、Orion+)。这些系统在实践中拥有更快的证明者,但不幸的是,它们生成的证明更长。

一个自然的问题是:我们能否鱼与熊掌兼得,即拥有一个既快速又能生成非常简短证明的证明者?为了开始回答这个问题,让我们先看看 SNARK 递归的一般原理。

递归证明的原理

当我们谈论证明递归时,我们指的是什么?让我们从两层 SNARK 递归开始解释。

通常,当我们应用一个 SNARK 时,我们有一个陈述 X 和一个见证 W,我们生成一个证明,表明见证 W 是陈述 X 的有效见证。

在递归 SNARK 中,其思想不是直接生成实际的证明,而是生成一个“关于证明的知识”的证明。换句话说,我们不是证明我们知道陈述 X 的一个见证 W,而是证明我们知道一个“关于陈述 X 的见证 W 的证明”。

让我们看看这是如何运作的。假设我们从一个陈述 X 和一个见证 W 开始。我们像往常一样,使用我们的证明系统(SNARK)来生成一个证明 π,证明见证 W 是陈述 X 的有效见证。这个证明 π 证明了我们知道一个 W,使得 C(x, W) = 0。

但这并不是我们停止的地方。现在我们要做的是,在这个证明 π 之上运行另一个证明系统。这个外层证明系统的公开陈述仍然是陈述 X,但其见证实际上是来自内层系统的证明 π。

因此,第二层系统实际上是在证明:我知道一个证明 π,该证明表明见证 W 是陈述 X 的有效见证。我们不是直接证明见证的知识,而是证明一个“证明该见证有效”的知识。

我们最终得到一个证明 π‘。它实际上证明的是:这个第二层证明 π’ 知道一个 π,使得 π 是陈述 X 的有效证明。因此,证明 π‘ 所处理的电路实际上是内层系统验证算法 V 的电路。

我们通常将第一层系统称为内层系统,它实际上是在证明相对于电路 C 的见证 W 的知识。第二层系统(外层系统 S‘, P‘, V‘)实际上是相对于一个实现了内层系统验证算法 V 的电路来生成证明。

所以你可以看到,π‘ 是一个递归证明,因为我们是在证明一个“证明的知识”,而不是直接证明一个“见证的知识”。这个图景在本节课中非常重要,递归的全部意义在于,与其直接证明见证的知识,不如证明一个“见证存在”的证明的知识。

递归的应用

应用一:证明压缩(兼顾速度与长度)

让我们看看递归的第一个应用:构建一个既拥有快速证明者,又能产生简短证明的系统。

假设内层证明系统(SPV)拥有快速的证明者,但可能产生较大的证明。例如,证明者和验证者在这个系统中都很快,但最终的证明 π 相当长(例如,100 KB)。

我们想要做的是将这个证明压缩成更短的东西。我们可以运行外层系统,它可能拥有较慢的证明者,但能产生较短的证明。现在,外层证明系统将再次证明:内层系统的验证者会接受见证证明 π。这将产生一个外层证明 π‘,它可能只有 1 KB 长。

这里的要点是:想象一下我们实际要证明见证知识的电路 C 非常巨大。直接使用外层系统为电路 C 构造证明会相当慢(因为证明者慢),虽然会产生小证明,但生成那个证明需要很长时间。

相反,我们可以这样做:使用这个内层证明系统为巨大的电路 C 快速生成一个证明(我们试图证明 W 是有效见证)。这比使用外层证明系统运行得更快,但会产生一个大证明。然后,我们只在内层系统的验证器电路上运行外层系统。因此,如果内层系统的验证器电路比我们试图用内层系统证明的原始电路 C 小得多,那么外层系统的工作速度将比我们试图直接用外层系统为原始电路 C 生成证明要快得多。

通过这种方式,我们实现了两全其美。我们使用快速的证明者来处理直接查看 W 的大型电路,但这会产生一个大证明。然后我们使用证明者较慢的内层系统,但现在它只应用于一个小得多的电路(即内层系统的验证器)。最终结果将只是一个 1 KB 长的证明。我们得到了一个快速的总体证明者,并且最终证明实际上很短。

这只有在内层系统的验证器电路比我们试图证明的实际陈述简单得多时才适用。这对于证明非常复杂的陈述非常有用,例如在证明 ZK EVM 的大规模执行时经常出现。

当然,当验证者想要验证 W 确实存在时,证明者实际上知道使得 C(x, W) = 0 的 W,它将验证递归证明 π‘,而证明 π 永远不会被实际世界中的验证者看到。

知识可靠性的论证

你可能会想,这为什么是可靠的呢?让我们快速论证一下为什么这个递归构造能提供知识可靠性。

固定某个电路 C。它接收 n 个域元素作为陈述,m 个域元素作为见证,并输出 0 或 1,基本上说明见证对陈述是否有效。

快速回顾一下知识可靠性的定义。我们说一个 SNARK (S, P, V) 对于电路 C 是知识可靠的,如果以下条件成立:对于每一个恶意的多项式时间证明者 A,存在一个高效的提取器 E,满足以下性质:对于所有我们可以想象的陈述 Y,如果我们的恶意证明者能够产生有效证明的概率(即恶意证明者能够为陈述 Y 生成令人信服的证明的概率),那么无论这个概率是多少,提取器在陈述 Y 上运行时,实际上能够从该恶意证明者中提取出一个见证,并且它产生有效见证的概率大于恶意证明者能够说服验证者的概率减去某个小的 ε(可忽略误差)。这个 ε 被称为知识误差。

我们想要证明我们的两层递归 SNARK 对于特定电路 C 是知识可靠的。让 C‘ 是外层系统实际使用的电路。C‘ 基本上是一个运行内层系统验证器的电路,使用证明 π 作为见证。

假设我们有一个令人信服的证明者 A,它对于外层系统 (S‘, P‘, V‘) 和电路 C‘ 是令人信服的。我们需要构建一个提取器,它不提取证明 π(那不是我们想要的),我们想要的是一个实际上为原始电路 C 提取见证的提取器。

我们的提取器将这样工作:给定一个陈述 X 以及内层系统的验证参数。我们知道外层系统对于电路 C‘ 是知识可靠的(根据假设)。这意味着存在一个提取器 E‘,可以从证明者 A 中提取一个证明 π,使得电路 C‘ 被满足。换句话说,内层验证算法在给定提取的证明 π 时会输出“是”。

但现在看这个 E‘,E‘ 是一个输出证明 π 的电路,该证明说服了内层证明系统。换句话说,E‘ 现在成为了内层证明系统的一个令人信服的证明者。那么,因为内层证明系统本身是知识可靠的,我们知道存在一个提取器 E,可以从提取器 E‘(将 E‘ 视为恶意证明者)中提取一个见证,使得 C(x, W) = 0。

所以这是一个两步提取过程。我们首先从外层证明系统中提取一个证明,然后现在我们有了一个来自外层证明系统的证明提取器,我们可以运行内层证明系统的提取器,并获得我们感兴趣陈述的实际见证。

我们的提取器(由先后运行两个提取器组成)成功的概率恰好是 A 说服外层证明系统的概率减去两个知识误差之和。因为两个知识误差都是可忽略的,它们的和也是可忽略的,所以总体提取成功。

这就是为什么两层递归构造是可靠的,因为我们可以从外层证明系统中提取,然后再从内层证明系统中提取,从而得到我们感兴趣的实际见证。

我们必须稍微注意的一点是,这些提取器的运行时间可能会随着递归的进行而变得越来越差。想象一下,外层证明系统的提取器 E‘ 的运行时间是恶意证明者运行时间的两倍。内层证明系统也是如此。那么我们整体提取器的运行时间将是给定恶意证明者运行时间的四倍。对于两层递归,这完全可以接受,四倍时间仍然是多项式时间。

但是,想象在一个 n 层递归系统中。如果我们必须重复这个过程 n 次,我们最终得到的最终提取器将以 2^n 倍于原始恶意证明者的时间运行,这不再是多项式时间。所以我们必须小心,递归深度实际上只能是对数于安全参数的,否则我们最终会得到一个运行时间过长的提取器。人们可能认为这只是我们证明技术的一个产物。也许有另一种证明技术可以避免这种指数级增长。但通常,我们会尝试将递归深度限制为最多安全参数的对数,这实际上避免了这个问题。但在某些应用中,我们会忽略这个技术问题,给出一个使用线性递归深度的构造。通常,只要我们有一个线性递归深度的构造,我们通常可以将其转换为对数递归深度的构造。

递归中的另一个问题:随机预言机

递归中还有另一个问题需要指出。当我们构造 SNARK 系统时,我们经常使用 Fiat-Shamir 变换将交互式证明系统转换为非交互式的。Fiat-Shamir 变换引入了随机预言机的概念,证明者和验证者通过使用哈希函数(我们将其建模为随机预言机)来生成挑战,以使证明系统非交互。

问题是,当我们进行递归时,我们最终会得到一个验证器电路,其中嵌入了随机预言机门。但现在,递归证明者必须处理内层证明系统的验证器电路,如果这个验证器电路中有随机预言机门,递归证明者就不知道如何处理这些门。这些不是计算门,它们调用随机预言机,而证明者无法处理。

因此,在递归实际工作之前,我们必须以某种方式摆脱这些随机预言机门。对此有一个标准的答案:在开始递归过程之前,我们用具体的哈希函数实例化验证器电路中的所有随机预言机。我们用具体的哈希函数(如 SHA-256 或其他更适合我们算术电路的哈希函数,如 Poseidon)替换随机预言机。关键是,我们用非常具体的哈希函数替换了随机预言机。

当然,一旦我们脱离了随机预言机模型,我们就不再拥有 SNARK 系统是知识可靠的安全证明。我们必须假设,在此之后,证明系统仍然是知识可靠的。我们可以通过它在随机预言机模型中成立这一事实来证明这个假设是合理的,但严格来说,一旦我们实例化了随机预言机,现在我们必须做出另一个假设,即实例化后的系统仍然是安全的。

关键是,现在我们有了一个实例化的系统,其中验证器电路不再使用随机预言机门,而是使用具体的哈希函数。因此,验证器电路是一个具体的电路,这现在允许我们进行递归,因为外层证明者现在可以实际处理内层 SNARK 系统的验证器电路。当然,为了证明由此产生的递归 SNARK 是安全的,我们必须依赖这个有些“丑陋”的假设,即这个用真实哈希函数替换了随机预言机的具体系统仍然是知识可靠的。

总结与更多应用

总结我们目前的讨论,我们看到的递归的第一个应用与证明压缩有关。当然,这个构造可以推广到三层递归、四层递归等等,停在两层递归并没有什么神奇之处。

接下来我想展示的下一个应用是为了流式证明生成。流式证明生成是什么意思?想象一下,我们有一个证明者,例如在 Rollup 中,它需要一次证明许多陈述。它需要证明一大堆交易(比如 100 笔交易)都是有效的。这意味着它实际上试图证明它知道 W1, W2, ..., Wn,使得 W1 是 x1 的有效见证,W2 是 x2 的有效见证,依此类推。它试图证明这个陈述的合取。

这样做的问题是,如果你必须为这整个合取生成一个单一的、庞大的证明,那实际上可能非常昂贵且构建缓慢。特别是,你只能在拥有了所有要证明的 n 个陈述之后才能开始构建这个证明。但在现实中,情况并非如此。当你构建一个 Rollup 系统时,公众会一次发送给你一笔交易。你可以等到有 1000 笔交易后再开始一次性为所有 1000 笔交易生成证明。但我们希望做的是,一旦第一笔交易可用,就开始生成证明交易有效的证明。我们称之为流式证明生成,因为我们不想等到所有陈述都可用后才生成证明,我们希望在陈述发送给我们的过程中就开始生成证明。

同样,递归是实现这一点的非常好的方法。让我们看看两个世界来解释这个问题。想象我们有 100 笔交易正在发送给我们。天真地,正如我们所说,我们只能在最后一笔交易提交后才能开始为这 10 笔交易生成证明。因此,在最后一笔交易提交和我们实际可以将证明提交到 Layer 1 链之间会有相当长的延迟。

我们希望做得更好。我们使用递归来做得更好。我们将获取前 10 笔交易,然后为它们生成一个证明。然后我们为第二批 10 笔交易生成一个证明。依此类推。所以你看,我们已经在交易还在流入时就开始生成证明了。最后,一旦所有交易都可用,我们要做的就是获取我们生成的 10 个证明,并生成一个“证明的证明”。我们生成一个证明,表明这 10 个证明是有效的,最后,这成为我们提交到 Layer 1 链的证明。

关键是,现在在所有 100 笔交易被接收后,我们唯一要做的就是为最后 10 笔交易的批次生成一个证明,以及一个“证明的证明”。这两个操作都比在收到所有交易后为所有交易生成一个单一的整体证明要快得多。因此,我们在最后一笔交易被接收和我们能够将东西发布到 Layer 1 之间的延迟要短得多。

这又是递归的另一个非常巧妙的应用,可以将其视为在陈述一次一个地流向您时加速证明生成的一种方式。

应用三:增量可验证计算

我们还有两个应用要展示,然后我们将开始研究如何构建高效的递归证明。

下一个应用实际上是一个重要的应用,称为增量可验证计算。这也可以用来大大加速证明生成。

这里的设置是,我们有一个通过迭代某个固定函数 F 来完成的非常长的计算。计算从某个初始状态 S0 开始,第一个输入 ω1 到来,我们应用函数 F,得到一个新状态 S1。另一个输入到来,我们得到一个新状态 S2。我们一直这样做,直到最终得到最终状态 Sn。

我们的目标基本上是生成一个简洁的证明,证明者知道见证 ω1 到 ωn,使得最终输出 Sn 实际上是正确的。换句话说,Sn 是将 ω1 到 ωn 应用于初始状态 S0 的结果。验证者当然知道函数 F,因为那是公共函数。它知道我们运行函数 F 的步数。它有初始状态和最终状态,它只想验证最终状态是正确的。

这实际上在实践中经常发生,尤其是在你将 F 视为微处理器时。想象 F 字面上实现了一个简单的微处理器,那么这里发生的是,我们实际上是在逐步遍历计算的状态。由于 F 是一个图灵完备的微处理器,这基本上捕获了通过运行这个微处理器许多许多周期而发生的任意计算。

既然我们提到了通过迭代函数 F 完成的长时间计算,我忍不住要联系到 Deep Thought。正如你可能从《银河系漫游指南》中记得的那样,Deep Thought 是一个由文明建造的计算机。文明问计算机“生命、宇宙和一切的答案是什么”。计算机说:“哦,这是一个非常有趣的问题。我需要 700 万年才能找出答案。”于是它工作了 700 万年。这就是我们在这里想到的长时间计算,在 700 万年后,它说答案是 42。这不是一个非常有帮助的答案,但那就是答案。这就是一个人们可能也想为其生成一个简洁证明的非常长计算的例子,证明计算是正确完成的,因此 42 确实是生命、宇宙和一切问题的正确答案。这样我们就不必相信 Deep Thought 的话了。

IVC 的构造

构造是一个非常自然的想法。在每一步,我们当然会输出该步骤的状态,但我们也会输出一个证明,表明计算直到该步骤都是正确的。具体来说,对于 i = 1 到 n,在步骤 i,证明者将输出当前状态 Si 以及一个证明 πi。这个证明证明了证明者有一个见证,即前一个状态 S(i-1)、当前步骤的输入 ωi 以及前一个状态正确的证明 π(i-1)。见证必须满足以下属性:首先,如果我们将 F 应用于这个前一个状态和 ωi,我们得到当前状态。这证明了 Si 是正确的。更重要的是,我们证明了前一个状态的证明 π(i-1) 实际上是相对于前一个计算状态的有效证明。

同样,在每一步,我们输出当前状态和一个证明,该证明证明:a) 当前状态相对于前一个状态是正确的,并且 b) 前一个状态的证明相对于前一个状态也是正确的。

我想简要地说服你,这意味着最后一个证明 πn 连同最终输出 Sn 证明了,实际上,证明者知道 ω1 到 ωn,使得输出 Sn 确实是正确的。为什么这是真的?我们需要构建一个提取器来证明知识可靠性。我们想要证明证明者知道使 Sn 正确的 ω1 到 ωn。我们将在 πn 上运行提取器。提取器将从 πn 中提取什么?它将提取前一个状态的有效见证。这个在点 n-1 的有效见证向我们证明 F(S(n-1)) = Sn,并且 π(n-1) 是前一个陈述的有效证明。现在我们将再次运行提取器,每次运行它,我们都会提取前一个状态,直到最开始,我们将提取 S0,然后电路只是检查 S0 确实是正确的初始状态。

因为我们可以迭代地运行提取器,并提取计算中的所有状态,并且我们知道所有这些状态实际上都是正确的,我们可以由此推断 πn 证明了 Sn 确实是计算的正确输出。

这是对提取器如何工作的高级描述,但这实际上可以变成一个正式的论证,对你来说,通过反复应用提取器,我们实际上可以恢复整个计算轨迹,并且该计算轨迹必须是正确的,这可能是一个很好的练习。

在这一点上,你可能想知道我之前所说的线性递归深度与对数递归深度的问题。我不会在这里深入细节,但要知道,实际上 IVC 也可以仅使用对数深度递归来证明是安全的。

IVC 的应用

让我们谈谈 IVC 的一些应用。第一个应用是我们已经讨论过的,你可以将一个长时间的计算分解为一系列小步骤,例如,如果函数 F 实现了一个微处理器,如 RISC-V 或 EVM 的步骤。现在证明者要做的就是一次证明计算的一个步骤,而不是在计算的整个生命周期上证明一个单一的整体证明。我们只是证明这个函数 F 的一个步骤,而不是一次证明所有东西。因为我们只证明一个步骤,这是一个足够简单的证明,它大大减少了证明者的内存需求。现在内存需求只是证明计算的一个步骤所需的内存。这就是人们喜欢做递归的原因之一,它允许我们证明非常非常大的陈述,否则我们可能无法证明,例如由于内存限制,但我们可以一步一步地做,这实际上根本不需要那么多内存。

我想提到的 IVC 还有另外两个非常巧妙的应用。第一个是构建一个单一的简短简洁证明,证明区块链的当前状态是正确的。我可以将整个区块链的有效性证明压缩成一个单一的简洁证明。我们这样做的方式是,让 S0 成为链的初始状态,让 Sn 成为链的当前状态。那么 ω1 到 ωn 就是我们每一步提供给 F 的输入,基本上是有效的交易区块,从 S0 到 S1 的转换只是说明 ω1 是一个有效的交易区块,以及这些交易如何改变链的状态。因此,证明 Sn 是正确的,就是一个简短的证明,验证速度快,表明所有区块都是有效的,并且它们确实导致了当前状态 Sn,这已经使验证者相信 Sn 是正确的。

没有这种机制,如果一个新验证者启动并想要验证 Sn 是正确的,它将不得不下载所有区块并自己重新运行它们,直到获得 Sn。只有这样,它才相信区块链是正确的。有了这些 IVC 方法,当一个新验证者启动并想要验证链的当前状态时,它所要做的就是验证证明 π。当然,它必须确保每个人都同意相同的 Sn。这是一个共识问题。但验证这个 Sn 是否正确是通过一个非常简短和简洁的证明来完成的。这在 Mina 区块链中使用,你可以通过字面上只验证一个证明来验证整个链的状态。

我想提到的另一个应用是所谓的可验证延迟函数,其目标是以一种计算无法通过并行性加速的方式计算一个函数。我们想要一个顺序计算,但要以这样的方式完成:一旦你计算出最终答案,实际上很容易说服别人 Sn 是正确的。我们这样做的方式基本上是使用哈希链。我们从 S0 开始,一遍又一遍地应用哈希,最终得到 Sn。这通常表示为 H^n(S0)。IVC 证明现在将使用一个验证速度快的简短证明来证明 Sn 是正确的。我们再次使用 IVC 的原因是,证明者实际上可以一步一步地做这个证明,而不必求助于如果它想一次性完成整体证明所需的大量内存。因此,IVC 允许我们以相对较少的内存对这些长时间计算进行证明。

应用四:ZK 证明者市场

我将给你的最后一个应用,然后我们将停止讨论应用并开始讨论构造,那就是这个即将到来的 ZK 证明者市场。人们家里有 GPU,也许用于游戏设备等,他们并不总是使用这些 GPU,他们可能想租出 GPU 以便其他人可以使用它们。然后将会有一个这些证明者的市场,市场接收需要被证明的陈述,例如,需要通过 ZK Rollup 证明的一堆交易。市场将从公众那里接收这些交易集合,然后将其分配给当前可用于进行证明的各种 GPU。

但是,我们不想将所有交易的整个工作分配给单个 GPU。我们希望将其分解成多个部分。因此,市场可能会将“证明交易 1 和 2 是正确的”分配给一个证明者,并同时将“证明交易 3 和 4 是正确的”分配给另一个证明者。现在,我们有了这两对交易的两个证明。第三个证明者现在将构建一个“证明的证明”,证明 π1 和 π2 是正确的,这样最终的证明将是 π,这就是将被推送到 Layer 1 链的东西。

这又是递归出现的一个例子,我们可以并行地让多个实体证明各种交易是正确的,然后我们递归地生成一个“证明的证明”,这就是实际被推送到链上的东西。

你可以看到递归有许多许多应用。SNARK 不仅用于证明陈述是正确的,还用于证明“证明的证明”,正如你所见,这在不同应用中一次又一次地出现。

递归的技术问题:算术与曲线选择

现在我们已经理解了递归证明能为我们做的所有美妙事情,我想谈谈进行递归时出现的一个技术问题。特别是,我想谈谈如何选择特别适合递归证明的曲线的问题。

首先,让我们快速回顾一下什么是两层 SNARK 递归。基本上,有一个内层证明系统 (S, P, V),有一个公开陈述 X,证明者 P 将产生一个证明 π,证明它知道陈述 X 的有效见证 W。这就是证明 π 所证明的。然后,外层证明系统基本上将使用证明 π 作为见证,并产生一个证明 π‘,证明证明者 P’ 知道一个将被内层证明系统接受的证明 π。因此,P‘ 不是直接证明它知道一个见证 W,而是证明它知道一个证明 π,该证明将被内层证明系统的验证者接受。

现在让我们快速回顾一下这些证明系统在高层次上是如何工作的。固定某个电路 C 和一个陈述 X。为了证明我知道一个见证 w 使得 C(x, w) = 0,我们使用对多项式的承诺。例如,我们可能承诺于单变量多项式或多变量多项式,但让我们暂时关注单变量多项式。证明者承诺于一个基本上编码了计算轨迹的多项式,然后它证明实际上计算轨迹是一个有效的计算轨迹。

我们如何承诺于一个单变量多项式?例如,我们可以使用 KZG 证明系统。为此,我们基本上需要一个阶为 p 的群。我们的电路是为有限域 Fp 中的算术运算定义的,所以我们进行模 p 的加法和乘法。因此,为了承诺于定义在 Fp 上的多项式,我们需要一个阶为 p 的群。使用这样的群,实际上,对 Fp 上多项式的 KZG 承诺是群 G 中的一个单一群元素。

但重要的是要记住,如果我们要支持在 Fp 中进行算术运算的电路,那么为了承诺于 Fp 上的多项式,我们需要一个阶为 p 的群 G。问题是如何表示群 G?为此,我必须引入一个有趣的概念,称为代数群

代数群

我们说群 G 是定义在域 Fq 上的代数群,如果实际上群 G 包含在 Fq^L 中。这意味着群 G 中的每个元素都表示为域 Fq 上的一个 L 元组。这个 L 元组表示群 G 中的一个元素。除此之外,群运算本身可以通过 Fq 上的多项式来计算。特别是,存在多项式 F1 到 FL,使得如果我给你群 G 中的两个元素 A 和 B(A 是 Fq 上的一个 L 元组,B 是另一个 L 元组),那么 A + B 的和可以通过将多项式 F1 到 FL 应用于点 (A, B) 来计算。因此,群运算本身可以通过简单地应用定义在域 Fq 上的多项式来计算。

此外,在任何代数群中,有许多 L 元组表示群中的相同元素。因此,我们还需要一个算法,给定群中的两个 L 元组,测试它们是否实际上对应于群中的相同元素。这是一个更机械的事情。更重要的是要理解,群的阶是 p,它有 p 个元素,但它是定义在 Fq 上的,意味着元素是 Fq 上的 L 元组,加法是使用定义在 Fq 上的多项式实现的。

举个例子,如果 G 是定义在 Fq 上的椭圆曲线点群,那么这就是一个代数群的例子。群运算可以使用多项式计算,在这种情况下,每个元素是一个三元组,群运算可以使用三个多项式计算,输出两个给定点的和。这个群可能有某个阶 p,结果证明 p 将相对接近 q。

递归中的算术问题

现在,当我们在做递归证明时,我们面临的问题是什么?结果证明我们有一个算术问题。假设 G 又是一个定义在 Fq 上的阶为 p 的群。

如果群 G 的阶是 p,这意味着证明者可以支持为定义在 Fp 上的电路做证明。电路中的加法和乘法是定义在 Fp 上的。但是因为验证者需要在群 G 中验证多项式求值证明,它实际上需要在 Fq 中进行运算,因为该群中的群运算是使用模 q 的加法和乘法完成的。

同样,证明者将支持定义在 Fp 上的电路,但验证者需要在 Fq 中进行运算以验证证明。因此,证明者 P‘ 使用验证者 V 的电路进行证明,但存在不匹配,因为 P’ 支持在 Fp 中的运算,但验证电路使用在 Fq 中的运算。

问题是如何解决这个技术问题。结果证明有一堆解决方案,有些好,有些不太好。

解决方案一:域模拟

第一个想到的解决方案是所谓的域模拟。让我们将验证者需要的 Fq 中的算术实现为 Fp 上的一个电路。现在证明者支持 Fp 上的电路,这些电路实现了 Fq 上的算术。因此,现在证明者可以为验证者电路生成证明,因为现在验证者电路是作为 Fp 上的电路实现的。同样,验证者需要在 Fq 中进行的每一次加法和乘法都将以某种方式转化为 Fp 中的许多加法和乘法。

当然,问题是这会大大增加验证电路的大小,因为 Fq 中的每一个算术运算现在都变成了 Fp 中的许多算术运算,现在证明者真的非常慢。实际上,这在现实世界中会出现,例如,验证一个 KZG 求值证明需要做所谓的配对,而使用域模拟实现配对是巨大的。如果证明者必须实现一个验证 KZG 证明的电路,并且它使用域模拟来做到这一点,验证电路会变得相当大,结果证明者变得相当慢。

解决方案二:寻找匹配的群(不可行)

我们希望做得更好。一个更好的想法是:让我们找到一个代数群 G,它的阶是 p,并且它也定义在 Fp 上。这样做的好处是,现在证明者和验证者都将使用 Fp 上的算术。因为群的阶是 p,这意味着证明者可以有效地为定义在 Fp 上的电路生成证明。并且因为这个群定义在 Fp 上,这意味着验证者需要 Fp 上的运算来完成其验证工作。因此,验证者所做的和证明者支持的之间有一个非常好的匹配。

但我们没有这么幸运。不幸的是,宇宙就是不想让我们拥有这样的对象。可以证明,在这样的群中,离散对数问题总是非常容易。因此,一个定义在 Fp 上的阶为 p 的群总是会有一个简单的离散对数,因此不能用于多项式承诺。所以这行不通。

解决方案三:群链

还有其他我们可以使用的解决方案吗?结果证明答案是肯定的。接下来想到的是所谓的群链。让我们尝试使用一个群链,其想法如下:让我们找到群 G1 和 G2,使得 G1 的阶为 p,因此证明者可以处理定义在 Fp 上的电路,但该群定义在 Fq 上,因此验证者需要 Fq 中的运算。G2 的阶为 q,因此证明者可以处理 Fq 中的电路。你注意到这里有一个匹配,并且该群定义在 Fr 上。

这幅图景有趣的是,内层证明者可以处理 Fp 上的电路。验证者将需要在 Fq 中进行运算。但幸运的是,外层证明者现在可以相当廉价地处理 Fq 上的运算。因此,这里有一个很好的匹配,好事将会发生。

让我们看看,实际上我们如何使用链进行两层递归。这正是我们刚才所说的。内层证明系统在 G1 中使用多项式承诺,这意味着证明者 P 支持定义在 Fp 上的电路。电路中的加法和乘法是在 Fp 上的,但因为承诺在 G1 中,验证者将需要在 Fq 中进行算术运算以验证证明。

外层证明系统将在 G2 中使用多项式承诺。它使用 G2 中的多项式承诺这一事实意味着证明者 P‘ 将支持 Fq 上的电路。因此,需要在 Fq 中进行算术运算的验证者电路现在非常容易地被支持 Fq 上算术的证明者 P’ 所支持。因此,外层证明者为该验证者电路做证明是相当廉价的。

这是一个非常巧妙的想法,使我们能够比如果我们必须进行域模拟时更快地进行证明递归。当然,如果我们有一个更长的群链,我们可以支持更多层的递归。

解决方案四:群循环(更优)

结果证明,有一个比简单地使用链更好的想法。结果证明,我们实际上可以使用群循环。什么是群循环?这里的想法是找到群 G1 和 G2,使得 G1 的阶为 p 并且定义在 Fq 上,而 G2 的阶为 q 并且定义在 Fp 上。这个结构的有趣之处在于,它允许我们进行许多层递归,我们在 G1 和 G2 之间来回跳跃。

也许你脑子里不完全清楚我们如何在 G1 和 G2 之间来回跳跃。让我们用一幅图来说明。我们的第一个证明将证明它知道陈述 X 的见证 W。这里 C 是一个定义在 Fp 上的电路,使用 Fp 中的算术,这意味着证明者 P 需要一个大小为 p 的群。那正是 G1。但 G1 当然包含在 Fq 中,因此验证者将需要 Fq 中的运算来验证 π1。我们需要在 Fq 中进行运算。

好的,下一个证明者 P‘ 将使用群 G2。正如我们所说,电路使用 Fq 中的运算。幸运的是,群 G2 的大小为 q,因此得到了很好的支持,并且它包含在 Fp 中。因此,P’ 的验证者将进行模 p 的运算。所以我们需要一个阶为 p 的群来进行下一层递归,但幸运的是,我们可以直接跳回 G1。G1 的阶为 p,因此可以支持 v‘ 需要做的运算,这些运算是 Fp 中的运算。因此证明者 P 将使用包含在 Fq 中的 G1,依此类推。

因此,当证明者需要为 Fp 上的电路生成证明时,它使用 G1。然后下一个证明者将需要为 Fq 上的电路生成证明,它将使用 G2。下一个证明者将需要为 Fp 上的电路生成证明,因此它将再次使用 G1,我们在两个群之间来回跳跃。

我希望这很清楚。这是一个非常巧妙的想法。我链接了定义这个想法的原始论文,如果你想了解更多,可以看看这篇论文,之后也有许多其他论文探索了使用循环进行高效递归证明的想法。

循环的类型与 Pasta 曲线

结果证明,存在三种类型的长度为 2 的循环。首先,如果你记得对于 KZG 多项式承诺,我们需要使用所谓的配对群,即支持双线性配对的群。因此,首先想到的是,我们能否找到两个群 G1 和 G2 形成一个循环,并且它们恰好是一个配对群?是的,G1 和 G2 都支持配对。不幸的是,结果证明,这类群的最佳构造导致相对较大的群,因此不经常使用。

我们可以做的另一件事是找到一个循环,其中 G1 是一个配对群(因此我们可以使用 KZG),但 G2 是一个常规群(因此它不支持配对)。这意味着如果我们想在 G2 上做多项式承诺方案,我们必须使用一个无配对的多项式承诺方案。例如,我们在之前的讲座中讨论了 Bulletproofs,还有其他不需要配对的多项式承诺方案。问题是它们的求值证明实际上相当大,因此使用 G2 的证明者最终会得到相当大的证明,但使用 G1 的证明者将拥有非常短的证明,因为 KZG 求值证明实际上非常短。因此,当我们在 G1 和 G2 之间来回跳跃时,我们只需要确保递归的最后一步以 G1 结束,这样最终证明就很短。

最后,还有另一种方法,其中群 G1 和 G2 都不是配对群,它们形成一个循环,但两者都不是配对群。在这种情况下,我们必须在这两个群中都使用非配对多项式承诺方案。我们可能最终会得到稍大的证明,但也不是太糟糕。结果证明,对 G1 和 G2 都使用非配对群还有其他一些好处。实际上,有一对非常著名的曲线,称为 Pasta 曲线(Pallas 和 Vesta)。这对 Pasta 曲线 G1 和 G2 正是为递归 SNARK 设计的。

如何构造 Pasta 曲线?

结果证明,有一个构建此类曲线的一般理论。让我们看看它是如何工作的。正如我们所说,基本上有一个非常大的这类两个循环的家族,其中没有一个支持配对。

我必须假设一点椭圆曲线的知识。如果你对此不熟悉,可以跳过这一部分,它与我们以后要做的任何事情无关。

让我们看一个由这种特定形式指定的椭圆曲线:y^2 = x^3 + D,其中 D 是某个常数。结果证明,当我们看这种特定形式的曲线时,对于许多素数 q,如果让 p 是定义在 Fq 上的曲线点的数量,并且这个数 p 恰好是一个素数,那么有一个定理(归功于 Silverman 和 Stange)表明,定义在 Fp 上的曲线 E 的点的数量等于 q,而定义在 Fq 上的曲线 E 的点的数量等于 p。

因此,这一条椭圆曲线实际上给了我们两个群,一个定义在 Fp 上,一个定义在 Fq 上,并且它们形成了一个循环。Pasta 使用了这种曲线的一个特定形式,特别是当 D = 5 时,对于特定的素数 p 和 q,结果证明这给了我们两条形成循环的曲线,并且它们都非常适合递归。这些 Pasta 曲线实际上是为 Halo 2 证明系统开发的,只要我们在使用不需要配对的多项式承诺方案,它们就可以非常方便地用于递归。

更高效的递归:陈述折叠与 Nova

在最后一个部分,我想向你展示一些使用称为陈述折叠的技术进行非常高效递归的优雅想法。这将自然地引出像 Nova、SuperNova 和其他一些系统。

首先,为什么我们需要更好的递归技术?如果你想想我们在讲座前几部分看到的证明递归,在这些方案中,证明者 P 必须为一个电路构建证明,该电路内部实际上包含了整个证明系统的验证算法。证明系统的验证算法需要验证多项式承诺方案的求值证明。结果证明,在算术电路内部验证这些求值证明可能相当昂贵。

第一个想法,实现 Halo 系统基本上允许我们将所有与多项式承诺方案相关的求值证明验证从证明者必须构建证明的电路 C 中移出。因此,在 Halo 中,验证算法的一大块是在证明者必须构建证明的电路 C 之外完成的。

Nova 将这一点推向了下一步,它进一步简化了证明者必须构建证明的那部分验证算法。结果,实际上,当我们进行递归时,证明者实际上不必为整个验证电路构建证明,它只需要为非常少量的检查构建证明,这正是我们将要看到的。因此,这种 Nova 技术使我们能够比以前更快地构建证明递归。

折叠方案

这种魔法是通过折叠方案的概念完成的。折叠方案的想法是将两个有效实例压缩成一个。让我们看看这意味着什么。

像往常一样,固定一个电路 C。我们将为电路 C 构建一个折叠方案。那么,什么是 C 的折叠方案?基本上,它是两个参与方之间的协议,我们称之为折叠证明者和折叠

011:从实践到理论

概述

在本节课中,我们将探讨密码学证明系统的一些理论层面。这包括一些源自上世纪八九十年代该领域奠基时期的重要思想,以及一些近几年的最新研究成果。我们将从交互式零知识协议的理论问题入手,深入分析Fiat-Shamir变换的理论基础及其在随机预言机模型中的局限性,最后探讨如何基于可证伪的密码学假设构建安全的证明系统。

密码学证明系统概述

密码学证明系统是理论和应用密码学数十年来最重要的基石之一。

在实践层面,你们在本课程中主要学习的是SNARKs,由于其在区块链领域的潜在应用,目前具有很高的实践相关性。但在区块链出现之前,证明系统对于实际的互联网安全(例如整个身份验证或数字签名的概念)仍然至关重要,本质上就是一种证明系统。因此,互联网上的任何安全通信都依赖于它。

在理论层面,证明系统,尤其是零知识证明系统,一直是密码学构造中的“瑞士军刀”。其中最突出的例子是安全多方计算。这是一种密码学原语,旨在使一群互不信任的参与方能够基于各自的私有输入联合计算任意函数。零知识证明可以将一个半诚实的协议(即仅在假设所有参与者都严格按照协议规定执行时才安全的协议)编译成一个完全安全的协议,其中不假设对手可能偏离协议。零知识证明以一种非常直接的方式被使用:让每一方证明他们正在按照协议规定行事。

更广泛地说,零知识证明是任何希望强制执行特定行为、同时又要保守秘密的场景中的关键工具,这在密码学的各种情境中都会出现。

理论研究的范畴

关于密码学证明系统的理论研究通常涉及三个相互重叠的范畴。

首先,大量思考集中在可行性问题上:具有X、Y、Z等属性的协议在原则上是否存在?这方面最突出的例子当然是SNARKs。直到今天,我们仍在试图理解关于SNARKs在数学上是否存在,我们能说些什么。在本讲座中,我将深入探讨相关问题。但除了SNARKs,还有大量其他类型的协议,例如满足零知识或较弱隐藏属性(如见证不可区分性、见证隐藏性)的非交互式协议。你可以列出希望协议具备的任何一组优良属性,这些属性可能受到某些应用的启发,然后从数学上提问:这样的证明系统是否存在?这个问题通常非常复杂,并且可能取决于你考虑的敌手模型。例如,如果你考虑一个可能同时开启多个协议并发执行的敌手,并试图联合利用这些执行来破坏安全性,问题会变得更难。如果敌手能够访问量子设备,情况也可能更加困难,例如,协议是否对量子攻击保持零知识就很不明确。因此,会出现许多可行性问题。

其次,我们希望尽可能最小化构造中所做的假设。让我举几个具体例子。在非交互式协议中,你通常有某种可信设置:要么假设存在一个由第三方建立的结构化参考字符串,证明者和验证者可以访问;要么假设他们可以访问一串随机硬币(这是一个较弱的假设);要么什么都不假设,这称为“明文模型”,即没有设置。不同类型协议的可行性当然可能取决于你假设了哪种设置。我们希望为所考虑的任何协议,尽可能使用最弱的设置。

此外,我们希望基于(最好是简单、经过充分研究的)“可证伪”假设,为我们的协议提供安全性证明或安全性归约。我将在讲座中间部分更详细地讨论这一点。

最后,尤其是在可行性问题得到解决之后,我们也会考虑提高各种协议效率。有许多维度你可能希望改进效率:在交互式设置中,你可能希望最小化通信轮数(有时非交互式不可能,但你仍然希望最小化通信轮数);你可能希望最小化通信量,即证明者和验证者之间发送的比特数;你可能希望优化证明者和验证者的计算效率。最后一点,尤其是证明者的计算效率,是你们在课程中到目前为止听到的很多内容。但幻灯片上列出的所有想法,更广泛地代表了理论研究者经常思考的问题。

当然,除此之外,我们还思考如何将各种密码学证明系统应用于严格意义上不属于密码学证明领域的其他应用。

交互式零知识协议的理论问题

前面的讨论有些抽象,现在让我们深入探讨一个具体的理论问题例子,即讨论交互式零知识协议时出现的问题,这在Shafi的第一讲中已经介绍过。

当我们讨论交互式零知识协议时,我们处于一个没有可信设置的环境中。证明者和验证者可以互相发送消息,但没有可信第三方,也没有他们可以访问的公共随机字符串。他们发送的就是他们能看到的全部信息。

在这种设置下,零知识比在有可信设置的非交互式设置中要棘手得多。原因在于,你希望你的协议能够抵抗任意的多项式时间验证者,这些验证者可能遵循也可能不遵循协议规范。在非交互式设置中,验证者没有作弊的协议可循,他们只是收到一条消息。但现在,他们可能发送本不应该发送的消息,这可能会破坏安全性,因此你的分析需要排除这种可能性。

尽管如此,我们在第一讲中看到,可以为任何NP语言构建一个零知识证明系统。你们看到的基础协议在三轮消息中具有某种逆多项式级别的可靠性误差。这很好,但并不是我们真正想要的。我们希望一个协议中,证明者只能以某种指数级小的概率欺骗验证者,因此我们需要考虑如何减少该协议中的误差。

有几种方法可以做到这一点。一种方法是顺序地重复该协议多次,你可以证明这是有效的,这将降低可靠性误差但保持零知识。然而,这会导致一个具有数万轮通信的协议,这是任何人都不愿意运行的。

另一种你可以尝试的方法是并行重复该协议多次,这是一种非常常见的做法。你可以证明,正如预期的那样,这降低了可靠性误差,至少在这种特定情况下是如此,而且对于任何统计可靠的协议来说都是如此。但事实证明,这种并行重复变换实际上并不能保持协议的零知识性,因此在交互式设置中你必须更加小心。

让我们看看问题出在哪里。让我们回顾一下第一讲中的协议。

你们看到的其中一个协议是,证明者希望说服验证者某个公开已知的图是三着色的。如果证明者知道该图的一种三着色方案,协议的工作方式如下:

首先,证明者获取其对图的着色并进行随机化,为每种颜色分配一个随机的0、1或2。
然后,证明者对这些随机化的颜色进行承诺。因此,对于图中的每个顶点,证明者承诺一个数字0、1或2,并将所有这些承诺发送给验证者。这些承诺将证明者绑定到着色分配上,但此时验证者并不知道颜色是什么。
这些消息发送后,验证者从图中采样一条随机边(我们称之为挑战)并将其发送给证明者。
然后,要求证明者打开这条边上两个顶点的颜色。如果证明者确实承诺了一个三着色,那么验证者将看到两种不同的颜色,并且如果确实看到两种不同的颜色,验证者将接受。
这是可靠的,因为证明者在边被采样之前就已经对着色进行了承诺。因此,无论这个着色是什么,如果它不是有效的三着色,验证者至少有逆多项式的机会采样到一条证明者无法打开并给出可接受响应的边。
另一方面,它在直觉上是零知识的,因为对于验证者可能询问的任何固定边,证明者将打开两个随机的不相等颜色,验证者似乎并没有从看到两个随机的不相等颜色中学到任何东西。

为了形式化这一点,我们转向模拟范式,这在第一讲中也介绍过。快速回顾一下,以下是你如何为这个协议编写零知识模拟器。

我们从一个任意的验证者V*开始,它可能不按规范行为。它可能不采样随机边,而是精心挑选一条特定的边,希望了解一些信息。这条边可能是证明者第一条消息的函数。尽管如此,我们希望证明我们可以在不知道图的三着色方案的情况下模拟验证者的视图。

我们这样做的方式如下:我们反复猜测一条随机边。然后,我们承诺一种尊重这条特定边的着色,即我们确保这条边的两个顶点被分配了一对随机的不相等颜色。例如,你可以承诺其他地方全为零。然后你发送这些承诺,接着运行验证者,验证者可能恶意地选择某条边。如果验证者发送的边等于你的猜测,那么你可以发送打开信息,这看起来完全像验证者在真实协议中会看到的那样。如果你的猜测错误,那么我们回滚验证者,回滚整个执行,重新开始,提出新的猜测、新的承诺等。

由于承诺是隐藏的,验证者不知道我们猜测了哪条边,而我们的猜测实际上具有逆多项式的正确概率。因此,经过多项式次数的尝试后,我们将成功创建一个看起来像验证者在协议真实执行中会看到的记录。

这就是你证明该协议是零知识的方式。

如果我们并行重复这个协议多次会发生什么?让我们看看哪里出了问题。

假设我们有T次该协议的重复,并且我们认为T相当大。具体来说,它至少需要达到安全参数的数量级,以使可靠性误差变小。但是,如果有大量重复,那么我们刚刚描述的模拟策略就不再有效了,它不是高效的。原因在于,验证者可能发送的消息至少有2T种。如果你随机猜测其中之一,那么它只会在概率为2的情况下匹配验证者发送的内容,因此你需要重复2^T次才能成功模拟。这样模拟器将无法在多项式时间内运行,因此该协议不满足零知识属性,至少我们还没有证明它满足。

这是对我们讨论的模拟策略为何不适用于并行重复协议的解释。但事实证明,从道义上讲,该协议不应被视为零知识的。Dwork、Naor、Reingold和Stockmeyer在1999年的一个结果表明了这种有趣的“两难”局面:对于这个并行重复协议,你可能期望两件事为真:一是它可能是零知识的,二是你可能能够为该协议实例化Fiat-Shamir变换。他们表明,这两者中至少有一个必须是假的。因此,要么你无法为该协议实例化Fiat-Shamir变换,要么如果你能为该协议实例化Fiat-Shamir变换,那么该交互式协议对恶意验证者而言就不是零知识的。我不会深入细节,但要点基本上是,你在这里无法模拟的恶意验证者是通过哈希证明者的消息来恶意生成其挑战的。

这里的启示是,我们不应该期望这个协议是零知识的。正确的直觉是Fiat-Shamir变换应该是可能的(我们将在本课程后面讨论),但因此,我们不认为这个协议是零知识的。

这是思考交互式零知识协议理论时出现的一个问题例子。并行重复不保持零知识这一事实引发了许多由此产生的问题,因此有许多研究方向致力于规避这个问题。

例如,由于这个问题,尚不清楚为了以零知识证明某事,你需要多少轮通信。即,一个实际达到小可靠性误差的零知识协议的最小通信轮数是多少?Bansal、Kalai和Paneth最近的一项工作给出了一个仅需三轮通信的构造,我们知道这必须是最优的(因为少于三轮是不可能的),他们给出了一个三轮消息协议的候选构造。

以下是你可以提出的其他一些问题:暂且不论通信轮数,零知识协议中证明者的效率能有多高?因此,有许多研究工作致力于编写比刚才给出的三着色零知识协议具有更好证明者效率的协议。特别是,如果你想证明某个其他陈述,而该陈述本身不具有三着色图的结构,那么为了使用我们刚刚讨论的协议,你必须将你关心的问题归约为三着色问题,而这种归约会带来巨大的开销,以至于在计算完这种归约后,你绝不会想实际运行三着色协议。另一方面,确实存在其他零知识协议的构造,对证明者来说效率要高得多,这些可能具有实际相关性。其中一些与本课程中学到的一些协议(如Bulletproofs)相关。

最后,正如我们在引言中已经提到的,除了我们目前讨论的定义之外,你可能还希望零知识协议具有更强的安全形式。例如,你可能希望协议对量子攻击是零知识的,我们实际上还没有论证这一点,因为我们描述的“回滚”过程由于一些微妙的原因在验证者是量子时不适用。类似地,我们讨论的回滚过程在敌手开启了零知识协议的多个并发执行并试图从这些执行的联合视图中学习信息的设置中也不适用。因此,关于如何在这些设置中编写协议并证明其安全性,有许多思考和技术。

转向Fiat-Shamir变换与SNARKs的理论

刚才我们简要探讨了交互式零知识协议的世界,但在本讲座的剩余部分,我想讨论Fiat-Shamir变换的理论方面以及简洁非交互式论证的存在性问题。

首先快速回顾一下简洁非交互式论证的定义。我们有一个NP语言,一个由字符串X表示的声明,诚实的证明者将拥有一个见证W,用于证明陈述X为真。证明者和验证者共享一个公共参考字符串,它可能是一些随机硬币,或者是通过可信设置生成的。然后,证明者使用其可能很长的见证W生成一个简短、易于验证的证明π,发送给验证者,验证者可以用它来检查X是否为真。只要证明者确实拥有X的见证W,验证者几乎总是应该接受。另一方面,如果陈述X实际上是假的,那么对于任何高效的策略P*,无论该策略生成什么证明,都应该使验证者以很高的概率拒绝。最后,正如我所说,我们希望证明是简短的,所以我们认为其长度是陈述和见证长度的多对数级别,并且验证应该非常快,大约是这个时间量级,并对输入有一些读取访问。

这就是我们想要的。我们已经在本课程中看到了使用交互式预言证明和随机预言机构造SNARKs的方法。还有另一个使用线性PCPs的构造家族,你们也简要看到过,但今天我们将重点讨论基于随机预言的构造。

使用随机预言将IOP转换为SNARK意味着什么?这是通过Fiat-Shamir变换完成的,你们也已经学过,但快速回顾一下:这是一个强大且非常通用的提议,用于从交互式协议中移除交互性。只要交互式协议中的验证者消息是真正的随机字符串,你就可以用哈希函数H替换与验证者的交互,证明者通过哈希自己的消息或哈希记录前缀来生成下一个验证者消息。屏幕上显示的是三轮消息的情况,所以你可以认为验证者挑战是对第一条消息α(可能连同陈述X,这里没有写出)的哈希。

事实证明,如果你将这个哈希函数建模为随机预言,那么这个变换将安全地编译任何常数轮的公开掷币交互式协议。实际上,它能编译比这更多的协议,但为了讨论简单,我们现在只考虑常数轮协议。

我认为我们实际上还没有在课程中看到这个的证明。所以我要做的第一件事是概述一个证明,即在随机预言模型中,这个编译器是有效的。

现在我们应该更具体一些。将H建模为随机预言意味着什么?粗略地说,随机预言模型是关于哈希函数攻击结构的一个假设。原则上,对于Fiat-Shamir协议,证明者被给予哈希函数的描述,并且它可以以任何它喜欢的方式使用这个哈希函数描述来尝试生成其证明。但随机预言模型假设,敌手策略所能做的最好的事情就是将哈希函数描述视为一个黑盒,一个接收消息α并输出消息β的盒子,并且该策略只会将哈希函数作为预言机进行查询,而不会使用哈希函数的实现细节。因此,在Fiat-Shamir的情况下,随机预言模型是一类试图通过在假陈述上查询哈希函数多次来生成可接受证明的攻击,而不实际查看哈希函数的实现细节。

粗略地说,如果你只将哈希函数视为黑盒,那么将其视为一个均匀随机函数是合理的。例如,哈希函数可以是一个伪随机函数,那么如果你只查询它,它就与真正的随机函数不可区分。

因此,非常具体的说法是,Fiat-Shamir在随机预言模型中有效,这意味着如果你只查询哈希函数而不查看其实现,那么非交互式协议的最佳证明者策略不会比交互式协议的最佳证明者策略好太多,并且存在一个随轮数增长的安全损失。对于现在的证明,我们将重点关注三轮消息的情况。

我们需要证明的是,任何非交互式协议的成功证明者都可以转换为交互式协议的成功证明者。因此,我们从这个证明者P*开始,它查询哈希函数多次,最终生成一个记录α, β, γ发送给验证者。我们想说,我们可以在交互式设置中模拟这一点。

这里安全证明的关键思想是,由P生成的第一条消息α本质上必须来自P对哈希函数H所做的一次查询。事实上,这可以在不失一般性的情况下假设,如果你真的需要,你总是可以假设每当证明者要输出某些东西时,首先查询哈希函数消息α。但现在的要点是,我们假设证明者只对哈希函数进行多项式次数的查询,因为它运行在多项式时间内,或者更一般地说,它运行在某个有界的时间内,因此它对哈希函数的查询次数是有界的。

我们可以利用这一点来实际攻破三轮消息协议。想法如下:如果我们想在交互式协议中欺骗验证者,我们有一个在非交互式设置中欺骗验证者的随机预言策略。如果证明者对哈希函数进行了Q次查询,那么本质上在Q个位置中有一个位置,它正在为交互式协议在假陈述上生成一个可接受的记录。我们攻破交互式协议的方式是:我们进行猜测,猜测哪一次查询将是成功的那一次。然后,在交互式协议中,我们将通过运行左侧策略并自己进行I-1次哈希函数查询来生成第一条消息α(其中I是我们猜测的将起作用的查询索引)。然后,我们将第I次查询本应发送的内容发送给验证者,我们不对那个α查询哈希函数,只是发送给验证者。然后验证者发回一个随机字符串β,我们基本上假装这是哈希函数的输出。然后,为了生成我们的最后一条消息γ,我们继续运行左侧策略,直到它最终输出一个记录,我们选择Fiat-Shamir证明者选择的任何γ并输出它。

基本上,这将以概率1/Q乘以非交互式证明者获胜的概率攻破交互式协议。因为如果我们碰巧猜对了非交互式证明者输出的α对应的查询,那么如果我们的猜测正确,我们就在交互式设置中正确地模拟了随机预言攻击。因此,这表明你的安全性只比交互式协议的安全性差1/Q倍。对于一个2R轮的协议,一般来说你的安全损失大约增长为1/Q^R,尽管对于满足更细粒度可靠性概念(如逐轮可靠性)的协议,你可以做得更好。

这就是Fiat-Shamir在随机预言模型中的合理假设下有效的证明。但现在让我们重新审视随机预言模型所做的这个假设。正如我们之前所说,该模型假设,尽管你被给予了某个哈希函数H的描述,但为了破坏Fiat-Shamir协议的目的,攻击所能做的最好的事情就是将该哈希函数视为黑盒,向其插入一堆输入,获取一些输出,并利用这些来尝试破坏安全性。

实际上,哈希函数并不是你可以作为黑盒调用的随机函数,而是像SHA-256这样的东西,可能加盐了一些身份、时间戳和随机字符串等,但它是像SHA-256这样的东西。所以这就是实践中的情况。但即使在理论上,无论怎样,你都将用某个高效可计算的公共算法来实例化这个哈希函数,因为它需要是证明者和验证者都能在合理时间内实际计算的东西。

因此,模型将H视为随机函数与现实(即它是高效可计算的算法)之间存在不匹配。这是一个明显的理论问题:高效的公共算法无法计算随机函数。

尽管如此,你可能仍然希望,对于你实际关心的任何安全属性,如果你能证明该安全属性在随机预言模型中成立,那么一个合理的哈希函数实例化也将满足该安全属性。这就是随机预言启发式背后的逻辑。当然,你不仅仅要希望它有效,你还可以尝试攻击你实现的任何特定协议。因此,该启发式得到了Fiat-Shamir范式在实践中尚未被真正攻破这一事实的支持。但理论研究者正试图更原则性地理解这种变换何时以及为何实际有效。

我要向你们展示的第一件事是一个负面结果:我们将看到随机预言具有的一个安全属性,该属性在随机预言模型中成立,但可证明地无法对任何高效可计算的哈希函数族成立。这是对随机预言启发式的一个违反。

这里有一族非常重要的安全属性。对于每个将字符串映射到固定长度(比如安全参数长度)字符串的函数F,都有一个安全属性。我们说,一个哈希函数或哈希函数族对于这个函数F是“关联不可行”的,如果给定哈希函数H的描述,很难找到一个哈希函数的输入x,使得hash(x)等于F(x)。

这是一个非常广泛的安全属性。你可以在这里插入任何你喜欢的函数F,你会得到一个不同的安全属性。但很容易看出,对于任何固定的函数F,你在随机预言模型中得到F-关联不可行性。也就是说,一个随机预言将满足这个安全属性。原因很简单:在随机预言模型中,攻击者只是对哈希函数进行多项式次数的查询,并且对于每次对哈希函数的查询x,你会得到一个随机输出字符串y。每次你这样做时,y等于f(x)的概率是2^{-λ}(λ是安全参数),这个概率非常小。因此,即使你这样做多项式次,你也极不可能找到一个x使得hash(x)等于f(x)。这对每个固定的函数F都成立。

因此,这是一个随机预言具有的非常合理的安全属性,所以你可能会期望,如果你用SHA或某个合理的哈希函数实例化随机预言,你也会得到这个安全属性。但事实证明,情况并非如此。存在一个特定的函数F,使得对于任何高效可计算的哈希函数族H,H对于这个函数F不是关联不可行的。

因此,随机预言启发式对于这种关联不可行性的概念完全失败。

如果你以前没见过这个,现在是个好时机暂停一下,思考一下你是否能想出一个函数F来证明这个主张。你能想出一个坏的F,使得对F的关联不可行性是不可能的吗?这是理论计算机科学中一个非常普遍的思想的例子,称为“对角化”。

我给你一点时间思考一下你是否能想出一个F。

这里有一个函数F,它展示了不可能性。F将任意字符串x作为输入,它会将这个字符串x解释为一个程序P的描述(你总是可以在字符串和程序之间建立某种映射,具体是什么并不重要)。然后,F(x)等于P(x)。也就是说,你运行x描述的程序,输入是x本身。

这看起来有点奇怪,这是合理的,因为它展示了这个随机预言不可能性。以下是你为什么不能对实际的哈希函数族拥有对这个F的关联不可行性的原因。

取任何哈希函数族H,我们可以写下一个非常简单的攻击来打破对这个F的关联不可行性。给定这个族中的一个哈希函数h,我们想找到一个输入x,使得hash(x)等于F(x)。我们要选择的输入x就是h本身的描述。我们拿到一个哈希函数,然后将其描述插入到它自身中。你可以看到,根据定义,对于这个特定的x选择,hash(x)将等于F(x),因为F(x)做了什么?F(x)将x插入到x描述的程序中。x是h的描述,因此,F(x)等于h(x)。这样我们就打破了关联不可行性,仅仅给定h的描述,我们就能高效地找到一个输入,使得hash(x)总是等于F(x),而F是某个固定函数。

随机预言不具有这个属性,所以这是一个对随机预言成立但对任何实际哈希函数族都不成立的性质。是的,这就是反例。写下来很简单,但要真正理解它可能需要相当长的时间,所以请务必花些时间思考这里到底发生了什么。

让我们再多讨论一下这个反例。比如,它有多合理?它应该如何让你思考随机预言的可实例化性?

这个反例在这里要求一个相当人为的安全属性。特别是,这只是对需要能够接受任意长输入的哈希函数的随机预言启发式的反例。具体来说,你的哈希函数需要能够哈希至少与哈希函数本身描述一样长的输入。因此,这个反例并不排除对有界输入长度的函数的关联不可行性,这一点非常重要,并且将在讲座末尾出现,因为我们实际上要构建满足较弱形式关联不可行性的哈希函数,它们确实存在。

另一方面,实际上还有其他对随机预言成立、但可证明地对任何仅涉及固定长度输入的哈希函数族都不成立的哈希函数安全属性。这不再关乎关联不可行性,但对于固定长度哈希函数,存在其他安全属性的反例。事实上,这些反例是针对Fiat-Shamir启发式的。Barak和Goldwasser、Kalai等人有两个这样的结果,他们构建了交互式协议,其中Fiat-Shamir启发式在随机预言模型中适用于这些协议(特别是常数轮协议),因此我们知道Fiat-Shamir应该有效。但尽管如此,他们表明,对于任何高效可计算的哈希函数族H,使用H的Fiat-Shamir协议是不安全的。因此,对于任何特定的H,都存在攻击,但对于随机预言,则没有攻击。

这是一个非常直接的反例,表明尽管我们有Fiat-Shamir的随机预言安全性证明,但在实例化时实际上必须小心。有些协议无论如何都不会有效。

因此,理论研究者正试图真正理解随机预言启发式何时实际适用,何时我们可以将其视为证据,表明当你在现实世界中实例化时,所得协议实际上是安全的。

关于负面结果,我们再注意几点。首先,我们在刚刚看到的攻击中,通过在其自身描述上运行哈希函数来破坏安全属性。所以你可以问:这在实践中有相关性吗?你会在其自身描述上运行哈希函数吗?如果你的协议从不关心这样的事情,那么你也许可以说这个反例与实践无关。你有时可以这么说,但有趣的是,通过递归组合构建的SNARKs在某种意义上确实在其自身描述上调用哈希函数,虽然不是完全像我们在攻击中写的那样,但至少表面上类似。这是因为当你进行递归SNARK组合时,你所做的是给出一个SNARK证明,证明SNARK验证者本应接受某个其他SNARK证明,因此你正在证明关于SNARK验证者的事情,而SNARK验证者正在运行哈希函数。因此,你想要给出一个关于哈希函数的SNARK。

需要明确的是,这绝不是说实践中的递归SNARKs被攻破了,我们实际上没有攻击。但这确实表明,在安全和不安全的实例化之间存在一条难以描述的界限,作为理论研究者,我们仍在试图理解这条界限到底是什么,以解释为什么实践中的构造是安全的,尽管理论上有这些奇怪的反例。

无随机预言的安全证明系统

既然我们已经看到了随机预言模型的一些严重理论问题,我将在讲座的剩余部分告诉你们一些我们可以在没有随机预言的情况下做的事情,即一些使用显式哈希函数族且完全不依赖随机预言模型的安全证明系统的构造。

那么,当我们没有随机预言时,我们做什么?我们基于什么来保证安全?理论研究者喜欢做的是利用所谓的“可证伪假设”。我们希望构建一个证明系统或可能其他密码学原语,并证明该原语或协议是安全的,假设某个具体的算法任务在多项式时间内或某个不太大的时间内是不可行的。

可证伪假设的一些例子是:在某些群上计算离散对数是困难的;或者,我们可以假设求解随机噪声线性方程是困难的,这称为“带错误学习”问题,这是在模p整数环上的噪声线性方程。这是理论密码学中两个非常常见的假设。但你也可以做出像SHA-256是抗碰撞的这样的假设。所以SHA-256本身并不是一个不可证伪的对象,不可证伪的是随机预言模型。你仍然可以采用像SHA这样的具体实例,并假设它是抗碰撞的,这是一个合理的可证伪假设,我们有相当多的信心。

这些是可证伪假设的例子。密码学中通常的工作方式是,我们基于可证伪假设构建一些密码学原语或协议,但构造效率非常低。因此,提高构造效率的一种方法通常是修改构造并利用随机预言,因为随机预言可以免费为你解决许多技术问题,它们通常可以简化密码学构造,代价是这种建模假设。

例如,幻灯片上列出了一些例子。但特别是对于非交互式零知识协议,暂且不考虑简洁性,假设你只想要非交互式零知识,我们既有基于Fiat-Shamir和随机预言模型的构造,也有基于可证伪假设的构造。拥有基于可证伪假设的构造的一个优势是,即使我们最终在现实世界中使用基于随机预言的协议,基于可证伪性的构造也是另一个证据,表明用SHA-256实例化的随机预言构造是合理的做法。

我们目前的目标是理解基于可证伪密码学假设可以构建哪些类型的协议。

我们从带K的SNARKs开始,这里我们问是否可以在可证伪假设的基础上构建简洁的非交互式知识论证。这里的答案很快就是:不能。许多不可能性结果都有附带条件,但这里的直觉应该是:不,你不应该期望能够做到这一点。

问题出在哪里?问题在于,知识论证要求你能够从任何成功的证明者中提取一个NP见证。但证明者只输出一个简短的证明字符串。因此,如何从一个简短的证明字符串中提取一个长的NP见证,这真的很不清楚。我们已有的构造实现这一点的方式是做出一个实际上只是假设掉这个问题的假设。该假设说,哦,神奇地存在某种方法可以从一个短字符串中提取一个长字符串。

随机预言给了我们这一点,因为在随机预言模型中,你可以考虑敌手对预言机进行的所有函数调用的历史,这可以给你一个长字符串,即使证明本身很短。基于线性PCPs的构造不使用随机预言,但它们转而基于某个密码群做出知识假设,同样,这个假设为我们解决了这个问题,它假设即使从实际上只明确告诉你少量信息的敌手那里,你也能神奇地提取大量信息。

这不是我们期望能用可证伪假设解决的问题,因此,如果你想要具有知识可靠性的SNARKs,似乎你需要超越理论密码学家通常处理的范围。

然而,对于带G的SNARGs(或仅满足可靠性而不一定满足知识可靠性的协议),情况就不同了。这里并没有那么明确的理由说明为什么你不能基于可证伪假设构建这些,尽管我们目前拥有的唯一通用构造使用了像随机预言这样的不可证伪假设。

因此,我们目前不知道这个问题的答案,即可证伪假设是否足够。存在一些障碍,一些理论论文讨论了为什么这可能很困难,但我会说证据远不如SNARK情况那么明确,陪审团可能仍在审议这是否应该可能。例如,我个人很多研究都在试图更好地理解这个问题。

但最先进的技术是我们不知道如何做到这一点,因此我们将满足于为计算子类构建SNARGs。我们不会为任何NP陈述构建SNARGs,而只是为一些仍然相当具有表达力的特殊NP陈述构建它们。

在讲座的剩余部分,我将概述一个基于带错误学习假设的构造,尽管假设的细节并不那么重要,但这就是我们今天将要看到的内容。

构建工具:关联不可行哈希与某处可提取承诺

在进入构造之前,让我先介绍构造中将要用到的两个主要工具和技术。

第一个是关联不可行性,我们今天早些时候已经看到。我们将使用满足某些形式关联不可行性的哈希函数,而不是在Fiat-Shamir范式中使用基于随机预言的哈希函数,这些形式被证明足以用于Fiat-Shamir,至少对于某些特定的交互式协议(而非一般情况)是如此。这是第一个工具。

第二个工具是称为“某处可提取承诺”的东西,这是一种特殊的简洁承诺方案。我们在本课程中已经见过简洁承诺,如Merkle树。但某处可提取承诺是简洁承诺方案的一种更强形式,它用于构建一个特殊的交互式协议,我们基于这些关联不可行哈希函数为该协议实例化Fiat-Shamir变换。最终构建的交互式协议看起来很像典型的基于IOP的交互式协议,但仅适用于特殊语言,并附带安全性证明。

让我们先回到关联不可行性。正如我们之前所说,给定一个函数F,如果很难找到一个输入x使得hash(x)等于f(x),则哈希函数族对F是关联不可行的。

事实证明,你可以将其视为更强形式的关联不可行性的特例,不是针对函数,而是针对二元关系。对于每个二元关系R,你可以说哈希函数对R是关联不可行的,如果很难找到一个输入输出对(x, hash(x))满足该关系。只要该关系没有太多的点对,那么随机预言将满足这个安全概念,你可以寻找满足这个性质的显式哈希函数。尽管再次强调,我们知道存在问题。

所以让我们回到函数版本。我在讲座早些时候花了一些时间告诉你们,这个性质实际上是无法实例化的。存在一些函数,你无法拥有满足该性质的哈希函数(没有高效的)。我们通过限制哈希函数的输入长度来规避这个问题。我们将考虑具有固定输入长度的哈希函数的这个性质,并且哈希函数的描述必须略大于这个输入长度。我们知道这个限制是必要的,否则该对象无法实例化。

然后我们将进一步限制这个概念,我们只考虑那些在某个先验有界多项式时间T内可计算的函数F。我们的目标是构建一个哈希函数,它对所有在时间T内可计算的函数F都是关联不可行的。哈希函数本身将在略大于T的时间内可计算。这就是我们目前知道如何构建的。尚不清楚这个限制是否必要,但这是目前我们知道如何处理的情况。

接下来,我将实际展示一个基于可证伪假设的此类哈希函数族的构造,我们这样做的主要工具是所谓的“全同态加密”。

这是一种加密方案,你可以加密一些数据x,并且在密文(x的加密)上,可以评估你选择的任何高效可计算函数F,并获得F(x)的加密。你可以在没有密钥、不知道x是什么的情况下做到这一点,只需给定x的加密和函数,你就可以在幕后对其进行评估。同态加密方案的保证是,如果你解密评估后的密文,你将得到f(x)。这是一个非常强大的工具,不一定实用,尽管人们花了很多时间试图使其变得实用,但构建哈希函数实际上并不需要它。我将使用这个高功率工具,因为它给出了最简单或最容易解释的构造,但也有其他不使用全同态加密的构造。

尽管如此,让我们看看使用它的构造。给定一个全同态加密方案,我们将写下一个哈希函数族,然后论证它是关联不可行的。

哈希函数H的描述将是两样东西:首先是同态加密方案的一个公钥,然后是某个函数G的描述的加密。你应该把G看作某个函数,比如接收每个输入并立即输出零之类的。实际上这个函数是什么并不太重要。实际上,你可以认为我们的H的描述实际上是一个均匀随机字符串,但在我们脑海中,我们会认为它是一个公钥和一个函数G的加密。只要加密方案具有公钥和密文是伪随机的性质,那么随机字符串就与这样的哈希函数描述不可区分,因此我们可以等价地将其视为随机字符串或具有这种结构。

好的,这就是我们将如何思考哈希函数的描述。那么现在我们应该说明哈希函数实际上是什么,即给定输入x,你如何哈希它?你所做的是在G的加密上运行FHE评估算法,你使用的函数由x描述。所以你想做的是评估一个函数,该函数接收加密的G并计算G(x)。这有点像是颠倒了你通常思考输入和函数的方式,但这是你可以做的事情。是的,所以哈希密钥是G的加密。当你哈希x时,你将得到G(x)的加密。

尽管哈希x是一个公开高效可计算的操作。是的,这是一个你可以写下来的哈希函数。

那么,为什么我们会期望这个哈希函数满足任何类型的安全性质呢?原因是函数G是隐藏的。它隐含在H的描述中,但没有人真正知道它是什么。因此,你可以通过想象G是各种不同的函数来论证不同的安全性质,因为G的加密与某个其他函数G'的加密是不可区分的。你可以想象G是对你证明某个安全性质最方便的任何函数。

这有点抽象。让我们看看这个哈希函数是关联不可行的证明。我们将通过反证法来证明。假设对于某个先验固定的高效可计算函数F,敌手可以找到一个输入x,使得hash(x)等于f(x)。

那么,以下是论证安全性的关键思想。我们将选择一个非常特殊的函数G(x)。它将计算F(x),然后解密结果字符串。你把F(x)看作一个密文,解密它,现在你有一些明文,你可以认为明文是一个比特,然后在解密后加一。这是一个你可以定义的函数。正如我之前所说,只要我们开始的G的加密与G的加密在计算上不可区分,前提是加密方案是安全的。这里有一个小的注意事项,即我定义的G*实际上运行加密方案的解密算法,因此它依赖于加密方案的秘密密钥。但是,如果我们知道加密方案在存在秘密密钥加密的情况下是安全的,我们仍然可以论证这种不可区分性。这称为“循环安全性”,在全同态加密中经常出现,我们对此有合理的信心。但这个假设并不是真正必要的,你可以通过更复杂的构造来避免它。

但为了最简单的构造和证明,我们现在假设循环安全性。好的,我们从一个哈希函数开始,它由G的加密描述。但由于G的加密与G'的加密不可区分,我们知道我们的攻击者即使被给予G'的加密,仍然会破坏安全性。但现在我声称,当哈希函数是G*的加密时,攻击者实际上无法破坏安全性,这是一个矛盾。

原因如下:我们知道,如果你的哈希密钥中有G的加密,那么hash(x)将是G(x)的加密。那么我们可以看到,另一方面,hash(x)也等于f(x)(这是我们假设攻击者找到了这样的x)。那么我们会得到,如果hash(x)等于f(x),那么如果你解密F(x),这应该与解密hash(x)相同。但我们也知道hash(x)是G(x)的加密,所以解密hash(x)等于G(x)。我们已经定义G*总是避免函数解密F(x),它字面上是解密F(x)加一,所以这两个明文不可能相等。该方案被构造为它们永远不可能相等。因此,当哈希密钥以这种奇怪的依赖于F的方式采样时,攻击者实际上不可能破坏安全性。

但通过不可区分性,这也意味着当哈希密钥是随机字符串时,攻击者也无法做到这一点。

这是一个证明,假设你的加密方案足够安全,那么所得的哈希函数是关联不可行的。

这就是一个基于高效可计算函数的关联不可行哈希族的构造。但在过去五年左右的时间里,有许多工作致力于构建各种关联不可行哈希族,包括不仅仅是函数的关系。我将在幻灯片上列出一些。当你限制在函数时,我们有基于带错误学习假设的构造。LWE可用于构建全同态加密,但后续工作实际上能够避免我在上一张幻灯片的证明中所做的循环安全性假设,因此你可以真正基于标准的带错误学习困难性来构建它。

还有一个基于判定性Diffie-Hellman假设的构造,这更类似于你们在本课程中可能见过的一些基于群的密码学。LWE(格)和DDH(群)是理论密码学家使用的两个主要构建块和假设家族,对于这两者,我们现在知道如何利用它们来构建至少某些类型高效可计算函数的关联不可行性。基于DDH的构造实际上仅限于某些特定子类的函数,但它相当具有表达力。

然后,你可以问,好吧,那些不是函数的关系呢?尽管仍然不清楚我们为什么问这些问题,但你可以问那些不是函数的关系。我们一般不知道是否能为有界输入长度的关系构建关联不可行性,但我们确实对满足良好组合结构的关系有一个构造。基本上,如果你将哈希值视为多个块的连接,那么你可以保证对尊重块状结构的属性具有关联不可行性,这在实例化Fiat-Shamir时被证明是相关的。不要太担心这个,但我们可以处理某些具有多个输出的关系,但如何

012:zkEVM 的设计、优化及应用

概述

在本节课中,我们将要学习零知识以太坊虚拟机(zkEVM)的设计、优化及其应用。我们将从理解为什么需要zkEVM开始,逐步深入到如何从零构建一个zkEVM,包括电路化、证明系统选择等核心环节。接着,我们会探讨在构建过程中遇到的有趣研究问题,并最后了解zkEVM除了作为扩容方案之外的其他应用场景。


第一部分:背景与动机

上一节我们介绍了区块链和零知识证明的基本概念,本节中我们来看看为什么需要zkEVM,以及它为何在近年来变得如此流行。

区块链网络由众多节点组成,它们通过P2P网络互联,并维护一个共享的数据库状态。用户发送交易,节点执行交易并更新状态。这种设计虽然安全、去中心化,但效率低下且成本高昂,因为所有节点都需要重复执行相同的计算。

零知识卷叠(zkRollup)是一种扩容解决方案。其核心思想是将大量交易的计算转移到链下的Layer 2网络执行,然后生成一个简洁的零知识证明(Proof)提交到Layer 1。Layer 1只需验证这个证明,而无需重新执行所有交易,从而极大地提升了网络吞吐量。

然而,为特定应用构建零知识电路非常复杂,且不同应用之间的电路缺乏可组合性。为了解决这些问题,我们转向构建通用的zkEVM。zkEVM能够证明任意类型的EVM交易的有效性,开发者无需编写复杂的零知识电路,同时保证了应用间的原子可组合性。

尽管构建zkEVM极具挑战性,但近年来多项技术的进步使其成为可能:

  1. 多项式承诺 带来了更灵活的电路化方案(如查找表),大幅减小了电路规模。
  2. 证明算法 的硬件加速(GPU/FPGA/ASIC)使证明生成速度提升了一到两个数量级。
  3. 递归证明 进一步降低了链上验证的成本。

zkEVM可以根据兼容性级别进行分类:

  • 语言级别:构建一个对ZK更友好的新虚拟机,并通过编译器将Solidity等语言编译到该虚拟机。兼容性在语言层面。
  • 字节码级别:直接证明EVM字节码的正确执行。兼容所有EVM工具,但证明时间更长。
  • 共识级别:完全等效于EVM,包括存储、区块头等所有细节。证明时间最长,仍在探索中。


第二部分:从零构建zkEVM

上一节我们了解了zkEVM的必要性和分类,本节中我们将深入探讨如何从零开始构建一个zkEVM,包括前端电路化和后端证明系统的选择。

构建流程概述

构建zkEVM证明的流程主要分为两步:

  1. 前端电路化:将需要证明的计算(即EVM执行)表达为一组算术约束。
  2. 后端证明生成:运行证明算法,为满足约束的赋值(即见证)生成证明。

对于zkEVM,我们选择使用 Plonkish算术化 作为前端,并使用 Plonk IOP + KZG多项式承诺方案 作为后端来生成证明。

前端:电路化方案选择

我们需要证明EVM中的计算,这面临几个挑战:

  1. EVM字长为256位,大于常用证明系统所基于的有限域(如254位),需要高效的范围证明。
  2. EVM包含许多对ZK不友好的操作码(如Keccak哈希)。
  3. 作为虚拟机电路,需要高效处理读写一致性(如内存、堆栈)。
  4. EVM具有动态的执行轨迹,需要高效的选择器来在不同位置启用不同的约束。

考虑到对高效范围证明、电路连接和读写一致性的需求,我们需要支持查找表的算术化方案。同时,为了处理动态执行轨迹,我们需要自定义门。因此,Plonkish算术化 成为更合适的选择。

以下是Plonkish中可用的约束类型:

  • 自定义门:可以定义表中任意形状的“门”,并规定门内单元格值的任意关系。公式示例:v3 * vB3 - vB4 = 0
  • 置换约束:用于证明不同单元格的值相等,从而连接不同的门。
  • 查找表:证明一个元组属于某个预定义或动态生成的表。这对于范围检查、位运算和证明内存读写一致性非常高效。

编写zkEVM电路

我们需要证明的计算是EVM正确执行了一批交易。公开输入是旧状态根、新状态根和交易列表。见证则是EVM执行这些交易时产生的完整执行轨迹。

执行轨迹中的每一步都需要三类见证信息:

  1. 步骤上下文:执行此步骤时的虚拟机上下文(如剩余Gas、程序计数器)。
  2. 密钥开关:一个one-hot编码向量,指示此步骤执行的是哪个操作码。
  3. 操作码特定见证:执行该操作码所需的具体数据(如操作数)。

ADD操作码为例,其约束包括:

  • 步骤上下文约束:确保如果此步骤是ADD,则程序计数器、步骤指针等按规则更新。
  • 密钥开关约束:确保密钥开关向量中只有一个元素为1,其余为0。
  • 操作码特定约束:确保加法运算 a + b = c 正确,并处理可能的溢出。

此外,还需要证明操作数 ab 确实来自堆栈顶部(即读一致性)。这里我们使用查找表。在EVM电路中,我们只需查找 (a, b) 等元组是否存在于一个“魔法表”中。而该魔法表(如RAM表)本身的正确性则由另一个专门的电路(如RAM电路)来证明。

类似地,对于Keccak哈希等复杂操作,我们也在EVM电路中通过查找(输入, 输出)元组到专门的哈希表来“外包”证明,哈希表的正确性则由独立的哈希电路证明。

zkEVM整体架构

zkEVM的电路架构是一个由查找表连接的电路网络:

  • 顶层是EVM电路,约束状态机的每一步执行,并通过查找表连接到其他功能电路。
  • 多个专用子电路:包括用于内存/堆栈/存储的RAM电路、Keccak哈希电路、交易电路、区块上下文电路、ECDSA签名电路等。
  • 连接方式:EVM电路通过查找表“调用”这些子电路提供的功能。每个子电路负责证明其对应查找表中所有条目的有效性。

后端:证明系统选择

zkEVM包含多个子电路,理论上需要为每个电路生成并验证证明,但这在链上成本过高。因此,我们采用两层架构:

  1. 第一层:为EVM逻辑及各子电路生成证明。
  2. 第二层:一个聚合电路,用于证明所有第一层子电路的证明是有效的。最终只需在链上验证这一个聚合证明。

不同层对证明系统有不同要求:

第一层要求

  • 高度可表达,支持自定义门和查找表。
  • 证明生成速度快,硬件友好(高度并行、低峰值内存)。
  • 验证电路规模小(便于聚合)。
  • 最好具有透明或通用可信设置。

有希望的候选方案

  • Plonky2Starky等:使用小域(如Goldilocks域),CPU计算快,支持自定义门和查找表,硬件友好,验证电路小,透明设置。
  • Halo2 with KZG:支持自定义门和查找表,GPU证明性能好,验证电路小(KZG),通用设置。这是我们选择的方案。
  • Nova/SuperNova:支持高效的递归证明,可能在未来支持Plonkish后成为有力候选。

第二层要求

  • 在EVM上验证效率极高(Gas成本低)。
  • 证明体积小。
  • 硬件友好。
  • 理想情况下具有透明设置(但非硬性要求,因为聚合逻辑固定)。

有希望的候选方案

  • Groth16:证明体积小,验证成本低。
  • 配置为“高瘦”形态的Plonk(列数少)+ KZG/Flink:验证成本低。
  • KZG with Keccak:利用EVM预编译,验证高效。

我们最终选择 Halo2 with KZG 用于两层。在第一层,它满足了可表达性和性能需求。在第二层,我们可以将聚合电路配置得非常“瘦”(列数少),从而生成验证成本极低的证明。

性能数据示例

以下是一些(非最终)性能数据,展示了zkEVM的可行性:

  • EVM电路:约100列,200+自定义门,50个查找表。证明100万Gas的交易约需 2^18 行,GPU证明时间约30秒。
  • 聚合电路:约23列,证明生成时间约3分钟。
  • 通过GPU加速、流水线、内存优化等技术,证明性能已得到大幅提升。

第三部分:构建zkEVM时遇到的研究问题

上一节我们走完了构建zkEVM的完整流程,本节中我们来看看在实际构建过程中遇到的一些有趣的研究挑战,从前端电路设计到后端证明生成都有涉及。

电路侧的问题

1. 随机线性组合与多阶段证明
为了高效处理256位的EVM字,我们将其拆分为32个8位肢体,并用一个随机数θ进行线性组合编码为一个值。但随机数θ必须在所有肢体值确定后才能安全生成,这要求证明系统支持多阶段证明(先合成部分见证,衍生随机数,再合成剩余见证)。这虽然强大,但在多电路协作时可能导致复杂的同步开销。一个开放问题是:能否在不使用RLC的情况下实现类似的查找表布局优化?

2. 电路布局与自定义门复用
我们利用Plonkish的自定义门来灵活支持不同的操作码,但这导致了大量(超过200个)自定义门。我们是否可以通过定义更基础的“微操作码”来复用自定义门,从而减少总数?但这可能会降低电路单元的利用率。如何在灵活性和效率之间取得最佳平衡,仍需探索。

3. 动态电路问题
当EVM电路将复杂操作(如哈希)“外包”给子电路时,不同子电路所需行数不同。目前我们通过填充使它们长度一致,但这带来了问题:

  • 限制了单个证明中能包含的特定操作(如哈希)的最大数量。
  • 为未使用的部分支付了证明成本。
  • 能否支持动态大小的子电路,并按需付费?一种可能的解决方案是使用递归证明来动态聚合多个子电路证明。

后端证明侧的问题

1. 硬件友好性与瓶颈转移
通过GPU加速,多重指数运算和FFT等核心计算已非常快。但见证生成CPU与GPU间的数据拷贝成为了新的瓶颈。未来的证明系统设计需要软硬件协同设计,综合考虑理论复杂度与实际硬件(GPU/ASIC)的并行化能力。

2. 证明系统组合与递归
第一层需要高表达性,第二层需要极高的链上验证效率。是否存在比我们当前方案(两层Halo2-KZG)更好的组合?有两个主要方向:

  • 转向更小的域(如Goldilocks, Mersenne素数):在CPU和专用硬件上计算更快,内存占用更低。
  • 坚持椭圆曲线构造并利用新方案(如SuperNova):实现更高效的递归和更小的递归阈值。
    如何选择最优组合仍是一个开放问题。

安全性问题

zkEVM电路极其复杂(超过3万行代码),难以保证完全没有错误。如何有效地审计基于Plonkish算术化的虚拟机电路?形式化验证、SMT求解器等自动化审计工具是否可行?这是一个关键且具有挑战性的研究方向。


第四部分:zkEVM的其他应用

上一节我们探讨了构建zkEVM的技术挑战,本节中我们来看看zkEVM除了作为zkRollup核心之外,还有哪些有趣的应用场景。

1. 链上封装(Enshrined zkEVM)
这个想法更为激进:在Layer 1本身,要求区块提议者为每个区块生成zkEVM证明。这样,其他节点就无需重新执行所有交易,只需验证证明即可达成共识。这可以大幅提升Layer 1的效率和去中心化程度。然而,这要求“共识级别”的zkEVM,且证明时间必须极短,目前仍是一个雄心勃勃的目标。

2. 递归证明与历史压缩
更进一步,每个区块的证明不仅可以证明本区块内的交易,还可以证明之前区块的证明是有效的。如此递归,最终整个区块链的历史都可以压缩为一个简洁的证明。这实现了对区块链历史的终极压缩验证。

3. 漏洞证明(Proof of Bug)
如果将交易内容也作为私有输入,zkEVM可以证明:“我知道某个(未公开的)交易,执行后能使状态从A变为B”。这可以用来证明智能合约中存在可导致资产损失的漏洞,而无需立即公开漏洞细节,从而有利于漏洞赏金计划的安全执行。

4. 零知识预言机(ZK Oracle)
智能合约有时需要访问历史链上数据(如过去一个月的平均价格)。通过zkEVM的状态证明,可以可信地获取历史某个时刻的状态(如价格),并在零知识电路中进行计算(如计算平均值),最后将结果和证明提交到链上。这实现了无需信任第三方预言机的链上数据访问与计算。


总结

在本节课中,我们一起深入学习了zkEVM的方方面面。我们首先了解了其出现的背景和动机,即为了提供开发者友好且具备可组合性的通用zkRollup解决方案。接着,我们详细剖析了如何从零构建zkEVM,包括选择Plonkish算术化来处理EVM的独特挑战,设计由查找表连接的模块化电路架构,以及选用Halo2-KZG证明系统来平衡表达性与验证效率。然后,我们探讨了在构建过程中遇到的实际研究问题,如随机线性组合的工程实现、动态电路设计以及证明生成的硬件瓶颈等。最后,我们展望了zkEVM超越扩容的广阔应用前景,包括链上封装、历史压缩、漏洞证明和零知识预言机等。zkEVM正处于快速发展阶段,仍有许多激动人心的挑战和机遇等待探索。

013:采用形式方法保障ZK电路安全 🔐

在本节课中,我们将学习如何运用形式化方法来保障零知识(ZK)电路的安全性。形式化方法是一套基于数学的严谨技术,可用于发现软件漏洞并构建关于软件正确性的证明。我们将首先介绍形式化方法的基本概念,然后探讨两个具体项目,它们分别通过静态分析和形式化验证来提升ZK电路的安全性。


区块链软件中的漏洞可能极其危险。这里所指的区块链软件范围很广,从底层的共识协议、ZK电路、智能合约,一直到顶层的Web3应用,包括钱包、游戏、去中心化自治组织等。如果在这些层面的任何一层存在逻辑错误或安全漏洞,攻击者就可能利用这些漏洞窃取巨额资金。

为了给这一论断提供数据支持,我们可以看几个例子。例如,去年四月发生的一次闪电贷漏洞,导致价值1.82亿美元的加密货币被盗。再如,Solana协议中的拒绝服务漏洞导致了大规模网络中断,造成Solana币价在一天内大幅下跌。

最后,同样与今天主题相关的是,漏洞也可能出现在零知识证明层面。这并不奇怪,因为零知识证明也是由人编写的,而人都会犯错。例如,一个ZK证明协议中的完整性漏洞,可能允许攻击者凭空伪造出ZK代币。

虽然这听起来有些令人沮丧,但我想强调的是,漏洞并非不可避免。如果在区块链应用开发中采用恰当的形式化方法,我们实际上可以消除大部分漏洞。

形式化方法概述 📊

形式化方法指的是一套数学上严谨的技术,用于发现漏洞以及构建关于软件正确性的证明。需要明确的是,这里的形式化方法并非指单一的技术或工具,而是一系列技术的统称。这些技术构成一个谱系,从左侧的系统化测试,到中间的静态分析,再到右侧的形式化验证。在这个谱系上,越往右,能获得的安全性保证就越强。然而,天下没有免费的午餐,为了获得更强的保证,也需要投入更多的人力。

在更高层次上,我们可以将形式化方法分为动态技术和静态技术。动态技术(如模糊测试)通过在某些有趣的输入上执行程序并监控其行为,通常以发现特定漏洞为目标。相比之下,静态技术(如抽象解释或形式化验证)则静态地审视源代码,尝试推理程序所有可能执行的行为,进而判断系统是否安全。

本节课我们将主要关注静态技术,包括抽象解释和形式化验证。

抽象解释

我们无法精确推理给定程序的所有行为,因为这本质上是一个不可判定问题。然而,这并不意味着我们不能推理程序的抽象版本。其核心思想是,获得对原始程序行为的保守性过近似,并希望这足以证明程序不存在某些漏洞或具有正确性。抽象解释就是一个用于计算程序状态过近似的流行框架。

为了理解抽象解释如何工作,让我们先回顾一个简单的具体解释器。它接收具体值和我们想要解释的代码片段作为输入,并产生具体输出。例如,给定一个简单的语句 x = y + z,如果 y 是 1,z 是 2,那么 x 将被求值为 3。

抽象解释器的工作原理与此类似,但它操作的是抽象值,这些抽象值代表一组具体值。例如,一个整数 x 可以有一个区间 [A, B] 作为其抽象值,表示 x 大于等于 A 且小于等于 B。对于语句 x = y + z,如果我们知道 y 的抽象值在区间 [A, B] 内,z 的抽象值在区间 [C, D] 内,那么通过符号化地求值表达式 y + z,我们可以得出结论:x 的抽象值在区间 [A+C, B+D] 内。

其核心思想是通过执行可容错的符号执行或符号推理来模拟所有可能的程序路径。对于复杂语句(如分支和循环),思路是相同的:当遇到分支或不确定选择哪条路径时,我们保守地假设两条分支都会发生,然后合并不同分支的结果,收集沿不同路径的所有值。

抽象解释可用于捕获以太坊智能合约中的许多棘手漏洞。例如,重入攻击对智能合约非常危险,因为它允许攻击者在未更新余额的情况下重复提取用户资金。即使定义协议的实际业务逻辑可能非常复杂,我们也可以使用时序顺序和读写依赖等抽象概念来编码重入行为。通过静态抽象重入攻击的行为模式(例如,是否存在外部函数调用后跟存储更新),我们可以识别此类漏洞。

抽象解释在识别特定类型的漏洞方面非常有效,例如整数溢出、交易顺序依赖、闪电贷攻击等。学术界和工业界都有大量相关研究,例如,Slither 是最流行的静态分析器之一,可以编码许多常见漏洞;Vanguard 框架则允许开发者编码和捕获分片攻击。

然而,抽象解释不能保证程序没有逻辑错误。例如,如果你有一段函数想要计算,并想推理该函数是否按预期行为执行,抽象解释无法做到。这时就需要形式化验证。

形式化验证

要使用形式化验证,前提是提供形式化规约来描述预期行为。形式化规约是对预期程序行为的数学精确描述,通常采用某种逻辑形式,如一阶逻辑或时序逻辑。

形式化验证工具(通常称为程序验证器)从高层视角看,接收以下输入:目标程序的源代码或字节码、描述待验证属性的形式化规约,以及可选的注解(如循环不变量、合约不变量或领域知识引理)。程序验证器将这些输入组合成一个逻辑公式,这个组件称为验证条件生成器。验证条件生成器确保代码满足规约当且仅当生成的公式是有效的。有效性检查通常通过外部定理证明器(如 SMT 求解器 Z3、CVC4)或交互式定理证明器(如 Coq、Lean)来完成。最终,形式化验证器会产生“是”或“否”的答案。如果证明成功,则从数学上保证了程序符合设计行为。

验证条件生成器像编译器一样输出逻辑公式。例如,假设我们有一个函数 foo,它给 xy 分配不同的常量,最后断言 x > 0。验证条件生成器将编译这些信息和规约,生成对应的一阶逻辑公式。为了形式化验证该断言始终为真,验证器将问题简化为检查该公式是否有效。如果公式有效,则断言始终成立。

形式化验证非常强大,因为当证明成功时,它能提供最强的保证。然而,由于形式化验证的不可判定性,当证明失败时,可能属于以下几种情况之一:一是确实发现了逻辑错误(真阳性);二是证明无法进行,因为需要人工输入(如循环不变量、缺失的引理);三是由于现有定理证明器的不完备性或能力限制(这在ZK领域很常见)。例如,现有的定理证明器(如Z3)通常只能处理简单的线性整数算术,而ZK电路中充满了非线性算术和大素数有限域运算。

形式化验证有多种形式。我们目前讨论的是无界验证,它试图推理软件所有可能的行为空间,但这在实践中极具挑战性。另一种选择是使用有界验证来削弱保证,这在现实世界的验证工具中很常见。例如,可以限制输入大小或循环执行次数。

在智能合约和Web3领域,已有许多形式化验证工具,例如 Certora 验证框架、Mythril、Slither 等。

形式化验证技术可以检查程序是否符合提供的规约,这非常酷,因为只要你能写出形式化规约来描述想要检查的漏洞,就可以用它来证明程序不存在任何漏洞。其缺点是需要大量手动工作来编写规约。另一方面,如果你不想提供规约,可以使用其他类型的静态分析技术(如之前提到的抽象解释),它允许我们寻找特定类型的漏洞,并且不需要任何规约,因为这些漏洞模式已经作为分析的一部分被编码在抽象解释器中。


以上我们完成了第一节的内容。快速回顾一下,在第一节中,我们简要介绍了形式化方法中的一些基本术语和概念,特别介绍了两种静态分析技术:基于抽象解释和基于形式化验证的技术。我们还回顾了它们在实践中的优缺点和权衡。有了这些强大的工具,我们可以继续我们的旅程,看看如何实际运用这些技术来确保ZK电路的健壮性。

ZK电路中的漏洞与静态分析 🛡️

在本节中,我们将首先回顾典型ZK电路的基本组成部分。需要指出的是,尽管大部分示例和评估基于 Circom 和 Halo2,但我们的底层技术实际上是语言无关的,因为它基于电路和证明系统的语义,而非ZK领域特定语言的语法特性。这意味着,通过足够的工程努力,所介绍的技术应该能够扩展到其他DSL和证明系统。

我们将首先分享ZK的一些基本组成部分,然后基于对现有流行ZK项目的大规模研究,分享一个ZK漏洞分类法。最后,我们将提出一个基于抽象解释的系统,使我们能够从大规模ZK项目中快速识别这些漏洞。

ZK电路构造简介

首先,开发者用源语言(如 Circom 或 Halo2)编写他们的设计程序。源代码包含两个部分:见证计算和约束。程序随后被编译成两部分:见证生成器(通过转换原始见证计算得到)和多项式域方程。多项式域方程可以看作是一组关于输入、输出以及ZK电路中出现的中间变量的二次方程。最后,我们需要这些多项式域方程,因为它们将被转换成ZK-SNARK,后者由证明者和验证者组成。

请注意,这里的“验证者”与前一节介绍的程序验证器不同。ZK开发者的一个关键挑战是,编写设计程序需要同时编写见证生成代码和维护一组约束,编译器用这些约束来生成多项式方程。为了编写正确的ZK程序,见证生成和约束应该是等价的。

让我们正式定义一下这种等价性。我们用程序 P 表示见证生成代码,它接收输入变量 x 并返回输出变量 y。我们用大写 C 表示关于 xy 的约束系统。对于 xy 的具体赋值,约束系统 C 如果可满足则产生 true,否则产生 false

我们说 PC 是等价的,如果对于 P 的任何输入 x,使得 P(x) = y,并且 xy 也满足约束 C。换句话说,定义包含两个要求:首先,对于见证计算程序 P 的任何输入输出对,必须满足约束系统 C;其次,对于任何已经满足约束 Cxy 对,它也必须是原始程序 P 的有效输入输出对。

重要的问题是,这种等价性如何被违反?实际上,违反是可能发生的。我们将以更正式的方式研究这些可能的违反,并将其恰当地分解为两类,因为这实际上涉及两个方面。

第一类违反,我们称之为“过约束”漏洞,发生在存在程序的某个输入输出对(即满足见证计算 P 的有效输入输出对)但不满足约束 C 的情况下。然而,在实践中,我们很少看到这种过约束漏洞,主要有两个原因:首先,ZK开发者没有动机过度约束电路中的变量,因为这会增加证明生成成本;其次,在大多数ZK编程语言(如 Circom 和 Halo2)中,这些方程和约束在电路中是作为断言编码的,这意味着一旦发生过约束漏洞(即满足原始计算但违反约束),在电路执行期间可以立即看到断言失败,因此很多这类漏洞在开发阶段就被发现了。

另一方面,第二类漏洞,我们称之为“欠约束”漏洞,实际上要棘手得多。第二类违反发生在存在某个 xy 对满足约束系统 C,但这个 xy 不是程序的有效输入输出对(即 P(x) 的返回值不等于 y)。基本上,在这种情况下,攻击者或恶意用户可以声称任何关于随机计算的虚假事实,因为这些计算都被约束系统接受了。这种漏洞在实践中相当危险。

为什么欠约束漏洞很重要

为了理解这一点,让我们回顾一下从源代码到ZK-SNARK的原始工作流程。最终,ZK电路将产生一个由证明者和验证者组成的SNARK。这意味着,如果ZK电路的原始源代码中存在欠约束漏洞,验证者可能会接受本不该被接受的错误输入输出对。这不仅仅是假设性的漏洞,实际上可能导致应用程序中的严重漏洞,包括使攻击者能够耗尽协议的所有代币,或允许用户进行双花。

那么,我们如何处理这种情况呢?因为这实际上是一个根本性问题,需要推理ZK电路的见证计算和约束系统之间的等价性。在我们深入研究如何使用能提供最强数学保证的形式化验证之前,让我们先看看现有的漏洞是什么样的,以及我们是否可以用一些轻量级的静态分析来捕获其中的一些漏洞。

ZK漏洞分类法

从前面的章节我们知道,基于抽象解释的轻量级分析在发现已知漏洞方面非常有效。然而,要使用现有的抽象解释工具,需要我们事先了解漏洞的模式(可以是语法模式或语义模式)。因此,为了应对这些问题,我们首先与ZK和以太坊基金会的研究人员合作,基于对大规模ZK开源项目的系统研究,绘制了一个ZK漏洞分类法。

我们的分类法包含多个类别,每个类别都有我们识别出的几种漏洞变体。

第一类:欠约束信号
顾名思义,欠约束信号发生在约束被评估为真时,电路本质上会接受一切,因为对该信号没有约束。这个信号可以是输出信号或任何可以被参与者访问的公共信号。

让我们看一个具体例子。这是一个来自 Circomlib 库的有漏洞的 Num2Bits 实现。Num2Bits 接收一个素域数 n 作为输入,并返回其对应的二进制表示(一个包含 n 个元素的数组,每个元素是0或1)。在代码实现中,开发者试图使用 === 操作符来约束输出元素的值,即数组中的每个元素应该是0或1。然而,这里存在一个细微的差一错误:循环体使用 n-1 作为循环边界,但正确的版本应该是 n。如果你将原始电路编译成对应的约束系统,对于大小为4的数组,输出0和输出1都被正确约束(即都应该是0或1),但由于源代码中引入的差一错误,对最后一个元素(输出2)没有约束。这意味着攻击者可以给数组的最后一个元素传递任何值,证明系统都会接受,因为没有约束来阻止它接受这个值。这在实践中可能极其危险。

第二类:不安全组件使用
正如我们所知,大型ZK电路通常由许多小型可重用的组件(或称为“小工具”)组成,就像搭乐高积木一样。每个单独的组件或小工具本身看起来可能是安全的,但由于不正确的假设,漏洞仍可能出现在边界上。这就是我们所说的不安全组件使用,包括欠约束的子电路输出和欠约束的子电路输入。

让我们看一个具体例子。在这个例子中,开发者试图实现一个提款函数,该函数试图从余额中提取一定金额。开发者调用了“小于”组件来比较金额和余额。当然,你总是希望确保金额小于余额。然而,由于开发者忘记约束“小于”组件的返回值(正确的约束应该添加一个断言来强制输出值始终为0,这本质上声明了金额应始终小于余额),这意味着对提款金额没有限制。结果,攻击者可以提取超过其应得金额的资金。

第三类:约束与见证计算之间的差异
由于ZK-SNARK的设计,并非所有计算都能直接在证明系统中表达为约束。因此,当约束不能忠实地捕获计算语义时,就会出现漏洞。这里我以“非零倒数”为例来说明这种漏洞,在实践中存在许多此类变体。

让我们看一个非常简单的电路,它试图实现乘法逆函数。该函数非常简单,它试图计算 a 除以 b。然而,由于多项式方程不直接支持除法运算符,开发者需要使用乘法来模拟这个除法运算符。例如,a / b = out 等价于断言 out * b = a。这看起来没问题,除了它实际上遗漏了一个约束情况:当 b 等于0时,在原始见证计算中,除以0是未定义行为,应该在程序执行期间被拒绝和禁止。然而,由于你使用乘法来模拟这个计算,出现在当前约束系统中的乘法实际上会允许 b 等于0。这意味着在这种情况下,电路和证明将本质上允许一些在原始见证计算中本应是未定义的行为。

现在我们看到了问题。但我们如何检测那些可能出现在许多其他电路中的漏洞呢?特别是,我们如何使用之前学到的抽象解释技术来做到这一点?首先,我们需要一个抽象表示来量化ZK电路中可能与我们的漏洞相关的关键语义。

电路依赖图

我们引入了一种称为“电路依赖图”的语义表示。顾名思义,它是一个表示ZK电路的图,其中节点代表不同类型的信号(输入和输出),边代表依赖关系。我们使用有向实线边来表示数据依赖。例如,如果你有一个见证计算试图将 I 赋值给 O,那么我们将在 IO 之间放置一条边,表示 O 的计算依赖于 I。类似地,我们引入了第二种边,称为“约束边”,用虚线表示。这条边是无向的,表示如果有一个约束系统建立了信号 OI 之间的关系,那么我们将放置一条约束边来连接这两个信号。

现在让我们看一个例子,看看如何用电路依赖图来表示ZK电路的某些关键语义。给定左侧的电路,我们首先为原始电路中出现的每个信号构造一个节点,并使用不同的颜色来表示它们是输入还是输出。一旦我们绘制了节点,还需要构造边。例如,我们根据见证计算构造数据依赖边:为了计算信号 V 的值,需要进行涉及 B1B2carry 的计算。因此,你可以看到从 carryB1B2 指向 V 的有向边。类似地,除了数据依赖边,我们还有约束边。在这种情况下,我们试图建立一个涉及变量 V 和其自身的约束,因此我们将在信号 V 上放置一个无向的自环,并对 C_out 做同样处理。这就是我们基于原始程序构造电路依赖图的方式。

一旦我们使用电路依赖图获得了原始电路的抽象表示,就可以将漏洞检测简化为对图的查询实例。例如,底部的查询用于编码本节第一个模式中提到的欠约束输出信号的模式。我们用 Datalog 编码这个查询。其本质是,我们试图寻找是否存在一个欠约束的输出信号。为了成为一个欠约束的输出信号,我们首先寻找一个将成为输出信号的信号 V,然后检查这个 V 是否被某些常量约束。如果这个输出信号没有被任何常量约束,并且也没有直接或间接地被任何输入信号约束,那么我们就说我们识别出一个潜在的欠约束输出信号。

Vanguard 框架实现

我们将提出的想法作为 Vanguard 框架的一部分实现。该框架接收 Circom 或 Halo2 格式的ZK电路作为输入。我们将这些输入转换成对应的电路依赖图,然后使用一系列代表我们之前学到的语义模式的查询,在生成的电路依赖图上执行查询。最后,该工具将报告源代码上的任何潜在违规。

为了评估这种方法的有效性,我们从 GitHub 上17个流行的ZK开源项目中收集了258个电路。初步结果相当有希望:该工具能够识别出一批漏洞,其中包含32个先前未知的漏洞。我们已将所有漏洞报告给项目开发者,其中大部分已得到确认。此外,除了漏洞,我们还绘制了条形图来深入了解不同漏洞的分布情况。正如你所见,开发者在维护约束系统和见证计算之间的语义一致性方面遇到最大困难,这并不奇怪。


现在我们已经看到了静态分析器在捕获常见ZK漏洞方面的有效性,让我们重新审视之前的 Num2Bits 示例。正如我之前指出的,这里存在一个导致欠约束问题的漏洞,开发者忘记约束输出数组中最后一个元素的值。结果,这将使攻击者能够为最后一个元素提供任意值,并且验证者仍然会接受。

假设开发者使用我们的工具来识别这个问题,然后她建议了右侧的修复方案。我们想要弄清楚这个修复是否正确,即它是否真的没有欠约束问题。在这种情况下,我们不能仅仅重新运行之前的静态分析器,然后基于静态分析器没有发现任何问题的事实得出结论说当前电路是安全的,因为我们知道静态分析是基于常见模式或先验知识的。然而,开发者现在想要的实际上是一个更强的保证,这需要进行形式化验证。

形式化验证ZK电路 ✅

这正是最后一节要讨论的内容。

重新审视欠约束问题

在我们提出解决方案之前,我想重新形式化地审视欠约束问题。假设我们有一个包含两部分(见证计算 P 和约束 C)的电路,两者都以 x 作为输入,y 作为输出。我们说欠约束漏洞发生在存在一对 (x, y) 满足约束 C,但它们不是程序 P 的有效输入输出对时。在这种情况下,我们想要做的是形式化地证明一个电路不是欠约束的。

有几种方法可以做到这一点。两种朴素的方法是:一是执行静态分析,即定义一组预定义的规则,允许你推理信号变量的属性;二是将问题形式化为一个SMT查询,因为欠约束属性可以在有限域理论中形式化为一个一阶逻辑公式,求解器可以直接回答这个查询。

两种方法各有优缺点。基于静态分析的约束分析非常可扩展,可以让我们验证大型电路是否被安全约束,但它们常常遭受假阳性的困扰(即如果它们说一个电路是欠约束的,可能并非如此)。另一方面,SMT求解器没有假阳性问题,因为它基于非常精确的符号推理,但很难扩展到只有几百个约束的大型电路。

在本节中,我将提出一个名为 Picus 的新系统,它接收一组多项式域方程作为输入,并自动尝试验证电路是否被良好约束。如果 Picus 证明电路被良好约束,它将返回一个对勾,表示电路安全;如果证明电路是欠约束的,它将返回一个叉号;如果以上情况均未发生,它将返回“未知”。

Picus 背后的关键思想是结合静态分析和SMT求解器的优势,使其能够扩展到大型电路,同时保持合理的精度。从高层次看,静态分析阶段和SMT求解阶段在循环中交互,相互提供信息,使它们能够取得单独无法取得的进展。

让我们看看细节,了解它是如何工作的。

静态分析阶段接收多项式域方程 P 以及一组迄今为止已被 P 证明为良好约束的信号集合 K 作为输入。在开始时,算法将 K 初始化为空集。给定这些输入,静态分析引擎将生成一个更新的约束集合 K',表示该集合中的所有信号已被证明为良好约束。这里的不变量是 K' 应始终大于 K,以表明在每次迭代中,它都在扩大被证明为良好约束的信号集合,从而表明它正在取得进展。如果约束集合包含输出信号,那么我们将返回一个对勾,表示 Picus 将终止并得出结论:电路被正确约束。

另一方面,由于某些场景无法用静态分析推理,它将把 K' 作为输入传递给SMT阶段。SMT阶段有类似的接口:它接收约束 P 以及一组已被证明为良好约束的集合 K 作为输入,并返回一个集合 K''(包含被证明为良好约束的信号)以及一个集合 K_under(包含被证明为欠约束的信号)。第一种情况与之前类似:如果输出信号是 K'' 的子集,那么我们就完成了,返回电路安全。第二种情况是:如果任何输出信号与欠约束信号集合有非空交集,那么我们将返回叉号,表示电路不安全。第三种情况是:如果 K 实际上等于 K',意味着SMT求解器无法取得进展,这通常是由于现有有限域求解器的局限性。在这种情况下,我们将返回问号表示“未知”。如果以上三种情况均未发生,我们将把这些信息 K' 传播回静态分析阶段,并重复此过程,直到得出决定性答案。

这是 Picus 在我们评估之前提到的修复版本电路时的输出。正如你所见,该工具将输出关于电路约束数量的某些统计信息,最后打印出“SAFE”。这里的“SAFE”意味着 Picus 已经形式化地验证了给定电路保证没有欠约束信号。

为了评估 Picus 的有效性,我们在从流行Circom库收集的163个电路上进行了测试,包括59个来自 Circomlib2,104个来自 Circom-core。顶部的表格显示了约束数量和输出信号数量的相关统计数据。正如我们所看到的,Circomlib2 中的电路相对较小,而 Circom-core 库中的电路实际上非常大。为了比较两者的有效性,我们还将 Picus 与两个基线(仅使用静态分析的中间基线和仅使用SMT的基线)进行了比较。蓝色条对应 Picus 的结果。条形图显示了三个工具在两个不同基准集(Circomlib2 和 Circom-core)上的性能。Y轴对应于被解决的基准的百分比,越高越好。结论是,Picus 始终优于基于静态分析和SMT求解器的基线。如果你仔细观察图表结果,会发现虽然SMT求解器在解决简单基准时有效,但一旦增加电路的复杂性,其性能就会急剧下降。


这基本上总结了今天主要想讨论的两个项目。在这里,我想再次展示程序验证器的早期图示。虽然形式化验证的思想非常通用,但在应用于ZK电路等复杂领域时面临许多挑战。例如,存在许多开放性问题,比如如何表达涉及复杂加密原语的规约?正如我们之前提到的,大多数形式化验证工具依赖其底层定理证明器来检查逻辑公式的有效性。那么,我们如何设计更高效的证明器来有效推理ZK电路中无处不在的有限域理论?在验证无法进行(即无法产生有效证明)的情况下,我们如何生成具体证据来展示电路的问题?最后,正如我之前提到的,在这个领域中,我们的技术主要应用于 Circom 和 Halo2 的上下文,支持其他系统(如 RISC Zero、Plonky2、Nova 等)也将是令人兴奋的。

总结 📝

本节课中,我们一起学习了如何运用形式化方法来保障零知识(ZK)电路的安全性。

我们首先对形式化方法进行了简要概述,了解了它作为一套数学上严谨的技术,如何用于发现漏洞和构建软件正确性证明。我们重点介绍了两种静态分析技术:基于抽象解释的分析,它通过过近似程序行为来高效识别已知漏洞模式;以及基于形式化验证的分析,它通过数学证明来提供最强的正确性保证,但需要编写形式化规约。

接着,我们深入探讨了ZK电路的安全问题。我们了解了ZK电路的基本构造(见证计算与约束),并学习了“欠约束”漏洞的严重性,这种漏洞可能导致验证者接受错误的证明。我们介绍了一个基于大规模研究得出的ZK漏洞分类法,包括欠约束信号、不安全组件使用以及约束与见证计算之间的差异等类别。

然后,我们看到了如何应用抽象解释技术,通过构建“电路依赖图”这一抽象表示,并对其执行模式查询,来高效地检测这些常见漏洞。Vanguard 框架的实现展示了该方法的有效性。

最后,对于需要更强保证的场景,我们介绍了 Picus 系统。它创新性地结合了静态分析和SMT求解,能够在可扩展的前提下,形式化地验证电路是否被良好约束,从而数学上保证不存在欠约束漏洞。

总而言之,形式化方法为保障软件(尤其是ZK电路这类安全关键系统)的健壮性提供了强大工具。通过结合轻量级的静态分析(用于快速发现常见问题)和深入的形式化验证(用于提供数学上的正确性证明),我们可以系统地提升ZK电路的安全性,为构建更可靠的Web3应用奠定基础。

014:零知识证明的硬件加速 🚀

在本节课中,我们将要学习零知识证明的硬件加速。我们将探讨硬件加速的含义、当前行业现状以及未来的发展方向。

概述

硬件加速是指使用专用硬件来加速特定操作,使其运行更快或更高效。对于零知识证明而言,加速证明生成过程是解锁更复杂、新颖应用场景的关键。


什么是硬件加速?💡

硬件加速是利用专用硬件来加速某项操作,使其运行更快或更高效。它可能涉及优化函数和代码以利用现有硬件(通常称为商用现货硬件,COTS),也可能涉及为特定任务开发新的硬件。

商用现货硬件的例子包括CPU、GPU和FPGA。定制硬件通常被称为专用集成电路。

硬件加速与软件优化的不同之处在于,它针对特定的计算平台或硬件资源来设计和实现高效算法。


为什么需要加速零知识证明?⚡

核心原因是证明生成过程的开销非常高。虽然证明验证也很重要,但随着零知识证明的大规模部署,它将成为焦点,但目前证明生成的计算是瓶颈。

一个估计是,零知识证明生成的成本可能是原生执行计算的100万到1000万倍甚至更多。例如,为价值100万Gas的以太坊交易生成证明在CPU上可能需要约40分钟,而原生计算每秒可处理约1000万Gas。这表明证明生成的开销估计在25,000倍以上。


硬件加速的目标 🎯

硬件加速通常有以下几个目标,每个目标可能需要不同的设计或硬件平台:

  • 吞吐量:在单位时间内执行尽可能多的操作。
  • 成本:降低执行特定操作相关的资本和运营支出。
  • 延迟:减少完成单个操作所需的时间。

需要加速的核心操作 🔧

不同的证明系统及其实现使用不同的密码学原语和软件库。然而,在各种证明系统中,有三个计算密集型操作反复出现:

以下是三个主要的计算密集型操作:

  1. 多标量乘法
  2. 数论变换
  3. 算术哈希

多标量乘法

多标量乘法是一种计算多个标量乘法之和的算法。可以将其视为椭圆曲线点和标量的点积。

由于其性质,该操作非常容易并行化。每个标量乘法或一组标量乘法可以拆分并由不同的硬件引擎处理,最后再汇总。

对于大规模MSM,可以使用像Pippenger这样的算法将计算成本从与基数和标量数量成线性关系降低到大约O(n / log n)。

将此类高度并行化的操作从CPU等主机设备转移到GPU等更并行的架构上非常适合硬件加速。但需要注意,将数据移动到加速器上的通信带宽可能成为性能瓶颈。


数论变换

数论变换是一种用于将两个多项式相乘的算法。它类似于FFT或DFT,但独特之处在于它在有限域元素上操作。

常用的实现算法是Cooley-Tukey算法,它将多项式乘法的复杂度从O(n²)降低到O(n log n)。

与MSM类似,在主机外执行NTT时,数据也必须移动到加速器,通信带宽可能限制性能。然而,NTT的独特之处在于它不容易并行化,算法操作中每个元素需要与许多其他元素交互,这意味着问题不易分割,并且对内存要求很高。


算术哈希

在许多零知识证明用例中,需要证明者展示对哈希原像的知识,或利用哈希、默克尔根和默克尔包含路径来表示电路外存在的数据。

在零知识证明系统中,通常使用像Poseidon或Rescue Prime这样的算术哈希函数,而不是像SHA这样的传统哈希函数。选择它们是因为虽然在原生计算中成本更高,但在电路内部使用时效率更高。

高效实现此原语主要取决于模乘运算。


硬件资源需求评估 📊

上一节我们介绍了需要加速的核心操作,本节中我们来看看如何评估所需的硬件资源。

提高证明生成性能的第一步是理解你所使用的证明系统和用例的计算、内存和带宽成本。通过将MSM和NTT等高级操作分解为计算它们所需的模乘次数,通常可以在完成实现之前,估算证明系统在各种硬件平台上的性能。

为了确保估算准确,需要提前了解一些参数:

以下是评估硬件需求时需要考虑的关键参数:

  • 操作数量:证明系统所需的每种操作的数量。
  • 操作规模:不同用例会导致每个操作的规模不同,这会影响最高效的算法选择。
  • 域和曲线规模:这有助于了解每个模算术运算的带宽和计算复杂度。
  • 其他因素:例如曲线点的表示形式、模数是否具有支持更快约简的特殊结构等。

一旦确定了这些参数,就可以轻松计算执行证明或证明生成过程所需的模乘次数。有了这个数字,就可以将其与给定硬件平台的模乘性能进行比较,以得出性能估算或计算时间。


选择合适的硬件平台 🖥️

了解了需要执行的计算后,硬件加速的下一步是选择合适的工作硬件。鉴于这些工作负载主要由模乘驱动,我们应该寻找能够快速且廉价地执行大量乘法运算的硬件平台。

可以通过查看平台上的硬件乘法器数量、乘法器大小以及每个乘法器的执行速度和频率来评估给定硬件平台的估计性能。

例如,桌面CPU、服务器CPU、FPGA和GPU在乘法能力上存在显著差异。如果优化模乘吞吐量或每模乘资本成本,GPU可能是一个有吸引力的目标平台。

然而,这些分析仅突出了硬件平台的基本能力。为了实现改进的性能并达到硬件加速的目标,通常还必须考虑其他因素,例如实现理论性能的能力、部署难易度、运营成本、编程便利性等。


实现硬件加速的关键 🔑

在充分理解证明系统的计算需求并确定目标硬件平台后,成功的硬件加速有两个关键领域需要关注:

以下是实现高效硬件加速的两个关键步骤:

  1. 选择适合目标平台的硬件友好算法:像GPU和FPGA这样的目标平台具有大量核心,最适合高度并行化的算法。同时,应选择旨在通过减少所需操作总数来降低总计算成本的算法。
  2. 创建高效的实现:通常,算法可能需要重构以更好地匹配目标平台的硬件能力和设计。除了重构算法,通常还需要使用低级汇编原语以更充分地利用硬件资源并实现最大性能。

硬件加速的局限性与挑战 ⚠️

上一节我们讨论了实现加速的关键步骤,本节中我们来看看其中的挑战和局限。

在追求硬件加速时,需要记住乘法并不是唯一需要的资源。虽然这些高级原语主要由模乘主导,但通常还需要其他计算资源和算术单元。

此外,根据加速操作的大小和类型,非计算资源可能成为瓶颈。例如,像NTT这样的操作有时可能受内存访问速度的限制。对于问题规模较大的用例,有时所有必需的数据无法放入目标平台的内存中,从而导致性能下降。

对于连接到主机系统的加速器,通信带宽也可能成为瓶颈。目前,许多硬件加速的NTT实现(无论是GPU还是FPGA)都受限于主机和加速器之间移动数据的能力,而不是其计算资源。

这种数据移动成为瓶颈而非数据计算的趋势,不仅出现在NTT和零知识证明系统中,在整个大数据和高性能计算环境中也很普遍。对于高度并行的算法,计算通常比数据移动本身更快,因此硬件加速设计应寻求最小化数据移动。

使用主机外加速器的另一个考虑因素是数据移动到加速器并返回所需的时间。对于小问题,有时直接在主机上执行计算可能比在加速器上更高效。

硬件加速的最后一个陷阱是众所周知的阿姆达尔定律。该定律指出,通过优化系统的单个或多个部分所获得的整体性能改进,受限于被改进部分实际使用的时间比例。简单来说,对于零知识证明系统,如果MSM、NTT和算术哈希约占时间的65%,那么即使这些操作被完全消除,所能获得的最佳加速比也只有约3倍。考虑到证明生成相对于原生计算10万到100万倍的开销,显然优化不能止步于此。


现状与实例 🌟

为了总结今天对零知识证明硬件加速的介绍,我想分享一个硬件加速当前如何使用的真实例子。

过去几年,Filecoin一直是生产中最大(如果不是最大的)零知识证明系统之一,平均每天生成100万到500万个证明。Filecoin使用零知识证明进行复制证明,这是一种加密方式来证明你已创建了数据集的唯一副本。

Filecoin中使用的复制证明需要大约470GB的Poseidon哈希。如果在多核CPU系统上执行此哈希,大约需要100分钟。相比之下,Filecoin的GPU实现仅需在现代GPU上大约1分钟,性能提升了约100倍。

对于Filecoin的密码学证明部分,他们利用Groth16协议。在Filecoin网络上执行的每次复制证明,存储提供者会产生10个证明,每个证明约有1.3亿个约束,总计超过10亿个约束。仅创建这些证明所需的MSM就总计约45亿个点标量对。如果这些证明在多核CPU上计算,大约需要一小时完成。然而,相比之下,在GPU上可以在大约三分钟内完成,性能提升了约20倍。这个例子突显了硬件加速使雄心勃勃的零知识证明用例变得切实可行的能力。


未来方向 🚀

虽然过去几年零知识证明硬件加速取得了巨大进展,但仍有很大的改进空间。

以下是几个可能有助于使证明生成更快的领域:

  • 改进核心原语的算法:例如MSM和NTT的新算法,或对现有算法的其他优化。
  • 全新的核心原语:例如具有更低计算要求的新哈希函数。
  • 新的证明系统:特别是简化的证明系统。简化的证明系统可以为硬件加速创造更多机会,例如具有更少不同操作、降低通信和内存要求,甚至消除当今存在的一些计算密集型操作。
  • 改进的实现:包括完整的证明系统以及硬件加速原语的实现,目标既包括GPU和FPGA等商用现货硬件,也包括ASIC等定制硅。

总结

本节课中我们一起学习了零知识证明硬件加速的基础知识。我们探讨了硬件加速的定义、必要性、核心操作(MSM、NTT、算术哈希)、硬件资源评估方法、平台选择策略、实现关键以及面临的挑战。通过Filecoin的实际案例,我们看到了硬件加速带来的显著性能提升。最后,我们展望了未来可能的改进方向,包括算法优化、新原语、新证明系统以及更高效的实现。硬件加速是推动零知识证明走向更广泛应用的关键技术之一。

posted @ 2026-03-29 09:28  布客飞龙II  阅读(0)  评论(0)    收藏  举报