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.set_valid(state);
25 } else if status.is_syncing() {
26 self.last_syncing = Some(state);
27 }
28
29 let received = ReceivedForkchoiceState { state, status };
30 self.latest = Some(received);
31 }
32
33 const fn set_valid(&mut self, state: ForkchoiceState) {
34 self.last_syncing = None;
36
37 self.last_valid = Some(state);
38 }
39
40 pub(crate) fn latest_status(&self) -> Option<ForkchoiceStatus> {
44 self.latest.as_ref().map(|s| s.status)
45 }
46
47 #[expect(dead_code)]
49 pub(crate) fn is_latest_valid(&self) -> bool {
50 self.latest_status().is_some_and(|s| s.is_valid())
51 }
52
53 #[expect(dead_code)]
55 pub(crate) fn is_latest_syncing(&self) -> bool {
56 self.latest_status().is_some_and(|s| s.is_syncing())
57 }
58
59 pub fn is_latest_invalid(&self) -> bool {
61 self.latest_status().is_some_and(|s| s.is_invalid())
62 }
63
64 pub fn last_valid_head(&self) -> Option<B256> {
66 self.last_valid.as_ref().map(|s| s.head_block_hash)
67 }
68
69 #[cfg_attr(not(test), expect(dead_code))]
71 pub(crate) fn sync_target(&self) -> Option<B256> {
72 self.last_syncing.as_ref().map(|s| s.head_block_hash)
73 }
74
75 pub fn latest_state(&self) -> Option<ForkchoiceState> {
79 self.latest.as_ref().map(|s| s.state)
80 }
81
82 pub const fn last_valid_state(&self) -> Option<ForkchoiceState> {
84 self.last_valid
85 }
86
87 #[inline]
93 pub fn last_valid_finalized(&self) -> Option<B256> {
94 self.last_valid
95 .filter(|state| !state.finalized_block_hash.is_zero())
96 .map(|state| state.finalized_block_hash)
97 }
98
99 pub const fn sync_target_state(&self) -> Option<ForkchoiceState> {
101 self.last_syncing
102 }
103
104 #[inline]
110 pub fn sync_target_finalized(&self) -> Option<B256> {
111 self.last_syncing
112 .filter(|state| !state.finalized_block_hash.is_zero())
113 .map(|state| state.finalized_block_hash)
114 }
115
116 pub const fn is_empty(&self) -> bool {
118 self.latest.is_none()
119 }
120}
121
122#[derive(Debug, Clone)]
124pub(crate) struct ReceivedForkchoiceState {
125 state: ForkchoiceState,
126 status: ForkchoiceStatus,
127}
128
129#[derive(Debug, Clone, Copy, Eq, PartialEq)]
131pub enum ForkchoiceStatus {
132 Valid,
134 Invalid,
136 Syncing,
138}
139
140impl ForkchoiceStatus {
141 pub const fn is_valid(&self) -> bool {
143 matches!(self, Self::Valid)
144 }
145
146 pub const fn is_invalid(&self) -> bool {
148 matches!(self, Self::Invalid)
149 }
150
151 pub const fn is_syncing(&self) -> bool {
153 matches!(self, Self::Syncing)
154 }
155
156 pub(crate) const fn from_payload_status(status: &PayloadStatusEnum) -> Self {
158 match status {
159 PayloadStatusEnum::Valid | PayloadStatusEnum::Accepted => {
160 Self::Valid
162 }
163 PayloadStatusEnum::Invalid { .. } => Self::Invalid,
164 PayloadStatusEnum::Syncing => Self::Syncing,
165 }
166 }
167}
168
169impl From<PayloadStatusEnum> for ForkchoiceStatus {
170 fn from(status: PayloadStatusEnum) -> Self {
171 Self::from_payload_status(&status)
172 }
173}
174
175#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub enum ForkchoiceStateHash {
178 Head(B256),
180 Safe(B256),
182 Finalized(B256),
184}
185
186impl ForkchoiceStateHash {
187 pub fn find(state: &ForkchoiceState, hash: B256) -> Option<Self> {
189 if state.head_block_hash == hash {
190 Some(Self::Head(hash))
191 } else if state.safe_block_hash == hash {
192 Some(Self::Safe(hash))
193 } else if state.finalized_block_hash == hash {
194 Some(Self::Finalized(hash))
195 } else {
196 None
197 }
198 }
199
200 pub const fn is_head(&self) -> bool {
202 matches!(self, Self::Head(_))
203 }
204}
205
206impl AsRef<B256> for ForkchoiceStateHash {
207 fn as_ref(&self) -> &B256 {
208 match self {
209 Self::Head(h) | Self::Safe(h) | Self::Finalized(h) => h,
210 }
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_forkchoice_state_tracker_set_latest_valid() {
220 let mut tracker = ForkchoiceStateTracker::default();
221
222 assert!(tracker.latest_status().is_none());
224
225 let state = ForkchoiceState {
227 head_block_hash: B256::from_slice(&[1; 32]),
228 safe_block_hash: B256::from_slice(&[2; 32]),
229 finalized_block_hash: B256::from_slice(&[3; 32]),
230 };
231 let status = ForkchoiceStatus::Valid;
232
233 tracker.set_latest(state, status);
234
235 assert!(tracker.latest.is_some());
237 assert_eq!(tracker.latest.as_ref().unwrap().state, state);
238
239 assert!(tracker.last_valid.is_some());
241 assert_eq!(tracker.last_valid.as_ref().unwrap(), &state);
242
243 assert!(tracker.last_syncing.is_none());
245
246 assert_eq!(tracker.latest_status(), Some(ForkchoiceStatus::Valid));
248 }
249
250 #[test]
251 fn test_forkchoice_state_tracker_set_latest_syncing() {
252 let mut tracker = ForkchoiceStateTracker::default();
253
254 let state = ForkchoiceState {
256 head_block_hash: B256::from_slice(&[1; 32]),
257 safe_block_hash: B256::from_slice(&[2; 32]),
258 finalized_block_hash: B256::from_slice(&[0; 32]), };
260 let status = ForkchoiceStatus::Syncing;
261
262 tracker.set_latest(state, status);
263
264 assert!(tracker.latest.is_some());
266 assert_eq!(tracker.latest.as_ref().unwrap().state, state);
267
268 assert!(tracker.last_valid.is_none());
270
271 assert!(tracker.last_syncing.is_some());
273 assert_eq!(tracker.last_syncing.as_ref().unwrap(), &state);
274
275 assert_eq!(tracker.latest_status(), Some(ForkchoiceStatus::Syncing));
277 }
278
279 #[test]
280 fn test_forkchoice_state_tracker_set_latest_invalid() {
281 let mut tracker = ForkchoiceStateTracker::default();
282
283 let state = ForkchoiceState {
285 head_block_hash: B256::from_slice(&[1; 32]),
286 safe_block_hash: B256::from_slice(&[2; 32]),
287 finalized_block_hash: B256::from_slice(&[3; 32]),
288 };
289 let status = ForkchoiceStatus::Invalid;
290
291 tracker.set_latest(state, status);
292
293 assert!(tracker.latest.is_some());
295 assert_eq!(tracker.latest.as_ref().unwrap().state, state);
296
297 assert!(tracker.last_valid.is_none());
299
300 assert!(tracker.last_syncing.is_none());
302
303 assert_eq!(tracker.latest_status(), Some(ForkchoiceStatus::Invalid));
305 }
306
307 #[test]
308 fn test_forkchoice_state_tracker_sync_target() {
309 let mut tracker = ForkchoiceStateTracker::default();
310
311 assert!(tracker.sync_target().is_none());
313
314 let state = ForkchoiceState {
316 head_block_hash: B256::from_slice(&[1; 32]),
317 safe_block_hash: B256::from_slice(&[2; 32]),
318 finalized_block_hash: B256::from_slice(&[3; 32]),
319 };
320 tracker.last_syncing = Some(state);
321
322 assert_eq!(tracker.sync_target(), Some(B256::from_slice(&[1; 32])));
324 }
325
326 #[test]
327 fn test_forkchoice_state_tracker_last_valid_finalized() {
328 let mut tracker = ForkchoiceStateTracker::default();
329
330 assert!(tracker.last_valid_finalized().is_none());
332
333 let zero_finalized_state = ForkchoiceState {
335 head_block_hash: B256::ZERO,
336 safe_block_hash: B256::ZERO,
337 finalized_block_hash: B256::ZERO, };
339 tracker.last_valid = Some(zero_finalized_state);
340 assert!(tracker.last_valid_finalized().is_none());
341
342 let valid_finalized_state = ForkchoiceState {
344 head_block_hash: B256::from_slice(&[1; 32]),
345 safe_block_hash: B256::from_slice(&[2; 32]),
346 finalized_block_hash: B256::from_slice(&[123; 32]), };
348 tracker.last_valid = Some(valid_finalized_state);
349 assert_eq!(tracker.last_valid_finalized(), Some(B256::from_slice(&[123; 32])));
350
351 tracker.last_valid = None;
353 assert!(tracker.last_valid_finalized().is_none());
354 }
355
356 #[test]
357 fn test_forkchoice_state_tracker_sync_target_finalized() {
358 let mut tracker = ForkchoiceStateTracker::default();
359
360 assert!(tracker.sync_target_finalized().is_none());
362
363 let zero_finalized_sync_target = ForkchoiceState {
365 head_block_hash: B256::from_slice(&[1; 32]),
366 safe_block_hash: B256::from_slice(&[2; 32]),
367 finalized_block_hash: B256::ZERO, };
369 tracker.last_syncing = Some(zero_finalized_sync_target);
370 assert!(tracker.sync_target_finalized().is_none());
371
372 let valid_sync_target = ForkchoiceState {
374 head_block_hash: B256::from_slice(&[1; 32]),
375 safe_block_hash: B256::from_slice(&[2; 32]),
376 finalized_block_hash: B256::from_slice(&[22; 32]), };
378 tracker.last_syncing = Some(valid_sync_target);
379 assert_eq!(tracker.sync_target_finalized(), Some(B256::from_slice(&[22; 32])));
380
381 tracker.last_syncing = None;
383 assert!(tracker.sync_target_finalized().is_none());
384 }
385
386 #[test]
387 fn test_forkchoice_state_tracker_is_empty() {
388 let mut forkchoice = ForkchoiceStateTracker::default();
389
390 assert!(forkchoice.is_empty());
392
393 forkchoice.set_latest(ForkchoiceState::default(), ForkchoiceStatus::Valid);
395 assert!(!forkchoice.is_empty());
396
397 forkchoice.latest = None;
399 assert!(forkchoice.is_empty());
400 }
401
402 #[test]
403 fn test_forkchoice_state_hash_find() {
404 let head_hash = B256::random();
406 let safe_hash = B256::random();
407 let finalized_hash = B256::random();
408 let non_matching_hash = B256::random();
409
410 let state = ForkchoiceState {
412 head_block_hash: head_hash,
413 safe_block_hash: safe_hash,
414 finalized_block_hash: finalized_hash,
415 };
416
417 assert_eq!(
419 ForkchoiceStateHash::find(&state, head_hash),
420 Some(ForkchoiceStateHash::Head(head_hash))
421 );
422
423 assert_eq!(
425 ForkchoiceStateHash::find(&state, safe_hash),
426 Some(ForkchoiceStateHash::Safe(safe_hash))
427 );
428
429 assert_eq!(
431 ForkchoiceStateHash::find(&state, finalized_hash),
432 Some(ForkchoiceStateHash::Finalized(finalized_hash))
433 );
434
435 assert_eq!(ForkchoiceStateHash::find(&state, non_matching_hash), None);
437 }
438}