ZKsync 协议预编译实现审计

本文对以太坊 zkEVM 上的预编译操作进行了审核,涵盖了 ModExp、ECAdd、ECMul 和 ECPairing 操作及其对应的零知识电路构造。审查重点在于算法实现的正确性、输入验证、边界条件处理以及计算逻辑向电路约束的正确转换。虽然发现了一些优化和最佳实践方面的问题,但总体而言,代码库展现了对安全问题的良好关注。

目录

总结

TypePrecompileTimelineFrom 2025-03-13To 2025-03-20LanguagesRustTotal Issues11 (5 解决, 1 部分解决)Critical Severity Issues0 (0 解决)High Severity Issues0 (0 解决)Medium Severity Issues1 (1 解决)Low Severity Issues5 (2 解决)Notes & Additional Information5 (2 解决, 1 部分解决)

范围

我们审计了 matter-labs/zksync-protocol 仓库在提交 97162cc 下的实现。

审计的范围包括以下文件:

 ./crates/zk_evm_abstractions/src/precompiles/
├── ecadd.rs
├── ecmul.rs
├── ecpairing.rs
└── modexp.rs
./crates/circuit_definitions/src/circuit_definitions/base_layer
├── ecadd.rs
├── ecmul.rs
├── ecpairing.rs
└── modexp.rs
./crates/zkevm_circuits/src/modexp/
├── implementation/
│ └── u256.rs
├── input.rs
└── mod.rs

系统概述

该项目在 zkEVM 环境中实现以太坊的预编译操作。以太坊的预编译是具有预定义地址的特殊合约,负责执行计算代价较高的操作。系统为这些操作同时提供了计算实现和零知识电路实现。

代码库遵循模块化架构,使用公共接口 (PrecompilesProcessor) 执行预编译操作,并为每个操作提供特定实现以及模块化的指数电路实现。每个预编译的实现包括:

  1. 执行实际计算的核心算法。
  2. 与 VM 交互的内存逻辑。
  3. 生成证明的零知识电路定义。

在本次审计范围内,zkevm_circuits/src/modexp/ 包括 u256.rs,用于处理 UInt256 模数指数运算的主要 modexp_32_32_32 功能,input.rs 设置 ModexpCircuitFSMInputOutput 以管理队列和证人以及 mod.rs,通过 modexp_function_entry_point 运行电路,满足零知识的要求。

电路构建器特征

该项目为每个预编译操作实现了 CircuitBuilder 特征,定义了电路的几何形状、查找参数和门配置。该特征负责设置零知识证明的约束系统,确保每个操作能够有效验证。

电路使用多种专用门,包括布尔约束、归约门和选择门,以优化证明生成和验证过程。它们还利用查找表来简化常见操作的约束复杂性。

预编译

ModExp

模指数预编译计算 b^e mod m,适用于大整数,这是许多密码协议中的基本操作。实现使用平方和乘法算法,以逐位处理指数。电路处理特殊情况,如零模数,并为常见输入模式实现优化。

ECAdd

椭圆曲线加法预编译在 BN254 椭圆曲线上添加两个点。BN254 在基于配对的密码学和零知识应用中被广泛使用。该实现验证输入点是否位于曲线上,并处理边缘情况,包括无穷远点。

ECMul

椭圆曲线乘法预编译将 BN254 曲线上的点与标量值相乘。该实现涵盖了处理大标量的优化,包括按群体顺序规约。它正确处理诸如乘以群体顺序产生无穷远点的边缘情况。

ECPairing

椭圆曲线配对预编译在 BN254 曲线上执行配对检查,这是验证多种零知识证明及其他密码协议的关键操作。这是预编译中最复杂的,支持多个输入对,并验证点是否位于曲线的正确子群中。

安全模型与信任假设

在审计过程中,做出了以下信任假设:

  1. Boojum 约束系统框架:假设构建电路所用的底层约束系统框架是健全的,正确地实现了必要的密码协议。
  2. VM 运行环境:调用这些预编译的 zkEVM 执行环境未经审计,假设其能正确处理返回状态标志和相应输出。具体来说,假设 VM 能通过返回的显式状态标志正确区分成功和失败场景,即便成功案例产生的结果在视觉上可能与错误条件相似(例如,无穷远点 [1, 0, 0] 与显式错误 [0, 0, 0])。如果 VM 无法正确解释这些状态标志,可能导致不正确或不安全的操作。
  3. 内存管理系统:虽然我们审查了内存交互代码,但底层内存系统的实现超出审计范围,并假设其正常工作。
  4. Gas计量:未验证这些操作的Gas成本计算与计量,假设其正确。
  5. ModExp 测试覆盖率:假定 modexp_32-32-32_tests.jsonmodmul_32-32_tests.json 中的测试用例结构正确。

