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