Dusk PLONK中的未验证评估

  • osecio
  • 发布于 2026-04-30 21:45
  • 阅读 76

文章发现Dusk Network的PLONK实现中存在严重漏洞,验证者未对四个选择器多项式评估值进行KZG打开验证,导致攻击者可伪造任意证明。该漏洞危及Dusk网络约6000万美元的市值,允许铸造任意DUSK代币和伪造屏蔽交易。类似漏洞也出现在Espresso Systems的Jellyfish实现中。Dusk和Jellyfish均已修复。文章强调标准化PLONK验证规范以预防此类bug。

Dusk's PLONK 中未经验证的求值

Dusk 的隐私层保护着约 6000 万美元的 DUSK,其安全性依赖于一个证明检查。dusk-plonk 的验证者从未验证证明者提交的四个多项式承诺,这足以凭空铸造 DUSK 并伪造网络确认为真实的屏蔽支出。

Dusk's PLONK 中未经验证的求值标题图片

承诺问题:Dusk's PLONK 中未经验证的求值

我们在 dusk-plonk 中发现了一个关键的安全性漏洞,该实现支持了市值约 6000 万美元的 Dusk Network。通过利用验证步骤中的一个缺口,恶意的证明者可以为任意虚假声明伪造可验证的证明,绕过交易电路中的每一个约束。在实时的 Rusk 网络上,这将允许铸造任意数量的 DUSK,并通过正常的 Phoenix 路径转移伪造的屏蔽资金。

根本原因在于,证明者将四个公开的选择器求值结果偷偷放入了证明结构中,而验证者在最终方程中使用了它们,却从未根据验证者密钥中的可信承诺对其进行验证。证明者可以将它们设置为任何能使方程成立的值。

PLONK 工作原理(简要)

严格的处理请参阅原始论文;以下仅涵盖理解该漏洞所需的部分。

证明者想要说服验证者,它知道满足某个计算(算术电路)的秘密输入,同时不泄露这些输入,并且生成的证明应该简短且验证迅速。

算术电路和约束

算术电路是一系列加法和乘法门连接而成。一个例子是证明我们知道椭圆曲线上的某个点 $(x,y)$,例如,证明 $y^2=x^3+7$,这里在 $\mathbb{F}_{37}$ 中。

在 $\mathbb{F}_{37}$ 中的算术电路

电路计算 $y^2-(x^3+7)$ over $\mathbb{F}_{37}$ 并检查其是否等于 $0$。

63631111abboaoababoaoboxy××7+×−0

x = 6y = 1

$q_M$ $q_L$ $q_R$ $q_O$ $q_C$ $a$ $b$ $o$
$\times (x\cdot x)$ 1 0 0 -1 0 6 6 36
$\times (x^2\cdot x)$ 1 0 0 -1 0 36 6 31
$+ (x^3+7)$ 0 1 0 -1 7 31 0 1
$\times (y\cdot y)$ 1 0 0 -1 0 1 1 1
$- (y^2-(x^3+7))$ 0 1 -1 -1 0 1 1 0
$\overset{?}{=}0$ 0 1 0 0 0 0 0 0

使用上面选择的相同见证,$x=6,;y=1$,表格列在 $\mathbb{F}_{37}$ 上生成插值多项式。

显示插值多项式

当 $F(x)$ 在所有六个插值点处为零时,$Z_H(x)=x^6-1$ 整除它,因此 $F(x)/Z_H(x)$ 同样是一个多项式。

对于当前的见证,$Z_H(x)=x^6-1$ 整除 $F(x)$。

每个门 $i$ 都有一个左输入 $l_i$、右输入 $r_i$ 和输出 $o_i$。证明者的工作是表明它知道满足每个门的电路线值。

