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 FullEthApiTypes, 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#[derive(Debug)]
40pub struct OtterscanApi<Eth> {
41 eth: Eth,
42}
43
44impl<Eth> OtterscanApi<Eth> {
45 pub const fn new(eth: Eth) -> Self {
47 Self { eth }
48 }
49}
50
51impl<Eth> OtterscanApi<Eth>
52where
53 Eth: FullEthApiTypes + TraceExt,
54{
55 async fn block_details(
57 &self,
58 block: RpcBlock<Eth::NetworkTypes>,
59 receipts: Vec<RpcReceipt<Eth::NetworkTypes>>,
60 ) -> RpcResult<BlockDetails<RpcHeader<Eth::NetworkTypes>>> {
61 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 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 + 'static,
122{
123 async fn get_header_by_number(
125 &self,
126 block_number: LenientBlockNumberOrTag,
127 ) -> RpcResult<Option<RpcHeader<Eth::NetworkTypes>>> {
128 self.eth.header_by_number(block_number.into()).await
129 }
130
131 async fn has_code(&self, address: Address, block_id: Option<BlockId>) -> RpcResult<bool> {
133 EthApiServer::get_code(&self.eth, address, block_id).await.map(|code| !code.is_empty())
134 }
135
136 async fn get_api_level(&self) -> RpcResult<u64> {
138 Ok(API_LEVEL)
139 }
140
141 async fn get_internal_operations(&self, tx_hash: TxHash) -> RpcResult<Vec<InternalOperation>> {
143 let permit = self.acquire_trace_permit().await?;
144 self.eth
145 .spawn_trace_transaction_in_block_with_inspector(
146 tx_hash,
147 InternalOperationsInspector::default(),
148 move |_tx_info, inspector, _, _| {
149 let _permit = permit;
150 Ok(inspector.into_operations())
151 },
152 )
153 .await
154 .map_err(Into::into)
155 .map(Option::unwrap_or_default)
156 }
157
158 async fn get_transaction_error(&self, tx_hash: TxHash) -> RpcResult<Option<Bytes>> {
160 let permit = self.acquire_trace_permit().await?;
161 self.eth
162 .spawn_replay_transaction(tx_hash, move |_tx_info, res, _| {
163 let _permit = permit;
164 Ok(transaction_error(res.result))
165 })
166 .await
167 .map_err(Into::into)
168 }
169
170 async fn trace_transaction(&self, tx_hash: TxHash) -> RpcResult<Option<Vec<TraceEntry>>> {
172 let permit = self.acquire_trace_permit().await?;
173 self.eth
174 .spawn_trace_transaction_in_block(
175 tx_hash,
176 TracingInspectorConfig::default_parity(),
177 move |_tx_info, inspector, _, _| {
178 let _permit = permit;
179 Ok(otterscan_traces(inspector.into_traces().into_nodes()))
180 },
181 )
182 .await
183 .map_err(Into::into)
184 }
185
186 async fn get_block_details(
188 &self,
189 block_number: LenientBlockNumberOrTag,
190 ) -> RpcResult<BlockDetails<RpcHeader<Eth::NetworkTypes>>> {
191 let block_number = block_number.into_inner();
192 let block = self.eth.block_by_number(block_number, false);
193 let block_id = block_number.into();
194 let receipts = self.eth.block_receipts(block_id);
195 let (block, receipts) = futures::try_join!(block, receipts)?;
196 self.block_details(
197 block.ok_or(EthApiError::HeaderNotFound(block_id))?,
198 receipts.ok_or(EthApiError::ReceiptsNotFound(block_id))?,
199 )
200 .await
201 }
202
203 async fn get_block_details_by_hash(
205 &self,
206 block_hash: B256,
207 ) -> RpcResult<BlockDetails<RpcHeader<Eth::NetworkTypes>>> {
208 let block = self.eth.block_by_hash(block_hash, false);
209 let block_id = block_hash.into();
210 let receipts = self.eth.block_receipts(block_id);
211 let (block, receipts) = futures::try_join!(block, receipts)?;
212 self.block_details(
213 block.ok_or(EthApiError::HeaderNotFound(block_id))?,
214 receipts.ok_or(EthApiError::ReceiptsNotFound(block_id))?,
215 )
216 .await
217 }
218
219 async fn get_block_transactions(
221 &self,
222 block_number: LenientBlockNumberOrTag,
223 page_number: usize,
224 page_size: usize,
225 ) -> RpcResult<
226 OtsBlockTransactions<RpcTransaction<Eth::NetworkTypes>, RpcHeader<Eth::NetworkTypes>>,
227 > {
228 let block_number = block_number.into_inner();
229 let block = self.eth.block_by_number(block_number, true);
231 let block_id = block_number.into();
232 let receipts = self.eth.block_receipts(block_id);
233 let (block, receipts) = futures::try_join!(block, receipts)?;
234
235 let mut block = block.ok_or(EthApiError::HeaderNotFound(block_id))?;
236 let mut receipts = receipts.ok_or(EthApiError::ReceiptsNotFound(block_id))?;
237
238 let tx_len = block.transactions.len();
240 if tx_len != receipts.len() {
241 return Err(internal_rpc_err(
242 "the number of transactions does not match the number of receipts",
243 ))
244 }
245
246 let BlockTransactions::Full(transactions) = &mut block.transactions else {
248 return Err(internal_rpc_err("block is not full"));
249 };
250
251 let page = block_transaction_page_range(tx_len, page_number, page_size);
252
253 *transactions = transactions.drain(page.clone()).collect::<Vec<_>>();
255
256 let timestamp = Some(block.header.timestamp());
258 let receipts = receipts
259 .drain(page)
260 .zip(transactions.iter().map(Typed2718::ty))
261 .map(|(receipt, tx_ty)| {
262 let inner = OtsReceipt {
263 status: receipt.status(),
264 cumulative_gas_used: receipt.cumulative_gas_used(),
265 logs: None,
266 logs_bloom: None,
267 r#type: tx_ty,
268 };
269
270 let receipt = TransactionReceipt {
271 inner,
272 transaction_hash: receipt.transaction_hash(),
273 transaction_index: receipt.transaction_index(),
274 block_hash: receipt.block_hash(),
275 block_number: receipt.block_number(),
276 gas_used: receipt.gas_used(),
277 effective_gas_price: receipt.effective_gas_price(),
278 blob_gas_used: receipt.blob_gas_used(),
279 blob_gas_price: receipt.blob_gas_price(),
280 from: receipt.from(),
281 to: receipt.to(),
282 contract_address: receipt.contract_address(),
283 };
284
285 OtsTransactionReceipt { receipt, timestamp }
286 })
287 .collect();
288
289 let mut block = OtsBlockTransactions { fullblock: block.into(), receipts };
291 block.fullblock.transaction_count = tx_len;
292 Ok(block)
293 }
294
295 async fn search_transactions_before(
297 &self,
298 _address: Address,
299 _block_number: LenientBlockNumberOrTag,
300 _page_size: usize,
301 ) -> RpcResult<TransactionsWithReceipts> {
302 Err(internal_rpc_err("unimplemented"))
303 }
304
305 async fn search_transactions_after(
307 &self,
308 _address: Address,
309 _block_number: LenientBlockNumberOrTag,
310 _page_size: usize,
311 ) -> RpcResult<TransactionsWithReceipts> {
312 Err(internal_rpc_err("unimplemented"))
313 }
314
315 async fn get_transaction_by_sender_and_nonce(
317 &self,
318 sender: Address,
319 nonce: u64,
320 ) -> RpcResult<Option<TxHash>> {
321 Ok(self
322 .eth
323 .get_transaction_by_sender_and_nonce(sender, nonce, false)
324 .await
325 .map_err(Into::into)?
326 .map(|tx| tx.tx_hash()))
327 }
328
329 async fn get_contract_creator(&self, address: Address) -> RpcResult<Option<ContractCreator>> {
331 if !self.has_code(address, None).await? {
332 return Ok(None);
333 }
334
335 let permit = self.acquire_trace_permit().await?;
336 let num = binary_search::<_, _, ErrorObjectOwned>(
337 1,
338 self.eth.block_number()?.saturating_to(),
339 |mid| {
340 Box::pin(async move {
341 Ok(!EthApiServer::get_code(&self.eth, address, Some(mid.into()))
342 .await?
343 .is_empty())
344 })
345 },
346 )
347 .await?;
348
349 let traces = self
350 .eth
351 .trace_block_with(
352 num.into(),
353 None,
354 TracingInspectorConfig::default_parity(),
355 move |tx_info, mut ctx| {
356 let _permit = &permit;
357 Ok(ctx
358 .take_inspector()
359 .into_parity_builder()
360 .into_localized_transaction_traces(tx_info))
361 },
362 )
363 .await
364 .map_err(Into::into)?
365 .map(|traces| find_contract_creator(address, traces))
366 .transpose()?;
367
368 Ok(traces.flatten())
371 }
372}
373
374fn find_contract_creator(
376 address: Address,
377 transactions: Vec<Vec<LocalizedTransactionTrace>>,
378) -> Result<Option<ContractCreator>, EthApiError> {
379 for traces in transactions {
380 let mut reverted_path: Option<Vec<usize>> = None;
381 for tx_trace in traces {
382 let trace = tx_trace.trace;
383 if reverted_path.as_ref().is_some_and(|path| trace.trace_address.starts_with(path)) {
385 continue
386 }
387 reverted_path = None;
388 if trace.error.is_some() {
389 reverted_path = Some(trace.trace_address);
390 continue
391 }
392 if let (
393 Action::Create(CreateAction { from: creator, .. }),
394 Some(TraceOutput::Create(CreateOutput { address: contract, .. })),
395 ) = (trace.action, trace.result) &&
396 contract == address
397 {
398 return Ok(Some(ContractCreator {
399 hash: tx_trace.transaction_hash.ok_or(EthApiError::TransactionNotFound)?,
400 creator,
401 }))
402 }
403 }
404 }
405 Ok(None)
406}
407
408const fn block_transaction_page_range(
413 tx_len: usize,
414 page_number: usize,
415 page_size: usize,
416) -> std::ops::Range<usize> {
417 let page_end = tx_len.saturating_sub(page_number.saturating_mul(page_size));
418 let page_start = page_end.saturating_sub(page_size);
419 page_start..page_end
420}
421
422fn calculate_issuance(
424 reward: u128,
425 number: u64,
426 ommer_numbers: impl ExactSizeIterator<Item = u64>,
427) -> InternalIssuance {
428 let block_reward = U256::from(block_reward(reward, ommer_numbers.len()));
429 let uncle_reward =
430 ommer_numbers.map(|ommer| U256::from(ommer_reward(reward, number, ommer))).sum::<U256>();
431 InternalIssuance { block_reward, uncle_reward, issuance: block_reward + uncle_reward }
432}
433
434fn transaction_error<H>(result: ExecutionResult<H>) -> Bytes {
435 match result {
436 ExecutionResult::Revert { output, .. } => output,
437 _ => Bytes::new(),
438 }
439}
440
441fn otterscan_traces(nodes: Vec<CallTraceNode>) -> Vec<TraceEntry> {
443 let mut entries = Vec::with_capacity(nodes.len());
444 let mut selfdestructs: Vec<TraceEntry> = Vec::new();
445 for CallTraceNode { trace, .. } in nodes {
446 while selfdestructs.last().is_some_and(|entry| entry.depth > trace.depth as u32) {
449 entries.push(selfdestructs.pop().expect("checked above"));
450 }
451 if let Some(to) = trace.selfdestruct_refund_target {
452 selfdestructs.push(TraceEntry {
453 r#type: "SELFDESTRUCT".to_string(),
454 depth: trace.depth as u32 + 1,
455 from: trace.selfdestruct_address.unwrap_or(trace.address),
456 to,
457 value: trace.selfdestruct_transferred_value,
458 input: Bytes::new(),
459 output: Bytes::new(),
460 });
461 }
462 entries.push(TraceEntry {
463 r#type: trace.kind.to_string(),
464 depth: trace.depth as u32,
465 from: trace.caller,
466 to: trace.address,
467 value: (!matches!(trace.kind, CallKind::StaticCall | CallKind::DelegateCall))
468 .then_some(trace.value),
469 input: trace.data,
470 output: trace.output,
471 });
472 }
473 entries.extend(selfdestructs.into_iter().rev());
474 entries
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480 use alloy_consensus::{constants::ETH_TO_WEI, Header};
481 use alloy_primitives::{hex, 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 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 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 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(hex!("6022ff").into(), 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 let (result, traces) =
843 execute(hex!("60006000600060006000603361fffff1506022ff").into(), 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(hex!("60006000600060006000600461fffff15000").into(), 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 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 let (result, traces) = execute(
960 hex!("6000600060006000602261fffffa506000600060006000602261fffff45060006000600060006001602261fffff25000").into(),
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::from(1)));
971 }
972}