Skip to main content

reth_rpc_eth_api/helpers/
estimate.rs

1//! Estimate gas needed implementation
2
3use super::{call::request_gas_price, Call, LoadPendingBlock};
4use crate::{AsEthApiError, FromEthApiError, FromEvmError, IntoEthApiError};
5use alloy_network::TransactionBuilder;
6use alloy_primitives::{TxKind, U256};
7use alloy_rpc_types_eth::{state::EvmOverrides, BlockId};
8use evm2::{
9    evm::{CacheDB, Db, DynDatabase},
10    interpreter::InstrStop,
11    EvmFeatures, TxResult,
12};
13use futures::Future;
14use reth_chainspec::MIN_TRANSACTION_GAS;
15use reth_evm::{
16    database::StateProviderDatabase, ConfigureEvm, Evm, EvmEnv, EvmEnvFor, EvmTypesFor,
17    TransactionEnvMut,
18};
19use reth_rpc_convert::{RpcConvert, RpcTxReq};
20use reth_rpc_eth_types::{
21    cache::db::{apply_block_overrides, apply_state_overrides},
22    error::api::{FromEvmHalt, FromRevert},
23    EthApiError, RpcInvalidTransactionError,
24};
25use reth_rpc_server_types::constants::gas_oracle::{CALL_STIPEND_GAS, ESTIMATE_GAS_ERROR_RATIO};
26use reth_storage_api::{EvmStateProvider, StateProvider};
27use reth_tasks::cancel::is_cancelled;
28use tracing::trace;
29
30/// Gas execution estimates
31pub trait EstimateCall: Call {
32    /// Estimates the gas usage of the `request` with the state.
33    ///
34    /// This will execute the [`RpcTxReq`] and find the best gas limit via binary search.
35    ///
36    /// ## EVM settings
37    ///
38    /// This modifies certain EVM settings to mirror geth's `SkipAccountChecks` when transacting requests, see also: <https://github.com/ethereum/go-ethereum/blob/380688c636a654becc8f114438c2a5d93d2db032/core/state_transition.go#L145-L148>:
39    ///
40    ///  - `disable_eip3607` is set to `true`
41    ///  - `disable_base_fee` is set to `true`
42    ///  - `disable_fee_charge` is set to `true`
43    ///  - `nonce` is set to `None`
44    fn estimate_gas_with<S>(
45        &self,
46        mut evm_env: EvmEnvFor<Self::Evm>,
47        mut request: RpcTxReq<<Self::RpcConvert as RpcConvert>::Network>,
48        state: S,
49        overrides: EvmOverrides,
50    ) -> Result<U256, Self::Error>
51    where
52        S: EvmStateProvider,
53    {
54        // Disabled because eth_estimateGas is sometimes used with eoa senders
55        // See <https://github.com/paradigmxyz/reth/issues/1959>
56        evm_env.version_mut().features.remove(EvmFeatures::EIP3607);
57
58        // The basefee should be ignored for eth_estimateGas and similar
59        // See:
60        // <https://github.com/ethereum/go-ethereum/blob/ee8e83fa5f6cb261dad2ed0a7bbcde4930c41e6c/internal/ethapi/api.go#L985>
61        evm_env.version_mut().features.remove(EvmFeatures::BASE_FEE_CHECK);
62
63        // Disable additional fee charges (e.g. L2 operator fees) for gas estimation,
64        // consistent with `prepare_call_env` for `eth_call`.
65        evm_env.version_mut().features.remove(
66            EvmFeatures::FEE_CHARGE | EvmFeatures::BALANCE_CHECK | EvmFeatures::BALANCE_TOP_UP,
67        );
68
69        // set nonce to None so that the correct nonce is chosen by the EVM
70        request.as_mut().take_nonce();
71
72        // Keep a copy of gas related request values
73        let tx_request_gas_limit = request.as_ref().gas_limit();
74        let tx_request_gas_price = request.as_ref().gas_price();
75
76        // Configure the evm env
77        let mut db = CacheDB::new(Db::new(StateProviderDatabase::new(state)));
78
79        // Apply any block overrides before deriving block-derived limits and the tx env so
80        // overrides for `gasLimit`, `baseFee` and `blobBaseFee` are visible to estimation.
81        // Mirrors geth's behavior, see:
82        // <https://github.com/ethereum/go-ethereum/pull/30695>
83        if let Some(block_overrides) = overrides.block {
84            apply_block_overrides(*block_overrides, &mut db, &mut evm_env);
85        }
86
87        // Apply any state overrides if specified.
88        if let Some(state_override) = overrides.state {
89            apply_state_overrides(state_override, &mut db).map_err(Self::Error::from_eth_err)?;
90        }
91
92        // the gas limit of the corresponding block
93        let block_gas_limit = evm_env.block_env().gas_limit.to::<u64>();
94        // If EIP-8037 is enabled, the transaction gas limit cap is not applicable
95        let mut max_gas_limit = evm_env.version().tx_gas_caps().0.min(block_gas_limit);
96
97        // Also bound diagnostic retries by the RPC gas cap. Zero means unlimited.
98        let gas_cap = self.call_gas_limit();
99        if gas_cap != 0 {
100            max_gas_limit = max_gas_limit.min(gas_cap);
101        }
102
103        let mut highest_gas_limit =
104            tx_request_gas_limit.unwrap_or(max_gas_limit).min(max_gas_limit);
105
106        // Check if this is a basic transfer (no input data to account with no code)
107        let is_basic_transfer = if request.as_ref().input().is_none_or(|input| input.is_empty()) &&
108            let Some(TxKind::Call(to)) = request.as_ref().kind()
109        {
110            // Fetch the account through the DB so the state overrides applied above are visible.
111            match db.get_account(&to) {
112                Ok(Some(account)) => account.is_empty_code_hash(),
113                Ok(None) => true,
114                Err(_) => false,
115            }
116        } else {
117            false
118        };
119
120        let request_gas_price = request_gas_price(request.as_ref(), evm_env.block_base_fee());
121        let mut tx_env = self.create_txn_env(&evm_env, request, &mut db)?;
122
123        // Check funds of the sender (only useful to check if transaction gas price is more than 0).
124        //
125        // The caller allowance reserves the value and maximum blob fee before funding execution
126        // gas at the transaction's gas price.
127        if request_gas_price > 0 {
128            let allowance = self.caller_gas_allowance(&mut db, &evm_env, &tx_env)?;
129            highest_gas_limit = highest_gas_limit.min(allowance);
130        }
131
132        trace!(target: "rpc::eth::estimate", ?tx_env, gas_limit = highest_gas_limit, is_basic_transfer, "Starting gas estimation");
133
134        let mut evm = self.evm_config().evm_with_env(&mut db, evm_env);
135        let mut execute = move |gas_limit| {
136            tx_env.inner_mut().set_gas_limit(gas_limit);
137            evm.transact_result(&tx_env).map_err(Self::Error::from_evm_err)
138        };
139
140        // For basic transfers, try using minimum gas before running full binary search
141        if is_basic_transfer &&
142            let Ok(res) = execute(MIN_TRANSACTION_GAS.min(max_gas_limit)) &&
143            res.status
144        {
145            return Ok(U256::from(res.tx_gas_used()))
146        }
147
148        // Execute the transaction with the highest possible gas limit.
149        let mut res = match execute(highest_gas_limit) {
150            // Handle the exceptional case where the transaction initialization uses too much
151            // gas. If the gas price or gas limit was specified in the request,
152            // retry the transaction with the block's gas limit to determine if
153            // the failure was due to insufficient gas.
154            Err(err)
155                if err.is_gas_too_high() &&
156                    (tx_request_gas_limit.is_some() || tx_request_gas_price.is_some()) =>
157            {
158                let retry = execute(max_gas_limit)?;
159                return map_out_of_gas_result::<_, EvmTypesFor<Self::Evm>>(retry, highest_gas_limit)
160            }
161            Err(err) if err.is_gas_too_low() => {
162                // This failed because the configured gas cost of the tx was lower than what
163                // actually consumed by the tx This can happen if the
164                // request provided fee values manually and the resulting gas cost exceeds the
165                // sender's allowance, so we return the appropriate error here
166                return Err(RpcInvalidTransactionError::GasRequiredExceedsAllowance {
167                    gas_limit: highest_gas_limit,
168                }
169                .into_eth_err());
170            }
171            // Propagate other results (successful or other errors).
172            ethres => ethres?,
173        };
174
175        let gas_refund = if res.status {
176            res.final_refunded()
177        } else if res.stop == InstrStop::Revert {
178            // if price or limit was included in the request then we can execute the request
179            // again with the block's gas limit to check if revert is gas related or not
180            return if tx_request_gas_limit.is_some() || tx_request_gas_price.is_some() {
181                let retry = execute(max_gas_limit)?;
182                map_out_of_gas_result::<_, EvmTypesFor<Self::Evm>>(retry, highest_gas_limit)
183            } else {
184                // the transaction did revert
185                Err(Self::Error::from_revert(res.output))
186            }
187        } else {
188            // here we don't check for invalid opcode because already executed with highest gas
189            // limit
190            return Err(Self::Error::from_evm_halt(res.stop, highest_gas_limit))
191        };
192
193        // At this point we know the call succeeded but want to find the _best_ (lowest) gas the
194        // transaction succeeds with. We find this by doing a binary search over the possible range.
195
196        // NOTE: this is the gas the transaction used, which is less than the
197        // transaction requires to succeed.
198        let mut gas_used = res.tx_gas_used();
199        // the lowest value is capped by the gas used by the unconstrained transaction
200        let mut lowest_gas_limit = gas_used.saturating_sub(1);
201
202        // As stated in Geth, there is a good chance that the transaction will pass if we set the
203        // gas limit to the execution gas used plus the gas refund, so we check this first
204        // <https://github.com/ethereum/go-ethereum/blob/a5a4fa7032bb248f5a7c40f4e8df2b131c4186a4/eth/gasestimator/gasestimator.go#L135
205        //
206        // Calculate the optimistic gas limit by adding gas used and gas refund,
207        // then applying a 64/63 multiplier to account for gas forwarding rules.
208        let optimistic_gas_limit = (gas_used + gas_refund + CALL_STIPEND_GAS) * 64 / 63;
209        if optimistic_gas_limit < highest_gas_limit {
210            // Re-execute the transaction with the new gas limit and update the result and
211            // environment.
212            res = execute(optimistic_gas_limit)?;
213
214            // Update the gas used based on the new result.
215            gas_used = res.tx_gas_used();
216            // Update the gas limit estimates (highest and lowest) based on the execution result.
217            update_estimated_gas_range::<EvmTypesFor<Self::Evm>>(
218                res,
219                optimistic_gas_limit,
220                &mut highest_gas_limit,
221                &mut lowest_gas_limit,
222            )?;
223        };
224
225        // Pick a point that's close to the estimated gas
226        let mut mid_gas_limit = std::cmp::min(
227            gas_used * 3,
228            ((highest_gas_limit as u128 + lowest_gas_limit as u128) / 2) as u64,
229        );
230
231        trace!(target: "rpc::eth::estimate", ?highest_gas_limit, ?lowest_gas_limit, ?mid_gas_limit, "Starting binary search for gas");
232
233        // Binary search narrows the range to find the minimum gas limit needed for the transaction
234        // to succeed.
235        while lowest_gas_limit + 1 < highest_gas_limit {
236            // An estimation error is allowed once the current gas limit range used in the binary
237            // search is small enough (less than 1.5% of the highest gas limit)
238            // <https://github.com/ethereum/go-ethereum/blob/a5a4fa7032bb248f5a7c40f4e8df2b131c4186a4/eth/gasestimator/gasestimator.go#L152
239            let ratio = (highest_gas_limit - lowest_gas_limit) as f64 / (highest_gas_limit as f64);
240            if ratio < ESTIMATE_GAS_ERROR_RATIO {
241                break
242            };
243
244            if is_cancelled() {
245                return Err(EthApiError::InternalEthError.into())
246            }
247            // Execute transaction and handle potential gas errors, adjusting limits accordingly.
248            match execute(mid_gas_limit) {
249                Err(err) if err.is_gas_too_high() => {
250                    // Decrease the highest gas limit if gas is too high
251                    highest_gas_limit = mid_gas_limit;
252                }
253                Err(err) if err.is_gas_too_low() => {
254                    // Increase the lowest gas limit if gas is too low
255                    lowest_gas_limit = mid_gas_limit;
256                }
257                // Handle other cases, including successful transactions.
258                ethres => {
259                    // Update the estimated gas range based on the execution result.
260                    update_estimated_gas_range::<EvmTypesFor<Self::Evm>>(
261                        ethres?,
262                        mid_gas_limit,
263                        &mut highest_gas_limit,
264                        &mut lowest_gas_limit,
265                    )?;
266                }
267            }
268
269            // New midpoint
270            mid_gas_limit = ((highest_gas_limit as u128 + lowest_gas_limit as u128) / 2) as u64;
271        }
272
273        Ok(U256::from(highest_gas_limit))
274    }
275
276    /// Estimate gas needed for execution of the `request` at the [`BlockId`].
277    fn estimate_gas_at(
278        &self,
279        request: RpcTxReq<<Self::RpcConvert as RpcConvert>::Network>,
280        at: BlockId,
281        overrides: EvmOverrides,
282    ) -> impl Future<Output = Result<U256, Self::Error>> + Send
283    where
284        Self: LoadPendingBlock,
285    {
286        async move {
287            let (evm_env, at) = self.evm_env_at(at).await?;
288
289            self.spawn_blocking_io_with_state(at, move |this, state| {
290                EstimateCall::estimate_gas_with(
291                    &this,
292                    evm_env,
293                    request,
294                    state.into_evm_state_provider(),
295                    overrides,
296                )
297            })
298            .await
299        }
300    }
301}
302
303fn map_out_of_gas_result<Error, EvmTypes>(
304    result: TxResult<EvmTypes>,
305    requested_gas_limit: u64,
306) -> Result<U256, Error>
307where
308    Error: FromEvmHalt<evm2::interpreter::InstrStop> + FromRevert + FromEthApiError,
309    EvmTypes: evm2::EvmTypes,
310{
311    if result.status {
312        // Transaction succeeded by manually increasing the gas limit,
313        // which means the caller lacks funds to pay for the tx
314        Err(RpcInvalidTransactionError::BasicOutOfGas(requested_gas_limit).into_eth_err())
315    } else if result.stop == InstrStop::Revert {
316        // reverted again after bumping the limit
317        Err(Error::from_revert(result.output))
318    } else {
319        Err(Error::from_evm_halt(result.stop, requested_gas_limit))
320    }
321}
322
323/// Updates the highest and lowest gas limits for binary search based on the execution result.
324///
325/// This function refines the gas limit estimates used in a binary search to find the optimal
326/// gas limit for a transaction. It adjusts the highest or lowest gas limits depending on
327/// whether the execution succeeded, reverted, or halted due to specific reasons.
328#[inline]
329pub fn update_estimated_gas_range<EvmTypes: evm2::EvmTypes>(
330    result: TxResult<EvmTypes>,
331    tx_gas_limit: u64,
332    highest_gas_limit: &mut u64,
333    lowest_gas_limit: &mut u64,
334) -> Result<(), EthApiError> {
335    if result.status {
336        // Cap the highest gas limit with the succeeding gas limit.
337        *highest_gas_limit = tx_gas_limit;
338    } else {
339        // We know that transaction succeeded with a higher gas limit before, so any failure
340        // means that we need to increase it.
341        //
342        // We are ignoring all halts here, and not just OOG errors because there are cases when
343        // non-OOG halt might flag insufficient gas limit as well.
344        //
345        // Common usage of invalid opcode in OpenZeppelin:
346        // <https://github.com/OpenZeppelin/openzeppelin-contracts/blob/94697be8a3f0dfcd95dfb13ffbd39b5973f5c65d/contracts/metatx/ERC2771Forwarder.sol#L360-L367>
347        *lowest_gas_limit = tx_gas_limit;
348    }
349
350    Ok(())
351}