每个门都施加一个约束,而 PLONK 使用作为开关的 选择器 值将所有门类型统一到一个表达式中:设置 $q_M=1$ 使该行成为乘法门,设置 $q_L=1$ 使其贡献一个加法项,依此类推。选择器值定义了电路的形状并且是公开的,证明者和验证者都知道,而电路线值是证明者的秘密见证。这种逐行检查并不确保门之间的连线一致(即一个门的输出等于下一个门的输入);PLONK 为此使用了一个单独的 置换论证,我们在此不讨论。

从多个检查到一个

PLONK 不是单独检查每个门,而是逐列读取执行轨迹,并使用 FFT 插值将每个值数组转换为单个多项式。电路线值成为 见证多项式 $f_L(x)$、$f_R(x)$、$f_O(x)$,选择器成为 选择器多项式 $Q_M(x)$、$Q_L(x)$ 等,所有多项式都在一个 $n$ 次单位根的域 $H$ 上进行插值。在 $i$ 次根处求值 $f_L(x)$ 可恢复第 $i$ 行的左电路线值。

交互式多项式插值

玩具电路 $(x+2y)z=0$

2-5-83-24xyz+×0

$q_M$ $q_L$ $q_R$ $q_O$ $q_C$
$+ (x+2y)$ 0 1 2 -1 0
$\times ((x+2y)\cdot z) \overset{?}{=} 0$ 1 0 0 0 0

这与上面的行到多项式步骤相同,但在实数上,域为 ${-1, 1}$。

选择器行插值得到 $Q_M(x)=\tfrac{1}{2}x+\tfrac{1}{2},;Q_L(x)=\tfrac{1}{2}-\tfrac{1}{2}x,;Q_R(x)=1-x,;Q_O(x)=\tfrac{1}{2}x-\tfrac{1}{2},;Q_C(x)=0$。

移动 $x,;y,;z$。关键在于观察当 $(x+2y)z=0$ 时,我们得到 $F(-1)=0$ 和 $F(1)=0$,因此 $Z(x)=x^2-1$ 整除 $F(x)$。

移动 $x$,$y$,$z$

$x = A(-1)$ $y = B(-1)$ $z = B(1)$

$A(x) = -5x - 3$

$B(x) = 4x - 1$

$O(x) = -8x - 16$

$F(x) = -10x^{3} - 19x^{2} - 2x + 7$

$Z(x) = x^2 - 1$

$Z(x)\nmid F(x),;\text{所以 }F(x)/Z(x)\text{ 不是多项式}$

$A(x)$ $B(x)$ $O(x)$ $F(x)$ $F(x)/Z(x)$

由于所有列现在都是多项式,整个电路压缩成一个单一的主约束多项式 $F(x)$,它结合了选择器和见证。如果证明者是诚实的,那么 $F(x)=0$ 在域中的每个行索引处成立。消逝多项式 $Z(x)=x^n-1$ 恰好在这些点上为零,因此如果所有约束都成立,那么 $Z(x)$ 整除 $F(x)$,得到一个商多项式 $T(x)$,满足 $F(x)=T(x)\cdot Z(x)$。

master_equation

多项式承诺和打开证明

为了保持证明简短,证明者不直接发送多项式。相反,它发送 承诺,即每个多项式的短密码学指纹(例如,使用 KZG 承诺)。当验证者需要在特定点处获得承诺多项式的值时,证明者提供该值以及一个 打开证明,以证明声称的值与先前的承诺一致。

因此,一个承诺的多项式求值在密码学上是绑定的,证明者无法在不被发现的情况下撒谎。

简化为单个随机点

在证明者承诺所有多项式(包括 $T(x)$)之后,验证者选择一个随机挑战点 $z$(通过 Fiat-Shamir 启发式从交互迹导出),并在该单点上检查 $F(z)=T(z)\cdot Z(z)$。根据 Schwartz-Zippel 引理,如果这在随机 $z$ 处成立,那么完整的多项式恒等式以压倒性的概率成立,因此验证者可以在常数时间内检查整个数百万行的电路。

