stratus/eth/executor/
evm.rs

1use std::cmp::min;
2use std::collections::BTreeMap;
3use std::sync::Arc;
4
5use alloy_consensus::transaction::TransactionInfo;
6use alloy_rpc_types_trace::geth::CallFrame;
7use alloy_rpc_types_trace::geth::FourByteFrame;
8use alloy_rpc_types_trace::geth::GethDebugBuiltInTracerType;
9use alloy_rpc_types_trace::geth::GethDebugTracerType;
10use alloy_rpc_types_trace::geth::GethTrace;
11use alloy_rpc_types_trace::geth::NoopFrame;
12use alloy_rpc_types_trace::geth::call::FlatCallFrame;
13use alloy_rpc_types_trace::geth::mux::MuxFrame;
14use anyhow::anyhow;
15use itertools::Itertools;
16use log::log_enabled;
17use revm::Context;
18use revm::Database;
19use revm::DatabaseRef;
20use revm::ExecuteCommitEvm;
21use revm::ExecuteEvm;
22use revm::InspectEvm;
23use revm::Journal;
24use revm::context::BlockEnv;
25use revm::context::CfgEnv;
26use revm::context::Evm as RevmEvm;
27use revm::context::TransactTo;
28use revm::context::TxEnv;
29use revm::context::result::EVMError;
30use revm::context::result::ExecutionResult as RevmExecutionResult;
31use revm::context::result::InvalidTransaction;
32use revm::context::result::ResultAndState;
33use revm::database::CacheDB;
34use revm::handler::EthFrame;
35use revm::handler::EthPrecompiles;
36use revm::handler::instructions::EthInstructions;
37use revm::interpreter::interpreter::EthInterpreter;
38use revm::primitives::B256;
39use revm::primitives::U256;
40use revm::primitives::hardfork::SpecId;
41use revm::state::AccountInfo;
42use revm::state::EvmState;
43use revm_inspectors::tracing::FourByteInspector;
44use revm_inspectors::tracing::MuxInspector;
45use revm_inspectors::tracing::TracingInspector;
46use revm_inspectors::tracing::TracingInspectorConfig;
47
48use super::evm_input::InspectorInput;
49use crate::alias::RevmAddress;
50use crate::alias::RevmBytecode;
51use crate::eth::codegen;
52use crate::eth::executor::EvmExecutionResult;
53use crate::eth::executor::EvmInput;
54use crate::eth::executor::ExecutorConfig;
55use crate::eth::primitives::Account;
56use crate::eth::primitives::Address;
57use crate::eth::primitives::BlockFilter;
58use crate::eth::primitives::Bytes;
59use crate::eth::primitives::EvmExecution;
60use crate::eth::primitives::EvmExecutionMetrics;
61use crate::eth::primitives::ExecutionAccountChanges;
62use crate::eth::primitives::ExecutionChanges;
63use crate::eth::primitives::ExecutionResult;
64use crate::eth::primitives::ExecutionValueChange;
65use crate::eth::primitives::Gas;
66use crate::eth::primitives::Log;
67use crate::eth::primitives::PointInTime;
68use crate::eth::primitives::Slot;
69use crate::eth::primitives::SlotIndex;
70use crate::eth::primitives::StorageError;
71use crate::eth::primitives::StratusError;
72use crate::eth::primitives::TransactionError;
73use crate::eth::primitives::TransactionStage;
74use crate::eth::primitives::UnexpectedError;
75use crate::eth::storage::StratusStorage;
76use crate::ext::OptionExt;
77use crate::ext::not;
78#[cfg(feature = "metrics")]
79use crate::infra::metrics;
80
81/// Maximum gas limit allowed for a transaction. Prevents a transaction from consuming too many resources.
82const GAS_MAX_LIMIT: u64 = 1_000_000_000;
83type ContextWithDB = Context<BlockEnv, TxEnv, CfgEnv, RevmSession, Journal<RevmSession>>;
84type GeneralRevm<DB> =
85    RevmEvm<Context<BlockEnv, TxEnv, CfgEnv, DB>, (), EthInstructions<EthInterpreter<()>, Context<BlockEnv, TxEnv, CfgEnv, DB>>, EthPrecompiles, EthFrame>;
86
87/// Implementation of EVM using [`revm`](https://crates.io/crates/revm).
88pub struct Evm {
89    evm: RevmEvm<ContextWithDB, (), EthInstructions<EthInterpreter, ContextWithDB>, EthPrecompiles, EthFrame>,
90    kind: EvmKind,
91}
92
93#[derive(Clone, Copy)]
94pub enum EvmKind {
95    Transaction,
96    Call,
97}
98
99impl Evm {
100    /// Creates a new instance of the Evm.
101    pub fn new(storage: Arc<StratusStorage>, config: ExecutorConfig, kind: EvmKind) -> Self {
102        tracing::info!(?config, "creating revm");
103
104        // configure revm
105        let chain_id = config.executor_chain_id;
106
107        Self {
108            evm: Self::create_evm(chain_id, config.executor_evm_spec, RevmSession::new(storage, config.clone()), kind),
109            kind,
110        }
111    }
112
113    /// Execute a transaction that deploys a contract or call a contract function.
114    pub fn execute(&mut self, input: EvmInput) -> Result<EvmExecutionResult, StratusError> {
115        #[cfg(feature = "metrics")]
116        let start = metrics::now();
117
118        // configure session
119        self.evm.journaled_state.database.reset(input.clone());
120
121        self.evm.fill_env(input);
122
123        if log_enabled!(log::Level::Debug) {
124            let block_env_log = self.evm.block.clone();
125            let tx_env_log = self.evm.tx.clone();
126            // execute transaction
127            tracing::debug!(block_env = ?block_env_log, tx_env = ?tx_env_log, "executing transaction in revm");
128        }
129
130        let tx = std::mem::take(&mut self.evm.tx);
131        let evm_result = self.evm.transact(tx);
132
133        // extract results
134        let session = &mut self.evm.journaled_state.database;
135        let session_input = std::mem::take(&mut session.input);
136        let session_storage_changes = std::mem::take(&mut session.storage_changes);
137        let session_metrics = std::mem::take(&mut session.metrics);
138        #[cfg(feature = "metrics")]
139        let session_point_in_time = session.input.point_in_time;
140
141        // parse result
142        let execution = match evm_result {
143            // executed
144            Ok(result) => Ok(parse_revm_execution(result, session_input, session_storage_changes)?),
145
146            // nonce errors
147            Err(EVMError::Transaction(InvalidTransaction::NonceTooHigh { tx, state })) => Err(TransactionError::Nonce {
148                transaction: tx.into(),
149                account: state.into(),
150            }
151            .into()),
152            Err(EVMError::Transaction(InvalidTransaction::NonceTooLow { tx, state })) => Err(TransactionError::Nonce {
153                transaction: tx.into(),
154                account: state.into(),
155            }
156            .into()),
157
158            // storage error
159            Err(EVMError::Database(e)) => {
160                tracing::warn!(reason = ?e, "evm storage error");
161                Err(e)
162            }
163
164            // unexpected errors
165            Err(e) => {
166                tracing::warn!(reason = ?e, "evm transaction error");
167                Err(TransactionError::EvmFailed(e.to_string()).into())
168            }
169        };
170
171        // track metrics
172        #[cfg(feature = "metrics")]
173        {
174            metrics::inc_evm_execution(start.elapsed(), session_point_in_time, execution.is_ok());
175            metrics::inc_evm_execution_account_reads(session_metrics.account_reads);
176        }
177
178        execution.map(|execution| EvmExecutionResult {
179            execution,
180            metrics: session_metrics,
181        })
182    }
183
184    fn create_evm<DB: Database>(chain_id: u64, spec: SpecId, db: DB, kind: EvmKind) -> GeneralRevm<DB> {
185        let ctx = Context::new(db, spec)
186            .modify_cfg_chained(|cfg_env| {
187                cfg_env.chain_id = chain_id;
188                cfg_env.disable_nonce_check = matches!(kind, EvmKind::Call);
189                cfg_env.disable_eip3607 = matches!(kind, EvmKind::Call);
190                cfg_env.limit_contract_code_size = Some(usize::MAX);
191            })
192            .modify_block_chained(|block_env: &mut BlockEnv| {
193                block_env.beneficiary = Address::COINBASE.into();
194            })
195            .modify_tx_chained(|tx_env: &mut TxEnv| {
196                tx_env.gas_priority_fee = None;
197            });
198
199        RevmEvm::new(ctx, EthInstructions::new_mainnet(), EthPrecompiles::default())
200    }
201
202    /// Execute a transaction using a tracer.
203    pub fn inspect(&mut self, input: InspectorInput) -> Result<GethTrace, StratusError> {
204        let InspectorInput {
205            tx_hash,
206            opts,
207            trace_unsuccessful_only,
208        } = input;
209        let tracer_type = opts.tracer.ok_or_else(|| anyhow!("no tracer type provided"))?;
210
211        if matches!(tracer_type, GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::NoopTracer)) {
212            return Ok(NoopFrame::default().into());
213        }
214
215        let tx = self
216            .evm
217            .journaled_state
218            .database
219            .storage
220            .read_transaction(tx_hash)?
221            .ok_or_else(|| anyhow!("transaction not found: {}", tx_hash))?;
222
223        if trace_unsuccessful_only && matches!(tx.result(), ExecutionResult::Success) {
224            return Ok(default_trace(tracer_type, tx));
225        }
226
227        let block = self
228            .evm
229            .journaled_state
230            .database
231            .storage
232            .read_block(BlockFilter::Number(tx.block_number()))?
233            .ok_or_else(|| {
234                StratusError::Storage(StorageError::BlockNotFound {
235                    filter: BlockFilter::Number(tx.block_number()),
236                })
237            })?;
238
239        let tx_info = TransactionInfo {
240            block_hash: Some(block.hash().0.0.into()),
241            hash: Some(tx_hash.0.0.into()),
242            index: tx.index().map_into(),
243            block_number: Some(block.number().as_u64()),
244            base_fee: None,
245        };
246        let inspect_input: EvmInput = tx.try_into()?;
247        self.evm.journaled_state.database.reset(EvmInput {
248            point_in_time: PointInTime::MinedPast(inspect_input.block_number.prev().unwrap_or_default()),
249            ..Default::default()
250        });
251
252        let spec = self.evm.cfg.spec;
253
254        let mut cache_db = CacheDB::new(&self.evm.journaled_state.database);
255        let mut evm = Self::create_evm(inspect_input.chain_id.unwrap_or_default().into(), spec, &mut cache_db, self.kind);
256
257        // Execute all transactions before target tx_hash
258        for tx in block.transactions.into_iter().sorted_by_key(|item| item.transaction_index) {
259            if tx.input.hash == tx_hash {
260                break;
261            }
262            let tx_input = tx.into();
263
264            // Configure EVM state
265            evm.fill_env(tx_input);
266            let tx = std::mem::take(&mut evm.tx);
267            evm.transact_commit(tx)?;
268        }
269
270        let trace_result: GethTrace = match tracer_type {
271            GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::FourByteTracer) => {
272                let mut inspector = FourByteInspector::default();
273                let mut evm_with_inspector = evm.with_inspector(&mut inspector);
274                evm_with_inspector.fill_env(inspect_input);
275                let tx = std::mem::take(&mut evm_with_inspector.tx);
276                evm_with_inspector.inspect_tx(tx)?;
277                FourByteFrame::from(&inspector).into()
278            }
279            GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::CallTracer) => {
280                let call_config = opts.tracer_config.into_call_config()?;
281                let mut inspector = TracingInspector::new(TracingInspectorConfig::from_geth_call_config(&call_config));
282                let mut evm_with_inspector = evm.with_inspector(&mut inspector);
283                evm_with_inspector.fill_env(inspect_input);
284                let tx = std::mem::take(&mut evm_with_inspector.tx);
285                let res = evm_with_inspector.inspect_tx(tx)?;
286                let mut trace = inspector.geth_builder().geth_call_traces(call_config, res.result.gas_used()).into();
287                enhance_trace_with_decoded_errors(&mut trace);
288                trace
289            }
290            GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::PreStateTracer) => {
291                let prestate_config = opts.tracer_config.into_pre_state_config()?;
292                let mut inspector = TracingInspector::new(TracingInspectorConfig::from_geth_prestate_config(&prestate_config));
293                let mut evm_with_inspector = evm.with_inspector(&mut inspector);
294                evm_with_inspector.fill_env(inspect_input);
295                let tx = std::mem::take(&mut evm_with_inspector.tx);
296                let res = evm_with_inspector.inspect_tx(tx)?;
297
298                inspector.geth_builder().geth_prestate_traces(&res, &prestate_config, &cache_db)?.into()
299            }
300            GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::NoopTracer) => NoopFrame::default().into(),
301            GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::MuxTracer) => {
302                let mux_config = opts.tracer_config.into_mux_config()?;
303                let mut inspector = MuxInspector::try_from_config(mux_config).map_err(|e| anyhow!(e))?;
304                let mut evm_with_inspector = evm.with_inspector(&mut inspector);
305                evm_with_inspector.fill_env(inspect_input);
306                let tx = std::mem::take(&mut evm_with_inspector.tx);
307                let res = evm_with_inspector.inspect_tx(tx)?;
308                inspector.try_into_mux_frame(&res, &cache_db, tx_info)?.into()
309            }
310            GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::FlatCallTracer) => {
311                let flat_call_config = opts.tracer_config.into_flat_call_config()?;
312                let mut inspector = TracingInspector::new(TracingInspectorConfig::from_flat_call_config(&flat_call_config));
313                let mut evm_with_inspector = evm.with_inspector(&mut inspector);
314                evm_with_inspector.fill_env(inspect_input);
315                let tx = std::mem::take(&mut evm_with_inspector.tx);
316                let res = evm_with_inspector.inspect_tx(tx)?;
317                inspector
318                    .with_transaction_gas_limit(res.result.gas_used())
319                    .into_parity_builder()
320                    .into_localized_transaction_traces(tx_info)
321                    .into()
322            }
323            _ => return Err(anyhow!("tracer not implemented").into()),
324        };
325
326        Ok(trace_result)
327    }
328}
329
330trait TxEnvExt {
331    fn fill_env(&mut self, input: EvmInput);
332}
333
334trait EvmExt<DB: Database> {
335    fn fill_env(&mut self, input: EvmInput);
336}
337
338impl<DB: Database, INSP, I, P, F> EvmExt<DB> for RevmEvm<Context<BlockEnv, TxEnv, CfgEnv, DB>, INSP, I, P, F> {
339    fn fill_env(&mut self, input: EvmInput) {
340        self.block.fill_env(&input);
341        self.tx.fill_env(input);
342    }
343}
344
345impl TxEnvExt for TxEnv {
346    fn fill_env(&mut self, input: EvmInput) {
347        self.caller = input.from.into();
348        self.kind = match input.to {
349            Some(contract) => TransactTo::Call(contract.into()),
350            None => TransactTo::Create,
351        };
352        self.gas_limit = min(input.gas_limit.into(), GAS_MAX_LIMIT);
353        self.gas_price = input.gas_price;
354        self.chain_id = input.chain_id.map_into();
355        self.nonce = input.nonce.map_into().unwrap_or_default();
356        self.data = input.data.into();
357        self.value = input.value.into();
358        self.gas_priority_fee = None;
359    }
360}
361
362trait BlockEnvExt {
363    fn fill_env(&mut self, input: &EvmInput);
364}
365
366impl BlockEnvExt for BlockEnv {
367    fn fill_env(&mut self, input: &EvmInput) {
368        self.timestamp = U256::from(*input.block_timestamp);
369        self.number = U256::from(input.block_number.as_u64());
370        self.basefee = 0;
371    }
372}
373
374// -----------------------------------------------------------------------------
375// Database
376// -----------------------------------------------------------------------------
377
378/// Contextual data that is read or set durint the execution of a transaction in the EVM.
379struct RevmSession {
380    /// Executor configuration.
381    config: ExecutorConfig,
382
383    /// Service to communicate with the storage.
384    storage: Arc<StratusStorage>,
385
386    /// Input passed to EVM to execute the transaction.
387    input: EvmInput,
388
389    /// Changes made to the storage during the execution of the transaction.
390    storage_changes: ExecutionChanges,
391
392    /// Metrics collected during EVM execution.
393    metrics: EvmExecutionMetrics,
394}
395
396impl RevmSession {
397    /// Creates the base session to be used with REVM.
398    pub fn new(storage: Arc<StratusStorage>, config: ExecutorConfig) -> Self {
399        Self {
400            config,
401            storage,
402            input: EvmInput::default(),
403            storage_changes: BTreeMap::default(),
404            metrics: EvmExecutionMetrics::default(),
405        }
406    }
407
408    /// Resets the session to be used with a new transaction.
409    pub fn reset(&mut self, input: EvmInput) {
410        self.input = input;
411        self.storage_changes = BTreeMap::default();
412        self.metrics = EvmExecutionMetrics::default();
413    }
414}
415
416impl Database for RevmSession {
417    type Error = StratusError;
418
419    fn basic(&mut self, revm_address: RevmAddress) -> Result<Option<AccountInfo>, StratusError> {
420        self.metrics.account_reads += 1;
421
422        // retrieve account
423        let address: Address = revm_address.into();
424        let account = self.storage.read_account(address, self.input.point_in_time, self.input.kind)?;
425
426        // warn if the loaded account is the `to` account and it does not have a bytecode
427        if let Some(to_address) = self.input.to
428            && account.bytecode.is_none()
429            && address == to_address
430            && self.input.is_contract_call()
431        {
432            if self.config.executor_reject_not_contract {
433                return Err(TransactionError::AccountNotContract { address: to_address }.into());
434            } else {
435                tracing::warn!(%address, "evm to_account is not a contract because does not have bytecode");
436            }
437        }
438
439        // early convert response because account will be moved
440        let revm_account: AccountInfo = (&account).into();
441        // track original value, except if ignored address
442        if not(account.address.is_ignored()) {
443            self.storage_changes
444                .insert(account.address, ExecutionAccountChanges::from_original_values(account));
445        }
446
447        Ok(Some(revm_account))
448    }
449
450    fn code_by_hash(&mut self, _: B256) -> Result<RevmBytecode, StratusError> {
451        todo!()
452    }
453
454    fn storage(&mut self, revm_address: RevmAddress, revm_index: U256) -> Result<U256, StratusError> {
455        self.metrics.slot_reads += 1;
456
457        // convert slot
458        let address: Address = revm_address.into();
459        let index: SlotIndex = revm_index.into();
460
461        // load slot from storage
462        let slot = self.storage.read_slot(address, index, self.input.point_in_time, self.input.kind)?;
463
464        // track original value, except if ignored address
465        if not(address.is_ignored()) {
466            match self.storage_changes.get_mut(&address) {
467                Some(account) => {
468                    account.slots.insert(index, ExecutionValueChange::from_original(slot));
469                }
470                None => {
471                    tracing::error!(reason = "reading slot without account loaded", %address, %index);
472                    return Err(UnexpectedError::Unexpected(anyhow!("Account '{}' was expected to be loaded by EVM, but it was not", address)).into());
473                }
474            };
475        }
476
477        Ok(slot.value.into())
478    }
479
480    fn block_hash(&mut self, _: u64) -> Result<B256, StratusError> {
481        todo!()
482    }
483}
484
485impl DatabaseRef for RevmSession {
486    type Error = StratusError;
487
488    fn basic_ref(&self, address: revm::primitives::Address) -> Result<Option<AccountInfo>, Self::Error> {
489        // retrieve account
490        let address: Address = address.into();
491        let account = self.storage.read_account(address, self.input.point_in_time, self.input.kind)?;
492        let revm_account: AccountInfo = (&account).into();
493
494        Ok(Some(revm_account))
495    }
496
497    fn storage_ref(&self, address: revm::primitives::Address, index: U256) -> Result<U256, Self::Error> {
498        // convert slot
499        let address: Address = address.into();
500        let index: SlotIndex = index.into();
501
502        // load slot from storage
503        let slot = self.storage.read_slot(address, index, self.input.point_in_time, self.input.kind)?;
504
505        Ok(slot.value.into())
506    }
507
508    fn block_hash_ref(&self, _: u64) -> Result<B256, Self::Error> {
509        todo!()
510    }
511
512    fn code_by_hash_ref(&self, _code_hash: B256) -> Result<revm::state::Bytecode, Self::Error> {
513        unimplemented!()
514    }
515}
516
517// -----------------------------------------------------------------------------
518// Conversion
519// -----------------------------------------------------------------------------
520
521fn parse_revm_execution(revm_result: ResultAndState, input: EvmInput, execution_changes: ExecutionChanges) -> Result<EvmExecution, StratusError> {
522    let (result, tx_output, logs, gas) = parse_revm_result(revm_result.result);
523    let changes = parse_revm_state(revm_result.state, execution_changes)?;
524
525    tracing::debug!(?result, %gas, tx_output_len = %tx_output.len(), %tx_output, "evm executed");
526    let mut deployed_contract_address = None;
527    for (address, changes) in changes.iter() {
528        if changes.bytecode.is_modified() {
529            deployed_contract_address = Some(*address);
530        }
531    }
532
533    Ok(EvmExecution {
534        block_timestamp: input.block_timestamp,
535        result,
536        output: tx_output,
537        logs,
538        gas,
539        changes,
540        deployed_contract_address,
541    })
542}
543
544fn parse_revm_result(result: RevmExecutionResult) -> (ExecutionResult, Bytes, Vec<Log>, Gas) {
545    match result {
546        RevmExecutionResult::Success { output, gas_used, logs, .. } => {
547            let result = ExecutionResult::Success;
548            let output = Bytes::from(output);
549            let logs = logs.into_iter().map_into().collect();
550            let gas = Gas::from(gas_used);
551            (result, output, logs, gas)
552        }
553        RevmExecutionResult::Revert { output, gas_used } => {
554            let output = Bytes::from(output);
555            let result = ExecutionResult::Reverted { reason: (&output).into() };
556            let gas = Gas::from(gas_used);
557            (result, output, Vec::new(), gas)
558        }
559        RevmExecutionResult::Halt { reason, gas_used } => {
560            let result = ExecutionResult::new_halted(format!("{reason:?}"));
561            let output = Bytes::default();
562            let gas = Gas::from(gas_used);
563            (result, output, Vec::new(), gas)
564        }
565    }
566}
567
568fn parse_revm_state(revm_state: EvmState, mut execution_changes: ExecutionChanges) -> Result<ExecutionChanges, StratusError> {
569    for (revm_address, revm_account) in revm_state {
570        let address: Address = revm_address.into();
571        if address.is_ignored() {
572            continue;
573        }
574
575        // apply changes according to account status
576        tracing::debug!(
577            %address,
578            status = ?revm_account.status,
579            balance = %revm_account.info.balance,
580            nonce = %revm_account.info.nonce,
581            slots = %revm_account.storage.len(),
582            "evm account"
583        );
584        let (account_created, account_touched) = (revm_account.is_created(), revm_account.is_touched());
585
586        // parse revm types to stratus primitives
587        let account: Account = (revm_address, revm_account.info).into();
588        let account_modified_slots: Vec<Slot> = revm_account
589            .storage
590            .into_iter()
591            .filter_map(|(index, value)| match value.is_changed() {
592                true => Some(Slot::new(index.into(), value.present_value.into())),
593                false => None,
594            })
595            .collect();
596
597        // handle account created (contracts) or touched (everything else)
598        if account_created {
599            let addr = account.address;
600            let account_changes = ExecutionAccountChanges::from_modified_values(account, account_modified_slots);
601            execution_changes.insert(addr, account_changes);
602        } else if account_touched {
603            let Some(account_changes) = execution_changes.get_mut(&address) else {
604                tracing::error!(keys = ?execution_changes.keys(), %address, "account touched, but not loaded by evm");
605                return Err(UnexpectedError::Unexpected(anyhow!("Account '{}' was expected to be loaded by EVM, but it was not", address)).into());
606            };
607            account_changes.apply_modifications(account, account_modified_slots);
608        }
609    }
610    Ok(execution_changes)
611}
612
613pub fn default_trace(tracer_type: GethDebugTracerType, tx: TransactionStage) -> GethTrace {
614    match tracer_type {
615        GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::FourByteTracer) => FourByteFrame::default().into(),
616        // HACK: Spoof empty call frame to prevent Blockscout from retrying unnecessary trace calls
617        GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::CallTracer) => {
618            let (typ, to) = match tx.to() {
619                Some(_) => ("CALL".to_string(), tx.to().map_into()),
620                None => ("CREATE".to_string(), tx.deployed_contract_address().map_into()),
621            };
622
623            CallFrame {
624                from: tx.from().into(),
625                to,
626                typ,
627                ..Default::default()
628            }
629            .into()
630        }
631        GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::MuxTracer) => MuxFrame::default().into(),
632        GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::FlatCallTracer) => FlatCallFrame::default().into(),
633        _ => NoopFrame::default().into(),
634    }
635}
636
637/// Enhances a GethTrace with decoded error information.
638fn enhance_trace_with_decoded_errors(trace: &mut GethTrace) {
639    match trace {
640        GethTrace::CallTracer(call_frame) => {
641            enhance_call_frame_errors(call_frame);
642        }
643        _ => {
644            // Other trace types don't have call frames with errors to decode
645        }
646    }
647}
648
649/// Enhances a single CallFrame and recursively enhances all nested calls.
650fn enhance_call_frame_errors(frame: &mut CallFrame) {
651    if let Some(error) = frame.error.as_ref()
652        && let Some(decoded_error) = frame.output.as_ref().and_then(|output| codegen::error_sig_opt(output))
653    {
654        frame.revert_reason = Some(format!("{error}: {decoded_error}"));
655    }
656
657    for nested_call in frame.calls.iter_mut() {
658        enhance_call_frame_errors(nested_call);
659    }
660}