同一个 URL 背后换了条链,Anvil 怎么知道?深入剖析 Foundry 的 fork 端点身份校验机制
2026/9/19 8:24:22 网站建设 项目流程

同一个 URL 背后换了条链,Anvil 怎么知道?深入剖析 Foundry 的 fork 端点身份校验机制

【免费下载链接】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跑 fork 的 Anvil 实例,最隐蔽的故障不是连不上,而是"连上了,但连错了":同一 URL 背后的节点被重启、换了数据目录,甚至整个换成了另一条链,而 Anvil 手里只剩一串 URL 字符串,缓存目录名(storage-{keccak256(url)}.json)看起来还是合法的。Foundry 的这项 Anvil patch 就是为此设计的——在anvil_resetanvil_setRpcUrl两条路径上,用一份可验证的端点身份ForkEndpointIdentity)取代 URL 字符串做判断,且校验与提交要么整体成功、要么整体放弃,不允许出现"旧身份已提交、新身份在途中"的半状态。

被校验的对象:ForkEndpointIdentity 的字段与权威性

校验的前提是先把"远端到底是谁"建模成一个可比对的值。这个结构定义在 crates/anvil/src/eth/backend/fork.rs(L53-L63):

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端点当前执行的链 ID判断远端执行上下文是否变化
source_chain_idfork 数据的来源链定位缓存目录(命名空间)
network/network_profile网络变体与完整执行画像判断能否跨网络家族 reset
hardfork端点上报的硬分叉权威性的唯一判据
instance_id端点实例指纹(B256自环检测:"是不是我自己"
source_fork_block_number/hashfork 锚定区块提交前核对区块未漂移

权威性如何判定:hardfork 上报决定一切

规则只有三行(fork.rs L65-L69):

pub(crate) const fn is_authoritative(self) -> bool { self.hardfork.is_some() }

hardfork只在远端成功响应了anvil_nodeInfo探测后才会被填充。换句话说,普通 RPC 节点永远只能得到"匿名身份"(hardfork 为None),只有另一个 Anvil 实例才能给出权威身份。这个设计决定了后续所有严格化行为的触发条件——对匿名端点,机制刻意保持宽松,避免误伤存量用户。

探测本身的策略在 crates/anvil/src/config.rs(L109-L153)的AnvilNodeInfoProbe中体现:首次成功响应之前,探测失败只算"可选能力不可用",静默返回None;一旦探测成功(或缓存身份已识别出 Anvil),此后任何探测失败都直接作为错误抛出。这条规则堵住的是另一种漂移:端点被重置后,身份探测悄悄失败而校验被跳过。此外还有一个配套的超时常量FORK_IDENTITY_PROBE_TIMEOUT(500ms),防止不响应的 RPC 拖住启动路径。

身份解析的完整流程在resolved_fork_endpoint_identity(config.rs L1678-L1696):先经anvil_nodeInfo判断是否 Anvil,再用anvil_metadatainstance_id与 fork 锚点,最后用ensure_fork_network_supported拒绝 Anvil EVM 无法执行的链(如 zkSync Era)。

一个刻意的设计:context_eq 忽略 instance_id

context_eq(fork.rs L71-L80)比较全部上下文字段,唯独排除instance_id。含义是:同一执行上下文下的新实例允许接管(比如源端 Anvil 重启、换了实例指纹),但上下文本身(链 ID、网络、hardfork、fork 锚点)一旦不同就拒绝。这个"上下文守恒、实例可换"的语义是两条运行时路径共同遵守的契约。

机制拆解:两条路径如何做到"先验证、后原子提交"

路径一:anvil_setRpcUrl 的 URL 替换

handler 位于 crates/anvil/src/eth/api.rs(L601-L643)。核心动作分两步,中间隔着完整的验证:

let _reset = self.reset_lock.lock().await; // ... validation_config.fork_chain_id = None; // 清空旧的链 ID 提示 let (provider, endpoint_identity) = validation_config .replacement_fork_provider( &url, expected_identity, block_number, block_hash, self.instance_id(), ).await?;

两个关键决策都在这里:

  1. 旧端点留下的身份提示不可信fork_chain_id被显式置None,强制replacement_fork_provider对新 URL 重新走一遍身份解析——否则一个不支持的网络可以藏在旧端点的离线提示后面,绕过ensure_fork_network_supported检查。
  2. 验证阶段自身也要抗漂移。config.rs L1698-L1738 的replacement_fork_provider是一个 3 次尝试的循环:先解析一次身份(before),核对 fork 区块哈希,再解析一次(after);before != after说明远端上下文在解析窗口内变了,直接重试,3 次后报"identity and active fork block were being resolved"错误。通过条件包括:before.context_eq(expected)(上下文必须与旧端点一致)、区块哈希匹配、以及自环检查——instance_id == serving_instance_id时报 "cannot set Anvil's fork provider to its own RPC endpoint"。

验证通过后才进入提交阶段:持有lifecycle_lock与 mining 锁,一次性写入providerfork_urlsendpoint_identity,并同步node_config.fork_endpoint_is_anvil = endpoint_identity.is_authoritative()。同步node_config不是顺手为之——函数尾部注释明确,后续一次不带 URL 的fork reset 会复用这份端点信息。整个函数被reset_lock串行化,api.rs 顶部注释说明其职责就是"身份读取与重置转换之间不存在交错"。

路径二:anvil_reset 的 staged 校验

fork 重置走 crates/anvil/src/eth/backend/mem/mod.rs(L4277 起)的prepare_fork_resetstage_fork_reset。结构化的意图写在注释里:"Builds and validates one complete fork replacement without mutating the live backend."

stage_fork_reset(L4334-L4468)的校验清单按执行顺序:

  1. 构建隔离环境:克隆node_configstaged_config,基于它setup_fork_db_config建出独立的staged_dbClientFork,运行中的后端此刻未被动过;
  2. 缓存身份变更检测ForkCacheSource::authoritative_identity_changed_at_same_url判断同一 URL 上权威身份是否变了(细节见下一节);
  3. 跨网络家族拒绝:未显式选择网络且新端点的network_profile不被当前实例支持时,返回invalid_params,提示需以匹配的网络配置启动新实例(L4404-L4414);
  4. 自环拒绝instance_id == serving_instance_id时报 "cannot reset Anvil to its own RPC endpoint"(L4415-L4419);
  5. fork 区块核对:取远端 fork 区块,header 哈希与解析出的block_hash不符则返回Ok(None)放弃本次(L4420-L4426);
  6. 提交前二次验证fork_urls_match_context再次核对 URL、身份、区块号、哈希(L4434-L4444)——专门防"验证时还是旧端点、提交时已经漂移"。

Ok(None)Err都会触发rollback_staged_fork_cache回滚缓存租约,然后干净退出。外层prepare_fork_resetstage_fork_reset包在 3 次尝试的循环里,全部失败则报 "fork endpoint changed while the replacement was being staged"。

最终形态是StagedMemoryReset(同文件 L248-L258)——"等待原子提交的内存替换":新db、新storage、新fork全部就绪,一次提交完成切换。与路径一相同的提交锁组合(lifecycle + mining)保证提交瞬间没有并发的区块产出。

两条路径的共性与差异

维度anvil_setRpcUrlanvil_reset
串行化手段reset_lock+ 提交期 lifecycle/mining 锁staging 期完全隔离 + 提交期同款锁
漂移防御before/after 双解析 + 区块哈希核对二次fork_urls_match_context+ 3 次重试
期望身份来源当前 fork 配置中的endpoint_identityForkCacheSource(上次提交的来源)
失败代价返回Err,后端不变Ok(None)/Err均回滚租约,后端不变

共同不变量一句话:验证用的身份快照必须覆盖从解析到提交的全窗口,任何窗口内的漂移都使整次操作作废。差异在于 reset 的替换面更大(DB、缓存、EVM 环境都要换),所以用"staged 对象"承载;URL 替换只换 provider 与身份,staged 结构更轻。

边界与副作用:身份变了,磁盘缓存必须跟着作废

严格校验不只是防运行时污染,还保护着 fork 磁盘缓存。ForkCacheSource(mem/mod.rs L260-L288)记录最近一次已提交fork 的rpc_url + endpoint_identity,其变更判定函数有一个精妙的限定:

let authoritative = self.endpoint_identity.is_authoritative() || endpoint_identity.is_authoritative(); self.rpc_url == rpc_url && authoritative && self.endpoint_identity != endpoint_identity

只有新旧两侧至少一方是权威身份时才启用严格比对。注释说得很直白:匿名 RPC 端点通过同一 URL 复用时,刻意保留 Foundry 原有的缓存行为——严格化只对 Anvil 对 Anvil 的场景生效。

一旦判定身份已变,stage_fork_reset做三件事(L4372-L4397):

  1. 新 DB 先清空staged_db.clear_into_state_snapshot()后仅写入新 fork 区块头,确保不继承旧端点的存储;
  2. 收集失效命名空间ForkCacheNamespace(L290-L303)由source_chain_id+keccak256(url)定位缓存目录与storage-{hash}.json文件名,旧、新两个chain_id对应的命名空间都被加入失效列表(去重后);
  3. 提交时原子作废discard_old_cached_state标志置位后,缓存命名空间在提交阶段统一失效并丢弃旧缓存状态。

配合前文的两条防御线,这套机制实际封死了四类故障:URL 未变但端点被替换导致的缓存污染、验证与提交间的远端漂移、把 Anvil 指向自己 RPC 的自环、以及跨网络家族(如主网画像 reset 到另一执行配置)的误切换。

不变量与验证:测试锁死了哪些行为

仓库内嵌了三处针对性测试,各自钉住一条不可漂移的语义。

1. 缓存来源判定矩阵—— mem/mod.rs(L9699 起的test_endpoint_identity,断言集中于 L9854-L9899):用同一 URL(http://localhost)分别组合匿名身份与权威身份(含 hardfork 和B256实例 ID),验证authoritative_identity_changed_at_same_url在以下情况下的取值:匿名→权威(true)、权威→权威且不同(true)、权威→自身(false)、以及换 URL 后匿名→匿名(false)。锁住的是"匿名端点保留旧缓存行为、权威端点同 URL 必比"这条分界。

2. 权威性规则本身—— config.rs(L2625-L2638)直接断言:hardfork 为None的身份is_authoritative() == false,带 hardfork 与 instance_id 的为true。这条测试守护的是一整片机制的触发开关——放宽或收紧它都会级联影响缓存失效与探测严格化。

3. 上下文守恒、实例切换—— api.rs(L5292-L5313 的set_rpc_url_installs_context_equivalent_identity_with_new_instance):起两个同一 genesis 的 Anvil 实例,让第三个以 origin 为 fork 源,再anvil_set_rpc_url指向 target。断言替换后endpoint_identity.context_eq(identity_before)为真,同时instance_id精确更新为 target 的实例 ID。这正是context_eq排除instance_id的运行时验证:换实例合法,换上下文非法

收束

这项 patch 的实际收益可以压缩成一句话:URL 从此只负责"去哪里",身份负责"那里是谁"。Anvil 在 reset 与 URL 替换两条路径上都以权威身份为准绳做窗口级校验,用 staged 提交保证校验结果与生效动作不可分割;同 URL 换端点时缓存命名空间随之作废,自环与跨网络切换在入口即被拒绝。对 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询