Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rpc): Filecoin.EthEstimateGas #4496

Merged
merged 16 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
- [#4474](https://github.com/ChainSafe/forest/pull/4474) Add new subcommand
`forest-cli healthcheck ready`.

- [#4496](https://github.com/ChainSafe/forest/pull/4496) Add support for the
`Filecoin.EthEstimateGas` RPC method.

- [#4547](https://github.com/ChainSafe/forest/pull/4547) Add support for the
`Filecoin.MpoolPushUntrusted` RPC method.

Expand Down
2 changes: 2 additions & 0 deletions scripts/tests/api_compare/filter-list-offline
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,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
12 changes: 12 additions & 0 deletions src/message/chain_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,15 @@ impl MessageTrait for ChainMessage {
}
}
}

impl From<Message> for ChainMessage {
fn from(value: Message) -> Self {
Self::Unsigned(value)
}
}

impl From<SignedMessage> for ChainMessage {
fn from(value: SignedMessage) -> Self {
Self::Signed(value)
}
}
157 changes: 157 additions & 0 deletions src/rpc/methods/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,21 @@ 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::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};
Expand Down Expand Up @@ -1166,6 +1170,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<BlockNumberOrHash>);
type Ok = Uint64;

async fn handle(
ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
(tx, block_param): Self::Params,
) -> Result<Self::Ok, ServerError> {
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<DB>(
data: &Ctx<DB>,
msg: Message,
tsk: &ApiTipsetKey,
) -> anyhow::Result<u64>
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<DB>(
data: &Ctx<DB>,
msg: &Message,
prior_messages: &[ChainMessage],
ts: Arc<Tipset>,
) -> anyhow::Result<u64>
where
DB: Blockstore + Send + Sync + 'static,
{
let mut high = msg.gas_limit;
let mut low = msg.gas_limit;

async fn can_succeed<DB>(
data: &Ctx<DB>,
mut msg: Message,
prior_messages: &[ChainMessage],
ts: Arc<Tipset>,
limit: u64,
) -> anyhow::Result<bool>
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 {
Expand Down
86 changes: 86 additions & 0 deletions src/rpc/methods/eth/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ pub struct EthBytes(
);
lotus_json_with_self!(EthBytes);

impl From<RawBytes> for EthBytes {
fn from(value: RawBytes) -> Self {
Self(value.into())
}
}

#[derive(Debug, Deserialize, Serialize)]
pub struct GetBytecodeReturn(pub Option<Cid>);

Expand Down Expand Up @@ -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<EthAddress>,
pub to: Option<EthAddress>,
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<RawBytes> {
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<EthCallMessage> for Message {
type Error = anyhow::Error;
fn try_from(tx: EthCallMessage) -> Result<Self, Self::Error> {
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() {
Expand All @@ -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");
}
}
Loading
Loading