reth_engine_primitives/
forkchoice.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
use alloy_primitives::B256;
use alloy_rpc_types_engine::{ForkchoiceState, PayloadStatusEnum};

/// The struct that keeps track of the received forkchoice state and their status.
#[derive(Debug, Clone, Default)]
pub struct ForkchoiceStateTracker {
    /// The latest forkchoice state that we received.
    ///
    /// Caution: this can be invalid.
    latest: Option<ReceivedForkchoiceState>,
    /// Tracks the latest forkchoice state that we received to which we need to sync.
    last_syncing: Option<ForkchoiceState>,
    /// The latest valid forkchoice state that we received and processed as valid.
    last_valid: Option<ForkchoiceState>,
}

impl ForkchoiceStateTracker {
    /// Sets the latest forkchoice state that we received.
    ///
    /// If the status is `VALID`, we also update the last valid forkchoice state and set the
    /// `sync_target` to `None`, since we're now fully synced.
    pub fn set_latest(&mut self, state: ForkchoiceState, status: ForkchoiceStatus) {
        if status.is_valid() {
            self.set_valid(state);
        } else if status.is_syncing() {
            self.last_syncing = Some(state);
        }

        let received = ReceivedForkchoiceState { state, status };
        self.latest = Some(received);
    }

    fn set_valid(&mut self, state: ForkchoiceState) {
        // we no longer need to sync to this state.
        self.last_syncing = None;

        self.last_valid = Some(state);
    }

    /// Returns the [`ForkchoiceStatus`] of the latest received FCU.
    ///
    /// Caution: this can be invalid.
    pub(crate) fn latest_status(&self) -> Option<ForkchoiceStatus> {
        self.latest.as_ref().map(|s| s.status)
    }

    /// Returns whether the latest received FCU is valid: [`ForkchoiceStatus::Valid`]
    #[allow(dead_code)]
    pub(crate) fn is_latest_valid(&self) -> bool {
        self.latest_status().is_some_and(|s| s.is_valid())
    }

    /// Returns whether the latest received FCU is syncing: [`ForkchoiceStatus::Syncing`]
    #[allow(dead_code)]
    pub(crate) fn is_latest_syncing(&self) -> bool {
        self.latest_status().is_some_and(|s| s.is_syncing())
    }

    /// Returns whether the latest received FCU is syncing: [`ForkchoiceStatus::Invalid`]
    #[allow(dead_code)]
    pub fn is_latest_invalid(&self) -> bool {
        self.latest_status().is_some_and(|s| s.is_invalid())
    }

    /// Returns the last valid head hash.
    #[allow(dead_code)]
    pub fn last_valid_head(&self) -> Option<B256> {
        self.last_valid.as_ref().map(|s| s.head_block_hash)
    }

    /// Returns the head hash of the latest received FCU to which we need to sync.
    #[allow(dead_code)]
    pub(crate) fn sync_target(&self) -> Option<B256> {
        self.last_syncing.as_ref().map(|s| s.head_block_hash)
    }

    /// Returns the latest received [`ForkchoiceState`].
    ///
    /// Caution: this can be invalid.
    pub const fn latest_state(&self) -> Option<ForkchoiceState> {
        self.last_valid
    }

    /// Returns the last valid [`ForkchoiceState`].
    pub const fn last_valid_state(&self) -> Option<ForkchoiceState> {
        self.last_valid
    }

    /// Returns the last valid finalized hash.
    ///
    /// This will return [`None`]:
    /// - If either there is no valid finalized forkchoice state,
    /// - Or the finalized hash for the latest valid forkchoice state is zero.
    #[inline]
    pub fn last_valid_finalized(&self) -> Option<B256> {
        self.last_valid
            .filter(|state| !state.finalized_block_hash.is_zero())
            .map(|state| state.finalized_block_hash)
    }

    /// Returns the last received `ForkchoiceState` to which we need to sync.
    pub const fn sync_target_state(&self) -> Option<ForkchoiceState> {
        self.last_syncing
    }

