ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

Foundry Anvil 性能优化解析:`eth_getBlockReceipts` 大区块批量收据加速 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_getBlockReceipts与eth_getTransactionReceipt不同它一次返回整个区块内所有交易的收据列表而不是单笔交易收据。在 Anvil 中该 RPC 方法在 crates/anvil/core/src/eth/mod.rs 处以sequence序列化方式注册返回类型为VecFoundryTxReceipt。其 HTTP 层处理函数位于 crates/anvil/src/eth/api.rs/// Handler for ETH RPC call: eth_getBlockReceipts pub async fn block_receipts(self, number: BlockId) - ResultOptionVecFoundryTxReceipt { 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数字、latest、safe、finalized、区块哈希等统一交给后端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)要点在于预先分配容量Vec::with_capacity(block.body.transactions.len())一次性分配好收据列表空间避免多次扩容。一次遍历完成所有构建所有交易收据在一次循环内构建完成且共享同一个block引用与同一把存储读锁。日志索引前缀和连续分配变量next_log_index记录到当前交易为止该区块累计产生的日志数每处理完一笔交易就累加其日志数量。这样区块内所有收据的日志log_index是全局连续的第 0 笔交易的日志从 0 开始第 1 笔交易的日志接着上一笔的末尾继续编号。对比逐笔查询的复杂度差异逐笔查询路径mined_transaction_receiptcrates/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_number、block_hash、transaction_hash、transaction_index与传入的next_log_index时间戳内联收据携带区块时间戳代码注释明确说明避免额外的区块查询例如 Otterscan API场景Tempo 扩展在is_tempo()模式下额外恢复 Tempo 交易的 fee payer并为非零 Gas 交易标注 fee token 地址。一个值得注意的细节是build_mined_transaction_receipt只在pending_block_receipts与mined_block_receipts的批量循环中被调用并传入连续递增的next_log_index而逐笔路径mined_transaction_receipt内部需要自行计算前缀和——再次印证批量路径省去重复扫描的设计意图。pending 区块先执行再出收据当请求目标是pending时处理逻辑进入 pending_block_receiptspub async fn pending_block_receipts( self, pool_transactions: VecArcPoolTransactionFoundryTxEnvelope, ) - ResultVecFoundryTxReceipt, 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_receiptscrates/anvil/src/eth/backend/mem/mod.rs先查本地已挖出区块未命中且存在 fork 时才委托给 fork 后端。Fork 后端的 block_receipts 实现包含两层关键逻辑本地缓存优先先查self.storage_read().block_receipts这个HashMapu64, VecFoundryTxReceipt命中则直接返回避免重复回源远端 RPC回源并写缓存未命中且区块位于 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),仅供参考
返回列表