PLONK是一种通用零知识证明协议,属于SNARK家族。它不依赖电路特定的可信设置,而是通过统一的预处理方案支持任意算术电路。与早期方案把每个电路映射到独立多项式不同,PLONK把电路约束编码为多项式恒等式,使证明体积与验证时间保持较小。其核心组件包括KZG多项式承诺、排列论证和Fiat-Shamir变换。本文将拆解它的约束系统、证明流程以及工程实现要点。

PLONK的算术化与约束系统
PLONK把任意计算先转化为算术电路。电路中每条线对应一个值,门分为加法门和乘法门,也可以扩展为自定义门。假设电路有n个门,索引从1到n。每个门接收左输入a_i、右输入b_i,产生输出c_i。PLONK使用选择器多项式区分门类型:qL表示左输入系数,qR表示右输入系数,qO表示输出系数,qM表示乘法项系数,qC表示常数项。最终的门约束可以写成 qL_i * a_i + qR_i * b_i + qO_i * c_i + qM_i * a_i * b_i + qC_i = 0。这样加法门、乘法门和公开输入都被纳入同一个等式。
门约束只保证单个门内部关系正确,还不能约束跨门的线值一致性。例如第2个门的左输入可能来自第5个门的输出,二者在语义上必须是同一个变量。PLONK通过复制约束解决这个问题。它引入置换多项式sigma,把所有相同线值的索引映射到同一轨道。证明者需要证明经过置换后的线值序列与原序列相等,这个性质通过累加器多项式和一个大乘积检验完成。复制约束是PLONK区别于早期方案的关键设计,它避免为每条线单独设置连接关系,极大降低了算术化复杂度。
# 门约束构建示例:out = x * y + z
# 第一个门是乘法门,第二个门是加法门
# 每个门使用选择器描述约束形式
gates = []
# 乘法门:qL=0, qR=0, qO=-1, qM=1, qC=0
gates.append({
"qL": 0, "qR": 0, "qO": -1, "qM": 1, "qC": 0,
"a": "x", "b": "y", "c": "tmp"
})
# 加法门:qL=1, qR=1, qO=-1, qM=0, qC=0
gates.append({
"qL": 1, "qR": 1, "qO": -1, "qM": 0, "qC": 0,
"a": "tmp", "b": "z", "c": "out"
})
# 验证门约束:qL*a + qR*b + qO*c + qM*a*b + qC == 0
for g in gates:
# 在真实协议中,a、b、c 是线值,而不是变量名
# 这里仅展示选择器与线值的对应关系
pass
实际实现中,线值会被编号并映射到乘法子群的元素上。定义域通常选择有限域上的单位根群,以便使用快速傅里叶变换进行多项式插值。门选择器也以多项式形式提交,验证者可以在任意挑战点计算约束关系。这样就把离散门约束转化为多项式恒等式,后续使用多项式承诺保证完整性。
多项式承诺与证明生成流程
PLONK的可信设置生成一个结构化引用字符串,也就是KZG承诺的公共参数。它包含群元素G、tau*G、tau^2*G等,用于对多项式进行隐藏和开证明。由于这组参数与具体电路无关,因此被称为通用设置。同一组SRS可以复用于任意规模不超过某个上限的电路。需要强调,通用不等于无需信任,它仍然依赖初始的tau被安全丢弃,但可以通过多方计算实现可更新设置,降低单点信任风险。
证明生成时,证明者首先根据实际线值和门选择器构造多个多项式:左线值a(x)、右线值b(x)、输出线值c(x)、排列累加器z(x)以及商多项式t(x)。商多项式来自门约束等式,它等于左侧约束除以消失多项式Z_H(x)。复制约束单独由z(x)的递推关系描述。证明者对这些多项式做KZG承诺,然后通过Fiat-Shamir变换生成随机挑战点zeta。随后证明者计算每个多项式在zeta处的值,并生成对应的打开证明。最终提交的证明包含若干群元素和域元素。
# 验证者侧简化逻辑:检查门约束与复制约束
# 注意:真实实现使用配对运算而不是直接求值
def verify_proof(proof, vk, public_inputs):
# 1. 检查承诺是否在群上
assert is_on_curve(proof.a_commit)
assert is_on_curve(proof.b_commit)
assert is_on_curve(proof.c_commit)
assert is_on_curve(proof.z_commit)
assert is_on_curve(proof.t_commit)
# 2. 从证明和公开输入派生挑战点
zeta = derive_challenge(proof, public_inputs)
# 3. 检查门约束多项式等式
lhs = evaluate_gate_constraint(proof, zeta)
rhs = evaluate_vanishing_polynomial(zeta) * proof.t_eval
assert lhs == rhs, "gate constraint check failed"
# 4. 检查复制约束的置换关系
assert check_permutation(proof, zeta), "permutation check failed"
return True
验证者不需要知道完整的多项式,只拿到承诺和点值。通过KZG配对,可以在不知道多项式系数的情况下验证承诺与点值一致。配对检查保证打开证明正确。PLONK验证复杂度通常是对数级别或常数级别,具体取决于实现细节。很多优化版本会把多个打开合并为一次配对,以减少验证成本。对于区块链场景,验证者计算量直接关系到交易费用,因此工程上会重点优化配对次数。
PLONK与Groth16的差异及工程实践
Groth16是目前证明体积最小、验证最快的SNARK之一,但它的可信设置与电路结构强绑定。每更换一次逻辑,都要重新执行一轮多方计算,这给高频迭代项目带来明显负担。PLONK把设置与电路解耦,同一个SRS可支持不同电路,显著降低了部署成本。代价是PLONK的证明通常比Groth16大,验证端配对运算也更多。选择哪个协议需要看应用场景:如果电路固定且追求极致链上开销,Groth16更合适;如果需要频繁升级或者希望复用设置,PLONK更灵活。
工程实践中,PLONK的性能还可以通过自定义门和查表论证进一步提升。自定义门把复杂运算如哈希、椭圆曲线加法、范围检查直接写成专用约束,减少总门数。查表证明则把非代数运算比如位运算或哈希压缩映射到预计算表,通过证明元素在表中来降低电路复杂度。zkSync等扩容方案采用PLONK类证明处理大批量交易,Plonky2则结合FRI多项式承诺,避免KZG对有限域和可信设置的依赖。递归证明还可以把多个PLONK证明压缩成一个,适合聚合场景。
常见误区包括认为PLONK不需要可信设置、证明一定比Groth16慢、或者所有SNARK都只能处理算术电路。实际上PLONK仍有预处理阶段的秘密参数,只是它更通用;性能差异随优化路径变化,部分实现已经接近Groth16;而自定义门和查表可以把很多非算术逻辑转化为约束。理解这些边界,有助于在隐私计算、Layer 2扩容和链下计算验证中做出合理选择。对于开发者,建议从公开的Rust或Python实现入手,先跑通最小电路,再逐步理解排列论证和商多项式的构造,避免一开始陷入配对运算细节。