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.and_then(|state| state.state_finalized_hash())
109 }
110
111 pub const fn sync_target_state(&self) -> Option<ForkchoiceState> {
113 self.last_syncing
114 }
115
116 #[inline]
122 pub fn sync_target_finalized(&self) -> Option<B256> {
123 self.last_syncing.and_then(|state| state.state_finalized_hash())
124 }
125
126 pub const fn is_empty(&self) -> bool {
128 self.latest.is_none()
129 }
130}
131
132#[derive(Debug, Clone)]
134pub(crate) struct ReceivedForkchoiceState {
135 state: ForkchoiceState,
136 status: ForkchoiceStatus,
137}
138
139#[derive(Debug, Clone, Copy, Eq, PartialEq)]
141pub enum ForkchoiceStatus {
142 Valid,
144 Invalid,
146 Syncing,
148}
149
150impl ForkchoiceStatus {
151 pub const fn is_valid(&self) -> bool {
153 matches!(self, Self::Valid)
154 }
155
156 pub const fn is_invalid(&self) -> bool {
158 matches!(self, Self::Invalid)
159 }
160
161 pub const fn is_syncing(&self) -> bool {
163 matches!(self, Self::Syncing)
164 }
165
166 pub(crate) const fn from_payload_status(status: &PayloadStatusEnum) -> Self {
168 match status {
169 PayloadStatusEnum::Valid | PayloadStatusEnum::Accepted => {
170 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187pub enum ForkchoiceStateHash {
188 Head(B256),
190 Safe(B256),
192 Finalized(B256),
194}
195
196impl ForkchoiceStateHash {
197 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 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 assert!(tracker.latest_status().is_none());
234
235 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!(tracker.latest.is_some());
247 assert_eq!(tracker.latest.as_ref().unwrap().state, state);
248
249 assert!(tracker.last_valid.is_some());
251 assert_eq!(tracker.last_valid.as_ref().unwrap(), &state);
252
253 assert!(tracker.last_syncing.is_none());
255
256 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 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]), };
270 let status = ForkchoiceStatus::Syncing;
271
272 tracker.set_latest(state, status);
273
274 assert!(tracker.latest.is_some());
276 assert_eq!(tracker.latest.as_ref().unwrap().state, state);
277
278 assert!(tracker.last_valid.is_none());
280
281 assert!(tracker.last_syncing.is_some());
283 assert_eq!(tracker.last_syncing.as_ref().unwrap(), &state);
284
285 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 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!(tracker.latest.is_some());
305 assert_eq!(tracker.latest.as_ref().unwrap().state, state);
306
307 assert!(tracker.last_valid.is_none());
309
310 assert!(tracker.last_syncing.is_none());
312
313 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 assert!(tracker.sync_target().is_none());
323
324 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 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 assert!(tracker.last_valid_finalized().is_none());
342
343 let zero_finalized_state = ForkchoiceState {
345 head_block_hash: B256::ZERO,
346 safe_block_hash: B256::ZERO,
347 finalized_block_hash: B256::ZERO, };
349 tracker.last_valid = Some(zero_finalized_state);
350 assert!(tracker.last_valid_finalized().is_none());
351
352 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]), };
358 tracker.last_valid = Some(valid_finalized_state);
359 assert_eq!(tracker.last_valid_finalized(), Some(B256::from_slice(&[123; 32])));
360
361 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 assert!(tracker.sync_target_finalized().is_none());
372
373 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, };
379 tracker.last_syncing = Some(zero_finalized_sync_target);
380 assert!(tracker.sync_target_finalized().is_none());
381
382 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]), };
388 tracker.last_syncing = Some(valid_sync_target);
389 assert_eq!(tracker.sync_target_finalized(), Some(B256::from_slice(&[22; 32])));
390
391 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 assert!(forkchoice.is_empty());
402
403 forkchoice.set_latest(ForkchoiceState::default(), ForkchoiceStatus::Valid);
405 assert!(!forkchoice.is_empty());
406
407 forkchoice.latest = None;
409 assert!(forkchoice.is_empty());
410 }
411
412 #[test]
413 fn test_forkchoice_state_hash_find() {
414 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 let state = ForkchoiceState {
422 head_block_hash: head_hash,
423 safe_block_hash: safe_hash,
424 finalized_block_hash: finalized_hash,
425 };
426
427 assert_eq!(
429 ForkchoiceStateHash::find(&state, head_hash),
430 Some(ForkchoiceStateHash::Head(head_hash))
431 );
432
433 assert_eq!(
435 ForkchoiceStateHash::find(&state, safe_hash),
436 Some(ForkchoiceStateHash::Safe(safe_hash))
437 );
438
439 assert_eq!(
441 ForkchoiceStateHash::find(&state, finalized_hash),
442 Some(ForkchoiceStateHash::Finalized(finalized_hash))
443 );
444
445 assert_eq!(ForkchoiceStateHash::find(&state, non_matching_hash), None);
447 }
448}