ethereum_forkid/
lib.rs

1#![deny(missing_docs)]
2#![allow(clippy::redundant_else, clippy::too_many_lines)]
3#![doc = include_str!("../README.md")]
4
5use crc::crc32;
6use maplit::btreemap;
7use primitive_types::H256;
8use std::{
9    collections::{BTreeMap, BTreeSet},
10    ops::{Add, AddAssign},
11};
12use thiserror::Error;
13
14/// Block number.
15pub type BlockNumber = u64;
16
17/// `CRC32` hash of all previous forks starting from genesis block.
18#[derive(
19    Clone,
20    Copy,
21    Debug,
22    PartialEq,
23    Eq,
24    Hash,
25    fastrlp::EncodableWrapper,
26    fastrlp::DecodableWrapper,
27    fastrlp::MaxEncodedLen,
28)]
29pub struct ForkHash(pub [u8; 4]);
30
31impl From<H256> for ForkHash {
32    fn from(genesis: H256) -> Self {
33        Self(crc32::checksum_ieee(&genesis[..]).to_be_bytes())
34    }
35}
36
37impl AddAssign<BlockNumber> for ForkHash {
38    fn add_assign(&mut self, block: BlockNumber) {
39        let blob = block.to_be_bytes();
40        self.0 = crc32::update(u32::from_be_bytes(self.0), &crc32::IEEE_TABLE, &blob).to_be_bytes();
41    }
42}
43
44impl Add<BlockNumber> for ForkHash {
45    type Output = Self;
46    fn add(mut self, block: BlockNumber) -> Self {
47        self += block;
48        self
49    }
50}
51
52/// A fork identifier as defined by EIP-2124.
53/// Serves as the chain compatibility identifier.
54#[derive(
55    Clone,
56    Copy,
57    Debug,
58    PartialEq,
59    Eq,
60    Hash,
61    fastrlp::Encodable,
62    fastrlp::Decodable,
63    fastrlp::MaxEncodedLen,
64)]
65pub struct ForkId {
66    /// CRC32 checksum of the all fork blocks from genesis.
67    pub hash: ForkHash,
68    /// Next upcoming fork block number, 0 if not yet known.
69    pub next: BlockNumber,
70}
71
72/// Reason for rejecting provided `ForkId`.
73#[derive(Clone, Copy, Debug, Error, PartialEq, Eq, Hash)]
74pub enum ValidationError {
75    /// Remote node is outdated and needs a software update.
76    #[error("remote node is outdated and needs a software update")]
77    RemoteStale,
78    /// Local node is on an incompatible chain or needs a software update.
79    #[error("local node is on an incompatible chain or needs a software update")]
80    LocalIncompatibleOrStale,
81}
82
83/// Filter that describes the state of blockchain and can be used to check incoming `ForkId`s for compatibility.
84#[derive(Clone, Debug, PartialEq)]
85pub struct ForkFilter {
86    forks: BTreeMap<BlockNumber, ForkHash>,
87
88    head: BlockNumber,
89
90    cache: Cache,
91}
92
93#[derive(Clone, Debug, PartialEq)]
94struct Cache {
95    // An epoch is a period between forks.
96    // When we progress from one fork to the next one we move to the next epoch.
97    epoch_start: BlockNumber,
98    epoch_end: Option<BlockNumber>,
99    past: Vec<(BlockNumber, ForkHash)>,
100    future: Vec<ForkHash>,
101    fork_id: ForkId,
102}
103
104impl Cache {
105    fn compute_cache(forks: &BTreeMap<BlockNumber, ForkHash>, head: BlockNumber) -> Self {
106        let mut past = Vec::with_capacity(forks.len());
107        let mut future = Vec::with_capacity(forks.len());
108
109        let mut epoch_start = 0;
110        let mut epoch_end = None;
111        for (block, hash) in forks {
112            if *block <= head {
113                epoch_start = *block;
114                past.push((*block, *hash));
115            } else {
116                if epoch_end.is_none() {
117                    epoch_end = Some(*block);
118                }
119                future.push(*hash);
120            }
121        }
122
123        let fork_id = ForkId {
124            hash: past
125                .last()
126                .expect("there is always at least one - genesis - fork hash; qed")
127                .1,
128            next: epoch_end.unwrap_or(0),
129        };
130
131        Self {
132            epoch_start,
133            epoch_end,
134            past,
135            future,
136            fork_id,
137        }
138    }
139}
140
141impl ForkFilter {
142    /// Create the filter from provided head, genesis block hash, past forks and expected future forks.
143    pub fn new<F>(head: BlockNumber, genesis: H256, forks: F) -> Self
144    where
145        F: IntoIterator<Item = BlockNumber>,
146    {
147        let genesis_fork_hash = ForkHash::from(genesis);
148        let mut forks = forks.into_iter().collect::<BTreeSet<_>>();
149        forks.remove(&0);
150        let forks = forks
151            .into_iter()
152            .fold(
153                (btreemap! { 0 => genesis_fork_hash }, genesis_fork_hash),
154                |(mut acc, base_hash), block| {
155                    let fork_hash = base_hash + block;
156                    acc.insert(block, fork_hash);
157                    (acc, fork_hash)
158                },
159            )
160            .0;
161
162        let cache = Cache::compute_cache(&forks, head);
163
164        Self { forks, head, cache }
165    }
166
167    fn set_head_priv(&mut self, head: BlockNumber) -> bool {
168        #[allow(clippy::option_if_let_else)]
169        let recompute_cache = {
170            if head < self.cache.epoch_start {
171                true
172            } else if let Some(epoch_end) = self.cache.epoch_end {
173                head >= epoch_end
174            } else {
175                false
176            }
177        };
178
179        if recompute_cache {
180            self.cache = Cache::compute_cache(&self.forks, head);
181        }
182        self.head = head;
183
184        recompute_cache
185    }
186
187    /// Set the current head
188    pub fn set_head(&mut self, head: BlockNumber) {
189        self.set_head_priv(head);
190    }
191
192    /// Return current fork id
193    #[must_use]
194    pub const fn current(&self) -> ForkId {
195        self.cache.fork_id
196    }
197
198    /// Check whether the provided `ForkId` is compatible based on the validation rules in `EIP-2124`.
199    ///
200    /// # Errors
201    /// Returns a `ValidationError` if the `ForkId` is not compatible.
202    pub fn validate(&self, fork_id: ForkId) -> Result<(), ValidationError> {
203        // 1) If local and remote FORK_HASH matches...
204        if self.current().hash == fork_id.hash {
205            if fork_id.next == 0 {
206                // 1b) No remotely announced fork, connect.
207                return Ok(());
208            }
209
210            //... compare local head to FORK_NEXT.
211            if self.head >= fork_id.next {
212                // 1a) A remotely announced but remotely not passed block is already passed locally, disconnect,
213                // since the chains are incompatible.
214                return Err(ValidationError::LocalIncompatibleOrStale);
215            } else {
216                // 1b) Remotely announced fork not yet passed locally, connect.
217                return Ok(());
218            }
219        }
220
221        // 2) If the remote FORK_HASH is a subset of the local past forks...
222        let mut it = self.cache.past.iter();
223        while let Some((_, hash)) = it.next() {
224            if *hash == fork_id.hash {
225                // ...and the remote FORK_NEXT matches with the locally following fork block number, connect.
226                if let Some((actual_fork_block, _)) = it.next() {
227                    if *actual_fork_block == fork_id.next {
228                        return Ok(());
229                    } else {
230                        return Err(ValidationError::RemoteStale);
231                    }
232                }
233
234                break;
235            }
236        }
237
238        // 3) If the remote FORK_HASH is a superset of the local past forks and can be completed with locally known future forks, connect.
239        for future_fork_hash in &self.cache.future {
240            if *future_fork_hash == fork_id.hash {
241                return Ok(());
242            }
243        }
244
245        // 4) Reject in all other cases.
246        Err(ValidationError::LocalIncompatibleOrStale)
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use hex_literal::hex;
254
255    const GENESIS_HASH: H256 = H256(hex!(
256        "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"
257    ));
258
259    // EIP test vectors.
260
261    #[test]
262    fn forkhash() {
263        let mut fork_hash = ForkHash::from(GENESIS_HASH);
264        assert_eq!(fork_hash.0, hex!("fc64ec04"));
265
266        fork_hash += 1_150_000;
267        assert_eq!(fork_hash.0, hex!("97c2c34c"));
268
269        fork_hash += 1_920_000;
270        assert_eq!(fork_hash.0, hex!("91d1f948"));
271    }
272
273    #[test]
274    fn compatibility_check() {
275        let mut filter = ForkFilter::new(
276            0,
277            GENESIS_HASH,
278            vec![
279                1_150_000, 1_920_000, 2_463_000, 2_675_000, 4_370_000, 7_280_000,
280            ],
281        );
282
283        // Local is mainnet Petersburg, remote announces the same. No future fork is announced.
284        filter.set_head(7_987_396);
285        assert_eq!(
286            filter.validate(ForkId {
287                hash: ForkHash(hex!("668db0af")),
288                next: 0
289            }),
290            Ok(())
291        );
292
293        // Local is mainnet Petersburg, remote announces the same. Remote also announces a next fork
294        // at block 0xffffffff, but that is uncertain.
295        filter.set_head(7_987_396);
296        assert_eq!(
297            filter.validate(ForkId {
298                hash: ForkHash(hex!("668db0af")),
299                next: BlockNumber::max_value()
300            }),
301            Ok(())
302        );
303
304        // Local is mainnet currently in Byzantium only (so it's aware of Petersburg),remote announces
305        // also Byzantium, but it's not yet aware of Petersburg (e.g. non updated node before the fork).
306        // In this case we don't know if Petersburg passed yet or not.
307        filter.set_head(7_279_999);
308        assert_eq!(
309            filter.validate(ForkId {
310                hash: ForkHash(hex!("a00bc324")),
311                next: 0
312            }),
313            Ok(())
314        );
315
316        // Local is mainnet currently in Byzantium only (so it's aware of Petersburg), remote announces
317        // also Byzantium, and it's also aware of Petersburg (e.g. updated node before the fork). We
318        // don't know if Petersburg passed yet (will pass) or not.
319        filter.set_head(7_279_999);
320        assert_eq!(
321            filter.validate(ForkId {
322                hash: ForkHash(hex!("a00bc324")),
323                next: 7_280_000
324            }),
325            Ok(())
326        );
327
328        // Local is mainnet currently in Byzantium only (so it's aware of Petersburg), remote announces
329        // also Byzantium, and it's also aware of some random fork (e.g. misconfigured Petersburg). As
330        // neither forks passed at neither nodes, they may mismatch, but we still connect for now.
331        filter.set_head(7_279_999);
332        assert_eq!(
333            filter.validate(ForkId {
334                hash: ForkHash(hex!("a00bc324")),
335                next: BlockNumber::max_value()
336            }),
337            Ok(())
338        );
339
340        // Local is mainnet Petersburg, remote announces Byzantium + knowledge about Petersburg. Remote is simply out of sync, accept.
341        filter.set_head(7_987_396);
342        assert_eq!(
343            filter.validate(ForkId {
344                hash: ForkHash(hex!("a00bc324")),
345                next: 7_280_000
346            }),
347            Ok(())
348        );
349
350        // Local is mainnet Petersburg, remote announces Spurious + knowledge about Byzantium. Remote
351        // is definitely out of sync. It may or may not need the Petersburg update, we don't know yet.
352        filter.set_head(7_987_396);
353        assert_eq!(
354            filter.validate(ForkId {
355                hash: ForkHash(hex!("3edd5b10")),
356                next: 4_370_000
357            }),
358            Ok(())
359        );
360
361        // Local is mainnet Byzantium, remote announces Petersburg. Local is out of sync, accept.
362        filter.set_head(7_279_999);
363        assert_eq!(
364            filter.validate(ForkId {
365                hash: ForkHash(hex!("668db0af")),
366                next: 0
367            }),
368            Ok(())
369        );
370
371        // Local is mainnet Spurious, remote announces Byzantium, but is not aware of Petersburg. Local
372        // out of sync. Local also knows about a future fork, but that is uncertain yet.
373        filter.set_head(4_369_999);
374        assert_eq!(
375            filter.validate(ForkId {
376                hash: ForkHash(hex!("a00bc324")),
377                next: 0
378            }),
379            Ok(())
380        );
381
382        // Local is mainnet Petersburg. remote announces Byzantium but is not aware of further forks.
383        // Remote needs software update.
384        filter.set_head(7_987_396);
385        assert_eq!(
386            filter.validate(ForkId {
387                hash: ForkHash(hex!("a00bc324")),
388                next: 0
389            }),
390            Err(ValidationError::RemoteStale)
391        );
392
393        // Local is mainnet Petersburg, and isn't aware of more forks. Remote announces Petersburg +
394        // 0xffffffff. Local needs software update, reject.
395        filter.set_head(7_987_396);
396        assert_eq!(
397            filter.validate(ForkId {
398                hash: ForkHash(hex!("5cddc0e1")),
399                next: 0
400            }),
401            Err(ValidationError::LocalIncompatibleOrStale)
402        );
403
404        // Local is mainnet Byzantium, and is aware of Petersburg. Remote announces Petersburg +
405        // 0xffffffff. Local needs software update, reject.
406        filter.set_head(7_279_999);
407        assert_eq!(
408            filter.validate(ForkId {
409                hash: ForkHash(hex!("5cddc0e1")),
410                next: 0
411            }),
412            Err(ValidationError::LocalIncompatibleOrStale)
413        );
414
415        // Local is mainnet Petersburg, remote is Rinkeby Petersburg.
416        filter.set_head(7_987_396);
417        assert_eq!(
418            filter.validate(ForkId {
419                hash: ForkHash(hex!("afec6b27")),
420                next: 0
421            }),
422            Err(ValidationError::LocalIncompatibleOrStale)
423        );
424
425        // Local is mainnet Petersburg, far in the future. Remote announces Gopherium (non existing fork)
426        // at some future block 88888888, for itself, but past block for local. Local is incompatible.
427        //
428        // This case detects non-upgraded nodes with majority hash power (typical Ropsten mess).
429        filter.set_head(88_888_888);
430        assert_eq!(
431            filter.validate(ForkId {
432                hash: ForkHash(hex!("668db0af")),
433                next: 88_888_888
434            }),
435            Err(ValidationError::LocalIncompatibleOrStale)
436        );
437
438        // Local is mainnet Byzantium. Remote is also in Byzantium, but announces Gopherium (non existing
439        // fork) at block 7279999, before Petersburg. Local is incompatible.
440        filter.set_head(7_279_999);
441        assert_eq!(
442            filter.validate(ForkId {
443                hash: ForkHash(hex!("a00bc324")),
444                next: 7_279_999
445            }),
446            Err(ValidationError::LocalIncompatibleOrStale)
447        );
448    }
449
450    #[test]
451    fn forkid_serialization() {
452        assert_eq!(
453            &*fastrlp::encode_fixed_size(&ForkId {
454                hash: ForkHash(hex!("00000000")),
455                next: 0
456            }),
457            hex!("c6840000000080")
458        );
459        assert_eq!(
460            &*fastrlp::encode_fixed_size(&ForkId {
461                hash: ForkHash(hex!("deadbeef")),
462                next: 0xBADD_CAFE
463            }),
464            hex!("ca84deadbeef84baddcafe")
465        );
466        assert_eq!(
467            &*fastrlp::encode_fixed_size(&ForkId {
468                hash: ForkHash(hex!("ffffffff")),
469                next: u64::max_value()
470            }),
471            hex!("ce84ffffffff88ffffffffffffffff")
472        );
473
474        assert_eq!(
475            <ForkId as fastrlp::Decodable>::decode(&mut (&hex!("c6840000000080") as &[u8]))
476                .unwrap(),
477            ForkId {
478                hash: ForkHash(hex!("00000000")),
479                next: 0
480            }
481        );
482        assert_eq!(
483            <ForkId as fastrlp::Decodable>::decode(&mut (&hex!("ca84deadbeef84baddcafe") as &[u8]))
484                .unwrap(),
485            ForkId {
486                hash: ForkHash(hex!("deadbeef")),
487                next: 0xBADD_CAFE
488            }
489        );
490        assert_eq!(
491            <ForkId as fastrlp::Decodable>::decode(
492                &mut (&hex!("ce84ffffffff88ffffffffffffffff") as &[u8])
493            )
494            .unwrap(),
495            ForkId {
496                hash: ForkHash(hex!("ffffffff")),
497                next: u64::max_value()
498            }
499        );
500    }
501
502    #[test]
503    fn compute_cache() {
504        let b1 = 1_150_000;
505        let b2 = 1_920_000;
506
507        let h0 = ForkId {
508            hash: ForkHash(hex!("fc64ec04")),
509            next: b1,
510        };
511        let h1 = ForkId {
512            hash: ForkHash(hex!("97c2c34c")),
513            next: b2,
514        };
515        let h2 = ForkId {
516            hash: ForkHash(hex!("91d1f948")),
517            next: 0,
518        };
519
520        let mut fork_filter = ForkFilter::new(0, GENESIS_HASH, vec![b1, b2]);
521
522        assert!(!fork_filter.set_head_priv(0));
523        assert_eq!(fork_filter.current(), h0);
524
525        assert!(!fork_filter.set_head_priv(1));
526        assert_eq!(fork_filter.current(), h0);
527
528        assert!(fork_filter.set_head_priv(b1 + 1));
529        assert_eq!(fork_filter.current(), h1);
530
531        assert!(!fork_filter.set_head_priv(b1));
532        assert_eq!(fork_filter.current(), h1);
533
534        assert!(fork_filter.set_head_priv(b1 - 1));
535        assert_eq!(fork_filter.current(), h0);
536
537        assert!(fork_filter.set_head_priv(b1));
538        assert_eq!(fork_filter.current(), h1);
539
540        assert!(!fork_filter.set_head_priv(b2 - 1));
541        assert_eq!(fork_filter.current(), h1);
542
543        assert!(fork_filter.set_head_priv(b2));
544        assert_eq!(fork_filter.current(), h2);
545    }
546}