在教科书式的 PLONK 中,选择器多项式是固定电路描述的一部分,但在实践中,实现在预处理期间承诺它们,并将这些承诺放入验证者密钥中。当验证者稍后需要它们在 $z$ 处的值时,证明者提供 求值声明,这些声明必须通过打开证明针对那些承诺进行检查。

安全论证依赖于一个链条:承诺在 推导出挑战 之前将证明者锁定到多项式上,而打开证明确保求值与这些承诺一致。打破这个链条中的任何一个环节都会完全破坏可靠安全性。

验证者实际可以信任什么

对于这个漏洞,一个不变量比其他所有都重要:进入最终验证者方程的每个标量必须要么由验证者本地计算,要么通过密码学方式绑定到先前的承诺

在实践中,进入验证者方程的值分为三类。验证者从公共数据本地计算一些值($Z_H(z)$、$L_1(z)$、公共输入多项式在 $z$ 处的值),这些是安全的,因为证明者从未选择它们。其他值是证明者提供的求值,并附有 KZG 打开证明($a(z)$、$b(z)$、$\sigma_1(z)$、$a(z\omega)$),这些是安全的,因为打开将它们绑定到先前承诺的多项式。第三类是由验证者密钥中的承诺直接在线性化多标量乘法中使用($[q_M]$、$[q_O]$、$[\sigma_4]$),这些是安全的,因为验证者从不信任这些项的裸域元素;它使用的是承诺本身。

任何不属于这三类的项都会被构造为攻击者控制的。


dusk-plonk 与教科书式 PLONK 的区别

dusk-plonk 并非 2019 年 PLONK 论文的直接转录。它使用第四条电路线 $d$ 扩展了算术门,添加了用于范围、逻辑和椭圆曲线操作的自定义组件,使用了 $z\omega$ 处的移位求值,并大量批处理 KZG 打开。在现代 PLONK 标准下,这些都不算奇特,但它确实使验证者比论文中的最小表述更难推理。

对于这个漏洞来说,重要的部分是 公共电路数据证明者关于该数据在随机挑战点处的声明 之间的边界。其他实现通过将选择器多项式严格排除在证明者控制之外来避免这种歧义。例如,Consensys 的 gnark(部署最广泛的 PLONK 实现之一)从不要求证明者提供任何选择器求值。相反,验证者将选择器承诺 $Q_l, Q_r, Q_m, Q_o, Q_k$ 直接纳入线性化多标量乘法,从而通过构造确保它们的值在密码学上是绑定的。

Dusk 的自定义组件更复杂(将选择器与其他求值项相乘),因此它们不能仅仅使用承诺的简单线性组合。它们的架构需要在 $z$ 处求值选择器并使用这些标量。但是,虽然他们将这四个选择器求值序列化到证明结构中,但他们从未通过打开证明根据验证者密钥中的承诺实际验证它们。

查看此漏洞的最短方式是通过下面的图表:安全的值通过打开路径流向最终的配对检查,而红色的选择器流进入验证者逻辑,却从未触及打开证明。

验证者依赖关系图

实际流入最终检查的是什么?

点击一个证明值或承诺以跟踪其在验证者中的切片。安全的值在通往配对检查的路上为打开累加器提供数据;红色的选择器流显示验证者消耗的值,却从未打开。

commit 82c08e8f11f2red = 已被消耗但未打开

选择的变量邻居1 跳2 跳3 跳过滤种类

证明求值证明承诺VK 承诺转录挑战/标量检查/恒等式聚合/配对_sigma_1_evals_evalsigma1s_sigma_2_evals_evalsigma2z_evalz_evals_sigma_3_evals_evalsigma3a_w_evala_w_evalb_w_evalb_w_evala_evala_evalb_evalb_evalc_evalc_evald_evald_evald_w_evald_w_evalq_l_evalq_l_evalq_r_evalq_r_evalq_c_evalq_c_evalq_arith_evalqarithevalq_mq_mq_lq_lq_rq_rq_oq_oq_fq_fq_cq_c吸收求值声明absorbevaluationclaimsv_challengev_challengeE_evalsE_evals算术恒等式arithmeticidentityE_scalaruE_scalarDZ_H(z)zD


