Skip to main content

reth_rpc/
otterscan.rs

1use alloy_consensus::{BlockHeader, Typed2718};
2use alloy_eips::{eip1898::LenientBlockNumberOrTag, BlockId};
3use alloy_evm::block::calc::{base_block_reward, block_reward, ommer_reward};
4use alloy_network::{primitives::HeaderResponse, ReceiptResponse, TransactionResponse};
5use alloy_primitives::{Address, Bytes, TxHash, B256, U256};
6use alloy_rpc_types_eth::{BlockTransactions, TransactionReceipt};
7use alloy_rpc_types_trace::{
8    otterscan::{
9        BlockDetails, ContractCreator, InternalIssuance, InternalOperation, OtsBlockTransactions,
10        OtsReceipt, OtsTransactionReceipt, TraceEntry, TransactionsWithReceipts,
11    },
12    parity::{Action, CreateAction, CreateOutput, LocalizedTransactionTrace, TraceOutput},
13};
14use async_trait::async_trait;
15use jsonrpsee::{core::RpcResult, types::ErrorObjectOwned};
16use reth_chainspec::ChainSpecProvider;
17use reth_primitives_traits::{BlockBody, TxTy};
18use reth_rpc_api::{EthApiServer, OtterscanServer};
19use reth_rpc_convert::RpcTxReq;
20use reth_rpc_eth_api::{
21    helpers::{EthTransactions, TraceExt},
22    RpcBlock, RpcHeader, RpcReceipt, RpcTransaction,
23};
24use reth_rpc_eth_types::{utils::binary_search, EthApiError};
25use reth_rpc_server_types::result::internal_rpc_err;
26use revm::context_interface::result::ExecutionResult;
27use revm_inspectors::{
28    otterscan::InternalOperationsInspector,
29    tracing::{
30        types::{CallKind, CallTraceNode},
31        TracingInspectorConfig,
32    },
33};
34use tokio::sync::OwnedSemaphorePermit;
35
36const API_LEVEL: u64 = 8;
37
38/// Otterscan API.
39#[derive(Debug)]
40pub struct OtterscanApi<Eth> {
41    eth: Eth,
42}
43
44impl<Eth> OtterscanApi<Eth> {
45    /// Creates a new instance of `Otterscan`.
46    pub const fn new(eth: Eth) -> Self {
47        Self { eth }
48    }
49}
50
51impl<Eth> OtterscanApi<Eth>
52where
53    Eth: TraceExt,
54{
55    /// Constructs a `BlockDetails` from a block and its receipts.
56    async fn block_details(
57        &self,
58        block: RpcBlock<Eth::NetworkTypes>,
59        receipts: Vec<RpcReceipt<Eth::NetworkTypes>>,
60    ) -> RpcResult<BlockDetails<RpcHeader<Eth::NetworkTypes>>> {
61        // Execution fees include the base fee; blob fees are not part of this field.
62        let total_fees = receipts
63            .iter()
64            .map(|receipt| {
65                U256::from(receipt.gas_used()) * U256::from(receipt.effective_gas_price())
66            })
67            .sum::<U256>();
68
69        let chain_spec = self.eth.provider().chain_spec();
70        let reward = if block.header.number() == 0 {
71            None
72        } else {
73            base_block_reward(&chain_spec, block.header.number())
74        };
75        let issuance = if let Some(reward) = reward {
76            let recovered = if block.uncles.is_empty() {
77                None
78            } else {
79                Some(
80                    self.eth
81                        .recovered_block(block.header.hash().into())
82                        .await
83                        .map_err(Into::<ErrorObjectOwned>::into)?
84                        .ok_or(EthApiError::HeaderNotFound(block.header.hash().into()))?,
85                )
86            };
87            let ommers =
88                recovered.as_ref().and_then(|block| block.body().ommers()).unwrap_or_default();
89            calculate_issuance(
90                reward,
91                block.header.number(),
92                ommers.iter().map(BlockHeader::number),
93            )
94        } else {
95            InternalIssuance::default()
96        };
97        Ok(BlockDetails::new(block, issuance, total_fees))
98    }
99
100    /// Acquires a permit to execute a tracing call.
101    ///
102    /// The permit should be moved into the spawned trace so it is held until the trace completes.
103    async fn acquire_trace_permit(&self) -> RpcResult<OwnedSemaphorePermit> {
104        Ok(self.eth.acquire_owned_tracing().await.map_err(|_| EthApiError::InternalEthError)?)
105    }
106}
107
108#[async_trait]
109impl<Eth> OtterscanServer<RpcTransaction<Eth::NetworkTypes>, RpcHeader<Eth::NetworkTypes>>
110    for OtterscanApi<Eth>
111where
112    Eth: EthApiServer<
113            RpcTxReq<Eth::NetworkTypes>,
114            RpcTransaction<Eth::NetworkTypes>,
115            RpcBlock<Eth::NetworkTypes>,
116            RpcReceipt<Eth::NetworkTypes>,
117            RpcHeader<Eth::NetworkTypes>,
118            TxTy<Eth::Primitives>,
119        > + EthTransactions
120        + TraceExt,
121{
122    /// Handler for `ots_getHeaderByNumber` and `erigon_getHeaderByNumber`
123    async fn get_header_by_number(
124        &self,
125        block_number: LenientBlockNumberOrTag,
126    ) -> RpcResult<Option<RpcHeader<Eth::NetworkTypes>>> {
127        self.eth.header_by_number(block_number.into()).await
128    }
129
130    /// Handler for `ots_hasCode`
131    async fn has_code(&self, address: Address, block_id: Option<BlockId>) -> RpcResult<bool> {
132        EthApiServer::get_code(&self.eth, address, block_id).await.map(|code| !code.is_empty())
133    }
134
135    /// Handler for `ots_getApiLevel`
136    async fn get_api_level(&self) -> RpcResult<u64> {
137        Ok(API_LEVEL)
138    }
139
140    /// Handler for `ots_getInternalOperations`
141    async fn get_internal_operations(&self, tx_hash: TxHash) -> RpcResult<Vec<InternalOperation>> {
142        let permit = self.acquire_trace_permit().await?;
143        self.eth
144            .spawn_trace_transaction_in_block_with_inspector(
145                tx_hash,
146                InternalOperationsInspector::default(),
147                move |_tx_info, inspector, _, _| {
148                    let _permit = permit;
149                    Ok(inspector.into_operations())
150                },
151            )
152            .await
153            .map_err(Into::into)
154            .map(Option::unwrap_or_default)
155    }
156
157    /// Handler for `ots_getTransactionError`
158    async fn get_transaction_error(&self, tx_hash: TxHash) -> RpcResult<Option<Bytes>> {
159        let permit = self.acquire_trace_permit().await?;
160        self.eth
161            .spawn_replay_transaction(tx_hash, move |_tx_info, res, _| {
162                let _permit = permit;
163                Ok(transaction_error(res.result))
164            })
165            .await
166            .map_err(Into::into)
167    }
168
169    /// Handler for `ots_traceTransaction`
170    async fn trace_transaction(&self, tx_hash: TxHash) -> RpcResult<Option<Vec<TraceEntry>>> {
171        let permit = self.acquire_trace_permit().await?;
172        self.eth
173            .spawn_trace_transaction_in_block(
174                tx_hash,
175                TracingInspectorConfig::default_parity(),
176                move |_tx_info, inspector, _, _| {
177                    let _permit = permit;
178                    Ok(otterscan_traces(inspector.into_traces().into_nodes()))
179                },
180            )
181            .await
182            .map_err(Into::into)
183    }
184
185    /// Handler for `ots_getBlockDetails`
186    async fn get_block_details(
187        &self,
188        block_number: LenientBlockNumberOrTag,
189    ) -> RpcResult<BlockDetails<RpcHeader<Eth::NetworkTypes>>> {
190        let block_number = block_number.into_inner();
191        let block = self.eth.block_by_number(block_number, false);
192        let block_id = block_number.into();
193        let receipts = self.eth.block_receipts(block_id);
194        let (block, receipts) = futures::try_join!(block, receipts)?;
195        self.block_details(
196            block.ok_or(EthApiError::HeaderNotFound(block_id))?,
197            receipts.ok_or(EthApiError::ReceiptsNotFound(block_id))?,
198        )
199        .await
200    }
201
202    /// Handler for `ots_getBlockDetailsByHash`
203    async fn get_block_details_by_hash(
204        &self,
205        block_hash: B256,
206    ) -> RpcResult<BlockDetails<RpcHeader<Eth::NetworkTypes>>> {
207        let block = self.eth.block_by_hash(block_hash, false);
208        let block_id = block_hash.into();
209        let receipts = self.eth.block_receipts(block_id);
210        let (block, receipts) = futures::try_join!(block, receipts)?;
211        self.block_details(
212            block.ok_or(EthApiError::HeaderNotFound(block_id))?,
213            receipts.ok_or(EthApiError::ReceiptsNotFound(block_id))?,
214        )
215        .await
216    }
217
218    /// Handler for `ots_getBlockTransactions`
219    async fn get_block_transactions(
220        &self,
221        block_number: LenientBlockNumberOrTag,
222        page_number: usize,
223        page_size: usize,
224    ) -> RpcResult<
225        OtsBlockTransactions<RpcTransaction<Eth::NetworkTypes>, RpcHeader<Eth::NetworkTypes>>,
226    > {
227        let block_number = block_number.into_inner();
228        // retrieve full block and its receipts
229        let block = self.eth.block_by_number(block_number, true);
230        let block_id = block_number.into();
231        let receipts = self.eth.block_receipts(block_id);
232        let (block, receipts) = futures::try_join!(block, receipts)?;
233
234        let mut block = block.ok_or(EthApiError::HeaderNotFound(block_id))?;
235        let mut receipts = receipts.ok_or(EthApiError::ReceiptsNotFound(block_id))?;
236
237        // check if the number of transactions matches the number of receipts
238        let tx_len = block.transactions.len();
239        if tx_len != receipts.len() {
240            return Err(internal_rpc_err(
241                "the number of transactions does not match the number of receipts",
242            ))
243        }
244
245        // make sure the block is full
246        let BlockTransactions::Full(transactions) = &mut block.transactions else {
247            return Err(internal_rpc_err("block is not full"));
248        };
249
250        let page = block_transaction_page_range(tx_len, page_number, page_size);
251
252        // Crop transactions
253        *transactions = transactions.drain(page.clone()).collect::<Vec<_>>();
254
255        // Crop receipts and transform them into OtsTransactionReceipt
256        let timestamp = Some(block.header.timestamp());
257        let receipts = receipts
258            .drain(page)
259            .zip(transactions.iter().map(Typed2718::ty))
260            .map(|(receipt, tx_ty)| {
261                let inner = OtsReceipt {
262                    status: receipt.status(),
263                    cumulative_gas_used: receipt.cumulative_gas_used(),
264                    logs: None,
265                    logs_bloom: None,
266                    r#type: tx_ty,
267                };
268
269                let receipt = TransactionReceipt {
270                    inner,
271                    transaction_hash: receipt.transaction_hash(),
272                    transaction_index: receipt.transaction_index(),
273                    block_hash: receipt.block_hash(),
274                    block_number: receipt.block_number(),
275                    gas_used: receipt.gas_used(),
276                    effective_gas_price: receipt.effective_gas_price(),
277                    blob_gas_used: receipt.blob_gas_used(),
278                    blob_gas_price: receipt.blob_gas_price(),
279                    from: receipt.from(),
280                    to: receipt.to(),
281                    contract_address: receipt.contract_address(),
282                };
283
284                OtsTransactionReceipt { receipt, timestamp }
285            })
286            .collect();
287
288        // use `transaction_count` to indicate the paginate information
289        let mut block = OtsBlockTransactions { fullblock: block.into(), receipts };
290        block.fullblock.transaction_count = tx_len;
291        Ok(block)
292    }
293
294    /// Handler for `ots_searchTransactionsBefore`
295    async fn search_transactions_before(
296        &self,
297        _address: Address,
298        _block_number: LenientBlockNumberOrTag,
299        _page_size: usize,
300    ) -> RpcResult<TransactionsWithReceipts> {
301        Err(internal_rpc_err("unimplemented"))
302    }
303
304    /// Handler for `ots_searchTransactionsAfter`
305    async fn search_transactions_after(
306        &self,
307        _address: Address,
308        _block_number: LenientBlockNumberOrTag,
309        _page_size: usize,
310    ) -> RpcResult<TransactionsWithReceipts> {
311        Err(internal_rpc_err("unimplemented"))
312    }
313
314    /// Handler for `ots_getTransactionBySenderAndNonce`
315    async fn get_transaction_by_sender_and_nonce(
316        &self,
317        sender: Address,
318        nonce: u64,
319    ) -> RpcResult<Option<TxHash>> {
320        Ok(self
321            .eth
322            .get_transaction_by_sender_and_nonce(sender, nonce, false)
323            .await
324            .map_err(Into::into)?
325            .map(|tx| tx.tx_hash()))
326    }
327
328    /// Handler for `ots_getContractCreator`
329    async fn get_contract_creator(&self, address: Address) -> RpcResult<Option<ContractCreator>> {
330        if !self.has_code(address, None).await? {
331            return Ok(None);
332        }
333
334        let permit = self.acquire_trace_permit().await?;
335        let num = binary_search::<_, _, ErrorObjectOwned>(
336            1,
337            self.eth.block_number()?.saturating_to(),
338            |mid| {
339                Box::pin(async move {
340                    Ok(!EthApiServer::get_code(&self.eth, address, Some(mid.into()))
341                        .await?
342                        .is_empty())
343                })
344            },
345        )
346        .await?;
347
348        let traces = self
349            .eth
350            .trace_block_with(
351                num.into(),
352                None,
353                TracingInspectorConfig::default_parity(),
354                move |tx_info, mut ctx| {
355                    let _permit = &permit;
356                    Ok(ctx
357                        .take_inspector()
358                        .into_parity_builder()
359                        .into_localized_transaction_traces(tx_info))
360                },
361            )
362            .await
363            .map_err(Into::into)?
364            .map(|traces| find_contract_creator(address, traces))
365            .transpose()?;
366
367        // Code-presence search assumes a single deployment. It cannot reliably identify
368        // the first deployment of contracts that were destroyed and recreated.
369        Ok(traces.flatten())
370    }
371}
372
373/// Finds a creation that was not rolled back by its own frame or an enclosing frame.
374fn find_contract_creator(
375    address: Address,
376    transactions: Vec<Vec<LocalizedTransactionTrace>>,
377) -> Result<Option<ContractCreator>, EthApiError> {
378    for traces in transactions {
379        let mut reverted_path: Option<Vec<usize>> = None;
380        for tx_trace in traces {
381            let trace = tx_trace.trace;
382            // Parity traces are in preorder, so a failed frame precedes its entire subtree.
383            if reverted_path.as_ref().is_some_and(|path| trace.trace_address.starts_with(path)) {
384                continue
385            }
386            reverted_path = None;
387            if trace.error.is_some() {
388                reverted_path = Some(trace.trace_address);
389                continue
390            }
391            if let (
392                Action::Create(CreateAction { from: creator, .. }),
393                Some(TraceOutput::Create(CreateOutput { address: contract, .. })),
394            ) = (trace.action, trace.result) &&
395                contract == address
396            {
397                return Ok(Some(ContractCreator {
398                    hash: tx_trace.transaction_hash.ok_or(EthApiError::TransactionNotFound)?,
399                    creator,
400                }))
401            }
402        }
403    }
404    Ok(None)
405}
406
407/// Returns the transaction slice for an Otterscan block page.
408///
409/// Pages are selected from the end of the block, retaining block order within each page.
410/// The frontend reverses each page and uses this ordering for transaction-index links.
411const fn block_transaction_page_range(
412    tx_len: usize,
413    page_number: usize,
414    page_size: usize,
415) -> std::ops::Range<usize> {
416    let page_end = tx_len.saturating_sub(page_number.saturating_mul(page_size));
417    let page_start = page_end.saturating_sub(page_size);
418    page_start..page_end
419}
420
421/// Rewards issued to the miner (including ommer inclusion) and to the ommers themselves.
422fn calculate_issuance(
423    reward: u128,
424    number: u64,
425    ommer_numbers: impl ExactSizeIterator<Item = u64>,
426) -> InternalIssuance {
427    let block_reward = U256::from(block_reward(reward, ommer_numbers.len()));
428    let uncle_reward =
429        ommer_numbers.map(|ommer| U256::from(ommer_reward(reward, number, ommer))).sum::<U256>();
430    InternalIssuance { block_reward, uncle_reward, issuance: block_reward + uncle_reward }
431}
432
433fn transaction_error<H>(result: ExecutionResult<H>) -> Bytes {
434    match result {
435        ExecutionResult::Revert { output, .. } => output,
436        _ => Bytes::new(),
437    }
438}
439
440/// A self-destruct is a separate operation at the end of its enclosing call, after any children.
441fn otterscan_traces(nodes: Vec<CallTraceNode>) -> Vec<TraceEntry> {
442    let mut entries = Vec::with_capacity(nodes.len());
443    let mut selfdestructs: Vec<TraceEntry> = Vec::new();
444    for CallTraceNode { trace, .. } in nodes {
445        // The arena is in call-entry order. Emit completed calls' self-destructs before
446        // entering a sibling or returning to an ancestor.
447        while selfdestructs.last().is_some_and(|entry| entry.depth > trace.depth as u32) {
448            entries.push(selfdestructs.pop().expect("checked above"));
449        }
450        if let Some(to) = trace.selfdestruct_refund_target {
451            selfdestructs.push(TraceEntry {
452                r#type: "SELFDESTRUCT".to_string(),
453                depth: trace.depth as u32 + 1,
454                from: trace.selfdestruct_address.unwrap_or(trace.address),
455                to,
456                value: trace.selfdestruct_transferred_value,
457                input: Bytes::new(),
458                output: Bytes::new(),
459            });
460        }
461        entries.push(TraceEntry {
462            r#type: trace.kind.to_string(),
463            depth: trace.depth as u32,
464            from: trace.caller,
465            to: trace.address,
466            value: (!matches!(trace.kind, CallKind::StaticCall | CallKind::DelegateCall))
467                .then_some(trace.value),
468            input: trace.data,
469            output: trace.output,
470        });
471    }
472    entries.extend(selfdestructs.into_iter().rev());
473    entries
474}
475
476#[cfg(test)]
477mod tests {
478
479    use super::*;
480    use alloy_consensus::{constants::ETH_TO_WEI, Header};
481    use alloy_primitives::{bytes, TxKind};
482    use alloy_rpc_types_trace::parity::TransactionTrace;
483    use reth_chainspec::MAINNET;
484    use reth_evm_ethereum::EthEvmConfig;
485    use reth_network_api::noop::NoopNetwork;
486    use reth_provider::test_utils::MockEthProvider;
487    use reth_transaction_pool::test_utils::testing_pool;
488    use revm::{
489        context::TxEnv,
490        context_interface::result::{HaltReason, Output, SuccessReason},
491        database::InMemoryDB,
492        inspector::InspectorEvmTr,
493        primitives::hardfork::SpecId,
494        state::{AccountInfo, Bytecode},
495        Context, InspectEvm, MainBuilder, MainContext,
496    };
497    use revm_inspectors::tracing::{types::CallTrace, TracingInspector};
498
499    #[test]
500    fn block_transaction_pages_match_frontend_index_navigation() {
501        // The frontend requests floor((count - 1 - index) / size), then reverses the page.
502        for count in [0, 1, 10, 25, 51] {
503            for size in [1, 10, 25] {
504                for index in 0..count {
505                    let reverse_index = count - 1 - index;
506                    let page = block_transaction_page_range(count, reverse_index / size, size);
507                    let displayed = page.rev().collect::<Vec<_>>();
508                    assert_eq!(displayed[reverse_index % size], index);
509                }
510            }
511        }
512        assert_eq!(block_transaction_page_range(25, 0, 10), 15..25);
513        assert_eq!(block_transaction_page_range(25, 1, 10), 5..15);
514        assert_eq!(block_transaction_page_range(25, 2, 10), 0..5);
515        assert_eq!(block_transaction_page_range(25, 3, 10), 0..0);
516        assert_eq!(block_transaction_page_range(25, usize::MAX, 10), 0..0);
517        assert_eq!(block_transaction_page_range(25, 0, usize::MAX), 0..25);
518        assert_eq!(block_transaction_page_range(25, 1, usize::MAX), 0..0);
519        assert_eq!(block_transaction_page_range(25, 0, 0), 25..25);
520    }
521
522    #[test]
523    fn transaction_error_returns_only_revert_data() {
524        let success: ExecutionResult = ExecutionResult::Success {
525            reason: SuccessReason::Return,
526            gas: Default::default(),
527            logs: vec![],
528            output: Output::Call(Bytes::from_static(b"successful return data")),
529        };
530        let halt = ExecutionResult::Halt {
531            reason: HaltReason::OutOfGas(revm::context_interface::result::OutOfGasError::Basic),
532            gas: Default::default(),
533            logs: vec![],
534        };
535        for result in [
536            success,
537            halt,
538            ExecutionResult::Revert { gas: Default::default(), logs: vec![], output: Bytes::new() },
539        ] {
540            assert_eq!(serde_json::to_value(Some(transaction_error(result))).unwrap(), "0x");
541        }
542        let output = Bytes::from_static(b"revert data");
543        assert_eq!(
544            transaction_error::<HaltReason>(ExecutionResult::Revert {
545                gas: Default::default(),
546                logs: vec![],
547                output: output.clone()
548            }),
549            output
550        );
551    }
552
553    async fn cache_empty_block(
554        cache: reth_rpc_eth_types::EthStateCache<reth_ethereum_primitives::EthPrimitives>,
555        block: reth_ethereum_primitives::Block,
556    ) {
557        // MockEthProvider does not implement recovered_block; seed the normal RPC cache.
558        let outcome = reth_execution_types::ExecutionOutcome::new(
559            Default::default(),
560            vec![vec![]],
561            block.header.number,
562            vec![],
563        );
564        let chain = reth_execution_types::Chain::new(
565            [reth_primitives_traits::RecoveredBlock::new_unhashed(block, vec![])],
566            outcome,
567            Default::default(),
568        );
569        reth_rpc_eth_types::cache::cache_new_blocks_task(
570            cache,
571            futures::stream::iter([reth_chain_state::CanonStateNotification::Commit {
572                new: std::sync::Arc::new(chain),
573            }]),
574        )
575        .await;
576    }
577
578    #[tokio::test]
579    async fn block_details_issuance_follows_chain_forks() {
580        for (spec, number, reward) in [
581            (0, 0),
582            (1, 5),
583            (4_369_999, 5),
584            (4_370_000, 3),
585            (7_279_999, 3),
586            (7_280_000, 2),
587            (15_537_393, 2),
588            (15_537_394, 0),
589        ]
590        .into_iter()
591        .map(|(number, reward)| (MAINNET.clone(), number, reward))
592        .chain(std::iter::once((
593            std::sync::Arc::new(
594                reth_chainspec::ChainSpecBuilder::mainnet().paris_activated().build(),
595            ),
596            1,
597            0,
598        ))) {
599            let provider = MockEthProvider::default().with_chain_spec((*spec).clone());
600            let header = Header { number, ..Default::default() };
601            let hash = header.hash_slow();
602            let block = reth_ethereum_primitives::Block { header, body: Default::default() };
603            provider.add_block(hash, block.clone());
604            provider.add_receipts(number, vec![]);
605            let api = OtterscanApi::new(
606                crate::eth::EthApiBuilder::new(
607                    provider,
608                    testing_pool(),
609                    NoopNetwork::default(),
610                    EthEvmConfig::new(spec),
611                )
612                .build(),
613            );
614            cache_empty_block(api.eth.cache().clone(), block).await;
615            let by_number = api
616                .get_block_details(alloy_eips::BlockNumberOrTag::Number(number).into())
617                .await
618                .unwrap();
619            let by_hash = api.get_block_details_by_hash(hash).await.unwrap();
620            assert_eq!(
621                by_number.issuance.block_reward,
622                U256::from(reward * ETH_TO_WEI),
623                "block {number}"
624            );
625            assert_eq!(by_number.issuance.uncle_reward, U256::ZERO);
626            assert_eq!(by_number.issuance.issuance, by_number.issuance.block_reward);
627            assert_eq!(by_number.issuance, by_hash.issuance);
628        }
629    }
630
631    #[tokio::test]
632    async fn block_details_loads_ommer_headers() {
633        let provider = MockEthProvider::default();
634        let block = reth_ethereum_primitives::Block {
635            header: Header { number: 126, ..Default::default() },
636            body: alloy_consensus::BlockBody {
637                ommers: vec![Header { number: 123, ..Default::default() }],
638                ..Default::default()
639            },
640        };
641        let hash = block.header.hash_slow();
642        provider.add_block(hash, block.clone());
643        provider.add_receipts(126, vec![]);
644        let api = OtterscanApi::new(
645            crate::eth::EthApiBuilder::new(
646                provider,
647                testing_pool(),
648                NoopNetwork::default(),
649                EthEvmConfig::new(MAINNET.clone()),
650            )
651            .build(),
652        );
653        cache_empty_block(api.eth.cache().clone(), block).await;
654        let expected = calculate_issuance(5 * ETH_TO_WEI, 126, [123].into_iter());
655        assert_eq!(
656            api.get_block_details(alloy_eips::BlockNumberOrTag::Number(126).into())
657                .await
658                .unwrap()
659                .issuance,
660            expected
661        );
662        assert_eq!(api.get_block_details_by_hash(hash).await.unwrap().issuance, expected);
663    }
664
665    #[tokio::test]
666    async fn unknown_transactions_and_unimplemented_history_remain_distinct() {
667        let api = OtterscanApi::new(
668            crate::eth::EthApiBuilder::new(
669                MockEthProvider::default(),
670                testing_pool(),
671                NoopNetwork::default(),
672                EthEvmConfig::new(MAINNET.clone()),
673            )
674            .build(),
675        );
676        assert_eq!(api.get_api_level().await.unwrap(), 8);
677        assert_eq!(api.get_transaction_error(B256::ZERO).await.unwrap(), None);
678        assert_eq!(api.trace_transaction(B256::ZERO).await.unwrap(), None);
679        assert!(api.get_internal_operations(B256::ZERO).await.unwrap().is_empty());
680        assert!(api
681            .get_block_details(alloy_eips::BlockNumberOrTag::Number(1).into())
682            .await
683            .is_err());
684        assert!(api
685            .get_block_transactions(alloy_eips::BlockNumberOrTag::Number(1).into(), 0, 10)
686            .await
687            .is_err());
688        for error in [
689            api.search_transactions_before(
690                Address::ZERO,
691                alloy_eips::BlockNumberOrTag::Number(0).into(),
692                10,
693            )
694            .await
695            .unwrap_err(),
696            api.search_transactions_after(
697                Address::ZERO,
698                alloy_eips::BlockNumberOrTag::Number(0).into(),
699                10,
700            )
701            .await
702            .unwrap_err(),
703        ] {
704            assert_eq!(error.code(), -32603);
705            assert_eq!(error.message(), "unimplemented");
706        }
707    }
708
709    #[tokio::test]
710    async fn trace_methods_wait_for_tracing_permit() {
711        use reth_rpc_eth_api::helpers::SpawnBlocking;
712        use std::time::Duration;
713
714        let api = OtterscanApi::new(
715            crate::eth::EthApiBuilder::new(
716                MockEthProvider::default(),
717                testing_pool(),
718                NoopNetwork::default(),
719                EthEvmConfig::new(MAINNET.clone()),
720            )
721            .proof_permits(1)
722            .build(),
723        );
724        let permit = api.eth.acquire_owned_tracing().await.unwrap();
725
726        let timeout = Duration::from_millis(100);
727        assert!(tokio::time::timeout(timeout, api.trace_transaction(B256::ZERO)).await.is_err());
728        assert!(tokio::time::timeout(timeout, api.get_transaction_error(B256::ZERO))
729            .await
730            .is_err());
731        assert!(tokio::time::timeout(timeout, api.get_internal_operations(B256::ZERO))
732            .await
733            .is_err());
734
735        drop(permit);
736        assert_eq!(api.trace_transaction(B256::ZERO).await.unwrap(), None);
737    }
738
739    #[tokio::test]
740    async fn execution_fees_use_full_u256_width() {
741        let api = OtterscanApi::new(
742            crate::eth::EthApiBuilder::new(
743                MockEthProvider::default(),
744                testing_pool(),
745                NoopNetwork::default(),
746                EthEvmConfig::new(MAINNET.clone()),
747            )
748            .build(),
749        );
750        let receipt = TransactionReceipt {
751            inner: alloy_consensus::ReceiptEnvelope::Legacy(
752                alloy_consensus::Receipt {
753                    status: true.into(),
754                    cumulative_gas_used: 2,
755                    logs: vec![],
756                }
757                .with_bloom(),
758            ),
759            transaction_hash: B256::ZERO,
760            transaction_index: Some(0),
761            block_hash: Some(B256::ZERO),
762            block_number: Some(0),
763            gas_used: 2,
764            effective_gas_price: u128::MAX,
765            blob_gas_used: None,
766            blob_gas_price: None,
767            from: Address::ZERO,
768            to: None,
769            contract_address: None,
770        };
771        let details =
772            api.block_details(Default::default(), vec![receipt.clone(), receipt]).await.unwrap();
773        assert_eq!(details.total_fees, U256::from(u128::MAX) * U256::from(4));
774    }
775
776    #[test]
777    fn issuance_includes_ommer_and_inclusion_rewards() {
778        // A Frontier reward with an ommer three blocks behind.
779        let issuance = calculate_issuance(5 * ETH_TO_WEI, 126, [123].into_iter());
780        assert_eq!(issuance.block_reward, U256::from(5_156_250_000_000_000_000u128));
781        assert_eq!(issuance.uncle_reward, U256::from(3_125_000_000_000_000_000u128));
782        assert_eq!(issuance.issuance, U256::from(8_281_250_000_000_000_000u128));
783        let issuance = calculate_issuance(2 * ETH_TO_WEI, 100, [99, 94].into_iter());
784        assert_eq!(issuance.block_reward, U256::from(2_125_000_000_000_000_000u128));
785        assert_eq!(issuance.uncle_reward, U256::from(2_250_000_000_000_000_000u128));
786    }
787
788    fn execute(code: Bytes, spec: SpecId) -> (ExecutionResult, Vec<TraceEntry>) {
789        let contract = Address::repeat_byte(0x11);
790        let mut db = InMemoryDB::default();
791        db.insert_account_info(
792            Address::ZERO,
793            AccountInfo { balance: U256::from(ETH_TO_WEI), ..Default::default() },
794        );
795        db.insert_account_info(
796            contract,
797            AccountInfo {
798                balance: U256::from(100),
799                code: Some(Bytecode::new_legacy(code)),
800                ..Default::default()
801            },
802        );
803        let mut evm = Context::mainnet()
804            .modify_cfg_chained(|cfg| cfg.spec = spec)
805            .with_db(db)
806            .build_mainnet_with_inspector(TracingInspector::new(
807                TracingInspectorConfig::default_parity(),
808            ));
809        let result = evm
810            .inspect_tx(TxEnv {
811                kind: TxKind::Call(contract),
812                value: U256::from(7),
813                gas_limit: 1_000_000,
814                ..Default::default()
815            })
816            .unwrap();
817        let (_, inspector) = evm.ctx_inspector();
818        (result.result, otterscan_traces(inspector.traces().nodes().to_vec()))
819    }
820
821    #[test]
822    fn selfdestruct_preserves_enclosing_call_and_beneficiary() {
823        for spec in [SpecId::SHANGHAI, SpecId::CANCUN] {
824            let (result, traces) = execute(bytes!("6022ff"), spec);
825            assert!(result.is_success());
826            assert_eq!(traces.len(), 2);
827            assert_eq!(traces[0].r#type, "CALL");
828            assert_eq!(traces[0].depth, 0);
829            assert_eq!(traces[0].value, Some(U256::from(7)));
830            assert_eq!(traces[1].r#type, "SELFDESTRUCT");
831            assert_eq!(traces[1].depth, 1);
832            assert_eq!(traces[1].from, Address::repeat_byte(0x11));
833            assert_eq!(traces[1].to, Address::with_last_byte(0x22));
834            assert_eq!(traces[1].value, Some(U256::from(107)));
835        }
836    }
837
838    #[test]
839    fn selfdestruct_after_a_child_call_uses_enclosing_depth() {
840        // Call another account, then destroy the root contract. The destruction is a sibling
841        // of that call, not its child.
842        let (result, traces) =
843            execute(bytes!("60006000600060006000603361fffff1506022ff"), SpecId::CANCUN);
844        assert!(result.is_success());
845        assert_eq!(traces.len(), 3);
846        assert_eq!(traces[1].r#type, "CALL");
847        assert_eq!(traces[1].depth, 1);
848        assert_eq!(traces[2].r#type, "SELFDESTRUCT");
849        assert_eq!(traces[2].depth, 1);
850        assert_eq!(traces[2].from, Address::repeat_byte(0x11));
851        assert_eq!(traces[2].to, Address::with_last_byte(0x22));
852    }
853
854    #[test]
855    fn selfdestructs_follow_children_and_precede_siblings() {
856        let nodes = [0, 1, 2, 1]
857            .into_iter()
858            .enumerate()
859            .map(|(idx, depth)| CallTraceNode {
860                trace: CallTrace {
861                    depth,
862                    address: Address::with_last_byte(idx as u8),
863                    selfdestruct_address: (idx < 3).then_some(Address::with_last_byte(idx as u8)),
864                    selfdestruct_refund_target: (idx < 3).then_some(Address::with_last_byte(0xff)),
865                    selfdestruct_transferred_value: (idx < 3).then_some(U256::ZERO),
866                    ..Default::default()
867                },
868                ..Default::default()
869            })
870            .collect();
871        let traces = otterscan_traces(nodes);
872        let order =
873            traces.iter().map(|trace| (trace.r#type.as_str(), trace.depth)).collect::<Vec<_>>();
874        assert_eq!(
875            order,
876            [
877                ("CALL", 0),
878                ("CALL", 1),
879                ("CALL", 2),
880                ("SELFDESTRUCT", 3),
881                ("SELFDESTRUCT", 2),
882                ("CALL", 1),
883                ("SELFDESTRUCT", 1)
884            ]
885        );
886        assert_eq!(
887            traces
888                .iter()
889                .filter(|trace| trace.r#type == "SELFDESTRUCT")
890                .map(|trace| trace.from)
891                .collect::<Vec<_>>(),
892            [Address::with_last_byte(2), Address::with_last_byte(1), Address::ZERO]
893        );
894    }
895
896    #[test]
897    fn precompile_calls_remain_in_transaction_traces() {
898        let (result, traces) =
899            execute(bytes!("60006000600060006000600461fffff15000"), SpecId::CANCUN);
900        assert!(result.is_success());
901        assert_eq!(traces.len(), 2);
902        assert_eq!(traces[1].to, Address::with_last_byte(4));
903    }
904
905    #[test]
906    fn contract_creator_ignores_reverted_ancestors() {
907        let contract = Address::repeat_byte(0x11);
908        let creator = Address::repeat_byte(0x22);
909        let localize = |trace, hash| LocalizedTransactionTrace {
910            trace,
911            transaction_hash: Some(hash),
912            block_hash: None,
913            block_number: Some(1),
914            transaction_position: None,
915        };
916        let creation = TransactionTrace {
917            action: Action::Create(CreateAction { from: creator, ..Default::default() }),
918            result: Some(TraceOutput::Create(CreateOutput {
919                address: contract,
920                code: Bytes::new(),
921                gas_used: 0,
922            })),
923            trace_address: vec![0, 0],
924            ..Default::default()
925        };
926        for path in [vec![], vec![0]] {
927            let failed = TransactionTrace {
928                trace_address: path,
929                error: Some("Reverted".into()),
930                ..Default::default()
931            };
932            let reverted =
933                vec![localize(failed, B256::ZERO), localize(creation.clone(), B256::ZERO)];
934            assert_eq!(find_contract_creator(contract, vec![reverted.clone()]).unwrap(), None);
935            let successful = vec![localize(creation.clone(), B256::repeat_byte(1))];
936            assert_eq!(
937                find_contract_creator(contract, vec![reverted, successful]).unwrap(),
938                Some(ContractCreator { creator, hash: B256::repeat_byte(1) })
939            );
940        }
941        // A reverted sibling does not invalidate a subsequent successful creation.
942        let failed = TransactionTrace {
943            trace_address: vec![0],
944            error: Some("Reverted".into()),
945            ..Default::default()
946        };
947        let successful = TransactionTrace { trace_address: vec![1], ..creation };
948        assert!(find_contract_creator(
949            contract,
950            vec![vec![localize(failed, B256::ZERO), localize(successful, B256::ZERO)]]
951        )
952        .unwrap()
953        .is_some());
954    }
955
956    #[test]
957    fn static_and_delegate_calls_have_no_value() {
958        // STATICCALL, DELEGATECALL and CALLCODE to 0x22 do not transfer ETH.
959        let (result, traces) = execute(
960            bytes!("6000600060006000602261fffffa506000600060006000602261fffff45060006000600060006001602261fffff25000"),
961            SpecId::CANCUN,
962        );
963        assert!(result.is_success());
964        assert_eq!(traces.len(), 4);
965        assert_eq!(traces[1].r#type, "STATICCALL");
966        assert_eq!(traces[1].value, None);
967        assert_eq!(traces[2].r#type, "DELEGATECALL");
968        assert_eq!(traces[2].value, None);
969        assert_eq!(traces[3].r#type, "CALLCODE");
970        assert_eq!(traces[3].value, Some(U256::ONE));
971    }
972}