feat(rpc): impl eth_estimate (#1599)

This commit is contained in:
Matthias Seitz
2023-03-02 17:07:36 +01:00
committed by GitHub
parent e9c2e884a4
commit f89b504245
6 changed files with 262 additions and 16 deletions

View File

@ -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",

View File

@ -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<Client, Pool, Network> EthApi<Client, Pool, Network>
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<U256> {
// 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<U256> {
// 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<S>(
&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))
}
}

View File

@ -76,6 +76,11 @@ impl<Client, Pool, Network> EthApi<Client, Pool, Network> {
}
}
/// 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

View File

@ -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<T> = Result<T, EthApiError>;
@ -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<InvalidTransactionError> 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<revm::primitives::InvalidTransaction> 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<bytes::Bytes>,
}
// === 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<PoolError> 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<String> {
use ethers_core::abi::AbiDecode;
let out = out.as_ref();
if out.len() < SELECTOR_LEN {
return None
}
String::decode(&out[SELECTOR_LEN..]).ok()
}