Dusk 如何使用 PLONK

Dusk Network 是一个专注于隐私的 L1 区块链。其交易模型有两种模式:

  • Phoenix(屏蔽):金额和参与者使用 ZK 证明隐藏,每笔 Phoenix 交易都带有一个证明交易有效的 PLONK 证明。
  • Moonlight(透明):基于账户的标准交易,由 BLS 签名验证,不涉及 PLONK。

在节点层面,每一笔 ProtocolTransaction::Phoenix 在预验证期间都会经过 verify_proof_with_version()。如果该 PLONK 证明验证通过,则交易被接受到内存池,并可随后被挖入区块。Moonlight 路径的交易则通过 BLS 签名验证。

同样的 Phoenix 证明路径涵盖的不仅仅是简单的屏蔽转账。Phoenix 路径的质押、奖励提取、解除质押以及 Phoenix 到 Moonlight 的转换,都通过 phoenix() 构建一笔 Phoenix 交易,例如在 phoenix_stake()phoenix_stake_reward()phoenix_unstake()phoenix_to_moonlight() 中。因此,如果 Phoenix 证明验证不可靠,整个屏蔽交易路径都会暴露。

phoenix_moonlight

PLONK 实现 dusk-plonk 是 Dusk 团队的一个独立库。它是最早的 PLONK 实现之一,开发始于原始论文发布的同一年。

Phoenix 交易 PLONK 电路定义在这里。该电路强制执行以下一组约束:

电路检查 被检查的声明
Merkle 树成员资格 每个输入笔记哈希都针对公共 Merkle 根打开,因此只有笔记树中已存在的笔记才能被花费
输入笔记密钥授权 证明者知道控制每个输入笔记的私钥
无效器正确性 每个无效器与相应的笔记密钥和位置匹配
输出值承诺正确性 每个公共输出承诺与秘密输出值和盲因子匹配
余额完整性 $\sum \text{输入} = \sum \text{输出} + \text{费用} + \text{存款}$
输入范围检查输出 所有笔记值都在 $[0, 2^{64}-1]$ 范围内
发送者签名 交易负载由发送者的两个签名密钥组件签名
发送者加密正确性 附加到每个输出笔记的发送者数据是在接收者笔记密钥下的正确 ElGamal 加密

Rusk 不会逐个消费这些声明。它通过 verify_proof_with_version() 消费 tx.public_inputs() 上的单个有效/无效证明裁决。

PLONK 中的可靠性破坏会同时使所有这些约束无效,因为伪造的选择器求值会使整个电路不受约束,而不是针对任何单一检查。


漏洞

PLONK 验证中,验证者将多项式求值批处理到一个单一的 KZG 打开证明检查中。此批处理中包含的求值(通过 E_evals 提交)是:

  • a_eval, b_eval, c_eval, d_eval (见证)
  • s_sigma_1_eval, s_sigma_2_eval, s_sigma_3_eval (置换)
  • a_w_eval, b_w_eval, d_w_eval (移位见证)
  • z_eval (置换累加器)

但是以下选择器求值 没有 被包含在内:

  • q_arith_eval (算术选择器)
  • q_c_eval (常量选择器)
  • q_l_eval (左选择器)
  • q_r_eval (右选择器)

证明者将四个选择器求值放在证明结构中。验证者将它们吸入转录中,并且组件验证者代码直接在线性化检查中使用它们( 证明结构转录吸收算术组件固定基 ECC 组件)。但它们从未根据验证者密钥中对应的选择器承诺进行检查,即使这些承诺已经存在。证明者发送它想要的任何值,验证者就信任它们。

