reth_engine_primitives/
forkchoice.rs1use 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 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 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 #[allow(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 #[allow(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 #[allow(dead_code)]
61 pub fn is_latest_invalid(&self) -> bool {
62 self.latest_status().is_some_and(|s| s.is_invalid())
63 }
64
65 #[allow(dead_code)]
67 pub fn last_valid_head(&self) -> Option<B256> {
68 self.last_valid.as_ref().map(|s| s.head_block_hash)
69 }
70
71 #[allow(dead_code)]
73 pub(crate) fn sync_target(&self) -> Option<B256> {
74 self.last_syncing.as_ref().map(|s| s.head_block_hash)
75 }
76
77 pub const fn latest_state(&self) -> Option<ForkchoiceState> {
81 self.last_valid
82 }
83
84 pub const fn last_valid_state(&self) -> Option<ForkchoiceState> {
86 self.last_valid
87 }
88
89 #[inline]
95 pub fn last_valid_finalized(&self) -> Option<B256> {
96 self.last_valid
97 .filter(|state| !state.finalized_block_hash.is_zero())
98 .map(|state| state.finalized_block_hash)
99 }
100
101 pub const fn sync_target_state(&self) -> Option<ForkchoiceState> {
103 self.last_syncing
104 }
105
106 #[inline]
112 pub fn sync_target_finalized(&self) -> Option<B256> {
113 self.last_syncing
114 .filter(|state| !state.finalized_block_hash.is_zero())
115 .map(|state| state.finalized_block_hash)
116 }
117
118 pub const fn is_empty(&self) -> bool {
120 self.latest.is_none()
121 }
122}
123
124#[derive(Debug, Clone)]
126#[allow(dead_code)]
127pub(crate) struct ReceivedForkchoiceState {
128 state: ForkchoiceState,
129 status: ForkchoiceStatus,
130}
131
132#[derive(Debug, Clone, Copy, Eq, PartialEq)]
134pub enum ForkchoiceStatus {
135 Valid,
137 Invalid,
139 Syncing,
141}
142
143impl ForkchoiceStatus {
144 pub const fn is_valid(&self) -> bool {
146 matches!(self, Self::Valid)
147 }
148
149 pub const fn is_invalid(&self) -> bool {
151 matches!(self, Self::Invalid)
152 }
153
154 pub const fn is_syncing(&self) -> bool {
156 matches!(self, Self::Syncing)
157 }
158
159 pub(crate) const fn from_payload_status(status: &PayloadStatusEnum) -> Self {
161 match status {
162 PayloadStatusEnum::Valid | PayloadStatusEnum::Accepted => {
163 Self::Valid
165 }
166 PayloadStatusEnum::Invalid { .. } => Self::Invalid,
167 PayloadStatusEnum::Syncing => Self::Syncing,
168 }
169 }
170}
171
172impl From<PayloadStatusEnum> for ForkchoiceStatus {
173 fn from(status: PayloadStatusEnum) -> Self {
174 Self::from_payload_status(&status)
175 }
176}
177
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
180pub enum ForkchoiceStateHash {
181 Head(B256),
183 Safe(B256),
185 Finalized(B256),
187}
188
189impl ForkchoiceStateHash {
190 pub fn find(state: &ForkchoiceState, hash: B256) -> Option<Self> {
192 if state.head_block_hash == hash {
193 Some(Self::Head(hash))
194 } else if state.safe_block_hash == hash {
195 Some(Self::Safe(hash))
196 } else if state.finalized_block_hash == hash {
197 Some(Self::Finalized(hash))
198 } else {
199 None
200 }
201 }
202
203 pub const fn is_head(&self) -> bool {
205 matches!(self, Self::Head(_))
206 }
207}
208
209impl AsRef<B256> for ForkchoiceStateHash {
210 fn as_ref(&self) -> &B256 {
211 match self {
212 Self::Head(h) | Self::Safe(h) | Self::Finalized(h) => h,
213 }
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_forkchoice_state_tracker_set_latest_valid() {
223 let mut tracker = ForkchoiceStateTracker::default();
224
225 assert!(tracker.latest_status().is_none());
227
228 let 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 };
234 let status = ForkchoiceStatus::Valid;
235
236 tracker.set_latest(state, status);
237
238 assert!(tracker.latest.is_some());
240 assert_eq!(tracker.latest.as_ref().unwrap().state, state);
241
242 assert!(tracker.last_valid.is_some());
244 assert_eq!(tracker.last_valid.as_ref().unwrap(), &state);
245
246 assert!(tracker.last_syncing.is_none());
248
249 assert_eq!(tracker.latest_status(), Some(ForkchoiceStatus::Valid));
251 }
252
253 #[test]
254 fn test_forkchoice_state_tracker_set_latest_syncing() {
255 let mut tracker = ForkchoiceStateTracker::default();
256
257 let 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]), };
263 let status = ForkchoiceStatus::Syncing;
264
265 tracker.set_latest(state, status);
266
267 assert!(tracker.latest.is_some());
269 assert_eq!(tracker.latest.as_ref().unwrap().state, state);
270
271 assert!(tracker.last_valid.is_none());
273
274 assert!(tracker.last_syncing.is_some());
276 assert_eq!(tracker.last_syncing.as_ref().unwrap(), &state);
277
278 assert_eq!(tracker.latest_status(), Some(ForkchoiceStatus::Syncing));
280 }
281
282 #[test]
283 fn test_forkchoice_state_tracker_set_latest_invalid() {
284 let mut tracker = ForkchoiceStateTracker::default();
285
286 let 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 };
292 let status = ForkchoiceStatus::Invalid;
293
294 tracker.set_latest(state, status);
295
296 assert!(tracker.latest.is_some());
298 assert_eq!(tracker.latest.as_ref().unwrap().state, state);
299
300 assert!(tracker.last_valid.is_none());
302
303 assert!(tracker.last_syncing.is_none());
305
306 assert_eq!(tracker.latest_status(), Some(ForkchoiceStatus::Invalid));
308 }
309
310 #[test]
311 fn test_forkchoice_state_tracker_sync_target() {
312 let mut tracker = ForkchoiceStateTracker::default();
313
314 assert!(tracker.sync_target().is_none());
316
317 let 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);
324
325 assert_eq!(tracker.sync_target(), Some(B256::from_slice(&[1; 32])));
327 }
328
329 #[test]
330 fn test_forkchoice_state_tracker_last_valid_finalized() {
331 let mut tracker = ForkchoiceStateTracker::default();
332
333 assert!(tracker.last_valid_finalized().is_none());
335
336 let zero_finalized_state = ForkchoiceState {
338 head_block_hash: B256::ZERO,
339 safe_block_hash: B256::ZERO,
340 finalized_block_hash: B256::ZERO, };
342 tracker.last_valid = Some(zero_finalized_state);
343 assert!(tracker.last_valid_finalized().is_none());
344
345 let 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]), };
351 tracker.last_valid = Some(valid_finalized_state);
352 assert_eq!(tracker.last_valid_finalized(), Some(B256::from_slice(&[123; 32])));
353
354 tracker.last_valid = None;
356 assert!(tracker.last_valid_finalized().is_none());
357 }
358
359 #[test]
360 fn test_forkchoice_state_tracker_sync_target_finalized() {
361 let mut tracker = ForkchoiceStateTracker::default();
362
363 assert!(tracker.sync_target_finalized().is_none());
365
366 let 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, };
372 tracker.last_syncing = Some(zero_finalized_sync_target);
373 assert!(tracker.sync_target_finalized().is_none());
374
375 let 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]), };
381 tracker.last_syncing = Some(valid_sync_target);
382 assert_eq!(tracker.sync_target_finalized(), Some(B256::from_slice(&[22; 32])));
383
384 tracker.last_syncing = None;
386 assert!(tracker.sync_target_finalized().is_none());
387 }
388
389 #[test]
390 fn test_forkchoice_state_tracker_is_empty() {
391 let mut forkchoice = ForkchoiceStateTracker::default();
392
393 assert!(forkchoice.is_empty());
395
396 forkchoice.set_latest(ForkchoiceState::default(), ForkchoiceStatus::Valid);
398 assert!(!forkchoice.is_empty());
399
400 forkchoice.latest = None;
402 assert!(forkchoice.is_empty());
403 }
404
405 #[test]
406 fn test_forkchoice_state_hash_find() {
407 let head_hash = B256::random();
409 let safe_hash = B256::random();
410 let finalized_hash = B256::random();
411 let non_matching_hash = B256::random();
412
413 let state = ForkchoiceState {
415 head_block_hash: head_hash,
416 safe_block_hash: safe_hash,
417 finalized_block_hash: finalized_hash,
418 };
419
420 assert_eq!(
422 ForkchoiceStateHash::find(&state, head_hash),
423 Some(ForkchoiceStateHash::Head(head_hash))
424 );
425
426 assert_eq!(
428 ForkchoiceStateHash::find(&state, safe_hash),
429 Some(ForkchoiceStateHash::Safe(safe_hash))
430 );
431
432 assert_eq!(
434 ForkchoiceStateHash::find(&state, finalized_hash),
435 Some(ForkchoiceStateHash::Finalized(finalized_hash))
436 );
437
438 assert_eq!(ForkchoiceStateHash::find(&state, non_matching_hash), None);
440 }
441}