新闻详情

新闻详情

首页 / 资讯中心 / 详情

Foundry 符号执行中 vm.expectCall 重复注册语义修复:从“静默不可达条目“到“加法合并与显式拒绝“

发布时间:2026/9/17 1:57:39来源:尧图网络
Foundry 符号执行中 vm.expectCall 重复注册语义修复:从“静默不可达条目“到“加法合并与显式拒绝“
Foundry 符号执行中 vm.expectCall 重复注册语义修复从静默不可达条目到加法合并与显式拒绝【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry本文深入剖析 Foundry 仓库中foundry-evm-symbolic针对符号执行symbolic execution模式下vm.expectCall注册语义的一次行为修复重复的非计数non-counted注册改为按加法合并additive merge与具体执行concrete模式下的文档化惯用法保持一致重复的计数counted注册则被显式拒绝而不再静默压入一个在结构上永远无法触达的第二个条目。读完本文你将理解vm.expectCall在符号执行后端中的内部数据结构、注册与匹配算法以及这次修复对应的单元测试如何锁定新语义。一、背景vm.expectCall在 Foundry 测试中的两种执行模式vm.expectCall是 Foundry 测试框架cheatcodes 规范 中定义于Testing组、标记为Unsafe安全级别提供的核心断言工具它预先注册一个期望调用声明后续被测代码中应当发生或不发生一次对某个地址、携带特定 calldata 的调用。接口共有 6 个重载function expectCall(address callee, bytes calldata data) external; function expectCall(address callee, bytes calldata data, uint64 count) external; function expectCall(address callee, uint256 msgValue, bytes calldata data) external; function expectCall(address callee, uint256 msgValue, bytes calldata data, uint64 count) external; function expectCall(address callee, uint256 msgValue, uint64 gas, bytes calldata data) external; function expectCall(address callee, uint256 msgValue, uint64 gas, bytes calldata data, uint64 count) external;配套的还有expectCallMinGas指定最小 gas 而非精确 gas与expectDelegateCall等变体。当测试运行在具体执行concrete模式即默认的 EVM 测试执行器下时重复对同一(callee, data)注册非计数expectCall是合法的、被文档认可的惯用法每次注册代表再多期望一次调用。这正是 changelog 中 matching concretes documented idiom 所指的语义基准。而当测试运行在符号执行symbolic模式foundry-evm-symboliccrate对应forge test --symbolic一类的符号测试流程下时期望调用的注册与匹配发生在符号状态机中其语义一度与 concrete 模式产生分歧从而催生了本次修复。关联的变更记录位于 .changelog/symbolic-expectcall-additive-idiom.mdfrontmatter 声明本次变更以forge: patch与foundry-evm-symbolic: patch两个补丁级别发布说明这是一次向后兼容的行为修正而非破坏性 API 变更。二、修复前的缺陷静默压入结构上不可达的重复条目在修复之前符号执行后端对重复的vm.expectCall注册采取的是来者不拒的策略每当调用vm.expectCall就无条件地向expected_calls列表追加一个新的ExpectedCall条目。这在符号执行的语义下会产生一个隐蔽的 bug符号执行器在检查某次外部调用是否命中期望时会遍历expected_calls对每个条目构造一个符号匹配条件callee 地址匹配 ∧ calldata 前缀匹配并将该条件并入当前执行路径的约束。若同一(callee, data)注册了多个条目只有第一个条目有机会被命中消费——因为第一条一旦匹配并满足后续条目在结构上已经不可能再被观察到而如果第一条被消费后计数已满exact 匹配下第二个条目更是永远无法满足形成结构上不可达structurally-unreachable的悬挂期望。更糟的是这种行为是静默发生的测试作者看不到任何警告最终只在路径满足性检查阶段得到令人困惑的结果。三、修复方案以键控加法合并对齐 concrete 惯用法修复的核心落点是crates/evm/symbolic/src/runtime/state.rs中的注册入口函数register_expected_callstate.rs#L1366-L1387。该函数现在以(callee, data)作为注册键先检索expected_calls中是否已存在相同 callee 且 calldata 字节序列相同通过data.same_bytes比较的条目再按以下三分支语义处理pub(crate) fn register_expected_call( expected_calls: mut VecExpectedCall, cx: mut SymCx, expected: ExpectedCall, ) - Result(), static str { if let Some(existing) expected_calls .iter_mut() .find(|call| call.callee expected.callee call.data.same_bytes(cx, expected.data)) { if expected.exact { return Err(counted expected calls can only bet set once); } if existing.exact { return Err(cannot overwrite a counted expectCall with a non-counted expectCall); } existing.expected 1; } else { expected_calls.push(expected); } Ok(()) }三条分支的语义分别为场景新行为结果已有非计数条目 新非计数条目加法合并existing.expected 1列表长度不变已有条目 新计数条目count已提供显式拒绝返回错误counted expected calls can only bet set once已有计数条目 新非计数条目显式拒绝返回错误cannot overwrite a counted expectCall with a non-counted expectCall键callee/data不匹配任何已有条目正常追加压入新的ExpectedCall第一个分支正是 changelog 所述 merge duplicate non-counted registrations additively对同一键重复调用非计数expectCall等价于把期望次数累加——这与 concrete 模式中每调用一次expectCall就多期望一次的文档化惯用法完全对齐。后两个分支则是 reject duplicate counted registrations一旦期望次数是精确计数exact再叠加任何新注册都会破坏计数的确定性因此直接以错误拒绝把问题在注册阶段暴露出来而不是等到路径执行阶段产生不可达的幽灵条目。错误会沿着 cheatcodes.rs#L197-L201 的set_expected_call调用链被包装成CheatcodeOutcome::Revert(error_string_return_data(...))返回即测试会以 revert 的形式收到明确的失败信息。四、源码级剖析ExpectedCall 的内部表示与计数语义要理解这次修复需要看清ExpectedCall这个核心数据结构state.rs#L1245-L1363pub(crate) struct ExpectedCall { callee: SymExpr, // 被调用方地址符号表达式 value: OptionU256, // 可选的 msg.value 约束 gas: Optionu64, // 可选的精确 gas 约束 min_gas: Optionu64, // 可选的 gas 下限约束 data: SymBytes, // 期望的 calldata符号字节序列 expected: u64, // 期望的调用次数 observed: u64, // 已观察到的调用次数 exact: bool, // 是否精确计数 }构造函数ExpectedCall::newstate.rs#L1282-L1308决定了exact与expected的初值非计数注册未传countcount.unwrap_or(1)使expected 1但exact false。此时语义是至少发生一次多次发生也合法——这正是它可以被加法合并的原因计数注册传入countexpected count且exact true。语义是恰好发生count次多了少了都算失败因此必须保持唯一性。此外构造时会处理带msg.value场景的 gas 语义若value非零gas/min_gas会被加上CALL_VALUE_STIPEND2300EVM 中值调用时传递给被调用方的固定 gas 补贴确保后续匹配时 gas 比较口径一致。匹配与满足性判定由两个方法完成observestate.rs#L1353-L1359当路径上发生一次命中调用时调用若exact observed expected说明计数已满再发生调用即为多调用返回false拒绝该次观察is_satisfiedstate.rs#L1361-L1363路径结束时检查exact要求observed expected非 exact 只需observed expected。这也解释了为什么修复前重复注册会产生结构上不可达的条目第一个非计数条目在observed达到 1 后即已满足而第二个同键条目永远等不到自己的第一次观察——无论执行路径怎么走它都不可能满足只会让测试在路径结束时的满足性检查expected_calls.iter().all(ExpectedCall::is_satisfied)见 state.rs#L1085中无故失败。修复后同键非计数注册合并为一个条目expected累加路径执行时按总期望次数逐一消费语义自洽。五、测试锁定四组单元测试验证新语义修复并非仅停留在实现层面crates/evm/symbolic/src/runtime/state.rs的测试模块state.rs#L2529-L2614用四组单元测试把新语义固化了下来1.expected_call_zero_count_is_satisfied_only_if_call_never_happensstate.rs#L2533-L2556验证vm.expectCall(callee, data, 0)的边界语义——期望零次调用即该调用绝不能发生。初始即满足is_satisfied()为 true一旦某次调用尝试observe()返回false表示拒绝且不递增计数。同时对照count 1的常规语义先不满足、第一次 observe 后满足、第二次 observe 被拒绝。2.duplicate_non_counted_expect_call_merges_additivelystate.rs#L2558-L2575这正是修复的主线场景。连续注册两个非计数ExpectedCall断言expected_calls.len() 1未新增条目且expected 2次数被加法合并随后第一次observe()返回 true 但is_satisfied()仍为 false第二次observe()后才满足。3.duplicate_counted_expect_call_is_rejectedstate.rs#L2577-L2598先注册count 3的计数条目再注册count 5的同键计数条目断言返回Err(counted expected calls can only bet set once)再注册非计数条目断言返回Err(cannot overwrite a counted expectCall with a non-counted expectCall)且列表长度保持 1、expected保持 3。4.counted_expect_call_over_existing_non_counted_is_rejectedstate.rs#L2600-L2615反向场景——先注册非计数条目再用计数条目覆盖同样返回Err(counted expected calls can only bet set once)保证两个方向上的精确计数语义都不被破坏。这四组测试共同覆盖了注册函数的全部错误分支与合并分支构成回归防线防止修复在后续迭代中被回退。六、入口接线从 cheatcode 选择器到注册函数vm.expectCall的 6 个重载在符号执行端由 cheatcodes.rs#L1069-L1248 中的expectCall_0Call::SELECTOR至expectCall_5Call::SELECTOR六个选择器分支分别处理expectCallMinGas的两个重载紧随其后cheatcodes.rs#L1250-L1326。每个分支都遵循相同的参数解析模式callee通过read_abi_word_arg读取为符号表达式SymExprcalldata 通过read_abi_symbolic_dynamic_byte_exprs_arg读取为符号化的动态字节表达式受self.config.max_calldata_bytes上限约束再包装为SymBytes——这是符号执行区别于 concrete 的关键期望的 calldata 本身可以是包含符号值的表达式匹配时以前缀匹配calldata.prefix_condition见 state.rs#L1325构建符号条件有count的重载通过read_abi_u64_arg读取u64计数有msgValue的重载通过read_abi_concrete_word_arg读取具体值。所有分支最终统一收敛到set_expected_callcheatcodes.rs#L186-L203它构造ExpectedCall::new(...)后调用register_expected_call完成注册并根据注册结果返回Continue或带错误信息的Revert。因此本次修复的语义统一作用于全部 8 个入口6 个expectCall 2 个expectCallMinGas而非只修了某一个重载。七、对测试编写者的影响与建议基于本次修复后的语义在符号测试中编写vm.expectCall断言时有如下实践要点重复注册非计数expectCall是合法的加法惯用法vm.expectCall(addr, data); vm.expectCall(addr, data);等价于期望该调用至少发生 2 次与 concrete 模式行为一致可放心使用计数注册必须唯一对同一(callee, data)调用两次带count的expectCall会直接 revertcounted expected calls can only bet set once。如需总计 N 次请在一次调用中传入count N而不是重复注册计数与非计数不要混用无论先后顺序用非计数注册覆盖计数注册、或在计数注册后再追加非计数注册都会触发错误拒绝这是有意为之的确定性保护count 0表示禁止发生该边界值在测试中有明确覆盖常用于断言某调用绝不应出现的负向场景失败信息是即时、显式的由于注册阶段即返回 revert重复注册的错误会在测试早期暴露而不是在执行结束后的满足性检查中表现为难以定位的悬挂期望。结语这次针对foundry-evm-symbolic的 patch 修复本质上是让符号执行后端的vm.expectCall注册语义与 concrete 模式的文档化惯用法对齐非计数重复注册加法合并、计数注册唯一且拒绝覆盖、注册阶段即时报错。从 changelog 条目 到 注册函数实现 再到 四组回归测试仓库中形成了完整的缺陷描述—修复实现—测试锁定闭环。对于在 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),仅供参考
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