要理解为什么这四个遗漏是特殊的,最简单的方法是将它们与两个 不是 漏洞的邻近情况进行对比:

  • 根本没有证明者提供的 c(zω) 字段。ProofEvaluations 包含 a_w_evalb_w_evald_w_eval,但没有 c_w_eval,因此验证者从不消费未绑定的 c(zω) 声明( 证明结构)。
  • 验证者密钥中存在第四个置换承诺 $[\sigma_4]$,但验证者在线性化 MSM 中使用承诺本身,而不是信任证明者提供的标量 $\sigma_4(z)$( 置换验证者密钥)。

这四个选择器求值不符合这两种安全模式:它们是证明者提供的标量,被验证者代码直接使用,并且从未出现在 E_evals 中,这使得主方程约束不足。

structural_trust_boundary


利用

由于选择器求值是自由变量,验证方程变成了一个证明者事后可以求解的线性方程。

证明者承诺任意的见证多项式(无需有效的见证)和任意的商多项式(小的随机线性多项式就足够了)。它遵循诚实的协议完成所有承诺轮次,推导出与验证者将看到的相同挑战。在看到 z_challenge 之后,它计算出线性化多项式为了通过配对检查 应该 求值为什么,然后求解出 q_arith_eval,即使验证方程平衡的单个自由变量(设置 q_c_eval = q_l_eval = q_r_eval = 0)。

exploit_algebra

为了实现这一点,可以计算所有选择器设为零时的线性化多项式 $r(x)$,在 $z$ 处求值,并与目标值比较;差值除以 q_arith_eval 的系数,通过一次域除法即可得出所需的值。


对 Dusk Network 的影响

PLONK 是 Phoenix 特定正确性声明的唯一把关者:笔记成员资格、所有权、笔记承诺、发送者签名和余额完整性完全编码在电路中。Rusk 在验证证明之前确实会检查其他前提条件,例如无效器的唯一性(预验证路径),但对于证明内部的声明,没有第二验证路径。通过伪造的证明,攻击者可以:

  1. 通过制造笔记树中不存在的具有任意值的输入笔记来膨胀代币供应。伪造的证明使网络相信这些笔记是真实的,攻击者凭空铸造 DUSK,随时可以转移到诚实用户或交易所。
  2. 伪造支出,绕过通常使 Phoenix 输入笔记有效的所有权、成员资格和余额检查。
  3. 通过诚实的钱包移动伪造的屏蔽资金,因为一旦伪造的 Phoenix 交易被接受,所产生的屏蔽输出在协议层面上与合法的 Phoenix 输出无法区分。

我们在本地 Dusk 测试网上通过一个完整的端到端概念验证展示了这一点:

  1. 设置一个单一的诚实 Rusk 节点,并创建两个钱包(诚实和恶意),余额均为 0
  2. 恶意钱包伪造一个 PLONK 证明,凭空创建 2000 DUSK
  3. 恶意钱包使用正常的(诚实证明的)交易向诚实钱包转账 1337 DUSK
  4. 诚实节点验证这两笔交易并将其挖掘到区块中
  5. 诚实钱包显示确认余额为 1337 DUSK

end_to_end

在发现时,DUSK 的市值约为 ~6000万美元。整个屏蔽交易层处于风险之中。由于 Phoenix 是保护隐私的,被接受到屏蔽池中的伪造输出事后将难以区分,类似于 Neptune Cash 的 Triton VM 漏洞


修复

修复方法是将四个选择器求值添加到 KZG 批量打开检查中,以便根据验证者密钥中已存在的选择器承诺进行验证:

  • 在证明者端扩展 compute_aggregate_witness,使其也包含 q_arithq_cq_lq_r
  • 在验证者端将它们添加进 E_evals,以便根据验证者密钥中的承诺进行检查

这项工作在 commit 645265b7 中完成,该提交于 2026 年 2 月 14 日落地。


为何被遗漏?

