1. 引言
零知识证明(Zero-Knowledge Proofs)近年来迅速发展,其中 Groth16 已成为最广泛使用的证明系统之一,具有许多非常适合区块链应用的特点:
- 验证继续3次 pairing 运算,使Groth16成为验证速度非常快的 ZK 证明。
- Groth16 生成的证明是固定大小,仅3个群元素,这比其它证明系统在链上存储和交易成本上更节省。
- 虽然Groth16 证明者(prover)端计算量很大,但通过硬件加速可以显著加快,使 Groth16 在高吞吐量应用中也可行。
Groth16 最大的权衡是每个电路(circuit)都需要可信设置(trusted setup)。尽管这是一个缺点,但Groth16的以上特点,使其成为区块链及其它以外用途的理想选择 ------ 单个证明就能证明复杂计算的正确性而不暴露输入,任何人只需3次 pairing 就能验证。
2. Groth16 工作原理
生成一个 Groth16 证明需要在大的素域(prime field)和椭圆曲线群上执行大量算术运算。本文不深入 QAP(Quadratic Arithmetic Programs)和 R1CS(Rank-1 Constraint Systems)。
可以把Groth16证明者阶段分成两步:
- 计算商多项式(quotient)
- 和 生成证明。
在这些步骤中,会包含:
- 3 个 IFFT(反快速傅里叶变换),
- 3 个 FFT(快速傅里叶变换),
- 以及 5 个 MSM(多标量乘法,多标量乘法是指多个标量与群元素的结合操作)。
- 除此之外,还有element-wise 按元素乘法和减法等操作。
MSM 和 NTT/FFT 这两大原语掌控了Groth16 证明过程的大部分时间。
- MSM(Multi-Scalar Multiplication)与基于快速傅里叶变换的多项式求值(FFT/NTT)是最主要的瓶颈。MSM 大约占证明时间的 ~60%,FFT 大约占 ~30%。
Groth16 证明者需要两个文件:witness 文件 与 zkey 文件。
- witness 文件包含给定输入根据电路产生的中间值,包括公开与私密输入。对每个输入都是独一无二的。
- zkey 文件则包含电路、可信设置、证明与验证密钥等,是在可信设置阶段生成的,对每个电路唯一。
3. 现有 Groth16 实现的状况
目前已有很多 Groth16 的实现,常见的包括:
- SnarkJS(https://github.com/iden3/snarkjs(JavaScript)) --- 易用、访问性好,也提供了3种zkSNARKs证明系统,以及用于设置与验证的工具。
- Rapidsnark(https://github.com/iden3/rapidsnark(C++ 和 汇编)) --- 用 C++ 和 汇编 写成,比 SnarkJS 快约 4 到 10 倍。
- Arkworks(http://github.com/arkworks-rs/groth16(Rust))
- Lambdaworks(https://github.com/lambdaclass/lambdaworks/tree/main/crates/provers/groth16(Rust))
- Gnark(https://github.com/Consensys/gnark(Go))
4. 用 ICICLE 突破 Groth16 速度障碍
ICICLE 是一个前沿的密码学库(ICICLE-snark(https://github.com/ingonyama-zk/icicle-snark(C++和Rust))),旨在加速诸如 ZKP(零知识证明)等高级算法与协议,在多种计算后端(包括 GPU、CPU、Metal 等)上运行。它支持 C++、Rust、Go 多种前端,使得在不同开发环境中集成更加容易。只用一个代码库,就能利用三种流行语言和多个硬件平台。
4.1 ICICLE 加速 Groth16
起初Ingonyama团队只计划把 MSM 和 NTT 部分集成进 ICICLE,但由于数据传输与类型转换的问题,性能并没有达到预期。最终的解决方案是从头构建一个 Groth16 的 prover,该 prover 使用 ICICLE 实现。参考代码库是 (iden3 团队的)SnarkJS(https://github.com/iden3/snarkjs(JavaScript))。这样可以允许所构造的 prover 使用现有的 zkey 文件。任何人都可以从 SnarkJS 或 Rapidsnark 切换到 ICICLE-snark(https://github.com/ingonyama-zk/icicle-snark(C++和Rust))。
- ICICLE 已经对 第2节"Groth16 工作原理" 中提到的那些原语(MSM, FFT/NTT 等)做了高度优化。
另一个现有工具的问题在于,它们被设计为 命令行工具(CLI) 使用。
这意味着如果要多次为同一个电路生成证明,那么其实有许多数据是可以缓存并复用的,如:
- 证明密钥(proving key)
- 基(bases)
- 和 NTT 域(NTT domain)。
为了利用这种缓存技巧,Ingonyama团将其Groth16 prover 开发成了一个 后台工作进程 和 Rust 库。
4.2 ICICLE 中的数据转换与数据传输
将代码迁移到 GPU 时,最大的问题之一是数据传输和数据类型转换。要能使用 ICICLE 并利用 CUDA,需要将数据转换为 ICICLE 的类型并移动到device端。通常为 CUDA kernel 准备数据的时间会比 kernel 本身执行时间还要长。
- 类型转换(conversion)如果用 naïve 的方式(如多次调用
from
函数)会非常慢。在 Rust 中,如果可以保证安全,可以使用transmute
直接改动已有内存的类型,这样几乎可以完全加速------transmute
调用只花费约 20--30 纳秒,而 naive 转换可能花费约 100 毫秒。 - 数据传输速度受限于硬件。因此如果可能,应尽量避免将数据在host与device之间频繁移动。这是为什么团队选择从头构造 Groth16 prover 而不是仅替换部分 MSM/NTT 调用。
4.3 ICICLE 中的 MSM 与 NTT 优化
ICICLE 中的 MSM 与 NTT 优化有:
- 替换5个 MSM 所用的实现为 ICICLE 的 MSM,在规模为 2 22 2^{22} 222 的情况下平均提升约 63倍。
- 同样替换 3 个 IFFT + 3 个 FFT 为 ICICLE 的 FFT API,在规模为 2 22 2^{22} 222 的情况下平均提升约 320倍。
4.4 ICICLE 中的VecOps(向量运算)优化
ICICLE 中的VecOps(向量运算)优化:
- 有很多地方需要对长向量做element-wise按元素的乘法与减法。这类操作在 GPU 上高度并行化。
- 使用 ICICLE 的 VecOps API 而不是在 CPU 上处理这些长数组,平均提升约 200倍(在规模 2 22 2^{22} 222 时)。

4.5 ICICLE 中的缓存机制(Cache)优化
在许多应用场景中(如证明服务或 Layer-2 rollups),可能会对同一个电路和证明密钥(proving key)做多个证明。
- ICICLE 利用这一点,把与 zkey 文件相关的计算保留在设备内存中,并缓存这些计算结果。称为
CacheManager
。如果某次已经针对某个 zkey 文件算过某些值,下次用到就可以重用,不必重新计算。

5. 性能基准与实验结果
在以下两种设置上进行了基准测试:
- 使用 4080 GPU + Intel i9-13900K CPU
- 使用 4090 GPU + Ryzen 9 9950X CPU
用 MoPro(https://github.com/zkmopro/benchmark-app/tree/main/mopro-core/examples/circom) 基准里的电路来对比不同证明系统的性能。测试包括:
- "复杂电路":用于纯粹基准测试,以便对比约束数量多寡与性能差异。
- Anon Aadhaar:一个零知识协议,允许 Aadhaar 身份所有者在不暴露其身份信息的情况下证明身份。
- Aptos Keyless:Aptos Keyless 让用户无需密钥或助记词,而使用诸如 OIDC 凭证(Google, Apple 等)来创建账户。
如前所述,在生产环境中,一个项目更有可能重复证明相同的电路。
为了利用这一点,ICICLE使用了 缓存系统(Cache system)。
然而,由于所对比的其他工具都是 命令行工具(CLI),因此在一次证明完成后就会退出。
为了保持公平,同时提供了 启用缓存 和 不启用缓存 两种基准测试结果。
6. 如何集成 ICICLE
可将 ICICLE-snark 集成到自身项目的代码库里。根据使用的语言有两种方式:
- 在 Rust 项目中使用
- 在其他编程语言中使用
6.1 在 Rust 项目中使用 ICICLE
如果自身项目代码库是用 Rust 编写的,那么最佳实践是将ICICLE Groth16 prover作为 Rust Crate 使用。
只需要通过执行:
cargo add ICICLE-snark
将其添加到依赖中。
之后,就可以导入证明函数,并创建一个 CacheManager 实例,然后通过提供 witness 文件 和 zkey 文件 的路径来开始证明。
rust
use icicle_snark::{groth16_prove, CacheManager};
fn main() {
let mut cache_manager = CacheManager::default();
let witness = "witness.wtns";
let zkey = "circuit_final.zkey";
let proof = "proof.json";
let public = "public.json";
let device = "CUDA"; // 若需可以替换为 CPU 或 METAL
groth16_prove(
&witness,
&zkey,
&proof,
&public,
device,
&mut cache_manager
).unwrap();
}
6.2 在其他编程语言中使用 ICICLE
如果自身项目代码库是用其它语言写的,也可以使用 ICICLE prover。方式是用"worker 模式"(worker mode)运行 ICICLE-snark,然后从项目代码库与ICICLE worker 通信。ICICLE 的例子目录里有相关用了该模式的示例。
参考资料
1\] 2025年3月18日Ingonyama团队博客 [ICICLE-Snark: The Fastest Groth16 Implementation in the World](https://medium.com/@ingonyama/icicle-snark-the-fastest-groth16-implementation-in-the-world-00901b39a21f)