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;
1011/// 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;
1617/// Keeps track of invalid headers.
18#[derive(Debug)]
19pub struct InvalidHeaderCache {
20/// This maps a header hash to a reference to its invalid ancestor.
21headers: LruMap<B256, HeaderEntry>,
22/// Metrics for the cache.
23metrics: InvalidHeaderCacheMetrics,
24}
2526impl InvalidHeaderCache {
27/// Invalid header cache constructor.
28pub fn new(max_length: u32) -> Self {
29Self { headers: LruMap::new(ByLength::new(max_length)), metrics: Default::default() }
30 }
3132fn insert_entry(&mut self, hash: B256, header: BlockWithParent) {
33self.headers.insert(hash, HeaderEntry { header, hit_count: 0 });
34 }
3536/// 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.
40pub fn get(&mut self, hash: &B256) -> Option<BlockWithParent> {
41 {
42let entry = self.headers.get(hash)?;
43entry.hit_count += 1;
44if entry.hit_count < INVALID_HEADER_HIT_EVICTION_THRESHOLD {
45return Some(entry.header)
46 }
47 }
48// if we get here, the entry has been hit too many times, so we evict it
49self.headers.remove(hash);
50self.metrics.hit_evictions.increment(1);
51None52 }
5354/// Inserts an invalid block into the cache, with a given invalid ancestor.
55pub fn insert_with_invalid_ancestor(
56&mut self,
57 header_hash: B256,
58 invalid_ancestor: BlockWithParent,
59 ) {
60if self.get(&header_hash).is_none() {
61warn!(target: "consensus::engine", hash=?header_hash, ?invalid_ancestor, "Bad block with existing invalid ancestor");
62self.insert_entry(header_hash, invalid_ancestor);
6364// update metrics
65self.metrics.known_ancestor_inserts.increment(1);
66self.metrics.count.set(self.headers.len() as f64);
67 }
68 }
6970/// Inserts an invalid ancestor into the map.
71pub fn insert(&mut self, invalid_ancestor: BlockWithParent) {
72if self.get(&invalid_ancestor.block.hash).is_none() {
73warn!(target: "consensus::engine", ?invalid_ancestor, "Bad block with hash");
74self.insert_entry(invalid_ancestor.block.hash, invalid_ancestor);
7576// update metrics
77self.metrics.unique_inserts.increment(1);
78self.metrics.count.set(self.headers.len() as f64);
79 }
80 }
81}
8283struct HeaderEntry {
84/// Keeps track how many times this header has been hit.
85hit_count: u8,
86/// The actual header entry
87header: BlockWithParent,
88}
8990/// Metrics for the invalid headers cache.
91#[derive(Metrics)]
92#[metrics(scope = "consensus.engine.beacon.invalid_headers")]
93struct InvalidHeaderCacheMetrics {
94/// The total number of invalid headers in the cache.
95count: Gauge,
96/// The number of inserts with a known ancestor.
97known_ancestor_inserts: Counter,
98/// The number of unique invalid header inserts (i.e. without a known ancestor).
99unique_inserts: Counter,
100/// The number of times a header was evicted from the cache because it was hit too many times.
101hit_evictions: Counter,
102}
103104#[cfg(test)]
105mod tests {
106use super::*;
107use alloy_consensus::Header;
108use reth_primitives_traits::SealedHeader;
109110#[test]
111fn test_hit_eviction() {
112let mut cache = InvalidHeaderCache::new(10);
113let header = Header::default();
114let header = SealedHeader::seal_slow(header);
115 cache.insert(header.block_with_parent());
116assert_eq!(cache.headers.get(&header.hash()).unwrap().hit_count, 0);
117118for hit in 1..INVALID_HEADER_HIT_EVICTION_THRESHOLD {
119assert!(cache.get(&header.hash()).is_some());
120assert_eq!(cache.headers.get(&header.hash()).unwrap().hit_count, hit);
121 }
122123assert!(cache.get(&header.hash()).is_none());
124 }
125}