Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Comment thread
sudo-shashank marked this conversation as resolved.
- [#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.
Expand Down
2 changes: 1 addition & 1 deletion scripts/devnet/.env
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/tests/api_compare/.env
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/tests/bootstrapper/.env
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/tests/snapshot_parity/.env
Original file line number Diff line number Diff line change
@@ -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
Expand Down
138 changes: 126 additions & 12 deletions src/rpc/methods/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::<crate::state_manager::Error>()
.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.
//
Expand All @@ -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::<EthErrors>().is_some_and(|eth_err| {
matches!(eth_err, EthErrors::ExecutionReverted { .. })
Expand All @@ -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<EthUint64, ServerError> {
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::<EthErrors>()
.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<Tipset>,
Expand All @@ -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::<crate::state_manager::Error>()
.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 {
Expand All @@ -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<u64> {
pub async fn eth_gas_search(
data: &Ctx,
msg: Message,
tsk: &ApiTipsetKey,
sender_validation: SenderValidation,
) -> anyhow::Result<u64> {
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());
}
Expand All @@ -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!(
Expand All @@ -1984,6 +2083,7 @@ async fn gas_search(
msg: &Message,
prior_messages: Arc<Vec<ChainMessage>>,
ts: Tipset,
sender_validation: SenderValidation,
) -> anyhow::Result<u64> {
let mut high = msg.gas_limit;
let mut low = msg.gas_limit;
Expand All @@ -1994,11 +2094,18 @@ async fn gas_search(
prior_messages: Arc<Vec<ChainMessage>>,
ts: Tipset,
limit: u64,
sender_validation: SenderValidation,
) -> anyhow::Result<bool> {
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())
}
Expand All @@ -2010,6 +2117,7 @@ async fn gas_search(
prior_messages.shallow_clone(),
ts.shallow_clone(),
high,
sender_validation,
)
.await?
{
Expand All @@ -2028,6 +2136,7 @@ async fn gas_search(
prior_messages.shallow_clone(),
ts.shallow_clone(),
median,
sender_validation,
)
.await?
{
Expand Down Expand Up @@ -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 =
Expand Down
70 changes: 54 additions & 16 deletions src/rpc/methods/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -200,7 +200,7 @@ impl RpcMethod<2> for GasEstimateGasLimit {
(msg, tsk): Self::Params,
_: &http::Extensions,
) -> Result<Self::Ok, ServerError> {
Ok(Self::estimate_gas_limit(&ctx, msg, &tsk).await?)
Ok(Self::estimate_gas_limit(&ctx, msg, &tsk, SenderValidation::Enforce).await?)
}
}

Expand All @@ -209,16 +209,26 @@ impl GasEstimateGasLimit {
data: &Ctx,
mut msg: Message,
ApiTipsetKey(tsk): &ApiTipsetKey,
sender_validation: SenderValidation,
) -> anyhow::Result<(ApplyRet, Arc<Vec<ChainMessage>>, 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<Vec<ChainMessage>> = pending
Expand Down Expand Up @@ -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<i64> {
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<i64> {
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)
)
}
}

Expand All @@ -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<MessageSendSpec>,
tsk: ApiTipsetKey,
) -> Result<Message, ServerError> {
) -> anyhow::Result<Message> {
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?;
Expand Down
Loading
Loading