中等严重性

未进行显式边界检查的内存访问

在预编译实现中 (ecadd.rs, ecmul.rs, ecpairing.rs, 和 modexp.rs),execute_precompile 方法在没有显式算术检查溢出或边界验证的情况下自增内存索引 (current_read_location.indexwrite_location.index)。例如,在 ecadd.rs 中,多次读取 (x1, y1, x2, y2) 和写入 (status, x, y) 直接自增偏移量,隐含假设提供的偏移量 (params.input_memory_offsetparams.output_memory_offset) 是安全且在有效范围内的。

如果由于大偏移量而发生算术溢出,内存读取可能会意外地引用不正确的索引,或者在内存页面内回绕到意想不到的位置。这可能导致在计算中使用意想不到的数据,从而导致不正确或不可预测的执行状态。同样,内存写入中的算术溢出可能导致数据写入错误或意想不到的内存位置。

为了解决此问题,可以考虑遵循以下建议:

  • 引入使用检查算术(例如 checked_add)的显式算术检查。
  • 在检测到算术溢出或边界违规时,在输出内存中实现显式错误状态(例如,设置状态指示器为 U256::zero())。

更新: 解决,不是问题。Matter Labs 团队声明:

此检查在电路层面上处理。

低严重性

预编译模块中代码重复过多

代码库在预编译模块中表现出显著的代码重复,特别是在 MemoryQuery 执行中。每个模块重复定义了逻辑以:

  • 手动增加内存索引位置。
  • 构建几乎相同的读/写查询。
  • 为成功和失败处理结构化相同的条件分支。

这种冗余增加了维护开销,引入了更高的不一致风险,并使全局改进或错误修复复杂化。

为提高可维护性和一致性,可以考虑以下一种方法:

  • 提取公共逻辑: 将重复的内存操作移至共享辅助函数。
  • 利用代码生成: 使用基于宏或过程宏的解决方案,以执行重复模式中的一致性。

以这种方式重构代码将增强可读性,减少重复,并简化未来的修改。

更新: 已确认,将解决。Matter Labs 团队声明:

我们希望推迟此问题。

文档不足和不一致

虽然一些函数包含最少的行内注释 (例如 ecpairing_innermodexp_inner),但很多关键部分,尤其是在电路构建器实现中,缺乏充分的解释。缺乏一致的文档使理解关键组件的设计原理和预期行为变得困难。

也不存在全面的模块或架构级文档,这可能阻碍新贡献者理解不同预编译功能(例如椭圆曲线加法、乘法、配对和模指数运算)如何与相应的电路合成组件交互。

此外,复杂操作(如椭圆曲线算术和模指数运算)的行内注释稀少。现有文档未充分详细说明边缘情况、错误处理或性能权衡,导致确保正确性和效率变得更加困难。

为解决这些问题,请考虑执行以下操作:

  • 采用标准化的文档样式,遵循 Rustdoc 约定,以确保所有公共模块、函数和数据结构都包括其目的、参数、预期输出和可能的错误条件的清晰描述。
  • 开发一个高层的架构概述,可以作为单独文档或作为介绍模块注释,来说明预编译和电路构建器组件的整体设计。包括图表或流程图将帮助贡献者理解组件之间的交互。
  • 增强密码函数的行内文档,通过补充算法选择、假设和潜在陷阱的解释。引用相关标准(例如 EIP 规范)可以进一步阐明实现。
  • 利用 Rustdoc 自动化文档生成和发布,确保为团队和社区提供最新且易于访问的参考。

改善文档将增强代码的可维护性,促进新开发者的上手,并确保所有贡献者对密码组件有充分的理解。

更新: 已确认,将解决。Matter Labs 团队声明:

我们希望推迟此问题。

modexp缺乏对简单情况的优化

modexp_inner 函数对模数指数运算使用固定的 256 位平方和乘法算法,但未针对简单情况进行优化。虽然它有效处理零模数(根据 EIP-198),但其他简单输入会带来不必要的计算开销:

  • 指数(e)= 0:结果显然是:
  • 1 如果 m > 1
  • 0 如果 m = 1
  • 指数(e)= 1:结果直接简化为 b mod m
  • 底数(b)= 0 或 1:这些值可以直接得到简单结果,而无需进一步计算。

