本文对以太坊 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
) 执行预编译操作,并为每个操作提供特定实现以及模块化的指数电路实现。每个预编译的实现包括:
在本次审计范围内,zkevm_circuits/src/modexp/
包括 u256.rs
,用于处理 UInt256
模数指数运算的主要 modexp_32_32_32
功能,input.rs
设置 ModexpCircuitFSMInputOutput
以管理队列和证人以及 mod.rs
,通过 modexp_function_entry_point
运行电路,满足零知识的要求。
该项目为每个预编译操作实现了 CircuitBuilder
特征,定义了电路的几何形状、查找参数和门配置。该特征负责设置零知识证明的约束系统,确保每个操作能够有效验证。
电路使用多种专用门,包括布尔约束、归约门和选择门,以优化证明生成和验证过程。它们还利用查找表来简化常见操作的约束复杂性。
模指数预编译计算 b^e mod m
,适用于大整数,这是许多密码协议中的基本操作。实现使用平方和乘法算法,以逐位处理指数。电路处理特殊情况,如零模数,并为常见输入模式实现优化。
椭圆曲线加法预编译在 BN254 椭圆曲线上添加两个点。BN254 在基于配对的密码学和零知识应用中被广泛使用。该实现验证输入点是否位于曲线上,并处理边缘情况,包括无穷远点。
椭圆曲线乘法预编译将 BN254 曲线上的点与标量值相乘。该实现涵盖了处理大标量的优化,包括按群体顺序规约。它正确处理诸如乘以群体顺序产生无穷远点的边缘情况。
椭圆曲线配对预编译在 BN254 曲线上执行配对检查,这是验证多种零知识证明及其他密码协议的关键操作。这是预编译中最复杂的,支持多个输入对,并验证点是否位于曲线的正确子群中。
在审计过程中,做出了以下信任假设:
[1, 0, 0]
与显式错误 [0, 0, 0]
)。如果 VM 无法正确解释这些状态标志,可能导致不正确或不安全的操作。modexp_32-32-32_tests.json
和 modmul_32-32_tests.json
中的测试用例结构正确。在预编译实现中 (ecadd.rs
, ecmul.rs
, ecpairing.rs
, 和 modexp.rs
),execute_precompile
方法在没有显式算术检查溢出或边界验证的情况下自增内存索引 (current_read_location.index
和 write_location.index
)。例如,在 ecadd.rs
中,多次读取 (x1
, y1
, x2
, y2
) 和写入 (status
, x
, y
) 直接自增偏移量,隐含假设提供的偏移量 (params.input_memory_offset
和 params.output_memory_offset
) 是安全且在有效范围内的。
如果由于大偏移量而发生算术溢出,内存读取可能会意外地引用不正确的索引,或者在内存页面内回绕到意想不到的位置。这可能导致在计算中使用意想不到的数据,从而导致不正确或不可预测的执行状态。同样,内存写入中的算术溢出可能导致数据写入错误或意想不到的内存位置。
为了解决此问题,可以考虑遵循以下建议:
checked_add
)的显式算术检查。U256::zero()
)。更新: 解决,不是问题。Matter Labs 团队声明:
此检查在电路层面上处理。
代码库在预编译模块中表现出显著的代码重复,特别是在 MemoryQuery
执行中。每个模块重复定义了逻辑以:
这种冗余增加了维护开销,引入了更高的不一致风险,并使全局改进或错误修复复杂化。
为提高可维护性和一致性,可以考虑以下一种方法:
以这种方式重构代码将增强可读性,减少重复,并简化未来的修改。
更新: 已确认,将解决。Matter Labs 团队声明:
我们希望推迟此问题。
虽然一些函数包含最少的行内注释 (例如 ecpairing_inner
和 modexp_inner
),但很多关键部分,尤其是在电路构建器实现中,缺乏充分的解释。缺乏一致的文档使理解关键组件的设计原理和预期行为变得困难。
也不存在全面的模块或架构级文档,这可能阻碍新贡献者理解不同预编译功能(例如椭圆曲线加法、乘法、配对和模指数运算)如何与相应的电路合成组件交互。
此外,复杂操作(如椭圆曲线算术和模指数运算)的行内注释稀少。现有文档未充分详细说明边缘情况、错误处理或性能权衡,导致确保正确性和效率变得更加困难。
为解决这些问题,请考虑执行以下操作:
改善文档将增强代码的可维护性,促进新开发者的上手,并确保所有贡献者对密码组件有充分的理解。
更新: 已确认,将解决。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 中得到解决。
在 ec_pairing.rs
,当前通过完整的 254 位标量乘法验证点 $P$ 的子群成员资格,这在计算上是比较昂贵的。
在预编译环境中,这种低效会因过多的椭圆曲线加法而增加Gas成本。考虑使用以下任一方法进行优化:
使用 ψ(P)=[6x²]P\psi (P) = [6x²]Pψ(P)=[6x²]P 进行有效验证,其中 x=4965661367192848881,使得 [6x²][6x²][6x²] 为 65 位标量。由于 Frobenius 映射 ψ\psiψ 在 Fp2\mathbb{F}_{p^2}Fp2 上几乎是免费(仅为共轭),因此此检查以较低成本确认成员资格。
而不是乘以完整的群体顺序 r(一个 254 位的标量),请使用更小的 cofactor h 进行更快的验证,从而减少标量乘法并提升性能。
更新: 在 pull request #148 中得到解决。
execute_precompile
方法未能适当地处理内存读取失败,导致无声失败情况,其中无效输入被误解为有效的椭圆曲线点。具体来说,当内存读取失败而未触发异常时(返回零),代码错误地将这些点视为合法的 (0,0),其在椭圆曲线密码学中表示无穷远点。
如果内存读取失败并返回(0,0),系统无法区分合法输入与因失败而引发的默认值。这引入了几种安全风险:
为防止此问题,实施应当:
execute_partial_query
未提供失败指示符,则应实施额外验证。更新: 已确认,未解决。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。这意味着你审查的电路逻辑将只在此合约(所有内存不变性等约束的作用下)产生执行。_
代码库中存在多种不合适的实践,包括不必要的 let
绑定、与零进行的长度比较、以及对 false
的相等检查等(整个代码库总共有1103个警告),这些都可以通过官方 Rust lint 工具 cargo clippy
捕获。
在没有此 lint 工具的情况下,项目可能会受到以下影响:
考虑在开发流程中使用 cargo clippy
,因为它可以帮助及早识别和解决这些问题。可能的集成策略包括:
这些措施将提升代码质量,简化审计流程,并在整个代码库中加强最佳实践。
更新: 已确认,将解决。Matter Labs 团队声明:
我们希望推迟此问题。
ecadd
模块包含 dbg!
宏的实例,通常用于开发过程中的临时调试。然而,将 dbg!
宏保留在生产代码中是不明智的,因为它们直接打印到 stderr
,可能导致日志混乱并暴露内部状态。此外,dbg!
并未针对性能进行优化,并且缺乏为不同日志级别配置的灵活性。
考虑从 ecadd
模块中删除 dbg!
宏。如果需要记录日志,应使用结构化日志或追踪库,例如 tracing
或类似的日志库。这些替代方案提供可配置的日志级别、结构化输出和更好的性能管理。
更新: 在 pull request #148 中得到解决。
所有每个预编译的单位结构中使用的 const
泛型参数 B
缺乏清晰度,使代码可读性降低和维护变得更加困难。没有描述性的名称或适当的文档,使得开发者难以理解其目的和影响。
考虑将 B
重命名为更具描述性的标识符,例如 ENABLE_WITNESS
,或补充文档以阐明其作用。
更新: 已确认,将解决。Matter Labs 团队声明:
我们希望推迟此问题。
在整个代码库中,发现多起不完整和/或错误的测试用例:
不完整覆盖
在 modexp.rs
中的 test()
调用 modexp_inner(5, 0, 1)
,但缺少断言以确保 result == U256::one()
,降低了测试的有效性。
ecadd.rs
、ecmul.rs
、ecpairing.rs
、ecpairing.rs
和 modexp.rs
的预编译测试套件缺乏边缘情况覆盖,尤其是在无效域元素和模数溢出方面。
确保遵循与这些预编译相关的 EIPs。
不正确实现
ecadd.rs
中的 test_ecadd_inner_invalid_x2y2
解析十六进制值 x1
和 y1
时使用的是基数十,而不是基数十六,导致验证不正确。薄弱的测试覆盖率可能导致未检测到的故障,尤其是在密码操作中,而不正确的解析可能引入误报/漏报,从而掩盖真实问题。
考虑通过增强测试用例,添加边缘情况、无效输入和边界条件,提高 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!