diff --git a/Cargo.lock b/Cargo.lock index 954fcb32cb..060f873857 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4973,6 +4973,8 @@ name = "reth-rpc" version = "0.1.0" dependencies = [ "async-trait", + "bytes", + "ethers-core", "futures", "hex", "http", diff --git a/crates/primitives/src/constants.rs b/crates/primitives/src/constants.rs index 4739074f82..c38819ae35 100644 --- a/crates/primitives/src/constants.rs +++ b/crates/primitives/src/constants.rs @@ -3,6 +3,9 @@ use crate::H256; use hex_literal::hex; +/// The first four bytes of the call data for a function call specifies the function to be called. +pub const SELECTOR_LEN: usize = 4; + /// Initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) pub const EIP1559_INITIAL_BASE_FEE: u64 = 1_000_000_000; diff --git a/crates/rpc/rpc/Cargo.toml b/crates/rpc/rpc/Cargo.toml index 9e1a9a6126..34e20387a1 100644 --- a/crates/rpc/rpc/Cargo.toml +++ b/crates/rpc/rpc/Cargo.toml @@ -24,6 +24,7 @@ reth-tasks = { path = "../../tasks" } # eth revm = { version = "3.0.0", features = ["optional_block_gas_limit"] } +ethers-core = { git = "https://github.com/gakonst/ethers-rs" } # rpc jsonrpsee = { version = "0.16" } @@ -39,6 +40,7 @@ tower = "0.4" tokio-stream = "0.1" pin-project = "1.0" +bytes = "1.4" secp256k1 = { version = "0.26.0", features = [ "global-context", "rand-std", diff --git a/crates/rpc/rpc/src/eth/api/call.rs b/crates/rpc/rpc/src/eth/api/call.rs index 60cb62bd66..46d4cca807 100644 --- a/crates/rpc/rpc/src/eth/api/call.rs +++ b/crates/rpc/rpc/src/eth/api/call.rs @@ -3,20 +3,25 @@ #![allow(unused)] // TODO rm later use crate::{ - eth::error::{EthApiError, EthResult, InvalidTransactionError}, + eth::error::{EthApiError, EthResult, InvalidTransactionError, RevertError}, EthApi, }; -use reth_primitives::{AccessList, Address, BlockId, Bytes, TransactionKind, U128, U256}; +use reth_primitives::{ + AccessList, Address, BlockId, BlockNumberOrTag, Bytes, TransactionKind, U128, U256, +}; use reth_provider::{BlockProvider, EvmEnvProvider, StateProvider, StateProviderFactory}; use reth_revm::database::{State, SubState}; use reth_rpc_types::CallRequest; use revm::{ - primitives::{ruint::Uint, BlockEnv, CfgEnv, Env, ResultAndState, TransactTo, TxEnv}, + primitives::{ + ruint::Uint, BlockEnv, CfgEnv, Env, ExecutionResult, Halt, ResultAndState, TransactTo, + TxEnv, + }, Database, }; // Gas per transaction not creating a contract. -pub(crate) const MIN_TRANSACTION_GAS: U256 = Uint::from_limbs([21_000, 0, 0, 0]); +const MIN_TRANSACTION_GAS: u64 = 21_000u64; impl EthApi where @@ -53,13 +58,34 @@ where transact(&mut db, env) } - /// Estimate gas needed for execution of the `request` at the [BlockId] . - pub(crate) fn estimate_gas_at(&self, mut request: CallRequest, at: BlockId) -> EthResult { - // TODO get a StateProvider for the given blockId and BlockEnv - todo!() + /// Estimate gas needed for execution of the `request` at the [BlockId]. + pub(crate) async fn estimate_gas_at( + &self, + request: CallRequest, + at: BlockId, + ) -> EthResult { + // TODO handle Pending state's env + let (cfg, block_env) = match at { + BlockId::Number(BlockNumberOrTag::Pending) => { + // This should perhaps use the latest env settings and update block specific + // settings like basefee/number + unimplemented!("support pending state env") + } + hash_or_num => { + let block_hash = self + .client() + .block_hash_for_id(hash_or_num)? + .ok_or_else(|| EthApiError::UnknownBlockNumber)?; + self.cache().get_evm_env(block_hash).await? + } + }; + let state = self.state_at_block_id(at)?.ok_or_else(|| EthApiError::UnknownBlockNumber)?; + self.estimate_gas_with(cfg, block_env, request, state) } /// Estimates the gas usage of the `request` with the state. + /// + /// This will execute the [CallRequest] and find the best gas limit via binary search fn estimate_gas_with( &self, cfg: CfgEnv, @@ -70,26 +96,160 @@ where where S: StateProvider, { + // keep a copy of gas related request values + let request_gas = request.gas; + let request_gas_price = request.gas_price; + let env_gas_limit = block.gas_limit; + // get the highest possible gas limit, either the request's set value or the currently // configured gas limit let mut highest_gas_limit = request.gas.unwrap_or(block.gas_limit); // Configure the evm env let mut env = build_call_evm_env(cfg, block, request)?; + let mut db = SubState::new(State::new(state)); // if the request is a simple transfer we can optimize if env.tx.data.is_empty() { if let TransactTo::Call(to) = env.tx.transact_to { - let no_code = state.account_code(to)?.map(|code| code.is_empty()).unwrap_or(true); - if no_code { - return Ok(MIN_TRANSACTION_GAS) + if let Ok(code) = db.db.state().account_code(to) { + let no_code_callee = code.map(|code| code.is_empty()).unwrap_or(true); + if no_code_callee { + // simple transfer, check if caller has sufficient funds + let mut available_funds = + db.basic(env.tx.caller)?.map(|acc| acc.balance).unwrap_or_default(); + if env.tx.value > available_funds { + return Err(InvalidTransactionError::InsufficientFundsForTransfer.into()) + } + return Ok(U256::from(MIN_TRANSACTION_GAS)) + } } } } - let mut db = SubState::new(State::new(state)); + // check funds of the sender + let gas_price = env.tx.gas_price; + if gas_price > U256::ZERO { + let mut available_funds = + db.basic(env.tx.caller)?.map(|acc| acc.balance).unwrap_or_default(); + if env.tx.value > available_funds { + return Err(InvalidTransactionError::InsufficientFunds.into()) + } + // subtract transferred value from available funds + // SAFETY: value < available_funds, checked above + available_funds -= env.tx.value; + // amount of gas the sender can afford with the `gas_price` + // SAFETY: gas_price not zero + let allowance = available_funds.checked_div(gas_price).unwrap_or_default(); - todo!() + if highest_gas_limit > allowance { + // cap the highest gas limit by max gas caller can afford with given gas price + highest_gas_limit = allowance; + } + } + + // if the provided gas limit is less than computed cap, use that + let gas_limit = std::cmp::min(U256::from(env.tx.gas_limit), highest_gas_limit); + env.block.gas_limit = gas_limit; + + // execute the call without writing to db + let (res, mut env) = transact(&mut db, env)?; + match res.result { + ExecutionResult::Success { .. } => { + // succeeded + } + ExecutionResult::Halt { reason, .. } => { + return match reason { + Halt::OutOfGas(_) => Err(InvalidTransactionError::OutOfGas(gas_limit).into()), + Halt::NonceOverflow => Err(InvalidTransactionError::NonceMaxValue.into()), + err => Err(InvalidTransactionError::EvmHalt(err).into()), + } + } + ExecutionResult::Revert { output, .. } => { + // if price or limit was included in the request then we can execute the request + // again with the block's gas limit to check if revert is gas related or not + return if request_gas.is_some() || request_gas_price.is_some() { + let req_gas_limit = env.tx.gas_limit; + env.tx.gas_limit = env_gas_limit.try_into().unwrap_or(u64::MAX); + let (res, _) = transact(&mut db, env)?; + match res.result { + ExecutionResult::Success { .. } => { + // transaction succeeded by manually increasing the gas limit to + // highest, which means the caller lacks funds to pay for the tx + Err(InvalidTransactionError::OutOfGas(U256::from(req_gas_limit)).into()) + } + ExecutionResult::Revert { .. } => { + // reverted again after bumping the limit + Err(InvalidTransactionError::Revert(RevertError::new(output)).into()) + } + ExecutionResult::Halt { reason, .. } => { + Err(InvalidTransactionError::EvmHalt(reason).into()) + } + } + } else { + // the transaction did revert + Err(InvalidTransactionError::Revert(RevertError::new(output)).into()) + } + } + } + + // at this point we know the call succeeded but want to find the _best_ (lowest) gas the + // transaction succeeds with. we find this by doing a binary search over the + // possible range NOTE: this is the gas the transaction used, which is less than the + // transaction requires to succeed + let gas_used = res.result.gas_used(); + // the lowest value is capped by the gas it takes for a transfer + let mut lowest_gas_limit = MIN_TRANSACTION_GAS; + let mut highest_gas_limit: u64 = highest_gas_limit.try_into().unwrap_or(u64::MAX); + // pick a point that's close to the estimated gas + let mut mid_gas_limit = + std::cmp::min(gas_used * 3, (highest_gas_limit + lowest_gas_limit) / 2); + + let mut last_highest_gas_limit = highest_gas_limit; + + // binary search + while (highest_gas_limit - lowest_gas_limit) > 1 { + let mut env = env.clone(); + env.tx.gas_limit = mid_gas_limit; + let (res, _) = transact(&mut db, env)?; + match res.result { + ExecutionResult::Success { .. } => { + // cap the highest gas limit with succeeding gas limit + highest_gas_limit = mid_gas_limit; + // if last two successful estimations only vary by 10%, we consider this to be + // sufficiently accurate + const ACCURACY: u64 = 10; + if (last_highest_gas_limit - highest_gas_limit) * ACCURACY / + last_highest_gas_limit < + 1u64 + { + return Ok(U256::from(highest_gas_limit)) + } + last_highest_gas_limit = highest_gas_limit; + } + ExecutionResult::Revert { .. } => { + // increase the lowest gas limit + lowest_gas_limit = mid_gas_limit; + } + ExecutionResult::Halt { reason, .. } => { + match reason { + Halt::OutOfGas(_) => { + // increase the lowest gas limit + lowest_gas_limit = mid_gas_limit; + } + err => { + // these should be unreachable because we know the transaction succeeds, + // but we consider these cases an error + return Err(InvalidTransactionError::EvmHalt(err).into()) + } + } + } + } + // new midpoint + mid_gas_limit = (highest_gas_limit + lowest_gas_limit) / 2; + } + + Ok(U256::from(highest_gas_limit)) } } diff --git a/crates/rpc/rpc/src/eth/api/mod.rs b/crates/rpc/rpc/src/eth/api/mod.rs index 3e4d14063d..29f321b482 100644 --- a/crates/rpc/rpc/src/eth/api/mod.rs +++ b/crates/rpc/rpc/src/eth/api/mod.rs @@ -76,6 +76,11 @@ impl EthApi { } } + /// Returns the state cache frontend + pub(crate) fn cache(&self) -> &EthStateCache { + &self.inner.eth_cache + } + /// Returns the inner `Client` pub(crate) fn client(&self) -> &Client { &self.inner.client diff --git a/crates/rpc/rpc/src/eth/error.rs b/crates/rpc/rpc/src/eth/error.rs index 82761d5111..ee1e42ab0e 100644 --- a/crates/rpc/rpc/src/eth/error.rs +++ b/crates/rpc/rpc/src/eth/error.rs @@ -2,10 +2,10 @@ use crate::result::{internal_rpc_err, rpc_err}; use jsonrpsee::{core::Error as RpcError, types::error::INVALID_PARAMS_CODE}; -use reth_primitives::U128; +use reth_primitives::{constants::SELECTOR_LEN, U128, U256}; use reth_rpc_types::BlockError; use reth_transaction_pool::error::PoolError; -use revm::primitives::EVMError; +use revm::primitives::{EVMError, Halt}; /// Result alias pub(crate) type EthResult = Result; @@ -166,6 +166,15 @@ pub enum InvalidTransactionError { /// Thrown if the sender of a transaction is a contract. #[error("sender not an eoa")] SenderNoEOA, + /// Thrown during estimate if caller has insufficient funds to cover the tx. + #[error("Out of gas: gas required exceeds allowance: {0:?}")] + OutOfGas(U256), + /// Thrown if executing a transaction failed during estimate/call + #[error("{0}")] + Revert(RevertError), + /// Unspecific evm halt error + #[error("EVM error {0:?}")] + EvmHalt(Halt), } impl InvalidTransactionError { @@ -175,6 +184,7 @@ impl InvalidTransactionError { InvalidTransactionError::GasTooLow | InvalidTransactionError::GasTooHigh => { EthRpcErrorCode::InvalidInput.code() } + InvalidTransactionError::Revert(_) => EthRpcErrorCode::ExecutionError.code(), _ => EthRpcErrorCode::TransactionRejected.code(), } } @@ -182,7 +192,17 @@ impl InvalidTransactionError { impl From for RpcError { fn from(err: InvalidTransactionError) -> Self { - rpc_err(err.error_code(), err.to_string(), None) + match err { + InvalidTransactionError::Revert(revert) => { + // include out data if some + rpc_err( + revert.error_code(), + revert.to_string(), + revert.output.as_ref().map(|out| out.as_ref()), + ) + } + err => rpc_err(err.error_code(), err.to_string(), None), + } } } @@ -215,6 +235,48 @@ impl From for InvalidTransactionError { } } +/// Represents a reverted transaction and its output data. +/// +/// Displays "execution reverted(: reason)?" if the reason is a string. +#[derive(Debug, Clone)] +pub struct RevertError { + /// The transaction output data + /// + /// Note: this is `None` if output was empty + output: Option, +} + +// === impl RevertError == + +impl RevertError { + /// Wraps the output bytes + /// + /// Note: this is intended to wrap an revm output + pub fn new(output: bytes::Bytes) -> Self { + if output.is_empty() { + Self { output: None } + } else { + Self { output: Some(output) } + } + } + + fn error_code(&self) -> i32 { + EthRpcErrorCode::ExecutionError.code() + } +} + +impl std::fmt::Display for RevertError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("execution reverted")?; + if let Some(reason) = self.output.as_ref().and_then(decode_revert_reason) { + write!(f, ": {reason}")?; + } + Ok(()) + } +} + +impl std::error::Error for RevertError {} + /// A helper error type that mirrors `geth` Txpool's error messages #[derive(Debug, thiserror::Error)] pub(crate) enum GethTxPoolError { @@ -254,3 +316,15 @@ impl From for EthApiError { EthApiError::PoolError(GethTxPoolError::from(err)) } } + +/// Returns the revert reason from the `revm::TransactOut` data, if it's an abi encoded String. +/// +/// **Note:** it's assumed the `out` buffer starts with the call's signature +pub(crate) fn decode_revert_reason(out: impl AsRef<[u8]>) -> Option { + use ethers_core::abi::AbiDecode; + let out = out.as_ref(); + if out.len() < SELECTOR_LEN { + return None + } + String::decode(&out[SELECTOR_LEN..]).ok() +}