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