目前,该实现对于这些简单场景不必要地处理所有 256 个指数位。

考虑在进入主指数循环之前,针对 e ∈ {0, 1}b ∈ {0, 1} 实现快速路径检查。这些改进将显著提升简单场景的性能,同时维持最佳的零知识电路效率。

更新:pull request #148 中得到解决。

BN254 G2中的低效子群检查

ec_pairing.rs,当前通过完整的 254 位标量乘法验证点 $P$ 的子群成员资格,这在计算上是比较昂贵的。

在预编译环境中,这种低效会因过多的椭圆曲线加法而增加Gas成本。考虑使用以下任一方法进行优化:

Frobenius 端同态

使用 ψ(P)=[6x²]P\psi (P) = [6x²]Pψ(P)=[6x²]P 进行有效验证,其中 x=4965661367192848881,使得 [6x²][6x²][6x²] 为 65 位标量。由于 Frobenius 映射 ψ\psiψ 在 Fp2\mathbb{F}_{p^2}Fp2​ 上几乎是免费(仅为共轭),因此此检查以较低成本确认成员资格。

Cofactor 乘法

而不是乘以完整的群体顺序 r(一个 254 位的标量),请使用更小的 cofactor h 进行更快的验证,从而减少标量乘法并提升性能。

更新:pull request #148 中得到解决。

execute_precompile内存读取中缺乏稳健的错误处理

execute_precompile 方法未能适当地处理内存读取失败,导致无声失败情况,其中无效输入被误解为有效的椭圆曲线点。具体来说,当内存读取失败而未触发异常时(返回零),代码错误地将这些点视为合法的 (0,0),其在椭圆曲线密码学中表示无穷远点。

如果内存读取失败并返回(0,0),系统无法区分合法输入与因失败而引发的默认值。这引入了几种安全风险:

  • 无声失败: 即使用户没有提供有效的椭圆曲线点,系统也不会引发错误。相反,它执行的操作可看似正确,但由于隐藏的错误却在语义上是错误的。
  • 输入处理中的模糊性: 系统假定(0,0)始终是故意输入,未能与内存读取失败区分开。
  • 密码操控攻击向量: 攻击者可以利用此行为,制造入口条件,使(0,0)作为操作数(通过操控输入偏移或内存页以返回他们知道会返回零而不是直接失败的区域),从而有效绕过部分椭圆曲线操作。
  • 协议不一致性: 如果在更高级别协议中使用预编译,则原本应失败的操作可能无声产生看似有效的结果,从而破坏安全假设。

为防止此问题,实施应当:

  1. 在其用于密码计算之前显式验证内存读取是否成功。如果 execute_partial_query 未提供失败指示符,则应实施额外验证。
  2. 引入显式错误检查,以区分故意 (0,0) 输入和由内存失败引起的默认值
  3. 在执行椭圆曲线操作之前加强内存访问验证,确保超出范围或被损坏的读取不会导致不正当的密码行为。

更新: 已确认,未解决。Matter Labs 团队表示:

_我们认为这不是一个问题,原因在于代码的使用上下文。密码预编译只能通过状态机中的 precompile_call 操作码被调用。VM处理 precompile_call 的方式——它检查调用 precompile_call 的合约地址是否为0x01, 则会在后台执行 ecrecover 电路;如果合约地址为0x02,它将执行 sha 逻辑;如果合约地址为0x08,则会执行 ecpairing 逻辑。但还要注意的是,0x08 的预测布依代码为 https://github.com/matter-labs/era-contracts/blob/draft-v28/system-contracts/contracts/precompiles/EcPairing.yul#L134。这意味着你审查的电路逻辑将只在此合约(所有内存不变性等约束的作用下)产生执行。_

备注与附加信息

缺乏自动化lint检查可能导致代码质量问题

代码库中存在多种不合适的实践,包括不必要的 let 绑定、与零进行的长度比较、以及对 false 的相等检查等(整个代码库总共有1103个警告),这些都可以通过官方 Rust lint 工具 cargo clippy 捕获。

在没有此 lint 工具的情况下,项目可能会受到以下影响:

  • 代码复杂性增加且可读性降低。
  • 性能低效和运行时错误的风险增加。
  • 难以保持一致性和质量标准。