    /// Returns the sync target finalized hash.
    ///
    /// This will return [`None`]:
    /// - If either there is no sync target forkchoice state,
    /// - Or the finalized hash for the sync target forkchoice state is zero.
    #[inline]
    pub fn sync_target_finalized(&self) -> Option<B256> {
        self.last_syncing
            .filter(|state| !state.finalized_block_hash.is_zero())
            .map(|state| state.finalized_block_hash)
    }

    /// Returns true if no forkchoice state has been received yet.
    pub const fn is_empty(&self) -> bool {
        self.latest.is_none()
    }
}

/// Represents a forkchoice update and tracks the status we assigned to it.
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub(crate) struct ReceivedForkchoiceState {
    state: ForkchoiceState,
    status: ForkchoiceStatus,
}

/// A simplified representation of [`PayloadStatusEnum`] specifically for FCU.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ForkchoiceStatus {
    /// The forkchoice state is valid.
    Valid,
    /// The forkchoice state is invalid.
    Invalid,
    /// The forkchoice state is unknown.
    Syncing,
}

impl ForkchoiceStatus {
    /// Returns `true` if the forkchoice state is [`ForkchoiceStatus::Valid`].
    pub const fn is_valid(&self) -> bool {
        matches!(self, Self::Valid)
    }

    /// Returns `true` if the forkchoice state is [`ForkchoiceStatus::Invalid`].
    pub const fn is_invalid(&self) -> bool {
        matches!(self, Self::Invalid)
    }

    /// Returns `true` if the forkchoice state is [`ForkchoiceStatus::Syncing`].
    pub const fn is_syncing(&self) -> bool {
        matches!(self, Self::Syncing)
    }

    /// Converts the general purpose [`PayloadStatusEnum`] into a [`ForkchoiceStatus`].
    pub(crate) const fn from_payload_status(status: &PayloadStatusEnum) -> Self {
        match status {
            PayloadStatusEnum::Valid | PayloadStatusEnum::Accepted => {
                // `Accepted` is only returned on `newPayload`. It would be a valid state here.
                Self::Valid
            }
            PayloadStatusEnum::Invalid { .. } => Self::Invalid,
            PayloadStatusEnum::Syncing => Self::Syncing,
        }
    }
}

impl From<PayloadStatusEnum> for ForkchoiceStatus {
    fn from(status: PayloadStatusEnum) -> Self {
        Self::from_payload_status(&status)
    }
}

/// A helper type to check represent hashes of a [`ForkchoiceState`]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ForkchoiceStateHash {
    /// Head hash of the [`ForkchoiceState`].
    Head(B256),
    /// Safe hash of the [`ForkchoiceState`].
    Safe(B256),
    /// Finalized hash of the [`ForkchoiceState`].
    Finalized(B256),
}

impl ForkchoiceStateHash {
    /// Tries to find a matching hash in the given [`ForkchoiceState`].
    pub fn find(state: &ForkchoiceState, hash: B256) -> Option<Self> {
        if state.head_block_hash == hash {
            Some(Self::Head(hash))
        } else if state.safe_block_hash == hash {
            Some(Self::Safe(hash))
        } else if state.finalized_block_hash == hash {
            Some(Self::Finalized(hash))
        } else {
            None
        }
    }

    /// Returns true if this is the head hash of the [`ForkchoiceState`]
    pub const fn is_head(&self) -> bool {
        matches!(self, Self::Head(_))
    }
}

