1use crate::ExecutionOutcome;
4use alloc::{borrow::Cow, collections::BTreeMap, vec::Vec};
5use alloy_consensus::{transaction::Recovered, BlockHeader, TxReceipt};
6use alloy_eips::{eip1898::ForkBlock, eip2718::Encodable2718, BlockNumHash};
7use alloy_primitives::{Address, BlockHash, BlockNumber, Log, TxHash};
8use core::{fmt, ops::RangeInclusive};
9use reth_primitives_traits::{
10 transaction::signed::SignedTransaction, Block, BlockBody, IndexedTx, NodePrimitives,
11 RecoveredBlock, SealedHeader,
12};
13use reth_trie_common::LazyTrieData;
14
15#[derive(Clone, Debug, PartialEq, Eq)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub struct Chain<N: NodePrimitives = reth_ethereum_primitives::EthPrimitives> {
28 blocks: BTreeMap<BlockNumber, RecoveredBlock<N::Block>>,
30 execution_outcome: ExecutionOutcome<N::Receipt>,
37 trie_data: BTreeMap<BlockNumber, LazyTrieData>,
41}
42
43type ChainTxReceiptMeta<'a, N> = (
44 &'a RecoveredBlock<<N as NodePrimitives>::Block>,
45 IndexedTx<'a, <N as NodePrimitives>::Block>,
46 &'a <N as NodePrimitives>::Receipt,
47 &'a [<N as NodePrimitives>::Receipt],
48);
49
50impl<N: NodePrimitives> Default for Chain<N> {
51 fn default() -> Self {
52 Self {
53 blocks: Default::default(),
54 execution_outcome: Default::default(),
55 trie_data: Default::default(),
56 }
57 }
58}
59
60impl<N: NodePrimitives> Chain<N> {
61 pub fn new(
67 blocks: impl IntoIterator<Item = RecoveredBlock<N::Block>>,
68 execution_outcome: ExecutionOutcome<N::Receipt>,
69 trie_data: BTreeMap<BlockNumber, LazyTrieData>,
70 ) -> Self {
71 let blocks =
72 blocks.into_iter().map(|b| (b.header().number(), b)).collect::<BTreeMap<_, _>>();
73 debug_assert!(!blocks.is_empty(), "Chain should have at least one block");
74
75 Self { blocks, execution_outcome, trie_data }
76 }
77
78 pub fn from_block(
80 block: RecoveredBlock<N::Block>,
81 execution_outcome: ExecutionOutcome<N::Receipt>,
82 trie_data: LazyTrieData,
83 ) -> Self {
84 let block_number = block.header().number();
85 Self::new([block], execution_outcome, BTreeMap::from([(block_number, trie_data)]))
86 }
87
88 pub const fn blocks(&self) -> &BTreeMap<BlockNumber, RecoveredBlock<N::Block>> {
90 &self.blocks
91 }
92
93 pub fn into_blocks(self) -> BTreeMap<BlockNumber, RecoveredBlock<N::Block>> {
95 self.blocks
96 }
97
98 pub fn headers(&self) -> impl Iterator<Item = SealedHeader<N::BlockHeader>> + '_ {
100 self.blocks.values().map(|block| block.clone_sealed_header())
101 }
102
103 pub const fn trie_data(&self) -> &BTreeMap<BlockNumber, LazyTrieData> {
105 &self.trie_data
106 }
107
108 pub fn trie_data_at(&self, block_number: BlockNumber) -> Option<&LazyTrieData> {
110 self.trie_data.get(&block_number)
111 }
112
113 pub fn clear_trie_data(&mut self) {
115 self.trie_data.clear();
116 }
117
118 pub const fn execution_outcome(&self) -> &ExecutionOutcome<N::Receipt> {
120 &self.execution_outcome
121 }
122
123 pub const fn execution_outcome_mut(&mut self) -> &mut ExecutionOutcome<N::Receipt> {
125 &mut self.execution_outcome
126 }
127
128 pub fn is_empty(&self) -> bool {
130 self.blocks.is_empty()
131 }
132
133 pub fn block_number(&self, block_hash: BlockHash) -> Option<BlockNumber> {
135 self.blocks.iter().find_map(|(num, block)| (block.hash() == block_hash).then_some(*num))
136 }
137
138 pub fn recovered_block(&self, block_hash: BlockHash) -> Option<&RecoveredBlock<N::Block>> {
140 self.blocks.iter().find_map(|(_num, block)| (block.hash() == block_hash).then_some(block))
141 }
142
143 pub fn execution_outcome_at_block(
145 &self,
146 block_number: BlockNumber,
147 ) -> Option<ExecutionOutcome<N::Receipt>> {
148 if self.tip().number() == block_number {
149 return Some(self.execution_outcome.clone())
150 }
151
152 if self.blocks.contains_key(&block_number) {
153 let mut execution_outcome = self.execution_outcome.clone();
154 execution_outcome.revert_to(block_number);
155 return Some(execution_outcome)
156 }
157 None
158 }
159
160 #[allow(clippy::type_complexity)]
165 pub fn into_inner(
166 self,
167 ) -> (
168 ChainBlocks<'static, N::Block>,
169 ExecutionOutcome<N::Receipt>,
170 BTreeMap<BlockNumber, LazyTrieData>,
171 ) {
172 (ChainBlocks { blocks: Cow::Owned(self.blocks) }, self.execution_outcome, self.trie_data)
173 }
174
175 pub const fn inner(&self) -> (ChainBlocks<'_, N::Block>, &ExecutionOutcome<N::Receipt>) {
179 (ChainBlocks { blocks: Cow::Borrowed(&self.blocks) }, &self.execution_outcome)
180 }
181
182 pub fn block_receipts_iter(&self) -> impl Iterator<Item = &Vec<N::Receipt>> + '_ {
184 self.execution_outcome.receipts().iter()
185 }
186
187 pub fn receipts_iter(&self) -> impl Iterator<Item = &N::Receipt> + '_ {
189 self.block_receipts_iter().flatten()
190 }
191
192 pub fn logs_iter(&self) -> impl Iterator<Item = &Log> + '_
194 where
195 N::Receipt: TxReceipt<Log = Log>,
196 {
197 self.receipts_iter().flat_map(|receipt| receipt.logs())
198 }
199
200 pub fn blocks_iter(&self) -> impl Iterator<Item = &RecoveredBlock<N::Block>> + '_ {
202 self.blocks().iter().map(|block| block.1)
203 }
204
205 pub fn blocks_and_receipts(
207 &self,
208 ) -> impl Iterator<Item = (&RecoveredBlock<N::Block>, &Vec<N::Receipt>)> + '_ {
209 self.blocks_iter().zip(self.block_receipts_iter())
210 }
211
212 pub fn find_transaction_and_receipt_by_hash(
216 &self,
217 tx_hash: TxHash,
218 ) -> Option<ChainTxReceiptMeta<'_, N>> {
219 for (block, receipts) in self.blocks_and_receipts() {
220 let Some(indexed_tx) = block.find_indexed(tx_hash) else {
221 continue;
222 };
223 let receipt = receipts.get(indexed_tx.index())?;
224 return Some((block, indexed_tx, receipt, receipts.as_slice()));
225 }
226
227 None
228 }
229
230 pub fn fork_block(&self) -> ForkBlock {
232 let first = self.first();
233 ForkBlock {
234 number: first.header().number().saturating_sub(1),
235 hash: first.header().parent_hash(),
236 }
237 }
238
239 #[track_caller]
245 pub fn first(&self) -> &RecoveredBlock<N::Block> {
246 self.blocks.first_key_value().expect("Chain should have at least one block").1
247 }
248
249 #[track_caller]
255 pub fn tip(&self) -> &RecoveredBlock<N::Block> {
256 self.blocks.last_key_value().expect("Chain should have at least one block").1
257 }
258
259 pub fn len(&self) -> usize {
261 self.blocks.len()
262 }
263
264 pub fn range(&self) -> RangeInclusive<BlockNumber> {
270 self.first().header().number()..=self.tip().header().number()
271 }
272
273 pub fn receipts_by_block_hash(&self, block_hash: BlockHash) -> Option<Vec<&N::Receipt>> {
275 let num = self.block_number(block_hash)?;
276 Some(self.execution_outcome.receipts_by_block(num).iter().collect())
277 }
278
279 pub fn receipts_with_attachment(&self) -> Vec<BlockReceipts<N::Receipt>>
283 where
284 N::SignedTx: Encodable2718,
285 {
286 let mut receipt_attach = Vec::with_capacity(self.blocks().len());
287
288 self.blocks_and_receipts().for_each(|(block, receipts)| {
289 let block_num_hash = BlockNumHash::new(block.number(), block.hash());
290
291 let tx_receipts = block
292 .body()
293 .transactions()
294 .iter()
295 .zip(receipts)
296 .map(|(tx, receipt)| (tx.trie_hash(), receipt.clone()))
297 .collect();
298
299 receipt_attach.push(BlockReceipts {
300 block: block_num_hash,
301 tx_receipts,
302 timestamp: block.timestamp(),
303 });
304 });
305
306 receipt_attach
307 }
308
309 pub fn append_block(
312 &mut self,
313 block: RecoveredBlock<N::Block>,
314 execution_outcome: ExecutionOutcome<N::Receipt>,
315 trie_data: LazyTrieData,
316 ) {
317 let block_number = block.header().number();
318 self.blocks.insert(block_number, block);
319 self.execution_outcome.extend(execution_outcome);
320 self.trie_data.insert(block_number, trie_data);
321 }
322
323 pub fn append_chain(&mut self, other: Self) -> Result<(), Self> {
330 let chain_tip = self.tip();
331 let other_fork_block = other.fork_block();
332 if chain_tip.hash() != other_fork_block.hash {
333 return Err(other)
334 }
335
336 self.blocks.extend(other.blocks);
338 self.execution_outcome.extend(other.execution_outcome);
339 self.trie_data.extend(other.trie_data);
340
341 Ok(())
342 }
343}
344
345#[derive(Debug)]
347pub struct DisplayBlocksChain<'a, B: reth_primitives_traits::Block>(
348 pub &'a BTreeMap<BlockNumber, RecoveredBlock<B>>,
349);
350
351impl<B: reth_primitives_traits::Block> fmt::Display for DisplayBlocksChain<'_, B> {
352 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353 let mut list = f.debug_list();
354 let mut values = self.0.values().map(|block| block.num_hash());
355 if values.len() <= 3 {
356 list.entries(values);
357 } else {
358 list.entry(&values.next().unwrap());
359 list.entry(&format_args!("..."));
360 list.entry(&values.next_back().unwrap());
361 }
362 list.finish()
363 }
364}
365
366#[derive(Clone, Debug, Default, PartialEq, Eq)]
368pub struct ChainBlocks<'a, B: Block> {
369 blocks: Cow<'a, BTreeMap<BlockNumber, RecoveredBlock<B>>>,
370}
371
372impl<B: Block<Body: BlockBody<Transaction: SignedTransaction>>> ChainBlocks<'_, B> {
373 #[inline]
377 pub fn into_blocks(self) -> impl Iterator<Item = RecoveredBlock<B>> {
378 self.blocks.into_owned().into_values()
379 }
380
381 #[inline]
383 pub fn iter(&self) -> impl Iterator<Item = (&BlockNumber, &RecoveredBlock<B>)> {
384 self.blocks.iter()
385 }
386
387 #[inline]
393 pub fn tip(&self) -> &RecoveredBlock<B> {
394 self.blocks.last_key_value().expect("Chain should have at least one block").1
395 }
396
397 #[inline]
403 pub fn first(&self) -> &RecoveredBlock<B> {
404 self.blocks.first_key_value().expect("Chain should have at least one block").1
405 }
406
407 #[inline]
409 pub fn transactions(&self) -> impl Iterator<Item = &<B::Body as BlockBody>::Transaction> + '_ {
410 self.blocks.values().flat_map(|block| block.body().transactions_iter())
411 }
412
413 #[inline]
415 pub fn transactions_with_sender(
416 &self,
417 ) -> impl Iterator<Item = (&Address, &<B::Body as BlockBody>::Transaction)> + '_ {
418 self.blocks.values().flat_map(|block| block.transactions_with_sender())
419 }
420
421 #[inline]
425 pub fn transactions_ecrecovered(
426 &self,
427 ) -> impl Iterator<Item = Recovered<<B::Body as BlockBody>::Transaction>> + '_ {
428 self.transactions_with_sender().map(|(signer, tx)| tx.clone().with_signer(*signer))
429 }
430
431 #[inline]
433 pub fn transaction_hashes(&self) -> impl Iterator<Item = TxHash> + '_ {
434 self.blocks
435 .values()
436 .flat_map(|block| block.body().transactions_iter().map(|tx| tx.trie_hash()))
437 }
438}
439
440impl<B: Block> IntoIterator for ChainBlocks<'_, B> {
441 type Item = (BlockNumber, RecoveredBlock<B>);
442 type IntoIter = alloc::collections::btree_map::IntoIter<BlockNumber, RecoveredBlock<B>>;
443
444 fn into_iter(self) -> Self::IntoIter {
445 self.blocks.into_owned().into_iter()
446 }
447}
448
449#[derive(Default, Clone, Debug, PartialEq, Eq)]
451pub struct BlockReceipts<T = reth_ethereum_primitives::Receipt> {
452 pub block: BlockNumHash,
454 pub tx_receipts: Vec<(TxHash, T)>,
456 pub timestamp: u64,
458}
459
460#[cfg(feature = "serde-bincode-compat")]
462pub(super) mod serde_bincode_compat {
463 use crate::{serde_bincode_compat, ExecutionOutcome};
464 use alloc::{borrow::Cow, collections::BTreeMap, sync::Arc};
465 use alloy_primitives::BlockNumber;
466 use reth_ethereum_primitives::EthPrimitives;
467 use reth_primitives_traits::{
468 serde_bincode_compat::{RecoveredBlock, SerdeBincodeCompat},
469 Block, NodePrimitives,
470 };
471 use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
472 use serde_with::{DeserializeAs, SerializeAs};
473
474 #[derive(Debug, Serialize, Deserialize)]
490 #[serde(bound = "")]
491 pub struct Chain<'a, N = EthPrimitives>
492 where
493 N: NodePrimitives<
494 Block: Block<Header: SerdeBincodeCompat, Body: SerdeBincodeCompat> + 'static,
495 >,
496 {
497 blocks: RecoveredBlocks<'a, N::Block>,
498 execution_outcome: serde_bincode_compat::ExecutionOutcome<'a, N::Receipt>,
499 #[serde(default, rename = "trie_updates_legacy")]
500 _trie_updates_legacy:
501 Option<reth_trie_common::serde_bincode_compat::updates::TrieUpdates<'a>>,
502 #[serde(default)]
503 trie_updates: BTreeMap<
504 BlockNumber,
505 reth_trie_common::serde_bincode_compat::updates::TrieUpdatesSorted<'a>,
506 >,
507 #[serde(default)]
508 hashed_state: BTreeMap<
509 BlockNumber,
510 reth_trie_common::serde_bincode_compat::hashed_state::HashedPostStateSorted<'a>,
511 >,
512 }
513
514 #[derive(Debug)]
515 struct RecoveredBlocks<
516 'a,
517 B: reth_primitives_traits::Block<Header: SerdeBincodeCompat, Body: SerdeBincodeCompat>
518 + 'static,
519 >(Cow<'a, BTreeMap<BlockNumber, reth_primitives_traits::RecoveredBlock<B>>>);
520
521 impl<B> Serialize for RecoveredBlocks<'_, B>
522 where
523 B: Block<Header: SerdeBincodeCompat, Body: SerdeBincodeCompat> + 'static,
524 {
525 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
526 where
527 S: Serializer,
528 {
529 let mut state = serializer.serialize_map(Some(self.0.len()))?;
530
531 for (block_number, block) in self.0.iter() {
532 state.serialize_entry(block_number, &RecoveredBlock::<'_, B>::from(block))?;
533 }
534
535 state.end()
536 }
537 }
538
539 impl<'de, B> Deserialize<'de> for RecoveredBlocks<'_, B>
540 where
541 B: Block<Header: SerdeBincodeCompat, Body: SerdeBincodeCompat> + 'static,
542 {
543 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
544 where
545 D: Deserializer<'de>,
546 {
547 Ok(Self(Cow::Owned(
548 BTreeMap::<BlockNumber, RecoveredBlock<'_, B>>::deserialize(deserializer)
549 .map(|blocks| blocks.into_iter().map(|(n, b)| (n, b.into())).collect())?,
550 )))
551 }
552 }
553
554 impl<'a, N> From<&'a super::Chain<N>> for Chain<'a, N>
555 where
556 N: NodePrimitives<
557 Block: Block<Header: SerdeBincodeCompat, Body: SerdeBincodeCompat> + 'static,
558 >,
559 {
560 fn from(value: &'a super::Chain<N>) -> Self {
561 Self {
562 blocks: RecoveredBlocks(Cow::Borrowed(&value.blocks)),
563 execution_outcome: value.execution_outcome.as_repr(),
564 _trie_updates_legacy: None,
565 trie_updates: value
566 .trie_data
567 .iter()
568 .map(|(k, v)| (*k, v.get().trie_updates.as_ref().into()))
569 .collect(),
570 hashed_state: value
571 .trie_data
572 .iter()
573 .map(|(k, v)| (*k, v.get().hashed_state.as_ref().into()))
574 .collect(),
575 }
576 }
577 }
578
579 impl<'a, N> From<Chain<'a, N>> for super::Chain<N>
580 where
581 N: NodePrimitives<
582 Block: Block<Header: SerdeBincodeCompat, Body: SerdeBincodeCompat> + 'static,
583 >,
584 {
585 fn from(value: Chain<'a, N>) -> Self {
586 use reth_trie_common::LazyTrieData;
587
588 let hashed_state_map: BTreeMap<_, _> =
589 value.hashed_state.into_iter().map(|(k, v)| (k, Arc::new(v.into()))).collect();
590
591 let trie_data: BTreeMap<BlockNumber, LazyTrieData> = value
592 .trie_updates
593 .into_iter()
594 .map(|(k, v)| {
595 let hashed_state = hashed_state_map.get(&k).cloned().unwrap_or_default();
596 (k, LazyTrieData::ready(hashed_state, Arc::new(v.into())))
597 })
598 .collect();
599
600 Self {
601 blocks: value.blocks.0.into_owned(),
602 execution_outcome: ExecutionOutcome::from_repr(value.execution_outcome),
603 trie_data,
604 }
605 }
606 }
607
608 impl<N> SerializeAs<super::Chain<N>> for Chain<'_, N>
609 where
610 N: NodePrimitives<
611 Block: Block<Header: SerdeBincodeCompat, Body: SerdeBincodeCompat> + 'static,
612 >,
613 {
614 fn serialize_as<S>(source: &super::Chain<N>, serializer: S) -> Result<S::Ok, S::Error>
615 where
616 S: Serializer,
617 {
618 Chain::from(source).serialize(serializer)
619 }
620 }
621
622 impl<'de, N> DeserializeAs<'de, super::Chain<N>> for Chain<'de, N>
623 where
624 N: NodePrimitives<
625 Block: Block<Header: SerdeBincodeCompat, Body: SerdeBincodeCompat> + 'static,
626 >,
627 {
628 fn deserialize_as<D>(deserializer: D) -> Result<super::Chain<N>, D::Error>
629 where
630 D: Deserializer<'de>,
631 {
632 Chain::deserialize(deserializer).map(Into::into)
633 }
634 }
635
636 #[cfg(test)]
637 mod tests {
638 use super::super::{serde_bincode_compat, Chain};
639 use arbitrary::Arbitrary;
640 use rand::Rng;
641 use reth_primitives_traits::RecoveredBlock;
642 use serde::{Deserialize, Serialize};
643 use serde_with::serde_as;
644
645 #[test]
646 fn test_chain_bincode_roundtrip() {
647 use alloc::collections::BTreeMap;
648
649 #[serde_as]
650 #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
651 struct Data {
652 #[serde_as(as = "serde_bincode_compat::Chain")]
653 chain: Chain,
654 }
655
656 let mut bytes = [0u8; 1024];
657 rand::rng().fill(bytes.as_mut_slice());
658 let data = Data {
659 chain: Chain::new(
660 vec![RecoveredBlock::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
661 .unwrap()],
662 Default::default(),
663 BTreeMap::new(),
664 ),
665 };
666
667 let encoded = bincode::serialize(&data).unwrap();
668 let decoded: Data = bincode::deserialize(&encoded).unwrap();
669 assert_eq!(decoded, data);
670 }
671 }
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677 use alloy_consensus::TxType;
678 use alloy_primitives::{Address, B256};
679 use reth_ethereum_primitives::Receipt;
680 use revm::{database::BundleState, primitives::HashMap, state::AccountInfo};
681
682 #[test]
683 fn chain_append() {
684 let block: RecoveredBlock<reth_ethereum_primitives::Block> = Default::default();
685 let block1_hash = B256::new([0x01; 32]);
686 let block2_hash = B256::new([0x02; 32]);
687 let block3_hash = B256::new([0x03; 32]);
688 let block4_hash = B256::new([0x04; 32]);
689
690 let mut block1 = block.clone();
691 let mut block2 = block.clone();
692 let mut block3 = block.clone();
693 let mut block4 = block;
694
695 block1.set_hash(block1_hash);
696 block2.set_hash(block2_hash);
697 block3.set_hash(block3_hash);
698 block4.set_hash(block4_hash);
699
700 block3.set_parent_hash(block2_hash);
701
702 let mut chain1: Chain =
703 Chain { blocks: BTreeMap::from([(1, block1), (2, block2)]), ..Default::default() };
704
705 let chain2 =
706 Chain { blocks: BTreeMap::from([(3, block3), (4, block4)]), ..Default::default() };
707
708 assert!(chain1.append_chain(chain2.clone()).is_ok());
709
710 assert!(chain1.append_chain(chain2).is_err());
712 }
713
714 #[test]
715 fn test_number_split() {
716 let execution_outcome1: ExecutionOutcome = ExecutionOutcome::new(
717 BundleState::new(
718 vec![(
719 Address::new([2; 20]),
720 None,
721 Some(AccountInfo::default()),
722 HashMap::default(),
723 )],
724 vec![vec![(Address::new([2; 20]), None, vec![])]],
725 vec![],
726 ),
727 vec![vec![]],
728 1,
729 vec![],
730 );
731
732 let execution_outcome2 = ExecutionOutcome::new(
733 BundleState::new(
734 vec![(
735 Address::new([3; 20]),
736 None,
737 Some(AccountInfo::default()),
738 HashMap::default(),
739 )],
740 vec![vec![(Address::new([3; 20]), None, vec![])]],
741 vec![],
742 ),
743 vec![vec![]],
744 2,
745 vec![],
746 );
747
748 let mut block1: RecoveredBlock<reth_ethereum_primitives::Block> = Default::default();
749 let block1_hash = B256::new([15; 32]);
750 block1.set_block_number(1);
751 block1.set_hash(block1_hash);
752 block1.push_sender(Address::new([4; 20]));
753
754 let mut block2: RecoveredBlock<reth_ethereum_primitives::Block> = Default::default();
755 let block2_hash = B256::new([16; 32]);
756 block2.set_block_number(2);
757 block2.set_hash(block2_hash);
758 block2.push_sender(Address::new([4; 20]));
759
760 let mut block_state_extended = execution_outcome1;
761 block_state_extended.extend(execution_outcome2);
762
763 let chain: Chain =
764 Chain::new(vec![block1.clone(), block2.clone()], block_state_extended, BTreeMap::new());
765
766 assert_eq!(
768 chain.execution_outcome_at_block(block2.number),
769 Some(chain.execution_outcome.clone())
770 );
771 assert_eq!(chain.execution_outcome_at_block(100), None);
773 }
774
775 #[test]
776 fn receipts_by_block_hash() {
777 let block: RecoveredBlock<reth_ethereum_primitives::Block> = Default::default();
779
780 let block1_hash = B256::new([0x01; 32]);
782 let block2_hash = B256::new([0x02; 32]);
783
784 let mut block1 = block.clone();
786 let mut block2 = block;
787
788 block1.set_hash(block1_hash);
790 block2.set_hash(block2_hash);
791
792 let receipt1 = Receipt {
794 tx_type: TxType::Legacy,
795 cumulative_gas_used: 46913,
796 logs: vec![],
797 success: true,
798 };
799
800 let receipt2 = Receipt {
802 tx_type: TxType::Legacy,
803 cumulative_gas_used: 1325345,
804 logs: vec![],
805 success: true,
806 };
807
808 let receipts = vec![vec![receipt1.clone()], vec![receipt2]];
810
811 let execution_outcome = ExecutionOutcome {
814 bundle: Default::default(),
815 receipts,
816 requests: vec![],
817 first_block: 10,
818 };
819
820 let chain: Chain = Chain {
823 blocks: BTreeMap::from([(10, block1), (11, block2)]),
824 execution_outcome: execution_outcome.clone(),
825 ..Default::default()
826 };
827
828 assert_eq!(chain.receipts_by_block_hash(block1_hash), Some(vec![&receipt1]));
830
831 let execution_outcome1 = ExecutionOutcome {
833 bundle: Default::default(),
834 receipts: vec![vec![receipt1]],
835 requests: vec![],
836 first_block: 10,
837 };
838
839 assert_eq!(chain.execution_outcome_at_block(10), Some(execution_outcome1));
841
842 assert_eq!(chain.execution_outcome_at_block(11), Some(execution_outcome));
844 }
845}