Skip to main content

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