diff --git a/CHANGELOG.md b/CHANGELOG.md index 45ac5b99b0bc..1d1738c3a530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ ### Fixed +- [#7435](https://github.com/ChainSafe/forest/pull/7435): `eth_call` and `eth_estimateGas` now accept a `from` address that is an EVM contract or that doesn't exist on chain, matching Lotus/Geth. + - [#7412](https://github.com/ChainSafe/forest/issues/7412): Fixes quicknet "unchained" logic to fetch the `max_beacon_round` for all covered epochs - [#7446](https://github.com/ChainSafe/forest/pull/7446): Fixed a panic condition on `ChainNotify` when a client closes a connection just after subscription. diff --git a/scripts/devnet/.env b/scripts/devnet/.env index 32f677915e23..5fd084c841f9 100644 --- a/scripts/devnet/.env +++ b/scripts/devnet/.env @@ -1,4 +1,4 @@ -LOTUS_IMAGE=filecoin/lotus-all-in-one:c4e3b46b1-2k +LOTUS_IMAGE=filecoin/lotus-all-in-one:v1.36.2-2k FOREST_DATA_DIR=/forest_data LOTUS_DATA_DIR=/lotus_data FIL_PROOFS_PARAMETER_CACHE=/var/tmp/filecoin-proof-parameters diff --git a/scripts/tests/api_compare/.env b/scripts/tests/api_compare/.env index 6173cf3e55fd..fa45ea7e6cb9 100644 --- a/scripts/tests/api_compare/.env +++ b/scripts/tests/api_compare/.env @@ -1,6 +1,6 @@ # Note: this should be a `fat` image so that it contains the pre-downloaded filecoin proof parameters FOREST_IMAGE=ghcr.io/chainsafe/forest:edge-fat -LOTUS_IMAGE=filecoin/lotus-all-in-one:c4e3b46b1-calibnet +LOTUS_IMAGE=filecoin/lotus-all-in-one:v1.36.2-calibnet FIL_PROOFS_PARAMETER_CACHE=/var/tmp/filecoin-proof-parameters LOTUS_RPC_PORT=1234 LOTUS_VIA_GATEWAY_RPC_PORT=4568 diff --git a/scripts/tests/bootstrapper/.env b/scripts/tests/bootstrapper/.env index b56c7864942e..e0f9f538c44b 100644 --- a/scripts/tests/bootstrapper/.env +++ b/scripts/tests/bootstrapper/.env @@ -1,5 +1,5 @@ # Note: this should be a `fat` image so that it contains the pre-downloaded filecoin proof parameters -LOTUS_IMAGE=filecoin/lotus-all-in-one:c4e3b46b1-calibnet +LOTUS_IMAGE=filecoin/lotus-all-in-one:v1.36.2-calibnet FIL_PROOFS_PARAMETER_CACHE=/var/tmp/filecoin-proof-parameters LOTUS_RPC_PORT=1234 FOREST_RPC_PORT=2345 diff --git a/scripts/tests/snapshot_parity/.env b/scripts/tests/snapshot_parity/.env index 1cdd4e9da868..1bd6d37a7217 100644 --- a/scripts/tests/snapshot_parity/.env +++ b/scripts/tests/snapshot_parity/.env @@ -1,4 +1,4 @@ -LOTUS_IMAGE=filecoin/lotus-all-in-one:c4e3b46b1-calibnet +LOTUS_IMAGE=filecoin/lotus-all-in-one:v1.36.2-calibnet FIL_PROOFS_PARAMETER_CACHE=/var/tmp/filecoin-proof-parameters LOTUS_RPC_PORT=1234 FOREST_RPC_PORT=2345 diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index f7a302e054c0..cb39097e2012 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -64,7 +64,9 @@ use crate::shim::gas::GasOutputs; use crate::shim::message::Message; use crate::shim::trace::{CallReturn, ExecutionEvent}; use crate::shim::{clock::ChainEpoch, state_tree::StateTree}; -use crate::state_manager::{ExecutedMessage, ExecutedTipset, StateManager, TipsetState, VMFlush}; +use crate::state_manager::{ + ExecutedMessage, ExecutedTipset, SenderValidation, StateManager, TipsetState, VMFlush, +}; use crate::utils::cache::SizeTrackingCache; use crate::utils::db::BlockstoreExt as _; use crate::utils::encoding::from_slice_with_fallback; @@ -1875,8 +1877,23 @@ async fn eth_estimate_gas( // gas estimation actually run. msg.gas_limit = 0; + if sender_validation_for(ctx, &msg.from, &tipset) == SenderValidation::Skip { + return eth_estimate_gas_skip_sender(ctx, msg, tipset).await; + } + match gas::estimate_message_gas(ctx, msg.clone(), None, tipset.key().clone().into()).await { Err(server_err) => { + // Covers both a sender that doesn't exist (address resolution failed) and one the FVM + // rejected in preflight; either way, retry with skip-sender-validation. + if server_err + .downcast_ref::() + .is_some_and(|e| { + matches!(e, crate::state_manager::Error::SenderValidationFailed(_)) + }) + { + return eth_estimate_gas_skip_sender(ctx, msg, tipset).await; + } + // On failure, GasEstimateMessageGas doesn't actually return the invocation result, // it just returns an error. That means we can't get the revert reason. // @@ -1885,7 +1902,7 @@ async fn eth_estimate_gas( // information. msg.set_gas_limit(BLOCK_GAS_LIMIT); let err = match apply_message(ctx, Some(tipset), msg).await { - Ok(_) => Error::msg(server_err.to_string()), + Ok(_) => server_err, Err(e) if e.downcast_ref::().is_some_and(|eth_err| { matches!(eth_err, EthErrors::ExecutionReverted { .. }) @@ -1899,12 +1916,63 @@ async fn eth_estimate_gas( Err(err.context("failed to estimate gas").into()) } Ok(gassed_msg) => { - let expected_gas = eth_gas_search(ctx, gassed_msg, &tipset.key().into()).await?; + let expected_gas = eth_gas_search( + ctx, + gassed_msg, + &tipset.key().into(), + SenderValidation::Enforce, + ) + .await?; Ok(expected_gas.into()) } } } +/// An EVM contract sender fails the FVM's explicit-path sender check, so detect it up front and +/// estimate via the skip-sender-validation path, as Geth allows. A sender that doesn't exist at all +/// is caught when gas estimation fails. +fn sender_validation_for(ctx: &Ctx, from: &FilecoinAddress, tipset: &Tipset) -> SenderValidation { + match ctx.state_manager.get_actor(from, *tipset.parent_state()) { + Ok(Some(actor)) if is_evm_actor(&actor.code) => SenderValidation::Skip, + _ => SenderValidation::Enforce, + } +} + +/// Estimates gas for a contract or non-existent sender, skipping sender validation. +async fn eth_estimate_gas_skip_sender( + ctx: &Ctx, + mut msg: Message, + tipset: Tipset, +) -> Result { + let tsk: ApiTipsetKey = tipset.key().clone().into(); + + let gas_limit = match gas::GasEstimateGasLimit::estimate_gas_limit( + ctx, + msg.clone(), + &tsk, + SenderValidation::Skip, + ) + .await + { + Ok(gas_limit) => gas_limit, + Err(estimate_err) => { + msg.set_gas_limit(BLOCK_GAS_LIMIT); + if let Err(e) = apply_message(ctx, Some(tipset), msg).await + && e.downcast_ref::() + .is_some_and(|eth_err| matches!(eth_err, EthErrors::ExecutionReverted { .. })) + { + return Err(e.into()); + } + return Err(estimate_err.context("failed to estimate gas").into()); + } + }; + + msg.set_gas_limit(gas::overestimate_gas_limit(ctx, gas_limit)); + + let expected_gas = eth_gas_search(ctx, msg, &tsk, SenderValidation::Skip).await?; + Ok(expected_gas.into()) +} + async fn apply_message( ctx: &Ctx, tipset: Option, @@ -1919,11 +1987,36 @@ async fn apply_message( return Err(crate::state_manager::Error::ExpensiveFork { epoch: ts.epoch() }.into()); } - let (invoc_res, _) = ctx + let result = ctx .state_manager - .apply_on_state_with_gas(tipset, msg, VMFlush::Skip) - .await - .context("failed to apply on state with gas")?; + .apply_on_state_with_gas( + tipset.clone(), + msg.clone(), + VMFlush::Skip, + SenderValidation::Enforce, + ) + .await; + + // A missing sender surfaces as `SenderValidationFailed`; a sender the FVM rejected in preflight + // (e.g. a contract) surfaces as a `SYS_SENDER_INVALID` receipt. + let needs_skip = match &result { + Err(e) => e + .downcast_ref::() + .is_some_and(|e| matches!(e, crate::state_manager::Error::SenderValidationFailed(_))), + Ok((invoc_res, _)) => invoc_res + .msg_rct + .as_ref() + .is_some_and(|rct| rct.exit_code() == fvm_shared4::error::ExitCode::SYS_SENDER_INVALID), + }; + + let (invoc_res, _) = if needs_skip { + ctx.state_manager + .apply_on_state_with_gas(tipset, msg, VMFlush::Skip, SenderValidation::Skip) + .await + .context("failed to apply on state with gas (skip sender validation)")? + } else { + result.context("failed to apply on state with gas")? + }; // Extract receipt or return early if none match &invoc_res.msg_rct { @@ -1946,9 +2039,15 @@ async fn apply_message( Ok(invoc_res) } -pub async fn eth_gas_search(data: &Ctx, msg: Message, tsk: &ApiTipsetKey) -> anyhow::Result { +pub async fn eth_gas_search( + data: &Ctx, + msg: Message, + tsk: &ApiTipsetKey, + sender_validation: SenderValidation, +) -> anyhow::Result { let (apply_ret, prior_messages, ts) = - gas::GasEstimateGasLimit::estimate_call_with_gas(data, msg.clone(), tsk).await?; + gas::GasEstimateGasLimit::estimate_call_with_gas(data, msg.clone(), tsk, sender_validation) + .await?; if apply_ret.exit_code().is_success() { return Ok(msg.gas_limit()); } @@ -1964,7 +2063,7 @@ pub async fn eth_gas_search(data: &Ctx, msg: Message, tsk: &ApiTipsetKey) -> any }) ) }) { - let ret = gas_search(data, &msg, prior_messages, ts).await?; + let ret = gas_search(data, &msg, prior_messages, ts, sender_validation).await?; Ok(((ret as f64) * data.mpool.gas_limit_overestimation()) as u64) } else { anyhow::bail!( @@ -1984,6 +2083,7 @@ async fn gas_search( msg: &Message, prior_messages: Arc>, ts: Tipset, + sender_validation: SenderValidation, ) -> anyhow::Result { let mut high = msg.gas_limit; let mut low = msg.gas_limit; @@ -1994,11 +2094,18 @@ async fn gas_search( prior_messages: Arc>, ts: Tipset, limit: u64, + sender_validation: SenderValidation, ) -> anyhow::Result { msg.gas_limit = limit; let (apply_ret, ..) = data .state_manager - .call_with_gas(msg.into(), prior_messages, Some(ts), VMFlush::Skip) + .call_with_gas( + msg.into(), + prior_messages, + Some(ts), + VMFlush::Skip, + sender_validation, + ) .await?; Ok(apply_ret.exit_code().is_success()) } @@ -2010,6 +2117,7 @@ async fn gas_search( prior_messages.shallow_clone(), ts.shallow_clone(), high, + sender_validation, ) .await? { @@ -2028,6 +2136,7 @@ async fn gas_search( prior_messages.shallow_clone(), ts.shallow_clone(), median, + sender_validation, ) .await? { @@ -3874,7 +3983,12 @@ impl RpcMethod<3> for EthTraceCall { let (invoke_result, post_state_root) = ctx .state_manager - .apply_on_state_with_gas(Some(ts.shallow_clone()), msg.clone(), VMFlush::Flush) + .apply_on_state_with_gas( + Some(ts.shallow_clone()), + msg.clone(), + VMFlush::Flush, + SenderValidation::Enforce, + ) .await .context("failed to apply message")?; let post_state_root = diff --git a/src/rpc/methods/gas.rs b/src/rpc/methods/gas.rs index cd44aaa1eb37..8acde4413dc8 100644 --- a/src/rpc/methods/gas.rs +++ b/src/rpc/methods/gas.rs @@ -13,7 +13,7 @@ use crate::shim::{ econ::{BLOCK_GAS_LIMIT, TokenAmount}, message::Message, }; -use crate::state_manager::VMFlush; +use crate::state_manager::{SenderValidation, VMFlush}; use anyhow::Result; use enumflags2::BitFlags; use num::BigInt; @@ -200,7 +200,7 @@ impl RpcMethod<2> for GasEstimateGasLimit { (msg, tsk): Self::Params, _: &http::Extensions, ) -> Result { - Ok(Self::estimate_gas_limit(&ctx, msg, &tsk).await?) + Ok(Self::estimate_gas_limit(&ctx, msg, &tsk, SenderValidation::Enforce).await?) } } @@ -209,16 +209,26 @@ impl GasEstimateGasLimit { data: &Ctx, mut msg: Message, ApiTipsetKey(tsk): &ApiTipsetKey, + sender_validation: SenderValidation, ) -> anyhow::Result<(ApplyRet, Arc>, Tipset)> { 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_deterministic_address(msg.from, &curr_ts) - .await?; + let from_a = match sender_validation { + SenderValidation::Skip => msg.from, + SenderValidation::Enforce => data + .state_manager + .resolve_to_deterministic_address(msg.from, &curr_ts) + .await + .map_err(|e| { + crate::state_manager::Error::SenderValidationFailed(format!( + "resolving sender {}: {e:#}", + msg.from + )) + })?, + }; let pending = data.mpool.pending_for(&from_a).await; let prior_messages: Arc> = pending @@ -252,22 +262,39 @@ impl GasEstimateGasLimit { prior_messages.shallow_clone(), Some(ts.shallow_clone()), VMFlush::Skip, + sender_validation, ) .await?; Ok((apply_ret, prior_messages, ts)) } - pub async fn estimate_gas_limit(data: &Ctx, msg: Message, tsk: &ApiTipsetKey) -> Result { - let (apply_ret, ..) = Self::estimate_call_with_gas(data, msg, tsk) + pub async fn estimate_gas_limit( + data: &Ctx, + msg: Message, + tsk: &ApiTipsetKey, + sender_validation: SenderValidation, + ) -> Result { + let (apply_ret, ..) = Self::estimate_call_with_gas(data, msg, tsk, sender_validation) .await .context("gas estimation failed")?; - anyhow::ensure!( - apply_ret.exit_code().is_success(), + if apply_ret.exit_code().is_success() { + return Ok(apply_ret.gas_used() as i64); + } + if sender_validation == SenderValidation::Enforce + && apply_ret.exit_code() == fvm_shared4::error::ExitCode::SYS_SENDER_INVALID + { + return Err(crate::state_manager::Error::SenderValidationFailed(format!( + "message execution failed (exit=[{}], vm error=[{}])", + apply_ret.exit_code(), + apply_ret.failure_info().unwrap_or_default() + )) + .into()); + } + anyhow::bail!( "message execution failed: exit code: {}, reason: {}", apply_ret.exit_code().value(), apply_ret.failure_info().unwrap_or_default() - ); - Ok(apply_ret.gas_used() as i64) + ) } } @@ -293,16 +320,27 @@ impl RpcMethod<3> for GasEstimateMessageGas { } } +/// Applies the message pool's overestimation multiplier to an estimated gas limit and clamps it to +/// the block gas limit. +pub fn overestimate_gas_limit(data: &Ctx, gas_limit: i64) -> u64 { + ((gas_limit as f64 * data.mpool.gas_limit_overestimation()) as u64).min(BLOCK_GAS_LIMIT) +} + pub async fn estimate_message_gas( data: &Ctx, mut msg: Message, msg_spec: Option, tsk: ApiTipsetKey, -) -> Result { +) -> anyhow::Result { if msg.gas_limit == 0 { - let gl = GasEstimateGasLimit::estimate_gas_limit(data, msg.clone(), &tsk).await?; - let gl = gl as f64 * data.mpool.gas_limit_overestimation(); - msg.set_gas_limit((gl as u64).min(BLOCK_GAS_LIMIT)); + let gl = GasEstimateGasLimit::estimate_gas_limit( + data, + msg.clone(), + &tsk, + SenderValidation::Enforce, + ) + .await?; + msg.set_gas_limit(overestimate_gas_limit(data, gl)); } if msg.gas_premium.is_zero() { let gp = estimate_gas_premium(data, 10, &tsk).await?; diff --git a/src/state_manager/errors.rs b/src/state_manager/errors.rs index 2651ffb3b3c6..d5bb45a5143f 100644 --- a/src/state_manager/errors.rs +++ b/src/state_manager/errors.rs @@ -18,6 +18,9 @@ pub enum Error { "required historical state unavailable: refusing explicit call due to state fork at epoch {epoch}" )] ExpensiveFork { epoch: ChainEpoch }, + /// Sender doesn't exist or isn't a valid sender type. + #[error("sender validation failed: {0}")] + SenderValidationFailed(String), /// Other state manager error #[error("{0}")] Other(String), diff --git a/src/state_manager/message_simulation.rs b/src/state_manager/message_simulation.rs index c2c73ec829f9..0834b984c976 100644 --- a/src/state_manager/message_simulation.rs +++ b/src/state_manager/message_simulation.rs @@ -7,7 +7,7 @@ use crate::interpreter::{ExecutionContext, IMPLICIT_MESSAGE_GAS_LIMIT, VM, VMTra use crate::message::{MessageRead as _, MessageReadWrite as _}; use crate::rpc::state::{ApiInvocResult, MessageGasCost}; use crate::shim::executor::ApplyRet; -use crate::shim::message::Message; +use crate::shim::message::{METHOD_SEND, Message}; use crate::state_migration::run_state_migrations; use std::time::Duration; use tracing::instrument; @@ -185,14 +185,30 @@ impl StateManager { tipset: Option, msg: Message, vm_flush: VMFlush, + sender_validation: SenderValidation, ) -> anyhow::Result<(ApiInvocResult, Option)> { let ts = tipset.unwrap_or_else(|| self.heaviest_tipset()); - let from_a = self.resolve_to_deterministic_address(msg.from, &ts).await?; - let chain_msg = ChainMessage::for_gas_estimation(msg.clone(), from_a.protocol()); + let from_protocol = match sender_validation { + SenderValidation::Skip => msg.from.protocol(), + SenderValidation::Enforce => self + .resolve_to_deterministic_address(msg.from, &ts) + .await + .map_err(|e| { + Error::SenderValidationFailed(format!("resolving sender {}: {e:#}", msg.from)) + })? + .protocol(), + }; + let chain_msg = ChainMessage::for_gas_estimation(msg.clone(), from_protocol); let (apply_ret, duration, state_root) = self - .call_with_gas(chain_msg, Default::default(), Some(ts), vm_flush) + .call_with_gas( + chain_msg, + Default::default(), + Some(ts), + vm_flush, + sender_validation, + ) .await?; Ok(( @@ -218,6 +234,7 @@ impl StateManager { prior_messages: Arc>, tipset: Option, vm_flush: VMFlush, + sender_validation: SenderValidation, ) -> Result<(ApplyRet, Duration, Option), Error> { let ts = tipset.unwrap_or_else(|| self.heaviest_tipset()); let TipsetState { state_root, .. } = self @@ -233,7 +250,7 @@ impl StateManager { tokio::task::spawn_blocking(move || { // FVM requires a stack size of 64MiB. The alternative is to use `ThreadedExecutor` from // FVM, but that introduces some constraints, and possible deadlocks. - let (ret, duration, state_cid) = stacker::grow(64 << 20, || -> anyhow::Result<_> { + let (ret, duration, state_cid) = stacker::grow(64 << 20, || -> Result<_, Error> { let mut vm = VM::new( ExecutionContext { heaviest_tipset: ts.clone(), @@ -258,13 +275,57 @@ impl StateManager { vm.apply_message(msg)?; } - let from_actor = vm + let mut sender_created = false; + let from_actor = match vm .get_actor(&message.from()) .map_err(|e| Error::Other(format!("Could not get actor from state: {e:#}")))? - .ok_or_else(|| Error::Other("cant find actor in state tree".to_string()))?; + { + Some(actor) => actor, + None => match sender_validation { + SenderValidation::Enforce => { + return Err(Error::SenderValidationFailed(format!( + "sender {} not found on chain", + message.from() + ))); + } + SenderValidation::Skip => { + let placeholder_send = Message { + from: Address::SYSTEM_ACTOR, + to: message.from(), + method_num: METHOD_SEND, + gas_limit: IMPLICIT_MESSAGE_GAS_LIMIT as u64, + ..Default::default() + }; + let (create_ret, _) = vm.apply_implicit_message(&placeholder_send)?; + if !create_ret.msg_receipt().exit_code().is_success() { + return Err(Error::Other(format!( + "failed to create ephemeral sender placeholder (exit {}): {}", + create_ret.msg_receipt().exit_code(), + create_ret.failure_info().unwrap_or_default() + ))); + } + sender_created = true; + vm.get_actor(&message.from()) + .map_err(|e| { + Error::Other(format!("Could not get placeholder actor: {e:#}")) + })? + .ok_or_else(|| { + Error::SenderValidationFailed(format!( + "ephemeral sender placeholder {} missing after creation", + message.from() + )) + })? + } + }, + }; message.set_sequence(from_actor.sequence); - let (ret, duration) = vm.apply_message(&message)?; + let (ret, duration) = match (sender_validation, sender_created) { + (SenderValidation::Skip, false) => { + vm.apply_implicit_message(message.message())? + } + _ => vm.apply_message(&message)?, + }; let state_root = match vm_flush { VMFlush::Flush => Some(vm.flush()?), VMFlush::Skip => None, diff --git a/src/state_manager/mod.rs b/src/state_manager/mod.rs index 3709d5202ad0..61d046530999 100644 --- a/src/state_manager/mod.rs +++ b/src/state_manager/mod.rs @@ -215,6 +215,16 @@ pub enum VMFlush { Skip, } +/// Whether to enforce the FVM sender checks. +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] +pub enum SenderValidation { + /// Enforce the FVM explicit-path sender checks. + #[default] + Enforce, + /// Skip sender validation for EVM contracts and non-existent senders. + Skip, +} + impl StateManager { pub fn new(cs: ChainStore) -> anyhow::Result { Self::new_with_engine(cs, GLOBAL_MULTI_ENGINE.clone()) diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index 3bedbee19308..64b328b23554 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -13,7 +13,7 @@ use crate::rpc; use crate::rpc::auth::AuthNewParams; use crate::rpc::beacon::BeaconGetEntry; use crate::rpc::eth::{ - ApiEthTx, BlockNumberOrHash, EthInt64, Predefined, new_eth_tx_from_signed_message, + ApiEthTx, BlockNumberOrHash, EthBigInt, EthInt64, Predefined, new_eth_tx_from_signed_message, trace::types::*, types::*, }; use crate::rpc::gas::{GasEstimateGasLimit, GasEstimateMessageGas}; @@ -145,6 +145,10 @@ const ZERO_ADDRESS: &str = "0x0000000000000000000000000000000000000000"; const MINER_ADDRESS: Address = Address::new_id(78216); // https://calibration.filscan.io/en/miner/t078216 const ACCOUNT_ADDRESS: Address = Address::new_id(1234); // account actor address `t01234` const EVM_ADDRESS: &str = "t410fbqoynu2oi2lxam43knqt6ordiowm2ywlml27z4i"; +// A calibnet EVM contract, and a `getBalance(address)` call against it. +const CALIBNET_EVM_CONTRACT: &str = "0x0c1d86d34e469770339b53613f3a2343accd62cb"; +const GET_BALANCE_CALLDATA: &str = + "0xf8b2cb4f000000000000000000000000CbfF24DED1CE6B53712078759233Ac8f91ea71B6"; /// Brief description of a single method call against a single host #[derive( @@ -261,6 +265,7 @@ pub struct TestResult { pub duration: Duration, } +#[derive(Clone, Copy)] pub(super) enum PolicyOnRejected { Fail, Pass, @@ -1499,13 +1504,8 @@ fn eth_tests(server_mode: ServerMode) -> anyhow::Result> { let cases = [ ( - Some(EthAddress::from_str( - "0x0c1d86d34e469770339b53613f3a2343accd62cb", - )?), - Some( - "0xf8b2cb4f000000000000000000000000CbfF24DED1CE6B53712078759233Ac8f91ea71B6" - .parse()?, - ), + Some(EthAddress::from_str(CALIBNET_EVM_CONTRACT)?), + Some(GET_BALANCE_CALLDATA.parse()?), ), (Some(EthAddress::from_str(ZERO_ADDRESS)?), None), // Assert contract creation, which is invoked via setting the `to` field to `None` and @@ -1546,11 +1546,11 @@ fn eth_tests(server_mode: ServerMode) -> anyhow::Result> { let cases = [ Some(EthAddressList::List(vec![])), Some(EthAddressList::List(vec![ - EthAddress::from_str("0x0c1d86d34e469770339b53613f3a2343accd62cb")?, + EthAddress::from_str(CALIBNET_EVM_CONTRACT)?, EthAddress::from_str("0x89beb26addec4bc7e9f475aacfd084300d6de719")?, ])), Some(EthAddressList::Single(EthAddress::from_str( - "0x0c1d86d34e469770339b53613f3a2343accd62cb", + CALIBNET_EVM_CONTRACT, )?)), None, ]; @@ -1643,6 +1643,118 @@ fn eth_call_api_err_tests(epoch: ChainEpoch) -> Vec { tests } +fn eth_skip_sender_tests(epoch: ChainEpoch) -> anyhow::Result> { + let mut tests = eth_skip_sender_success_tests(epoch)?; + tests.extend(eth_skip_sender_insufficient_funds_tests(epoch)?); + tests.extend(eth_skip_sender_create_reject_tests(epoch)?); + tests.extend(eth_skip_sender_block_param_tests(epoch)?); + Ok(tests) +} + +/// Both methods share the sender-validation logic, so every message is tested against both. +fn eth_call_and_estimate_gas_tests( + epoch: ChainEpoch, + policy: PolicyOnRejected, + messages: impl IntoIterator, +) -> anyhow::Result> { + let messages = messages.into_iter(); + let mut tests = Vec::with_capacity(2 * messages.len()); + let block = BlockNumberOrHash::from_block_number(epoch); + for msg in messages { + tests.push( + RpcTest::identity(EthCall::request((msg.clone(), block.clone()))?) + .policy_on_rejected(policy), + ); + tests.push( + RpcTest::identity(EthEstimateGas::request((msg, Some(block.clone())))?) + .policy_on_rejected(policy), + ); + } + Ok(tests) +} + +fn eth_skip_sender_success_tests(epoch: ChainEpoch) -> anyhow::Result> { + let to = EthAddress::from_str(CALIBNET_EVM_CONTRACT)?; + let calldata: EthBytes = GET_BALANCE_CALLDATA.parse()?; + let initcode = + EthBytes::from_str(concat!("0x", include_str!("contracts/cthulhu/invoke.hex")).trim())?; + let contract = EthAddress::from_filecoin_address(&Address::from_str(EVM_ADDRESS)?)?; + let non_existent = generate_eth_random_address()?; + + let messages = [ + // `from` is an existing EVM contract. + (Some(contract), Some(to), Some(calldata.clone())), + // `from` is an address that does not exist on chain. + (Some(non_existent), Some(to), Some(calldata.clone())), + // No `from` field — should still succeed. + (None, Some(to), Some(calldata)), + // No `to` means contract creation; `from` does not exist on chain. + (Some(non_existent), None, Some(initcode)), + ] + .map(|(from, to, data)| EthCallMessage { + from, + to, + data, + ..Default::default() + }); + eth_call_and_estimate_gas_tests(epoch, PolicyOnRejected::Fail, messages) +} + +fn eth_skip_sender_insufficient_funds_tests(epoch: ChainEpoch) -> anyhow::Result> { + let to = EthAddress::from_str(CALIBNET_EVM_CONTRACT)?; + let contract = EthAddress::from_filecoin_address(&Address::from_str(EVM_ADDRESS)?)?; + let non_existent = generate_eth_random_address()?; + let eoa = EthAddress::from_filecoin_address(&KNOWN_CALIBNET_F4_ADDRESS)?; + let value = EthBigInt::from(TokenAmount::from_whole(1_000_000)); + + // Value is higher than the sender's balance. + let messages = [contract, non_existent, eoa].map(|from| EthCallMessage { + from: Some(from), + to: Some(to), + value: Some(value), + ..Default::default() + }); + eth_call_and_estimate_gas_tests(epoch, PolicyOnRejected::PassWithIdenticalError, messages) +} + +fn eth_skip_sender_create_reject_tests(epoch: ChainEpoch) -> anyhow::Result> { + let initcode = + EthBytes::from_str(concat!("0x", include_str!("contracts/cthulhu/invoke.hex")).trim())?; + let div_zero = EthBytes::from_str(include_str!( + "./contracts/divide_by_zero_err/divide_by_zero_err.hex" + ))?; + let assert_err = EthBytes::from_str(include_str!("contracts/assert_err/assert_err.hex"))?; + let contract = EthAddress::from_filecoin_address(&Address::from_str(EVM_ADDRESS)?)?; + let non_existent = generate_eth_random_address()?; + + let messages = [ + // Contract creation with a contract as `from` is not allowed. + (Some(contract), initcode), + // Contract creation whose init code fails via divide by zero. + (Some(contract), div_zero.clone()), + (Some(non_existent), div_zero), + // Contract creation whose init code fails via assert. + (Some(contract), assert_err.clone()), + (Some(non_existent), assert_err), + ] + .map(|(from, data)| EthCallMessage { + from, + data: Some(data), + ..Default::default() + }); + eth_call_and_estimate_gas_tests(epoch, PolicyOnRejected::PassWithIdenticalError, messages) +} + +fn eth_skip_sender_block_param_tests(epoch: ChainEpoch) -> anyhow::Result> { + let to = EthAddress::from_str(CALIBNET_EVM_CONTRACT)?; + // Asking for a block in the future — both nodes reject + let messages = [EthCallMessage { + to: Some(to), + ..Default::default() + }]; + eth_call_and_estimate_gas_tests(epoch + 1000, PolicyOnRejected::Pass, messages) +} + fn eth_tests_with_tipset( store: &DB, shared_tipset: &Tipset, @@ -2506,6 +2618,8 @@ fn eth_state_tests_with_tipset( // Test eth_call API errors tests.extend(eth_call_api_err_tests(shared_tipset.epoch())); + tests.extend(eth_skip_sender_tests(shared_tipset.epoch())?); + Ok(tests) } diff --git a/src/tool/subcommands/api_cmd/test_snapshots.txt b/src/tool/subcommands/api_cmd/test_snapshots.txt index a89ad79de311..6d102eac2abb 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots.txt @@ -57,10 +57,18 @@ filecoin_ethblocknumber_1741272348346171.rpcsnap.json.zst filecoin_ethcall_1744204533050503.rpcsnap.json.zst filecoin_ethcall_1744204533058637.rpcsnap.json.zst filecoin_ethcall_1744204533066529.rpcsnap.json.zst +filecoin_ethcall_contract_from_1786069718284042.rpcsnap.json.zst +filecoin_ethcall_create_1786069718289675.rpcsnap.json.zst +filecoin_ethcall_nonexistent_from_1786069718279056.rpcsnap.json.zst +filecoin_ethcall_omitted_from_1786069718288176.rpcsnap.json.zst filecoin_ethcall_v2_1765790311230200.rpcsnap.json.zst filecoin_ethcall_v2_1765790311230270.rpcsnap.json.zst filecoin_ethcall_v2_1765790311230334.rpcsnap.json.zst filecoin_ethchainid_1736937942819147.rpcsnap.json.zst +filecoin_ethestimategas_contract_from_1786068422578959.rpcsnap.json.zst +filecoin_ethestimategas_create_1786068423971276.rpcsnap.json.zst +filecoin_ethestimategas_nonexistent_from_1786068422839752.rpcsnap.json.zst +filecoin_ethestimategas_omitted_from_1786068423870066.rpcsnap.json.zst filecoin_ethfeehistory_1781166099973654.rpcsnap.json.zst filecoin_ethfeehistory_v2_1781166099990041.rpcsnap.json.zst filecoin_ethgasprice_1758725940980141.rpcsnap.json.zst