1use crate::Head;
6use alloc::{
7 collections::{BTreeMap, BTreeSet},
8 vec::Vec,
9};
10use alloy_primitives::{hex, BlockNumber, B256};
11use alloy_rlp::{Error as RlpError, *};
12use core::{
13 cmp::Ordering,
14 fmt,
15 ops::{Add, AddAssign},
16};
17use crc::*;
18
19const CRC_32_IEEE: Crc<u32> = Crc::<u32>::new(&CRC_32_ISO_HDLC);
20const TIMESTAMP_BEFORE_ETHEREUM_MAINNET: u64 = 1_300_000_000;
21
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[cfg_attr(any(all(test, feature = "std"), feature = "arbitrary"), derive(arbitrary::Arbitrary))]
25#[derive(
26 Clone, Copy, PartialEq, Eq, Hash, RlpEncodableWrapper, RlpDecodableWrapper, RlpMaxEncodedLen,
27)]
28pub struct ForkHash(pub [u8; 4]);
29
30impl fmt::Debug for ForkHash {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 f.debug_tuple("ForkHash").field(&hex::encode(&self.0[..])).finish()
33 }
34}
35
36impl From<B256> for ForkHash {
37 fn from(genesis: B256) -> Self {
38 Self(CRC_32_IEEE.checksum(&genesis[..]).to_be_bytes())
39 }
40}
41
42impl<T> AddAssign<T> for ForkHash
43where
44 T: Into<u64>,
45{
46 fn add_assign(&mut self, v: T) {
47 let blob = v.into().to_be_bytes();
48 let digest = CRC_32_IEEE.digest_with_initial(u32::from_be_bytes(self.0));
49 let value = digest.finalize();
50 let mut digest = CRC_32_IEEE.digest_with_initial(value);
51 digest.update(&blob);
52 self.0 = digest.finalize().to_be_bytes();
53 }
54}
55
56impl<T> Add<T> for ForkHash
57where
58 T: Into<u64>,
59{
60 type Output = Self;
61 fn add(mut self, block: T) -> Self {
62 self += block;
63 self
64 }
65}
66
67#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub enum ForkFilterKey {
71 Block(BlockNumber),
73 Time(u64),
75}
76
77impl PartialOrd for ForkFilterKey {
78 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
79 Some(self.cmp(other))
80 }
81}
82
83impl Ord for ForkFilterKey {
84 fn cmp(&self, other: &Self) -> Ordering {
85 match (self, other) {
86 (Self::Block(a), Self::Block(b)) | (Self::Time(a), Self::Time(b)) => a.cmp(b),
87 (Self::Block(_), Self::Time(_)) => Ordering::Less,
88 _ => Ordering::Greater,
89 }
90 }
91}
92
93impl From<ForkFilterKey> for u64 {
94 fn from(value: ForkFilterKey) -> Self {
95 match value {
96 ForkFilterKey::Block(block) => block,
97 ForkFilterKey::Time(time) => time,
98 }
99 }
100}
101
102#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
105#[cfg_attr(any(all(test, feature = "std"), feature = "arbitrary"), derive(arbitrary::Arbitrary))]
106#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, RlpEncodable, RlpDecodable, RlpMaxEncodedLen)]
107pub struct ForkId {
108 pub hash: ForkHash,
110 pub next: u64,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable)]
122pub struct EnrForkIdEntry {
123 pub fork_id: ForkId,
125}
126
127impl Decodable for EnrForkIdEntry {
128 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
132 let b = &mut &**buf;
133 let rlp_head = Header::decode(b)?;
134 if !rlp_head.list {
135 return Err(RlpError::UnexpectedString);
136 }
137 let started_len = b.len();
138
139 let this = Self { fork_id: Decodable::decode(b)? };
140
141 let consumed = started_len - b.len();
145 if consumed > rlp_head.payload_length {
146 return Err(RlpError::ListLengthMismatch {
147 expected: rlp_head.payload_length,
148 got: consumed,
149 });
150 }
151
152 let rem = rlp_head.payload_length - consumed;
153 b.advance(rem);
154 *buf = *b;
155
156 Ok(this)
157 }
158}
159
160impl From<ForkId> for EnrForkIdEntry {
161 fn from(fork_id: ForkId) -> Self {
162 Self { fork_id }
163 }
164}
165
166impl From<EnrForkIdEntry> for ForkId {
167 fn from(entry: EnrForkIdEntry) -> Self {
168 entry.fork_id
169 }
170}
171
172#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq, Hash)]
174pub enum ValidationError {
175 #[error(
177 "remote node is outdated and needs a software update: local={local:?}, remote={remote:?}"
178 )]
179 RemoteStale {
180 local: ForkId,
182 remote: ForkId,
184 },
185 #[error("local node is on an incompatible chain or needs a software update: local={local:?}, remote={remote:?}")]
187 LocalIncompatibleOrStale {
188 local: ForkId,
190 remote: ForkId,
192 },
193}
194
195#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
198#[derive(Clone, Debug, PartialEq, Eq)]
199pub struct ForkFilter {
200 forks: BTreeMap<ForkFilterKey, ForkHash>,
209
210 head: Head,
212
213 cache: Cache,
214}
215
216impl ForkFilter {
217 pub fn new<F>(head: Head, genesis_hash: B256, genesis_timestamp: u64, forks: F) -> Self
220 where
221 F: IntoIterator<Item = ForkFilterKey>,
222 {
223 let genesis_fork_hash = ForkHash::from(genesis_hash);
224 let mut forks = forks.into_iter().collect::<BTreeSet<_>>();
225 forks.remove(&ForkFilterKey::Time(0));
226 forks.remove(&ForkFilterKey::Block(0));
227
228 let forks = forks
229 .into_iter()
230 .filter(|key| match key {
232 ForkFilterKey::Block(_) => true,
233 ForkFilterKey::Time(time) => *time > genesis_timestamp,
234 })
235 .collect::<BTreeSet<_>>()
236 .into_iter()
237 .fold(
238 (BTreeMap::from([(ForkFilterKey::Block(0), genesis_fork_hash)]), genesis_fork_hash),
239 |(mut acc, base_hash), key| {
240 let fork_hash = base_hash + u64::from(key);
241 acc.insert(key, fork_hash);
242 (acc, fork_hash)
243 },
244 )
245 .0;
246
247 let cache = Cache::compute_cache(&forks, head);
249
250 Self { forks, head, cache }
252 }
253
254 fn set_head_priv(&mut self, head: Head) -> Option<ForkTransition> {
255 let head_in_past = match self.cache.epoch_start {
256 ForkFilterKey::Block(epoch_start_block) => head.number < epoch_start_block,
257 ForkFilterKey::Time(epoch_start_time) => head.timestamp < epoch_start_time,
258 };
259 let head_in_future = match self.cache.epoch_end {
260 Some(ForkFilterKey::Block(epoch_end_block)) => head.number >= epoch_end_block,
261 Some(ForkFilterKey::Time(epoch_end_time)) => head.timestamp >= epoch_end_time,
262 None => false,
263 };
264
265 self.head = head;
266
267 (head_in_past || head_in_future).then(|| {
269 let past = self.current();
270 self.cache = Cache::compute_cache(&self.forks, head);
271 ForkTransition { current: self.current(), past }
272 })
273 }
274
275 pub fn set_head(&mut self, head: Head) -> Option<ForkTransition> {
279 self.set_head_priv(head)
280 }
281
282 #[must_use]
284 pub const fn current(&self) -> ForkId {
285 self.cache.fork_id
286 }
287
288 pub fn set_current_fork_id(&mut self, fork_id: ForkId) {
294 self.cache.fork_id = fork_id;
295 }
296
297 pub fn validate(&self, fork_id: ForkId) -> Result<(), ValidationError> {
306 if self.current().hash == fork_id.hash {
308 if fork_id.next == 0 {
309 return Ok(());
311 }
312
313 let is_incompatible = if self.head.number < TIMESTAMP_BEFORE_ETHEREUM_MAINNET {
314 (fork_id.next > TIMESTAMP_BEFORE_ETHEREUM_MAINNET
319 && self.head.timestamp >= fork_id.next)
320 || (fork_id.next <= TIMESTAMP_BEFORE_ETHEREUM_MAINNET
321 && self.head.number >= fork_id.next)
322 } else {
323 let head_block_or_time = match self.cache.epoch_start {
325 ForkFilterKey::Block(_) => self.head.number,
326 ForkFilterKey::Time(_) => self.head.timestamp,
327 };
328 head_block_or_time >= fork_id.next
329 };
330
331 return if is_incompatible {
332 Err(ValidationError::LocalIncompatibleOrStale {
335 local: self.current(),
336 remote: fork_id,
337 })
338 } else {
339 Ok(())
341 };
342 }
343
344 let mut it = self.cache.past.iter();
346 while let Some((_, hash)) = it.next() {
347 if *hash == fork_id.hash {
348 if let Some((actual_key, _)) = it.next() {
351 return if u64::from(*actual_key) == fork_id.next {
352 Ok(())
353 } else {
354 Err(ValidationError::RemoteStale { local: self.current(), remote: fork_id })
355 };
356 }
357
358 break;
359 }
360 }
361
362 for future_fork_hash in &self.cache.future {
365 if *future_fork_hash == fork_id.hash {
366 return Ok(());
367 }
368 }
369
370 Err(ValidationError::LocalIncompatibleOrStale { local: self.current(), remote: fork_id })
372 }
373}
374
375#[derive(Debug, Clone, Eq, PartialEq)]
379pub struct ForkTransition {
380 pub current: ForkId,
382 pub past: ForkId,
384}
385
386#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
387#[derive(Clone, Debug, PartialEq, Eq)]
388struct Cache {
389 epoch_start: ForkFilterKey,
392 epoch_end: Option<ForkFilterKey>,
393 past: Vec<(ForkFilterKey, ForkHash)>,
394 future: Vec<ForkHash>,
395 fork_id: ForkId,
396}
397
398impl Cache {
399 fn compute_cache(forks: &BTreeMap<ForkFilterKey, ForkHash>, head: Head) -> Self {
401 let mut past = Vec::with_capacity(forks.len());
403 let mut future = Vec::with_capacity(forks.len());
404
405 let mut epoch_start = ForkFilterKey::Block(0);
407 let mut epoch_end = None;
408
409 for (key, hash) in forks {
411 let active = match key {
413 ForkFilterKey::Block(block) => *block <= head.number,
414 ForkFilterKey::Time(time) => *time <= head.timestamp,
415 };
416
417 if active {
419 epoch_start = *key;
420 past.push((*key, *hash));
421 } else {
422 if epoch_end.is_none() {
423 epoch_end = Some(*key);
424 }
425 future.push(*hash);
426 }
427 }
428
429 let fork_id = ForkId {
431 hash: past.last().expect("there is always at least one - genesis - fork hash").1,
432 next: epoch_end.unwrap_or(ForkFilterKey::Block(0)).into(),
433 };
434
435 Self { epoch_start, epoch_end, past, future, fork_id }
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443 use alloy_primitives::b256;
444 use alloy_rlp::encode_fixed_size;
445
446 const MAINNET_GENESIS_HASH: B256 =
448 b256!("d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3");
449
450 #[test]
452 fn forkhash() {
453 let mut fork_hash = ForkHash::from(MAINNET_GENESIS_HASH);
454 assert_eq!(fork_hash.0, hex!("fc64ec04"));
455
456 fork_hash += 1_150_000u64;
457 assert_eq!(fork_hash.0, hex!("97c2c34c"));
458
459 fork_hash += 1_920_000u64;
460 assert_eq!(fork_hash.0, hex!("91d1f948"));
461 }
462
463 #[test]
464 fn compatibility_check() {
465 let mut filter = ForkFilter::new(
466 Head { number: 0, ..Default::default() },
467 MAINNET_GENESIS_HASH,
468 0,
469 vec![
470 ForkFilterKey::Block(1_150_000),
471 ForkFilterKey::Block(1_920_000),
472 ForkFilterKey::Block(2_463_000),
473 ForkFilterKey::Block(2_675_000),
474 ForkFilterKey::Block(4_370_000),
475 ForkFilterKey::Block(7_280_000),
476 ],
477 );
478
479 filter.set_head(Head { number: 7_987_396, ..Default::default() });
481 assert_eq!(filter.validate(ForkId { hash: ForkHash(hex!("668db0af")), next: 0 }), Ok(()));
482
483 filter.set_head(Head { number: 7_987_396, ..Default::default() });
486 assert_eq!(
487 filter.validate(ForkId { hash: ForkHash(hex!("668db0af")), next: BlockNumber::MAX }),
488 Ok(())
489 );
490
491 filter.set_head(Head { number: 7_279_999, ..Default::default() });
496 assert_eq!(filter.validate(ForkId { hash: ForkHash(hex!("a00bc324")), next: 0 }), Ok(()));
497
498 filter.set_head(Head { number: 7_279_999, ..Default::default() });
502 assert_eq!(
503 filter.validate(ForkId { hash: ForkHash(hex!("a00bc324")), next: 7_280_000 }),
504 Ok(())
505 );
506
507 filter.set_head(Head { number: 7_279_999, ..Default::default() });
512 assert_eq!(
513 filter.validate(ForkId { hash: ForkHash(hex!("a00bc324")), next: BlockNumber::MAX }),
514 Ok(())
515 );
516
517 filter.set_head(Head { number: 7_987_396, ..Default::default() });
520 assert_eq!(
521 filter.validate(ForkId { hash: ForkHash(hex!("a00bc324")), next: 7_280_000 }),
522 Ok(())
523 );
524
525 filter.set_head(Head { number: 7_987_396, ..Default::default() });
529 assert_eq!(
530 filter.validate(ForkId { hash: ForkHash(hex!("3edd5b10")), next: 4_370_000 }),
531 Ok(())
532 );
533
534 filter.set_head(Head { number: 7_279_999, ..Default::default() });
536 assert_eq!(filter.validate(ForkId { hash: ForkHash(hex!("668db0af")), next: 0 }), Ok(()));
537
538 filter.set_head(Head { number: 4_369_999, ..Default::default() });
542 assert_eq!(filter.validate(ForkId { hash: ForkHash(hex!("a00bc324")), next: 0 }), Ok(()));
543
544 filter.set_head(Head { number: 7_987_396, ..Default::default() });
547 let remote = ForkId { hash: ForkHash(hex!("a00bc324")), next: 0 };
548 assert_eq!(
549 filter.validate(remote),
550 Err(ValidationError::RemoteStale { local: filter.current(), remote })
551 );
552
553 filter.set_head(Head { number: 7_987_396, ..Default::default() });
556 let remote = ForkId { hash: ForkHash(hex!("5cddc0e1")), next: 0 };
557 assert_eq!(
558 filter.validate(remote),
559 Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
560 );
561
562 filter.set_head(Head { number: 7_279_999, ..Default::default() });
565 let remote = ForkId { hash: ForkHash(hex!("5cddc0e1")), next: 0 };
566 assert_eq!(
567 filter.validate(remote),
568 Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
569 );
570
571 filter.set_head(Head { number: 7_987_396, ..Default::default() });
573 let remote = ForkId { hash: ForkHash(hex!("afec6b27")), next: 0 };
574 assert_eq!(
575 filter.validate(remote),
576 Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
577 );
578
579 filter.set_head(Head { number: 88_888_888, ..Default::default() });
585 let remote = ForkId { hash: ForkHash(hex!("668db0af")), next: 88_888_888 };
586 assert_eq!(
587 filter.validate(remote),
588 Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
589 );
590
591 filter.set_head(Head { number: 7_279_999, ..Default::default() });
594 let remote = ForkId { hash: ForkHash(hex!("a00bc324")), next: 7_279_999 };
595 assert_eq!(
596 filter.validate(remote),
597 Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
598 );
599
600 filter
603 .set_head(Head { number: TIMESTAMP_BEFORE_ETHEREUM_MAINNET + 1, ..Default::default() });
604 let remote = ForkId {
605 hash: ForkHash(hex!("668db0af")),
606 next: TIMESTAMP_BEFORE_ETHEREUM_MAINNET + 1,
607 };
608 assert_eq!(
609 filter.validate(remote),
610 Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
611 );
612
613 filter
616 .set_head(Head { number: TIMESTAMP_BEFORE_ETHEREUM_MAINNET + 1, ..Default::default() });
617 let remote = ForkId {
618 hash: ForkHash(hex!("668db0af")),
619 next: TIMESTAMP_BEFORE_ETHEREUM_MAINNET + 2,
620 };
621 assert_eq!(filter.validate(remote), Ok(()));
622
623 filter.set_head(Head {
627 number: TIMESTAMP_BEFORE_ETHEREUM_MAINNET - 1,
628 timestamp: TIMESTAMP_BEFORE_ETHEREUM_MAINNET + 2,
629 ..Default::default()
630 });
631 let remote = ForkId {
632 hash: ForkHash(hex!("668db0af")),
633 next: TIMESTAMP_BEFORE_ETHEREUM_MAINNET + 1,
634 };
635 assert_eq!(
636 filter.validate(remote),
637 Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
638 );
639
640 filter
644 .set_head(Head { number: TIMESTAMP_BEFORE_ETHEREUM_MAINNET - 1, ..Default::default() });
645 let remote = ForkId {
646 hash: ForkHash(hex!("668db0af")),
647 next: TIMESTAMP_BEFORE_ETHEREUM_MAINNET - 2,
648 };
649 assert_eq!(
650 filter.validate(remote),
651 Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
652 );
653
654 filter
659 .set_head(Head { number: TIMESTAMP_BEFORE_ETHEREUM_MAINNET - 2, ..Default::default() });
660 let remote = ForkId {
661 hash: ForkHash(hex!("668db0af")),
662 next: TIMESTAMP_BEFORE_ETHEREUM_MAINNET - 1,
663 };
664 assert_eq!(filter.validate(remote), Ok(()));
665 }
666
667 #[test]
668 fn forkid_serialization() {
669 assert_eq!(
670 &*encode_fixed_size(&ForkId { hash: ForkHash(hex!("00000000")), next: 0 }),
671 hex!("c6840000000080")
672 );
673 assert_eq!(
674 &*encode_fixed_size(&ForkId { hash: ForkHash(hex!("deadbeef")), next: 0xBADD_CAFE }),
675 hex!("ca84deadbeef84baddcafe")
676 );
677 assert_eq!(
678 &*encode_fixed_size(&ForkId { hash: ForkHash(hex!("ffffffff")), next: u64::MAX }),
679 hex!("ce84ffffffff88ffffffffffffffff")
680 );
681
682 assert_eq!(
683 ForkId::decode(&mut (&hex!("c6840000000080") as &[u8])).unwrap(),
684 ForkId { hash: ForkHash(hex!("00000000")), next: 0 }
685 );
686 assert_eq!(
687 ForkId::decode(&mut (&hex!("ca84deadbeef84baddcafe") as &[u8])).unwrap(),
688 ForkId { hash: ForkHash(hex!("deadbeef")), next: 0xBADD_CAFE }
689 );
690 assert_eq!(
691 ForkId::decode(&mut (&hex!("ce84ffffffff88ffffffffffffffff") as &[u8])).unwrap(),
692 ForkId { hash: ForkHash(hex!("ffffffff")), next: u64::MAX }
693 );
694 }
695
696 #[test]
697 fn fork_id_rlp() {
698 let val = hex!("c6840000000080");
700 let id = ForkId::decode(&mut &val[..]).unwrap();
701 assert_eq!(id, ForkId { hash: ForkHash(hex!("00000000")), next: 0 });
702 assert_eq!(alloy_rlp::encode(id), &val[..]);
703
704 let val = hex!("ca84deadbeef84baddcafe");
705 let id = ForkId::decode(&mut &val[..]).unwrap();
706 assert_eq!(id, ForkId { hash: ForkHash(hex!("deadbeef")), next: 0xBADDCAFE });
707 assert_eq!(alloy_rlp::encode(id), &val[..]);
708
709 let val = hex!("ce84ffffffff88ffffffffffffffff");
710 let id = ForkId::decode(&mut &val[..]).unwrap();
711 assert_eq!(id, ForkId { hash: ForkHash(u32::MAX.to_be_bytes()), next: u64::MAX });
712 assert_eq!(alloy_rlp::encode(id), &val[..]);
713 }
714
715 #[test]
716 fn compute_cache() {
717 let b1 = 1_150_000;
718 let b2 = 1_920_000;
719
720 let h0 = ForkId { hash: ForkHash(hex!("fc64ec04")), next: b1 };
721 let h1 = ForkId { hash: ForkHash(hex!("97c2c34c")), next: b2 };
722 let h2 = ForkId { hash: ForkHash(hex!("91d1f948")), next: 0 };
723
724 let mut fork_filter = ForkFilter::new(
725 Head { number: 0, ..Default::default() },
726 MAINNET_GENESIS_HASH,
727 0,
728 vec![ForkFilterKey::Block(b1), ForkFilterKey::Block(b2)],
729 );
730
731 assert!(fork_filter.set_head_priv(Head { number: 0, ..Default::default() }).is_none());
732 assert_eq!(fork_filter.current(), h0);
733
734 assert!(fork_filter.set_head_priv(Head { number: 1, ..Default::default() }).is_none());
735 assert_eq!(fork_filter.current(), h0);
736
737 assert_eq!(
738 fork_filter.set_head_priv(Head { number: b1 + 1, ..Default::default() }).unwrap(),
739 ForkTransition { current: h1, past: h0 }
740 );
741 assert_eq!(fork_filter.current(), h1);
742
743 assert!(fork_filter.set_head_priv(Head { number: b1, ..Default::default() }).is_none());
744 assert_eq!(fork_filter.current(), h1);
745
746 assert_eq!(
747 fork_filter.set_head_priv(Head { number: b1 - 1, ..Default::default() }).unwrap(),
748 ForkTransition { current: h0, past: h1 }
749 );
750 assert_eq!(fork_filter.current(), h0);
751
752 assert!(fork_filter.set_head_priv(Head { number: b1, ..Default::default() }).is_some());
753 assert_eq!(fork_filter.current(), h1);
754
755 assert!(fork_filter.set_head_priv(Head { number: b2 - 1, ..Default::default() }).is_none());
756 assert_eq!(fork_filter.current(), h1);
757
758 assert!(fork_filter.set_head_priv(Head { number: b2, ..Default::default() }).is_some());
759 assert_eq!(fork_filter.current(), h2);
760 }
761
762 mod eip8 {
763 use super::*;
764
765 fn junk_enr_fork_id_entry() -> Vec<u8> {
766 let mut buf = Vec::new();
767 let fork_id = ForkId { hash: ForkHash(hex!("deadbeef")), next: 0xBADDCAFE };
769
770 let junk: u64 = 112233;
772
773 let payload_length = fork_id.length() + junk.length();
775 alloy_rlp::Header { list: true, payload_length }.encode(&mut buf);
776
777 fork_id.encode(&mut buf);
779 junk.encode(&mut buf);
780
781 buf
782 }
783
784 #[test]
785 fn eip8_decode_enr_fork_id_entry() {
786 let enr_fork_id_entry_with_junk = junk_enr_fork_id_entry();
787
788 let mut buf = enr_fork_id_entry_with_junk.as_slice();
789 let decoded = EnrForkIdEntry::decode(&mut buf).unwrap();
790 assert_eq!(
791 decoded.fork_id,
792 ForkId { hash: ForkHash(hex!("deadbeef")), next: 0xBADDCAFE }
793 );
794 }
795 }
796}