reth_rpc_eth_api/helpers/
estimate.rs

1//! Estimate gas needed implementation
2
3use super::{Call, LoadPendingBlock};
4use crate::{AsEthApiError, FromEthApiError, IntoEthApiError};
5use alloy_primitives::U256;
6use alloy_rpc_types_eth::{state::StateOverride, transaction::TransactionRequest, BlockId};
7use futures::Future;
8use reth_chainspec::MIN_TRANSACTION_GAS;
9use reth_provider::StateProvider;
10use reth_revm::{
11    database::StateProviderDatabase,
12    db::CacheDB,
13    primitives::{BlockEnv, CfgEnvWithHandlerCfg, ExecutionResult, HaltReason, TransactTo},
14};
15use reth_rpc_eth_types::{
16    revm_utils::{apply_state_overrides, caller_gas_allowance},
17    EthApiError, RevertError, RpcInvalidTransactionError,
18};
19use reth_rpc_server_types::constants::gas_oracle::{CALL_STIPEND_GAS, ESTIMATE_GAS_ERROR_RATIO};
20use revm_primitives::{db::Database, EnvWithHandlerCfg};
21use tracing::trace;
22
23/// Gas execution estimates
24pub trait EstimateCall: Call {
25    /// Estimates the gas usage of the `request` with the state.
26    ///
27    /// This will execute the [`TransactionRequest`] and find the best gas limit via binary search.
28    ///
29    /// ## EVM settings
30    ///
31    /// 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>:
32    ///
33    ///  - `disable_eip3607` is set to `true`
34    ///  - `disable_base_fee` is set to `true`
35    ///  - `nonce` is set to `None`
36    fn estimate_gas_with<S>(
37        &self,
38        mut cfg: CfgEnvWithHandlerCfg,
39        block: BlockEnv,
40        mut request: TransactionRequest,
41        state: S,
42        state_override: Option<StateOverride>,
43    ) -> Result<U256, Self::Error>
44    where
45        S: StateProvider,
46    {
47        // Disabled because eth_estimateGas is sometimes used with eoa senders
48        // See <https://github.com/paradigmxyz/reth/issues/1959>
49        cfg.disable_eip3607 = true;
50
51        // The basefee should be ignored for eth_estimateGas and similar
52        // See:
53        // <https://github.com/ethereum/go-ethereum/blob/ee8e83fa5f6cb261dad2ed0a7bbcde4930c41e6c/internal/ethapi/api.go#L985>
54        cfg.disable_base_fee = true;
55
56        // set nonce to None so that the correct nonce is chosen by the EVM
57        request.nonce = None;
58
59        // Keep a copy of gas related request values
60        let tx_request_gas_limit = request.gas.map(U256::from);
61        let tx_request_gas_price = request.gas_price;
62        // the gas limit of the corresponding block
63        let block_env_gas_limit = block.gas_limit;
64
65        // Determine the highest possible gas limit, considering both the request's specified limit
66        // and the block's limit.
67        let mut highest_gas_limit = tx_request_gas_limit
68            .map(|mut tx_gas_limit| {
69                if block_env_gas_limit < tx_gas_limit {
70                    // requested gas limit is higher than the allowed gas limit, capping
71                    tx_gas_limit = block_env_gas_limit;
72                }
73                tx_gas_limit
74            })
75            .unwrap_or(block_env_gas_limit);
76
77        // Configure the evm env
78        let mut env = self.build_call_evm_env(cfg, block, request)?;
79        let mut db = CacheDB::new(StateProviderDatabase::new(state));
80
81        // Apply any state overrides if specified.
82        if let Some(state_override) = state_override {
83            apply_state_overrides(state_override, &mut db).map_err(Self::Error::from_eth_err)?;
84        }
85
86        // Optimize for simple transfer transactions, potentially reducing the gas estimate.
87        if env.tx.data.is_empty() {
88            if let TransactTo::Call(to) = env.tx.transact_to {
89                if let Ok(code) = db.db.account_code(to) {
90                    let no_code_callee = code.map(|code| code.is_empty()).unwrap_or(true);
91                    if no_code_callee {
92                        // If the tx is a simple transfer (call to an account with no code) we can
93                        // shortcircuit. But simply returning
94                        // `MIN_TRANSACTION_GAS` is dangerous because there might be additional
95                        // field combos that bump the price up, so we try executing the function
96                        // with the minimum gas limit to make sure.
97                        let mut env = env.clone();
98                        env.tx.gas_limit = MIN_TRANSACTION_GAS;
99                        if let Ok((res, _)) = self.transact(&mut db, env) {
100                            if res.result.is_success() {
101                                return Ok(U256::from(MIN_TRANSACTION_GAS))
102                            }
103                        }
104                    }
105                }
106            }
107        }
108
109        // Check funds of the sender (only useful to check if transaction gas price is more than 0).
110        //
111        // The caller allowance is check by doing `(account.balance - tx.value) / tx.gas_price`
112        if env.tx.gas_price > U256::ZERO {
113            // cap the highest gas limit by max gas caller can afford with given gas price
114            highest_gas_limit = highest_gas_limit
115                .min(caller_gas_allowance(&mut db, &env.tx).map_err(Self::Error::from_eth_err)?);
116        }
117
118        // We can now normalize the highest gas limit to a u64
119        let mut highest_gas_limit = highest_gas_limit.saturating_to::<u64>();
120
121        // If the provided gas limit is less than computed cap, use that
122        env.tx.gas_limit = env.tx.gas_limit.min(highest_gas_limit);
123
124        // Execute the transaction with the highest possible gas limit.
125        let (mut res, mut env) = match self.transact(&mut db, env.clone()) {
126            // Handle the exceptional case where the transaction initialization uses too much gas.
127            // If the gas price or gas limit was specified in the request, retry the transaction
128            // with the block's gas limit to determine if the failure was due to
129            // insufficient gas.
130            Err(err)
131                if err.is_gas_too_high() &&
132                    (tx_request_gas_limit.is_some() || tx_request_gas_price.is_some()) =>
133            {
134                return Err(self.map_out_of_gas_err(block_env_gas_limit, env, &mut db))
135            }
136            // Propagate other results (successful or other errors).
137            ethres => ethres?,
138        };
139
140        let gas_refund = match res.result {
141            ExecutionResult::Success { gas_refunded, .. } => gas_refunded,
142            ExecutionResult::Halt { reason, gas_used } => {
143                // here we don't check for invalid opcode because already executed with highest gas
144                // limit
145                return Err(RpcInvalidTransactionError::halt(reason, gas_used).into_eth_err())
146            }
147            ExecutionResult::Revert { output, .. } => {
148                // if price or limit was included in the request then we can execute the request
149                // again with the block's gas limit to check if revert is gas related or not
150                return if tx_request_gas_limit.is_some() || tx_request_gas_price.is_some() {
151                    Err(self.map_out_of_gas_err(block_env_gas_limit, env, &mut db))
152                } else {
153                    // the transaction did revert
154                    Err(RpcInvalidTransactionError::Revert(RevertError::new(output)).into_eth_err())
155                }
156            }
157        };
158
159        // At this point we know the call succeeded but want to find the _best_ (lowest) gas the
160        // transaction succeeds with. We find this by doing a binary search over the possible range.
161
162        // we know the tx succeeded with the configured gas limit, so we can use that as the
163        // highest, in case we applied a gas cap due to caller allowance above
164        highest_gas_limit = env.tx.gas_limit;
165
166        // NOTE: this is the gas the transaction used, which is less than the
167        // transaction requires to succeed.
168        let mut gas_used = res.result.gas_used();
169        // the lowest value is capped by the gas used by the unconstrained transaction
170        let mut lowest_gas_limit = gas_used.saturating_sub(1);
171
172        // As stated in Geth, there is a good chance that the transaction will pass if we set the
173        // gas limit to the execution gas used plus the gas refund, so we check this first
174        // <https://github.com/ethereum/go-ethereum/blob/a5a4fa7032bb248f5a7c40f4e8df2b131c4186a4/eth/gasestimator/gasestimator.go#L135
175        //
176        // Calculate the optimistic gas limit by adding gas used and gas refund,
177        // then applying a 64/63 multiplier to account for gas forwarding rules.
178        let optimistic_gas_limit = (gas_used + gas_refund + CALL_STIPEND_GAS) * 64 / 63;
179        if optimistic_gas_limit < highest_gas_limit {
180            // Set the transaction's gas limit to the calculated optimistic gas limit.
181            env.tx.gas_limit = optimistic_gas_limit;
182            // Re-execute the transaction with the new gas limit and update the result and
183            // environment.
184            (res, env) = self.transact(&mut db, env)?;
185            // Update the gas used based on the new result.
186            gas_used = res.result.gas_used();
187            // Update the gas limit estimates (highest and lowest) based on the execution result.
188            update_estimated_gas_range(
189                res.result,
190                optimistic_gas_limit,
191                &mut highest_gas_limit,
192                &mut lowest_gas_limit,
193            )?;
194        };
195
196        // Pick a point that's close to the estimated gas
197        let mut mid_gas_limit = std::cmp::min(
198            gas_used * 3,
199            ((highest_gas_limit as u128 + lowest_gas_limit as u128) / 2) as u64,
200        );
201
202        trace!(target: "rpc::eth::estimate", ?env, ?highest_gas_limit, ?lowest_gas_limit, ?mid_gas_limit, "Starting binary search for gas");
203
204        // Binary search narrows the range to find the minimum gas limit needed for the transaction
205        // to succeed.
206        while (highest_gas_limit - lowest_gas_limit) > 1 {
207            // An estimation error is allowed once the current gas limit range used in the binary
208            // search is small enough (less than 1.5% of the highest gas limit)
209            // <https://github.com/ethereum/go-ethereum/blob/a5a4fa7032bb248f5a7c40f4e8df2b131c4186a4/eth/gasestimator/gasestimator.go#L152
210            if (highest_gas_limit - lowest_gas_limit) as f64 / (highest_gas_limit as f64) <
211                ESTIMATE_GAS_ERROR_RATIO
212            {
213                break
214            };
215
216            env.tx.gas_limit = mid_gas_limit;
217
218            // Execute transaction and handle potential gas errors, adjusting limits accordingly.
219            match self.transact(&mut db, env.clone()) {
220                Err(err) if err.is_gas_too_high() => {
221                    // Decrease the highest gas limit if gas is too high
222                    highest_gas_limit = mid_gas_limit;
223                }
224                Err(err) if err.is_gas_too_low() => {
225                    // Increase the lowest gas limit if gas is too low
226                    lowest_gas_limit = mid_gas_limit;
227                }
228                // Handle other cases, including successful transactions.
229                ethres => {
230                    // Unpack the result and environment if the transaction was successful.
231                    (res, env) = ethres?;
232                    // Update the estimated gas range based on the transaction result.
233                    update_estimated_gas_range(
234                        res.result,
235                        mid_gas_limit,
236                        &mut highest_gas_limit,
237                        &mut lowest_gas_limit,
238                    )?;
239                }
240            }
241
242            // New midpoint
243            mid_gas_limit = ((highest_gas_limit as u128 + lowest_gas_limit as u128) / 2) as u64;
244        }
245
246        Ok(U256::from(highest_gas_limit))
247    }
248
249    /// Estimate gas needed for execution of the `request` at the [`BlockId`].
250    fn estimate_gas_at(
251        &self,
252        request: TransactionRequest,
253        at: BlockId,
254        state_override: Option<StateOverride>,
255    ) -> impl Future<Output = Result<U256, Self::Error>> + Send
256    where
257        Self: LoadPendingBlock,
258    {
259        async move {
260            let (cfg, block_env, at) = self.evm_env_at(at).await?;
261
262            self.spawn_blocking_io(move |this| {
263                let state = this.state_at_block_id(at)?;
264                EstimateCall::estimate_gas_with(
265                    &this,
266                    cfg,
267                    block_env,
268                    request,
269                    state,
270                    state_override,
271                )
272            })
273            .await
274        }
275    }
276
277    /// Executes the requests again after an out of gas error to check if the error is gas related
278    /// or not
279    #[inline]
280    fn map_out_of_gas_err<DB>(
281        &self,
282        env_gas_limit: U256,
283        mut env: EnvWithHandlerCfg,
284        db: &mut DB,
285    ) -> Self::Error
286    where
287        DB: Database,
288        EthApiError: From<DB::Error>,
289    {
290        let req_gas_limit = env.tx.gas_limit;
291        env.tx.gas_limit = env_gas_limit.try_into().unwrap_or(u64::MAX);
292        let (res, _) = match self.transact(db, env) {
293            Ok(res) => res,
294            Err(err) => return err,
295        };
296        match res.result {
297            ExecutionResult::Success { .. } => {
298                // transaction succeeded by manually increasing the gas limit to
299                // highest, which means the caller lacks funds to pay for the tx
300                RpcInvalidTransactionError::BasicOutOfGas(req_gas_limit).into_eth_err()
301            }
302            ExecutionResult::Revert { output, .. } => {
303                // reverted again after bumping the limit
304                RpcInvalidTransactionError::Revert(RevertError::new(output)).into_eth_err()
305            }
306            ExecutionResult::Halt { reason, .. } => {
307                RpcInvalidTransactionError::EvmHalt(reason).into_eth_err()
308            }
309        }
310    }
311}
312
313/// Updates the highest and lowest gas limits for binary search based on the execution result.
314///
315/// This function refines the gas limit estimates used in a binary search to find the optimal
316/// gas limit for a transaction. It adjusts the highest or lowest gas limits depending on
317/// whether the execution succeeded, reverted, or halted due to specific reasons.
318#[inline]
319pub fn update_estimated_gas_range(
320    result: ExecutionResult,
321    tx_gas_limit: u64,
322    highest_gas_limit: &mut u64,
323    lowest_gas_limit: &mut u64,
324) -> Result<(), EthApiError> {
325    match result {
326        ExecutionResult::Success { .. } => {
327            // Cap the highest gas limit with the succeeding gas limit.
328            *highest_gas_limit = tx_gas_limit;
329        }
330        ExecutionResult::Revert { .. } => {
331            // Increase the lowest gas limit.
332            *lowest_gas_limit = tx_gas_limit;
333        }
334        ExecutionResult::Halt { reason, .. } => {
335            match reason {
336                HaltReason::OutOfGas(_) | HaltReason::InvalidFEOpcode => {
337                    // Both `OutOfGas` and `InvalidEFOpcode` can occur dynamically if the gas
338                    // left is too low. Treat this as an out of gas
339                    // condition, knowing that the call succeeds with a
340                    // higher gas limit.
341                    //
342                    // Common usage of invalid opcode in OpenZeppelin:
343                    // <https://github.com/OpenZeppelin/openzeppelin-contracts/blob/94697be8a3f0dfcd95dfb13ffbd39b5973f5c65d/contracts/metatx/ERC2771Forwarder.sol#L360-L367>
344
345                    // Increase the lowest gas limit.
346                    *lowest_gas_limit = tx_gas_limit;
347                }
348                err => {
349                    // These cases should be unreachable because we know the transaction
350                    // succeeds, but if they occur, treat them as an
351                    // error.
352                    return Err(RpcInvalidTransactionError::EvmHalt(err).into_eth_err())
353                }
354            }
355        }
356    };
357
358    Ok(())
359}