Foundry Anvil 区块 RLP 大小计算优化:免分配计算本地区块 size 字段
2026/9/15 17:25:50 网站建设 项目流程

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 展开,剖析 Anvil(Foundry 内置的本地以太坊节点)如何在不分配内存、不完整编码整个区块的前提下,计算出区块对象中的size(RLP 编码长度)字段。读完本文,你将理解以太坊区块对象size字段的来源、Encodable::length()免分配求长的实现原理,以及eth_getBlockByNumbereth_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 编码后的字节长度。客户端工具(区块浏览器、钱包、索引器等)常依赖它估算区块体积;对节点而言,它也是区块对象的标准组成部分,必须如实返回。

RLP(Recursive Length Prefix)是以太坊用于序列化区块、交易、收据等核心数据结构的编码格式。区块 = 区块头(Header)+ 区块体(BlockBody,含交易列表、ommers、withdrawals 等),整体再套一层 RLP list 前缀。计算size最朴素的做法是:

  1. 分配一个Vec<u8>缓冲区;
  2. 将整个区块encode进缓冲区;
  3. 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 it's 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: Option<B256>) -> 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_getBlockByNumbereth_getBlockByHash及其带交易详情与哈希形式的变体)的路径,都会统一受益于这次免分配的length()计算,无需为每个字段单独处理。

免分配求长的正确性保证:length 与 encode 长度一致

“只求长度”要想取代“完整编码后取长度”,前提是二者严格一致。Anvil 核心模块的单元测试对这一点做了直接约束,见 crates/anvil/core/src/eth/block.rs 的block_network_roundtrip

let 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-4844(Blob)交易为例:Anvil 内存中可能持有带 sidecar 的池化形式(TxEip4844Variant::TxEip4844WithSidecar),而区块体在网络上传播/存储时只包含签名交易本体,sidecar 不属于区块 RLP。区块体编码逻辑EncodableBlockTransaction::encode_2718_for_block(crates/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中的每个模拟块临时组装出BlockBodyHeader,随后同样调用.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 性能优化,其完整闭环是:

  1. 变更意图:本地区块的 RLPsize不再通过“分配缓冲区 + 完整 encode”获得,而是用Encodable::length()免分配求长(crates/anvil/src/eth/backend/mem/mod.rs);
  2. 正确性保证length()与完整编码长度的一致性由核心单元测试锁定(crates/anvil/core/src/eth/block.rs);
  3. 语义保证:求长前先转换为 canonical 区块表示,确保size与真实网络区块一致,EIP-4844 等特殊交易类型得到正确处理(crates/anvil/core/src/eth/block.rs);
  4. 覆盖范围:常规区块查询与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),仅供参考

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

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

立即咨询