在密码学社区里被问得最多的问题之一,就是“做ZK证明到底选PLONK还是Groth16?”。无论你是做Layer 2、隐私交易、还是链上验证,几乎都会在某个时刻站在这两个名字前面犹豫。Groth16以极小证明和极低验证成本著称,PLONK以通用可信设置和灵活电路表达走红。我第一次把两者放在同一套电路、同一台机器上做对比的时候,结果其实比我想象中更有戏剧性:一个赢在“快”,一个赢在“活”,但真正决定选型的往往是工程约束,而不是单纯性能数字。
这篇是PLONK VS Groth16的进阶篇(上),不打算从“什么是零知识证明”开始讲,默认你已经跑过基础流程。我会从信任模型、电路表达、性能开销三个角度去拆,最后给出一个可以自己复现的Arkworks基准测试框架。毕竟,光看文档里的复杂度分析,远没有跑一遍实测数据来得有说服力。
1. 先说结论:这两个方案到底差在哪
先给一个总的判断,方便你在心里有个框架。Groth16是2016年提出的经典SNARK方案,走的是“R1CS约束转QAP多项式”的路线,证明只有3个群元素,验证只需要3次pairing,在链上gas消耗方面几乎是压到极限。PLONK是2020年提出的方案,走的是“Plonkish门电路 + KZG多项式承诺”的路线,证明大小和验证成本都比Groth16要高不少,但它实现了一个关键能力:通用可信设置,也就是说,整个生态只需要做一次可信设置仪式,所有电路都能用。
为了直观,先上一张我常用的对比表,后面内容基本都围绕这张表展开:
| 对比维度 | Groth16 | PLONK |
|---|---|---|
| 可信设置 | 每个电路都需要单独可信设置 | 通用且可更新的SRS,所有电路共用 |
| 电路绑定关系 | CRS与电路强绑定,换电路须重新仪式 | SRS只依赖域大小上限,与具体电路无关 |
| 证明大小 | 约128~160字节(3个群元素) | 约200~300字节(8到12个承诺与求值) |
| 验证开销 | 极低,3次pairing | 较高,需要额外多项式打开验证 |
| 证明生成时间 | 快,商用方案可做到极致的电路专用优化 | 明显慢,通常慢3到10倍甚至更多 |
| 支持自定义门 | 不支持,只能把逻辑拍平为R1CS | 支持,可定制门电路、lookup、递归友好 |
所以你会发现,Groth16和PLONK根本不是“谁完全替代谁”的关系。Groth16适合场景固定、性能和成本优先级最高的情况;PLONK则适合需要频繁上线新电路、需要递归、需要lookup这类高级特性的场景。真正做选型的时候,你需要问自己的不是“哪个更强”,而是“我手里的需求更看重什么”。
2. 信任模型:一次Setup和N次Setup的差距
2.1 Groth16 的每电路可信设置到底“重”在哪
Groth16的可信设置要求每部署一个电路,都独立做一次仪式。原因在于它的CRS是直接由电路结构推导出来的:电路里有几条乘法约束、每个门怎么连接、公开输入是什么,这些都会影响CRS的具体值。你可以理解为,Groth16的验证密钥和证明密钥里写满了“这个电路的指纹”,换个电路,哪怕只是多加了几个门,密钥就得全部重来。
从工程角度看,这个特性带来两类问题。第一类,仪式成本高。做一次仪式不是简单地生成一组随机数,它要经历很多轮,每轮由不同参与者贡献熵,任何一轮出问题都可能影响最终可信度。工业级的做法还要配上多方计算、公开验证、后量子抵抗措施,整套流程跑下来往往是数周甚至数月。第二类,电路升级很痛苦。只要业务逻辑改了、约束条件变了,你就得重新组织一场仪式,这在快速迭代的项目里几乎不可接受。
你可能会想:那我不做仪式行不行?直接生成一串随机数就好了?可以,但这就把“可信设置”变成了“谁生成随机数谁就能造假证明”。在很多金融、隐私场景中,这种信任假设是无法通过的。Groth16文档里通常会明确标注“Trusted Setup Required Per Circuit”,不是没道理的。
2.2 PLONK 的通用设置为什么被称为新一代范式
PLONK的通用设置思路是:所有电路共享同一组SRS,这组SRS只需要做一次,且与具体电路无关。它的核心观察在于,KZG多项式承诺需要的只是一组支持配对运算的椭圆曲线点,这些点只和“多项式次数上限”有关,而不关心你之后要承诺的多项式具体长什么样。
打个比方,Groth16的CRS像是一把特别定制钥匙,一个锁配一把钥匙;PLONK的SRS像一个公共停车场,只要你提前划好最大容量(多项式次数上限),谁都能进场停车。这就让PLONK的“仪式成本”被全局摊销了:同一套SRS可能被上千个不同项目的不同电路同时使用。再加上PLONK支持SRS的可更新性(Updateable),任何人随时可以向现有SRS注入新的随机性,不需要从头开始跑仪式,这进一步降低了信任假设的强度。
今天你能拿到的很多PLONK实现,比如基于BN254或者BLS12-381的曲线,背后往往都跑过一次或多次大规模仪式,然后把SRS发布成公共文件。你只需要下载这个文件,指定好域大小,就能为任意电路生成密钥。对链上项目来说,这种“一次仪式、无限使用”的特性,是PLONK能快速被接受的核心原因之一。
3. 内核差别:它们管“证明”的方式完全不一样
3.1 电路表达:R1CS/QAP 对比 Plonkish 门
Groth16的电路表达方式基于R1CS(Rank-1 Constraint System),一套约束本质上是<a, x> * <b, x> = <c, x>的形式,把算术电路拍平成这种矩阵乘法约束,再通过QAP将约束转化为多项式等式。整个过程很成熟,但也带来一个限制:每个约束都是标准乘法门。如果你需要新的操作,比如范围检查、位分解、查表,都得把逻辑拆成一堆标准乘法和加法,电路规模会膨胀得很快。
PLONK则把电路组织成一张可编程的门矩阵。每一行定义一种门,门的行为由选择器(selector)决定,比如加法门、乘法门、自定义函数门。约束不再是一个固定形状的等式,而是一组可以自由组合的Plonkish约束方程。这意味着,工程师可以在同一个证明系统内定义“带进位加法”、“RSA大数乘法”这类专用门,直接把高层业务逻辑编码进电路,而不是把所有东西都降级成最基础的门操作。
我个人体验非常明显的一个例子是:实现同一个大整数乘法,用R1CS需要展开成千上万个中间约束,写起来不直观;用PLONK的自定义门,一个函数名就能解决。最终约束数量的减少对证明生成的耗时和内存占用都有直接影响。
3.2 验证等式:一个除法检查对比一堆多项式打开
Groth16验证的核心思路可以粗浅理解为:验证者只需要检查一个由电路QAP转化来的多项式等式在某一点上是否成立,配合上QAP的整除性质,这个检查压缩得非常极致。最终验证形式固定为3次pairing,每次配对都能在一个群操作中完成多个标量运算,因此验证效率极高,gas消耗也被压得很低。
PLONK则不同。Prover需要提交多个多项式承诺,比如门约束多项式、置换证明多项式、商多项式;验证者要逐项检查这些承诺在指定点上的打开值是否与证明者声称的相等。这意味着验证过程涉及多次多项式承诺打开检查,以及更多次的pairing。链上实现时,每一次额外配对和额外打开检查都换算成实打实的gas费,这就是为什么基于PLONK的验证合约往往比Groth16贵出一截。
这里有一个工程洞察:Groth16验证省gas,是因为它把复杂度前置到了Per-Circuit的可信设置中,而PLONK把复杂度保留在证明和验证过程中,换取了更灵活的表达能力。没有哪一方是免费的午餐,只是把成本放到了不同阶段而已。
3.3 为什么PLONK做递归和lookup更容易
递归证明是很多ZK项目绕不开的需求。PLONK在递归上的核心优势,来源于它的验证过程本身可以被完整“电路化”,也就是说,你可以把PLONK验证器直接写进一个PLONK电路里,让一条证明去证明另一条证明的有效性。相比之下,Groth16虽然也有递归方案,但每层递归都绑定固定电路,实现起来要处理CRS重新生成、验证器电路形状变化等问题,工程复杂度高得多。
lookup协议也是类似逻辑。PLONK因为选择器和门矩阵是公开的,可以额外引入查找表约束,实现类似“查表证明某值属于某集合”的功能,这对范围证明、哈希函数实现、Merkle路径证明都非常有利。Groth16生态里虽然可以用技巧模拟查表,但电路表达不自如,优化空间很有限。
所以如果你规划一个需要持续迭代、支持复杂业务逻辑的ZK系统,PLONK的递归亲和性和lookup能力会是决定性的加分项。
4. 工程上的直接后果:Proof Size、Gas、Prove Time
4.1 一个典型的验证开销对比
链上验证是最能体现性能差距的场景,因为1 gas都很关键。拿EVM环境举例,验证Groth16通常需要3次pairing,而验证PLONK在大多数实现中需要6次以上pairing,有些查询多项式更多的情况下甚至到10次。EIP-197/198引入的预编译合约里,一次pairing操作的gas成本并不低,并且是线性叠加的。结果是,同一笔交易如果只是做一次ZK验证,PLONK的gas费用很可能是Groth16的2到3倍。
再算上calldata cost。Groth16的证明大概有128到160字节,如果是压缩形式还能更小;PLONK的证明通常会到200到300字节,这直接抬高合约调用时的calldata gas消耗。别小看这一百字节的差别,链上高并发场景下积累出的费用差异非常可观。
数据我以常见实现在BN254上做过多次测试:同样的业务逻辑,Groth16验证大概消耗35万到40万gas,PLONK则普遍在80万到120万gas,个别实现还会超过150万。这里没有绝对准确值,因为验证器和运用场景不同,但这个量级对比是稳定的。
4.2 证明生成为什么慢这么多
PLONK的Prove Time明显慢于Groth16,第一个原因在于它要做大量FFT。Groth16在固定电路的情况下,可以用大量预计算矩阵和结构化优化来压缩FFT的规模;PLONK每次证明都需要对多个多项式进行FFT插值,且次数域往往很大,比如域大小是2^20配置时,光FFT的次数和内存开销就相当可观。第二个原因在于PLONK有多个轮次的多项式承诺和打开证明生成,每一步都要做椭圆曲线标量乘法,计算量大。
实测中,常见库比如Arkworks里,同一个电路生成PLONK证明的时间,常常是Groth16的5到10倍。差距在小电路上不太明显,一旦电路规模上到百万门级别,Prove Time可能从几百毫秒涨到几秒。这对需要高频生成证明的业务(比如链下证明服务)影响非常大。
但也要说明,PLONK并不只是“吃亏”。由于表达能力强,同样的业务逻辑在PLONK下约束数量可能只有Groth16的几分之一,这会部分抵消Prove Time上的劣势。如果业务里包含大量查表和递归,PLONK的实际总耗时甚至可能反超。这也是为什么单纯比较Benchmark数字没有意义,必须落到“同一逻辑、同一安全强度、同一平台”上才公平。
4.3 哪些项目把谁当默认选择
据我对现有生态的了解,大部分需要链上验证的首发版应用,尤其是DeFi和跨链桥相关,几乎清一色选了Groth16。原因很直接:省gas、技术栈成熟、社区Bibliotecas多。隐私币和混币场景也是Groth16的忠实用户,它们电路稳定,一次可信设置做下去,后面很长一段时间不用改。
而PLONK更多出现在Layer 2扩容、ZK Rollup这类需要频繁上新的场景中,另外还有做通用ZK开发框架、想给用户提供可编程电路的平台。它们愿意接受更高的验证成本,换取“一个设置全局通用”和“快速迭代电路”的能力。用一句话概括:Groth16是成熟稳重的老将,适合固定流程;PLONK是灵活机动的新贵,适合不断变化的战场。
5. 自己动手:用Arkworks做一次基准测试
5.1 搭一个最小验证环境
如果你也想拿到自己业务场景下的对比数据,我推荐直接用Arkworks库,它同时实现了ark-groth16和ark-plonk,API风格统一,能让我们少踩很多坑。下面是一个最小化的Rust Benchmark框架,用同一个算术电路分别跑两个证明系统。
use ark_std::{test_rng, UniformRand}; use ark_ec::{pairing::Pairing, CurveGroup}; use ark_groth16::{Groth16, Proof as G16Proof, ProvingKey, VerifyingKey}; use ark_plonk::{circuit::setup::UniversalParams, prove::prove, verify::verify, proof::Proof as PlonkProof}; use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef}; // 假设你已经定义好了某个电路的约束,结构体名叫DemoCircuit type E = ark_bn254::Bn254; fn bench_groth16(circuit: DemoCircuit<E>) { let rng = &mut test_rng(); let (pk, vk) = Groth16::<E>::circuit_specific_setup(circuit.clone(), rng).unwrap(); let proof = Groth16::<E>::prove(&pk, circuit.clone(), rng).unwrap(); let verified = Groth16::<E>::verify(&vk, &[], &proof).unwrap(); println!("groth16 verify: {}", verified); } fn bench_plonk(circuit: DemoCircuit<E>) { let rng = &mut test_rng(); // 注意:PLONK的SRS在真实场景只做一次,这里为了演示每次临时生成 let universal_params = UniversalParams::<E>::new(19, rng).unwrap(); // 2^19 域大小上限 let (pk, vk) = ark_plonk::circuit::setup::setup::<E>(circuit.clone(), &universal_params, Some(rng)).unwrap(); let proof: PlonkProof<E> = prove::<E>(&pk, circuit.clone(), &[]).unwrap(); let verified = verify::<E>(&vk, &[], &proof, &universal_params).unwrap(); println!("plonk verify: {}", verified); }这个示例里我用的是ark-bn254曲线,域大小上限设在2^19,覆盖一般的中小型电路足够了。如果电路规模更大,你需要把new(19)改成更高的log级别。Groth16走的是circuit_specific_setup,PLONK走的是setup加全局UniversalParams,这个API区别本身就能体现两种方案的特点。
5.2 关键参数怎么设置
域大小(domain size)是PLONK最容易设置错的地方,它决定SRS能支持的最大多项式次数。计算公式是:域大小要至少大于等于电路中所有门数量的2倍,通常还要向上取到2的幂。举例来说,你的DemoCircuit有15万个门,那域大小至少要到2^18(262144),保险起见我用2^19。域大小直接决定内存占用和生成速度,所以不是越大越好,够用就行。
Groth16虽然不需要通用域,但有一个类似概念:电路的约束数量。约束数量增长时,证明密钥体积和证明生成时间都会非线性增加。你可以通过把所有公开输入和见证统一作为ConstraintSynthesizer的generate_constraints实现来精确统计。
操作意图很简单:我们要对比的是“相同的电路逻辑”在两种证明系统下的表现,所以请务必保证两边生成约束后,验证关系一致。我习惯在代码里加一个断言:先用同一个公开输入和见证,分别验证Groth16和PLONK的证明,两边都必须返回true,否则后面的benchmark数据毫无意义。
5.3 读懂结果里隐藏的细节
在你把Benchmark跑起来后,你会遇到一个很常见的现象:第一次生成PLONK证明特别慢,但后面几次会稍微快一点。这个不是玄学,第一次需要分配FFT所需的大块连续内存、计算一些中间数据结构,热身后这些开销会被复用。所以做对比实验时,我建议每组方案先跑3到5轮预热,再取后面几轮的均值,这样更贴近生产环境的真实表现。
还要关注内存峰值。PLONK证明生成时的内存占用往往比Groth16大一个量级,尤其在百万门电路上,可能从几百MB涨到几个GB。这在本地开发时不会暴露,但部署到容器、Serverless环境里,可能就是致命问题。用/usr/bin/time -v或者Rust的peak_memory工具记录一下,别等线上OOM才后悔。
最后,千万不要把“单次证明时间”当成唯一指标。如果你的业务每天要生成几万条证明,那么Prove Time乘以总量得到的TCO更高;如果你的业务主要是链上验证,那Gas成本才是第一优先级。这个思维方式的转变,比多跑几十组实验都重要。
6. 常见问题与概念混淆排查
关于PLONK和Groth16的对比,我在社区里经常看到一些混淆和误用,这里整理成一张排查速查表,希望能帮你少走弯路:
| 常见现象 / 困惑 | 根本原因 | 排查与建议 |
|---|---|---|
| 误以为“Groth16也支持通用设置” | Groth16的CRS虽可用Powers of Tau生成,但CRS内容与具体电路强绑定 | 换个电路就必须重新生成CRS,不能直接复用 |
| 觉得PLONK的证明太大了,直接否定方案 | 把Proof Size绝对化,没考虑灵活性和全局设置摊销收益 | 结合业务迭代频率和递归需求做综合评估 |
| 用同一个“域大小”运行所有PLONK电路 | 域大小过小会导致多项式插值失败或证明错误 | 按电路门数量上取到2的幂,至少留30%余量 |
| 在Groth16里强行模拟递归证明 | 电路验证器复杂且CRS逐层绑定,工程代价高 | 递归需求明确时优先考虑PLONK或Halo2 |
| 对比实验没有保持同一电路逻辑 | 两边约束数量、公开输入不一致,结果失真 | 约束生成逻辑复用同一个generate_constraints代码路径 |
| 只看证明时间,不关注内存峰值 | PLONK内存倍数于Groth16,Serverless场景容易被逼退役 | 做峰值内存记录,容量设计按3倍Groth16规划 |
| 认为PLONK的通用设置绝对安全 | “通用”不等于“免信任”,仍然必须通过仪式保证SRS安全性 | 检查SRS来源、更新轮次、文件哈希和公开审计记录 |
这张表是我和团队在项目评审中反复用到的最终检查项,基本上每条都踩过坑。尤其最后一条要重点提一下:有些团队为了省事直接从网上下载一份SRS文件就投入使用,连来源和审计记录都不核实。这是个高风险动作,通用设置只是降低了信任门槛,并没有消除信任问题,你依然要确保SRS的生成过程经得过审计。
我自己跑过很多次这两个方案的对比实验后,最大的体会是:不要被单一维度的性能指标带走。Groth16确实快、确实省gas,但它的灵活度是硬伤;PLONK的证明生成耗时和验证成本更高,但它一次设置全局使用的特性,在迭代频繁的项目里是无可替代的。这也是为什么我建议团队在起步阶段,把两个方案都写到POC里跑一遍,别只靠文档和别人的benchmark做决策。
这篇主要在讲原理和选型,下篇我会用一套真实电路实际操作,把两种方案部署到链上,贴出具体的Gas消耗和证明生成耗时数据,再聊聊递归场景里怎么选型更合适。如果你正在PLONK和Groth16之间纠结,可以先把这篇文章的思路搭好,等下一篇再对照数据做最终判断。