考虑在开发流程中使用 cargo clippy,因为它可以帮助及早识别和解决这些问题。可能的集成策略包括:

  • CI/CD 执行限制:在 CI/CD 管道中添加一个步骤,在出现 clippy 警告时失败构建。
  • IDE 支持:配置 IDE 插件,如 rust-analyzer 或 JetBrains Rust,以启用实时 lint 反馈。
  • Git Hook:实现一个提交前Hook,防止提交时出现 lint 错误。

这些措施将提升代码质量,简化审计流程,并在整个代码库中加强最佳实践。

更新: 已确认,将解决。Matter Labs 团队声明:

我们希望推迟此问题。

ecadd模块中存在dbg!宏

ecadd 模块包含 dbg! 宏的实例,通常用于开发过程中的临时调试。然而,将 dbg! 宏保留在生产代码中是不明智的,因为它们直接打印到 stderr,可能导致日志混乱并暴露内部状态。此外,dbg! 并未针对性能进行优化,并且缺乏为不同日志级别配置的灵活性。

考虑从 ecadd 模块中删除 dbg! 宏。如果需要记录日志,应使用结构化日志或追踪库,例如 tracing 或类似的日志库。这些替代方案提供可配置的日志级别、结构化输出和更好的性能管理。

更新:pull request #148 中得到解决。

不明确的通用const参数

所有每个预编译的单位结构中使用的 const 泛型参数 B 缺乏清晰度,使代码可读性降低和维护变得更加困难。没有描述性的名称或适当的文档,使得开发者难以理解其目的和影响。

考虑将 B 重命名为更具描述性的标识符,例如 ENABLE_WITNESS,或补充文档以阐明其作用。

更新: 已确认,将解决。Matter Labs 团队声明:

我们希望推迟此问题。

测试用例不完整和错误

在整个代码库中,发现多起不完整和/或错误的测试用例:

不完整覆盖

  • modexp.rs 中的 test() 调用 modexp_inner(5, 0, 1),但缺少断言以确保 result == U256::one(),降低了测试的有效性。

  • ecadd.rsecmul.rsecpairing.rsecpairing.rsmodexp.rs 的预编译测试套件缺乏边缘情况覆盖,尤其是在无效域元素和模数溢出方面。

  • 确保遵循与这些预编译相关的 EIPs。

不正确实现

  • ecadd.rs 中的 test_ecadd_inner_invalid_x2y2 解析十六进制值 x1y1 时使用的是基数十,而不是基数十六,导致验证不正确。

薄弱的测试覆盖率可能导致未检测到的故障,尤其是在密码操作中,而不正确的解析可能引入误报/漏报,从而掩盖真实问题。

考虑通过增强测试用例,添加边缘情况、无效输入和边界条件,提高 test_ecadd_inner_invalid_x2y2 中基数解析的正确性,并验证预编译行为是否符合相关 EIPs(EIP-196/197/198/2565)。

_更新: 部分解决。边缘案例的覆盖已在 precompiles.rs 中处理。然而,ecadd.rs 中的 test_ecadd_inner_invalid_x2y2 仍使用基数十而非基数十六解析十六进制值,而 modexp.rs 中的 test() 依旧缺乏确保 result == U256::one() 的断言以验证 modexp_inner(5, 0, 1) 的有效性,从而降低测试效用。_

排版错误

排版错误可能会对代码库的清晰度和可维护性产生负面影响。

ecpairing.rs 中第 356 行的注释当前引用了 EIP-192,这是错误的。

考虑更新上述注释以提及 EIP-197

更新:pull request #148 中得到解决。

结论

本次审计涉及 Ethereum 预编译操作在 zkEVM 环境中的实现,特别关注于 ModExp、ECAdd、ECMul 和 ECPairing 操作。还涉及 ModExp 电路的实现和测试。审查包括这些操作的计算实现和它们的相应零知识电路构造。评估重点是算法实现的正确性、输入验证、边缘情况处理,以及如何将计算逻辑适当转换为电路约束。

在审计过程中,发现一个关键严重性问题,即在 execute_precompile 中内存读取失败可能被无声地解读为有效的 (0,0) 点,从而形成脆弱性。此外,还发现了一些优化和最佳实践方面的问题,虽然这些问题并不立即威胁到系统安全,但可能会影响性能、可维护性和Gas效率。

总体而言,代码库展示了对复杂密码操作的良好实现,并对安全问题给予了适当的关注。模块化架构和一致的接口设计反映了良好的工程原则。尽管如此,在代码效率和文档完整性方面仍有提升空间。

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

0 条评论

请先 登录 后评论
OpenZeppelin
OpenZeppelin
江湖只有他的大名,没有他的介绍。