Foundry 符号执行改进:为无分支 XOR 字选择惯用法(P-256 验证路径)提供更强的规范化简
【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry
Foundry 的forge test --symbolic原生符号执行引擎(foundry-evm-symbolic)在本仓库的补丁 .changelog/symbolic-p256-normalization.md 中描述了一项改进:为无分支(branchless)XOR 字选择(word-selection)惯用法提供更强的符号化证明能力。本文以此为切入点,结合 crates/evm/symbolic 的源码实现,讲解这类惯用法的形态、引擎如何将其规范化为ite(分支)表达式、为什么要做这种化简,以及它为何在 P-256 椭圆曲线验签这类恒定时间(constant-time)代码中尤为重要。读完本文,你将能识别 Solidity/汇编中的这类模式,理解符号执行器底层表达式 DAG 的化简规则,并学会用forge test --symbolic验证依赖该能力的属性。
补丁内容与定位
该变更日志条目只有两行,格式遵循仓库.changelog的碎片化补丁说明惯例:
forge: patch Improved symbolic proofs for branchless XOR word-selection idioms.- 组件:
forge(符号执行能力随forge test --symbolic一起发布); - 变更类型:
patch,即行为修正/改进,不引入破坏性变化; - 核心内容:改进了对无分支 XOR 字选择惯用法的符号证明能力。
“字选择”(word-selection)是恒定时间代码中非常典型的无分支条件选择模式:把布尔条件先转成 0/1 的“字”(word,即 256 位 EVM 字),再通过XOR/AND/乘法等位运算,在两个候选值之间做无分支选择,从而避免JUMPI条件跳转带来的时间侧信道。变更日志标题中的 “p256” 指代这类模式在 P-256(secp256r1,椭圆曲线 P-256 验签)等密码学实现中的高频出现。
需要强调的是:这一条目是仓库中 .changelog 目录下大量symbolic-*补丁之一,与 symbolic-checked-add-normalization.md、symbolic-polynomial-normalization.md、symbolic-boolean-word-mul.md 等共同构成符号表达式规范化(normalization)的持续改进系列。
为什么“字选择”难证明:条件分支 vs 无分支选择
符号执行器要证明一个属性,会沿着所有可行路径展开,并把每个分支条件累积为路径约束。对于普通的if (cond) x = a; else x = b;,引擎只需生成ite(cond, a, b)并对两条路径分别求解。但恒定时间代码刻意不写条件跳转,而是写成:
// 伪代码:cond 是布尔值,被转成 0/1 的 uint256 x = b ^ ((condWord) * (a ^ b));当condWord == 0时结果为b,当condWord == 1时结果为b ^ (a ^ b) == a。这类表达式在符号层面产生的是Xor(Mul(BoolWord(cond), Xor(a, b)), b)这样的复合项——没有任何JUMPI,因此不会产生路径分支,但会给求解器留下一个非线性(乘法)约束,且后续对x的任何断言都无法被“按条件分情况”简化,证明负担全部压在 SMT 求解器身上。
更麻烦的是,如果选择词不是严格的0/1,而是条件经过比较运算得到的任意字,符号执行器还需要先从该字的位模式中恢复出布尔条件,才能完成化简。这正是本补丁的用武之地。
源码证据:XOR 表达式的四级化简管线
补丁对应的核心实现在 crates/evm/symbolic/src/runtime/expr/expr.rs 中SymBinOp::Xor的构造/化简路径(expr.rs L619-L651)。从源码结构看,该路径依次尝试多级重写,命中即返回化简结果,否则保留原始Xor节点:
- 常量折叠:
const ^ const => const、0 ^ a => a、a ^ 0 => a; - 自反消去:
a ^ a => 0(L628-L629); - 共享操作数消去(
xor_with_shared_operand,L855-L864):a ^ (a ^ b) => b,两边方向都尝试; - 无分支选择恢复:这是本次补丁的焦点,包含两个重写:
xor_with_bool_select(L828-L853):把a ^ ((a ^ b) * bool_word(c))化简为ite(c, b, a);xor_with_zero_ite(L866-L885):把a ^ ite(c, b, 0)化简为ite(c, a ^ b, a)。
核心重写:xor_with_bool_select
代码关键片段(L828-L853):
fn xor_with_bool_select(cx: &mut SymCx, base: &Self, selector: &Self) -> Option<Self> { let SymExprKind::BinOp(SymBinOp::Mul, left, right) = selector.kind() else { return None }; let (condition_word, selected) = match left.kind() { SymExprKind::BinOp(SymBinOp::Xor, delta_left, delta_right) if delta_left == base => { (right, delta_right.clone()) } SymExprKind::BinOp(SymBinOp::Xor, delta_left, delta_right) if delta_right == base => { (left, delta_left.clone()) } _ => match right.kind() { /* 对称分支 */ }, }; let condition = condition_word.bitwise_bool_word_condition(cx)?; Some(Self::ite(cx, condition, selected, base.clone())) }它识别selector == (base ^ selected) * condition_word的结构(Mul的两个操作数方向都考虑),再从condition_word中恢复布尔条件,最终产出ite(condition, selected, base)。这里有两个关键安全约束:
- 必须确认 delta 确实以 base 为共享操作数,否则拒绝重写——L2716-L2730 的测试
xor_select_rejects_delta_before_recovering_condition明确验证了“delta 与 base 无关时必须返回None”的行为,防止把a ^ ((x ^ y) * c)误化简为错误的ite; bitwise_bool_word_condition失败时返回None,即无法确认condition_word语义上等价于布尔字时绝不化简。
恢复布尔条件:bitwise_bool_word_condition
选择词通常不是字面量0/1,而是bool_word(c)这类由比较、ite、Or组合成的表达式。引擎通过 expr.rs L1274-L1314 的bitwise_bool_word_condition做有界、迭代式的位宽分析来恢复条件:
- 用显式栈 +
HashSet去重遍历表达式 DAG,避免递归爆栈和共享子图重复访问; - 遇到
Or节点时下推其两个操作数(布尔字通常是多个条件的按位或); - 对位宽为 1 的叶子,生成
word != 0(等价于word == 1)的等式约束作为叶子条件; - 受
MAX_BITWISE_BOOL_WORD_VISITS节点预算限制,超限或遇到非布尔字结构时返回None。
对应的单元测试覆盖了共享OrDAG 的去重访问(L2483、L2513、L2539)、节点预算截断(L2497、L2563)、位宽分析上限(L2665)以及单比特叶子比较的保留行为(L2680)。
防爆炸保护:duplicating_branchless_rewrite_fits
把操作数复制进ite的两个分支会放大下游消费者看到的表达式规模。引擎用 expr.rs L887-L907 的duplicating_branchless_rewrite_fits预先统计“操作数展开后节点数 × 2 + 条件节点数 + 2 个包装节点”,超过MAX_BRANCHLESS_REWRITE_UNFOLDED_NODES上限就拒绝重写,注释明确说明这是为了防止“线性系列的无分支操作变成指数级”。add_with_const_ite(L810-L826)等其他无分支重写也复用了同一防爆保护。
为什么对 P-256 验证特别重要
P-256(secp256r1)验签等密码学代码为了抵抗时序攻击,几乎必然使用恒定时间(constant-time)实现:不使用任何if/JUMPI条件跳转,所有“选择”(如选坐标、选标量位、条件取反、条件复制)都用位运算表达。其典型形态正是:
// 条件取反:cond ? -x : x 的恒定时间写法 result = x ^ ((0 - condWord) ^ ...) // 或 (x ^ (-x)) * condWord 等变体而 Solidity 编译器(含 Yul 优化)也常常把高层c ? a : b编译为无分支的位运算序列,尤其当条件可被证明为布尔时。这意味着:任何要在符号层面证明 P-256 验签相关属性的尝试,都会大量遇到Xor+Mul/Ite组合的表达式。
在本补丁之前,这类表达式停留在原始Xor/Mul形态:
- 求解器面对的是非线性位向量约束,SMT 求解困难、容易超时,进而触发
Incomplete(见下节“结果语义”); - 引擎无法把后续断言按条件分情况分析,
PASS的证明质量受限。
本补丁把最常见的两个形态(a ^ ((a ^ b) * c)与a ^ ite(c, b, 0))在表达式构造期就规范化为ite,使后续路径探索、断言检查和求解器查询都基于更简单的结构进行,从而在保持语义不变的前提下提升了这类证明的可行性与稳定性。需要说明的是,changelog 标题中的 “p256” 指向的是这类惯用法的典型出处,引擎本身是对通用表达式的化简,并不针对 P-256 预编译或特定密码学实现做特判(P-256 预编译地址在 precompiles.rs 中按活跃 EVM 版本识别,与本次表达式化简相互独立)。
在forge test --symbolic中的实际意义
该补丁提升的“证明能力”最终体现在forge test --symbolic的结果语义上。依据 crates/evm/symbolic/README.md,符号测试结果分为:
PASS:在当前建模语义与配置边界内,所有被探索路径均无可行的失败;FAIL+ 反例:求解器找到失败模型,且经普通执行器具体重放(replay)确认;FAIL: incomplete symbolic execution (...):搜索未完成或反例无法验证,应视为“未建立”。
表达式化简失败并不会直接导致Incomplete,但会显著提高Incomplete的概率:未化简的非线性Xor/Mul约束更容易导致求解器超时(Timeout)、达到查询预算(max_solver_queries)或进入“硬算术”难解区。README 的“硬算术”一节也印证了这一点:引擎先探索由局部检查(含规范化)决定的简单分支,再把手头难解的算术分支交给 SMT 求解器,而“更大的多项式与求解器难解的非线性表达式可能报Incomplete或超时”。因此,把无分支选择恢复为ite本质上是在减少落入难解区的表达式。
验证路径示例
要实际观察这一能力,可以写一个使用恒定时间风格的check*函数:
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.20; import "forge-std/Test.sol"; contract ConstantTimeSelectTest is Test { // 恒定时间风格:c ? b : a,用 XOR/MUL 表达 function select(uint256 a, uint256 b, bool c) internal pure returns (uint256) { uint256 mask = c ? type(uint256).max : 0; return a ^ (mask & (a ^ b)); } function check_select_identity(uint256 a, uint256 b, bool c) external pure { uint256 r = select(a, b, c); // 无论 c 取何值,r 都必须等于对应分支 assert(r == (c ? b : a)); } }forge test --symbolic --match-test check_select_identity如果选择词采用bool_word(c) * (a ^ b)的形态(编译器对c ? b : a的常见编译产物),引擎会在构造Xor表达式时直接恢复出ite(c, b, a),随后对r == (c ? b : a)的断言即可在表达式层面直接消解,从而以较低求解成本得到PASS。若刻意构造 delta 与 base 无关的形态(如a ^ ((x ^ y) * bool_word(c))),引擎会按 测试用例 L2716 的预期拒绝重写,属性仍可被证明,但求解负担会更大。
注意:运行符号测试要求本地可用求解器,默认命令为
z3(macOS 可brew install z3,Ubuntu 可sudo apt-get install z3),详见 README 快速开始 一节。
总结
本次forge: patch补丁的核心价值在于:
- 补全了 XOR 无分支选择惯用法的规范化:
a ^ ((a ^ b) * bool_word(c))与a ^ ite(c, b, 0)两类形态在表达式构造期即化简为ite; - 恢复布尔条件是有界且安全的:
bitwise_bool_word_condition用节点预算与去重遍历保证不爆炸,无法确认语义时拒绝化简(有测试用例佐证); - 防爆炸保护保障工程稳健性:
duplicating_branchless_rewrite_fits阻止无分支重写把线性表达式放大成指数级; - 实战收益聚焦恒定时间代码:P-256 验签等密码学实现普遍使用这类位运算选择,该化简直接减少符号证明中落入 SMT 难解区的非线性约束,从而减少
Incomplete/超时,提升forge test --symbolic对这类属性的证明可行性。
相关源码位置:表达式化简核心在 crates/evm/symbolic/src/runtime/expr/expr.rs,外部语义与结果定义见 crates/evm/symbolic/README.md,表达式内部不变量约定见 crates/evm/symbolic/AGENTS.md,同类补丁可对比 symbolic-checked-add-normalization.md 与 symbolic-boolean-word-mul.md。
【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考