MATLAB多智能体时变编队控制闭环验证系统 2026/9/17 2:39:47

MATLAB多智能体时变编队控制闭环验证系统

简介:本资源是一套基于IEEE TCST经典论文实现的多智能体编队控制Matlab仿真程序,面向控制理论、无人系统协同及一致性算法方向的初学者与科研入门者,聚焦无人机/移动机器人集群的时变队形控制问题。压缩包共7个文件,含4个核心m脚本…

阅读更多 →
Java工具类静态属性注入的3种解决方案 2026/9/17 2:39:47

Java工具类静态属性注入的3种解决方案

1. 工具类静态属性注入的痛点与解决方案在Java开发中,工具类(Utility Class)是我们经常使用的一种设计模式。这类类通常包含一些静态方法,用于提供各种通用功能。然而,当我们需要在工具类中使用一些需要依赖注入的属性时,就会遇到…

阅读更多 →
PyTorch实战A2C调参:在Pendulum-v1上实现稳定收敛的完整指南 2026/9/17 2:39:47

PyTorch实战A2C调参:在Pendulum-v1上实现稳定收敛的完整指南

PyTorch实战 A2C 调参,这是个老话题,但每次写都感觉有新的东西可以挖。尤其是 Pendulum-v1 这个环境,看着简单——就一个倒立摆,动作空间也就一维扭矩,但真把 A2C 丢进去跑起来,你会发现它比 CartPole 刁钻…

