reth_engine_primitives/
forkchoice.rs

1use alloy_primitives::B256;
2use alloy_rpc_types_engine::{ForkchoiceState, PayloadStatusEnum};
3
4/// The struct that keeps track of the received forkchoice state and their status.
5#[derive(Debug, Clone, Default)]
6pub struct ForkchoiceStateTracker {
7    /// The latest forkchoice state that we received.
8    ///
9    /// Caution: this can be invalid.
10    latest: Option<ReceivedForkchoiceState>,
11    /// Tracks the latest forkchoice state that we received to which we need to sync.
12    last_syncing: Option<ForkchoiceState>,
13    /// The latest valid forkchoice state that we received and processed as valid.
14    last_valid: Option<ForkchoiceState>,
15}
16
17impl ForkchoiceStateTracker {
18    /// Sets the latest forkchoice state that we received.
19    ///
20    /// If the status is `VALID`, we also update the last valid forkchoice state and set the
21    /// `sync_target` to `None`, since we're now fully synced.
22    pub const fn set_latest(&mut self, state: ForkchoiceState, status: ForkchoiceStatus) {
23        if status.is_valid() {
24            self.set_valid(state);
25        } else if status.is_syncing() {
26            self.last_syncing = Some(state);
27        }
28
29        let received = ReceivedForkchoiceState { state, status };
30        self.latest = Some(received);
31    }
32
33    const fn set_valid(&mut self, state: ForkchoiceState) {
34        // we no longer need to sync to this state.
35        self.last_syncing = None;
36
37        self.last_valid = Some(state);
38    }
39
40    /// Returns the [`ForkchoiceStatus`] of the latest received FCU.
41    ///
42    /// Caution: this can be invalid.
43    pub(crate) fn latest_status(&self) -> Option<ForkchoiceStatus> {
44        self.latest.as_ref().map(|s| s.status)
45    }
46
47    /// Returns whether the latest received FCU is valid: [`ForkchoiceStatus::Valid`]
48    #[expect(dead_code)]
49    pub(crate) fn is_latest_valid(&self) -> bool {
50        self.latest_status().is_some_and(|s| s.is_valid())
51    }
52
53    /// Returns whether the latest received FCU is syncing: [`ForkchoiceStatus::Syncing`]
54    #[expect(dead_code)]
55    pub(crate) fn is_latest_syncing(&self) -> bool {
56        self.latest_status().is_some_and(|s| s.is_syncing())
57    }
58
59    /// Returns whether the latest received FCU is syncing: [`ForkchoiceStatus::Invalid`]
60    pub fn is_latest_invalid(&self) -> bool {
61        self.latest_status().is_some_and(|s| s.is_invalid())
62    }
63
64    /// Returns the last valid head hash.
65    pub fn last_valid_head(&self) -> Option<B256> {
66        self.last_valid.as_ref().map(|s| s.head_block_hash)
67    }
68
69    /// Returns the head hash of the latest received FCU to which we need to sync.
70    #[cfg_attr(not(test), expect(dead_code))]
71    pub(crate) fn sync_target(&self) -> Option<B256> {
72        self.last_syncing.as_ref().map(|s| s.head_block_hash)
73    }
74
75    /// Returns the latest received [`ForkchoiceState`].
76    ///
77    /// Caution: this can be invalid.
78    pub fn latest_state(&self) -> Option<ForkchoiceState> {
79        self.latest.as_ref().map(|s| s.state)
80    }
81
82    /// Returns the last valid [`ForkchoiceState`].
83    pub const fn last_valid_state(&self) -> Option<ForkchoiceState> {
84        self.last_valid
85    }
86
87    /// Returns the last valid finalized hash.
88    ///
89    /// This will return [`None`]:
90    /// - If either there is no valid finalized forkchoice state,
91    /// - Or the finalized hash for the latest valid forkchoice state is zero.
92    #[inline]
93    pub fn last_valid_finalized(&self) -> Option<B256> {
94        self.last_valid
95            .filter(|state| !state.finalized_block_hash.is_zero())
96            .map(|state| state.finalized_block_hash)
97    }
98
99    /// Returns the last received `ForkchoiceState` to which we need to sync.
100    pub const fn sync_target_state(&self) -> Option<ForkchoiceState> {
101        self.last_syncing
102    }
103
104    /// Returns the sync target finalized hash.
105    ///
106    /// This will return [`None`]:
107    /// - If either there is no sync target forkchoice state,
108    /// - Or the finalized hash for the sync target forkchoice state is zero.
109    #[inline]
110    pub fn sync_target_finalized(&self) -> Option<B256> {
111        self.last_syncing
112            .filter(|state| !state.finalized_block_hash.is_zero())
113            .map(|state| state.finalized_block_hash)
114    }
115
116    /// Returns true if no forkchoice state has been received yet.
117    pub const fn is_empty(&self) -> bool {
118        self.latest.is_none()
119    }
120}
121
122/// Represents a forkchoice update and tracks the status we assigned to it.
123#[derive(Debug, Clone)]
124pub(crate) struct ReceivedForkchoiceState {
125    state: ForkchoiceState,
126    status: ForkchoiceStatus,
127}
128
129/// A simplified representation of [`PayloadStatusEnum`] specifically for FCU.
130#[derive(Debug, Clone, Copy, Eq, PartialEq)]
131pub enum ForkchoiceStatus {
132    /// The forkchoice state is valid.
133    Valid,
134    /// The forkchoice state is invalid.
135    Invalid,
136    /// The forkchoice state is unknown.
137    Syncing,
138}
139
140impl ForkchoiceStatus {
141    /// Returns `true` if the forkchoice state is [`ForkchoiceStatus::Valid`].
142    pub const fn is_valid(&self) -> bool {
143        matches!(self, Self::Valid)
144    }
145
146    /// Returns `true` if the forkchoice state is [`ForkchoiceStatus::Invalid`].
147    pub const fn is_invalid(&self) -> bool {
148        matches!(self, Self::Invalid)
149    }
150
151    /// Returns `true` if the forkchoice state is [`ForkchoiceStatus::Syncing`].
152    pub const fn is_syncing(&self) -> bool {
153        matches!(self, Self::Syncing)
154    }
155
156    /// Converts the general purpose [`PayloadStatusEnum`] into a [`ForkchoiceStatus`].
157    pub(crate) const fn from_payload_status(status: &PayloadStatusEnum) -> Self {
158        match status {
159            PayloadStatusEnum::Valid | PayloadStatusEnum::Accepted => {
160                // `Accepted` is only returned on `newPayload`. It would be a valid state here.
161                Self::Valid
162            }
163            PayloadStatusEnum::Invalid { .. } => Self::Invalid,
164            PayloadStatusEnum::Syncing => Self::Syncing,
165        }
166    }
167}
168
169impl From<PayloadStatusEnum> for ForkchoiceStatus {
170    fn from(status: PayloadStatusEnum) -> Self {
171        Self::from_payload_status(&status)
172    }
173}
174
175/// A helper type to check represent hashes of a [`ForkchoiceState`]
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub enum ForkchoiceStateHash {
178    /// Head hash of the [`ForkchoiceState`].
179    Head(B256),
180    /// Safe hash of the [`ForkchoiceState`].
181    Safe(B256),
182    /// Finalized hash of the [`ForkchoiceState`].
183    Finalized(B256),
184}
185
186impl ForkchoiceStateHash {
187    /// Tries to find a matching hash in the given [`ForkchoiceState`].
188    pub fn find(state: &ForkchoiceState, hash: B256) -> Option<Self> {
189        if state.head_block_hash == hash {
190            Some(Self::Head(hash))
191        } else if state.safe_block_hash == hash {
192            Some(Self::Safe(hash))
193        } else if state.finalized_block_hash == hash {
194            Some(Self::Finalized(hash))
195        } else {
196            None
197        }
198    }
199
200    /// Returns true if this is the head hash of the [`ForkchoiceState`]
201    pub const fn is_head(&self) -> bool {
202        matches!(self, Self::Head(_))
203    }
204}
205
206impl AsRef<B256> for ForkchoiceStateHash {
207    fn as_ref(&self) -> &B256 {
208        match self {
209            Self::Head(h) | Self::Safe(h) | Self::Finalized(h) => h,
210        }
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_forkchoice_state_tracker_set_latest_valid() {
220        let mut tracker = ForkchoiceStateTracker::default();
221
222        // Latest state is None
223        assert!(tracker.latest_status().is_none());
224
225        // Create a valid ForkchoiceState
226        let state = ForkchoiceState {
227            head_block_hash: B256::from_slice(&[1; 32]),
228            safe_block_hash: B256::from_slice(&[2; 32]),
229            finalized_block_hash: B256::from_slice(&[3; 32]),
230        };
231        let status = ForkchoiceStatus::Valid;
232
233        tracker.set_latest(state, status);
234
235        // Assert that the latest state is set
236        assert!(tracker.latest.is_some());
237        assert_eq!(tracker.latest.as_ref().unwrap().state, state);
238
239        // Assert that last valid state is updated
240        assert!(tracker.last_valid.is_some());
241        assert_eq!(tracker.last_valid.as_ref().unwrap(), &state);
242
243        // Assert that last syncing state is None
244        assert!(tracker.last_syncing.is_none());
245
246        // Test when there is a latest status and it is valid
247        assert_eq!(tracker.latest_status(), Some(ForkchoiceStatus::Valid));
248    }
249
250    #[test]
251    fn test_forkchoice_state_tracker_set_latest_syncing() {
252        let mut tracker = ForkchoiceStateTracker::default();
253
254        // Create a syncing ForkchoiceState
255        let state = ForkchoiceState {
256            head_block_hash: B256::from_slice(&[1; 32]),
257            safe_block_hash: B256::from_slice(&[2; 32]),
258            finalized_block_hash: B256::from_slice(&[0; 32]), // Zero to simulate not finalized
259        };
260        let status = ForkchoiceStatus::Syncing;
261
262        tracker.set_latest(state, status);
263
264        // Assert that the latest state is set
265        assert!(tracker.latest.is_some());
266        assert_eq!(tracker.latest.as_ref().unwrap().state, state);
267
268        // Assert that last valid state is None since the status is syncing
269        assert!(tracker.last_valid.is_none());
270
271        // Assert that last syncing state is updated
272        assert!(tracker.last_syncing.is_some());
273        assert_eq!(tracker.last_syncing.as_ref().unwrap(), &state);
274
275        // Test when there is a latest status and it is syncing
276        assert_eq!(tracker.latest_status(), Some(ForkchoiceStatus::Syncing));
277    }
278
279    #[test]
280    fn test_forkchoice_state_tracker_set_latest_invalid() {
281        let mut tracker = ForkchoiceStateTracker::default();
282
283        // Create an invalid ForkchoiceState
284        let state = ForkchoiceState {
285            head_block_hash: B256::from_slice(&[1; 32]),
286            safe_block_hash: B256::from_slice(&[2; 32]),
287            finalized_block_hash: B256::from_slice(&[3; 32]),
288        };
289        let status = ForkchoiceStatus::Invalid;
290
291        tracker.set_latest(state, status);
292
293        // Assert that the latest state is set
294        assert!(tracker.latest.is_some());
295        assert_eq!(tracker.latest.as_ref().unwrap().state, state);
296
297        // Assert that last valid state is None since the status is invalid
298        assert!(tracker.last_valid.is_none());
299
300        // Assert that last syncing state is None since the status is invalid
301        assert!(tracker.last_syncing.is_none());
302
303        // Test when there is a latest status and it is invalid
304        assert_eq!(tracker.latest_status(), Some(ForkchoiceStatus::Invalid));
305    }
306
307    #[test]
308    fn test_forkchoice_state_tracker_sync_target() {
309        let mut tracker = ForkchoiceStateTracker::default();
310
311        // Test when there is no last syncing state (should return None)
312        assert!(tracker.sync_target().is_none());
313
314        // Set a last syncing forkchoice state
315        let state = ForkchoiceState {
316            head_block_hash: B256::from_slice(&[1; 32]),
317            safe_block_hash: B256::from_slice(&[2; 32]),
318            finalized_block_hash: B256::from_slice(&[3; 32]),
319        };
320        tracker.last_syncing = Some(state);
321
322        // Test when the last syncing state is set (should return the head block hash)
323        assert_eq!(tracker.sync_target(), Some(B256::from_slice(&[1; 32])));
324    }
325
326    #[test]
327    fn test_forkchoice_state_tracker_last_valid_finalized() {
328        let mut tracker = ForkchoiceStateTracker::default();
329
330        // No valid finalized state (should return None)
331        assert!(tracker.last_valid_finalized().is_none());
332
333        // Valid finalized state, but finalized hash is zero (should return None)
334        let zero_finalized_state = ForkchoiceState {
335            head_block_hash: B256::ZERO,
336            safe_block_hash: B256::ZERO,
337            finalized_block_hash: B256::ZERO, // Zero finalized hash
338        };
339        tracker.last_valid = Some(zero_finalized_state);
340        assert!(tracker.last_valid_finalized().is_none());
341
342        // Valid finalized state with non-zero finalized hash (should return finalized hash)
343        let valid_finalized_state = ForkchoiceState {
344            head_block_hash: B256::from_slice(&[1; 32]),
345            safe_block_hash: B256::from_slice(&[2; 32]),
346            finalized_block_hash: B256::from_slice(&[123; 32]), // Non-zero finalized hash
347        };
348        tracker.last_valid = Some(valid_finalized_state);
349        assert_eq!(tracker.last_valid_finalized(), Some(B256::from_slice(&[123; 32])));
350
351        // Reset the last valid state to None
352        tracker.last_valid = None;
353        assert!(tracker.last_valid_finalized().is_none());
354    }
355
356    #[test]
357    fn test_forkchoice_state_tracker_sync_target_finalized() {
358        let mut tracker = ForkchoiceStateTracker::default();
359
360        // No sync target state (should return None)
361        assert!(tracker.sync_target_finalized().is_none());
362
363        // Sync target state with finalized hash as zero (should return None)
364        let zero_finalized_sync_target = ForkchoiceState {
365            head_block_hash: B256::from_slice(&[1; 32]),
366            safe_block_hash: B256::from_slice(&[2; 32]),
367            finalized_block_hash: B256::ZERO, // Zero finalized hash
368        };
369        tracker.last_syncing = Some(zero_finalized_sync_target);
370        assert!(tracker.sync_target_finalized().is_none());
371
372        // Sync target state with non-zero finalized hash (should return the hash)
373        let valid_sync_target = ForkchoiceState {
374            head_block_hash: B256::from_slice(&[1; 32]),
375            safe_block_hash: B256::from_slice(&[2; 32]),
376            finalized_block_hash: B256::from_slice(&[22; 32]), // Non-zero finalized hash
377        };
378        tracker.last_syncing = Some(valid_sync_target);
379        assert_eq!(tracker.sync_target_finalized(), Some(B256::from_slice(&[22; 32])));
380
381        // Reset the last sync target state to None
382        tracker.last_syncing = None;
383        assert!(tracker.sync_target_finalized().is_none());
384    }
385
386    #[test]
387    fn test_forkchoice_state_tracker_is_empty() {
388        let mut forkchoice = ForkchoiceStateTracker::default();
389
390        // Initially, no forkchoice state has been received, so it should be empty.
391        assert!(forkchoice.is_empty());
392
393        // After setting a forkchoice state, it should no longer be empty.
394        forkchoice.set_latest(ForkchoiceState::default(), ForkchoiceStatus::Valid);
395        assert!(!forkchoice.is_empty());
396
397        // Reset the forkchoice latest, it should be empty again.
398        forkchoice.latest = None;
399        assert!(forkchoice.is_empty());
400    }
401
402    #[test]
403    fn test_forkchoice_state_hash_find() {
404        // Define example hashes
405        let head_hash = B256::random();
406        let safe_hash = B256::random();
407        let finalized_hash = B256::random();
408        let non_matching_hash = B256::random();
409
410        // Create a ForkchoiceState with specific hashes
411        let state = ForkchoiceState {
412            head_block_hash: head_hash,
413            safe_block_hash: safe_hash,
414            finalized_block_hash: finalized_hash,
415        };
416
417        // Test finding the head hash
418        assert_eq!(
419            ForkchoiceStateHash::find(&state, head_hash),
420            Some(ForkchoiceStateHash::Head(head_hash))
421        );
422
423        // Test finding the safe hash
424        assert_eq!(
425            ForkchoiceStateHash::find(&state, safe_hash),
426            Some(ForkchoiceStateHash::Safe(safe_hash))
427        );
428
429        // Test finding the finalized hash
430        assert_eq!(
431            ForkchoiceStateHash::find(&state, finalized_hash),
432            Some(ForkchoiceStateHash::Finalized(finalized_hash))
433        );
434
435        // Test with a hash that doesn't match any of the hashes in ForkchoiceState
436        assert_eq!(ForkchoiceStateHash::find(&state, non_matching_hash), None);
437    }
438}