Skip to main content

reth_rpc/
otterscan.rs

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