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