reth_engine_tree/tree/
invalid_headers.rs

1use alloy_eips::eip1898::BlockWithParent;
2use alloy_primitives::B256;
3use reth_metrics::{
4    metrics::{Counter, Gauge},
5    Metrics,
6};
7use schnellru::{ByLength, LruMap};
8use std::fmt::Debug;
9use tracing::warn;
10
11/// The max hit counter for invalid headers in the cache before it is forcefully evicted.
12///
13/// In other words, if a header is referenced more than this number of times, it will be evicted to
14/// allow for reprocessing.
15const INVALID_HEADER_HIT_EVICTION_THRESHOLD: u8 = 128;
16
17/// Keeps track of invalid headers.
18#[derive(Debug)]
19pub struct InvalidHeaderCache {
20    /// This maps a header hash to a reference to its invalid ancestor.
21    headers: LruMap<B256, HeaderEntry>,
22    /// Metrics for the cache.
23    metrics: InvalidHeaderCacheMetrics,
24}
25
26impl InvalidHeaderCache {
27    /// Invalid header cache constructor.
28    pub fn new(max_length: u32) -> Self {
29        Self { headers: LruMap::new(ByLength::new(max_length)), metrics: Default::default() }
30    }
31
32    fn insert_entry(&mut self, hash: B256, header: BlockWithParent) {
33        self.headers.insert(hash, HeaderEntry { header, hit_count: 0 });
34    }
35
36    /// Returns the invalid ancestor's header if it exists in the cache.
37    ///
38    /// If this is called, the hit count for the entry is incremented.
39    /// If the hit count exceeds the threshold, the entry is evicted and `None` is returned.
40    pub fn get(&mut self, hash: &B256) -> Option<BlockWithParent> {
41        {
42            let entry = self.headers.get(hash)?;
43            entry.hit_count += 1;
44            if entry.hit_count < INVALID_HEADER_HIT_EVICTION_THRESHOLD {
45                return Some(entry.header)
46            }
47        }
48        // if we get here, the entry has been hit too many times, so we evict it
49        self.headers.remove(hash);
50        self.metrics.hit_evictions.increment(1);
51        self.metrics.count.set(self.headers.len() as f64);
52        None
53    }
54
55    /// Inserts an invalid block into the cache, with a given invalid ancestor.
56    pub fn insert_with_invalid_ancestor(
57        &mut self,
58        header_hash: B256,
59        invalid_ancestor: BlockWithParent,
60    ) {
61        if self.get(&header_hash).is_none() {
62            warn!(target: "consensus::engine", hash=?header_hash, ?invalid_ancestor, "Bad block with existing invalid ancestor");
63            self.insert_entry(header_hash, invalid_ancestor);
64
65            // update metrics
66            self.metrics.known_ancestor_inserts.increment(1);
67            self.metrics.count.set(self.headers.len() as f64);
68        }
69    }
70
71    /// Inserts an invalid ancestor into the map.
72    pub fn insert(&mut self, invalid_ancestor: BlockWithParent) {
73        if self.get(&invalid_ancestor.block.hash).is_none() {
74            warn!(target: "consensus::engine", ?invalid_ancestor, "Bad block with hash");
75            self.insert_entry(invalid_ancestor.block.hash, invalid_ancestor);
76
77            // update metrics
78            self.metrics.unique_inserts.increment(1);
79            self.metrics.count.set(self.headers.len() as f64);
80        }
81    }
82}
83
84struct HeaderEntry {
85    /// Keeps track how many times this header has been hit.
86    hit_count: u8,
87    /// The actual header entry
88    header: BlockWithParent,
89}
90
91/// Metrics for the invalid headers cache.
92#[derive(Metrics)]
93#[metrics(scope = "consensus.engine.beacon.invalid_headers")]
94struct InvalidHeaderCacheMetrics {
95    /// The total number of invalid headers in the cache.
96    count: Gauge,
97    /// The number of inserts with a known ancestor.
98    known_ancestor_inserts: Counter,
99    /// The number of unique invalid header inserts (i.e. without a known ancestor).
100    unique_inserts: Counter,
101    /// The number of times a header was evicted from the cache because it was hit too many times.
102    hit_evictions: Counter,
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use alloy_consensus::Header;
109    use reth_primitives_traits::SealedHeader;
110
111    #[test]
112    fn test_hit_eviction() {
113        let mut cache = InvalidHeaderCache::new(10);
114        let header = Header::default();
115        let header = SealedHeader::seal_slow(header);
116        cache.insert(header.block_with_parent());
117        assert_eq!(cache.headers.get(&header.hash()).unwrap().hit_count, 0);
118
119        for hit in 1..INVALID_HEADER_HIT_EVICTION_THRESHOLD {
120            assert!(cache.get(&header.hash()).is_some());
121            assert_eq!(cache.headers.get(&header.hash()).unwrap().hit_count, hit);
122        }
123
124        assert!(cache.get(&header.hash()).is_none());
125    }
126}