Foundry Anvil 性能优化解析:`eth_getBlockReceipts` 大区块批量收据加速
2026/9/16 6:07:26 网站建设 项目流程

Foundry Anvil 性能优化解析:eth_getBlockReceipts大区块批量收据加速

【免费下载链接】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 补丁级变更(.changelog/anvil-block-receipts.md)展开,深入剖析 Anvil 节点中eth_getBlockReceiptsRPC 的实现原理、批量构建收据的优化手段及其在本地挖矿与 Fork 两种模式下的行为差异。读完你将理解该变更为何能显著加速大区块(包含大量交易的区块)的收据查询,并掌握如何在集成测试中验证收据与日志索引的正确性。

变更背景:一条聚焦大区块收据查询的 patch

该变更记录在仓库的 .changelog/anvil-block-receipts.md 中,以anvil: patch标记为针对 Anvil 的补丁级改进,内容只有一句话:

Speed upeth_getBlockReceiptsfor blocks with many transactions.

即:针对交易数量较多的区块,加速eth_getBlockReceipts的响应速度。这属于不影响公共 API 形态、只优化内部实现细节的补丁。要理解它,需要先看清 Anvil 中该 RPC 从网络层到后端存储层的完整调用链,再对照优化前后两种实现路径的差异。

RPC 入口:eth_getBlockReceipts的请求分派

eth_getBlockReceiptseth_getTransactionReceipt不同,它一次返回整个区块内所有交易的收据列表,而不是单笔交易收据。在 Anvil 中,该 RPC 方法在 crates/anvil/core/src/eth/mod.rs 处以sequence序列化方式注册,返回类型为Vec<FoundryTxReceipt>

其 HTTP 层处理函数位于 crates/anvil/src/eth/api.rs:

/// Handler for ETH RPC call: `eth_getBlockReceipts` pub async fn block_receipts(&self, number: BlockId) -> Result<Option<Vec<FoundryTxReceipt>>> { node_info!("eth_getBlockReceipts"); if number == BlockId::pending() { let transactions = self.pool.ready_transactions().collect::<Vec<_>>(); if transactions.is_empty() { return Ok(Some(Vec::new())); } return Ok(Some(self.backend.pending_block_receipts(transactions).await?)); } self.backend.block_receipts(number).await }

从这段代码可以提炼出 Anvil 对eth_getBlockReceipts的分派策略:

  • pending标签:不走已挖出区块路径,而是从交易池取出所有就绪交易(self.pool.ready_transactions()),交给pending_block_receipts先模拟执行 pending 区块再返回收据;若交易池为空则直接返回空数组,避免无谓计算。
  • 其他 BlockId(数字、latestsafefinalized、区块哈希等):统一交给后端Backend::block_receipts处理。

因此,优化是否生效,取决于后端block_receipts/mined_block_receipts的批量实现质量。

核心优化:已挖出区块收据的批量构建

对已挖出区块的查询最终落到 crates/anvil/src/eth/backend/mem/mod.rs 的mined_block_receipts。该函数是本条 patch 优化的主战场,其实现分为两条路径。

快路径:数据一致性检查

函数先根据BlockId解析出区块哈希,从blockchain.storage读取区块体,随后做一次一致性检查:若区块内任意一笔交易在storage.transactions中缺失,或其block_hash/transaction_index与区块体不符,则退化为逐笔查询——对每笔交易调用mined_transaction_receipt单独构建收据再收集成 Vec。这是为了应对历史数据不完整等异常情况而保留的兜底路径。

主路径:单次遍历批量构建收据并连续分配日志索引

在数据完整的前提下,函数走批量构建逻辑:

let mut receipts = Vec::with_capacity(block.body.transactions.len()); let mut next_log_index = 0; for block_transaction in &block.body.transactions { let transaction = storage.transactions.get(&block_transaction.hash())?; let log_count = transaction.receipt.logs().len(); let receipt = self.build_mined_transaction_receipt( &transaction.info, transaction.receipt.clone(), transaction.block_hash, &block, next_log_index, ); receipts.push(receipt.inner); next_log_index += log_count; } Some(receipts)

要点在于:

  1. 预先分配容量Vec::with_capacity(block.body.transactions.len())一次性分配好收据列表空间,避免多次扩容。
  2. 一次遍历完成所有构建:所有交易收据在一次循环内构建完成,且共享同一个block引用与同一把存储读锁。
  3. 日志索引前缀和连续分配:变量next_log_index记录"到当前交易为止该区块累计产生的日志数",每处理完一笔交易就累加其日志数量。这样区块内所有收据的日志log_index全局连续的(第 0 笔交易的日志从 0 开始,第 1 笔交易的日志接着上一笔的末尾继续编号)。

对比逐笔查询的复杂度差异

逐笔查询路径mined_transaction_receipt(crates/anvil/src/eth/backend/mem/mod.rs)在计算第index笔交易的日志起点时,需要遍历block.body.transactions[..index]累加前面所有交易的日志数:

let mut next_log_index = 0; for block_transaction in &block.body.transactions[..index] { next_log_index += storage.transactions.get(&block_transaction.hash())?.receipt.logs().len(); }

如果对区块内 N 笔交易逐笔调用该逻辑,总复杂度是 O(1 + 2 + … + N) =O(N²)——每笔交易都要重复扫描其前方的交易日志。而批量路径用单个next_log_index累加器在一次遍历内完成,复杂度为O(N)。这正是该 patch 对"交易数量很多的区块"(大区块)产生加速的根本原因:交易越多,逐笔方案的前缀和重复计算越昂贵,批量方案的优势越明显。

