1use crate::ExecutionOutcome;
4use alloc::{borrow::Cow, collections::BTreeMap, vec::Vec};
5use alloy_consensus::{
6 transaction::{Recovered, TxHashRef},
7 BlockHeader, TxReceipt,
8};
9use alloy_eips::{eip1898::ForkBlock, BlockNumHash};
10use alloy_primitives::{map::HashSet, Address, BlockHash, BlockNumber, Log, TxHash};
11use core::{fmt, ops::RangeInclusive};
12use reth_primitives_traits::{
13 transaction::signed::SignedTransaction, Block, BlockBody, IndexedTx, NodePrimitives,
14 RecoveredBlock, SealedHeader,
15};
16use reth_trie_common::LazyTrieData;
17
18#[derive(Clone, Debug, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct Chain<N: NodePrimitives = reth_ethereum_primitives::EthPrimitives> {
31 blocks: BTreeMap<BlockNumber, RecoveredBlock<N::Block>>,
33 execution_outcome: ExecutionOutcome<N::Receipt>,
40 trie_data: BTreeMap<BlockNumber, LazyTrieData>,
44}
45
46type ChainTxReceiptMeta<'a, N> = (
47 &'a RecoveredBlock<<N as NodePrimitives>::Block>,
48 IndexedTx<'a, <N as NodePrimitives>::Block>,
49 &'a <N as NodePrimitives>::Receipt,
50 &'a [<N as NodePrimitives>::Receipt],
51);
52
53impl<N: NodePrimitives> Default for Chain<N> {
54 fn default() -> Self {
55 Self {
56 blocks: Default::default(),
57 execution_outcome: Default::default(),
58 trie_data: Default::default(),
59 }
60 }
61}
62
63impl<N: NodePrimitives> Chain<N> {
64 pub fn new(
70 blocks: impl IntoIterator<Item = RecoveredBlock<N::Block>>,
71 execution_outcome: ExecutionOutcome<N::Receipt>,
72 trie_data: BTreeMap<BlockNumber, LazyTrieData>,
73 ) -> Self {
74 let blocks =
75 blocks.into_iter().map(|b| (b.header().number(), b)).collect::<BTreeMap<_, _>>();
76 debug_assert!(!blocks.is_empty(), "Chain should have at least one block");
77
78 Self { blocks, execution_outcome, trie_data }
79 }
80
81 pub fn from_block(
83 block: RecoveredBlock<N::Block>,
84 execution_outcome: ExecutionOutcome<N::Receipt>,
85 trie_data: LazyTrieData,
86 ) -> Self {
87 let block_number = block.header().number();
88 Self::new([block], execution_outcome, BTreeMap::from([(block_number, trie_data)]))
89 }
90
91 pub const fn blocks(&self) -> &BTreeMap<BlockNumber, RecoveredBlock<N::Block>> {
93 &self.blocks
94 }
95
96 pub fn into_blocks(self) -> BTreeMap<BlockNumber, RecoveredBlock<N::Block>> {
98 self.blocks
99 }
100
101 pub fn headers(&self) -> impl Iterator<Item = SealedHeader<N::BlockHeader>> + '_ {
103 self.blocks.values().map(|block| block.clone_sealed_header())
104 }
105
106 pub const fn trie_data(&self) -> &BTreeMap<BlockNumber, LazyTrieData> {
108 &self.trie_data
109 }
110
111 pub fn trie_data_at(&self, block_number: BlockNumber) -> Option<&LazyTrieData> {
113 self.trie_data.get(&block_number)
114 }
115
116 pub fn clear_trie_data(&mut self) {
118 self.trie_data.clear();
119 }
120
121 pub const fn execution_outcome(&self) -> &ExecutionOutcome<N::Receipt> {
123 &self.execution_outcome
124 }
125
126 pub const fn execution_outcome_mut(&mut self) -> &mut ExecutionOutcome<N::Receipt> {
128 &mut self.execution_outcome
129 }
130
131 pub fn is_empty(&self) -> bool {
133 self.blocks.is_empty()
134 }
135
136 pub fn block_number(&self, block_hash: BlockHash) -> Option<BlockNumber> {
138 self.blocks.iter().find_map(|(num, block)| (block.hash() == block_hash).then_some(*num))
139 }
140
141 pub fn recovered_block(&self, block_hash: BlockHash) -> Option<&RecoveredBlock<N::Block>> {
143 self.blocks.iter().find_map(|(_num, block)| (block.hash() == block_hash).then_some(block))
144 }
145
146 pub fn execution_outcome_at_block(
148 &self,
149 block_number: BlockNumber,
150 ) -> Option<ExecutionOutcome<N::Receipt>> {
151 if self.tip().number() == block_number {
152 return Some(self.execution_outcome.clone())
153 }
154
155 if self.blocks.contains_key(&block_number) {
156 let mut execution_outcome = self.execution_outcome.clone();
157 execution_outcome.revert_to(block_number);
158 return Some(execution_outcome)
159 }
160 None
161 }
162
163 #[expect(clippy::type_complexity)]
168 pub fn into_inner(
169 self,
170 ) -> (
171 ChainBlocks<'static, N::Block>,
172 ExecutionOutcome<N::Receipt>,
173 BTreeMap<BlockNumber, LazyTrieData>,
174 ) {
175 (ChainBlocks { blocks: Cow::Owned(self.blocks) }, self.execution_outcome, self.trie_data)
176 }
177
178 pub const fn inner(&self) -> (ChainBlocks<'_, N::Block>, &ExecutionOutcome<N::Receipt>) {
182 (ChainBlocks { blocks: Cow::Borrowed(&self.blocks) }, &self.execution_outcome)
183 }
184
185 pub fn block_receipts_iter(&self) -> impl Iterator<Item = &Vec<N::Receipt>> + '_ {
187 self.execution_outcome.receipts().iter()
188 }
189
190 pub fn receipts_iter(&self) -> impl Iterator<Item = &N::Receipt> + '_ {
192 self.block_receipts_iter().flatten()
193 }
194
195 pub fn logs_iter(&self) -> impl Iterator<Item = &Log> + '_
197 where
198 N::Receipt: TxReceipt<Log = Log>,
199 {
200 self.receipts_iter().flat_map(|receipt| receipt.logs())
201 }
202
203 pub fn blocks_iter(&self) -> impl Iterator<Item = &RecoveredBlock<N::Block>> + '_ {
205 self.blocks().iter().map(|block| block.1)
206 }
207
208 pub fn transactions_iter(&self) -> impl Iterator<Item = &N::SignedTx> + '_ {
210 self.blocks_iter().flat_map(|block| block.body().transactions())
211 }
212
213 pub fn transaction_hashes(&self) -> impl Iterator<Item = &TxHash> + '_ {
215 self.transactions_iter().map(|tx| tx.tx_hash())
216 }
217
218 pub fn transactions_recovered_iter(
220 &self,
221 ) -> impl Iterator<Item = Recovered<&N::SignedTx>> + '_ {
222 self.blocks_iter().flat_map(|block| block.transactions_recovered())
223 }
224
225 pub fn blocks_and_receipts(
227 &self,
228 ) -> impl Iterator<Item = (&RecoveredBlock<N::Block>, &Vec<N::Receipt>)> + '_ {
229 self.blocks_iter().zip(self.block_receipts_iter())
230 }
231
232 pub fn find_transaction_and_receipt_by_hash(
236 &self,
237 tx_hash: TxHash,
238 ) -> Option<ChainTxReceiptMeta<'_, N>> {
239 for (block, receipts) in self.blocks_and_receipts() {
240 let Some(indexed_tx) = block.find_indexed(tx_hash) else {
241 continue;
242 };
243 let receipt = receipts.get(indexed_tx.index())?;
244 return Some((block, indexed_tx, receipt, receipts.as_slice()));
245 }
246
247 None
248 }
249
250 pub fn fork_block(&self) -> ForkBlock {
252 let first = self.first();
253 ForkBlock {
254 number: first.header().number().saturating_sub(1),
255 hash: first.header().parent_hash(),
256 }
257 }
258
259 #[track_caller]
265 pub fn first(&self) -> &RecoveredBlock<N::Block> {
266 self.blocks.first_key_value().expect("Chain should have at least one block").1
267 }
268
269 #[track_caller]
275 pub fn tip(&self) -> &RecoveredBlock<N::Block> {
276 self.blocks.last_key_value().expect("Chain should have at least one block").1
277 }
278
279 pub fn len(&self) -> usize {
281 self.blocks.len()
282 }
283
284 pub fn range(&self) -> RangeInclusive<BlockNumber> {
290 self.first().header().number()..=self.tip().header().number()
291 }
292
293 pub fn receipts_by_block_hash(&self, block_hash: BlockHash) -> Option<Vec<&N::Receipt>> {
295 let num = self.block_number(block_hash)?;
296 Some(self.execution_outcome.receipts_by_block(num).iter().collect())
297 }
298
299 pub fn receipts_with_attachment(&self) -> Vec<BlockReceipts<N::Receipt>> {
303 let mut receipt_attach = Vec::with_capacity(self.blocks().len());
304
305 self.blocks_and_receipts().for_each(|(block, receipts)| {
306 let block_num_hash = BlockNumHash::new(block.number(), block.hash());
307
308 let tx_receipts = block
309 .body()
310 .transactions()
311 .iter()
312 .zip(receipts)
313 .map(|(tx, receipt)| (*tx.tx_hash(), receipt.clone()))
314 .collect();
315
316 receipt_attach.push(BlockReceipts {
317 block: block_num_hash,
318 tx_receipts,
319 timestamp: block.timestamp(),
320 });
321 });
322
323 receipt_attach
324 }
325
326 pub fn append_block(
329 &mut self,
330 block: RecoveredBlock<N::Block>,
331 execution_outcome: ExecutionOutcome<N::Receipt>,
332 trie_data: LazyTrieData,
333 ) {
334 let block_number = block.header().number();
335 self.blocks.insert(block_number, block);
336 self.execution_outcome.extend(execution_outcome);
337 self.trie_data.insert(block_number, trie_data);
338 }
339
340 pub fn append_chain(&mut self, other: Self) -> Result<(), Self> {
347 let chain_tip = self.tip();
348 let other_fork_block = other.fork_block();
349 if chain_tip.hash() != other_fork_block.hash {
350 return Err(other)
351 }
352
353 self.blocks.extend(other.blocks);
355 self.execution_outcome.extend(other.execution_outcome);
356 self.trie_data.extend(other.trie_data);
357
358 Ok(())
359 }
360}
361
362#[derive(Debug)]
364pub struct DisplayBlocksChain<'a, B: reth_primitives_traits::Block>(
365 pub &'a BTreeMap<BlockNumber, RecoveredBlock<B>>,
366);
367
368impl<B: reth_primitives_traits::Block> fmt::Display for DisplayBlocksChain<'_, B> {
369 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370 let mut list = f.debug_list();
371 let mut values = self.0.values().map(|block| block.num_hash());
372 if values.len() <= 3 {
373 list.entries(values);
374 } else {
375 list.entry(&values.next().unwrap());
376 list.entry(&format_args!("..."));
377 list.entry(&values.next_back().unwrap());
378 }
379 list.finish()
380 }
381}
382
383#[derive(Clone, Debug, Default, PartialEq, Eq)]
385pub struct ChainBlocks<'a, B: Block> {
386 blocks: Cow<'a, BTreeMap<BlockNumber, RecoveredBlock<B>>>,
387}
388
389impl<B: Block<Body: BlockBody<Transaction: SignedTransaction>>> ChainBlocks<'_, B> {
390 #[inline]
394 pub fn into_blocks(self) -> impl Iterator<Item = RecoveredBlock<B>> {
395 self.blocks.into_owned().into_values()
396 }
397
398 #[inline]
400 pub fn iter(&self) -> impl Iterator<Item = (&BlockNumber, &RecoveredBlock<B>)> {
401 self.blocks.iter()
402 }
403
404 #[inline]
410 pub fn tip(&self) -> &RecoveredBlock<B> {
411 self.blocks.last_key_value().expect("Chain should have at least one block").1
412 }
413
414 #[inline]
420 pub fn first(&self) -> &RecoveredBlock<B> {
421 self.blocks.first_key_value().expect("Chain should have at least one block").1
422 }
423
424 #[inline]
426 pub fn transactions(&self) -> impl Iterator<Item = &<B::Body as BlockBody>::Transaction> + '_ {
427 self.blocks.values().flat_map(|block| block.body().transactions_iter())
428 }
429
430 #[inline]
432 pub fn transactions_with_sender(
433 &self,
434 ) -> impl Iterator<Item = (&Address, &<B::Body as BlockBody>::Transaction)> + '_ {
435 self.blocks.values().flat_map(|block| block.transactions_with_sender())
436 }
437
438 #[inline]
442 pub fn transactions_ecrecovered(
443 &self,
444 ) -> impl Iterator<Item = Recovered<<B::Body as BlockBody>::Transaction>> + '_ {
445 self.transactions_with_sender().map(|(signer, tx)| tx.clone().with_signer(*signer))
446 }
447
448 #[inline]
450 pub fn transaction_hashes(&self) -> impl Iterator<Item = TxHash> + '_ {
451 self.blocks
452 .values()
453 .flat_map(|block| block.body().transactions_iter().map(|tx| *tx.tx_hash()))
454 }
455
456 #[inline]
458 pub fn transaction_hashes_vec(&self) -> Vec<TxHash> {
459 let capacity = self.blocks.values().map(|block| block.body().transactions().len()).sum();
460
461 let mut hashes = Vec::with_capacity(capacity);
462 hashes.extend(self.transaction_hashes());
463 hashes
464 }
465
466 #[inline]
468 pub fn transaction_hashes_set(&self) -> HashSet<TxHash> {
469 let capacity = self.blocks.values().map(|block| block.body().transactions().len()).sum();
470
471 let mut hashes = HashSet::with_capacity_and_hasher(capacity, Default::default());
472 hashes.extend(self.transaction_hashes());
473 hashes
474 }
475}
476
477impl<B: Block> IntoIterator for ChainBlocks<'_, B> {
478 type Item = (BlockNumber, RecoveredBlock<B>);
479 type IntoIter = alloc::collections::btree_map::IntoIter<BlockNumber, RecoveredBlock<B>>;
480
481 fn into_iter(self) -> Self::IntoIter {
482 self.blocks.into_owned().into_iter()
483 }
484}
485
486#[derive(Default, Clone, Debug, PartialEq, Eq)]
488pub struct BlockReceipts<T = reth_ethereum_primitives::Receipt> {
489 pub block: BlockNumHash,
491 pub tx_receipts: Vec<(TxHash, T)>,
493 pub timestamp: u64,
495}
496
497#[cfg(feature = "serde-bincode-compat")]
499pub(super) mod serde_bincode_compat {
500 use crate::serde_bincode_compat;
501 use alloc::{collections::BTreeMap, sync::Arc, vec::Vec};
502 use alloy_primitives::{Address, BlockNumber, Bytes};
503 use alloy_rlp::Decodable;
504 use core::marker::PhantomData;
505 use reth_ethereum_primitives::EthPrimitives;
506 use reth_primitives_traits::{NodePrimitives, SealedBlock};
507 use serde::{Deserialize, Deserializer, Serialize, Serializer};
508 use serde_with::{DeserializeAs, SerializeAs};
509
510 #[derive(Debug, Serialize, Deserialize)]
526 #[serde(bound = "")]
527 pub struct Chain<'a, N = EthPrimitives>
528 where
529 N: NodePrimitives,
530 {
531 #[serde(skip)]
532 _phantom: PhantomData<N>,
533 blocks: BTreeMap<BlockNumber, RecoveredBlockRepr>,
534 execution_outcome: serde_bincode_compat::ExecutionOutcome<'a>,
535 #[serde(default)]
536 trie_updates: BTreeMap<
537 BlockNumber,
538 reth_trie_common::serde_bincode_compat::updates::TrieUpdatesSorted<'a>,
539 >,
540 #[serde(default)]
541 hashed_state: BTreeMap<
542 BlockNumber,
543 reth_trie_common::serde_bincode_compat::hashed_state::HashedPostStateSorted<'a>,
544 >,
545 }
546
547 #[derive(Debug, Serialize, Deserialize)]
548 struct RecoveredBlockRepr {
549 rlp: Bytes,
550 senders: Vec<Address>,
551 }
552
553 impl<'a, N> From<&'a super::Chain<N>> for Chain<'a, N>
554 where
555 N: NodePrimitives,
556 {
557 fn from(value: &'a super::Chain<N>) -> Self {
558 Self {
559 _phantom: PhantomData,
560 blocks: value
561 .blocks
562 .iter()
563 .map(|(num, recovered)| {
564 let senders = recovered.senders().to_vec();
565 let rlp = Bytes::from(alloy_rlp::encode(recovered.sealed_block()));
566 (*num, RecoveredBlockRepr { rlp, senders })
567 })
568 .collect(),
569 execution_outcome: (&value.execution_outcome).into(),
570 trie_updates: value
571 .trie_data
572 .iter()
573 .map(|(k, v)| (*k, v.get().trie_updates.as_ref().into()))
574 .collect(),
575 hashed_state: value
576 .trie_data
577 .iter()
578 .map(|(k, v)| (*k, v.get().hashed_state.as_ref().into()))
579 .collect(),
580 }
581 }
582 }
583
584 impl<'a, N> From<Chain<'a, N>> for super::Chain<N>
585 where
586 N: NodePrimitives,
587 {
588 fn from(value: Chain<'a, N>) -> Self {
589 use reth_primitives_traits::RecoveredBlock;
590 use reth_trie_common::LazyTrieData;
591
592 let hashed_state_map: BTreeMap<_, _> =
593 value.hashed_state.into_iter().map(|(k, v)| (k, Arc::new(v.into()))).collect();
594
595 let trie_data: BTreeMap<BlockNumber, LazyTrieData> = value
596 .trie_updates
597 .into_iter()
598 .map(|(k, v)| {
599 let hashed_state = hashed_state_map.get(&k).cloned().unwrap_or_default();
600 (k, LazyTrieData::ready(hashed_state, Arc::new(v.into())))
601 })
602 .collect();
603
604 let blocks = value
605 .blocks
606 .into_iter()
607 .map(|(num, repr)| {
608 let block = N::Block::decode(&mut repr.rlp.as_ref())
609 .expect("invalid RLP for block in serde_bincode_compat");
610 let sealed = SealedBlock::new_unhashed(block);
611 (num, RecoveredBlock::new_sealed(sealed, repr.senders))
612 })
613 .collect();
614
615 Self { blocks, execution_outcome: value.execution_outcome.into(), trie_data }
616 }
617 }
618
619 impl<N> SerializeAs<super::Chain<N>> for Chain<'_, N>
620 where
621 N: NodePrimitives,
622 {
623 fn serialize_as<S>(source: &super::Chain<N>, serializer: S) -> Result<S::Ok, S::Error>
624 where
625 S: Serializer,
626 {
627 Chain::from(source).serialize(serializer)
628 }
629 }
630
631 impl<'de, N> DeserializeAs<'de, super::Chain<N>> for Chain<'de, N>
632 where
633 N: NodePrimitives,
634 {
635 fn deserialize_as<D>(deserializer: D) -> Result<super::Chain<N>, D::Error>
636 where
637 D: Deserializer<'de>,
638 {
639 Chain::deserialize(deserializer).map(Into::into)
640 }
641 }
642
643 #[cfg(test)]
644 mod tests {
645 use super::super::{serde_bincode_compat, Chain};
646 use arbitrary::Arbitrary;
647 use rand::Rng;
648 use reth_primitives_traits::RecoveredBlock;
649 use serde::{Deserialize, Serialize};
650 use serde_with::serde_as;
651
652 #[test]
653 fn test_chain_bincode_roundtrip() {
654 use alloc::collections::BTreeMap;
655
656 #[serde_as]
657 #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
658 struct Data {
659 #[serde_as(as = "serde_bincode_compat::Chain")]
660 chain: Chain,
661 }
662
663 let mut bytes = [0u8; 1024];
664 rand::rng().fill(bytes.as_mut_slice());
665 let data = Data {
666 chain: Chain::new(
667 vec![RecoveredBlock::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
668 .unwrap()],
669 Default::default(),
670 BTreeMap::new(),
671 ),
672 };
673
674 let encoded = bincode::serialize(&data).unwrap();
675 let decoded: Data = bincode::deserialize(&encoded).unwrap();
676 assert_eq!(decoded, data);
677 }
678 }
679}
680
681#[cfg(test)]
682mod tests {
683 use super::*;
684 use alloy_consensus::TxType;
685 use alloy_primitives::{Address, B256};
686 use reth_ethereum_primitives::Receipt;
687 use revm::{database::BundleState, primitives::HashMap, state::AccountInfo};
688
689 #[test]
690 fn chain_append() {
691 let block: RecoveredBlock<reth_ethereum_primitives::Block> = Default::default();
692 let block1_hash = B256::new([0x01; 32]);
693 let block2_hash = B256::new([0x02; 32]);
694 let block3_hash = B256::new([0x03; 32]);
695 let block4_hash = B256::new([0x04; 32]);
696
697 let mut block1 = block.clone();
698 let mut block2 = block.clone();
699 let mut block3 = block.clone();
700 let mut block4 = block;
701
702 block1.set_hash(block1_hash);
703 block2.set_hash(block2_hash);
704 block3.set_hash(block3_hash);
705 block4.set_hash(block4_hash);
706
707 block3.set_parent_hash(block2_hash);
708
709 let mut chain1: Chain =
710 Chain { blocks: BTreeMap::from([(1, block1), (2, block2)]), ..Default::default() };
711
712 let chain2 =
713 Chain { blocks: BTreeMap::from([(3, block3), (4, block4)]), ..Default::default() };
714
715 assert!(chain1.append_chain(chain2.clone()).is_ok());
716
717 assert!(chain1.append_chain(chain2).is_err());
719 }
720
721 #[test]
722 fn test_number_split() {
723 let execution_outcome1: ExecutionOutcome = ExecutionOutcome::new(
724 BundleState::new(
725 vec![(
726 Address::new([2; 20]),
727 None,
728 Some(AccountInfo::default()),
729 HashMap::default(),
730 )],
731 vec![vec![(Address::new([2; 20]), None, vec![])]],
732 vec![],
733 ),
734 vec![vec![]],
735 1,
736 vec![],
737 );
738
739 let execution_outcome2 = ExecutionOutcome::new(
740 BundleState::new(
741 vec![(
742 Address::new([3; 20]),
743 None,
744 Some(AccountInfo::default()),
745 HashMap::default(),
746 )],
747 vec![vec![(Address::new([3; 20]), None, vec![])]],
748 vec![],
749 ),
750 vec![vec![]],
751 2,
752 vec![],
753 );
754
755 let mut block1: RecoveredBlock<reth_ethereum_primitives::Block> = Default::default();
756 let block1_hash = B256::new([15; 32]);
757 block1.set_block_number(1);
758 block1.set_hash(block1_hash);
759 block1.push_sender(Address::new([4; 20]));
760
761 let mut block2: RecoveredBlock<reth_ethereum_primitives::Block> = Default::default();
762 let block2_hash = B256::new([16; 32]);
763 block2.set_block_number(2);
764 block2.set_hash(block2_hash);
765 block2.push_sender(Address::new([4; 20]));
766
767 let mut block_state_extended = execution_outcome1;
768 block_state_extended.extend(execution_outcome2);
769
770 let chain: Chain =
771 Chain::new(vec![block1.clone(), block2.clone()], block_state_extended, BTreeMap::new());
772
773 assert_eq!(
775 chain.execution_outcome_at_block(block2.number),
776 Some(chain.execution_outcome.clone())
777 );
778 assert_eq!(chain.execution_outcome_at_block(100), None);
780 }
781
782 #[test]
783 fn receipts_by_block_hash() {
784 let block: RecoveredBlock<reth_ethereum_primitives::Block> = Default::default();
786
787 let block1_hash = B256::new([0x01; 32]);
789 let block2_hash = B256::new([0x02; 32]);
790
791 let mut block1 = block.clone();
793 let mut block2 = block;
794
795 block1.set_hash(block1_hash);
797 block2.set_hash(block2_hash);
798
799 let receipt1 = Receipt {
801 tx_type: TxType::Legacy,
802 cumulative_gas_used: 46913,
803 logs: vec![],
804 success: true,
805 };
806
807 let receipt2 = Receipt {
809 tx_type: TxType::Legacy,
810 cumulative_gas_used: 1325345,
811 logs: vec![],
812 success: true,
813 };
814
815 let receipts = vec![vec![receipt1.clone()], vec![receipt2]];
817
818 let execution_outcome = ExecutionOutcome {
821 bundle: Default::default(),
822 receipts,
823 requests: vec![],
824 first_block: 10,
825 };
826
827 let chain: Chain = Chain {
830 blocks: BTreeMap::from([(10, block1), (11, block2)]),
831 execution_outcome: execution_outcome.clone(),
832 ..Default::default()
833 };
834
835 assert_eq!(chain.receipts_by_block_hash(block1_hash), Some(vec![&receipt1]));
837
838 let execution_outcome1 = ExecutionOutcome {
840 bundle: Default::default(),
841 receipts: vec![vec![receipt1]],
842 requests: vec![],
843 first_block: 10,
844 };
845
846 assert_eq!(chain.execution_outcome_at_block(10), Some(execution_outcome1));
848
849 assert_eq!(chain.execution_outcome_at_block(11), Some(execution_outcome));
851 }
852}