From b73358a1bd0814de653e2fdf589ef80d9ec68f21 Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 18 Dec 2025 15:57:12 +0530 Subject: [PATCH 1/5] Impl EthEstimateGas V2 --- src/rpc/methods/eth.rs | 269 ++++++++++-------- src/rpc/mod.rs | 1 + .../subcommands/api_cmd/api_compare_tests.rs | 19 +- 3 files changed, 167 insertions(+), 122 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 9e5a2b61779..f04a93e437a 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -1845,10 +1845,6 @@ impl RpcMethod<2> for EthEstimateGas { ctx: Ctx, (tx, block_param): Self::Params, ) -> Result { - let mut msg = Message::try_from(tx)?; - // Set the gas limit to the zero sentinel value, which makes - // gas estimation actually run. - msg.gas_limit = 0; let tipset = if let Some(block_param) = block_param { tipset_by_block_number_or_hash( ctx.chain_store(), @@ -1858,37 +1854,77 @@ impl RpcMethod<2> for EthEstimateGas { } else { ctx.chain_store().heaviest_tipset() }; + eth_estimate_gas(&ctx, tx, tipset).await + } +} - match gas::estimate_message_gas(&ctx, msg.clone(), None, tipset.key().clone().into()).await - { - Err(mut err) => { - // On failure, GasEstimateMessageGas doesn't actually return the invocation result, - // it just returns an error. That means we can't get the revert reason. - // - // So we re-execute the message with EthCall (well, applyMessage which contains the - // guts of EthCall). This will give us an ethereum specific error with revert - // information. - msg.set_gas_limit(BLOCK_GAS_LIMIT); - if let Err(e) = apply_message(&ctx, Some(tipset), msg).await { - // if the error is an execution reverted, return it directly - if e.downcast_ref::().is_some_and(|eth_err| { - matches!(eth_err, EthErrors::ExecutionReverted { .. }) - }) { - return Err(e.into()); - } +pub enum EthEstimateGasV2 {} + +impl RpcMethod<2> for EthEstimateGasV2 { + const NAME: &'static str = "Filecoin.EthEstimateGas"; + const NAME_ALIAS: Option<&'static str> = Some("eth_estimateGas"); + const N_REQUIRED_PARAMS: usize = 1; + const PARAM_NAMES: [&'static str; 2] = ["tx", "blockParam"]; + const API_PATHS: BitFlags = make_bitflags!(ApiPaths::V2); + const PERMISSION: Permission = Permission::Read; + + type Params = (EthCallMessage, Option); + type Ok = EthUint64; + + async fn handle( + ctx: Ctx, + (tx, block_param): Self::Params, + ) -> Result { + let tipset = if let Some(block_param) = block_param { + tipset_by_block_number_or_hash_v2(&ctx, block_param, ResolveNullTipset::TakeOlder) + .await? + } else { + ctx.chain_store().heaviest_tipset() + }; + eth_estimate_gas(&ctx, tx, tipset).await + } +} - err = e.into(); +async fn eth_estimate_gas( + ctx: &Ctx, + tx: EthCallMessage, + tipset: Tipset, +) -> Result +where + DB: Blockstore + Send + Sync + 'static, +{ + let mut msg = Message::try_from(tx)?; + // Set the gas limit to the zero sentinel value, which makes + // gas estimation actually run. + msg.gas_limit = 0; + + match gas::estimate_message_gas(ctx, msg.clone(), None, tipset.key().clone().into()).await { + Err(mut err) => { + // On failure, GasEstimateMessageGas doesn't actually return the invocation result, + // it just returns an error. That means we can't get the revert reason. + // + // So we re-execute the message with EthCall (well, applyMessage which contains the + // guts of EthCall). This will give us an ethereum specific error with revert + // information. + msg.set_gas_limit(BLOCK_GAS_LIMIT); + if let Err(e) = apply_message(ctx, Some(tipset), msg).await { + // if the error is an execution reverted, return it directly + if e.downcast_ref::() + .is_some_and(|eth_err| matches!(eth_err, EthErrors::ExecutionReverted { .. })) + { + return Err(e.into()); } - Err(anyhow::anyhow!("failed to estimate gas: {err}").into()) - } - Ok(gassed_msg) => { - log::info!("correct gassed_msg: do eth_gas_search {gassed_msg:?}"); - let expected_gas = - Self::eth_gas_search(&ctx, gassed_msg, &tipset.key().into()).await?; - log::info!("trying eth_gas search: {expected_gas}"); - Ok(expected_gas.into()) + err = e.into(); } + + Err(anyhow::anyhow!("failed to estimate gas: {err}").into()) + } + Ok(gassed_msg) => { + log::info!("correct gassed_msg: do eth_gas_search {gassed_msg:?}"); + let expected_gas = eth_gas_search(ctx, gassed_msg, &tipset.key().into()).await?; + log::info!("trying eth_gas search: {expected_gas}"); + Ok(expected_gas.into()) } } } @@ -1928,109 +1964,102 @@ where Ok(invoc_res) } -impl EthEstimateGas { - pub async fn eth_gas_search( - data: &Ctx, - msg: Message, - tsk: &ApiTipsetKey, - ) -> anyhow::Result - where - DB: Blockstore + Send + Sync + 'static, - { - let (_invoc_res, apply_ret, prior_messages, ts) = - gas::GasEstimateGasLimit::estimate_call_with_gas( - data, - msg.clone(), - tsk, - VMTrace::Traced, - ) +pub async fn eth_gas_search( + data: &Ctx, + msg: Message, + tsk: &ApiTipsetKey, +) -> anyhow::Result +where + DB: Blockstore + Send + Sync + 'static, +{ + let (_invoc_res, apply_ret, prior_messages, ts) = + gas::GasEstimateGasLimit::estimate_call_with_gas(data, msg.clone(), tsk, VMTrace::Traced) .await?; - if apply_ret.msg_receipt().exit_code().is_success() { - return Ok(msg.gas_limit()); - } - - let exec_trace = apply_ret.exec_trace(); - let _expected_exit_code: ExitCode = fvm_shared4::error::ExitCode::SYS_OUT_OF_GAS.into(); - if exec_trace.iter().any(|t| { - matches!( - t, - &ExecutionEvent::CallReturn(CallReturn { - exit_code: Some(_expected_exit_code), - .. - }) - ) - }) { - let ret = Self::gas_search(data, &msg, &prior_messages, ts).await?; - Ok(((ret as f64) * data.mpool.config.gas_limit_overestimation) as u64) - } else { - anyhow::bail!( - "message execution failed: exit {}, reason: {}", - apply_ret.msg_receipt().exit_code(), - apply_ret.failure_info().unwrap_or_default(), - ); - } + if apply_ret.msg_receipt().exit_code().is_success() { + return Ok(msg.gas_limit()); + } + + let exec_trace = apply_ret.exec_trace(); + let _expected_exit_code: ExitCode = fvm_shared4::error::ExitCode::SYS_OUT_OF_GAS.into(); + if exec_trace.iter().any(|t| { + matches!( + t, + &ExecutionEvent::CallReturn(CallReturn { + exit_code: Some(_expected_exit_code), + .. + }) + ) + }) { + let ret = gas_search(data, &msg, &prior_messages, ts).await?; + Ok(((ret as f64) * data.mpool.config.gas_limit_overestimation) as u64) + } else { + anyhow::bail!( + "message execution failed: exit {}, reason: {}", + apply_ret.msg_receipt().exit_code(), + apply_ret.failure_info().unwrap_or_default(), + ); } +} - /// `gas_search` does an exponential search to find a gas value to execute the - /// message with. It first finds a high gas limit that allows the message to execute - /// by doubling the previous gas limit until it succeeds then does a binary - /// search till it gets within a range of 1% - async fn gas_search( +/// `gas_search` does an exponential search to find a gas value to execute the +/// message with. It first finds a high gas limit that allows the message to execute +/// by doubling the previous gas limit until it succeeds then does a binary +/// search till it gets within a range of 1% +async fn gas_search( + data: &Ctx, + msg: &Message, + prior_messages: &[ChainMessage], + ts: Tipset, +) -> anyhow::Result +where + DB: Blockstore + Send + Sync + 'static, +{ + let mut high = msg.gas_limit; + let mut low = msg.gas_limit; + + async fn can_succeed( data: &Ctx, - msg: &Message, + mut msg: Message, prior_messages: &[ChainMessage], ts: Tipset, - ) -> anyhow::Result + limit: u64, + ) -> anyhow::Result where DB: Blockstore + Send + Sync + 'static, { - let mut high = msg.gas_limit; - let mut low = msg.gas_limit; - - async fn can_succeed( - data: &Ctx, - mut msg: Message, - prior_messages: &[ChainMessage], - ts: Tipset, - limit: u64, - ) -> anyhow::Result - where - DB: Blockstore + Send + Sync + 'static, - { - msg.gas_limit = limit; - let (_invoc_res, apply_ret, _) = data - .state_manager - .call_with_gas( - &mut msg.into(), - prior_messages, - Some(ts), - VMTrace::NotTraced, - ) - .await?; - Ok(apply_ret.msg_receipt().exit_code().is_success()) - } + msg.gas_limit = limit; + let (_invoc_res, apply_ret, _) = data + .state_manager + .call_with_gas( + &mut msg.into(), + prior_messages, + Some(ts), + VMTrace::NotTraced, + ) + .await?; + Ok(apply_ret.msg_receipt().exit_code().is_success()) + } - while high <= BLOCK_GAS_LIMIT { - if can_succeed(data, msg.clone(), prior_messages, ts.clone(), high).await? { - break; - } - low = high; - high = high.saturating_mul(2).min(BLOCK_GAS_LIMIT); + while high <= BLOCK_GAS_LIMIT { + if can_succeed(data, msg.clone(), prior_messages, ts.clone(), high).await? { + break; } + low = high; + high = high.saturating_mul(2).min(BLOCK_GAS_LIMIT); + } - let mut check_threshold = high / 100; - while (high - low) > check_threshold { - let median = (high + low) / 2; - if can_succeed(data, msg.clone(), prior_messages, ts.clone(), high).await? { - high = median; - } else { - low = median; - } - check_threshold = median / 100; + let mut check_threshold = high / 100; + while (high - low) > check_threshold { + let median = (high + low) / 2; + if can_succeed(data, msg.clone(), prior_messages, ts.clone(), high).await? { + high = median; + } else { + low = median; } - - Ok(high) + check_threshold = median / 100; } + + Ok(high) } pub enum EthFeeHistory {} @@ -2663,7 +2692,7 @@ impl RpcMethod<2> for EthCallV2 { const NAME_ALIAS: Option<&'static str> = Some("eth_call"); const N_REQUIRED_PARAMS: usize = 2; const PARAM_NAMES: [&'static str; 2] = ["tx", "blockParam"]; - const API_PATHS: BitFlags = make_bitflags!(ApiPaths::{ V2 }); + const API_PATHS: BitFlags = make_bitflags!(ApiPaths::V2); const PERMISSION: Permission = Permission::Read; type Params = (EthCallMessage, ExtBlockNumberOrHash); type Ok = EthBytes; diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 6bc81c202a2..3224ebec6ae 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -105,6 +105,7 @@ macro_rules! for_each_rpc_method { $callback!($crate::rpc::eth::EthCallV2); $callback!($crate::rpc::eth::EthChainId); $callback!($crate::rpc::eth::EthEstimateGas); + $callback!($crate::rpc::eth::EthEstimateGasV2); $callback!($crate::rpc::eth::EthFeeHistory); $callback!($crate::rpc::eth::EthGasPrice); $callback!($crate::rpc::eth::EthGetBalance); diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index a1df0eb9b43..fc6f2bb8082 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -2044,11 +2044,11 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset )) .unwrap(), )]); - if let Ok(eth_to_addr) = msg.to.try_into() { + if let Ok(eth_to_addr) = EthAddress::try_from(msg.to) { tests.extend([RpcTest::identity( EthEstimateGas::request(( EthCallMessage { - to: Some(eth_to_addr), + to: Some(eth_to_addr.clone()), value: Some(msg.value.clone().into()), data: Some(msg.params.clone().into()), ..Default::default() @@ -2058,6 +2058,21 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset .unwrap(), ) .policy_on_rejected(PolicyOnRejected::Pass)]); + tests.extend([RpcTest::identity( + EthEstimateGasV2::request(( + EthCallMessage { + to: Some(eth_to_addr), + value: Some(msg.value.clone().into()), + data: Some(msg.params.clone().into()), + ..Default::default() + }, + Some(ExtBlockNumberOrHash::BlockNumber( + shared_tipset.epoch().into(), + )), + )) + .unwrap(), + ) + .policy_on_rejected(PolicyOnRejected::Pass)]); } } } From 57d92cf1a70ca2e705825b3a333c1e9e311a3b59 Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 18 Dec 2025 15:57:20 +0530 Subject: [PATCH 2/5] reduce code dup in EthCall --- src/rpc/methods/eth.rs | 51 ++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index f04a93e437a..fd09c8efc1e 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -2663,26 +2663,12 @@ impl RpcMethod<2> for EthCall { ctx: Ctx, (tx, block_param): Self::Params, ) -> Result { - let msg = Message::try_from(tx)?; let ts = tipset_by_block_number_or_hash( ctx.chain_store(), block_param, ResolveNullTipset::TakeOlder, )?; - let invoke_result = apply_message(&ctx, Some(ts), msg.clone()).await?; - - if msg.to() == FilecoinAddress::ETHEREUM_ACCOUNT_MANAGER_ACTOR { - Ok(EthBytes::default()) - } else { - let msg_rct = invoke_result.msg_rct.context("no message receipt")?; - let return_data = msg_rct.return_data(); - if return_data.is_empty() { - Ok(Default::default()) - } else { - let bytes = decode_payload(&return_data, CBOR)?; - Ok(bytes) - } - } + eth_call(&ctx, tx, ts).await } } @@ -2700,22 +2686,33 @@ impl RpcMethod<2> for EthCallV2 { ctx: Ctx, (tx, block_param): Self::Params, ) -> Result { - let msg = Message::try_from(tx)?; let ts = tipset_by_block_number_or_hash_v2(&ctx, block_param, ResolveNullTipset::TakeOlder) .await?; - let invoke_result = apply_message(&ctx, Some(ts), msg.clone()).await?; + eth_call(&ctx, tx, ts).await + } +} + +async fn eth_call( + ctx: &Ctx, + tx: EthCallMessage, + ts: Tipset, +) -> Result +where + DB: Blockstore + Send + Sync + 'static, +{ + let msg = Message::try_from(tx)?; + let invoke_result = apply_message(ctx, Some(ts), msg.clone()).await?; - if msg.to() == FilecoinAddress::ETHEREUM_ACCOUNT_MANAGER_ACTOR { - Ok(EthBytes::default()) + if msg.to() == FilecoinAddress::ETHEREUM_ACCOUNT_MANAGER_ACTOR { + Ok(EthBytes::default()) + } else { + let msg_rct = invoke_result.msg_rct.context("no message receipt")?; + let return_data = msg_rct.return_data(); + if return_data.is_empty() { + Ok(Default::default()) } else { - let msg_rct = invoke_result.msg_rct.context("no message receipt")?; - let return_data = msg_rct.return_data(); - if return_data.is_empty() { - Ok(Default::default()) - } else { - let bytes = decode_payload(&return_data, CBOR)?; - Ok(bytes) - } + let bytes = decode_payload(&return_data, CBOR)?; + Ok(bytes) } } } From a66f9505cbc629a14866dd7387270f09d4a2c92e Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 18 Dec 2025 16:10:43 +0530 Subject: [PATCH 3/5] Update Changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97596adbe50..ca2f46700b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,8 @@ - [#6339](https://github.com/ChainSafe/forest/pull/6339) Implemented `Filecoin.EthCall` for API v2. +- [#6364](https://github.com/ChainSafe/forest/pull/6364) Implemented `Filecoin.EthEstimateGas` for API v2. + ## Forest v0.30.5 "Dulce de Leche" Non-mandatory release supporting new API methods and addressing a critical panic issue. From 254e3f289421deeea8c50245a46932e6cc5ccadf Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 18 Dec 2025 17:14:57 +0530 Subject: [PATCH 4/5] Fix entry in Changelog --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca2f46700b6..31150f57d29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,10 @@ ### Added +- [#6339](https://github.com/ChainSafe/forest/pull/6339) Implemented `Filecoin.EthCall` for API v2. + +- [#6364](https://github.com/ChainSafe/forest/pull/6364) Implemented `Filecoin.EthEstimateGas` for API v2. + ### Changed ### Removed @@ -37,10 +41,6 @@ - [#6327](https://github.com/ChainSafe/forest/pull/6327) Fixed: Forest returns 404 for all invalid api paths. -- [#6339](https://github.com/ChainSafe/forest/pull/6339) Implemented `Filecoin.EthCall` for API v2. - -- [#6364](https://github.com/ChainSafe/forest/pull/6364) Implemented `Filecoin.EthEstimateGas` for API v2. - ## Forest v0.30.5 "Dulce de Leche" Non-mandatory release supporting new API methods and addressing a critical panic issue. From 41be34e542fb937a3ffc4421d849d7a7afd7355a Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 18 Dec 2025 19:34:30 +0530 Subject: [PATCH 5/5] fix gas search --- src/rpc/methods/eth.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index fd09c8efc1e..da1ca7de597 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -2040,7 +2040,7 @@ where Ok(apply_ret.msg_receipt().exit_code().is_success()) } - while high <= BLOCK_GAS_LIMIT { + while high < BLOCK_GAS_LIMIT { if can_succeed(data, msg.clone(), prior_messages, ts.clone(), high).await? { break; } @@ -2051,7 +2051,7 @@ where let mut check_threshold = high / 100; while (high - low) > check_threshold { let median = (high + low) / 2; - if can_succeed(data, msg.clone(), prior_messages, ts.clone(), high).await? { + if can_succeed(data, msg.clone(), prior_messages, ts.clone(), median).await? { high = median; } else { low = median;