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