Foundry 符号执行改进:无分支饱和乘法与 checked-multiply guard 的形式化证明
【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry
本篇文章基于 Foundry 仓库.changelog/symbolic-saturating-mul.md变更记录,深入解析 Foundry 符号执行(symbolic execution)模块对无分支(branchless)饱和乘法与checked-multiply 溢出守卫的形式化证明能力改进。读者将理解这类 EVM 汇编模式在符号执行中的难点、Foundry 如何通过表达式重写与求解器规范化消除多余 SMT 查询,以及如何用forge test --symbolic在真实合约上验证这些模式。
变更背景:一次针对乘法溢出的符号证明 patch
.changelog/symbolic-saturating-mul.md记录了该变更的核心内容:
forge: patch — Improved symbolic proofs for branchless saturating multiplication and checked-multiply guards.
即:改进了符号执行对"无分支饱和乘法"和"checked-multiply 守卫"的证明能力,属于forge组件的一个补丁级(patch)变更。
要理解这个 patch 的价值,先要明白这两类模式在真实 Solidity 生态中的出现场景:
- 饱和乘法(saturating multiplication):当乘积溢出时返回
type(uint).max而非回滚,常用于 Uniswap 类 AMM 的价格计算、预言机聚合等"宁可钳制也不失败"的场景。标准写法通常是带分支的 if-else;而无分支写法(branchless)使用位掩码技巧,在assembly块中实现,避免条件跳转以降低 gas。 - checked-multiply 守卫(checked-mul guard):Solidity 0.8.x 编译器的内置溢出检查,即
x == 0 || (x * y) / x == y,用于判断x * y是否溢出。它大量出现在经 solc 编译的字节码中,是符号执行器必须高频处理的约束形态。
问题在于:符号执行器会把每条汇编指令翻译成符号表达式树。无分支写法与守卫约束会产生"乘法-除法-或-掩码"等深嵌套表达式,若不做规范化,SMT 求解器需要处理高非线性算术,导致查询超时或返回错误反例。本次 patch 正是围绕"如何把这类表达式化简到可判定形式"展开。
无分支饱和乘法的符号表达:一个经典习语
在 Foundry 符号执行器的测试中,无分支饱和乘法被构造为如下表达式结构(见 crates/evm/symbolic/src/tests.rs 的expression_simplifies_saturating_select_idiom):
product = x * y quotient = product / x exact = (quotient == y) safe = exact || (x == 0) guard = (x == 0 ? 1 : 0) | (exact ? 1 : 0) # 布尔词形 sub_actual = (guard - 1) | product # 溢出时掩码为全 1,钳制到 UINT_MAX add_actual = (guard + UINT_MAX) | product # 另一种掩码形态 expected = safe ? product : UINT_MAX # 语义等价的 if-then-else这里展示了两种等价的掩码实现:(guard - 1) | product与(guard + max) | product。二者在溢出时都让掩码变为全 1、再与乘积做按位或,从而把结果钳制到UINT_MAX。该测试断言:这两种掩码形态都能被化简为标准的ite(safe, product, max)——即符号表达式树被折叠成一个三元的条件选择节点。
对符号执行而言,这个折叠至关重要:求解器面对ite远比面对"乘法、除法、加减、或"的组合更容易处理,也方便后续路径分支的合并与反例构造。
重写保持边界语义:极端输入下的正确性保证
表达式重写最大的风险是"化简出错"。无分支饱和乘法的语义边界集中在几个极端输入组合上,saturating_mul_rewrite_preserves_boundary_values(crates/evm/symbolic/src/tests.rs)专门覆盖了这些边界:
(0, UINT_MAX)、(UINT_MAX, 0):一个操作数为 0,乘积必为 0,不应饱和;(UINT_MAX, 1)、(1, UINT_MAX):乘以 1,乘积不溢出;(UINT_MAX, 2)、(2, UINT_MAX):严格溢出,结果必须钳制到UINT_MAX;(2^255, 2)、(2, 2^255):恰好跨越 256 位边界的中等溢出。
该测试对"原始掩码表达式"和"化简后表达式"同时求值,并以x_value.checked_mul(y_value).unwrap_or(UINT_MAX)作为期望值逐一比对,确保重写前后在所有边界点结果完全一致。这印证了 Foundry 符号执行器对表达式重写采取"先验证语义等价、再应用到求解流程"的严谨态度。
checked-mul guard 的求解器规范化
Solidity 的乘法溢出守卫x == 0 || (x * y) / x == y在符号执行中会翻译成布尔词(boolean word)上的约束。Foundry 在求解器侧对它做了专门的规范化处理(crates/evm/symbolic/src/runtime/solver/hard_arith_fallback.rs 的文档注释明确写出该守卫的语义形态)。
有界操作数的恒真判定
solver_normalizes_checked_mul_guard_for_bounded_operands(crates/evm/symbolic/src/tests.rs)构造了两个被& u64::MAX掩码限界的操作数a、b。两个u64的乘积不可能超过 128 位,因而永远不会溢出 256 位字。此时规范化函数把guard == 0(即守卫为假)直接化简为常量false——求解器无需任何 SMT 查询即可判定该分支不可达,这大幅减少了非线性算术的求解压力。
测试辅助函数checked_mul_guard_word(crates/evm/symbolic/src/tests.rs)给出了守卫的完整符号构造方式:先判断零操作数,再检查(x * y) / x == y,两者以布尔词形式取或,与 solc 生成的字节码语义一一对应。
反例分支的本地短路
saturating_mul_counterexample_branches_short_circuit_locally(crates/evm/symbolic/src/tests.rs)验证了反例构造的本地化:对于guard == false分支,断言"饱和结果必须等于UINT_MAX";对于guard == true分支,断言"结果必须等于乘积"。测试表明这两个约束组合都不可满足(is_sat_branch返回 false),并且整个过程中smt_queries == 0、heuristic_witnesses == 0——即纯靠表达式规范化和算术重写就完成了证明,完全没有调用底层的 SMT 求解器(z3)。
这意味着:当符号执行器走到饱和乘法相关断言时,溢出路径的不可达性可以本地判定,而不必把复杂的乘除约束抛给求解器,从而显著提升证明速度与稳定性。
构造性模型:为守卫分支直接给出可行赋值
对于无法静态判定、必须二分求解的守卫分支,Foundry 还提供了"构造性模型"(constructive model)机制。checked_mul_guard_branch_model(crates/evm/symbolic/src/runtime/solver/hard_arith_fallback.rs)直接按守卫的三个语义情形分配具体值:
- 零析取项:
x == 0为真,此时乘积为 0; - 非零精确乘积:
(x * y) / x == y成立,结果取乘积本身; - 包装乘积:乘积溢出(wrapping),结果钳制到
UINT_MAX,两个操作数顺序均可覆盖。
模型构造遵循"先补全简单的支持约束(support constraints)"原则,保证路径上已有的精确操作数值不被语义默认值覆盖,且只有在满足全部原始约束时才返回模型。为防御病态表达式,该函数还设置了支持访问预算MAX_CHECKED_MUL_SUPPORT_VISITS = 256(同文件第 115 行),超过预算即放弃构造、退回通用求解路径。
配套测试覆盖了模型构造的边界情形:checked_mul_guard_branch_model_preserves_exact_operand_constraints(保留精确操作数约束)、checked_mul_guard_branch_model_matches_nested_boolean_guard(嵌套布尔守卫)、checked_mul_guard_branch_model_completes_original_model_symbols(补全原模型符号)以及若干"拒绝"测试——当存在符号哈希赋值、gasleft赋值或非可重放(non-replayable)变量时,构造性模型必须拒绝返回,避免生成不可重放的错误反例(见 crates/evm/symbolic/src/runtime/solver/hard_arith_fallback.rs 附近的测试模块)。
端到端验证:forge test --symbolic实战
上述底层能力最终通过 forge 的符号执行测试入口暴露给用户。仓库集成测试symbolic_proves_branchless_operation_state(crates/forge/tests/cli/test_cmd/symbolic.rs)演示了完整流程:
- 定义合约
SymbolicOperationState,其中original函数用带分支的 Solidity描述状态机(packed == 0返回 0、奇数位返回 3、时间比较返回 1/2); - 在
assembly中用一条mul指令实现无分支等价计算:
assembly { optimized := mul( iszero(iszero(packed)), // packed != 0 ? 1 : 0 add(and(packed, 1), sub(2, lt(time, shr(1, packed)))) ) }- 断言
optimized == original(packed, time),即无分支汇编实现与语义等价的带分支版本完全一致。
随后以如下命令运行:
forge test --symbolic --json --optimize --match-test checkOperationState--symbolic启用符号执行模式,--optimize开启表达式优化重写(本次 patch 的表达式折叠正是在该路径生效),--json输出结构化测试结果,--match-test只运行目标测试。该测试要求环境装有 z3(z3_available()检查),否则跳过。
这个用例说明:无分支优化的正确性证明,正是本次 patch 改进的表达式重写能力在真实场景中的落地——开发者可以放心地把手写汇编优化与原始 Solidity 语义做形式化等价验证,而不必依赖手工审查。
适用前提与限制
- 本文所述的饱和乘法/checked-mul 规范化能力位于符号执行模块
crates/evm/symbolic,仅在forge test --symbolic模式下生效;普通 EVM 执行路径不涉及这些重写。 - 部分求解器优化依赖 z3 等外部 SMT 求解器;无 z3 环境时相关测试与证明路径会跳过(如集成测试中的显式检查所示)。
- 构造性模型只对"守卫类"约束形态生效,且受支持预算与可重放性约束限制;超出预算或遇到不透明变量时会安全地退回通用求解流程,不会生成错误反例。
- 重写正确性通过边界值测试保障,覆盖零操作数、乘以 1、全量溢出与 2^255 边界等关键输入组合,但符号证明的完备性仍取决于具体合约路径的约束复杂度。
总结
本次forge: patch变更从三个层面提升了 Foundry 符号执行对乘法溢出模式的处理能力:
- 表达式层:将无分支饱和乘法的掩码习语(
(guard-1)|product、(guard+max)|product)折叠为ite语义,并经边界值测试保证重写前后等价; - 求解器层:对 checked-mul guard 做有界规范化,让"两个 u64 相乘不会溢出"这类事实不触发任何 SMT 查询,同时为守卫分支提供构造性模型以加速反例生成;
- 用户层:通过
forge test --symbolic --optimize提供端到端验证入口,使开发者能用符号证明替代人工审查,验证无分支汇编优化与带分支语义实现的等价性。
对于在 gas 敏感合约中广泛使用位运算与内联汇编的开发者,这些能力意味着:复杂掩码技巧的正确性不再依赖"目测",而是可以交由 Foundry 的符号执行器进行自动化、可重复的形式化验证。
【免费下载链接】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),仅供参考