Skip to main content

reth_rpc_eth_api/helpers/
estimate.rs

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