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