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#[derive(Debug)]
39pub struct OtterscanApi<Eth> {
40 eth: Eth,
41}
42
43impl<Eth> OtterscanApi<Eth> {
44 pub const fn new(eth: Eth) -> Self {
46 Self { eth }
47 }
48}
49
50impl<Eth> OtterscanApi<Eth>
51where
52 Eth: TraceExt,
53{
54 async fn block_details(
56 &self,
57 block: RpcBlock<Eth::NetworkTypes>,
58 receipts: Vec<RpcReceipt<Eth::NetworkTypes>>,
59 ) -> RpcResult<BlockDetails<RpcHeader<Eth::NetworkTypes>>> {
60 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 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 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 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 async fn get_api_level(&self) -> RpcResult<u64> {
135 Ok(API_LEVEL)
136 }
137
138 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 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 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 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 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 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 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 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 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 *transactions = transactions.drain(page.clone()).collect::<Vec<_>>();
252
253 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 let mut block = OtsBlockTransactions { fullblock: block.into(), receipts };
288 block.fullblock.transaction_count = tx_len;
289 Ok(block)
290 }
291
292 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 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 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 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 Ok(traces.flatten())
368 }
369}
370
371fn 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 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
405const 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
419fn 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
439fn 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 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 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 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 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 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 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 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}