资讯动态

Foundry Anvil 优化解读:eth_feeHistory 奖励百分位缓存的单次扫描(Percentile Sweep)

发布时间:2026/9/15 12:08:54 来源:尧图企业网站定制
Foundry Anvil 优化解读eth_feeHistory 奖励百分位缓存的单次扫描Percentile Sweep【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry导读eth_feeHistory是 EIP-1559 客户端向钱包、Gas 预言机等上层工具提供历史费用数据的关键 RPC 接口其中reward字段需要按请求者指定的百分位返回每个区块的交易有效奖励effective tip分布。本文基于 Foundry 仓库中的 Anvil 节点实现变更记录见 .changelog/anvil-fee-history-percentile-sweep.md深入解析一次针对该接口的 patch 优化将原本每次构建缓存条目时重复生成的百分位列表改为一次性构造并全局复用同时用单遍游标扫描sweep替代对每个百分位的独立二分/线性查找显著降低eth_feeHistory奖励缓存构建的计算开销。读完本文你将理解 Anvil 内部FeeHistoryCache的数据结构、百分位奖励的计算原理、该优化前后的差异以及它如何与eth_maxPriorityFeePerGas、fork 模式下的历史数据合并等特性协同工作。一、变更背景一次针对奖励缓存构建的 patch该 changelog 条目属于anvil: patch级别原文为Improvedeth_feeHistoryreward cache construction by sweeping reward percentiles once.即通过一次性扫描百分位来改进eth_feeHistory奖励缓存的构建过程。这是一个典型的低风险、性能收益明确的内部优化 patch不改变 RPC 对外返回的数据语义只改变数据计算的方式。结合源码crates/anvil/src/eth/fees.rs 与 crates/anvil/src/eth/api.rs可以看到该优化的落点集中在两个核心常量与一个关键函数REWARD_PERCENTILE_RESOLUTION百分位列表的分辨率REWARD_PERCENTILES预计算的 0.0100.0 百分位列表LazyLock静态常量全局只计算一次reward_percentiles()按百分位列表单遍扫描交易的内部函数。二、奖励百分位列表从每次重建到全局一次2.1 常量定义与静态初始化在 crates/anvil/src/eth/fees.rs 中定义如下/// Number of cached reward samples per percentile. pub(crate) const REWARD_PERCENTILE_RESOLUTION: f64 2.0; /// Percentile list from 0.0 to 100.0 with a 0.5 resolution (201 points). /// /// Constant across blocks, so it is computed once instead of being rebuilt on every /// create_fee_history_cache_item call. static REWARD_PERCENTILES: LazyLockVecf64 LazyLock::new(|| (0..200).map(|index| index as f64 / REWARD_PERCENTILE_RESOLUTION).collect());关键点REWARD_PERCENTILE_RESOLUTION 2.0表示每个百分位点对应的索引步长为 0.5即列表覆盖0.0, 0.5, 1.0, ..., 100.0共0..200共201 个百分位点REWARD_PERCENTILES使用LazyLockVecf64声明为进程级静态常量仅在首次访问时构造一次源码注释明确指出该列表跨区块恒定Constant across blocks因此被提升为静态常量避免在每次create_fee_history_cache_item调用时重复构建——这正是 changelog 所描述的优化前半部分sweeping reward percentiles once 中列表只构造一次的体现。对比旧实现可从注释推断旧代码在每次构建缓存条目时都重新生成一份完整百分位列表区块越多、缓存重建越频繁浪费越大新实现将其提升为全局惰性常量首次访问后零重建成本。2.2 为什么 201 个点覆盖完整百分位从 0.0 到 100.0 全覆盖任何合法请求的百分位0100 区间内的任意值都能在预计算数组中找到对应样本插值精度0.5 的分辨率意味着相邻百分位点间隔 0.5%足以支撑reward_at_percentile通过取整索引近似定位见下文 §4内存成本恒定每区块缓存条目中rewards: Vecu128固定为 201 个u128约 3.2 KB/区块在 fees.rs 的FeeHistoryCacheItem中作为普通字段存储。三、核心算法单遍游标扫描sweep3.1 计算原理reward_percentiles函数crates/anvil/src/eth/fees.rs接收已按有效奖励升序排序的交易数组(gas_used, effective_reward)与区块总 Gas 消耗为 201 个百分位点逐一计算对应的有效奖励/// Calculates percentile rewards from transactions sorted by effective reward. /// /// [REWARD_PERCENTILES] must remain ascending because the transaction cursor never rewinds. fn reward_percentiles(transactions: [(u64, u128)], block_gas_used: f64) - Vecu128 { let mut rewards Vec::with_capacity(REWARD_PERCENTILES.len()); let mut transactions transactions.iter().copied(); let Some((mut cumulative_gas, mut current_reward)) transactions.next() else { return rewards; }; for percentile in REWARD_PERCENTILES.iter() { let target_gas (percentile * block_gas_used / 100f64) as u64; while target_gas cumulative_gas { let Some((tx_gas_used, effective_reward)) transactions.next() else { return rewards }; cumulative_gas tx_gas_used; current_reward effective_reward; } rewards.push(current_reward); } rewards }算法要点语义定义百分位p的奖励 当累计 Gas 首次达到p / 100 * block_gas_used时所落到的那笔交易的有效奖励单遍扫描由于REWARD_PERCENTILES严格递增源码注释明确要求必须保持升序因为交易游标从不回退外层循环逐点推进内层while只在当前目标百分位超过累计 Gas 时才消费下一笔交易——每个百分位点共享同一个前向游标交易数组最多被完整遍历一次无回退never rewinds这是该实现正确性的前提也是其性能优于对每个百分位独立从头遍历方案的根本原因提前终止当交易耗尽如区块 Gas 未满时直接返回已收集的结果剩余百分位点保持为空。3.2 优化前后的复杂度对比朴素实现优化前对每个百分位点都需要从头或在累积意义上遍历交易直到达到目标 Gas。201 个百分位 × 每区块交易数单区块最坏为 O(201 × n)。单遍扫描优化后201 个百分位点共享一个游标交易数组整体只被扫描一遍单区块最坏为 O(201 n)。在自动化测试驱动的本地链Anvil 常用于测试区块频繁产生、交易密度高上该优化直接降低eth_feeHistory奖励构建的热路径成本。3.3 单元测试的验证fees.rs 内嵌的测试模块专门验证了新算法与参考实现的一致性reward_percentile_sweep_preserves_boundaries_and_empty_results覆盖空交易数组返回空奖励、零 Gas 边界如[(0, 10), (1, 20)]时前 200 个百分位点奖励恒为 10、第 201 个点为 20等边界情况reward_percentile_sweep_matches_reference_for_randomized_inputs用确定性伪随机数生成器LCG见next_random随机生成 2000 组交易序列与 Gas 组合逐一断言单遍扫描结果与参考实现reward_percentiles_reference即朴素逐点遍历完全一致验证了算法正确性不受交易数、Gas 缺口、零 Gas 交易等因素影响。四、缓存条目的构建与消费链路4.1 数据来源从区块头与收据提取交易信息create_fee_history_cache_itemcrates/anvil/src/eth/fees.rs负责为单个区块构建FeeHistoryCacheItem从区块头提取base_fee、excess_blob_gas、blob_gas_used并通过blob_params计算base_fee_per_blob_gas若区块与收据均可用从storage_info.block(hash)/storage_info.receipts(hash)获取计算gas_used_ratiogas_used / gas_limit与blob_gas_used_ratio相对blob_params.max_blob_gas_per_block()逐笔交易计算gas_used当前收据cumulative_gas_used减上一收据的累积值与effective_reward即effective_tip_per_gas(base_fee)按有效奖励升序排序后交给reward_percentiles生成 201 个百分位样本若区块或收据缺失则以 201 个 0 填充rewardsvec![0; REWARD_PERCENTILES.len()]保证缓存条目结构完整。注意这里按奖励升序排序transactions.sort_by_key(|(_, reward)| *reward)见 fees.rs正是上游reward_percentiles前置条件已排序的保证。4.2 双路径写入异步服务 RPC 兜底FeeHistoryCache的类型为ArcMutexBTreeMapu64, FeeHistoryCacheItemfees.rs以区块号为键有序存储。缓存写入存在两条路径异步服务FeeHistoryServicefees.rs实现Future轮询ChainNotifications新区块通知对每个新块调用insert_cache_entry_for_blockRPC 兜底由于异步服务可能落后于链头只在节点任务被 poll 时运行eth_feeHistory处理器在缓存缺失或哈希不匹配时按需现场计算同一条create_fee_history_cache_item逻辑再以单次加锁批量回填缓存crates/anvil/src/eth/api.rs。两条路径共用同一个构建函数保证数据口径一致缓存写入统一走insert_fee_history_cache_item并在超出MAX_FEE_HISTORY_CACHE_SIZE2048见 fees.rs时通过pop_first()裁剪最旧区块fees.rs。4.3 百分位请求的解析reward_at_percentileFeeHistoryCacheItem中存储的是固定 201 点的奖励样本而请求方可能只要求少数几个百分位如[50.0]。RPC 处理器通过reward_at_percentilecrates/anvil/src/eth/api.rs从预计算样本中按索引取近似值fn reward_at_percentile(rewards: [u128], percentile: f64) - u128 { let index (percentile * REWARD_PERCENTILE_RESOLUTION).round() as usize; rewards.get(index).copied().unwrap_or_default() }即请求的百分位p对应样本索引round(p × 2.0)与 §2.1 中 0.5 分辨率的列表一一对应。这正是预计算一次、任意请求即时查表的设计闭环无论请求方要 1 个还是 50 个百分位区块侧都只做一次 201 点的扫描后续全部是 O(1) 查表。4.4 请求参数校验在进入计算前fee_history处理器会先做参数校验api.rs任何百分位必须落在[0.0, 100.0]区间内百分位列表必须严格递增pair[0] pair[1]即非法block_count为 0 时直接返回空FeeHistoryblock_count上限 1024MAX_BLOCK_COUNT并受缓存范围约束超出best_number - fee_history_limit返回InvalidBlockRange。对应的行为由集成测试覆盖fee_history_rejects_invalid_reward_percentilesapi.rs断言非法百分位返回FeeHistoryError::InvalidRewardPercentiles合法值正常通过。五、优化带来的连锁收益5.1 eth_maxPriorityFeePerGas 的基石lowest_suggestion_tipapi.rs依赖FeeHistoryCacheItem.rewards获取当前区块所有百分位奖励的最小值作为建议小费再与MIN_SUGGESTED_PRIORITY_FEE1 gweifees.rs取较大者最终由eth_maxPriorityFeePerGas返回。由于奖励样本是预计算的 201 点列表小费建议无需额外遍历交易直接读取缓存即可——优化后的单遍扫描间接加速了该路径。5.2 fork 模式下的一致性在 fork 模式下eth_feeHistory处理器先对 pre-fork 区块段调用 fork provider 的fee_history再对 post-fork 段走本地缓存/兜底计算并通过merge_pre_fork_fee_historyapi.rs合并详见 api.rs 的注释说明。本地段无论来自异步缓存还是兜底计算都走同一条create_fee_history_cache_item因此本优化对 fork 场景的本地侧同样生效且不会改变合并后的数据语义。5.3 对外的可观测行为本 patch 不改变eth_feeHistory响应的字段结构、数值语义或错误码属于纯内部性能优化。对于通过该接口获取 Gas 预估如钱包估算maxPriorityFeePerGas的工具来说返回结果保持一致但 Anvil 在频繁出块、交易密集的测试场景下构建奖励缓存的开销更低。六、小结与扩展阅读anvil-fee-history-percentile-sweep这个 patch 的核心是把每区块重复构造百分位列表重构为全局一次性构造 单遍游标扫描 O(1) 查表三段式设计静态化REWARD_PERCENTILES201 点由LazyLock全局构造一次单遍化reward_percentiles用不回退的游标一次性为全部百分位点计算奖励交易数组只遍历一遍查表化reward_at_percentile按索引直接取样本任意请求即时响应。对于希望深入源码的读者建议按以下路径阅读缓存数据结构与裁剪策略crates/anvil/src/eth/fees.rsFeeHistoryService、insert_fee_history_cache_item奖励百分位算法与边界测试crates/anvil/src/eth/fees.rs 与 测试模块RPC 入口与参数校验crates/anvil/src/eth/api.rsfee_history处理器缓存缺失兜底与回填crates/anvil/src/eth/api.rs小费建议的消费方crates/anvil/src/eth/api.rslowest_suggestion_tip。【免费下载链接】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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价