diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e535fd5290..b4f03d61dea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,9 @@ - [#4515](https://github.com/ChainSafe/forest/pull/4515) Add support for the `Filecoin.StateLookupRobustAddress` RPC method. +- [#4496](https://github.com/ChainSafe/forest/pull/4496) Add support for the + `Filecoin.EthEstimateGas` RPC method. + - [#4558](https://github.com/ChainSafe/forest/pull/4558) Add support for the `Filecoin.StateVerifiedRegistryRootKey` RPC method. diff --git a/scripts/tests/api_compare/filter-list-offline b/scripts/tests/api_compare/filter-list-offline index eb6a5f16304..49ecc21284e 100644 --- a/scripts/tests/api_compare/filter-list-offline +++ b/scripts/tests/api_compare/filter-list-offline @@ -17,3 +17,5 @@ !Filecoin.MinerCreateBlock # CustomCheckFailed in Forest: https://github.com/ChainSafe/forest/issues/4446 !Filecoin.StateCirculatingSupply +# The estimation is inaccurate only for offline RPC server, to be investigated: https://github.com/ChainSafe/forest/issues/4555 +!Filecoin.EthEstimateGas diff --git a/src/message/chain_message.rs b/src/message/chain_message.rs index 3a1af3da2f0..24a73b05dcb 100644 --- a/src/message/chain_message.rs +++ b/src/message/chain_message.rs @@ -131,3 +131,15 @@ impl MessageTrait for ChainMessage { } } } + +impl From for ChainMessage { + fn from(value: Message) -> Self { + Self::Unsigned(value) + } +} + +impl From for ChainMessage { + fn from(value: SignedMessage) -> Self { + Self::Signed(value) + } +} diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 832ff3a12ee..d4a5892a55c 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -15,19 +15,23 @@ use crate::eth::{ EAMMethod, EVMMethod, EthChainId as EthChainIdType, EthEip1559TxArgs, EthLegacyEip155TxArgs, EthLegacyHomesteadTxArgs, }; +use crate::interpreter::VMTrace; use crate::lotus_json::{lotus_json_with_self, HasLotusJson}; use crate::message::{ChainMessage, Message as _, SignedMessage}; use crate::rpc::error::ServerError; +use crate::rpc::types::ApiTipsetKey; use crate::rpc::{ApiPaths, Ctx, Permission, RpcMethod}; use crate::shim::actors::is_evm_actor; use crate::shim::actors::EVMActorStateLoad as _; use crate::shim::address::{Address as FilecoinAddress, Protocol}; use crate::shim::crypto::Signature; use crate::shim::econ::{TokenAmount, BLOCK_GAS_LIMIT}; +use crate::shim::error::ExitCode; use crate::shim::executor::Receipt; use crate::shim::fvm_shared_latest::address::{Address as VmAddress, DelegatedAddress}; use crate::shim::fvm_shared_latest::MethodNum; use crate::shim::message::Message; +use crate::shim::trace::{CallReturn, ExecutionEvent}; use crate::shim::{clock::ChainEpoch, state_tree::StateTree}; use crate::utils::db::BlockstoreExt as _; use anyhow::{bail, Result}; @@ -1168,6 +1172,159 @@ impl RpcMethod<0> for EthSyncing { } } +pub enum EthEstimateGas {} + +impl RpcMethod<2> for EthEstimateGas { + const NAME: &'static str = "Filecoin.EthEstimateGas"; + const N_REQUIRED_PARAMS: usize = 1; + const PARAM_NAMES: [&'static str; 2] = ["tx", "block_param"]; + const API_PATHS: ApiPaths = ApiPaths::V1; + const PERMISSION: Permission = Permission::Read; + + type Params = (EthCallMessage, Option); + type Ok = Uint64; + + async fn handle( + 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 tsk = if let Some(block_param) = block_param { + Some( + tipset_by_block_number_or_hash(ctx.chain_store(), block_param)? + .key() + .clone(), + ) + } else { + None + }; + match gas::estimate_message_gas(&ctx, msg, None, tsk.clone().into()).await { + Err(e) => { + // 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. + // TODO(forest): https://github.com/ChainSafe/forest/issues/4554 + Err(anyhow::anyhow!("failed to estimate gas: {e}").into()) + } + Ok(gassed_msg) => { + let expected_gas = Self::eth_gas_search(&ctx, gassed_msg, &tsk.into()).await?; + Ok(expected_gas.into()) + } + } + } +} + +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, + ) + .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(), + ); + } + } + + /// `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: Arc, + ) -> 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: Arc, + 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()) + } + + 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; + } + + Ok(high) + } +} + pub enum EthFeeHistory {} impl RpcMethod<3> for EthFeeHistory { diff --git a/src/rpc/methods/eth/types.rs b/src/rpc/methods/eth/types.rs index ed733b1e419..6c287c9cfa5 100644 --- a/src/rpc/methods/eth/types.rs +++ b/src/rpc/methods/eth/types.rs @@ -24,6 +24,12 @@ pub struct EthBytes( ); lotus_json_with_self!(EthBytes); +impl From for EthBytes { + fn from(value: RawBytes) -> Self { + Self(value.into()) + } +} + #[derive(Debug, Deserialize, Serialize)] pub struct GetBytecodeReturn(pub Option); @@ -226,9 +232,75 @@ pub struct GasReward { pub premium: TokenAmount, } +#[derive(PartialEq, Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EthCallMessage { + pub from: Option, + pub to: Option, + pub gas: Uint64, + pub gas_price: EthBigInt, + pub value: EthBigInt, + pub data: EthBytes, +} +lotus_json_with_self!(EthCallMessage); + +impl EthCallMessage { + pub fn convert_data_to_message_params(data: EthBytes) -> anyhow::Result { + if data.0.is_empty() { + Ok(RawBytes::new(data.0)) + } else { + Ok(RawBytes::new(fvm_ipld_encoding::to_vec(&RawBytes::new( + data.0, + ))?)) + } + } +} + +impl TryFrom for Message { + type Error = anyhow::Error; + fn try_from(tx: EthCallMessage) -> Result { + let from = match &tx.from { + Some(addr) if addr != &EthAddress::default() => { + // The from address must be translatable to an f4 address. + let from = addr.to_filecoin_address()?; + if from.protocol() != Protocol::Delegated { + anyhow::bail!("expected a class 4 address, got: {}", from.protocol()); + } + from + } + _ => { + // Send from the filecoin "system" address. + EthAddress::default().to_filecoin_address()? + } + }; + let params = EthCallMessage::convert_data_to_message_params(tx.data)?; + let (to, method_num) = if let Some(to) = tx.to { + ( + to.to_filecoin_address()?, + EVMMethod::InvokeContract as MethodNum, + ) + } else { + ( + FilecoinAddress::ETHEREUM_ACCOUNT_MANAGER_ACTOR, + EAMMethod::CreateExternal as MethodNum, + ) + }; + Ok(Message { + from, + to, + value: tx.value.0.into(), + method_num, + params, + gas_limit: BLOCK_GAS_LIMIT, + ..Default::default() + }) + } +} + #[cfg(test)] mod tests { use super::*; + use base64::{prelude::BASE64_STANDARD, Engine as _}; #[test] fn get_bytecode_return_roundtrip() { @@ -250,4 +322,18 @@ mod tests { "815820000000000000000000000000000000000000000000000000000000000000000a" ); } + + #[test] + fn test_convert_data_to_message_params_empty() { + let data = EthBytes(vec![]); + let params = EthCallMessage::convert_data_to_message_params(data).unwrap(); + assert!(params.is_empty()); + } + + #[test] + fn test_convert_data_to_message_params() { + let data = EthBytes(BASE64_STANDARD.decode("RHt4g0E=").unwrap()); + let params = EthCallMessage::convert_data_to_message_params(data).unwrap(); + assert_eq!(BASE64_STANDARD.encode(&*params).as_str(), "RUR7eINB"); + } } diff --git a/src/rpc/methods/gas.rs b/src/rpc/methods/gas.rs index db586a68b9c..82648461da2 100644 --- a/src/rpc/methods/gas.rs +++ b/src/rpc/methods/gas.rs @@ -1,9 +1,14 @@ // Copyright 2019-2024 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +use std::sync::Arc; + +use crate::blocks::Tipset; use crate::chain::{BASE_FEE_MAX_CHANGE_DENOM, BLOCK_GAS_TARGET}; +use crate::interpreter::VMTrace; use crate::message::{ChainMessage, Message as MessageTrait, SignedMessage}; use crate::rpc::{error::ServerError, types::*, ApiPaths, Ctx, Permission, RpcMethod}; +use crate::shim::executor::ApplyRet; use crate::shim::{ address::{Address, Protocol}, crypto::{Signature, SignatureType, SECP_SIG_LEN}, @@ -16,6 +21,8 @@ use num::BigInt; use num_traits::{FromPrimitive, Zero}; use rand_distr::{Distribution, Normal}; +use super::state::InvocResult; + const MIN_GAS_PREMIUM: f64 = 100000.0; /// Estimate the fee cap @@ -170,7 +177,83 @@ impl RpcMethod<2> for GasEstimateGasLimit { ctx: Ctx, (msg, tsk): Self::Params, ) -> Result { - estimate_gas_limit(&ctx, msg, &tsk).await + Ok(Self::estimate_gas_limit(&ctx, msg, &tsk).await?) + } +} + +impl GasEstimateGasLimit { + pub async fn estimate_call_with_gas( + data: &Ctx, + mut msg: Message, + ApiTipsetKey(tsk): &ApiTipsetKey, + trace_config: VMTrace, + ) -> anyhow::Result<(InvocResult, ApplyRet, Vec, Arc)> + where + DB: Blockstore + Send + Sync + 'static, + { + msg.set_gas_limit(BLOCK_GAS_LIMIT); + msg.set_gas_fee_cap(TokenAmount::from_atto(0)); + msg.set_gas_premium(TokenAmount::from_atto(0)); + + let curr_ts = data.chain_store().load_required_tipset_or_heaviest(tsk)?; + let from_a = data + .state_manager + .resolve_to_key_addr(&msg.from, &curr_ts) + .await?; + + let pending = data.mpool.pending_for(&from_a); + let prior_messages: Vec = pending + .map(|s| s.into_iter().map(ChainMessage::Signed).collect::>()) + .unwrap_or_default(); + + let ts = data.mpool.cur_tipset.lock().clone(); + // Pretend that the message is signed. This has an influence on the gas + // cost. We obviously can't generate a valid signature. Instead, we just + // fill the signature with zeros. The validity is not checked. + let mut chain_msg = match from_a.protocol() { + Protocol::Secp256k1 => ChainMessage::Signed(SignedMessage::new_unchecked( + msg, + Signature::new_secp256k1(vec![0; SECP_SIG_LEN]), + )), + Protocol::Delegated => ChainMessage::Signed(SignedMessage::new_unchecked( + msg, + // In Lotus, delegated signatures have the same length as SECP256k1. + // This may or may not change in the future. + Signature::new(SignatureType::Delegated, vec![0; SECP_SIG_LEN]), + )), + _ => ChainMessage::Unsigned(msg), + }; + + let (invoc_res, apply_ret) = data + .state_manager + .call_with_gas( + &mut chain_msg, + &prior_messages, + Some(ts.clone()), + trace_config, + ) + .await?; + Ok((invoc_res, apply_ret, prior_messages, ts)) + } + + pub async fn estimate_gas_limit( + data: &Ctx, + msg: Message, + tsk: &ApiTipsetKey, + ) -> anyhow::Result + where + DB: Blockstore + Send + Sync + 'static, + { + let (res, ..) = Self::estimate_call_with_gas(data, msg, tsk, VMTrace::NotTraced).await?; + match res.msg_rct { + Some(rct) => { + if rct.exit_code().value() != 0 { + return Ok(-1); + } + Ok(rct.gas_used() as i64) + } + None => Ok(-1), + } } } @@ -193,75 +276,17 @@ impl RpcMethod<3> for GasEstimateMessageGas { } } -async fn estimate_gas_limit( - data: &Ctx, - msg: Message, - ApiTipsetKey(tsk): &ApiTipsetKey, -) -> Result -where - DB: Blockstore + Send + Sync + 'static, -{ - let mut msg = msg; - msg.set_gas_limit(BLOCK_GAS_LIMIT); - msg.set_gas_fee_cap(TokenAmount::from_atto(0)); - msg.set_gas_premium(TokenAmount::from_atto(0)); - - let curr_ts = data.chain_store().load_required_tipset_or_heaviest(tsk)?; - let from_a = data - .state_manager - .resolve_to_key_addr(&msg.from, &curr_ts) - .await?; - - let pending = data.mpool.pending_for(&from_a); - let prior_messages: Vec = pending - .map(|s| s.into_iter().map(ChainMessage::Signed).collect::>()) - .unwrap_or_default(); - - let ts = data.mpool.cur_tipset.lock().clone(); - // Pretend that the message is signed. This has an influence on the gas - // cost. We obviously can't generate a valid signature. Instead, we just - // fill the signature with zeros. The validity is not checked. - let mut chain_msg = match from_a.protocol() { - Protocol::Secp256k1 => ChainMessage::Signed(SignedMessage::new_unchecked( - msg, - Signature::new_secp256k1(vec![0; SECP_SIG_LEN]), - )), - Protocol::Delegated => ChainMessage::Signed(SignedMessage::new_unchecked( - msg, - // In Lotus, delegated signatures have the same length as SECP256k1. - // This may or may not change in the future. - Signature::new(SignatureType::Delegated, vec![0; SECP_SIG_LEN]), - )), - _ => ChainMessage::Unsigned(msg), - }; - - let res = data - .state_manager - .call_with_gas(&mut chain_msg, &prior_messages, Some(ts)) - .await?; - match res.msg_rct { - Some(rct) => { - if rct.exit_code().value() != 0 { - return Ok(-1); - } - Ok(rct.gas_used() as i64) - } - None => Ok(-1), - } -} - pub async fn estimate_message_gas( data: &Ctx, - msg: Message, + mut msg: Message, _spec: Option, tsk: ApiTipsetKey, ) -> Result where DB: Blockstore + Send + Sync + 'static, { - let mut msg = msg; if msg.gas_limit == 0 { - let gl = estimate_gas_limit::(data, msg.clone(), &tsk).await?; + let gl = GasEstimateGasLimit::estimate_gas_limit(data, msg.clone(), &tsk).await?; let gl = gl as f64 * data.mpool.config.gas_limit_overestimation; msg.set_gas_limit((gl as u64).min(BLOCK_GAS_LIMIT)); } diff --git a/src/rpc/methods/state/types.rs b/src/rpc/methods/state/types.rs index e79fef0b957..32a191a60c3 100644 --- a/src/rpc/methods/state/types.rs +++ b/src/rpc/methods/state/types.rs @@ -195,6 +195,16 @@ pub struct InvocResult { } lotus_json_with_self!(InvocResult); +impl InvocResult { + pub fn new(msg: Message, ret: &ApplyRet) -> Self { + Self { + msg, + msg_rct: Some(ret.msg_receipt()), + error: ret.failure_info(), + } + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "PascalCase")] pub struct SectorExpiration { diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 15768a04d57..af1c4a5a12d 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -70,6 +70,7 @@ macro_rules! for_each_method { $callback!(crate::rpc::eth::EthAccounts); $callback!(crate::rpc::eth::EthBlockNumber); $callback!(crate::rpc::eth::EthChainId); + $callback!(crate::rpc::eth::EthEstimateGas); $callback!(crate::rpc::eth::EthFeeHistory); $callback!(crate::rpc::eth::EthGetCode); $callback!(crate::rpc::eth::EthGetStorageAt); diff --git a/src/rpc/types/mod.rs b/src/rpc/types/mod.rs index 207aff86b82..1a080f6a305 100644 --- a/src/rpc/types/mod.rs +++ b/src/rpc/types/mod.rs @@ -149,7 +149,17 @@ pub struct PeerID { pub multihash: Multihash, } -#[derive(Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq)] +#[derive( + Debug, + Clone, + Default, + Serialize, + Deserialize, + Eq, + PartialEq, + derive_more::From, + derive_more::Into, +)] pub struct ApiTipsetKey(pub Option); /// This wrapper is needed because of a bug in Lotus. diff --git a/src/shim/econ.rs b/src/shim/econ.rs index 70bba553dd7..467f0540f6d 100644 --- a/src/shim/econ.rs +++ b/src/shim/econ.rs @@ -130,6 +130,12 @@ impl From for BigInt { } } +impl From for TokenAmount { + fn from(value: BigInt) -> Self { + Self::from_atto(value) + } +} + impl From for TokenAmount { fn from(other: TokenAmount_v2) -> Self { (&other).into() diff --git a/src/state_manager/mod.rs b/src/state_manager/mod.rs index 9f171854bd6..4206dbab75a 100644 --- a/src/state_manager/mod.rs +++ b/src/state_manager/mod.rs @@ -27,10 +27,13 @@ use crate::metrics::HistogramTimerExt; use crate::networks::ChainConfig; use crate::rpc::state::{ApiInvocResult, InvocResult, MessageGasCost}; use crate::rpc::types::{MiningBaseInfo, SectorOnChainInfo}; -use crate::shim::actors::miner::MinerStateExt as _; -use crate::shim::actors::state_load::*; -use crate::shim::actors::verifreg::VerifiedRegistryStateExt; -use crate::shim::actors::LoadActorStateFromBlockstore; +use crate::shim::{ + actors::{ + miner::MinerStateExt as _, state_load::*, verifreg::VerifiedRegistryStateExt as _, + LoadActorStateFromBlockstore, + }, + executor::ApplyRet, +}; use crate::shim::{ address::{Address, Payload, Protocol}, clock::ChainEpoch, @@ -542,7 +545,8 @@ where message: &mut ChainMessage, prior_messages: &[ChainMessage], tipset: Option>, - ) -> Result { + trace_config: VMTrace, + ) -> Result<(InvocResult, ApplyRet), Error> { let ts = tipset.unwrap_or_else(|| self.cs.heaviest_tipset()); let (st, _) = self .tipset_state(&ts) @@ -574,7 +578,7 @@ where timestamp: ts.min_timestamp(), }, &self.engine, - VMTrace::NotTraced, + trace_config, )?; for msg in prior_messages { @@ -589,11 +593,7 @@ where vm.apply_message(message) })?; - Ok(InvocResult { - msg: message.message().clone(), - msg_rct: Some(ret.msg_receipt()), - error: ret.failure_info(), - }) + Ok((InvocResult::new(message.message().clone(), &ret), ret)) } /// Replays the given message and returns the result of executing the diff --git a/src/tool/subcommands/api_cmd.rs b/src/tool/subcommands/api_cmd.rs index 0001d20766f..6f05e8af9ea 100644 --- a/src/tool/subcommands/api_cmd.rs +++ b/src/tool/subcommands/api_cmd.rs @@ -20,7 +20,10 @@ use crate::rpc::gas::GasEstimateGasLimit; use crate::rpc::miner::BlockTemplate; use crate::rpc::state::StateGetAllClaims; use crate::rpc::types::{ApiTipsetKey, MessageFilter, MessageLookup}; -use crate::rpc::{self, eth::*}; +use crate::rpc::{ + self, + eth::{types::*, *}, +}; use crate::rpc::{prelude::*, start_rpc, RPCState}; use crate::shim::actors::MarketActorStateLoad as _; use crate::shim::address::{CurrentNetwork, Network}; @@ -1164,11 +1167,11 @@ fn eth_tests() -> Vec { ] } -fn eth_tests_with_tipset(shared_tipset: &Tipset) -> Vec { +fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset) -> Vec { let block_cid = shared_tipset.key().cid().unwrap(); let block_hash: Hash = block_cid.into(); - vec![ + let mut tests = vec![ RpcTest::identity( EthGetBalance::request(( EthAddress::from_str("0xff38c072f286e3b20b3954ca9f99c05fbecc64aa").unwrap(), @@ -1290,7 +1293,32 @@ fn eth_tests_with_tipset(shared_tipset: &Tipset) -> Vec { .unwrap(), ), RpcTest::identity(EthGetTransactionHashByCid::request((block_cid,)).unwrap()), - ] + ]; + + for block in shared_tipset.block_headers() { + let (bls_messages, secp_messages) = + crate::chain::store::block_messages(store, block).unwrap(); + for msg in sample_messages(bls_messages.iter(), secp_messages.iter()) { + if let Ok(eth_to_addr) = msg.to.try_into() { + tests.extend([RpcTest::identity( + EthEstimateGas::request(( + EthCallMessage { + from: None, + to: Some(eth_to_addr), + value: msg.value.clone().into(), + data: msg.params.clone().into(), + ..Default::default() + }, + Some(BlockNumberOrHash::BlockNumber(shared_tipset.epoch().into())), + )) + .unwrap(), + ) + .policy_on_rejected(PolicyOnRejected::Pass)]); + } + } + } + + tests } fn eth_state_tests_with_tipset( @@ -1363,7 +1391,7 @@ fn snapshot_tests(store: Arc, config: &ApiTestFlags) -> anyhow::Result< config.miner_address, )?); tests.extend(state_tests_with_tipset(&store, &tipset)?); - tests.extend(eth_tests_with_tipset(&tipset)); + tests.extend(eth_tests_with_tipset(&store, &tipset)); tests.extend(gas_tests_with_tipset(&tipset)); tests.extend(mpool_tests_with_tipset(&tipset)); tests.extend(eth_state_tests_with_tipset(