alloy_eip2124/
forkid.rs

1//! EIP-2124 implementation based on <https://eips.ethereum.org/EIPS/eip-2124>.
2//!
3//! Previously version of Apache licenced [`ethereum-forkid`](https://crates.io/crates/ethereum-forkid).
4
5use 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/// `CRC32` hash of all previous forks starting from genesis block.
23#[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/// How to filter forks.
68#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub enum ForkFilterKey {
71    /// By block number activation.
72    Block(BlockNumber),
73    /// By timestamp activation.
74    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/// A fork identifier as defined by EIP-2124.
103/// Serves as the chain compatibility identifier.
104#[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    /// CRC32 checksum of the all fork blocks and timestamps from genesis.
109    pub hash: ForkHash,
110    /// Next upcoming fork block number or timestamp, 0 if not yet known.
111    pub next: u64,
112}
113
114/// Represents a forward-compatible ENR entry for including the forkid in a node record via
115/// EIP-868. Forward compatibility is achieved via EIP-8.
116///
117/// See:
118/// <https://github.com/ethereum/devp2p/blob/master/enr-entries/eth.md#entry-format>
119///
120/// for how geth implements `ForkId` values and forward compatibility.
121#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable)]
122pub struct EnrForkIdEntry {
123    /// The inner forkid
124    pub fork_id: ForkId,
125}
126
127impl Decodable for EnrForkIdEntry {
128    // NOTE(onbjerg): Manual implementation to satisfy EIP-8.
129    //
130    // See https://eips.ethereum.org/EIPS/eip-8
131    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        // NOTE(onbjerg): Because of EIP-8, we only check that we did not consume *more* than the
142        // payload length, i.e. it is ok if payload length is greater than what we consumed, as we
143        // just discard the remaining list items
144        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/// Reason for rejecting provided `ForkId`.
173#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq, Hash)]
174pub enum ValidationError {
175    /// Remote node is outdated and needs a software update.
176    #[error(
177        "remote node is outdated and needs a software update: local={local:?}, remote={remote:?}"
178    )]
179    RemoteStale {
180        /// locally configured forkId
181        local: ForkId,
182        /// `ForkId` received from remote
183        remote: ForkId,
184    },
185    /// Local node is on an incompatible chain or needs a software update.
186    #[error("local node is on an incompatible chain or needs a software update: local={local:?}, remote={remote:?}")]
187    LocalIncompatibleOrStale {
188        /// locally configured forkId
189        local: ForkId,
190        /// `ForkId` received from remote
191        remote: ForkId,
192    },
193}
194
195/// Filter that describes the state of blockchain and can be used to check incoming `ForkId`s for
196/// compatibility.
197#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
198#[derive(Clone, Debug, PartialEq, Eq)]
199pub struct ForkFilter {
200    /// The forks in the filter are keyed by `(timestamp, block)`. This ensures that block-based
201    /// forks (`time == 0`) are processed before time-based forks as required by
202    /// [EIP-6122][eip-6122].
203    ///
204    /// Time-based forks have their block number set to 0, allowing easy comparisons with a [Head];
205    /// a fork is active if both it's time and block number are less than or equal to [Head].
206    ///
207    /// [eip-6122]: https://eips.ethereum.org/EIPS/eip-6122
208    forks: BTreeMap<ForkFilterKey, ForkHash>,
209
210    /// The current head, used to select forks that are active locally.
211    head: Head,
212
213    cache: Cache,
214}
215
216impl ForkFilter {
217    /// Create the filter from provided head, genesis block hash, past forks and expected future
218    /// forks.
219    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 out forks that are pre-genesis by timestamp
231            .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        // Compute cache based on filtered forks and the current head.
248        let cache = Cache::compute_cache(&forks, head);
249
250        // Create and return a new `ForkFilter`.
251        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        // Recompute the cache if the head is in the past or future epoch.
268        (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    /// Set the current head.
276    ///
277    /// If the update updates the current [`ForkId`] it returns a [`ForkTransition`]
278    pub fn set_head(&mut self, head: Head) -> Option<ForkTransition> {
279        self.set_head_priv(head)
280    }
281
282    /// Return current fork id
283    #[must_use]
284    pub const fn current(&self) -> ForkId {
285        self.cache.fork_id
286    }
287
288    /// Manually set the current fork id.
289    ///
290    /// Caution: this disregards all configured fork filters and is reset on the next head update.
291    /// This is useful for testing or to connect to networks over p2p where only the latest forkid
292    /// is known.
293    pub fn set_current_fork_id(&mut self, fork_id: ForkId) {
294        self.cache.fork_id = fork_id;
295    }
296
297    /// Check whether the provided `ForkId` is compatible based on the validation rules in
298    /// `EIP-2124`.
299    ///
300    /// Implements the rules following: <https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2124.md#stale-software-examples>
301    ///
302    /// # Errors
303    ///
304    /// Returns a `ValidationError` if the `ForkId` is not compatible.
305    pub fn validate(&self, fork_id: ForkId) -> Result<(), ValidationError> {
306        // 1) If local and remote FORK_HASH matches...
307        if self.current().hash == fork_id.hash {
308            if fork_id.next == 0 {
309                // 1b) No remotely announced fork, connect.
310                return Ok(());
311            }
312
313            let is_incompatible = if self.head.number < TIMESTAMP_BEFORE_ETHEREUM_MAINNET {
314                // When the block number is less than an old timestamp before Ethereum mainnet,
315                // we check if this fork is time-based or block number-based by estimating that,
316                // if fork_id.next is bigger than the old timestamp, we are dealing with a
317                // timestamp, otherwise with a block.
318                (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                // Extra safety check to future-proof for when Ethereum has over a billion blocks.
324                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                // 1a) A remotely announced but remotely not passed block is already passed locally,
333                // disconnect, since the chains are incompatible.
334                Err(ValidationError::LocalIncompatibleOrStale {
335                    local: self.current(),
336                    remote: fork_id,
337                })
338            } else {
339                // 1b) Remotely announced fork not yet passed locally, connect.
340                Ok(())
341            };
342        }
343
344        // 2) If the remote FORK_HASH is a subset of the local past forks...
345        let mut it = self.cache.past.iter();
346        while let Some((_, hash)) = it.next() {
347            if *hash == fork_id.hash {
348                // ...and the remote FORK_NEXT matches with the locally following fork block number
349                // or timestamp, connect.
350                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        // 3) If the remote FORK_HASH is a superset of the local past forks and can be completed
363        // with locally known future forks, connect.
364        for future_fork_hash in &self.cache.future {
365            if *future_fork_hash == fork_id.hash {
366                return Ok(());
367            }
368        }
369
370        // 4) Reject in all other cases.
371        Err(ValidationError::LocalIncompatibleOrStale { local: self.current(), remote: fork_id })
372    }
373}
374
375/// Represents a transition from one fork to another
376///
377/// See also [`ForkFilter::set_head`]
378#[derive(Debug, Clone, Eq, PartialEq)]
379pub struct ForkTransition {
380    /// The new, active `ForkId`
381    pub current: ForkId,
382    /// The previously active `ForkId` before the transition
383    pub past: ForkId,
384}
385
386#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
387#[derive(Clone, Debug, PartialEq, Eq)]
388struct Cache {
389    // An epoch is a period between forks.
390    // When we progress from one fork to the next one we move to the next epoch.
391    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    /// Compute cache.
400    fn compute_cache(forks: &BTreeMap<ForkFilterKey, ForkHash>, head: Head) -> Self {
401        // Prepare vectors to store past and future forks.
402        let mut past = Vec::with_capacity(forks.len());
403        let mut future = Vec::with_capacity(forks.len());
404
405        // Initialize variables to track the epoch range.
406        let mut epoch_start = ForkFilterKey::Block(0);
407        let mut epoch_end = None;
408
409        // Iterate through forks and categorize them into past and future.
410        for (key, hash) in forks {
411            // Check if the fork is active based on its type (Block or Time).
412            let active = match key {
413                ForkFilterKey::Block(block) => *block <= head.number,
414                ForkFilterKey::Time(time) => *time <= head.timestamp,
415            };
416
417            // Categorize forks into past or future based on activity.
418            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        // Create ForkId using the last past fork's hash and the next epoch start.
430        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        // Return the computed cache.
436        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    /// The Ethereum mainnet genesis hash.
447    const MAINNET_GENESIS_HASH: B256 =
448        b256!("d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3");
449
450    // EIP test vectors.
451    #[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        // Local is mainnet Petersburg, remote announces the same. No future fork is announced.
480        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        // Local is mainnet Petersburg, remote announces the same. Remote also announces a next fork
484        // at block 0xffffffff, but that is uncertain.
485        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        // Local is mainnet currently in Byzantium only (so it's aware of Petersburg),remote
492        // announces also Byzantium, but it's not yet aware of Petersburg (e.g. non updated
493        // node before the fork). In this case we don't know if Petersburg passed yet or
494        // not.
495        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        // Local is mainnet currently in Byzantium only (so it's aware of Petersburg), remote
499        // announces also Byzantium, and it's also aware of Petersburg (e.g. updated node
500        // before the fork). We don't know if Petersburg passed yet (will pass) or not.
501        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        // Local is mainnet currently in Byzantium only (so it's aware of Petersburg), remote
508        // announces also Byzantium, and it's also aware of some random fork (e.g.
509        // misconfigured Petersburg). As neither forks passed at neither nodes, they may
510        // mismatch, but we still connect for now.
511        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        // Local is mainnet Petersburg, remote announces Byzantium + knowledge about Petersburg.
518        // Remote is simply out of sync, accept.
519        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        // Local is mainnet Petersburg, remote announces Spurious + knowledge about Byzantium.
526        // Remote is definitely out of sync. It may or may not need the Petersburg update,
527        // we don't know yet.
528        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        // Local is mainnet Byzantium, remote announces Petersburg. Local is out of sync, accept.
535        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        // Local is mainnet Spurious, remote announces Byzantium, but is not aware of Petersburg.
539        // Local out of sync. Local also knows about a future fork, but that is uncertain
540        // yet.
541        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        // Local is mainnet Petersburg. remote announces Byzantium but is not aware of further
545        // forks. Remote needs software update.
546        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        // Local is mainnet Petersburg, and isn't aware of more forks. Remote announces Petersburg +
554        // 0xffffffff. Local needs software update, reject.
555        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        // Local is mainnet Byzantium, and is aware of Petersburg. Remote announces Petersburg +
563        // 0xffffffff. Local needs software update, reject.
564        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        // Local is mainnet Petersburg, remote is Rinkeby Petersburg.
572        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        // Local is mainnet Petersburg, far in the future. Remote announces Gopherium (non existing
580        // fork) at some future block 88888888, for itself, but past block for local. Local
581        // is incompatible.
582        //
583        // This case detects non-upgraded nodes with majority hash power (typical Ropsten mess).
584        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        // Local is mainnet Byzantium. Remote is also in Byzantium, but announces Gopherium (non
592        // existing fork) at block 7279999, before Petersburg. Local is incompatible.
593        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        // Block far in the future (block number bigger than TIMESTAMP_BEFORE_ETHEREUM_MAINNET), not
601        // compatible.
602        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        // Block far in the future (block number bigger than TIMESTAMP_BEFORE_ETHEREUM_MAINNET),
614        // compatible.
615        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        // block number smaller than TIMESTAMP_BEFORE_ETHEREUM_MAINNET and
624        // fork_id.next > TIMESTAMP_BEFORE_ETHEREUM_MAINNET && self.head.timestamp >= fork_id.next,
625        // not compatible.
626        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        // block number smaller than TIMESTAMP_BEFORE_ETHEREUM_MAINNET and
641        // fork_id.next <= TIMESTAMP_BEFORE_ETHEREUM_MAINNET && self.head.number >= fork_id.next,
642        // not compatible.
643        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        // block number smaller than TIMESTAMP_BEFORE_ETHEREUM_MAINNET and
655        // !((fork_id.next > TIMESTAMP_BEFORE_ETHEREUM_MAINNET && self.head.timestamp >=
656        // fork_id.next) || (fork_id.next <= TIMESTAMP_BEFORE_ETHEREUM_MAINNET && self.head.number
657        // >= fork_id.next)), compatible.
658        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        // <https://github.com/ethereum/go-ethereum/blob/767b00b0b514771a663f3362dd0310fc28d40c25/core/forkid/forkid_test.go#L370-L370>
699        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            // enr request is just an expiration
768            let fork_id = ForkId { hash: ForkHash(hex!("deadbeef")), next: 0xBADDCAFE };
769
770            // add some junk
771            let junk: u64 = 112233;
772
773            // rlp header encoding
774            let payload_length = fork_id.length() + junk.length();
775            alloy_rlp::Header { list: true, payload_length }.encode(&mut buf);
776
777            // fields
778            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}