1use alloy_primitives::B256;
2use alloy_rpc_types_engine::{ForkchoiceState, PayloadStatusEnum};
3
4#[derive(Debug, Clone, Default)]
6pub struct ForkchoiceStateTracker {
7 latest: Option<ReceivedForkchoiceState>,
11 last_syncing: Option<ForkchoiceState>,
13 last_valid: Option<ForkchoiceState>,
15}
16
17impl ForkchoiceStateTracker {
18 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 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 pub(crate) fn latest_status(&self) -> Option<ForkchoiceStatus> {
58 self.latest.as_ref().map(|s| s.status)
59 }
60
61 #[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 #[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 pub fn is_latest_invalid(&self) -> bool {
75 self.latest_status().is_some_and(|s| s.is_invalid())
76 }
77
78 pub fn last_valid_head(&self) -> Option<B256> {
80 self.last_valid.as_ref().map(|s| s.head_block_hash)
81 }
82
83 #[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 pub fn latest_state(&self) -> Option<ForkchoiceState> {
93 self.latest.as_ref().map(|s| s.state)
94 }
95
96 pub const fn last_valid_state(&self) -> Option<ForkchoiceState> {
98 self.last_valid
99 }
100
101 #[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 pub const fn sync_target_state(&self) -> Option<ForkchoiceState> {
115 self.last_syncing
116 }
117
118 #[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 pub const fn is_empty(&self) -> bool {
132 self.latest.is_none()
133 }
134}
135
136#[derive(Debug, Clone)]
138pub(crate) struct ReceivedForkchoiceState {
139 state: ForkchoiceState,
140 status: ForkchoiceStatus,
141}
142
143#[derive(Debug, Clone, Copy, Eq, PartialEq)]
145pub enum ForkchoiceStatus {
146 Valid,
148 Invalid,
150 Syncing,
152}
153
154impl ForkchoiceStatus {
155 pub const fn is_valid(&self) -> bool {
157 matches!(self, Self::Valid)
158 }
159
160 pub const fn is_invalid(&self) -> bool {
162 matches!(self, Self::Invalid)
163 }
164
165 pub const fn is_syncing(&self) -> bool {
167 matches!(self, Self::Syncing)
168 }
169
170 pub(crate) const fn from_payload_status(status: &PayloadStatusEnum) -> Self {
172 match status {
173 PayloadStatusEnum::Valid | PayloadStatusEnum::Accepted => {
174 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191pub enum ForkchoiceStateHash {
192 Head(B256),
194 Safe(B256),
196 Finalized(B256),
198}
199
200impl ForkchoiceStateHash {
201 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 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 assert!(tracker.latest_status().is_none());
238
239 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!(tracker.latest.is_some());
251 assert_eq!(tracker.latest.as_ref().unwrap().state, state);
252
253 assert!(tracker.last_valid.is_some());
255 assert_eq!(tracker.last_valid.as_ref().unwrap(), &state);
256
257 assert!(tracker.last_syncing.is_none());
259
260 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 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]), };
274 let status = ForkchoiceStatus::Syncing;
275
276 tracker.set_latest(state, status);
277
278 assert!(tracker.latest.is_some());
280 assert_eq!(tracker.latest.as_ref().unwrap().state, state);
281
282 assert!(tracker.last_valid.is_none());
284
285 assert!(tracker.last_syncing.is_some());
287 assert_eq!(tracker.last_syncing.as_ref().unwrap(), &state);
288
289 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 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!(tracker.latest.is_some());
309 assert_eq!(tracker.latest.as_ref().unwrap().state, state);
310
311 assert!(tracker.last_valid.is_none());
313
314 assert!(tracker.last_syncing.is_none());
316
317 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 assert!(tracker.sync_target().is_none());
327
328 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 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 assert!(tracker.last_valid_finalized().is_none());
346
347 let zero_finalized_state = ForkchoiceState {
349 head_block_hash: B256::ZERO,
350 safe_block_hash: B256::ZERO,
351 finalized_block_hash: B256::ZERO, };
353 tracker.last_valid = Some(zero_finalized_state);
354 assert!(tracker.last_valid_finalized().is_none());
355
356 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]), };
362 tracker.last_valid = Some(valid_finalized_state);
363 assert_eq!(tracker.last_valid_finalized(), Some(B256::from_slice(&[123; 32])));
364
365 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 assert!(tracker.sync_target_finalized().is_none());
376
377 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, };
383 tracker.last_syncing = Some(zero_finalized_sync_target);
384 assert!(tracker.sync_target_finalized().is_none());
385
386 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]), };
392 tracker.last_syncing = Some(valid_sync_target);
393 assert_eq!(tracker.sync_target_finalized(), Some(B256::from_slice(&[22; 32])));
394
395 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 assert!(forkchoice.is_empty());
406
407 forkchoice.set_latest(ForkchoiceState::default(), ForkchoiceStatus::Valid);
409 assert!(!forkchoice.is_empty());
410
411 forkchoice.latest = None;
413 assert!(forkchoice.is_empty());
414 }
415
416 #[test]
417 fn test_forkchoice_state_hash_find() {
418 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 let state = ForkchoiceState {
426 head_block_hash: head_hash,
427 safe_block_hash: safe_hash,
428 finalized_block_hash: finalized_hash,
429 };
430
431 assert_eq!(
433 ForkchoiceStateHash::find(&state, head_hash),
434 Some(ForkchoiceStateHash::Head(head_hash))
435 );
436
437 assert_eq!(
439 ForkchoiceStateHash::find(&state, safe_hash),
440 Some(ForkchoiceStateHash::Safe(safe_hash))
441 );
442
443 assert_eq!(
445 ForkchoiceStateHash::find(&state, finalized_hash),
446 Some(ForkchoiceStateHash::Finalized(finalized_hash))
447 );
448
449 assert_eq!(ForkchoiceStateHash::find(&state, non_matching_hash), None);
451 }
452}