Skip to main content

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