
Foundry Anvil 区块 RLP 大小计算优化免分配计算本地区块 size 字段【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry导读本文基于 Foundry 仓库的变更记录 .changelog/anvil-block-rlp-size.md 展开剖析 AnvilFoundry 内置的本地以太坊节点如何在不分配内存、不完整编码整个区块的前提下计算出区块对象中的sizeRLP 编码长度字段。读完本文你将理解以太坊区块对象size字段的来源、Encodable::length()免分配求长的实现原理以及eth_getBlockByNumber、eth_simulateV1等 RPC 返回该字段时 Anvil 内部的实际调用链。变更记录原文解读.changelog目录下的每个.md文件都是一个标准化的变更片段changelog fragment由 frontmatter 与发布说明两部分组成。本文对应的片段全文如下--- anvil: patch --- Compute local block RLP sizes without allocating and encoding the full block.frontmatter中的anvil: patch表明该变更归属anvil包影响级别为patch缺陷修复 / 低风险优化不会引入破坏性 API 变更。发布说明则精确描述了变更内容计算本地区块local block即 Anvil 自行挖掘的区块而非从远程节点 fork 而来的区块的 RLP 大小时不再“分配缓冲区并完整编码整个区块”而是采用免分配的方式直接求长。按照 .changelog/README.md 中定义的版本契约patch级别的片段会被合并进下一个候选版本的发布说明中随 Anvil 的常规版本更新一起发布仓库规定每个 PR 都必须附带此类片段除非维护者打上L-ignore标签因此这个文件本身就是该优化被合入代码库的直接证据。背景区块对象中的 size 字段与 RLP 编码在以太坊 JSON-RPC 规范中区块对象包含一个size字段语义为该区块完整 RLP 编码后的字节长度。客户端工具区块浏览器、钱包、索引器等常依赖它估算区块体积对节点而言它也是区块对象的标准组成部分必须如实返回。RLPRecursive Length Prefix是以太坊用于序列化区块、交易、收据等核心数据结构的编码格式。区块 区块头Header 区块体BlockBody含交易列表、ommers、withdrawals 等整体再套一层 RLP list 前缀。计算size最朴素的做法是分配一个Vecu8缓冲区将整个区块encode进缓冲区取buf.len()作为 size。这种做法的缺点是为了一个数字白白做了一次完整的序列化并产生一次堆分配与一次 O(区块大小) 的拷贝。在区块交易量大的场景尤其是eth_simulateV1这种批量模拟多个区块时会积累可观的开销。本变更正是针对这一浪费做的优化。源码实现本地区块 size 的免分配计算Anvil 在把内部区块结构转换成 RPC 响应格式时计算size关键代码位于 crates/anvil/src/eth/backend/mem/mod.rs 的convert_block_with_hash/// Takes a block as its stored internally and returns the eth api conform block format. /// If known_hash is provided, it will be used instead of computing hash_slow(). pub fn convert_block_with_hash(self, block: Block, known_hash: OptionB256) - AnyRpcBlock { let transactions block.body.transactions.iter().map(|tx| tx.hash()).collect(); let block canonical_block(block); let size U256::from(block.length() as u32); let header block.header; ... }其中block.length()来自alloy_consensus::Block对 RLPEncodabletrait 的实现它只计算编码后的长度而不实际生成编码字节因此不存在Vec::with_capacity分配和逐字段写入的开销。这正是变更说明中 without allocating and encoding the full block 的落点。结果以u32窄化后包成U256写入 RPC 对象——区块 RLP 长度远小于 4 GiB该窄化在实践上是安全的。从代码结构可以推断凡是通过convert_block_with_hash/convert_block生成响应如eth_getBlockByNumber、eth_getBlockByHash及其带交易详情与哈希形式的变体的路径都会统一受益于这次免分配的length()计算无需为每个字段单独处理。免分配求长的正确性保证length 与 encode 长度一致“只求长度”要想取代“完整编码后取长度”前提是二者严格一致。Anvil 核心模块的单元测试对这一点做了直接约束见 crates/anvil/core/src/eth/block.rs 的block_network_roundtriplet block Block::decode(mut data.as_slice()).unwrap(); // encode and check that it matches the original data let mut encoded Vec::new(); block.encode(mut encoded); assert_eq!(data, encoded); // check that length of encoding is the same as the output of length assert_eq!(block.length(), encoded.len());该测试用一条真实网络的区块 RLP 字节串做往返验证先解码再重新编码确认字节级一致最后断言length()与encode产出的缓冲区长度完全相等。同类断言也出现在区块头编码测试test_encode_block_header中assert_eq!(header.length(), data.len())。这些测试在区块与区块头两个层级上锁死了“免分配求长 完整编码长度”这一不变式是本次优化能安全落地的前提保障。canonical 编码确保 size 与真实网络区块一致convert_block_with_hash在求长前先调用了canonical_block(block)其定义在 crates/anvil/core/src/eth/block.rs/// Returns a block whose transactions use canonical block-body representations. pub fn canonical_block(mut block: Block) - Block { block.body.transactions block .body .transactions .into_iter() .map(|tx| tx.map(FoundryTxEnvelope::into_canonical)) .collect(); block }这一步把内部持有的交易信封统一转换为规范区块体表示。以 EIP-4844Blob交易为例Anvil 内存中可能持有带 sidecar 的池化形式TxEip4844Variant::TxEip4844WithSidecar而区块体在网络上传播/存储时只包含签名交易本体sidecar 不属于区块 RLP。区块体编码逻辑EncodableBlockTransaction::encode_2718_for_blockcrates/anvil/core/src/eth/block.rs为此做了专门处理对 EIP-4844 交易先写类型字节再rlp_encode_signed其余类型走标准encode_2718。因此size的语义是规范区块体的 RLP 长度与真实网络上下载/广播的区块完全对齐而不是 Anvil 内部内存表示的“毛体积”。同一文件中的blob_transaction_root_uses_canonical_encoding测试crates/anvil/core/src/eth/block.rs#L121-L131进一步验证了用规范编码计算交易根的行为说明“入块必须用规范表示”是 Anvil 的一贯原则。eth_simulateV1 中的同款优化size的计算并不只出现在常规区块查询里eth_simulateV1的模拟结果同样需要为每个模拟出的区块填充size。模拟路径在 crates/anvil/src/eth/backend/mem/mod.rs 中采用完全一致的手法let size U256::from( BlockBody { transactions: transaction_envelopes, ommers: vec![], withdrawals: withdrawals.clone(), } .into_block(header.clone()) .length(), );simulate会为blockStateCalls中的每个模拟块临时组装出BlockBody与Header随后同样调用.length()免分配求长。由于模拟调用可能批量提交大量区块每块省下一次完整编码与堆分配对eth_simulateV1这类高吞吐路径的收益更为明显。该行为有集成测试兜底见 crates/anvil/tests/it/simulate.rs 的test_simulate_includes_block_size_rpc#[tokio::test(flavor multi_thread)] async fn test_simulate_includes_block_size_rpc() { let (_api, handle) spawn(NodeConfig::test()).await; let response rpc_request( handle.http_endpoint(), eth_simulateV1, json!([{blockStateCalls: [{}]}, latest]), ) .await; assert!(response.get(error).is_none(), {response}); assert!(quantity(response[result][0][size]) 0); }测试通过真实 HTTP RPC 端点发起eth_simulateV1断言返回的模拟区块带有size字段且大于 0验证了模拟结果的完整性也间接验证了上述求长路径确实生效。与 fork 区块的对照size 的两种来源变更说明特意限定为local block是有原因的。Anvil 存在两种区块来源本地区块由 Anvil 自行挖掘auto-mine / interval-mine区块数据完整存在于本地内存中size可由上述免分配length()路径计算Fork 区块来自远程节点状态快照其 RLP 原始字节raw_block在 fork 时即已取得size可直接取原始字节长度无需重新编码。集成测试 crates/anvil/tests/it/eip4844.rs 展示了 fork 场景下的断言模式assert_eq!(block.header.size, Some(U256::from(raw_block.len())))即 fork 区块的 size 直接对应用户提交的原始 RLP 字节长度。这也从侧面印证本次优化只针对本地路径避免了两类来源各自多余的序列化工作。总结.changelog/anvil-block-rlp-size.md记录的是一个小而精准的 Anvil 性能优化其完整闭环是变更意图本地区块的 RLPsize不再通过“分配缓冲区 完整 encode”获得而是用Encodable::length()免分配求长crates/anvil/src/eth/backend/mem/mod.rs正确性保证length()与完整编码长度的一致性由核心单元测试锁定crates/anvil/core/src/eth/block.rs语义保证求长前先转换为 canonical 区块表示确保size与真实网络区块一致EIP-4844 等特殊交易类型得到正确处理crates/anvil/core/src/eth/block.rs覆盖范围常规区块查询与eth_simulateV1模拟路径同时受益后者有端到端 RPC 集成测试验证crates/anvil/tests/it/simulate.rs。对于希望在自研客户端或工具链中复刻该思路的读者核心启发是当只需要 RLP 长度而无需字节时优先使用编码 trait 提供的length()/encoded_length类接口并在测试中显式断言“求长结果 完整编码长度”从而在性能与正确性之间取得可验证的平衡。【免费下载链接】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),仅供参考