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