深入解析 Foundry Anvil fork 端点身份校验:reset 与换源时的严格性与原子化
【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry
同一个--fork-url,背后的节点却被换了,旧缓存还算数吗?Foundry Anvil 现在的回答是“不算”:在anvil_reset与anvil_setRpcUrl期间,它按权威身份校验 fork 端点身份(endpoint identity),并整体原子地提交。本文带你拆这套校验机制。
一、60 秒速览 📌
开头抛了风险,这里先花 60 秒把词汇表对齐:
| 术语 | 一句话解释 |
|---|---|
端点身份ForkEndpointIdentity | 远端执行上下文的一整组指纹:链 ID、网络画像、hardfork、实例、fork 锚定区块,和 URL 字符串完全是两回事 |
权威身份is_authoritative | 远端成功上报过 hardfork(经由anvil_nodeInfo)时身份才权威,才有资格进入严苛校验 |
| 严格 | 识别出 Anvil 之后,探测失败按错误处理;同 URL 上身份一变,缓存整体作废 |
| 原子 | 要么全过然后生效,要么全不生效就放弃,不存在半切换的中间态 |
| staged 提交 | 先把整套替换(新 fork、新 DB、新缓存租约)备齐再一次性切换,对应StagedMemoryReset/StagedForkCacheLease |
二、身份模型:为什么不能只比对 URL
词汇表有了,第一个问题来了:Anvil 为什么不干脆只比对 URL 字符串?
打个比方:URL 像门牌号,端点身份是住户的“身份档案”。门牌不变,里面住的却可能整个换了一家人——节点重启、数据目录切换、执行配置升级,都会让同一个地址背后变成完全不同的执行上下文。只盯门牌,你永远不知道对面是不是还是同一位交易对手。
所以 Foundry 把身份建模成一个结构体(crates/anvil/src/eth/backend/fork.rsL53-L63):
#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct ForkEndpointIdentity { pub(crate) execution_chain_id: u64, pub(crate) source_chain_id: u64, pub(crate) network: Option<NetworkVariant>, pub(crate) network_profile: Option<NetworkConfigs>, pub(crate) hardfork: Option<FoundryHardfork>, pub(crate) instance_id: Option<B256>, pub(crate) source_fork_block_number: Option<u64>, pub(crate) source_fork_block_hash: Option<B256>, }每个字段各自回答的问题:
| 字段 | 它回答什么问题 |
|---|---|
execution_chain_id | 远端实际在跑哪条链? |
source_chain_id | fork 的数据取自哪条链? |
network/network_profile | 它属于哪个网络家族?用于跨家族判定 |
hardfork | 远端上报到哪个硬分叉?——这一项有值,身份才算权威 |
instance_id | 具体是哪一个 Anvil 实例?顺带能判断“我是不是在跟自己说话” |
source_fork_block_number/source_fork_block_hash | fork 锚在哪个区块上? |
权威性判定只有一行(fork.rs L65-L69):hardfork为Some即权威。因为只有远端成功应答过anvil_nodeInfo,它才有资格上报 hardfork。
探测本身还有两段式节奏,由AnvilNodeInfoProbe实现(crates/anvil/src/config.rsL109-L141):
- 首次成功响应
anvil_nodeInfo之前,探测失败只被当作“可选能力不可用”,不拖慢启动,标准 RPC 读取层照常暴露端点级错误; - 一旦响应成功(或缓存身份已识别出 Anvil),此后任何探测失败都会直接作为错误返回——否则端点被悄悄重置、执行配置被偷偷替换,你就看不见了。
三、两条"关卡"路径:先过哪一关?
身份既然是标尺,那两条变更路径各自在哪设卡?先看更复杂的anvil_reset,再看换源的anvil_setRpcUrl。
fork reset 的五道检查(anvil_reset)
整条 reset 路径由stage_fork_reset承担(crates/anvil/src/eth/backend/mem/mod.rsL4358-L4468)。你可以把它想象成连续五道关卡:
第一关:同 URL 的权威身份漂移。ForkCacheSource::authoritative_identity_changed_at_same_url(mem/mod.rs L276-L287)比对新旧身份,但只有新旧至少一方是权威身份时才进入严格比较;匿名 RPC 经同一 URL 复用时保留原有缓存行为,不会被误伤。
第二关:跨网络家族直接拒绝。你没显式选网络、而新端点的网络画像又不被支持时,返回invalid_params,报错话很直白:Anvil 不支持跨网络家族 reset,请用匹配的网络配置另起一个新实例。
第三关:自环检测。新端点的instance_id恰好等于本地serving_instance_id时,说明你想把 Anvil reset 回它自己的 RPC,当场拒绝。
第四关:fork 区块哈希核对。取回远端 fork 区块,header 哈希与解析出的block_hash对不上就返回Ok(None)放弃——高度一样但内容不对的区块不算数。
第五关:提交前二次验证。真正提交前,fork_urls_match_context把 URL、身份、区块号、区块哈希再核一遍(L4458-L4468),专抓"验证之后、提交之前"远端上下文发生的漂移;对不上照旧Ok(None)。
换源时为什么要清空旧身份提示(anvil_setRpcUrl)
anvil_setRpcUrl的 handler 在crates/anvil/src/eth/api.rs(L601-L643),关键有三点:
- 新 URL 必须重新解析真实身份。旧端点留下的身份提示不可信,它可能把一个不受支持的网络藏在背后,所以
replacement_fork_provider会实际探测新 URL,解析出真实网络家族与身份; - 清空旧链 ID 提示。先把
fork_chain_id = None,强制全程以"实际解析到的源身份"为准,而不是旧端点的离线提示; - 全程串行化。整个流程握在
reset_lock下执行,身份读取与重置转换之间不存在交错。
let _reset = self.reset_lock.lock().await; validation_config.fork_chain_id = None; let (provider, endpoint_identity) = validation_config .replacement_fork_provider( &url, expected_identity, block_number, block_hash, self.instance_id(), ) .await?;验证通过后,provider、fork_urls、endpoint_identity在lifecycle_lock与 mining 锁下一次写入,同时把node_config.fork_endpoint_is_anvil同步成新身份的权威性,后续不带 URL 的 reset 就能用上更新后的端点信息。
四、原子提交:一次"舞台式"切换 🔁
关卡看明白了,下一个问题:过了关,切换动作本身怎么做?答案是 staged——像戏剧里的换景。
运行中的后端是舞台,新替换在后台备齐:新的 fork(ClientFork)、新的 DB、新的缓存租约(StagedForkCacheLease)先在"舞台侧"全部就位,期间谁也不许碰运行中的后端。备齐后的整体就是StagedMemoryReset——一个"等待原子提交的内存替换"结构(mem/mod.rs L248-L258)。
只有五道关全部通过,切换才在一次提交里整体生效。任何一步失败——区块哈希不符、URL 与上下文漂移——都走Err分支或Ok(None),缓存租约回滚,运行中的后端毫发无损。这就是"原子"的含义:要么全过、整体切换,要么全不生效、场景恢复原样。不会出现半新半旧的混杂状态,也不存在"校验过了又被别人覆盖"的身份。
五、身份一变,缓存如何作废
切换讲完了,可磁盘上的 fork 缓存怎么办?严苛校验最终保护的就是它。
ForkCacheSource(mem/mod.rs L260-L288)记录"最近一次提交 fork 的供应端点":rpc_url加身份一对。它的漂移判定规则就是第一关那套:URL 相同、至少一方为权威身份、身份又不同,三者齐备才认定缓存作废。
判定作废后发生三件事:
- 新 DB 先清空为状态快照,只写入新 fork 的区块头(L4396-L4401),不继承旧端点的存储;
ForkCacheNamespace(L290-L303)按source_chain_id加 URL 哈希定位缓存目录,文件名即storage-{keccak256(url)}.json;旧、新两侧的命名空间都进失效列表(L4403-L4419);- 最终在提交阶段原子地失效这些命名空间并丢弃旧缓存状态(
discard_old_cached_state,L4420-L4421)。
于是即便 URL 一个字符没动,只要身份变了——hardfork、链 ID、网络画像,哪一项算数——旧缓存就不会再被复用。
六、测试如何锁定这套行为
作废做得这么坚决,怎么证明它不会误伤?仓库里有三处测试,各锁一条规则:
| 规则 | 对应断言 | 位置 |
|---|---|---|
| hardfork 上报决定权威性 | 匿名身份is_authoritative() == false,权威身份为true | crates/anvil/src/config.rsL2625-L2638 |
| 上下文保持、实例可换 | 换源后endpoint_identity与换源前context_eq,而instance_id已等于新目标实例的 ID | crates/anvil/src/eth/api.rsL5310-L5318 |
| 至少一方权威才严格比对 | 用test_endpoint_identity辅助构造匿名(无 hardfork、无实例 ID)与权威(带 hardfork、B256实例 ID)身份,验证不同实例 ID 下的身份比较与缓存来源判定 | crates/anvil/src/eth/backend/mem/mod.rsL9699、L9876-L9899 |
其中第二行最能体现设计意图:换源之后,"上下文"没变、只有"住户"换了——这正是context_eq想表达的语义,它比较链 ID、网络、hardfork、fork 锚点,唯独不比较instance_id。
七、收尾速览 ⚠️
最后换个视角,从风险场景看这套机制挡住了什么:
| 风险场景 | 没有这次改动会怎样 | 现在的行为如何挡住它 |
|---|---|---|
| 同 URL 背后节点被换 | 旧缓存照用,新旧状态混在一起,状态污染 | 同 URL 权威身份漂移触发 DB 清空 + 双命名空间失效 +discard_old_cached_state |
| reset 期间远端上下文漂移 | 提交了一个身份与预期不符的 fork | 提交前fork_urls_match_context二次核对,不符则Ok(None)整体放弃 |
| 把 Anvil 当自己的 fork 源 | 自环 reset,节点把自己锁死 | instance_id == serving_instance_id时自环检测直接拒绝 |
| 换源切到不受支持的网络 | 旧端点的链 ID 提示把不受支持的网络藏起来 | 换源强制重新解析真实身份,旧提示被fork_chain_id = None清掉 |
一句话总结:从今往后,Anvil 不再把 URL 字符串当脸面——reset 与换源都拿权威身份当标尺,严苛校验与整体切换同步到位,陈旧缓存、跨家族误切换和自环 fork 通通被拦在关卡之外。
【免费下载链接】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),仅供参考