Foundry Anvil 在 eth_simulateV1 中输出区块访问列表哈希:Amsterdam 硬分叉后的实现与验证
【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry
本篇围绕 Foundry 仓库中一条 Anvil 的 patch 级变更展开:在eth_simulateV1返回的模拟区块头中,将计算得到的区块访问列表哈希(Block Access List Hash,BAL Hash)随头部字段一并输出,且仅在 Amsterdam 硬分叉生效之后才填充该字段。读完本文,你将掌握eth_simulateV1模拟区块头的字段构成、区块访问列表哈希的计算与放置逻辑、Amsterdam 分叉开关对它的门控方式,以及仓库中用于验证这一行为的集成测试细节,可直接在自己的链上开发与测试工具链中复现与使用。
变更背景:一条 changelog 背后的功能
.changelog/anvil-simulate-block-access-list-hash.md是仓库采用 changelog 片段(fragment)机制维护的变更记录,原文只有一句话:
anvil: patch— Include the computed block access list hash ineth_simulateV1block headers after Amsterdam.
它声明了一个 patch 级(向后兼容的缺陷修复/小增强)行为变更:在 Amsterdam 硬分叉激活之后,Anvil 的eth_simulateV1返回的每个模拟区块头中会包含计算得到的区块访问列表哈希字段。
这里的"区块访问列表哈希"对应的是围绕区块级访问列表(Block Access List,BAL)的规范——在仓库依赖的alloy_eips中由eip7928模块承载。从 crates/anvil/tests/it/simulate.rs 的导入可以看到该模块暴露的核心类型:
AccountChanges:某个账户在区块生命周期内发生的全部变更(nonce 变更、存储槽读写/变更);SlotChanges、StorageChange、NonceChange、BlockAccessIndex:描述具体存储槽与 nonce 变化,并携带其在访问序列中的索引;compute_block_access_list_hash:将排序后的AccountChanges列表计算为最终的 32 字节哈希;EMPTY_BLOCK_ACCESS_LIST_HASH:空访问列表对应的常量哈希。
简言之,区块访问列表试图把"区块执行过程中读写了哪些账户与存储槽"以确定性的方式压缩成一个可验证的哈希,Anvil 在模拟(eth_simulateV1)与出块路径上都需要正确携带它,以保证返回的区块头与主网共识规则一致。
行为细节:什么条件下会出现 blockAccessListHash
该变更的核心行为可以用"分叉门控 + 字段填充"两句话概括:
- Amsterdam 之后才填充:只有当前模拟区块时间戳已经激活 Amsterdam 硬分叉时,区块头中才会带上
blockAccessListHash字段;在此之前(例如 Osaka 或更早硬分叉下)该字段不会出现。 - 每个模拟区块独立计算:
eth_simulateV1可以一次请求多个blockStateCalls产生一串连续模拟区块,每个区块头都携带自己的访问列表哈希,且后一个区块的parentHash链到前一个区块的hash,因此访问列表哈希会实质影响链式区块哈希的最终结果。
这一行为在 crates/anvil/tests/it/simulate.rs 的test_simulate_block_access_list_hash_rpc中被逐条断言:测试分别在Osaka与Amsterdam两种硬分叉下、以及traceTransfers为false/true两种参数组合下发起eth_simulateV1请求,并断言:
- Amsterdam 下返回的区块对象带有
blockAccessListHash且值等于本地用compute_block_access_list_hash预计算的expected_hash; - 非 Amsterdam(Osaka)下区块对象不包含
blockAccessListHash键; - 区块头的
hash与header.hash_slow()一致,即该字段确实参与头部哈希计算; - 对一个"空调用"的模拟区块,
blockAccessListHash仍是字符串但值不同于有访问行为的区块,说明空区块使用空列表的哈希,而一旦有状态访问,哈希即随之变化。
源码实现解析:哈希从哪来、放到哪里去
1. 模拟路径:从 BAL 状态构建器到区块头字段
eth_simulateV1的核心实现在 crates/anvil/src/eth/backend/mem/mod.rs 的simulate_at内部。模拟开始时(L8061),cache_db.bal_state被初始化为带 BAL 构建器的状态跟踪器:
cache_db.bal_state = BalState::new().with_bal_builder();执行完一个区块内的全部调用后,模拟循环从 BAL 状态构建器中取出已收集的访问列表数据,并计算区块访问列表哈希(L8456-L8459):
let block_access_list_hash = cache_db .bal_state .take_built_alloy_bal() .map(|bal| compute_block_access_list_hash(bal.as_slice()));随后该值被写入Header的block_access_list_hash字段(L8460-L8461),与logs_bloom、transactions_root、receipts_root、parent_hash、base_fee_per_gas等字段一起构造出完整的模拟区块头,最后通过header.hash_slow()计算区块哈希(L8493)并作为parentHash传递给下一个模拟区块。
Header结构体中对应字段的定义可以在 crates/anvil/core/src/eth/block.rs 一带看到:block_access_list_hash是一个可空的B256字段,其余各处(如该文件 的默认构造路径)在非 Amsterdam 语境下保持None。
2. 分叉开关:is_amsterdam 如何决定字段是否出现
在 crates/anvil/src/eth/backend/mem/mod.rs 处,Anvil 用创世配置中的amsterdam_time判定当前区块时间戳是否已进入 Amsterdam:
let is_amsterdam = genesis.config.amsterdam_time.is_some_and(|fork| fork <= timestamp);- 若
amsterdam_time未配置(链未声明 Amsterdam 激活时间)或当前区块时间戳早于激活时间,则is_amsterdam为false,模拟区块头中的block_access_list_hash保持None,RPC 输出中不会出现blockAccessListHash键; - 反之则按上文流程填充计算哈希。
同一个开关也作用于常规出块路径:在 L9581 处,Anvil 出块时为 Amsterdam 之后的区块写入EMPTY_BLOCK_ACCESS_LIST_HASH(空列表哈希),保证即使区块没有任何访问行为,头部字段也是确定性的常量而非缺失。
3. 从调用链到 RPC 输出
模拟产生的SimulatedBlock最终以 RPC 对象返回:区块头被包装进AnyRpcHeader并序列化为 JSON,blockAccessListHash因此作为区块对象的顶层字段暴露给调用方(见 crates/anvil/src/eth/backend/mem/mod.rs 附近对alloy_rpc_types::Block的组装逻辑)。也就是说,这次 changelog 变更不是"另起炉灶",而是在既有eth_simulateV1区块构造流程中补上了 Amsterdam 规范要求缺失的头部字段,使模拟结果与真实出块行为对齐。
测试验证:如何确认该字段行为正确
仓库用 crates/anvil/tests/it/simulate.rs 中的test_simulate_block_access_list_hash_rpc全面覆盖了这一变更,其验证手法值得借鉴:
构造确定性的访问模式。测试通过stateOverrides将系统合约(HISTORY_STORAGE_ADDRESS、BEACON_ROOTS_ADDRESS、提款/存款请求预部署地址等)的代码覆盖为0x,再对HISTORY_STORAGE_ADDRESS与WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS注入固定的写槽字节码(0x602a5f5500,即向 slot 0 写入 42),对测试合约注入0x5f5450602a60015500(读 slot 0、写 slot 1),从而让"谁读了哪个槽、谁写了哪个槽"完全确定,便于在测试侧用同一套AccountChanges数据结构复算期望哈希。
本地复算期望值。测试手工构造expected(系统合约的存储变更、from账户的 nonce 变更、contract的存储读与存储变更、beneficiary的空变更),按地址排序后调用compute_block_access_list_hash得到expected_hash(L105-L129),再与 RPC 返回值逐字节比对。
分叉与参数矩阵。测试对Osaka/Amsterdam两种硬分叉 ×traceTransfers两种取值分别断言:Amsterdam 下有字段且等于期望哈希;Osaka 下无字段;第三个空模拟区块的哈希存在但不同于有访问的区块;且所有区块的hash与header.hash_slow()一致、相邻区块parentHash正确串联。
如需在本地复现,可在仓库根目录用cargo test -p anvil并指定该测试名(test_simulate_block_access_list_hash_rpc)运行 anvil 集成测试套件,测试会自动拉起内存节点、通过 HTTP RPC 发起eth_simulateV1请求并完成上述断言。
对使用者的影响与注意事项
- 面向链上工具与测试框架:如果你依赖
eth_simulateV1做交易批处理预演、MEV 策略回放或状态差异分析,在配置了 Amsterdam 激活时间的链上,现在可以直接从返回区块对象中读取blockAccessListHash,无需自行复算访问列表。 - 分叉配置决定字段有无:Anvil 默认测试链如果未配置
amsterdam_time,该字段不会出现;只有在genesis.config.amsterdam_time配置且区块时间戳达到激活时间后才会填充,判断时应以链配置而非 Anvil 版本为准。 - 字段参与区块哈希:
block_access_list_hash是头部字段之一,直接影响header.hash_slow()的结果,进而影响模拟区块的parentHash链。任何依赖模拟区块哈希做后续计算的场景都应意识到该字段已纳入哈希输入。 - 空区块也有确定性哈希:Amsterdam 下的空访问区块使用
EMPTY_BLOCK_ACCESS_LIST_HASH常量(出块路径)或对空列表计算得到的哈希(模拟路径),而不是缺失字段,消费方可以据此区分"未激活分叉"与"激活但无访问"两种状态。
总结
这条 changelog 片段背后是一次小而完整的共识对齐改动:Anvil 在eth_simulateV1的模拟区块头构造流程中接入区块访问列表哈希计算,并通过amsterdam_time时间戳判断实现分叉门控;配套集成测试用确定性状态覆盖与本地复算的方式,对字段有无、哈希值正确性、区块哈希一致性做了全矩阵验证。理解这条变更,等于同时掌握了 Anvil 模拟区块头的构造链路、BAL 哈希的计算入口,以及 Foundry 仓库"changelog 片段 + 集成测试"驱动的功能开发模式。
【免费下载链接】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),仅供参考