从源码结构看,本补丁的核心工作就是将原本可能逐笔构建收据的路径收敛为上述批量路径,并保证批量路径在数据完整时总是命中。

构建收据:build_mined_transaction_receipt的字段组装

无论批量还是逐笔路径,最终都调用 build_mined_transaction_receipt 组装标准TransactionReceipt。该函数负责:

  • Cancun 相关字段:根据区块头的excess_blob_gas与交易的blob_gas_used计算blob_gas_pricecalc_blob_gasprice);
  • 有效 Gas 价格effective_gas_price = transaction.effective_gas_price(block.header.base_fee_per_gas())
  • 日志 RPC 化convert_logs_rpc将日志填充block_numberblock_hashtransaction_hashtransaction_index与传入的next_log_index
  • 时间戳内联:收据携带区块时间戳(代码注释明确说明"避免额外的区块查询,例如 Otterscan API"场景);
  • Tempo 扩展:在is_tempo()模式下,额外恢复 Tempo 交易的 fee payer,并为非零 Gas 交易标注 fee token 地址。

一个值得注意的细节是:build_mined_transaction_receipt只在pending_block_receiptsmined_block_receipts的批量循环中被调用并传入连续递增的next_log_index,而逐笔路径mined_transaction_receipt内部需要自行计算前缀和——再次印证批量路径省去重复扫描的设计意图。

pending 区块:先执行再出收据

当请求目标是pending时,处理逻辑进入 pending_block_receipts:

pub async fn pending_block_receipts( &self, pool_transactions: Vec<Arc<PoolTransaction<FoundryTxEnvelope>>>, ) -> Result<Vec<FoundryTxReceipt>, BlockchainError> { let BlockInfo { block, transactions, receipts } = self.pending_block(pool_transactions).await?; let block_hash = block.header.hash_slow(); let mut pending_receipts = Vec::with_capacity(receipts.len()); let mut next_log_index = 0; for (info, receipt) in transactions.iter().zip(receipts) { let log_count = receipt.logs().len(); let receipt = self.build_mined_transaction_receipt( info, receipt, block_hash, &block, next_log_index, ); pending_receipts.push(receipt.inner); next_log_index += log_count; } Ok(pending_receipts) }

pending 收据是对交易池中交易执行一次模拟挖矿pending_block)后得到的,其批量循环同样采用next_log_index连续分配日志索引的方式。值得注意的是,pending 块没有真实区块哈希,代码以hash_slow()计算的哈希作为收据的block_hash,与已挖出区块的收据形态保持一致。

Fork 模式:RPC 回源与本地缓存

当 Anvil 以--fork-url方式运行、请求的区块号落在分叉点之前时,收据需要从远端 RPC 获取。后端block_receipts(crates/anvil/src/eth/backend/mem/mod.rs)先查本地已挖出区块,未命中且存在 fork 时才委托给 fork 后端。

Fork 后端的 block_receipts 实现包含两层关键逻辑:

  1. 本地缓存优先:先查self.storage_read().block_receipts这个HashMap<u64, Vec<FoundryTxReceipt>>,命中则直接返回,避免重复回源远端 RPC;
  2. 回源并写缓存:未命中且区块位于 fork 边界之前时,调用self.provider().get_block_receipts(BlockId::from(number))一次性拉取整块收据,逐条解码为FoundryTxReceipt后写入缓存再返回。源码注释还提示了一个已知约束:alloy 无法从结果中判断区块是否存在,因此"区块不存在"的判定暂时由 Anvil 自行实现(见 crates/anvil/src/eth/backend/fork.rs 的 TODO)。

该缓存机制确保同一 fork 区块的收据查询只回源一次,后续请求全部命中内存,同样有助于多交易大区块场景下的响应提速。

测试验证:收据数量、日志索引与边界行为

仓库中的集成测试从多个维度验证了eth_getBlockReceipts的行为,可作为理解该补丁语义的佐证:

  • 日志索引连续分配:crates/anvil/tests/it/logs.rs 的get_block_receipts_assigns_log_indices关闭自动挖矿后,向分别产生 1 条、0 条、2 条日志的三个地址发送交易并手动出块,随后断言eth_getBlockReceipts返回的日志索引为[vec![0], vec![], vec![1, 2]]——即日志索引跨交易全局连续,与next_log_index累加逻辑一一对应。
  • fork 区块、未来区块与哈希查询:crates/anvil/tests/it/fork.rs 的test_block_receipts验证了三个边界:fork 块(14608400)能取到收据、未来区块(14608401)返回None、按区块哈希查询同样能命中。
  • pending 收据:crates/anvil/tests/it/api.rs 的can_get_pending_block在关闭自动挖矿、发送一笔交易后,验证eth_getBlockReceipts(pending)能返回 pending 块的收据。
  • 多链 fork 兼容性:crates/anvil/tests/it/fork_chains.rs 提到eth_getBlockReceipts一次解码整块收据,某条链的专属收据类型无法解码会拖垮整块查询,且公共端点并非都允许该 RPC,因此该场景下仅做尽力而为的检查。

小结

围绕.changelog/anvil-block-receipts.md记录的这条 patch,可以从源码层面还原其完整价值:Anvil 的eth_getBlockReceipts在已挖出区块路径上以单次遍历 + 日志索引前缀和累加的方式批量构建收据,将大区块场景下的时间复杂度从逐笔查询的 O(N²) 收敛到 O(N);pending 路径复用同一批量构建函数;fork 路径则以内存缓存避免重复回源。配套集成测试覆盖了日志索引连续性、fork 边界、pending 与多链解码等关键语义,保证了优化后的行为正确性。

【免费下载链接】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),仅供参考

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

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

立即咨询