Skip to main content

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