1use alloy_primitives::B256;
2use alloy_rpc_types_engine::{ForkchoiceState, PayloadStatusEnum};
34/// 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.
10latest: Option<ReceivedForkchoiceState>,
11/// Tracks the latest forkchoice state that we received to which we need to sync.
12last_syncing: Option<ForkchoiceState>,
13/// The latest valid forkchoice state that we received and processed as valid.
14last_valid: Option<ForkchoiceState>,
15}
1617impl 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.
22pub fn set_latest(&mut self, state: ForkchoiceState, status: ForkchoiceStatus) {
23if status.is_valid() {
24self.set_valid(state);
25 } else if status.is_syncing() {
26self.last_syncing = Some(state);
27 }
2829let received = ReceivedForkchoiceState { state, status };
30self.latest = Some(received);
31 }
3233fn set_valid(&mut self, state: ForkchoiceState) {
34// we no longer need to sync to this state.
35self.last_syncing = None;
3637self.last_valid = Some(state);
38 }
3940/// Returns the [`ForkchoiceStatus`] of the latest received FCU.
41 ///
42 /// Caution: this can be invalid.
43pub(crate) fn latest_status(&self) -> Option<ForkchoiceStatus> {
44self.latest.as_ref().map(|s| s.status)
45 }
4647/// Returns whether the latest received FCU is valid: [`ForkchoiceStatus::Valid`]
48#[allow(dead_code)]
49pub(crate) fn is_latest_valid(&self) -> bool {
50self.latest_status().is_some_and(|s| s.is_valid())
51 }
5253/// Returns whether the latest received FCU is syncing: [`ForkchoiceStatus::Syncing`]
54#[allow(dead_code)]
55pub(crate) fn is_latest_syncing(&self) -> bool {
56self.latest_status().is_some_and(|s| s.is_syncing())
57 }
5859/// Returns whether the latest received FCU is syncing: [`ForkchoiceStatus::Invalid`]
60#[allow(dead_code)]
61pub fn is_latest_invalid(&self) -> bool {
62self.latest_status().is_some_and(|s| s.is_invalid())
63 }
6465/// Returns the last valid head hash.
66#[allow(dead_code)]
67pub fn last_valid_head(&self) -> Option<B256> {
68self.last_valid.as_ref().map(|s| s.head_block_hash)
69 }
7071/// Returns the head hash of the latest received FCU to which we need to sync.
72#[allow(dead_code)]
73pub(crate) fn sync_target(&self) -> Option<B256> {
74self.last_syncing.as_ref().map(|s| s.head_block_hash)
75 }
7677/// Returns the latest received [`ForkchoiceState`].
78 ///
79 /// Caution: this can be invalid.
80pub const fn latest_state(&self) -> Option<ForkchoiceState> {
81self.last_valid
82 }
8384/// Returns the last valid [`ForkchoiceState`].
85pub const fn last_valid_state(&self) -> Option<ForkchoiceState> {
86self.last_valid
87 }
8889/// 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]
95pub fn last_valid_finalized(&self) -> Option<B256> {
96self.last_valid
97 .filter(|state| !state.finalized_block_hash.is_zero())
98 .map(|state| state.finalized_block_hash)
99 }
100101/// Returns the last received `ForkchoiceState` to which we need to sync.
102pub const fn sync_target_state(&self) -> Option<ForkchoiceState> {
103self.last_syncing
104 }
105106/// 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]
112pub fn sync_target_finalized(&self) -> Option<B256> {
113self.last_syncing
114 .filter(|state| !state.finalized_block_hash.is_zero())
115 .map(|state| state.finalized_block_hash)
116 }
117118/// Returns true if no forkchoice state has been received yet.
119pub const fn is_empty(&self) -> bool {
120self.latest.is_none()
121 }
122}
123124/// 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}
131132/// A simplified representation of [`PayloadStatusEnum`] specifically for FCU.
133#[derive(Debug, Clone, Copy, Eq, PartialEq)]
134pub enum ForkchoiceStatus {
135/// The forkchoice state is valid.
136Valid,
137/// The forkchoice state is invalid.
138Invalid,
139/// The forkchoice state is unknown.
140Syncing,
141}
142143impl ForkchoiceStatus {
144/// Returns `true` if the forkchoice state is [`ForkchoiceStatus::Valid`].
145pub const fn is_valid(&self) -> bool {
146matches!(self, Self::Valid)
147 }
148149/// Returns `true` if the forkchoice state is [`ForkchoiceStatus::Invalid`].
150pub const fn is_invalid(&self) -> bool {
151matches!(self, Self::Invalid)
152 }
153154/// Returns `true` if the forkchoice state is [`ForkchoiceStatus::Syncing`].
155pub const fn is_syncing(&self) -> bool {
156matches!(self, Self::Syncing)
157 }
158159/// Converts the general purpose [`PayloadStatusEnum`] into a [`ForkchoiceStatus`].
160pub(crate) const fn from_payload_status(status: &PayloadStatusEnum) -> Self {
161match status {
162 PayloadStatusEnum::Valid | PayloadStatusEnum::Accepted => {
163// `Accepted` is only returned on `newPayload`. It would be a valid state here.
164Self::Valid
165 }
166 PayloadStatusEnum::Invalid { .. } => Self::Invalid,
167 PayloadStatusEnum::Syncing => Self::Syncing,
168 }
169 }
170}
171172impl From<PayloadStatusEnum> for ForkchoiceStatus {
173fn from(status: PayloadStatusEnum) -> Self {
174Self::from_payload_status(&status)
175 }
176}
177178/// 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`].
182Head(B256),
183/// Safe hash of the [`ForkchoiceState`].
184Safe(B256),
185/// Finalized hash of the [`ForkchoiceState`].
186Finalized(B256),
187}
188189impl ForkchoiceStateHash {
190/// Tries to find a matching hash in the given [`ForkchoiceState`].
191pub fn find(state: &ForkchoiceState, hash: B256) -> Option<Self> {
192if state.head_block_hash == hash {
193Some(Self::Head(hash))
194 } else if state.safe_block_hash == hash {
195Some(Self::Safe(hash))
196 } else if state.finalized_block_hash == hash {
197Some(Self::Finalized(hash))
198 } else {
199None200 }
201 }
202203/// Returns true if this is the head hash of the [`ForkchoiceState`]
204pub const fn is_head(&self) -> bool {
205matches!(self, Self::Head(_))
206 }
207}
208209impl AsRef<B256> for ForkchoiceStateHash {
210fn as_ref(&self) -> &B256 {
211match self{
212Self::Head(h) | Self::Safe(h) | Self::Finalized(h) => h,
213 }
214 }
215}
216217#[cfg(test)]
218mod tests {
219use super::*;
220221#[test]
222fn test_forkchoice_state_tracker_set_latest_valid() {
223let mut tracker = ForkchoiceStateTracker::default();
224225// Latest state is None
226assert!(tracker.latest_status().is_none());
227228// Create a valid ForkchoiceState
229let 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 };
234let status = ForkchoiceStatus::Valid;
235236 tracker.set_latest(state, status);
237238// Assert that the latest state is set
239assert!(tracker.latest.is_some());
240assert_eq!(tracker.latest.as_ref().unwrap().state, state);
241242// Assert that last valid state is updated
243assert!(tracker.last_valid.is_some());
244assert_eq!(tracker.last_valid.as_ref().unwrap(), &state);
245246// Assert that last syncing state is None
247assert!(tracker.last_syncing.is_none());
248249// Test when there is a latest status and it is valid
250assert_eq!(tracker.latest_status(), Some(ForkchoiceStatus::Valid));
251 }
252253#[test]
254fn test_forkchoice_state_tracker_set_latest_syncing() {
255let mut tracker = ForkchoiceStateTracker::default();
256257// Create a syncing ForkchoiceState
258let 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};
263let status = ForkchoiceStatus::Syncing;
264265 tracker.set_latest(state, status);
266267// Assert that the latest state is set
268assert!(tracker.latest.is_some());
269assert_eq!(tracker.latest.as_ref().unwrap().state, state);
270271// Assert that last valid state is None since the status is syncing
272assert!(tracker.last_valid.is_none());
273274// Assert that last syncing state is updated
275assert!(tracker.last_syncing.is_some());
276assert_eq!(tracker.last_syncing.as_ref().unwrap(), &state);
277278// Test when there is a latest status and it is syncing
279assert_eq!(tracker.latest_status(), Some(ForkchoiceStatus::Syncing));
280 }
281282#[test]
283fn test_forkchoice_state_tracker_set_latest_invalid() {
284let mut tracker = ForkchoiceStateTracker::default();
285286// Create an invalid ForkchoiceState
287let 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 };
292let status = ForkchoiceStatus::Invalid;
293294 tracker.set_latest(state, status);
295296// Assert that the latest state is set
297assert!(tracker.latest.is_some());
298assert_eq!(tracker.latest.as_ref().unwrap().state, state);
299300// Assert that last valid state is None since the status is invalid
301assert!(tracker.last_valid.is_none());
302303// Assert that last syncing state is None since the status is invalid
304assert!(tracker.last_syncing.is_none());
305306// Test when there is a latest status and it is invalid
307assert_eq!(tracker.latest_status(), Some(ForkchoiceStatus::Invalid));
308 }
309310#[test]
311fn test_forkchoice_state_tracker_sync_target() {
312let mut tracker = ForkchoiceStateTracker::default();
313314// Test when there is no last syncing state (should return None)
315assert!(tracker.sync_target().is_none());
316317// Set a last syncing forkchoice state
318let 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);
324325// Test when the last syncing state is set (should return the head block hash)
326assert_eq!(tracker.sync_target(), Some(B256::from_slice(&[1; 32])));
327 }
328329#[test]
330fn test_forkchoice_state_tracker_last_valid_finalized() {
331let mut tracker = ForkchoiceStateTracker::default();
332333// No valid finalized state (should return None)
334assert!(tracker.last_valid_finalized().is_none());
335336// Valid finalized state, but finalized hash is zero (should return None)
337let 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);
343assert!(tracker.last_valid_finalized().is_none());
344345// Valid finalized state with non-zero finalized hash (should return finalized hash)
346let 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);
352assert_eq!(tracker.last_valid_finalized(), Some(B256::from_slice(&[123; 32])));
353354// Reset the last valid state to None
355tracker.last_valid = None;
356assert!(tracker.last_valid_finalized().is_none());
357 }
358359#[test]
360fn test_forkchoice_state_tracker_sync_target_finalized() {
361let mut tracker = ForkchoiceStateTracker::default();
362363// No sync target state (should return None)
364assert!(tracker.sync_target_finalized().is_none());
365366// Sync target state with finalized hash as zero (should return None)
367let 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);
373assert!(tracker.sync_target_finalized().is_none());
374375// Sync target state with non-zero finalized hash (should return the hash)
376let 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);
382assert_eq!(tracker.sync_target_finalized(), Some(B256::from_slice(&[22; 32])));
383384// Reset the last sync target state to None
385tracker.last_syncing = None;
386assert!(tracker.sync_target_finalized().is_none());
387 }
388389#[test]
390fn test_forkchoice_state_tracker_is_empty() {
391let mut forkchoice = ForkchoiceStateTracker::default();
392393// Initially, no forkchoice state has been received, so it should be empty.
394assert!(forkchoice.is_empty());
395396// After setting a forkchoice state, it should no longer be empty.
397forkchoice.set_latest(ForkchoiceState::default(), ForkchoiceStatus::Valid);
398assert!(!forkchoice.is_empty());
399400// Reset the forkchoice latest, it should be empty again.
401forkchoice.latest = None;
402assert!(forkchoice.is_empty());
403 }
404405#[test]
406fn test_forkchoice_state_hash_find() {
407// Define example hashes
408let head_hash = B256::random();
409let safe_hash = B256::random();
410let finalized_hash = B256::random();
411let non_matching_hash = B256::random();
412413// Create a ForkchoiceState with specific hashes
414let state = ForkchoiceState {
415 head_block_hash: head_hash,
416 safe_block_hash: safe_hash,
417 finalized_block_hash: finalized_hash,
418 };
419420// Test finding the head hash
421assert_eq!(
422 ForkchoiceStateHash::find(&state, head_hash),
423Some(ForkchoiceStateHash::Head(head_hash))
424 );
425426// Test finding the safe hash
427assert_eq!(
428 ForkchoiceStateHash::find(&state, safe_hash),
429Some(ForkchoiceStateHash::Safe(safe_hash))
430 );
431432// Test finding the finalized hash
433assert_eq!(
434 ForkchoiceStateHash::find(&state, finalized_hash),
435Some(ForkchoiceStateHash::Finalized(finalized_hash))
436 );
437438// Test with a hash that doesn't match any of the hashes in ForkchoiceState
439assert_eq!(ForkchoiceStateHash::find(&state, non_matching_hash), None);
440 }
441}