reth_engine_tree/tree/
state.rs

1//! Functionality related to tree state.
2
3use crate::engine::EngineApiKind;
4use alloy_eips::BlockNumHash;
5use alloy_primitives::{
6    map::{HashMap, HashSet},
7    BlockNumber, B256,
8};
9use reth_chain_state::{EthPrimitives, ExecutedBlock};
10use reth_primitives_traits::{AlloyBlockHeader, NodePrimitives, SealedHeader};
11use std::{
12    collections::{btree_map, hash_map, BTreeMap, VecDeque},
13    ops::Bound,
14};
15use tracing::debug;
16
17/// Keeps track of the state of the tree.
18///
19/// ## Invariants
20///
21/// - This only stores blocks that are connected to the canonical chain.
22/// - All executed blocks are valid and have been executed.
23#[derive(Debug, Default)]
24pub struct TreeState<N: NodePrimitives = EthPrimitives> {
25    /// __All__ unique executed blocks by block hash that are connected to the canonical chain.
26    ///
27    /// This includes blocks of all forks.
28    pub(crate) blocks_by_hash: HashMap<B256, ExecutedBlock<N>>,
29    /// Executed blocks grouped by their respective block number.
30    ///
31    /// This maps unique block number to all known blocks for that height.
32    ///
33    /// Note: there can be multiple blocks at the same height due to forks.
34    pub(crate) blocks_by_number: BTreeMap<BlockNumber, Vec<ExecutedBlock<N>>>,
35    /// Map of any parent block hash to its children.
36    pub(crate) parent_to_child: HashMap<B256, HashSet<B256>>,
37    /// Currently tracked canonical head of the chain.
38    pub(crate) current_canonical_head: BlockNumHash,
39    /// The engine API variant of this handler
40    pub(crate) engine_kind: EngineApiKind,
41}
42
43impl<N: NodePrimitives> TreeState<N> {
44    /// Returns a new, empty tree state that points to the given canonical head.
45    pub(crate) fn new(current_canonical_head: BlockNumHash, engine_kind: EngineApiKind) -> Self {
46        Self {
47            blocks_by_hash: HashMap::default(),
48            blocks_by_number: BTreeMap::new(),
49            current_canonical_head,
50            parent_to_child: HashMap::default(),
51            engine_kind,
52        }
53    }
54
55    /// Resets the state and points to the given canonical head.
56    pub(crate) fn reset(&mut self, current_canonical_head: BlockNumHash) {
57        *self = Self::new(current_canonical_head, self.engine_kind);
58    }
59
60    /// Returns the number of executed blocks stored.
61    pub(crate) fn block_count(&self) -> usize {
62        self.blocks_by_hash.len()
63    }
64
65    /// Returns the [`ExecutedBlock`] by hash.
66    pub(crate) fn executed_block_by_hash(&self, hash: B256) -> Option<&ExecutedBlock<N>> {
67        self.blocks_by_hash.get(&hash)
68    }
69
70    /// Returns the sealed block header by hash.
71    pub(crate) fn sealed_header_by_hash(
72        &self,
73        hash: &B256,
74    ) -> Option<SealedHeader<N::BlockHeader>> {
75        self.blocks_by_hash.get(hash).map(|b| b.sealed_block().sealed_header().clone())
76    }
77
78    /// Returns all available blocks for the given hash that lead back to the canonical chain, from
79    /// newest to oldest, and the parent hash of the oldest returned block. This parent hash is the
80    /// highest persisted block connected to this chain.
81    ///
82    /// Returns `None` if the block for the given hash is not found.
83    pub(crate) fn blocks_by_hash(&self, hash: B256) -> Option<(B256, Vec<ExecutedBlock<N>>)> {
84        let block = self.blocks_by_hash.get(&hash).cloned()?;
85        let mut parent_hash = block.recovered_block().parent_hash();
86        let mut blocks = vec![block];
87        while let Some(executed) = self.blocks_by_hash.get(&parent_hash) {
88            parent_hash = executed.recovered_block().parent_hash();
89            blocks.push(executed.clone());
90        }
91
92        Some((parent_hash, blocks))
93    }
94
95    /// Insert executed block into the state.
96    pub(crate) fn insert_executed(&mut self, executed: ExecutedBlock<N>) {
97        let hash = executed.recovered_block().hash();
98        let parent_hash = executed.recovered_block().parent_hash();
99        let block_number = executed.recovered_block().number();
100
101        if self.blocks_by_hash.contains_key(&hash) {
102            return;
103        }
104
105        self.blocks_by_hash.insert(hash, executed.clone());
106
107        self.blocks_by_number.entry(block_number).or_default().push(executed);
108
109        self.parent_to_child.entry(parent_hash).or_default().insert(hash);
110    }
111
112    /// Remove single executed block by its hash.
113    ///
114    /// ## Returns
115    ///
116    /// The removed block and the block hashes of its children.
117    fn remove_by_hash(&mut self, hash: B256) -> Option<(ExecutedBlock<N>, HashSet<B256>)> {
118        let executed = self.blocks_by_hash.remove(&hash)?;
119
120        // Remove this block from collection of children of its parent block.
121        let parent_entry = self.parent_to_child.entry(executed.recovered_block().parent_hash());
122        if let hash_map::Entry::Occupied(mut entry) = parent_entry {
123            entry.get_mut().remove(&hash);
124
125            if entry.get().is_empty() {
126                entry.remove();
127            }
128        }
129
130        // Remove point to children of this block.
131        let children = self.parent_to_child.remove(&hash).unwrap_or_default();
132
133        // Remove this block from `blocks_by_number`.
134        let block_number_entry = self.blocks_by_number.entry(executed.recovered_block().number());
135        if let btree_map::Entry::Occupied(mut entry) = block_number_entry {
136            // We have to find the index of the block since it exists in a vec
137            if let Some(index) = entry.get().iter().position(|b| b.recovered_block().hash() == hash)
138            {
139                entry.get_mut().swap_remove(index);
140
141                // If there are no blocks left then remove the entry for this block
142                if entry.get().is_empty() {
143                    entry.remove();
144                }
145            }
146        }
147
148        Some((executed, children))
149    }
150
151    /// Returns whether or not the hash is part of the canonical chain.
152    pub(crate) fn is_canonical(&self, hash: B256) -> bool {
153        let mut current_block = self.current_canonical_head.hash;
154        if current_block == hash {
155            return true
156        }
157
158        while let Some(executed) = self.blocks_by_hash.get(&current_block) {
159            current_block = executed.recovered_block().parent_hash();
160            if current_block == hash {
161                return true
162            }
163        }
164
165        false
166    }
167
168    /// Removes canonical blocks below the upper bound, only if the last persisted hash is
169    /// part of the canonical chain.
170    pub(crate) fn remove_canonical_until(
171        &mut self,
172        upper_bound: BlockNumber,
173        last_persisted_hash: B256,
174    ) {
175        debug!(target: "engine::tree", ?upper_bound, ?last_persisted_hash, "Removing canonical blocks from the tree");
176
177        // If the last persisted hash is not canonical, then we don't want to remove any canonical
178        // blocks yet.
179        if !self.is_canonical(last_persisted_hash) {
180            return
181        }
182
183        // First, let's walk back the canonical chain and remove canonical blocks lower than the
184        // upper bound
185        let mut current_block = self.current_canonical_head.hash;
186        while let Some(executed) = self.blocks_by_hash.get(&current_block) {
187            current_block = executed.recovered_block().parent_hash();
188            if executed.recovered_block().number() <= upper_bound {
189                let num_hash = executed.recovered_block().num_hash();
190                debug!(target: "engine::tree", ?num_hash, "Attempting to remove block walking back from the head");
191                self.remove_by_hash(executed.recovered_block().hash());
192            }
193        }
194        debug!(target: "engine::tree", ?upper_bound, ?last_persisted_hash, "Removed canonical blocks from the tree");
195    }
196
197    /// Removes all blocks that are below the finalized block, as well as removing non-canonical
198    /// sidechains that fork from below the finalized block.
199    pub(crate) fn prune_finalized_sidechains(&mut self, finalized_num_hash: BlockNumHash) {
200        let BlockNumHash { number: finalized_num, hash: finalized_hash } = finalized_num_hash;
201
202        // We remove disconnected sidechains in three steps:
203        // * first, remove everything with a block number __below__ the finalized block.
204        // * next, we populate a vec with parents __at__ the finalized block.
205        // * finally, we iterate through the vec, removing children until the vec is empty
206        // (BFS).
207
208        // We _exclude_ the finalized block because we will be dealing with the blocks __at__
209        // the finalized block later.
210        let blocks_to_remove = self
211            .blocks_by_number
212            .range((Bound::Unbounded, Bound::Excluded(finalized_num)))
213            .flat_map(|(_, blocks)| blocks.iter().map(|b| b.recovered_block().hash()))
214            .collect::<Vec<_>>();
215        for hash in blocks_to_remove {
216            if let Some((removed, _)) = self.remove_by_hash(hash) {
217                debug!(target: "engine::tree", num_hash=?removed.recovered_block().num_hash(), "Removed finalized sidechain block");
218            }
219        }
220
221        // The only block that should remain at the `finalized` number now, is the finalized
222        // block, if it exists.
223        //
224        // For all other blocks, we  first put their children into this vec.
225        // Then, we will iterate over them, removing them, adding their children, etc,
226        // until the vec is empty.
227        let mut blocks_to_remove = self.blocks_by_number.remove(&finalized_num).unwrap_or_default();
228
229        // re-insert the finalized hash if we removed it
230        if let Some(position) =
231            blocks_to_remove.iter().position(|b| b.recovered_block().hash() == finalized_hash)
232        {
233            let finalized_block = blocks_to_remove.swap_remove(position);
234            self.blocks_by_number.insert(finalized_num, vec![finalized_block]);
235        }
236
237        let mut blocks_to_remove = blocks_to_remove
238            .into_iter()
239            .map(|e| e.recovered_block().hash())
240            .collect::<VecDeque<_>>();
241        while let Some(block) = blocks_to_remove.pop_front() {
242            if let Some((removed, children)) = self.remove_by_hash(block) {
243                debug!(target: "engine::tree", num_hash=?removed.recovered_block().num_hash(), "Removed finalized sidechain child block");
244                blocks_to_remove.extend(children);
245            }
246        }
247    }
248
249    /// Remove all blocks up to __and including__ the given block number.
250    ///
251    /// If a finalized hash is provided, the only non-canonical blocks which will be removed are
252    /// those which have a fork point at or below the finalized hash.
253    ///
254    /// Canonical blocks below the upper bound will still be removed.
255    ///
256    /// NOTE: if the finalized block is greater than the upper bound, the only blocks that will be
257    /// removed are canonical blocks and sidechains that fork below the `upper_bound`. This is the
258    /// same behavior as if the `finalized_num` were `Some(upper_bound)`.
259    pub(crate) fn remove_until(
260        &mut self,
261        upper_bound: BlockNumHash,
262        last_persisted_hash: B256,
263        finalized_num_hash: Option<BlockNumHash>,
264    ) {
265        debug!(target: "engine::tree", ?upper_bound, ?finalized_num_hash, "Removing blocks from the tree");
266
267        // If the finalized num is ahead of the upper bound, and exists, we need to instead ensure
268        // that the only blocks removed, are canonical blocks less than the upper bound
269        let finalized_num_hash = finalized_num_hash.map(|mut finalized| {
270            if upper_bound.number < finalized.number {
271                finalized = upper_bound;
272                debug!(target: "engine::tree", ?finalized, "Adjusted upper bound");
273            }
274            finalized
275        });
276
277        // We want to do two things:
278        // * remove canonical blocks that are persisted
279        // * remove forks whose root are below the finalized block
280        // We can do this in 2 steps:
281        // * remove all canonical blocks below the upper bound
282        // * fetch the number of the finalized hash, removing any sidechains that are __below__ the
283        // finalized block
284        self.remove_canonical_until(upper_bound.number, last_persisted_hash);
285
286        // Now, we have removed canonical blocks (assuming the upper bound is above the finalized
287        // block) and only have sidechains below the finalized block.
288        if let Some(finalized_num_hash) = finalized_num_hash {
289            self.prune_finalized_sidechains(finalized_num_hash);
290        }
291    }
292
293    /// Updates the canonical head to the given block.
294    pub(crate) const fn set_canonical_head(&mut self, new_head: BlockNumHash) {
295        self.current_canonical_head = new_head;
296    }
297
298    /// Returns the tracked canonical head.
299    pub(crate) const fn canonical_head(&self) -> &BlockNumHash {
300        &self.current_canonical_head
301    }
302
303    /// Returns the block hash of the canonical head.
304    pub(crate) const fn canonical_block_hash(&self) -> B256 {
305        self.canonical_head().hash
306    }
307
308    /// Returns the block number of the canonical head.
309    pub(crate) const fn canonical_block_number(&self) -> BlockNumber {
310        self.canonical_head().number
311    }
312}
313
314#[cfg(test)]
315impl<N: NodePrimitives> TreeState<N> {
316    /// Determines if the second block is a descendant of the first block.
317    ///
318    /// If the two blocks are the same, this returns `false`.
319    pub(crate) fn is_descendant(
320        &self,
321        first: BlockNumHash,
322        second: alloy_eips::eip1898::BlockWithParent,
323    ) -> bool {
324        // If the second block's parent is the first block's hash, then it is a direct child
325        // and we can return early.
326        if second.parent == first.hash {
327            return true
328        }
329
330        // If the second block is lower than, or has the same block number, they are not
331        // descendants.
332        if second.block.number <= first.number {
333            return false
334        }
335
336        // iterate through parents of the second until we reach the number
337        let Some(mut current_block) = self.blocks_by_hash.get(&second.parent) else {
338            // If we can't find its parent in the tree, we can't continue, so return false
339            return false
340        };
341
342        while current_block.recovered_block().number() > first.number + 1 {
343            let Some(block) =
344                self.blocks_by_hash.get(&current_block.recovered_block().parent_hash())
345            else {
346                // If we can't find its parent in the tree, we can't continue, so return false
347                return false
348            };
349
350            current_block = block;
351        }
352
353        // Now the block numbers should be equal, so we compare hashes.
354        current_block.recovered_block().parent_hash() == first.hash
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use reth_chain_state::test_utils::TestBlockBuilder;
362
363    #[test]
364    fn test_tree_state_normal_descendant() {
365        let mut tree_state = TreeState::new(BlockNumHash::default(), EngineApiKind::Ethereum);
366        let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..4).collect();
367
368        tree_state.insert_executed(blocks[0].clone());
369        assert!(tree_state.is_descendant(
370            blocks[0].recovered_block().num_hash(),
371            blocks[1].recovered_block().block_with_parent()
372        ));
373
374        tree_state.insert_executed(blocks[1].clone());
375
376        assert!(tree_state.is_descendant(
377            blocks[0].recovered_block().num_hash(),
378            blocks[2].recovered_block().block_with_parent()
379        ));
380        assert!(tree_state.is_descendant(
381            blocks[1].recovered_block().num_hash(),
382            blocks[2].recovered_block().block_with_parent()
383        ));
384    }
385
386    #[tokio::test]
387    async fn test_tree_state_insert_executed() {
388        let mut tree_state = TreeState::new(BlockNumHash::default(), EngineApiKind::Ethereum);
389        let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..4).collect();
390
391        tree_state.insert_executed(blocks[0].clone());
392        tree_state.insert_executed(blocks[1].clone());
393
394        assert_eq!(
395            tree_state.parent_to_child.get(&blocks[0].recovered_block().hash()),
396            Some(&HashSet::from_iter([blocks[1].recovered_block().hash()]))
397        );
398
399        assert!(!tree_state.parent_to_child.contains_key(&blocks[1].recovered_block().hash()));
400
401        tree_state.insert_executed(blocks[2].clone());
402
403        assert_eq!(
404            tree_state.parent_to_child.get(&blocks[1].recovered_block().hash()),
405            Some(&HashSet::from_iter([blocks[2].recovered_block().hash()]))
406        );
407        assert!(tree_state.parent_to_child.contains_key(&blocks[1].recovered_block().hash()));
408
409        assert!(!tree_state.parent_to_child.contains_key(&blocks[2].recovered_block().hash()));
410    }
411
412    #[tokio::test]
413    async fn test_tree_state_insert_executed_with_reorg() {
414        let mut tree_state = TreeState::new(BlockNumHash::default(), EngineApiKind::Ethereum);
415        let mut test_block_builder = TestBlockBuilder::eth();
416        let blocks: Vec<_> = test_block_builder.get_executed_blocks(1..6).collect();
417
418        for block in &blocks {
419            tree_state.insert_executed(block.clone());
420        }
421        assert_eq!(tree_state.blocks_by_hash.len(), 5);
422
423        let fork_block_3 = test_block_builder
424            .get_executed_block_with_number(3, blocks[1].recovered_block().hash());
425        let fork_block_4 = test_block_builder
426            .get_executed_block_with_number(4, fork_block_3.recovered_block().hash());
427        let fork_block_5 = test_block_builder
428            .get_executed_block_with_number(5, fork_block_4.recovered_block().hash());
429
430        tree_state.insert_executed(fork_block_3.clone());
431        tree_state.insert_executed(fork_block_4.clone());
432        tree_state.insert_executed(fork_block_5.clone());
433
434        assert_eq!(tree_state.blocks_by_hash.len(), 8);
435        assert_eq!(tree_state.blocks_by_number[&3].len(), 2); // two blocks at height 3 (original and fork)
436        assert_eq!(tree_state.parent_to_child[&blocks[1].recovered_block().hash()].len(), 2); // block 2 should have two children
437
438        // verify that we can insert the same block again without issues
439        tree_state.insert_executed(fork_block_4.clone());
440        assert_eq!(tree_state.blocks_by_hash.len(), 8);
441
442        assert!(tree_state.parent_to_child[&fork_block_3.recovered_block().hash()]
443            .contains(&fork_block_4.recovered_block().hash()));
444        assert!(tree_state.parent_to_child[&fork_block_4.recovered_block().hash()]
445            .contains(&fork_block_5.recovered_block().hash()));
446
447        assert_eq!(tree_state.blocks_by_number[&4].len(), 2);
448        assert_eq!(tree_state.blocks_by_number[&5].len(), 2);
449    }
450
451    #[tokio::test]
452    async fn test_tree_state_remove_before() {
453        let start_num_hash = BlockNumHash::default();
454        let mut tree_state = TreeState::new(start_num_hash, EngineApiKind::Ethereum);
455        let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..6).collect();
456
457        for block in &blocks {
458            tree_state.insert_executed(block.clone());
459        }
460
461        let last = blocks.last().unwrap();
462
463        // set the canonical head
464        tree_state.set_canonical_head(last.recovered_block().num_hash());
465
466        // inclusive bound, so we should remove anything up to and including 2
467        tree_state.remove_until(
468            BlockNumHash::new(2, blocks[1].recovered_block().hash()),
469            start_num_hash.hash,
470            Some(blocks[1].recovered_block().num_hash()),
471        );
472
473        assert!(!tree_state.blocks_by_hash.contains_key(&blocks[0].recovered_block().hash()));
474        assert!(!tree_state.blocks_by_hash.contains_key(&blocks[1].recovered_block().hash()));
475        assert!(!tree_state.blocks_by_number.contains_key(&1));
476        assert!(!tree_state.blocks_by_number.contains_key(&2));
477
478        assert!(tree_state.blocks_by_hash.contains_key(&blocks[2].recovered_block().hash()));
479        assert!(tree_state.blocks_by_hash.contains_key(&blocks[3].recovered_block().hash()));
480        assert!(tree_state.blocks_by_hash.contains_key(&blocks[4].recovered_block().hash()));
481        assert!(tree_state.blocks_by_number.contains_key(&3));
482        assert!(tree_state.blocks_by_number.contains_key(&4));
483        assert!(tree_state.blocks_by_number.contains_key(&5));
484
485        assert!(!tree_state.parent_to_child.contains_key(&blocks[0].recovered_block().hash()));
486        assert!(!tree_state.parent_to_child.contains_key(&blocks[1].recovered_block().hash()));
487        assert!(tree_state.parent_to_child.contains_key(&blocks[2].recovered_block().hash()));
488        assert!(tree_state.parent_to_child.contains_key(&blocks[3].recovered_block().hash()));
489        assert!(!tree_state.parent_to_child.contains_key(&blocks[4].recovered_block().hash()));
490
491        assert_eq!(
492            tree_state.parent_to_child.get(&blocks[2].recovered_block().hash()),
493            Some(&HashSet::from_iter([blocks[3].recovered_block().hash()]))
494        );
495        assert_eq!(
496            tree_state.parent_to_child.get(&blocks[3].recovered_block().hash()),
497            Some(&HashSet::from_iter([blocks[4].recovered_block().hash()]))
498        );
499    }
500
501    #[tokio::test]
502    async fn test_tree_state_remove_before_finalized() {
503        let start_num_hash = BlockNumHash::default();
504        let mut tree_state = TreeState::new(start_num_hash, EngineApiKind::Ethereum);
505        let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..6).collect();
506
507        for block in &blocks {
508            tree_state.insert_executed(block.clone());
509        }
510
511        let last = blocks.last().unwrap();
512
513        // set the canonical head
514        tree_state.set_canonical_head(last.recovered_block().num_hash());
515
516        // we should still remove everything up to and including 2
517        tree_state.remove_until(
518            BlockNumHash::new(2, blocks[1].recovered_block().hash()),
519            start_num_hash.hash,
520            None,
521        );
522
523        assert!(!tree_state.blocks_by_hash.contains_key(&blocks[0].recovered_block().hash()));
524        assert!(!tree_state.blocks_by_hash.contains_key(&blocks[1].recovered_block().hash()));
525        assert!(!tree_state.blocks_by_number.contains_key(&1));
526        assert!(!tree_state.blocks_by_number.contains_key(&2));
527
528        assert!(tree_state.blocks_by_hash.contains_key(&blocks[2].recovered_block().hash()));
529        assert!(tree_state.blocks_by_hash.contains_key(&blocks[3].recovered_block().hash()));
530        assert!(tree_state.blocks_by_hash.contains_key(&blocks[4].recovered_block().hash()));
531        assert!(tree_state.blocks_by_number.contains_key(&3));
532        assert!(tree_state.blocks_by_number.contains_key(&4));
533        assert!(tree_state.blocks_by_number.contains_key(&5));
534
535        assert!(!tree_state.parent_to_child.contains_key(&blocks[0].recovered_block().hash()));
536        assert!(!tree_state.parent_to_child.contains_key(&blocks[1].recovered_block().hash()));
537        assert!(tree_state.parent_to_child.contains_key(&blocks[2].recovered_block().hash()));
538        assert!(tree_state.parent_to_child.contains_key(&blocks[3].recovered_block().hash()));
539        assert!(!tree_state.parent_to_child.contains_key(&blocks[4].recovered_block().hash()));
540
541        assert_eq!(
542            tree_state.parent_to_child.get(&blocks[2].recovered_block().hash()),
543            Some(&HashSet::from_iter([blocks[3].recovered_block().hash()]))
544        );
545        assert_eq!(
546            tree_state.parent_to_child.get(&blocks[3].recovered_block().hash()),
547            Some(&HashSet::from_iter([blocks[4].recovered_block().hash()]))
548        );
549    }
550
551    #[tokio::test]
552    async fn test_tree_state_remove_before_lower_finalized() {
553        let start_num_hash = BlockNumHash::default();
554        let mut tree_state = TreeState::new(start_num_hash, EngineApiKind::Ethereum);
555        let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..6).collect();
556
557        for block in &blocks {
558            tree_state.insert_executed(block.clone());
559        }
560
561        let last = blocks.last().unwrap();
562
563        // set the canonical head
564        tree_state.set_canonical_head(last.recovered_block().num_hash());
565
566        // we have no forks so we should still remove anything up to and including 2
567        tree_state.remove_until(
568            BlockNumHash::new(2, blocks[1].recovered_block().hash()),
569            start_num_hash.hash,
570            Some(blocks[0].recovered_block().num_hash()),
571        );
572
573        assert!(!tree_state.blocks_by_hash.contains_key(&blocks[0].recovered_block().hash()));
574        assert!(!tree_state.blocks_by_hash.contains_key(&blocks[1].recovered_block().hash()));
575        assert!(!tree_state.blocks_by_number.contains_key(&1));
576        assert!(!tree_state.blocks_by_number.contains_key(&2));
577
578        assert!(tree_state.blocks_by_hash.contains_key(&blocks[2].recovered_block().hash()));
579        assert!(tree_state.blocks_by_hash.contains_key(&blocks[3].recovered_block().hash()));
580        assert!(tree_state.blocks_by_hash.contains_key(&blocks[4].recovered_block().hash()));
581        assert!(tree_state.blocks_by_number.contains_key(&3));
582        assert!(tree_state.blocks_by_number.contains_key(&4));
583        assert!(tree_state.blocks_by_number.contains_key(&5));
584
585        assert!(!tree_state.parent_to_child.contains_key(&blocks[0].recovered_block().hash()));
586        assert!(!tree_state.parent_to_child.contains_key(&blocks[1].recovered_block().hash()));
587        assert!(tree_state.parent_to_child.contains_key(&blocks[2].recovered_block().hash()));
588        assert!(tree_state.parent_to_child.contains_key(&blocks[3].recovered_block().hash()));
589        assert!(!tree_state.parent_to_child.contains_key(&blocks[4].recovered_block().hash()));
590
591        assert_eq!(
592            tree_state.parent_to_child.get(&blocks[2].recovered_block().hash()),
593            Some(&HashSet::from_iter([blocks[3].recovered_block().hash()]))
594        );
595        assert_eq!(
596            tree_state.parent_to_child.get(&blocks[3].recovered_block().hash()),
597            Some(&HashSet::from_iter([blocks[4].recovered_block().hash()]))
598        );
599    }
600}