Skip to main content

chainindex_core/
tracker.rs

1//! Block tracker — maintains a sliding window of recent block headers
2//! for parent-hash chain verification and reorg detection.
3
4use std::collections::VecDeque;
5
6use crate::types::BlockSummary;
7
8/// Information about a single tracked block.
9pub type BlockInfo = BlockSummary;
10
11/// Tracks the last N block headers to enable reorg detection.
12///
13/// When a new block arrives, the tracker checks whether its `parent_hash`
14/// matches the hash of the previous block. A mismatch means a reorg occurred.
15pub struct BlockTracker {
16    /// Sliding window of recent blocks (oldest first).
17    window: VecDeque<BlockInfo>,
18    /// Maximum number of blocks to retain.
19    window_size: usize,
20}
21
22impl BlockTracker {
23    /// Create a new tracker with the given window size.
24    /// A window of 128 covers deep reorgs for all major EVM chains.
25    pub fn new(window_size: usize) -> Self {
26        Self {
27            window: VecDeque::with_capacity(window_size),
28            window_size,
29        }
30    }
31
32    /// Add a new block to the tracker.
33    ///
34    /// Returns `Ok(())` if the block extends the current head.
35    /// Returns `Err(reorg_depth)` if a reorg is detected (parent hash mismatch).
36    pub fn push(&mut self, block: BlockInfo) -> Result<(), u64> {
37        if let Some(head) = self.window.back() {
38            if !block.extends(head) {
39                // Reorg — find how deep by walking back
40                let depth = self.find_reorg_depth(&block);
41                return Err(depth);
42            }
43        }
44        if self.window.len() >= self.window_size {
45            self.window.pop_front();
46        }
47        self.window.push_back(block);
48        Ok(())
49    }
50
51    /// Returns the current chain head (most recently added block).
52    pub fn head(&self) -> Option<&BlockInfo> {
53        self.window.back()
54    }
55
56    /// Returns a block by number if it's in the window.
57    pub fn get(&self, number: u64) -> Option<&BlockInfo> {
58        self.window.iter().find(|b| b.number == number)
59    }
60
61    /// Number of blocks in the window.
62    pub fn len(&self) -> usize {
63        self.window.len()
64    }
65
66    /// Returns `true` if the window is empty.
67    pub fn is_empty(&self) -> bool {
68        self.window.is_empty()
69    }
70
71    /// Rewind the tracker to a given block number (discard everything after it).
72    pub fn rewind_to(&mut self, block_number: u64) {
73        while let Some(back) = self.window.back() {
74            if back.number > block_number {
75                self.window.pop_back();
76            } else {
77                break;
78            }
79        }
80    }
81
82    /// Find how deep the reorg is by scanning the window.
83    fn find_reorg_depth(&self, new_block: &BlockInfo) -> u64 {
84        // Walk from newest to oldest looking for a common ancestor
85        for (i, tracked) in self.window.iter().enumerate().rev() {
86            if tracked.hash == new_block.parent_hash {
87                // Found the fork point; depth = number of blocks we need to drop
88                return (self.window.len() - 1 - i) as u64;
89            }
90        }
91        // Common ancestor not in window — assume deep reorg
92        self.window.len() as u64
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    fn block(number: u64, hash: &str, parent: &str) -> BlockInfo {
101        BlockSummary {
102            number,
103            hash: hash.into(),
104            parent_hash: parent.into(),
105            timestamp: (number * 12) as i64,
106            tx_count: 0,
107        }
108    }
109
110    #[test]
111    fn push_normal_chain() {
112        let mut tracker = BlockTracker::new(10);
113        tracker.push(block(100, "0xa", "0x0")).unwrap();
114        tracker.push(block(101, "0xb", "0xa")).unwrap();
115        tracker.push(block(102, "0xc", "0xb")).unwrap();
116        assert_eq!(tracker.head().unwrap().number, 102);
117        assert_eq!(tracker.len(), 3);
118    }
119
120    #[test]
121    fn push_detects_reorg() {
122        let mut tracker = BlockTracker::new(10);
123        tracker.push(block(100, "0xa", "0x0")).unwrap();
124        tracker.push(block(101, "0xb", "0xa")).unwrap();
125        // Reorg: block 102 has a parent that is NOT 0xb
126        let result = tracker.push(block(102, "0xc2", "0xb-different"));
127        assert!(result.is_err(), "should detect reorg");
128    }
129
130    #[test]
131    fn rewind_to() {
132        let mut tracker = BlockTracker::new(10);
133        for i in 100..=110 {
134            let prev = if i == 100 {
135                "0x0".to_string()
136            } else {
137                format!("0x{}", i - 1)
138            };
139            tracker.push(block(i, &format!("0x{i}"), &prev)).unwrap();
140        }
141        assert_eq!(tracker.head().unwrap().number, 110);
142        tracker.rewind_to(105);
143        assert_eq!(tracker.head().unwrap().number, 105);
144    }
145
146    #[test]
147    fn window_size_enforced() {
148        let mut tracker = BlockTracker::new(5);
149        for i in 0..10 {
150            let prev = if i == 0 {
151                "0x0".to_string()
152            } else {
153                format!("0x{}", i - 1)
154            };
155            tracker.push(block(i, &format!("0x{i}"), &prev)).unwrap();
156        }
157        assert_eq!(tracker.len(), 5); // oldest blocks evicted
158    }
159}