Orchard 伪造漏洞:深度代码剖析
2026 年 5 月 29 日,安全研究员 Taylor Hornby 在 Zcash 的 Orchard 隐私池中发现了一个极其致命的正确性漏洞。这个 bug 自 2022 年 5 月 Orchard 激活以来就一直存在,理论上攻击者可以在不被察觉的情况下无限量地伪造 ZEC 代币。虽然该漏洞在 6 月 3 日通过紧急网络升级(NU6.2)完成修补,但整个事件无疑是零知识电路安全领域里一堂极其昂贵的实战课。
下面我从开发者的视角,把这个漏洞的来龙去脉彻底拆解一遍:根因是什么、哪行代码写错了、如何利用、以及最终的修复方案到底改了啥。
电路本应做什么:密码学约束的目标
要理解这个 bug,首先得搞清楚 Orchard Action 电路到底要验证哪些东西。当用户花费一个隐私 note 时,电路必须证明:
- 花费者确实拥有该 note(由花费验证密钥
ak控制) - 空值
nf必须由空值密钥nk正确推导出来 - 多样化地址
(d, pk_d)是合法的,并且与正确的传入查看密钥ivk绑定
其中最关键的一层绑定关系是:ivk = Commit(ak, nk, rivk)。多样化的基点 g_d = DiversifyHash(d) 与 pk_d 共同构成地址。电路必须强制 nf 的推导过程与正确的 ivk 和 pk_d 保持密码学绑定。
而这次漏洞,恰恰在电路层面打破了这一绑定。
根因:一个字符之差,酿成滔天大祸
bug 出在 halo2_gadgets 库中的变基标量乘法电路(variable-base scalar multiplication gadget)。
文件路径: halo2_gadgets/src/ecc/chip/mul/incomplete.rs
有漏洞的代码(概念化):
rust
// 当时代码实际做的(错误的):
let base_point = region.assign_advice(|| "base", config.base_column, offset)?;
正确的代码(修复后):
rust
// 应该做的:
let base_point = region.copy_advice(|| "base", config.base_column, offset)?;
两者区别极其细微,但后果天差地别:
assign_advice()只是往一个单元格里写入数值,并不强制该值与其他地方的副本保持任何关系。copy_advice()则会创建一条复制约束,强制当前单元格的值必须等于另一个源单元格的值。
变基标量乘法电路的本意是使用真正的多样化基点 g_d 作为输入。但因为用了 assign_advice() 而非 copy_advice(),内部的基点变量从未被锚定到外部传入的实际基点上。
整个电路的约束系统里,压根没有这样一条等式约束:
internal_base_point == actual_g_d
这意味着恶意证明者可以在生成证明时,随意为基点提供任意坐标。乘法校验本身依然能通过,因为电路只是在验证标量乘法是否与给定的基点匹配------既然这个基点没被约束为正确的那个,证明者就可以选择任何他们想要的基点。
后果:为什么这能打破空值与地址的绑定
Orchard Action 电路在多样化地址完整性检查中,会用到这个标量乘法。该检查要求:
pk_d == [ivk] g_d
其中 [ivk] g_d 表示用传入查看密钥 ivk 对多样化基点 g_d 做标量乘法。
当标量乘法电路未能约束基点为 g_d 时,恶意证明者就能:
- 用一个任意选定的基点
g'来代替g_d - 选一个任意标量
s,使得pk_d == [s] g' - 生成一个看似合法的证明,声称该 note 属于地址
(d, pk_d)
电路会接受这个证明,因为标量乘法校验通过了------只不过它校验的是错误的基点。
而空值的推导依赖于 nk 和其他 note 组件。一旦 pk_d、g_d、ivk 和 nk 之间的绑定被打破,攻击者就能花费他们并不真正拥有的 note。由于推导出的空值与真实 note 的空值不一致,同一个 note 可以被反复花费而不被察觉。
攻击手法:如何凭空造出无限 ZEC
Taylor Hornby 与 Anthropic 的 Claude Opus 4.8 合作,构造了一个完整的可运行利用代码,并在本地 regtest 环境中成功测试。
攻击流程大致如下:
第一步: 攻击者选定一个想要伪造的目标 note。该 note 有合法的空值 nf,正常花费时会公开发布。
第二步: 攻击者并不去证明自己拥有该 note(即正确的 nk 和 g_d),而是往标量乘法电路里塞入一个任意基点 g'。
第三步: 攻击者选择一个标量 s,使得 pk_d == [s] g' 成立,其中 pk_d 是攻击者自己控制的地址。因为基点没有被正确约束,这个检查顺利通过。
第四步: 电路生成了一个看似合法的 Orchard Action 证明,表面上是在花费一个合法的 note。但由于使用了攻击者自己选的参数,该证明计算出的空值与原始 note 的空值不同。
第五步: 网络验证该证明并通过。原始 note 并未被真正花费(其空值从未发布),而攻击者伪造的 note 却进入了流通。
整个过程可以无限重复,最终造成无限增发 ZEC。
由于 Orchard 交易是完全隐私的,链上没有任何签名或透明痕迹能检测到这种伪造。外部观察者完全无法发现异常。
修复方案:一行改动,全网升级
修复方法在概念上非常简单:把变基标量乘法电路里的 assign_advice() 替换成 copy_advice()。
修复前(有漏洞):
rust
// 在 halo2_gadgets/src/ecc/chip/mul/incomplete.rs 中
let base_point = region.assign_advice(
|| "base",
config.base_column,
offset
)?;
// 没有任何约束把 base_point 绑定到真正的外部基点上
修复后(已修补):
rust
// 在 halo2_gadgets/src/ecc/chip/mul/incomplete.rs 中
let base_point = region.copy_advice(
|| "base",
config.base_column,
offset
)?;
// copy_advice() 强制 base_point 必须等于源单元格的值
copy_advice() 会在 halo2 电路中生成一条复制约束(copy constraint)。这迫使内部使用的基点必须与外部传入的多样化基点 g_d 完全一致。
为什么这样就有效:
- 等式约束现在成为了电路约束系统的一部分
- 任何试图使用不同基点的证明者都会在约束检查时失败
- 空值推导被正确地绑定到了实际花费的 note 上
- 多样化地址完整性检查强制要求
pk_d == [ivk] g_d,且g_d必须是正确的
网络升级过程:
由于修改了 Orchard 电路的验证密钥,必须通过一次网络升级来部署。时间线如下:
- 2026 年 5 月 29 日: Taylor Hornby 发现漏洞并秘密报告给 Zcash Open Development Lab(ZODL)
- 2026 年 6 月 1 日: 紧急修复完成
- 2026 年 6 月 2 日: Zebra 4.5.3 发布,通过软分叉暂时禁用了 Orchard 动作
- 2026 年 6 月 3 日: NU6.2 硬分叉在区块高度 3,364,600 激活,重新启用 Orchard,并应用了修正后的电路
为什么这个 bug 能潜伏四年之久
从 2022 年 5 月到 2026 年 5 月,这个漏洞一直没有被发现,背后有几个原因:
多轮专业审计全部漏掉
Orchard 代码经过了全球顶尖密码学家的多次审计。然而漏洞依然坚挺。这恰恰说明 ZK 电路 bug 的隐蔽性------少了一条约束,不会导致编译错误,也不会引发运行时崩溃,它只会以"正确性漏洞"的形式潜伏着,等待恶意证明者去利用。
错误藏在依赖库里
bug 位于 halo2_gadgets 这个库中,而非 Orchard 本身的代码。这导致审计时更容易被忽视,因为大家的目光都集中在 Orchard 的实现上,而对底层密码学 gadget 的检查不够深入。偏偏这个 gadget 是许多协议共同依赖的基础组件。
最终还是 AI 辅助审计揪了出来
Taylor Hornby 使用了一套自定义分析框架,结合 Anthropic 的 Claude Opus 4.8 模型。AI 模型能够系统性地执行电路级检查,自动识别出约束不足的关系。Hornby 不仅仅是收到一个警告,他还借助 AI 写出了一个完整的可利用证明。
教训很清楚:即便经过最严格审查的密码学代码,也可能藏有致命漏洞。持续、AI 增强的安全审计,已经不是"可选项",而是"必选项"。
无法回避的未知:到底有没有被利用过?
由于 Orchard 交易完全私密,我们无法从密码学上判断该漏洞在修补之前是否已经被利用过。
这就带来一个令人不安的现实:Zcash 的总供应量可能已经被悄悄增发了,而我们无从知晓。Shielded Labs 承认了这种不确定性,同时也表示谨慎乐观------他们认为实际被利用的可能性不大。
下一步计划是通过一次网络升级,让任何人都能通过密码学方式证明 Zcash 供应量的完整性。这样就能给出确凿证据,证明到底有没有发生过伪造。
给开发者的核心启示
ZK 电路与普通代码完全是两码事
在传统编程里,我们通过运行代码并检查输出来测试正确性。而在 ZK 电路中,你必须证明所有可能的输入都能满足约束。漏掉一条约束,就相当于漏掉了一条断言------只不过没有任何运行时环境会帮你捕捉到它。
assign_advice() 与 copy_advice() 的区分至关重要
在 halo2 中,assign_advice() 只是给一个单元格赋值。而 copy_advice() 不但赋值,还强制要求该值必须与源单元格相等。如果你需要某个值在电路中与另一个值保持密码学绑定,就必须使用 copy_advice(),或者显式添加等式约束。
不仅要审计自己的代码,更要审计依赖
漏洞位于 halo2_gadgets,这是 Orchard 依赖的底层库。很多审计团队只盯着 Orchard 电路本身,却忽略了关键问题出在依赖库里。依赖审计的地位和自身代码审计一样重要。
AI 辅助审计是增效利器
这个 bug 躲过了四年的人类审计,最终被人类+AI 的组合发现。人类专家的领域知识,加上 AI 系统性地探索约束空间的能力,两者结合才取得了决定性突破。
正确性漏洞(Soundness Bug)是 ZK 体系里最危险的一类
完备性漏洞(有效证明被拒绝)最多算个麻烦。而正确性漏洞(无效证明被接受)则是灾难性的。这次漏洞就属于最恶劣的那种------允许任意创造价值且无法被检测。
结语
Zcash Orchard 伪造漏洞是一记沉重的警钟:哪怕经过最严格审计的密码学系统,也可能藏着致命缺陷。根源仅仅是一个函数调用------assign_advice() 代替了 copy_advice()------出现在标量乘法 gadget 里。这一个字符的差异,就破坏了多样化地址与空值之间的密码学绑定,理论上可能导致无限且无法检测的伪造。
修复只需要改动一行代码。但要部署这行代码,却需要一次不简单的全网升级。而该漏洞是否曾被利用,至今仍是个悬而未决的疑问。
对于所有在 ZK 电路上构建应用的开发者,这件事留下了一个朴素的教训:敬畏每一条约束。没有被约束的东西,就默认它可以被攻击。永远不要以为审计过几次就万事大吉。持续、AI 加持的安全研究,才是新的底线。