From 090c3322a57141a8454f55101359d7b89a3cbefc Mon Sep 17 00:00:00 2001 From: Artem Vorotnikov Date: Thu, 6 Feb 2020 16:40:19 +0300 Subject: [PATCH] Implement EIP-2124 (#11456) * Implement EIP-2124 * Implement ForkFilter * ForkId deserialization * Derive RLP for ForkId * docs * comments by @dvdplm * docs * clippy * period --- Cargo.lock | 33 ++++ Cargo.toml | 1 + util/EIP-2124/Cargo.toml | 21 +++ util/EIP-2124/src/lib.rs | 328 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 383 insertions(+) create mode 100644 util/EIP-2124/Cargo.toml create mode 100644 util/EIP-2124/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index b3f37311a..4eaae311d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -479,6 +479,12 @@ dependencies = [ "serde", ] +[[package]] +name = "build_const" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" + [[package]] name = "bumpalo" version = "3.1.2" @@ -748,6 +754,15 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" +[[package]] +name = "crc" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" +dependencies = [ + "build_const", +] + [[package]] name = "criterion" version = "0.3.0" @@ -988,6 +1003,18 @@ dependencies = [ "rustc-hex 2.0.1", ] +[[package]] +name = "eip-2124" +version = "0.1.0" +dependencies = [ + "crc", + "ethereum-types", + "hex-literal", + "maplit", + "rlp", + "rlp_derive", +] + [[package]] name = "eip-712" version = "0.1.1" @@ -2826,6 +2853,12 @@ dependencies = [ "synstructure 0.12.3", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "matches" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index 9897a2012..dd9384aa4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,4 +135,5 @@ members = [ "chainspec", "ethcore/wasm/run", "evmbin", + "util/EIP-2124" ] diff --git a/util/EIP-2124/Cargo.toml b/util/EIP-2124/Cargo.toml new file mode 100644 index 000000000..09b095625 --- /dev/null +++ b/util/EIP-2124/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "eip-2124" +version = "0.1.0" +authors = ["Parity Technologies "] +repository = "https://github.com/paritytech/parity-ethereum" +documentation = "https://docs.rs/eip-2124" +readme = "README.md" +description = "EIP-2124 Fork ID implementation" +keywords = ["eip-2124", "eip"] +license = "GPL-3.0" +edition = "2018" + +[dependencies] +crc = "1" +ethereum-types = "0.8.0" +maplit = "1" +rlp = "0.4" +rlp_derive = { path = "../rlp-derive" } + +[dev-dependencies] +hex-literal = "0.2" diff --git a/util/EIP-2124/src/lib.rs b/util/EIP-2124/src/lib.rs new file mode 100644 index 000000000..612465814 --- /dev/null +++ b/util/EIP-2124/src/lib.rs @@ -0,0 +1,328 @@ +// Copyright 2015-2019 Parity Technologies (UK) Ltd. +// This file is part of Parity Ethereum. + +// Parity Ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Ethereum. If not, see . + +//! EIP-2124 implementation based on . + +#![deny(missing_docs)] + +#![warn( + clippy::all, + clippy::pedantic, + clippy::nursery, +)] + +use crc::crc32; +use ethereum_types::H256; +use maplit::btreemap; +use rlp::{DecoderError, Rlp, RlpStream}; +use rlp_derive::{RlpDecodable, RlpEncodable}; +use std::collections::{BTreeMap, BTreeSet}; + +/// Block number. +pub type BlockNumber = u64; + +/// `CRC32` hash of all previous forks starting from genesis block. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct ForkHash(pub u32); + +impl rlp::Encodable for ForkHash { + fn rlp_append(&self, s: &mut RlpStream) { + s.encoder().encode_value(&self.0.to_be_bytes()); + } +} + +impl rlp::Decodable for ForkHash { + fn decode(rlp: &Rlp) -> Result { + rlp.decoder().decode_value(|b| { + if b.len() != 4 { + return Err(DecoderError::RlpInvalidLength); + } + + let mut blob = [0; 4]; + blob.copy_from_slice(&b[..]); + + Ok(Self(u32::from_be_bytes(blob))) + }) + } +} + +impl From for ForkHash { + fn from(genesis: H256) -> Self { + Self(crc32::checksum_ieee(&genesis[..])) + } +} + +impl std::ops::AddAssign for ForkHash { + fn add_assign(&mut self, height: BlockNumber) { + let blob = height.to_be_bytes(); + self.0 = crc32::update(self.0, &crc32::IEEE_TABLE, &blob) + } +} + +impl std::ops::Add for ForkHash { + type Output = Self; + fn add(mut self, height: BlockNumber) -> Self { + self += height; + self + } +} + +/// A fork identifier as defined by EIP-2124. +/// Serves as the chain compatibility identifier. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, RlpEncodable, RlpDecodable)] +pub struct ForkId { + /// CRC32 checksum of the all fork blocks from genesis. + pub hash: ForkHash, + /// Next upcoming fork block number, 0 if not yet known. + pub next: BlockNumber +} + +/// Reason for rejecting provided `ForkId`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum RejectReason { + /// Remote node is outdated and needs a software update. + RemoteStale, + /// Local node is on an incompatible chain or needs a sofwtare update. + LocalIncompatibleOrStale, +} + +/// Filter that describes the state of blockchain and can be used to check incoming `ForkId`s for compatibility. +#[derive(Clone, Debug)] +pub struct ForkFilter { + /// Blockchain head + pub head: BlockNumber, + past_forks: BTreeMap, + next_forks: BTreeSet, +} + +impl ForkFilter { + /// Create the filter from provided head, genesis block hash, past forks and expected future forks. + pub fn new(head: BlockNumber, genesis: H256, past_forks: PF, next_forks: NF) -> Self + where + PF: IntoIterator, + NF: IntoIterator, + { + let genesis_fork_hash = ForkHash::from(genesis); + Self { + head, + past_forks: past_forks.into_iter().fold((btreemap! { 0 => genesis_fork_hash }, genesis_fork_hash), |(mut acc, base_hash), block| { + let fork_hash = base_hash + block; + acc.insert(block, fork_hash); + (acc, fork_hash) + }).0, + next_forks: next_forks.into_iter().collect(), + } + } + + fn current_fork_hash(&self) -> ForkHash { + *self.past_forks.values().next_back().expect("there is always at least one - genesis - fork hash; qed") + } + + fn future_fork_hashes(&self) -> Vec { + self.next_forks.iter().fold((Vec::new(), self.current_fork_hash()), |(mut acc, hash), fork| { + let next = hash + *fork; + acc.push(next); + (acc, next) + }).0 + } + + /// Insert a new past fork + pub fn insert_past_fork(&mut self, height: BlockNumber) { + self.past_forks.insert(height, self.current_fork_hash() + height); + } + + /// Insert a new upcoming fork + pub fn insert_next_fork(&mut self, height: BlockNumber) { + self.next_forks.insert(height); + } + + /// Mark an upcoming fork as already happened and immutable. + /// Returns `false` if no such fork existed and the call was a no-op. + pub fn promote_next_fork(&mut self, height: BlockNumber) -> bool { + let promoted = self.next_forks.remove(&height); + if promoted { + self.insert_past_fork(height); + } + promoted + } + + /// Check whether the provided `ForkId` is compatible based on the validation rules in `EIP-2124`. + /// + /// # Errors + /// Returns a `RejectReason` if the `ForkId` is not compatible. + pub fn is_valid(&self, fork_id: ForkId) -> Result<(), RejectReason> { + // 1) If local and remote FORK_HASH matches... + if self.current_fork_hash() == fork_id.hash { + if fork_id.next == 0 { + // 1b) No remotely announced fork, connect. + return Ok(()) + } + + //... compare local head to FORK_NEXT. + if self.head >= fork_id.next { + // 1a) A remotely announced but remotely not passed block is already passed locally, disconnect, + // since the chains are incompatible. + return Err(RejectReason::LocalIncompatibleOrStale) + } else { + // 1b) Remotely announced fork not yet passed locally, connect. + return Ok(()) + } + } + + // 2) If the remote FORK_HASH is a subset of the local past forks... + let mut it = self.past_forks.iter(); + while let Some((_, hash)) = it.next() { + if *hash == fork_id.hash { + // ...and the remote FORK_NEXT matches with the locally following fork block number, connect. + if let Some((actual_fork_block, _)) = it.next() { + if *actual_fork_block == fork_id.next { + return Ok(()) + } else { + return Err(RejectReason::RemoteStale); + } + } + + break; + } + } + + // 3) If the remote FORK_HASH is a superset of the local past forks and can be completed with locally known future forks, connect. + for future_fork_hash in self.future_fork_hashes() { + if future_fork_hash == fork_id.hash { + return Ok(()) + } + } + + // 4) Reject in all other cases. + Err(RejectReason::LocalIncompatibleOrStale) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + + const GENESIS_HASH: &str = "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"; + const BYZANTIUM_FORK_HEIGHT: BlockNumber = 4370000; + const PETERSBURG_FORK_HEIGHT: BlockNumber = 7280000; + + // EIP test vectors. + + #[test] + fn test_forkhash() { + let mut fork_hash = ForkHash::from(GENESIS_HASH.parse::().unwrap()); + assert_eq!(fork_hash.0, 0xfc64ec04); + + fork_hash += 1150000; + assert_eq!(fork_hash.0, 0x97c2c34c); + + fork_hash += 1920000; + assert_eq!(fork_hash.0, 0x91d1f948); + } + + #[test] + fn test_compatibility_check() { + let spurious_filter = ForkFilter::new( + 4369999, + GENESIS_HASH.parse().unwrap(), + vec![1150000, 1920000, 2463000, 2675000], + vec![BYZANTIUM_FORK_HEIGHT] + ); + let mut byzantium_filter = spurious_filter.clone(); + byzantium_filter.promote_next_fork(BYZANTIUM_FORK_HEIGHT); + byzantium_filter.insert_next_fork(PETERSBURG_FORK_HEIGHT); + byzantium_filter.head = 7279999; + + let mut petersburg_filter = byzantium_filter.clone(); + petersburg_filter.promote_next_fork(PETERSBURG_FORK_HEIGHT); + petersburg_filter.head = 7987396; + + // Local is mainnet Petersburg, remote announces the same. No future fork is announced. + assert_eq!(petersburg_filter.is_valid(ForkId { hash: ForkHash(0x668db0af), next: 0 }), Ok(())); + + // Local is mainnet Petersburg, remote announces the same. Remote also announces a next fork + // at block 0xffffffff, but that is uncertain. + assert_eq!(petersburg_filter.is_valid(ForkId { hash: ForkHash(0x668db0af), next: BlockNumber::max_value() }), Ok(())); + + // Local is mainnet currently in Byzantium only (so it's aware of Petersburg),remote announces + // also Byzantium, but it's not yet aware of Petersburg (e.g. non updated node before the fork). + // In this case we don't know if Petersburg passed yet or not. + assert_eq!(byzantium_filter.is_valid(ForkId { hash: ForkHash(0xa00bc324), next: 0 }), Ok(())); + + // Local is mainnet currently in Byzantium only (so it's aware of Petersburg), remote announces + // also Byzantium, and it's also aware of Petersburg (e.g. updated node before the fork). We + // don't know if Petersburg passed yet (will pass) or not. + assert_eq!(byzantium_filter.is_valid(ForkId { hash: ForkHash(0xa00bc324), next: PETERSBURG_FORK_HEIGHT }), Ok(())); + + // Local is mainnet currently in Byzantium only (so it's aware of Petersburg), remote announces + // also Byzantium, and it's also aware of some random fork (e.g. misconfigured Petersburg). As + // neither forks passed at neither nodes, they may mismatch, but we still connect for now. + assert_eq!(byzantium_filter.is_valid(ForkId { hash: ForkHash(0xa00bc324), next: BlockNumber::max_value() }), Ok(())); + + // Local is mainnet Petersburg, remote announces Byzantium + knowledge about Petersburg. Remote is simply out of sync, accept. + assert_eq!(petersburg_filter.is_valid(ForkId { hash: ForkHash(0xa00bc324), next: PETERSBURG_FORK_HEIGHT }), Ok(())); + + // Local is mainnet Petersburg, remote announces Spurious + knowledge about Byzantium. Remote + // is definitely out of sync. It may or may not need the Petersburg update, we don't know yet. + assert_eq!(petersburg_filter.is_valid(ForkId { hash: ForkHash(0x3edd5b10), next: 4370000 }), Ok(())); + + // Local is mainnet Byzantium, remote announces Petersburg. Local is out of sync, accept. + assert_eq!(byzantium_filter.is_valid(ForkId { hash: ForkHash(0x668db0af), next: 0 }), Ok(())); + + // Local is mainnet Spurious, remote announces Byzantium, but is not aware of Petersburg. Local + // out of sync. Local also knows about a future fork, but that is uncertain yet. + assert_eq!(spurious_filter.is_valid(ForkId { hash: ForkHash(0xa00bc324), next: 0 }), Ok(())); + + // Local is mainnet Petersburg. remote announces Byzantium but is not aware of further forks. + // Remote needs software update. + assert_eq!(petersburg_filter.is_valid(ForkId { hash: ForkHash(0xa00bc324), next: 0 }), Err(RejectReason::RemoteStale)); + + // Local is mainnet Petersburg, and isn't aware of more forks. Remote announces Petersburg + + // 0xffffffff. Local needs software update, reject. + assert_eq!(petersburg_filter.is_valid(ForkId { hash: ForkHash(0x5cddc0e1), next: 0 }), Err(RejectReason::LocalIncompatibleOrStale)); + + // Local is mainnet Byzantium, and is aware of Petersburg. Remote announces Petersburg + + // 0xffffffff. Local needs software update, reject. + assert_eq!(byzantium_filter.is_valid(ForkId { hash: ForkHash(0x5cddc0e1), next: 0 }), Err(RejectReason::LocalIncompatibleOrStale)); + + // Local is mainnet Petersburg, remote is Rinkeby Petersburg. + assert_eq!(petersburg_filter.is_valid(ForkId { hash: ForkHash(0xafec6b27), next: 0 }), Err(RejectReason::LocalIncompatibleOrStale)); + + // Local is mainnet Petersburg, far in the future. Remote announces Gopherium (non existing fork) + // at some future block 88888888, for itself, but past block for local. Local is incompatible. + // + // This case detects non-upgraded nodes with majority hash power (typical Ropsten mess). + let mut far_away_petersburg = petersburg_filter.clone(); + far_away_petersburg.head = 88888888; + assert_eq!(far_away_petersburg.is_valid(ForkId { hash: ForkHash(0x668db0af), next: 88888888 }), Err(RejectReason::LocalIncompatibleOrStale)); + + // Local is mainnet Byzantium. Remote is also in Byzantium, but announces Gopherium (non existing + // fork) at block 7279999, before Petersburg. Local is incompatible. + assert_eq!(byzantium_filter.is_valid(ForkId { hash: ForkHash(0xa00bc324), next: 7279999 }), Err(RejectReason::LocalIncompatibleOrStale)); + } + + #[test] + fn test_forkid_serialization() { + assert_eq!(rlp::encode(&ForkId { hash: ForkHash(0), next: 0 }), hex!("c6840000000080")); + assert_eq!(rlp::encode(&ForkId { hash: ForkHash(0xdeadbeef), next: 0xBADDCAFE }), hex!("ca84deadbeef84baddcafe")); + assert_eq!(rlp::encode(&ForkId { hash: ForkHash(u32::max_value()), next: u64::max_value() }), hex!("ce84ffffffff88ffffffffffffffff")); + + assert_eq!(rlp::decode::(&hex!("c6840000000080")).unwrap(), ForkId { hash: ForkHash(0), next: 0 }); + assert_eq!(rlp::decode::(&hex!("ca84deadbeef84baddcafe")).unwrap(), ForkId { hash: ForkHash(0xdeadbeef), next: 0xBADDCAFE }); + assert_eq!(rlp::decode::(&hex!("ce84ffffffff88ffffffffffffffff")).unwrap(), ForkId { hash: ForkHash(u32::max_value()), next: u64::max_value() }); + } +}