Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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 @@ -41,6 +41,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.
## Forest v0.35.0 "Shravan"

Non-mandatory release for all node operators. It includes some fixes and improvements, notably around state-related RPC. Note that this release contains breaking changes, so please read the changelog carefully before upgrading.
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
134 changes: 122 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,6 +1877,10 @@ async fn eth_estimate_gas(
// gas estimation actually run.
msg.gas_limit = 0;

if resolve_sender_validation(ctx, &msg.from, &tipset).await == 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) => {
// On failure, GasEstimateMessageGas doesn't actually return the invocation result,
Expand All @@ -1884,7 +1890,7 @@ async fn eth_estimate_gas(
// guts of EthCall). This will give us an ethereum specific error with revert
// information.
msg.set_gas_limit(BLOCK_GAS_LIMIT);
let err = match apply_message(ctx, Some(tipset), msg).await {
let err = match apply_message(ctx, None, msg).await {
Ok(_) => Error::msg(server_err.to_string()),
Err(e)
if e.downcast_ref::<EthErrors>().is_some_and(|eth_err| {
Expand All @@ -1899,12 +1905,72 @@ 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())
}
}
}

/// Returns [`SenderValidation::Skip`] for an EVM-contract or non-existent sender, which the FVM
/// can't validate. Defaults to [`SenderValidation::Enforce`].
async fn resolve_sender_validation(
ctx: &Ctx,
from: &FilecoinAddress,
tipset: &Tipset,
) -> SenderValidation {
let Ok(state) = ctx.state_manager.load_tipset_state(tipset).await else {
return SenderValidation::Enforce;
};
match ctx.state_manager.get_actor(from, state.state_root) {
Ok(None) => SenderValidation::Skip,
Ok(Some(actor)) if is_evm_actor(&actor.code) => SenderValidation::Skip,
_ => SenderValidation::Enforce,
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/// 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());
}
};

let gas_limit =
((gas_limit as f64 * ctx.mpool.gas_limit_overestimation()) as u64).min(BLOCK_GAS_LIMIT);
msg.set_gas_limit(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 +1985,34 @@ 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;

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 +2035,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 (_invoc_res, 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.msg_receipt().exit_code().is_success() {
return Ok(msg.gas_limit());
}
Expand All @@ -1964,7 +2059,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 +2079,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 +2090,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 (_invoc_res, 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.msg_receipt().exit_code().is_success())
}
Expand All @@ -2010,6 +2113,7 @@ async fn gas_search(
prior_messages.shallow_clone(),
ts.shallow_clone(),
high,
sender_validation,
)
.await?
{
Expand All @@ -2028,6 +2132,7 @@ async fn gas_search(
prior_messages.shallow_clone(),
ts.shallow_clone(),
median,
sender_validation,
)
.await?
{
Expand Down Expand Up @@ -3845,7 +3950,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
35 changes: 26 additions & 9 deletions src/rpc/methods/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,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 @@ -201,7 +201,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 @@ -210,16 +210,21 @@ impl GasEstimateGasLimit {
data: &Ctx,
mut msg: Message,
ApiTipsetKey(tsk): &ApiTipsetKey,
sender_validation: SenderValidation,
) -> anyhow::Result<(InvocResult, 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?
}
};

let pending = data.mpool.pending_for(&from_a).await;
let prior_messages: Arc<Vec<ChainMessage>> = pending
Expand Down Expand Up @@ -253,13 +258,19 @@ impl GasEstimateGasLimit {
prior_messages.shallow_clone(),
Some(ts.shallow_clone()),
VMFlush::Skip,
sender_validation,
)
.await?;
Ok((invoc_res, apply_ret, prior_messages, ts))
}

pub async fn estimate_gas_limit(data: &Ctx, msg: Message, tsk: &ApiTipsetKey) -> Result<i64> {
let (res, ..) = 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 (res, ..) = Self::estimate_call_with_gas(data, msg, tsk, sender_validation)
.await
.context("gas estimation failed")?;
match res.msg_rct {
Expand Down Expand Up @@ -306,7 +317,13 @@ pub async fn estimate_message_gas(
tsk: ApiTipsetKey,
) -> Result<Message, ServerError> {
if msg.gas_limit == 0 {
let gl = GasEstimateGasLimit::estimate_gas_limit(data, msg.clone(), &tsk).await?;
let gl = GasEstimateGasLimit::estimate_gas_limit(
data,
msg.clone(),
&tsk,
SenderValidation::Enforce,
)
.await?;
let gl = gl as f64 * data.mpool.gas_limit_overestimation();
msg.set_gas_limit((gl as u64).min(BLOCK_GAS_LIMIT));
}
Expand Down
13 changes: 13 additions & 0 deletions src/state_manager/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,24 @@ 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")]
SenderValidationFailed,
/// Other state manager error
#[error("{0}")]
Other(String),
}

/// 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 Error {
pub fn state(e: impl Display) -> Self {
Self::State(e.to_string())
Expand Down
Loading
Loading