简介
Groth16 是由 Jens Groth 于 2016 年提出的 zk-SNARK 协议。它是目前区块链领域(如 Zcash, Ethereum L2, Filecoin)应用最广泛的算法。
核心特性:
- 极致简洁 (Succinct):证明 π 仅包含 3 个群元素 (在 BN254 曲线下仅约 128-200 字节)。
- 验证极快 (Fast Verification):验证过程仅需 3 次双线性配对 (Pairing) 运算。
- 需要可信设置 (Trusted Setup):需要为每一个电路单独生成 CRS (Common Reference String)。
整体工作流:
- 计算逻辑 (Code) → 算术电路 (Arithmetic Circuit)
- 算术电路 → 一阶约束系统 (R1CS)
- R1CS → 二次算术程序 (QAP)
- Groth16 (基于椭圆曲线和配对的证明生成与验证)
第一节:计算逻辑到算术电路 (Logic to Arithmetic Circuit)
1.1 核心概念转换
零知识证明不直接处理计算机程序(如 C++, Python),而是处理数学多项式。将代码转化为数学形式的第一步是算术化 (Arithmetization)。
| 传统程序 (CPU) | 算术电路 (ZKP) | 区别核心 |
|---|
| 指令流 | 数据流 | 电路没有寄存器,只有导线连接 |
| 动态执行 (Loops, Jumps) | 静态拓扑 (Unrolled Loops) | 循环必须展开,路径必须固定 |
| 布尔运算 (AND, XOR) | 有限域算术 (+, ×) | 位运算极其昂贵,需用多项式模拟 |
1.2 算术电路的构成
电路定义在有限域 Fr 上(模 p 的整数域)。
- 导线 (Wires):承载数值(Witness)。
- 加法门 (Addition Gate):z=x+y(计算成本极低,甚至视为免费)。
- 乘法门 (Multiplication Gate):z=x×y(昂贵,决定了生成证明的速度和大小)。
1.3 转换实例:三次方程
假设证明目标:“我知道一个数 x,使得 x3+x+5=35”。
第一步:拍平 (Flattening)
将复杂的嵌套计算拆解为单一操作符(op∈{+,×})的序列。引入中间变量 vi。
- 原始逻辑:
func(x) { return x*x*x + x + 5 == 35 }
- 拍平后:
- v1=x⋅x
- v2=v1⋅x
- out=v2+x+5
第二步:构建电路门 (Circuit Gates)
将上述步骤转化为电路约束。每个乘法关系对应一个门。
-
乘法门 1 (M1):
- 左输入: x
- 右输入: x
- 输出: v1
- 代数约束: x⋅x−v1=0
-
乘法门 2 (M2):
- 左输入: v1
- 右输入: x
- 输出: v2
- 代数约束: v1⋅x−v2=0
-
加法/线性组合 (作为最终约束):
- 约束: (v2+x+5)⋅1=35
- 注:在 R1CS 中,加法通常融合在 A⋅B=C 的线性组合中,不需要单独的门。
1.4 处理复杂逻辑 (Difficulties)
对于非代数运算,必须用数学约束模拟:
- 布尔约束 (Boolean Check):
证明一个变量 b 是二进制位(0 或 1)。
- 公式: b⋅(1−b)=0
- 条件选择 (If-Else / Multiplexer):
逻辑
if (b) y = x else y = z。
- 公式: y=b⋅x+(1−b)⋅z
- 注:必须计算所有分支,通过数学公式“选择”结果。
- 位运算 (Bitwise Operations):
由于域元素 Fr 是大整数,无法直接进行位操作。需要针对单 bit (a,b∈{0,1}) 使用代数公式:
- XOR (⊕): a⊕b=a+b−2ab
- 验证: 0+0=0,1+0=1,1+1−2=0 (正确)
- AND (∧): a∧b=a⋅b
- OR (∨): a∨b=a+b−ab
- NOT (¬): ¬a=1−a
- 位分解 (Bit Decomposition):
若要对 32 位整数 X 进行位运算,必须先将其拆解为 32 个变量 b0…b31,并添加约束:
- 约束 1 (聚合): X=∑i=0312i⋅bi
- 约束 2 (二进制检查): 对每个 bi 都要约束 bi(1−bi)=0。
- 代价:这就是 SHA-256 等哈希函数在 ZK 中极其昂贵的原因(需要数万个约束)。
1.5 总结
- 输入:一段代码逻辑。
- 输出:一张包含输入导线、中间导线、输出导线以及乘法门的“图纸”。
- Witness:使得电路所有门都成立的一组数值向量(例如 [1,35,3,9,27])。
第二节:算术电路到 R1CS (Arithmetic Circuit to R1CS)
2.1 什么是 R1CS (Rank-1 Constraint System)
R1CS 是一组定义在有限域上的特殊方程组。它的核心思想是把电路中的每一个乘法门(以及相关的线性组合)都标准化为同一个数学格式。
标准公式:
对于每一个逻辑门 i,必须满足:
(Ai⋅s)×(Bi⋅s)=(Ci⋅s)
- s (Solution Vector): 全局变量解向量(Witness)。
- Ai,Bi,Ci: 系数向量。
- ⋅ (Dot Product): 向量点积,代表对变量进行线性组合(加法)。
- ×: 数值乘法。
直译含义:
(左输入导线的线性组合)×(右输入导线的线性组合)=(输出导线的线性组合)
2.2 构建解向量 (The Witness Vector s)
为了用矩阵描述电路,首先要将电路中出现过的所有数值拍平成一个长向量 s。
通常结构如下:
s=[ 1, Public Outputs, Public Inputs, Private Inputs, Intermediate Variables ]
- 常数 1: 必须包含,用于处理加法中的常数项(如 x+5 中的 5,即 5×1)。
实例 (x3+x+5=35) 的向量定义:
假设变量顺序为 [one,out,x,v1,v2],若 x=3,则:
s=[ 1, 35, 3, 9, 27 ]
2.3 门到约束的转换 (Gate to Constraints)
我们需要为电路中的每一步运算构建 a,b,c 三个系数向量。
回顾逻辑:
- v1=x⋅x
- v2=v1⋅x
- out=(v2+x+5)⋅1 (加法通过乘以常数 1 转化为乘法约束)
约束 1: x⋅x=v1
- 左输入 (A): 取 x (向量第 3 位)。a1=[0,0,1,0,0]
- 右输入 (B): 取 x (向量第 3 位)。b1=[0,0,1,0,0]
- 输出 (C): 取 v1 (向量第 4 位)。c1=[0,0,0,1,0]
- 验证: (1⋅x)×(1⋅x)=v1
约束 2: v1⋅x=v2
- 左输入 (A): 取 v1。a2=[0,0,0,1,0]
- 右输入 (B): 取 x。b2=[0,0,1,0,0]
- 输出 (C): 取 v2。c2=[0,0,0,0,1]
- 验证: (1⋅v1)×(1⋅x)=v2
约束 3: (v2+x+5)⋅1=out
这是一次利用 R1CS 特性处理加法的操作。
- 左输入 (A): 线性组合 v2+x+5。
- 5 对应常数项 (5×s[0])。
- a3=[5,0,1,0,1]
- 右输入 (B): 常数 1。
- b3=[1,0,0,0,0]
- 输出 (C): 取 out。
- c3=[0,1,0,0,0]
- 验证: (5+x+v2)×1=out
2.4 矩阵形式 (Matrix Representation)
将所有约束的向量堆叠起来,形成三个矩阵 A,B,C。
假设电路有 m 个门,解向量长度为 n。矩阵维度为 m×n。
A=005000101010001,B=001000110000000,C=000001000100010
R1CS 系统定义:
对于给定的公开输入和私有 Witness 组成的向量 s,如果它是合法的,则必须满足:
(A⋅s)∘(B⋅s)=(C⋅s)
(∘ 代表阿达玛乘积,即逐元素相乘)
2.5 总结
- 输入:算术电路图。
- 输出:三个稀疏矩阵 A,B,C。
- 意义:将复杂的程序逻辑,彻底转化为了线性代数问题(矩阵运算)。
- 下一步:矩阵是离散的,为了能进行极简验证(Probabilistic Check),我们需要通过多项式插值将其转化为连续形式(QAP)。
第三节:R1CS 到 QAP (R1CS to QAP)
3.1 核心目标
R1CS 虽然严谨,但验证成本很高:如果有 100 万个约束(矩阵有 100 万行),验证者就需要逐行检查。
QAP (Quadratic Arithmetic Program) 的目标是将这 m 个离散的约束方程,打包成 1 个连续的多项式方程。
变换逻辑:
- 矩阵的行 (Rows) → 多项式的求值点 (Evaluation Points)
- 矩阵的列 (Columns) → 基多项式 (Basis Polynomials)
3.2 拉格朗日插值 (Lagrange Interpolation)
这是实现变换的数学工具。它的作用是找到一个多项式,使其穿过所有指定的坐标点。
变换过程:
- 定义根 (Roots):为 R1CS 的每一行(每一个约束门)分配一个索引值 rk。
- 理论上:可以是 {1,2,3,…,m}。
- 工程上:为了利用 FFT 加速,使用 单位根 (Roots of Unity) {1,ω,ω2,…,ωm−1}。
- 列变换:
对于矩阵 A 的每一列 i(对应第 i 个变量),我们构造一个多项式 ui(x),使得在第 k 行,多项式的值等于矩阵该位置的元素值。
ui(rk)=Ak,i
同理构造 vi(x) 对应矩阵 B, wi(x) 对应矩阵 C。
3.3 构造 QAP 多项式
一旦有了基多项式 {ui(x),vi(x),wi(x)},我们将解向量 s 中的数值(Witness)作为系数进行线性组合。
定义三个全局多项式:
U(x)=i=0∑nsi⋅ui(x)
V(x)=i=0∑nsi⋅vi(x)
W(x)=i=0∑nsi⋅wi(x)
- 含义:U(x) 代表了在任意 x 点,电路左输入导线的加权总和。
3.4 目标多项式 t(x) (The Target Polynomial)
这是一个系统预设的公有多项式,它的根就是所有约束行的索引。
如果使用单位根,它具有极其简洁的形式:
t(x)=k=0∏m−1(x−ωk)=xm−1
它的零点恰好就是我们需要检查约束的所有位置。
3.5 QAP 核心方程与整除性
这是整个转换的终点。
如果 Witness s 是正确的,那么在所有的约束点 x∈{r0,…,rm−1} 上,都有:
U(x)⋅V(x)−W(x)=0
根据因式定理 (Factor Theorem),如果多项式 P(x)=U(x)V(x)−W(x) 在所有根处都为 0,那么它一定能被 t(x) 整除。
因此,必然存在一个商多项式 H(x) 使得:
U(x)⋅V(x)−W(x)=H(x)⋅t(x)
3.6 为什么叫“二次” (Quadratic)?
虽然多项式的阶数(Degree)很高(取决于电路规模 m),但约束关系的代数结构是:
(线性组合)×(线性组合)−(线性组合)=0
这在代数定义上属于二次约束 (Quadratic Constraint)。这是双线性配对(只能做一次乘法)所能验证的极限。
3.7 总结
- 输入:R1CS 矩阵和解向量 s。
- 输出:一组多项式 U(x),V(x),W(x),H(x) 和目标 t(x)。
- 核心优势:
验证者不再需要检查 100 万个方程。根据 Schwartz-Zippel 引理,验证者只需要随机挑选一个点 τ,检查 U(τ)V(τ)−W(τ)=?H(τ)t(τ) 是否成立。
- 如果在这个随机点成立,那么整个多项式在全域相等的概率接近 100%。
- 这就是 zk-SNARKs "Succinct"(简洁)特性的来源。
第四节:Groth16 原理 (Groth16 Protocol over QAP)
4.1 核心数学工具
Groth16 建立在椭圆曲线对 (Elliptic Curve Pairings) 之上。
- 群结构: 定义三个循环群 G1,G2,GT,阶为质数 r。生成元分别为 g∈G1,h∈G2。
- 符号: [x]1=x⋅g, [x]2=x⋅h。
- 双线性配对 (Bilinear Pairing): 映射 e:G1×G2→GT。
- 关键性质: e([a]1,[b]2)=e([1]1,[1]2)ab。这使得我们可以验证加密数据的乘法关系。
4.2 第一步:可信设置 (Trusted Setup)
为了生成 CRS (Common Reference String),必须采样一组随机数(有毒废料),并在生成密钥后销毁。
随机采样: α,β,γ,δ,τ∈Fr∗。
生成密钥:
-
验证密钥 (vk): 用于处理公开输入和最终检查。
vk=([α]1,[β]2,[γ]2,[δ]2,{[γβui(τ)+αvi(τ)+wi(τ)]1}i=0l)
- 最后一项是针对公开输入 (i=0…l) 的预计算。
-
证明密钥 (pk): 必须包含生成 A,B,C 所需的所有群元素。
pk=G1 常数[α]1,[β]1,[δ]1,G1 幂次{[τi]1}i=0d−1,G1 私有 Witness{[δβui(τ)+αvi(τ)+wi(τ)]1}i=l+1m,G1 H项{[δτit(τ)]1}i=0d−2,G2 常数[β]2,[δ]2,G2 幂次{[τi]2}i=0d−1
- 注: pk 必须包含 [β]1 和 {[τi]2} 才能完成 C 和 B 的计算。
4.3 第二步:证明生成 (Proving)
证明者拥有 Witness a={a0,…,am},满足 QAP 方程 U(x)V(x)−W(x)=h(x)t(x)。
为了实现零知识,证明者采样随机数 r,s∈Fr。
构造 π=([A]1,[B]2,[C]1):
-
计算 A (G1 群):
A=[α]1+i=0∑maiui(τ)[1]1+r[δ]1
- 意义: 对 U(τ) 的加密评估,加上 α 偏移和 r 混淆。
-
计算 B (G2 群):
B=[β]2+i=0∑maivi(τ)[1]2+s[δ]2
- 意义: 对 V(τ) 的加密评估,加上 β 偏移和 s 混淆。
-
计算 C (G1 群 - 核心推导):
为了满足配对等式,需根据 A⋅B 的展开式构造 C。
C=利用 pk 计算私有部分i=l+1∑mai[δβui(τ)+αvi(τ)+wi(τ)]1+[δh(τ)t(τ)]1+平衡随机项As+Br−rsδ
- 注意: 计算 Br 时需要用到 pk 中的 [β]1 以及 G1 上的 [V(τ)]1。
4.4 第三步:验证 (Verification)
验证者接收 π=(A,B,C) 和公开输入 apub={a1,…,al}。
-
计算公开输入项 [Lpub]1:
[Lpub]1=i=0∑lai[γβui(τ)+αvi(τ)+wi(τ)]1
-
执行配对检查:
e(A,B)=?e(α,β)⋅e(Lpub,γ)⋅e(C,δ)
4.5 严谨性推导:为什么等式成立?
将验证等式右边 (RHS) 的指数部分展开,证明其等于 A⋅B。
RHS 展开:
RHS=αβ+Lpubγ+Cδ
代入 Lpub 和 C 的定义(忽略群同构,看作代数):
=αβ+公开部分i=0∑lai(βui+αvi+wi)+私有部分(i=l+1∑mai(βui+αvi+wi)+ht)+δ(As+Br−rsδ)
合并公开和私有部分的求和 ∑i=0m:
=αβ+(βU(τ)+αV(τ)+W(τ))+h(τ)t(τ)+δ(As+Br−rsδ)
利用 QAP 核心关系 W+ht=UV:
=αβ+βU+αV+UV+δ(As+Br−rsδ)
这正是 A⋅B=(α+U+rδ)(β+V+sδ) 的展开形式。
4.6 参数作用总结
- τ: 多项式求值的秘密坐标,保证计算基于 QAP。
- α,β: 强制 A 和 B 包含特定的线性结构,确保 UV 乘积被正确构造。
- γ: 绑定公开输入。验证者必须使用 vk 中的 γ 项,无法篡改 Public Input。
- δ: 绑定私有 Witness。证明者必须使用 pk 中的 δ 项构造 C,无法篡改 Private Witness。
- r,s: 提供零知识性,使 A,B 的分布随机化,隐藏真实的 U,V 值。
第五节:实战演练——Keyless Account (Google OIDC) 认证原理
场景目标:
用户拥有一份由 Google 签发的 JWT (JSON Web Token),其中包含 Google 的 RSA 签名。用户想向区块链合约证明:“我是这个 JWT 的拥有者,且这个 JWT 授权了当前交易使用的临时公钥 (pkeph),但我不想把 JWT 原始内容暴露在链上。”
5.1 初始状态:输入数据 (Inputs)
在将数据喂给 Groth16 之前,我们需要将业务数据转化为有限域 Fr 上的向量。
A. 解向量 (Witness Vector a)
假设电路已经过 R1CS 转换。向量 a 包含所有输入变量:
a=[ apub ∣ apriv ]
1. 公开输入 (apub - 链上合约已知):
- a[0]=1 (常数项)
- a[1]=Hash(pkeph,salt_commitment) (Nonce/临时公钥指纹)
- a[2]=Hash(PKGoogle) (Google 公钥指纹)
2. 私有输入 (apriv - 用户本地持有):
- a[3…k]: JWT Header & Payload (包含用户的 Google ID, iss, aud 等)
- a[k+1…m]: RSA Signature (Google 对 JWT 的 2048 位数字签名)
- a[m+1]: Salt (用户隐私盐值)
B. 电路逻辑 (The Circuit Logic)
这个电路包含成千上万个乘法门(主要是 RSA 模幂运算),但核心逻辑可以简化为两个超级约束:
- 验签约束: RSA_Verify(Payload,Signature,PKGoogle)==True
- 绑定约束: Payload.nonce==Hash(pkeph,Salt)
5.2 证明生成 (Prover's Computation - Off-Chain)
用户在本地(浏览器/App)生成证明。他拥有 pk (Proving Key) 和完整的 Witness a。
为了实现零知识(隐藏 Signature 和 Salt),用户选择随机数 r,s∈Fr。
1. 计算多项式快照
用户将 a 代入基多项式,在秘密点 τ 处求值(实际上是通过 pk 中的椭圆曲线点进行线性组合):
- UJWT(τ): 代表 JWT 数据和 RSA 签名的多项式总和。
- VJWT(τ): 对应于电路逻辑的右输入总和。
- WJWT(τ): 对应于电路逻辑的输出总和。
逻辑正确性保证:
因为用户的 JWT 签名是真的,且 Nonce 匹配,所以必定存在商多项式 H(x) 使得:
UJWT(τ)⋅VJWT(τ)−WJWT(τ)=H(τ)t(τ)
2. 生成加密点 A,B,C
用户利用 pk 计算出三个混淆后的点:
-
计算 A (在 G1):
A=[α]1+[UJWT(τ)]1+r[δ]1
- 隐喻: 这里面包含了用户的 RSA 签名信息,但被随机数 r 和系统参数 α 彻底打乱了。
-
计算 B (在 G2):
B=[β]2+[VJWT(τ)]2+s[δ]2
- 隐喻: 电路验证逻辑的另一半,同样被随机数 s 混淆。
-
计算 C (在 G1 - 核心粘合剂):
C=δ1(Privatepart(βU+αV+W)+H(τ)t(τ))+(As+Br−rsδ)
- 隐喻: C 包含了所有私有数据的校验和,以及商 H。它是为了让等式平衡而构造的“补丁”。
输出: 用户发送 π=(A,B,C) 和公开的 pkeph 到链上。
5.3 链上验证 (Verifier's Computation - On-Chain Contract)
智能合约(Verifier)不知道 JWT 长什么样,也不知道 Google 签名是多少。它只有 vk 和用户提交的 pkeph。
1. 构建公开挑战 (Lpub)
合约根据接收到的 pkeph 和写死在合约里的 Google 公钥指纹,计算公开输入的线性组合:
[Lpub]1=1⋅[IC0]+Hash(pkeph)⋅[IC1]+Hash(PKGoogle)⋅[IC2]
(注: ICi 是 vk 中预计算好的 γβui+αvi+wi)
2. 最终配对检查 (The Pairing Check)
合约执行以下指令(消耗约 200k Gas):
e(A,B)=?e(α,β)⋅e(Lpub,γ)⋅e(C,δ)
5.4 深度解析:为什么这个等式能确权?
让我们看看如果用户作弊会发生什么,以此反推验证的有效性。
情景一:用户拿了一个伪造的 JWT(签名不对)
- 在电路运算中,RSA 模幂约束无法满足。
- 导致 U⋅V−W 产生了一个无法被 t(x) 整除的余数 R(x)。
- 用户在计算 C 时,无法消除分母中的 δ(因为余数项没法除尽)。
- 为了强行生成 C,用户只能瞎填一个点。
- 链上验算时:e(C,δ) 产生的值将无法填补 e(A,B) 展开后的缺口。
- 结果:验证失败。
情景二:用户拿了别人的真实 JWT(偷来的),但想绑定自己的 Key
- JWT 签名是真的,RSA 约束通过。
- 但是 Nonce 绑定约束 失败:
JWT.nonce=Hash(pkuser_fake,salt)
- 同样导致多项式无法整除。
- 结果:验证失败。
情景三:用户想根据 A,B 反推 Google 签名
- A=α+U+rδ。
- 由于 r 是用户本地生成的随机数,对于链上观察者来说,A 只是一个随机点。
- 根据离散对数难题,无法从点 A 还原出数值 U。
- 结果:隐私 100% 安全。
5.5 总结:Groth16 在 Keyless 中的数据流
| 步骤 | 数据状态 | 可见性 | 含义 |
|---|
| Input | JWT_String, Signature, Salt | Private (用户本地) | 原始凭证 |
| Circuit | RSA_Check, Nonce_Check | Public (电路逻辑) | 验证规则 |
| Proof | A,B,C (三个椭圆曲线点) | Public (链上) | "我知道凭证"的数学证据 |
| Verify | e(A,B)=… | Public (合约执行) | 零知识放行 |
通过 Groth16,区块链合约得以在看不见 Google 签名的情况下,确信 Google 确实为该用户签发了证书。