Foundry Anvil eth_simulateV1 补丁解析:在模拟执行中保留 EIP-4844 Blob 交易费用上限
【免费下载链接】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-simulate-blob-fee-cap.md 记录的anvil: patch级变更展开,深入剖析 Anvil 在eth_simulateV1模拟执行中**保留 Blob 交易费用上限(blob fee cap)**的底层实现。读完本文,你将理解eth_simulateV1的执行路径、EIP-4844 Blob 费用校验在模拟场景下的处理逻辑,以及如何构造包含maxFeePerBlobGas、blobVersionedHashes的模拟请求并在本地测试中验证该行为。
一、变更背景:一次针对 eth_simulateV1 的 patch
该 changelog 条目全文如下:
--- anvil: patch --- Preserved blob transaction fee caps during `eth_simulateV1` execution.虽然条目简短,但指向的是一个具体且影响深远的修复点:Anvil 的eth_simulateV1RPC 方法在执行模拟时,此前未能完整保留 EIP-4844 Blob 类型交易(type 3 交易)中的费用上限字段(maxFeePerBlobGas),该补丁修正了这一点,使模拟环境中的 Blob 交易与真实交易执行遵循一致的费用校验规则。
从源码结构看,这一修复同时涉及三个层面:
- 模拟请求的字段保真传递(
preserve_simulation_request_fields); - 后端 EVM 环境构建时的 blob 费用校验(
BlobFeeCapTooLow); - 模拟专用
SimulationHandler对零价格 Blob 调用的豁免逻辑。
二、eth_simulateV1 的执行入口与字段保留
2.1 RPC 入口:simulate_v1
Anvil 的eth_simulateV1处理入口位于 crates/anvil/src/eth/api.rs 的EthApi::simulate_v1:
pub async fn simulate_v1( &self, request: SimulatePayload, block_number: Option<BlockId>, ) -> Result<Vec<SimulatedBlock<AnyRpcBlock>>> { self.simulate_v1_raw(preserve_simulation_request_fields(request), block_number).await }关键点在于:进入simulate_v1_raw之前,请求先经过preserve_simulation_request_fields预处理。simulate_v1_raw内部会校验空输入、块数量上限(MAX_SIMULATE_BLOCKS,超限返回-38026 too many blocks)、解析区块请求与区块间隔,最终在分叉(fork)模式下走fork.simulate_v1,否则调用backend.simulate_raw在阻塞任务中执行模拟(crates/anvil/src/eth/api.rs#L3285-L3359)。
2.2 字段保留:preserve_simulation_request_fields
补丁的核心机制是preserve_simulation_request_fields,实现在 crates/anvil/src/eth/mod.rs:
pub(crate) fn preserve_simulation_request_fields( request: SimulatePayload, ) -> SimulatePayload<WithOtherFields<TransactionRequest>> { SimulatePayload { block_state_calls: request .block_state_calls .into_iter() .map(|block| SimBlock { block_overrides: block.block_overrides, state_overrides: block.state_overrides, calls: block.calls.into_iter().map(WithOtherFields::new).collect(), }) .collect(), trace_transfers: request.trace_transfers, validation: request.validation, return_full_transactions: request.return_full_transactions, } }这里使用了 alloy 的WithOtherFields<TransactionRequest>:它把每个TransactionRequest中标准字段之外的所有其他字段(other映射)完整保留。对 EIP-4844 交易而言,maxFeePerBlobGas、blobVersionedHashes、maxPriorityFeePerGas等字段正是以这类"附加字段"形式存在。在补丁之前,这些字段在模拟请求转换过程中可能被剥离,导致模拟执行时 Blob 交易的费用上限丢失;补丁通过WithOtherFields::new包装,确保这些字段在进入 EVM 上下文前原样保留——这正是 changelog 中 "Preserved blob transaction fee caps" 的代码级体现。
三、模拟执行中的 Blob 费用校验
3.1 常规路径:BlobFeeCapTooLow 校验
在 crates/anvil/src/eth/backend/mem/mod.rs 的交易池校验逻辑中,EIP-4844 Blob 费用校验位于 EIP-1559 费用校验之后:
// EIP-4844 blob fee validation if evm_env.cfg_env.spec >= SpecId::CANCUN && tx.is_eip4844() && let Some(max_fee_per_blob_gas) = tx.max_fee_per_blob_gas() && let Some(blob_gas_and_price) = &evm_env.block_env.blob_excess_gas_and_price && max_fee_per_blob_gas < blob_gas_and_price.blob_gasprice { debug!(target: "backend", "max fee per blob gas={}, too low, block blob gas price={}", max_fee_per_blob_gas, blob_gas_and_price.blob_gasprice); return Err(InvalidTransactionError::BlobFeeCapTooLow( max_fee_per_blob_gas, blob_gas_and_price.blob_gasprice, )); }条件链为:硬分叉不低于 Cancun(SpecId::CANCUN)+ 交易为 EIP-4844 类型 + 交易携带maxFeePerBlobGas+ 区块环境包含blob_excess_gas_and_price+费用上限低于区块 blob 气体价格→ 返回BlobFeeCapTooLow错误。在后续的资金充足性检查中,max_cost也计入blob_gas_used() * max_fee_per_blob_gas()(同文件 crates/anvil/src/eth/backend/mem/mod.rs#L9119-L9126)。
这意味着:当eth_simulateV1的调用保留maxFeePerBlobGas后,模拟会像真实交易一样对过低的 blob 费用上限报错,从而真实反映主网/分叉环境的费用约束,而不是静默忽略。
3.2 模拟专用路径:SimulationHandler 的豁免
与普通eth_call走transact不同,零价格的 Blob 调用会走模拟专用的SimulationHandler(crates/anvil/src/eth/backend/mem/mod.rs#L544-L578):
fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> { let skip_blob_fee_check = evm.ctx_ref().cfg().is_base_fee_check_disabled() && evm.ctx_ref().tx().tx_type == 3 && evm.ctx_ref().tx().max_fee_per_blob_gas == 0; if !skip_blob_fee_check { return validation::validate_env(evm.ctx()); } let block = evm.ctx_ref().block().clone(); let mut validation_block = block.clone(); if let Some(blob_gas_and_price) = &mut validation_block.blob_excess_gas_and_price { blob_gas_and_price.blob_gasprice = 0; } evm.ctx().set_block(validation_block); let result = validation::validate_env(evm.ctx()); evm.ctx().set_block(block); result }逻辑要点:
- 当base fee 校验被禁用且交易类型为 3(Blob)且
max_fee_per_blob_gas == 0时,跳过标准 blob 费用校验; - 执行校验前将区块环境的
blob_gasprice临时置 0,校验完再恢复,实现"零价格 blob 调用"也能通过环境校验。
这条豁免路径与"保留费用上限"的补丁形成互补:有上限的 blob 模拟按真实规则校验,显式传0x0的零价格 blob 调用则被宽容处理,便于开发者在不关心 gas 价格时做纯逻辑模拟。
四、测试验证:零价格 Blob 调用走模拟处理器
仓库的集成测试直接印证了上述行为。crates/anvil/tests/it/simulate.rs 中test_simulate_v1_preserves_precompile_warming_rpc(第 347-396 行)注释明确写道:
// A zero-priced blob call uses the simulation handler; an ordinary call uses transact.测试构造了两类调用逐一验证:
for call in [ json!({"to": helper}), json!({ "to": helper, "blobVersionedHashes": [format!("0x01{}", "00".repeat(31))], "maxFeePerBlobGas": "0x0" }), ] { let response = rpc_request( &handle.http_endpoint(), "eth_simulateV1", json!([{"blockStateCalls": [{ "stateOverrides": { source: {"movePrecompileToAddress": destination}, helper: {"code": helper_code} }, "calls": [call] }]}, "latest"]), ) .await; assert!(response.get("error").is_none(), "{response}"); // 断言 access_costs 一致 … }该用例同时验证:
- 带
blobVersionedHashes+maxFeePerBlobGas: "0x0"的 blob 调用在eth_simulateV1中不会报错(error为 None); - blob 调用与普通调用的模拟结果一致(访问成本数组
[107, 2607, 107]相同)。
结合 crates/anvil/tests/it/eip4844.rs、crates/anvil/tests/it/simulate.rs#L1028-L1085(test_simulate_scopes_block_overrides_and_derives_base_fee_rpc等)可以看到 Anvil 对模拟场景的区块覆盖、base fee 推导与 EIP-4844 参数的组合测试体系。
五、实战:如何在本地复现与使用
5.1 启动 Anvil 并准备分叉环境
Blob 费用校验要求硬分叉不低于 Cancun。可以通过--hardfork参数显式指定:
# 本地开发链,显式启用 Cancun 硬分叉 anvil --hardfork cancun # 或分叉主网(自动继承主网 hardfork 与 blob 基础费用状态) anvil --fork-url https://eth-mainnet.example.com/rpc --fork-block-number 200000005.2 构造 eth_simulateV1 请求
调用eth_simulateV1模拟一个携带 blob 的调用,务必保留maxFeePerBlobGas与blobVersionedHashes:
{ "method": "eth_simulateV1", "params": [ { "blockStateCalls": [ { "blockOverrides": { "blobBaseFee": "0x15" }, "calls": [ { "to": "0x0000000000000000000000000000000000000000", "blobVersionedHashes": [ "0x0100000000000000000000000000000000000000000000000000000000000000" ], "maxFeePerBlobGas": "0x2540be400" } ] } ], "traceTransfers": false, "validation": true, "returnFullTransactions": true }, "latest" ], "id": 1, "jsonrpc": "2.0" }要点说明:
blockOverrides.blobBaseFee覆盖模拟区块的 blob 基础费用(参考 crates/anvil/tests/it/simulate.rs#L1028-L1085 中"blobBaseFee": "0x15"的用法);- 若
maxFeePerBlobGas低于区块 blob 气体价格,模拟将返回BlobFeeCapTooLow类错误——这正是补丁后费用上限被保留的直接体现; - 若显式传
maxFeePerBlobGas: "0x0",则走SimulationHandler的豁免路径,零价格 blob 调用可通过校验(见test_simulate_v1_preserves_precompile_warming_rpc)。
5.3 通过集成测试回归验证
仓库已内置相关测试,可在本地运行验证该行为:
cargo test -p anvil --test it simulate其中 crates/anvil/tests/it/simulate.rs 覆盖eth_simulateV1的字段保留、区块覆盖、blob 调用等场景,是理解本补丁行为最直接的参考用例。
六、小结
.changelog/anvil-simulate-blob-fee-cap.md虽然只有一行描述,但它对应的是一个贯穿 Anvil RPC 层(preserve_simulation_request_fields字段保真)、后端 EVM 层(BlobFeeCapTooLow校验与资金检查)与模拟处理器层(SimulationHandler零价格豁免)的完整修复:
| 层级 | 文件 | 职责 |
|---|---|---|
| RPC 入口 | crates/anvil/src/eth/api.rs | simulate_v1转发前调用字段保留函数 |
| 字段保留 | crates/anvil/src/eth/mod.rs | WithOtherFields::new保留maxFeePerBlobGas等附加字段 |
| 费用校验 | crates/anvil/src/eth/backend/mem/mod.rs | 常规 blob 费用校验、资金上限计算 |
| 模拟豁免 | crates/anvil/src/eth/backend/mem/mod.rs#L544-L578 | 零价格 blob 调用的validate_env豁免 |
| 测试佐证 | crates/anvil/tests/it/simulate.rs | 零价格 blob 调用走模拟处理器的集成测试 |
从源码结构可以推断,该补丁的动机是让eth_simulateV1的模拟结果与真实 blob 交易执行保持一致的费用语义:既能用maxFeePerBlobGas触发真实的费用校验,也能以显式零价格快速进行纯逻辑模拟。对于依赖 Anvil 做 EIP-4844 相关合约开发、分叉模拟与费用分析的开发者,理解这一行为有助于构造更贴近主网语义的模拟请求。
【免费下载链接】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),仅供参考