impl AsRef<B256> for ForkchoiceStateHash {
    fn as_ref(&self) -> &B256 {
        match self {
            Self::Head(h) | Self::Safe(h) | Self::Finalized(h) => h,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_forkchoice_state_tracker_set_latest_valid() {
        let mut tracker = ForkchoiceStateTracker::default();

        // Latest state is None
        assert!(tracker.latest_status().is_none());

        // Create a valid ForkchoiceState
        let state = ForkchoiceState {
            head_block_hash: B256::from_slice(&[1; 32]),
            safe_block_hash: B256::from_slice(&[2; 32]),
            finalized_block_hash: B256::from_slice(&[3; 32]),
        };
        let status = ForkchoiceStatus::Valid;

        tracker.set_latest(state, status);

        // Assert that the latest state is set
        assert!(tracker.latest.is_some());
        assert_eq!(tracker.latest.as_ref().unwrap().state, state);

        // Assert that last valid state is updated
        assert!(tracker.last_valid.is_some());
        assert_eq!(tracker.last_valid.as_ref().unwrap(), &state);

        // Assert that last syncing state is None
        assert!(tracker.last_syncing.is_none());

        // Test when there is a latest status and it is valid
        assert_eq!(tracker.latest_status(), Some(ForkchoiceStatus::Valid));
    }

    #[test]
    fn test_forkchoice_state_tracker_set_latest_syncing() {
        let mut tracker = ForkchoiceStateTracker::default();

        // Create a syncing ForkchoiceState
        let state = ForkchoiceState {
            head_block_hash: B256::from_slice(&[1; 32]),
            safe_block_hash: B256::from_slice(&[2; 32]),
            finalized_block_hash: B256::from_slice(&[0; 32]), // Zero to simulate not finalized
        };
        let status = ForkchoiceStatus::Syncing;

        tracker.set_latest(state, status);

        // Assert that the latest state is set
        assert!(tracker.latest.is_some());
        assert_eq!(tracker.latest.as_ref().unwrap().state, state);

        // Assert that last valid state is None since the status is syncing
        assert!(tracker.last_valid.is_none());

        // Assert that last syncing state is updated
        assert!(tracker.last_syncing.is_some());
        assert_eq!(tracker.last_syncing.as_ref().unwrap(), &state);

        // Test when there is a latest status and it is syncing
        assert_eq!(tracker.latest_status(), Some(ForkchoiceStatus::Syncing));
    }

    #[test]
    fn test_forkchoice_state_tracker_set_latest_invalid() {
        let mut tracker = ForkchoiceStateTracker::default();

        // Create an invalid ForkchoiceState
        let state = ForkchoiceState {
            head_block_hash: B256::from_slice(&[1; 32]),
            safe_block_hash: B256::from_slice(&[2; 32]),
            finalized_block_hash: B256::from_slice(&[3; 32]),
        };
        let status = ForkchoiceStatus::Invalid;

        tracker.set_latest(state, status);

        // Assert that the latest state is set
        assert!(tracker.latest.is_some());
        assert_eq!(tracker.latest.as_ref().unwrap().state, state);

        // Assert that last valid state is None since the status is invalid
        assert!(tracker.last_valid.is_none());

        // Assert that last syncing state is None since the status is invalid
        assert!(tracker.last_syncing.is_none());

        // Test when there is a latest status and it is invalid
        assert_eq!(tracker.latest_status(), Some(ForkchoiceStatus::Invalid));
    }

    #[test]
    fn test_forkchoice_state_tracker_sync_target() {
        let mut tracker = ForkchoiceStateTracker::default();

        // Test when there is no last syncing state (should return None)
        assert!(tracker.sync_target().is_none());

        // Set a last syncing forkchoice state
        let state = ForkchoiceState {
            head_block_hash: B256::from_slice(&[1; 32]),
            safe_block_hash: B256::from_slice(&[2; 32]),
            finalized_block_hash: B256::from_slice(&[3; 32]),
        };
        tracker.last_syncing = Some(state);

        // Test when the last syncing state is set (should return the head block hash)
        assert_eq!(tracker.sync_target(), Some(B256::from_slice(&[1; 32])));
    }

    #[test]
    fn test_forkchoice_state_tracker_last_valid_finalized() {
        let mut tracker = ForkchoiceStateTracker::default();

        // No valid finalized state (should return None)
        assert!(tracker.last_valid_finalized().is_none());

        // Valid finalized state, but finalized hash is zero (should return None)
        let zero_finalized_state = ForkchoiceState {
            head_block_hash: B256::ZERO,
            safe_block_hash: B256::ZERO,
            finalized_block_hash: B256::ZERO, // Zero finalized hash
        };
        tracker.last_valid = Some(zero_finalized_state);
        assert!(tracker.last_valid_finalized().is_none());

        // Valid finalized state with non-zero finalized hash (should return finalized hash)
        let valid_finalized_state = ForkchoiceState {
            head_block_hash: B256::from_slice(&[1; 32]),
            safe_block_hash: B256::from_slice(&[2; 32]),
            finalized_block_hash: B256::from_slice(&[123; 32]), // Non-zero finalized hash
        };
        tracker.last_valid = Some(valid_finalized_state);
        assert_eq!(tracker.last_valid_finalized(), Some(B256::from_slice(&[123; 32])));

        // Reset the last valid state to None
        tracker.last_valid = None;
        assert!(tracker.last_valid_finalized().is_none());
    }

    #[test]
    fn test_forkchoice_state_tracker_sync_target_finalized() {
        let mut tracker = ForkchoiceStateTracker::default();

        // No sync target state (should return None)
        assert!(tracker.sync_target_finalized().is_none());

        // Sync target state with finalized hash as zero (should return None)
        let zero_finalized_sync_target = ForkchoiceState {
            head_block_hash: B256::from_slice(&[1; 32]),
            safe_block_hash: B256::from_slice(&[2; 32]),
            finalized_block_hash: B256::ZERO, // Zero finalized hash
        };
        tracker.last_syncing = Some(zero_finalized_sync_target);
        assert!(tracker.sync_target_finalized().is_none());

        // Sync target state with non-zero finalized hash (should return the hash)
        let valid_sync_target = ForkchoiceState {
            head_block_hash: B256::from_slice(&[1; 32]),
            safe_block_hash: B256::from_slice(&[2; 32]),
            finalized_block_hash: B256::from_slice(&[22; 32]), // Non-zero finalized hash
        };
        tracker.last_syncing = Some(valid_sync_target);
        assert_eq!(tracker.sync_target_finalized(), Some(B256::from_slice(&[22; 32])));

        // Reset the last sync target state to None
        tracker.last_syncing = None;
        assert!(tracker.sync_target_finalized().is_none());
    }

    #[test]
    fn test_forkchoice_state_tracker_is_empty() {
        let mut forkchoice = ForkchoiceStateTracker::default();

        // Initially, no forkchoice state has been received, so it should be empty.
        assert!(forkchoice.is_empty());

        // After setting a forkchoice state, it should no longer be empty.
        forkchoice.set_latest(ForkchoiceState::default(), ForkchoiceStatus::Valid);
        assert!(!forkchoice.is_empty());

        // Reset the forkchoice latest, it should be empty again.
        forkchoice.latest = None;
        assert!(forkchoice.is_empty());
    }

    #[test]
    fn test_forkchoice_state_hash_find() {
        // Define example hashes
        let head_hash = B256::random();
        let safe_hash = B256::random();
        let finalized_hash = B256::random();
        let non_matching_hash = B256::random();

        // Create a ForkchoiceState with specific hashes
        let state = ForkchoiceState {
            head_block_hash: head_hash,
            safe_block_hash: safe_hash,
            finalized_block_hash: finalized_hash,
        };

        // Test finding the head hash
        assert_eq!(
            ForkchoiceStateHash::find(&state, head_hash),
            Some(ForkchoiceStateHash::Head(head_hash))
        );

        // Test finding the safe hash
        assert_eq!(
            ForkchoiceStateHash::find(&state, safe_hash),
            Some(ForkchoiceStateHash::Safe(safe_hash))
        );

        // Test finding the finalized hash
        assert_eq!(
            ForkchoiceStateHash::find(&state, finalized_hash),
            Some(ForkchoiceStateHash::Finalized(finalized_hash))
        );

        // Test with a hash that doesn't match any of the hashes in ForkchoiceState
        assert_eq!(ForkchoiceStateHash::find(&state, non_matching_hash), None);
    }
}