阅读更多 →
FastapiAdmin生产级日志体系:可审计、可追溯、可告警的七参数配置军规 2026/9/17 2:39:47

FastapiAdmin生产级日志体系:可审计、可追溯、可告警的七参数配置军规

1. 这不是个“后台管理模板”,而是一套可审计、可追溯、可告警的生产级日志中枢FastapiAdmin 不是那种装完就能跑、跑起来就不管的玩具型后台框架。我用它搭过三个中型 SaaS 系统,从电商订单调度中心到医疗设备远程监控平台,最后都卡在同一个…

阅读更多 →
基于Python爬虫与深度学习的豆瓣影评情感分析实战 2026/9/17 2:39:47

基于Python爬虫与深度学习的豆瓣影评情感分析实战

简介:基于Python爬虫与深度学习方法的豆瓣影评分析项目,围绕数据采集、文本清洗、情感分类与可视化展示,提供一套完整的高校课程设计/毕业设计解决方案。面向AI、通信、自动化、电子信息、物联网等专业学生与科研人员,既可用于项目…

阅读更多 →
PySide6定时播放器开发:QMediaPlayer与APScheduler实战指南 2026/9/17 2:36:46

PySide6定时播放器开发:QMediaPlayer与APScheduler实战指南

简介:这套基于PySide6开发的校园广播播放系统,以完整源代码形式呈现,主要面向校园广播管理员、运维人员及Python GUI应用开发者。系统具备定时播放、自定义铃声、一键切换阴雨天与调休模式、批量修改与导入导出铃声等功能,可满足课…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