mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
feat(rpc): impl eth_estimate (#1599)
This commit is contained in:
@ -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",
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user