From b032120ff1b848eebdfd0fc444183eab698a434f Mon Sep 17 00:00:00 2001 From: greged93 <82421016+greged93@users.noreply.github.com> Date: Thu, 14 Mar 2024 13:22:54 +0100 Subject: [PATCH] fix: estimate gas (#7133) Co-authored-by: Matthias Seitz --- crates/rpc/rpc/src/eth/api/call.rs | 118 ++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 34 deletions(-) diff --git a/crates/rpc/rpc/src/eth/api/call.rs b/crates/rpc/rpc/src/eth/api/call.rs index df4ed476a4..fe3006c827 100644 --- a/crates/rpc/rpc/src/eth/api/call.rs +++ b/crates/rpc/rpc/src/eth/api/call.rs @@ -35,7 +35,10 @@ use tracing::trace; // Gas per transaction not creating a contract. const MIN_TRANSACTION_GAS: u64 = 21_000u64; -const MIN_CREATE_GAS: u64 = 53_000u64; +/// Allowed error ratio for gas estimation +/// Taken from Geth's implementation in order to pass the hive tests +/// +const ESTIMATE_GAS_ERROR_RATIO: f64 = 0.015; impl EthApi where @@ -262,7 +265,7 @@ where } } - let (res, env) = ethres?; + let (mut res, mut env) = ethres?; match res.result { ExecutionResult::Success { .. } => { // succeeded @@ -285,14 +288,33 @@ where } // 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 + // 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 = - if env.tx.transact_to.is_create() { MIN_CREATE_GAS } else { MIN_TRANSACTION_GAS }; let mut highest_gas_limit: u64 = highest_gas_limit.try_into().unwrap_or(u64::MAX); + // the lowest value is capped by the gas used by the unconstrained transaction + let mut lowest_gas_limit = gas_used.saturating_sub(1); + + let gas_refund = match res.result { + ExecutionResult::Success { gas_refunded, .. } => gas_refunded, + _ => 0, + }; + // As stated in Geth, there is a good change that the transaction will pass if we set the + // gas limit to the execution gas used plus the gas refund, so we check this first + // 1 { - let mut env = env.clone(); + // An estimation error is allowed once the current gas limit range used in the binary + // search is small enough (less than 1.5% of the highest gas limit) + // { - // cap the highest gas limit with succeeding gas limit - highest_gas_limit = mid_gas_limit; - } - ExecutionResult::Revert { .. } => { - // increase the lowest gas limit - lowest_gas_limit = mid_gas_limit; - } - ExecutionResult::Halt { reason, .. } => { - match reason { - HaltReason::OutOfGas(_) | HaltReason::InvalidFEOpcode => { - // either out of gas or invalid opcode can be thrown dynamically if - // gasLeft is too low, so we treat this as `out of gas`, we know this - // call succeeds with a higher gaslimit. common usage of invalid opcode in openzeppelin + (res, env) = ethres?; + update_estimated_gas_range( + res.result, + mid_gas_limit, + &mut highest_gas_limit, + &mut lowest_gas_limit, + )?; - // 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(RpcInvalidTransactionError::EvmHalt(err).into()) - } - } - } - } // new midpoint mid_gas_limit = ((highest_gas_limit as u128 + lowest_gas_limit as u128) / 2) as u64; } @@ -464,3 +475,42 @@ where ExecutionResult::Halt { reason, .. } => RpcInvalidTransactionError::EvmHalt(reason).into(), } } + +/// Updates the highest and lowest gas limits for binary search +/// based on the result of the execution +#[inline] +fn update_estimated_gas_range( + result: ExecutionResult, + tx_gas_limit: u64, + highest_gas_limit: &mut u64, + lowest_gas_limit: &mut u64, +) -> EthResult<()> { + match result { + ExecutionResult::Success { .. } => { + // cap the highest gas limit with succeeding gas limit + *highest_gas_limit = tx_gas_limit; + } + ExecutionResult::Revert { .. } => { + // increase the lowest gas limit + *lowest_gas_limit = tx_gas_limit; + } + ExecutionResult::Halt { reason, .. } => { + match reason { + HaltReason::OutOfGas(_) | HaltReason::InvalidFEOpcode => { + // either out of gas or invalid opcode can be thrown dynamically if + // gasLeft is too low, so we treat this as `out of gas`, we know this + // call succeeds with a higher gaslimit. common usage of invalid opcode in openzeppelin + + // increase the lowest gas limit + *lowest_gas_limit = tx_gas_limit; + } + err => { + // these should be unreachable because we know the transaction succeeds, + // but we consider these cases an error + return Err(RpcInvalidTransactionError::EvmHalt(err).into()) + } + } + } + }; + Ok(()) +}