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