reth_chain_state/
chain_info.rs

1use alloy_consensus::BlockHeader;
2use alloy_eips::BlockNumHash;
3use alloy_primitives::BlockNumber;
4use parking_lot::RwLock;
5use reth_chainspec::ChainInfo;
6use reth_primitives_traits::{NodePrimitives, SealedHeader};
7use std::{
8    sync::{
9        atomic::{AtomicU64, Ordering},
10        Arc,
11    },
12    time::Instant,
13};
14use tokio::sync::watch;
15
16/// Tracks the chain info: canonical head, safe block, finalized block.
17#[derive(Debug, Clone)]
18pub struct ChainInfoTracker<N: NodePrimitives> {
19    inner: Arc<ChainInfoInner<N>>,
20}
21
22impl<N> ChainInfoTracker<N>
23where
24    N: NodePrimitives,
25    N::BlockHeader: BlockHeader,
26{
27    /// Create a new chain info container for the given canonical head and finalized header if it
28    /// exists.
29    pub fn new(
30        head: SealedHeader<N::BlockHeader>,
31        finalized: Option<SealedHeader<N::BlockHeader>>,
32        safe: Option<SealedHeader<N::BlockHeader>>,
33    ) -> Self {
34        let (finalized_block, _) = watch::channel(finalized);
35        let (safe_block, _) = watch::channel(safe);
36
37        Self {
38            inner: Arc::new(ChainInfoInner {
39                last_forkchoice_update: RwLock::new(None),
40
41                canonical_head_number: AtomicU64::new(head.number()),
42                canonical_head: RwLock::new(head),
43                safe_block,
44                finalized_block,
45            }),
46        }
47    }
48
49    /// Returns the [`ChainInfo`] for the canonical head.
50    pub fn chain_info(&self) -> ChainInfo {
51        let inner = self.inner.canonical_head.read();
52        ChainInfo { best_hash: inner.hash(), best_number: inner.number() }
53    }
54
55    /// Update the timestamp when we received a forkchoice update.
56    pub fn on_forkchoice_update_received(&self) {
57        self.inner.last_forkchoice_update.write().replace(Instant::now());
58    }
59
60    /// Returns the instant when we received the latest forkchoice update.
61    pub fn last_forkchoice_update_received_at(&self) -> Option<Instant> {
62        *self.inner.last_forkchoice_update.read()
63    }
64
65    /// Returns the canonical head of the chain.
66    pub fn get_canonical_head(&self) -> SealedHeader<N::BlockHeader> {
67        self.inner.canonical_head.read().clone()
68    }
69
70    /// Returns the safe header of the chain.
71    pub fn get_safe_header(&self) -> Option<SealedHeader<N::BlockHeader>> {
72        self.inner.safe_block.borrow().clone()
73    }
74
75    /// Returns the finalized header of the chain.
76    pub fn get_finalized_header(&self) -> Option<SealedHeader<N::BlockHeader>> {
77        self.inner.finalized_block.borrow().clone()
78    }
79
80    /// Returns the canonical head of the chain.
81    pub fn get_canonical_num_hash(&self) -> BlockNumHash {
82        self.inner.canonical_head.read().num_hash()
83    }
84
85    /// Returns the canonical head of the chain.
86    pub fn get_canonical_block_number(&self) -> BlockNumber {
87        self.inner.canonical_head_number.load(Ordering::Relaxed)
88    }
89
90    /// Returns the safe header of the chain.
91    pub fn get_safe_num_hash(&self) -> Option<BlockNumHash> {
92        self.inner.safe_block.borrow().as_ref().map(SealedHeader::num_hash)
93    }
94
95    /// Returns the finalized header of the chain.
96    pub fn get_finalized_num_hash(&self) -> Option<BlockNumHash> {
97        self.inner.finalized_block.borrow().as_ref().map(SealedHeader::num_hash)
98    }
99
100    /// Sets the canonical head of the chain.
101    pub fn set_canonical_head(&self, header: SealedHeader<N::BlockHeader>) {
102        let number = header.number();
103        *self.inner.canonical_head.write() = header;
104
105        // also update the atomic number.
106        self.inner.canonical_head_number.store(number, Ordering::Relaxed);
107    }
108
109    /// Sets the safe header of the chain.
110    pub fn set_safe(&self, header: SealedHeader<N::BlockHeader>) {
111        self.inner.safe_block.send_if_modified(|current_header| {
112            if current_header.as_ref().map(SealedHeader::hash) != Some(header.hash()) {
113                let _ = current_header.replace(header);
114                return true
115            }
116
117            false
118        });
119    }
120
121    /// Sets the finalized header of the chain.
122    pub fn set_finalized(&self, header: SealedHeader<N::BlockHeader>) {
123        self.inner.finalized_block.send_if_modified(|current_header| {
124            if current_header.as_ref().map(SealedHeader::hash) != Some(header.hash()) {
125                let _ = current_header.replace(header);
126                return true
127            }
128
129            false
130        });
131    }
132
133    /// Subscribe to the finalized block.
134    pub fn subscribe_finalized_block(
135        &self,
136    ) -> watch::Receiver<Option<SealedHeader<N::BlockHeader>>> {
137        self.inner.finalized_block.subscribe()
138    }
139
140    /// Subscribe to the safe block.
141    pub fn subscribe_safe_block(&self) -> watch::Receiver<Option<SealedHeader<N::BlockHeader>>> {
142        self.inner.safe_block.subscribe()
143    }
144}
145
146/// Container type for all chain info fields
147#[derive(Debug)]
148struct ChainInfoInner<N: NodePrimitives = reth_ethereum_primitives::EthPrimitives> {
149    /// Timestamp when we received the last fork choice update.
150    ///
151    /// This is mainly used to track if we're connected to a beacon node.
152    last_forkchoice_update: RwLock<Option<Instant>>,
153
154    /// Tracks the number of the `canonical_head`.
155    canonical_head_number: AtomicU64,
156    /// The canonical head of the chain.
157    canonical_head: RwLock<SealedHeader<N::BlockHeader>>,
158    /// The block that the beacon node considers safe.
159    safe_block: watch::Sender<Option<SealedHeader<N::BlockHeader>>>,
160    /// The block that the beacon node considers finalized.
161    finalized_block: watch::Sender<Option<SealedHeader<N::BlockHeader>>>,
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use reth_ethereum_primitives::EthPrimitives;
168    use reth_testing_utils::{generators, generators::random_header};
169
170    #[test]
171    fn test_chain_info() {
172        // Create a random header
173        let mut rng = generators::rng();
174        let header = random_header(&mut rng, 10, None);
175
176        // Create a new chain info tracker with the header
177        let tracker: ChainInfoTracker<EthPrimitives> =
178            ChainInfoTracker::new(header.clone(), None, None);
179
180        // Fetch the chain information from the tracker
181        let chain_info = tracker.chain_info();
182
183        // Verify that the chain information matches the header
184        assert_eq!(chain_info.best_number, header.number);
185        assert_eq!(chain_info.best_hash, header.hash());
186    }
187
188    #[test]
189    fn test_on_forkchoice_update_received() {
190        // Create a random block header
191        let mut rng = generators::rng();
192        let header = random_header(&mut rng, 10, None);
193
194        // Create a new chain info tracker with the header
195        let tracker: ChainInfoTracker<EthPrimitives> = ChainInfoTracker::new(header, None, None);
196
197        // Assert that there has been no forkchoice update yet (the timestamp is None)
198        assert!(tracker.last_forkchoice_update_received_at().is_none());
199
200        // Call the method to record the receipt of a forkchoice update
201        tracker.on_forkchoice_update_received();
202
203        // Assert that there is now a timestamp indicating when the forkchoice update was received
204        assert!(tracker.last_forkchoice_update_received_at().is_some());
205    }
206
207    #[test]
208    fn test_set_canonical_head() {
209        // Create a random number generator
210        let mut rng = generators::rng();
211        // Generate two random headers for testing
212        let header1 = random_header(&mut rng, 10, None);
213        let header2 = random_header(&mut rng, 20, None);
214
215        // Create a new chain info tracker with the first header
216        let tracker: ChainInfoTracker<EthPrimitives> = ChainInfoTracker::new(header1, None, None);
217
218        // Set the second header as the canonical head of the tracker
219        tracker.set_canonical_head(header2.clone());
220
221        // Assert that the tracker now uses the second header as its canonical head
222        let canonical_head = tracker.get_canonical_head();
223        assert_eq!(canonical_head, header2);
224    }
225
226    #[test]
227    fn test_set_safe() {
228        // Create a random number generator
229        let mut rng = generators::rng();
230
231        // Case 1: basic test
232        // Generate two random headers for the test
233        let header1 = random_header(&mut rng, 10, None);
234        let header2 = random_header(&mut rng, 20, None);
235
236        // Create a new chain info tracker with the first header (header1)
237        let tracker: ChainInfoTracker<EthPrimitives> = ChainInfoTracker::new(header1, None, None);
238
239        // Call the set_safe method with the second header (header2)
240        tracker.set_safe(header2.clone());
241
242        // Verify that the tracker now has header2 as the safe block
243        let safe_header = tracker.get_safe_header();
244        assert!(safe_header.is_some()); // Ensure a safe header is present
245        let safe_header = safe_header.unwrap();
246        assert_eq!(safe_header, header2);
247
248        // Case 2: call with the same header as the current safe block
249        // Call set_safe again with the same header (header2)
250        tracker.set_safe(header2.clone());
251
252        // Verify that nothing changes and the safe header remains the same
253        let same_safe_header = tracker.get_safe_header();
254        assert!(same_safe_header.is_some());
255        let same_safe_header = same_safe_header.unwrap();
256        assert_eq!(same_safe_header, header2);
257
258        // Case 3: call with a different (new) header
259        // Generate a third header with a higher block number
260        let header3 = random_header(&mut rng, 30, None);
261
262        // Call set_safe with this new header (header3)
263        tracker.set_safe(header3.clone());
264
265        // Verify that the safe header is updated with the new header
266        let updated_safe_header = tracker.get_safe_header();
267        assert!(updated_safe_header.is_some());
268        let updated_safe_header = updated_safe_header.unwrap();
269        assert_eq!(updated_safe_header, header3);
270    }
271
272    #[test]
273    fn test_set_finalized() {
274        // Create a random number generator
275        let mut rng = generators::rng();
276
277        // Generate random headers for testing
278        let header1 = random_header(&mut rng, 10, None);
279        let header2 = random_header(&mut rng, 20, None);
280        let header3 = random_header(&mut rng, 30, None);
281
282        // Create a new chain info tracker with the first header
283        let tracker: ChainInfoTracker<EthPrimitives> = ChainInfoTracker::new(header1, None, None);
284
285        // Initial state: finalize header should be None
286        assert!(tracker.get_finalized_header().is_none());
287
288        // Set the second header as the finalized header
289        tracker.set_finalized(header2.clone());
290
291        // Assert that the tracker now uses the second header as its finalized block
292        let finalized_header = tracker.get_finalized_header();
293        assert!(finalized_header.is_some());
294        let finalized_header = finalized_header.unwrap();
295        assert_eq!(finalized_header, header2);
296
297        // Case 2: attempt to set the same finalized header again
298        tracker.set_finalized(header2.clone());
299
300        // The finalized header should remain unchanged
301        let unchanged_finalized_header = tracker.get_finalized_header();
302        assert_eq!(unchanged_finalized_header.unwrap(), header2); // Should still be header2
303
304        // Case 3: set a higher block number as finalized
305        tracker.set_finalized(header3.clone());
306
307        // The finalized header should now be updated to header3
308        let updated_finalized_header = tracker.get_finalized_header();
309        assert!(updated_finalized_header.is_some());
310        assert_eq!(updated_finalized_header.unwrap(), header3);
311    }
312
313    #[test]
314    fn test_get_finalized_num_hash() {
315        // Create a random header
316        let mut rng = generators::rng();
317        let finalized_header = random_header(&mut rng, 10, None);
318
319        // Create a new chain info tracker with the finalized header
320        let tracker: ChainInfoTracker<EthPrimitives> =
321            ChainInfoTracker::new(finalized_header.clone(), Some(finalized_header.clone()), None);
322
323        // Assert that the BlockNumHash returned matches the finalized header
324        assert_eq!(tracker.get_finalized_num_hash(), Some(finalized_header.num_hash()));
325    }
326
327    #[test]
328    fn test_get_safe_num_hash() {
329        // Create a random header
330        let mut rng = generators::rng();
331        let safe_header = random_header(&mut rng, 10, None);
332
333        // Create a new chain info tracker with the safe header
334        let tracker: ChainInfoTracker<EthPrimitives> =
335            ChainInfoTracker::new(safe_header.clone(), None, None);
336        tracker.set_safe(safe_header.clone());
337
338        // Assert that the BlockNumHash returned matches the safe header
339        assert_eq!(tracker.get_safe_num_hash(), Some(safe_header.num_hash()));
340    }
341}