在密码学社区里被问得最多的问题之一就是“做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要高不少但它实现了一个关键能力通用可信设置也就是说整个生态只需要做一次可信设置仪式所有电路都能用。为了直观先上一张我常用的对比表后面内容基本都围绕这张表展开对比维度Groth16PLONK可信设置每个电路都需要单独可信设置通用且可更新的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的电路表达方式基于R1CSRank-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 Time4.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万gasPLONK则普遍在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-plonkAPI风格统一能让我们少踩很多坑。下面是一个最小化的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: DemoCircuitE) { 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: DemoCircuitE) { 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: PlonkProofE 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_setupPLONK走的是setup加全局UniversalParams这个API区别本身就能体现两种方案的特点。5.2 关键参数怎么设置域大小domain size是PLONK最容易设置错的地方它决定SRS能支持的最大多项式次数。计算公式是域大小要至少大于等于电路中所有门数量的2倍通常还要向上取到2的幂。举例来说你的DemoCircuit有15万个门那域大小至少要到2^18262144保险起见我用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内存倍数于Groth16Serverless场景容易被逼退役做峰值内存记录容量设计按3倍Groth16规划认为PLONK的通用设置绝对安全“通用”不等于“免信任”仍然必须通过仪式保证SRS安全性检查SRS来源、更新轮次、文件哈希和公开审计记录这张表是我和团队在项目评审中反复用到的最终检查项基本上每条都踩过坑。尤其最后一条要重点提一下有些团队为了省事直接从网上下载一份SRS文件就投入使用连来源和审计记录都不核实。这是个高风险动作通用设置只是降低了信任门槛并没有消除信任问题你依然要确保SRS的生成过程经得过审计。我自己跑过很多次这两个方案的对比实验后最大的体会是不要被单一维度的性能指标带走。Groth16确实快、确实省gas但它的灵活度是硬伤PLONK的证明生成耗时和验证成本更高但它一次设置全局使用的特性在迭代频繁的项目里是无可替代的。这也是为什么我建议团队在起步阶段把两个方案都写到POC里跑一遍别只靠文档和别人的benchmark做决策。这篇主要在讲原理和选型下篇我会用一套真实电路实际操作把两种方案部署到链上贴出具体的Gas消耗和证明生成耗时数据再聊聊递归场景里怎么选型更合适。如果你正在PLONK和Groth16之间纠结可以先把这篇文章的思路搭好等下一篇再对照数据做最终判断。