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