Dusk 的堆栈经过了大量审计:一份 2023 年 12 月对 dusk-plonk 的审计、一份 2024 年 9 月对 Phoenix 的审计,以及一份 2024 年 9 月 Oak Security 对 Rusk 节点库的审计。Dusk 的公开 审计概览 总结了更广泛的审计项目。该漏洞仍然未被注意到,因为它隐藏在一个非常容易产生的思维模型错误之后。

在多项式层面,选择器是公开的电路描述。一个牢记标准 PLONK 模型的审查者自然会将“选择器在验证者端”铭记于心,然后继续工作,从而忽略了 Dusk 验证者开始消费证明者提供的选择器 求值 这一架构偏差。

这是一个纯粹的证明系统漏洞,而不是 Phoenix 电路的漏洞;电路约束本身是正确的。失败完全是由于验证者接受了绕过先前建立的基本不变量的证明字段:它们既不是本地计算的,也没有通过密码学方式绑定到打开证明。

对此类漏洞的检查是机械性的:列举证明的求值结构中的每个字段,并验证每个字段要么出现在打开证明批处理中,要么由验证者本地计算。

Espresso Systems 的 Jellyfish 中的类似漏洞

在研究 PLONK 实现时,我们发现 Espresso Systems 的 jf-plonk 中存在一个类似的漏洞。具体机制不同,但其利用同样归结为用于最终检查的变量未经密码学绑定。

Jellyfish 实现了 UltraPlonk,它通过 Plookup 查找参数扩展了标准 PLONK。Plookup 向证明中添加了 15 个多项式求值。函数 append_plookup_evaluations 本应在批处理挑战 $v$ 被推导之前将所有 15 个添加到 Fiat-Shamir 转录中。相反,它只添加了 15 个中的 6 个,而剩余的 9 个求值被用于批处理验证检查,但不影响 $v$,因此证明者可以在事后调整它们以使检查通过。

攻击需要修改一个求值 (key_table_next_eval) 乘以 delta / (u * v^3) 以缩小真实与期望批处理求值之间的差距,这与 Dusk 的利用类似,归结为一次域除法。

据我们所知,Jellyfish 的 UltraPlonk 模式目前未在生产环境中部署。PR #867 修复了该问题,并于 2026 年 3 月 18 日标记为 jf-plonk-v0.8.0


走向标准化

两个独立的 PLONK 实现包含同一类漏洞,并且 类似的模式出现在 zkVM 中,这表明这不是仅靠单独审计就能解决的问题。上述检查(对比“使用的求值”与“绑定的求值”)是机械性的,可以构建到开发工具、CI 管道或标准化的 PLONK 验证规范中。

我们正在与 Dusk 团队和其他利益相关者进行早期讨论,探讨 PLONK 标准化工作的可能形式:一份与曲线无关、与后端无关的验证协议规范,使诸如求值绑定之类的不变量变得明确且可检查。

现状是每个团队都根据论文实现自己的 PLONK 变体,并希望审计者能捕捉到他们遗漏的内容,这很脆弱。一个共享的、经过充分审查的验证规范将减少这些漏洞的暴露面,并为审计者提供一份具体清单来对照验证。

披露时间线

日期 事件
2026-02-13 Dusk 漏洞报告
2026-02-14 Dusk 确认
2026-02-14 Dusk 修复提交
2026-02-27 公开的 dusk-rusk-1.6.0 版本发布
2026-03-16 Jellyfish 修复 PR 已开启 ( #867)
2026-03-18 Jellyfish 修复已合并至 #867 并标记为 jf-plonk-v0.8.0

致谢

我们感谢 Dusk 团队在一天内做出响应,透明地协调修复工作,并参与更广泛的标准化讨论。我们也感谢 Espresso Systems 团队在一周内完成了 Jellyfish 的补丁。

  • 原文链接: osec.io/blog/2026-04-29-...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
osecio
osecio
Audits that protect blockchain ideas.