diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..1b0e150ce6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/ef_tests/eth2.0-spec-tests"] + path = tests/ef_tests/eth2.0-spec-tests + url = https://github.com/ethereum/eth2.0-spec-tests diff --git a/Cargo.toml b/Cargo.toml index 8931899412..704bfde137 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ members = [ "beacon_node/rpc", "beacon_node/version", "beacon_node/beacon_chain", + "tests/ef_tests", "protos", "validator_client", "account_manager", diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 3b022551eb..0000000000 --- a/Jenkinsfile +++ /dev/null @@ -1,34 +0,0 @@ -pipeline { - agent { - dockerfile { - filename 'Dockerfile' - args '-v cargo-cache:/cache/cargocache:rw -e "CARGO_HOME=/cache/cargocache"' - } - } - stages { - stage('Build') { - steps { - sh 'cargo build --verbose --all' - sh 'cargo build --verbose --all --release' - } - } - stage('Check') { - steps { - sh 'cargo fmt --all -- --check' - // No clippy until later... - //sh 'cargo clippy' - } - } - stage('Test') { - steps { - sh 'cargo test --verbose --all' - sh 'cargo test --verbose --all --release' - sh 'cargo test --manifest-path eth2/state_processing/Cargo.toml --verbose \ - --release --features fake_crypto' - sh 'cargo test --manifest-path eth2/state_processing/Cargo.toml --verbose \ - --release --features fake_crypto -- --ignored' - - } - } - } -} diff --git a/eth2/types/src/beacon_block_body.rs b/eth2/types/src/beacon_block_body.rs index 8bbdbec15c..e3609d889b 100644 --- a/eth2/types/src/beacon_block_body.rs +++ b/eth2/types/src/beacon_block_body.rs @@ -1,4 +1,4 @@ -use crate::test_utils::TestRandom; +use crate::test_utils::{graffiti_from_hex_str, TestRandom}; use crate::*; use serde_derive::{Deserialize, Serialize}; @@ -24,6 +24,7 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; pub struct BeaconBlockBody { pub randao_reveal: Signature, pub eth1_data: Eth1Data, + #[serde(deserialize_with = "graffiti_from_hex_str")] pub graffiti: [u8; 32], pub proposer_slashings: Vec, pub attester_slashings: Vec, diff --git a/eth2/types/src/lib.rs b/eth2/types/src/lib.rs index e2225d6386..4d0ec5fae3 100644 --- a/eth2/types/src/lib.rs +++ b/eth2/types/src/lib.rs @@ -81,7 +81,7 @@ pub type AttesterMap = HashMap<(u64, u64), Vec>; pub type ProposerMap = HashMap; pub use bls::{AggregatePublicKey, AggregateSignature, Keypair, PublicKey, SecretKey, Signature}; -pub use fixed_len_vec::{typenum::Unsigned, FixedLenVec}; +pub use fixed_len_vec::{typenum, typenum::Unsigned, FixedLenVec}; pub use libp2p::floodsub::{Topic, TopicBuilder, TopicHash}; pub use libp2p::multiaddr; pub use libp2p::Multiaddr; diff --git a/eth2/types/src/test_utils/mod.rs b/eth2/types/src/test_utils/mod.rs index b88a623a36..ee8327be86 100644 --- a/eth2/types/src/test_utils/mod.rs +++ b/eth2/types/src/test_utils/mod.rs @@ -14,5 +14,5 @@ pub use rand::{ RngCore, {prng::XorShiftRng, SeedableRng}, }; -pub use serde_utils::{fork_from_hex_str, u8_from_hex_str}; +pub use serde_utils::{fork_from_hex_str, graffiti_from_hex_str, u8_from_hex_str}; pub use test_random::TestRandom; diff --git a/eth2/types/src/test_utils/serde_utils.rs b/eth2/types/src/test_utils/serde_utils.rs index 761aee5233..5c0238c0bb 100644 --- a/eth2/types/src/test_utils/serde_utils.rs +++ b/eth2/types/src/test_utils/serde_utils.rs @@ -2,6 +2,7 @@ use serde::de::Error; use serde::{Deserialize, Deserializer}; pub const FORK_BYTES_LEN: usize = 4; +pub const GRAFFITI_BYTES_LEN: usize = 32; pub fn u8_from_hex_str<'de, D>(deserializer: D) -> Result where @@ -32,3 +33,24 @@ where } Ok(array) } + +pub fn graffiti_from_hex_str<'de, D>(deserializer: D) -> Result<[u8; GRAFFITI_BYTES_LEN], D::Error> +where + D: Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + let mut array = [0 as u8; GRAFFITI_BYTES_LEN]; + let decoded: Vec = hex::decode(&s.as_str()[2..]).map_err(D::Error::custom)?; + + if decoded.len() > GRAFFITI_BYTES_LEN { + return Err(D::Error::custom("Fork length too long")); + } + + for (i, item) in array.iter_mut().enumerate() { + if i > decoded.len() { + break; + } + *item = decoded[i]; + } + Ok(array) +} diff --git a/eth2/utils/bls/Cargo.toml b/eth2/utils/bls/Cargo.toml index dcace15c81..877afa4428 100644 --- a/eth2/utils/bls/Cargo.toml +++ b/eth2/utils/bls/Cargo.toml @@ -5,10 +5,11 @@ authors = ["Paul Hauner "] edition = "2018" [dependencies] -bls-aggregates = { git = "https://github.com/sigp/signature-schemes", tag = "0.6.1" } +bls-aggregates = { git = "https://github.com/sigp/signature-schemes", branch = "secret-key-serialization" } cached_tree_hash = { path = "../cached_tree_hash" } hashing = { path = "../hashing" } hex = "0.3" +rand = "0.5" serde = "1.0" serde_derive = "1.0" serde_hex = { path = "../serde_hex" } diff --git a/eth2/utils/bls/src/fake_aggregate_public_key.rs b/eth2/utils/bls/src/fake_aggregate_public_key.rs new file mode 100644 index 0000000000..80256034a8 --- /dev/null +++ b/eth2/utils/bls/src/fake_aggregate_public_key.rs @@ -0,0 +1,36 @@ +use super::{PublicKey, BLS_PUBLIC_KEY_BYTE_SIZE}; +use bls_aggregates::AggregatePublicKey as RawAggregatePublicKey; + +/// A BLS aggregate public key. +/// +/// This struct is a wrapper upon a base type and provides helper functions (e.g., SSZ +/// serialization). +#[derive(Debug, Clone, Default)] +pub struct FakeAggregatePublicKey { + bytes: Vec, +} + +impl FakeAggregatePublicKey { + pub fn new() -> Self { + Self::zero() + } + + /// Creates a new all-zero's aggregate public key + pub fn zero() -> Self { + Self { + bytes: vec![0; BLS_PUBLIC_KEY_BYTE_SIZE], + } + } + + pub fn add(&mut self, _public_key: &PublicKey) { + // No nothing. + } + + pub fn as_raw(&self) -> &FakeAggregatePublicKey { + &self + } + + pub fn as_bytes(&self) -> Vec { + self.bytes.clone() + } +} diff --git a/eth2/utils/bls/src/fake_aggregate_signature.rs b/eth2/utils/bls/src/fake_aggregate_signature.rs index 12532d080e..709c008aac 100644 --- a/eth2/utils/bls/src/fake_aggregate_signature.rs +++ b/eth2/utils/bls/src/fake_aggregate_signature.rs @@ -1,4 +1,7 @@ -use super::{fake_signature::FakeSignature, AggregatePublicKey, BLS_AGG_SIG_BYTE_SIZE}; +use super::{ + fake_aggregate_public_key::FakeAggregatePublicKey, fake_signature::FakeSignature, + BLS_AGG_SIG_BYTE_SIZE, +}; use cached_tree_hash::cached_tree_hash_ssz_encoding_as_vector; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; @@ -43,7 +46,7 @@ impl FakeAggregateSignature { &self, _msg: &[u8], _domain: u64, - _aggregate_public_key: &AggregatePublicKey, + _aggregate_public_key: &FakeAggregatePublicKey, ) -> bool { true } @@ -53,7 +56,7 @@ impl FakeAggregateSignature { &self, _messages: &[&[u8]], _domain: u64, - _aggregate_public_keys: &[&AggregatePublicKey], + _aggregate_public_keys: &[&FakeAggregatePublicKey], ) -> bool { true } diff --git a/eth2/utils/bls/src/fake_public_key.rs b/eth2/utils/bls/src/fake_public_key.rs new file mode 100644 index 0000000000..2c14191c0a --- /dev/null +++ b/eth2/utils/bls/src/fake_public_key.rs @@ -0,0 +1,169 @@ +use super::{SecretKey, BLS_PUBLIC_KEY_BYTE_SIZE}; +use bls_aggregates::PublicKey as RawPublicKey; +use cached_tree_hash::cached_tree_hash_ssz_encoding_as_vector; +use serde::de::{Deserialize, Deserializer}; +use serde::ser::{Serialize, Serializer}; +use serde_hex::{encode as hex_encode, HexVisitor}; +use ssz::{ssz_encode, Decode, DecodeError}; +use std::default; +use std::fmt; +use std::hash::{Hash, Hasher}; +use tree_hash::tree_hash_ssz_encoding_as_vector; + +/// A single BLS signature. +/// +/// This struct is a wrapper upon a base type and provides helper functions (e.g., SSZ +/// serialization). +#[derive(Debug, Clone, Eq)] +pub struct FakePublicKey { + bytes: Vec, +} + +impl FakePublicKey { + pub fn from_secret_key(_secret_key: &SecretKey) -> Self { + Self::zero() + } + + /// Creates a new all-zero's public key + pub fn zero() -> Self { + Self { + bytes: vec![0; BLS_PUBLIC_KEY_BYTE_SIZE], + } + } + + /// Returns the underlying point as compressed bytes. + /// + /// Identical to `self.as_uncompressed_bytes()`. + pub fn as_bytes(&self) -> Vec { + self.bytes.clone() + } + + /// Converts compressed bytes to FakePublicKey + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(Self { + bytes: bytes.to_vec(), + }) + } + + /// Returns the FakePublicKey as (x, y) bytes + pub fn as_uncompressed_bytes(&self) -> Vec { + self.as_bytes() + } + + /// Converts (x, y) bytes to FakePublicKey + pub fn from_uncompressed_bytes(bytes: &[u8]) -> Result { + Self::from_bytes(bytes) + } + + /// Returns the last 6 bytes of the SSZ encoding of the public key, as a hex string. + /// + /// Useful for providing a short identifier to the user. + pub fn concatenated_hex_id(&self) -> String { + let bytes = ssz_encode(self); + let end_bytes = &bytes[bytes.len().saturating_sub(6)..bytes.len()]; + hex_encode(end_bytes) + } + + // Returns itself + pub fn as_raw(&self) -> &Self { + self + } +} + +impl fmt::Display for FakePublicKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.concatenated_hex_id()) + } +} + +impl default::Default for FakePublicKey { + fn default() -> Self { + let secret_key = SecretKey::random(); + FakePublicKey::from_secret_key(&secret_key) + } +} + +impl_ssz!(FakePublicKey, BLS_PUBLIC_KEY_BYTE_SIZE, "FakePublicKey"); + +impl Serialize for FakePublicKey { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&hex_encode(self.as_bytes())) + } +} + +impl<'de> Deserialize<'de> for FakePublicKey { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let bytes = deserializer.deserialize_str(HexVisitor)?; + let pubkey = Self::from_ssz_bytes(&bytes[..]) + .map_err(|e| serde::de::Error::custom(format!("invalid pubkey ({:?})", e)))?; + Ok(pubkey) + } +} + +tree_hash_ssz_encoding_as_vector!(FakePublicKey); +cached_tree_hash_ssz_encoding_as_vector!(FakePublicKey, 48); + +impl PartialEq for FakePublicKey { + fn eq(&self, other: &FakePublicKey) -> bool { + ssz_encode(self) == ssz_encode(other) + } +} + +impl Hash for FakePublicKey { + /// Note: this is distinct from consensus serialization, it will produce a different hash. + /// + /// This method uses the uncompressed bytes, which are much faster to obtain than the + /// compressed bytes required for consensus serialization. + /// + /// Use `ssz::Encode` to obtain the bytes required for consensus hashing. + fn hash(&self, state: &mut H) { + self.as_uncompressed_bytes().hash(state) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ssz::ssz_encode; + use tree_hash::TreeHash; + + #[test] + pub fn test_ssz_round_trip() { + let sk = SecretKey::random(); + let original = FakePublicKey::from_secret_key(&sk); + + let bytes = ssz_encode(&original); + let decoded = FakePublicKey::from_ssz_bytes(&bytes).unwrap(); + + assert_eq!(original, decoded); + } + + #[test] + pub fn test_cached_tree_hash() { + let sk = SecretKey::random(); + let original = FakePublicKey::from_secret_key(&sk); + + let mut cache = cached_tree_hash::TreeHashCache::new(&original).unwrap(); + + assert_eq!( + cache.tree_hash_root().unwrap().to_vec(), + original.tree_hash_root() + ); + + let sk = SecretKey::random(); + let modified = FakePublicKey::from_secret_key(&sk); + + cache.update(&modified).unwrap(); + + assert_eq!( + cache.tree_hash_root().unwrap().to_vec(), + modified.tree_hash_root() + ); + } +} diff --git a/eth2/utils/bls/src/lib.rs b/eth2/utils/bls/src/lib.rs index afe6559392..2c257d3264 100644 --- a/eth2/utils/bls/src/lib.rs +++ b/eth2/utils/bls/src/lib.rs @@ -3,33 +3,50 @@ extern crate ssz; #[macro_use] mod macros; -mod aggregate_public_key; mod keypair; -mod public_key; mod secret_key; -#[cfg(not(feature = "fake_crypto"))] -mod aggregate_signature; -#[cfg(not(feature = "fake_crypto"))] -mod signature; -#[cfg(not(feature = "fake_crypto"))] -pub use crate::aggregate_signature::AggregateSignature; -#[cfg(not(feature = "fake_crypto"))] -pub use crate::signature::Signature; +pub use crate::keypair::Keypair; +pub use crate::secret_key::SecretKey; +pub use bls_aggregates::{compress_g2, hash_on_g2}; +#[cfg(feature = "fake_crypto")] +mod fake_aggregate_public_key; #[cfg(feature = "fake_crypto")] mod fake_aggregate_signature; #[cfg(feature = "fake_crypto")] +mod fake_public_key; +#[cfg(feature = "fake_crypto")] mod fake_signature; -#[cfg(feature = "fake_crypto")] -pub use crate::fake_aggregate_signature::FakeAggregateSignature as AggregateSignature; -#[cfg(feature = "fake_crypto")] -pub use crate::fake_signature::FakeSignature as Signature; -pub use crate::aggregate_public_key::AggregatePublicKey; -pub use crate::keypair::Keypair; -pub use crate::public_key::PublicKey; -pub use crate::secret_key::SecretKey; +#[cfg(not(feature = "fake_crypto"))] +mod aggregate_public_key; +#[cfg(not(feature = "fake_crypto"))] +mod aggregate_signature; +#[cfg(not(feature = "fake_crypto"))] +mod public_key; +#[cfg(not(feature = "fake_crypto"))] +mod signature; + +#[cfg(feature = "fake_crypto")] +pub use fakes::*; +#[cfg(feature = "fake_crypto")] +mod fakes { + pub use crate::fake_aggregate_public_key::FakeAggregatePublicKey as AggregatePublicKey; + pub use crate::fake_aggregate_signature::FakeAggregateSignature as AggregateSignature; + pub use crate::fake_public_key::FakePublicKey as PublicKey; + pub use crate::fake_signature::FakeSignature as Signature; +} + +#[cfg(not(feature = "fake_crypto"))] +pub use reals::*; +#[cfg(not(feature = "fake_crypto"))] +mod reals { + pub use crate::aggregate_public_key::AggregatePublicKey; + pub use crate::aggregate_signature::AggregateSignature; + pub use crate::public_key::PublicKey; + pub use crate::signature::Signature; +} pub const BLS_AGG_SIG_BYTE_SIZE: usize = 96; pub const BLS_SIG_BYTE_SIZE: usize = 96; diff --git a/eth2/utils/bls/src/secret_key.rs b/eth2/utils/bls/src/secret_key.rs index 620780261b..6fdc702c61 100644 --- a/eth2/utils/bls/src/secret_key.rs +++ b/eth2/utils/bls/src/secret_key.rs @@ -1,3 +1,5 @@ +extern crate rand; + use super::BLS_SECRET_KEY_BYTE_SIZE; use bls_aggregates::SecretKey as RawSecretKey; use hex::encode as hex_encode; @@ -16,7 +18,7 @@ pub struct SecretKey(RawSecretKey); impl SecretKey { pub fn random() -> Self { - SecretKey(RawSecretKey::random()) + SecretKey(RawSecretKey::random(&mut rand::thread_rng())) } /// Returns the underlying point as compressed bytes. diff --git a/eth2/utils/fixed_len_vec/src/lib.rs b/eth2/utils/fixed_len_vec/src/lib.rs index 5066db288d..b8a3292bd5 100644 --- a/eth2/utils/fixed_len_vec/src/lib.rs +++ b/eth2/utils/fixed_len_vec/src/lib.rs @@ -9,6 +9,7 @@ pub use typenum; mod impls; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(transparent)] pub struct FixedLenVec { vec: Vec, _phantom: PhantomData, diff --git a/eth2/utils/ssz/src/decode/impls.rs b/eth2/utils/ssz/src/decode/impls.rs index cecc1935ec..ccb99fa2f0 100644 --- a/eth2/utils/ssz/src/decode/impls.rs +++ b/eth2/utils/ssz/src/decode/impls.rs @@ -1,5 +1,5 @@ use super::*; -use ethereum_types::H256; +use ethereum_types::{H256, U128, U256}; macro_rules! impl_decodable_for_uint { ($type: ident, $bit_size: expr) => { @@ -85,6 +85,48 @@ impl Decode for H256 { } } +impl Decode for U256 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 32 + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + Ok(U256::from_little_endian(bytes)) + } + } +} + +impl Decode for U128 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 16 + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + Ok(U128::from_little_endian(bytes)) + } + } +} + macro_rules! impl_decodable_for_u8_array { ($len: expr) => { impl Decode for [u8; $len] { diff --git a/eth2/utils/ssz/src/encode/impls.rs b/eth2/utils/ssz/src/encode/impls.rs index dbe4e700aa..0d6891c5e2 100644 --- a/eth2/utils/ssz/src/encode/impls.rs +++ b/eth2/utils/ssz/src/encode/impls.rs @@ -1,5 +1,5 @@ use super::*; -use ethereum_types::H256; +use ethereum_types::{H256, U128, U256}; macro_rules! impl_encodable_for_uint { ($type: ident, $bit_size: expr) => { @@ -77,6 +77,42 @@ impl Encode for H256 { } } +impl Encode for U256 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 32 + } + + fn ssz_append(&self, buf: &mut Vec) { + let n = ::ssz_fixed_len(); + let s = buf.len(); + + buf.resize(s + n, 0); + self.to_little_endian(&mut buf[s..]); + } +} + +impl Encode for U128 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 16 + } + + fn ssz_append(&self, buf: &mut Vec) { + let n = ::ssz_fixed_len(); + let s = buf.len(); + + buf.resize(s + n, 0); + self.to_little_endian(&mut buf[s..]); + } +} + macro_rules! impl_encodable_for_u8_array { ($len: expr) => { impl Encode for [u8; $len] { diff --git a/tests/ef_tests/Cargo.toml b/tests/ef_tests/Cargo.toml new file mode 100644 index 0000000000..b7596755de --- /dev/null +++ b/tests/ef_tests/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ef_tests" +version = "0.1.0" +authors = ["Paul Hauner "] +edition = "2018" + +[features] +fake_crypto = ["bls/fake_crypto"] + +[dependencies] +bls = { path = "../../eth2/utils/bls" } +ethereum-types = "0.5" +hex = "0.3" +rayon = "1.0" +serde = "1.0" +serde_derive = "1.0" +serde_yaml = "0.8" +ssz = { path = "../../eth2/utils/ssz" } +tree_hash = { path = "../../eth2/utils/tree_hash" } +cached_tree_hash = { path = "../../eth2/utils/cached_tree_hash" } +types = { path = "../../eth2/types" } +walkdir = "2" +yaml-rust = { git = "https://github.com/sigp/yaml-rust", branch = "escape_all_str"} diff --git a/tests/ef_tests/eth2.0-spec-tests b/tests/ef_tests/eth2.0-spec-tests new file mode 160000 index 0000000000..161a36ee62 --- /dev/null +++ b/tests/ef_tests/eth2.0-spec-tests @@ -0,0 +1 @@ +Subproject commit 161a36ee6232d8d251d798c8262638ed0c34c9c6 diff --git a/tests/ef_tests/src/case_result.rs b/tests/ef_tests/src/case_result.rs new file mode 100644 index 0000000000..7b7afd1bd2 --- /dev/null +++ b/tests/ef_tests/src/case_result.rs @@ -0,0 +1,52 @@ +use super::*; +use std::fmt::Debug; + +#[derive(Debug, PartialEq, Clone)] +pub struct CaseResult { + pub case_index: usize, + pub desc: String, + pub result: Result<(), Error>, +} + +impl CaseResult { + pub fn new(case_index: usize, case: &T, result: Result<(), Error>) -> Self { + CaseResult { + case_index, + desc: format!("{:?}", case), + result, + } + } +} + +/// Compares `result` with `expected`. +/// +/// If `expected.is_none()` then `result` is expected to be `Err`. Otherwise, `T` in `result` and +/// `expected` must be equal. +pub fn compare_result(result: &Result, expected: &Option) -> Result<(), Error> +where + T: PartialEq + Debug, + E: Debug, +{ + match (result, expected) { + // Pass: The should have failed and did fail. + (Err(_), None) => Ok(()), + // Fail: The test failed when it should have produced a result (fail). + (Err(e), Some(expected)) => Err(Error::NotEqual(format!( + "Got {:?} Expected {:?}", + e, expected + ))), + // Fail: The test produced a result when it should have failed (fail). + (Ok(result), None) => Err(Error::DidntFail(format!("Got {:?}", result))), + // Potential Pass: The test should have produced a result, and it did. + (Ok(result), Some(expected)) => { + if result == expected { + Ok(()) + } else { + Err(Error::NotEqual(format!( + "Got {:?} expected {:?}", + result, expected + ))) + } + } + } +} diff --git a/tests/ef_tests/src/cases.rs b/tests/ef_tests/src/cases.rs new file mode 100644 index 0000000000..f62150893c --- /dev/null +++ b/tests/ef_tests/src/cases.rs @@ -0,0 +1,59 @@ +use super::*; +use crate::yaml_decode::*; +use yaml_rust::YamlLoader; + +mod bls_aggregate_pubkeys; +mod bls_aggregate_sigs; +mod bls_g2_compressed; +mod bls_g2_uncompressed; +mod bls_priv_to_pub; +mod bls_sign_msg; +mod ssz_generic; +mod ssz_static; + +pub use bls_aggregate_pubkeys::*; +pub use bls_aggregate_sigs::*; +pub use bls_g2_compressed::*; +pub use bls_g2_uncompressed::*; +pub use bls_priv_to_pub::*; +pub use bls_sign_msg::*; +pub use ssz_generic::*; +pub use ssz_static::*; + +#[derive(Debug)] +pub struct Cases { + pub test_cases: Vec, +} + +impl YamlDecode for Cases { + /// Decodes a YAML list of test cases + fn yaml_decode(yaml: &String) -> Result { + let mut p = 0; + let mut elems: Vec<&str> = yaml + .match_indices("\n- ") + // Skip the `\n` used for matching a new line + .map(|(i, _)| i + 1) + .map(|i| { + let yaml_element = &yaml[p..i]; + p = i; + + yaml_element + }) + .collect(); + + elems.push(&yaml[p..]); + + let test_cases = elems + .iter() + .map(|s| { + // Remove the `- ` prefix. + let s = &s[2..]; + // Remove a single level of indenting. + s.replace("\n ", "\n") + }) + .map(|s| T::yaml_decode(&s.to_string()).unwrap()) + .collect(); + + Ok(Self { test_cases }) + } +} diff --git a/tests/ef_tests/src/cases/bls_aggregate_pubkeys.rs b/tests/ef_tests/src/cases/bls_aggregate_pubkeys.rs new file mode 100644 index 0000000000..8bbf1fc5ae --- /dev/null +++ b/tests/ef_tests/src/cases/bls_aggregate_pubkeys.rs @@ -0,0 +1,51 @@ +use super::*; +use crate::case_result::compare_result; +use bls::{AggregatePublicKey, PublicKey}; +use serde_derive::Deserialize; +use types::EthSpec; + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsAggregatePubkeys { + pub input: Vec, + pub output: String, +} + +impl YamlDecode for BlsAggregatePubkeys { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl EfTest for Cases { + fn test_results(&self) -> Vec { + self.test_cases + .iter() + .enumerate() + .map(|(i, tc)| { + let result = bls_add_pubkeys(&tc.input, &tc.output); + + CaseResult::new(i, tc, result) + }) + .collect() + } +} + +/// Execute a `aggregate_pubkeys` test case. +fn bls_add_pubkeys(inputs: &[String], output: &String) -> Result<(), Error> { + let mut aggregate_pubkey = AggregatePublicKey::new(); + + for key_str in inputs { + let key = + hex::decode(&key_str[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let key = PublicKey::from_bytes(&key) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + + aggregate_pubkey.add(&key); + } + + let output_bytes = + Some(hex::decode(&output[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?); + let aggregate_pubkey = Ok(aggregate_pubkey.as_raw().as_bytes()); + + compare_result::, Vec>(&aggregate_pubkey, &output_bytes) +} diff --git a/tests/ef_tests/src/cases/bls_aggregate_sigs.rs b/tests/ef_tests/src/cases/bls_aggregate_sigs.rs new file mode 100644 index 0000000000..1b8bede33b --- /dev/null +++ b/tests/ef_tests/src/cases/bls_aggregate_sigs.rs @@ -0,0 +1,51 @@ +use super::*; +use crate::case_result::compare_result; +use bls::{AggregateSignature, Signature}; +use serde_derive::Deserialize; +use types::EthSpec; + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsAggregateSigs { + pub input: Vec, + pub output: String, +} + +impl YamlDecode for BlsAggregateSigs { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl EfTest for Cases { + fn test_results(&self) -> Vec { + self.test_cases + .iter() + .enumerate() + .map(|(i, tc)| { + let result = bls_add_signatures(&tc.input, &tc.output); + + CaseResult::new(i, tc, result) + }) + .collect() + } +} + +/// Execute a `aggregate_sigs` test case. +fn bls_add_signatures(inputs: &[String], output: &String) -> Result<(), Error> { + let mut aggregate_signature = AggregateSignature::new(); + + for key_str in inputs { + let sig = + hex::decode(&key_str[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let sig = Signature::from_bytes(&sig) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + + aggregate_signature.add(&sig); + } + + let output_bytes = + Some(hex::decode(&output[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?); + let aggregate_signature = Ok(aggregate_signature.as_bytes()); + + compare_result::, Vec>(&aggregate_signature, &output_bytes) +} diff --git a/tests/ef_tests/src/cases/bls_g2_compressed.rs b/tests/ef_tests/src/cases/bls_g2_compressed.rs new file mode 100644 index 0000000000..95d36028f6 --- /dev/null +++ b/tests/ef_tests/src/cases/bls_g2_compressed.rs @@ -0,0 +1,71 @@ +use super::*; +use crate::case_result::compare_result; +use bls::{compress_g2, hash_on_g2}; +use serde_derive::Deserialize; +use types::EthSpec; + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsG2CompressedInput { + pub message: String, + pub domain: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsG2Compressed { + pub input: BlsG2CompressedInput, + pub output: Vec, +} + +impl YamlDecode for BlsG2Compressed { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl EfTest for Cases { + fn test_results(&self) -> Vec { + self.test_cases + .iter() + .enumerate() + .map(|(i, tc)| { + let result = compressed_hash(&tc.input.message, &tc.input.domain, &tc.output); + + CaseResult::new(i, tc, result) + }) + .collect() + } +} + +/// Execute a `compressed hash to g2` test case. +fn compressed_hash(message: &String, domain: &String, output: &Vec) -> Result<(), Error> { + // Convert message and domain to required types + let msg = + hex::decode(&message[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let d = hex::decode(&domain[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let d = bytes_to_u64(&d); + + // Calculate the point and convert it to compressed bytes + let mut point = hash_on_g2(&msg, d); + let point = compress_g2(&mut point); + + // Convert the output to one set of bytes + let mut decoded = + hex::decode(&output[0][2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let mut decoded_y = + hex::decode(&output[1][2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + decoded.append(&mut decoded_y); + + compare_result::, Vec>(&Ok(point), &Some(decoded)) +} + +// Converts a vector to u64 (from big endian) +fn bytes_to_u64(array: &Vec) -> u64 { + let mut result: u64 = 0; + for (i, value) in array.iter().rev().enumerate() { + if i == 8 { + break; + } + result += u64::pow(2, i as u32 * 8) * (*value as u64); + } + result +} diff --git a/tests/ef_tests/src/cases/bls_g2_uncompressed.rs b/tests/ef_tests/src/cases/bls_g2_uncompressed.rs new file mode 100644 index 0000000000..49c9c734f6 --- /dev/null +++ b/tests/ef_tests/src/cases/bls_g2_uncompressed.rs @@ -0,0 +1,85 @@ +use super::*; +use crate::case_result::compare_result; +use bls::hash_on_g2; +use serde_derive::Deserialize; +use types::EthSpec; + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsG2UncompressedInput { + pub message: String, + pub domain: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsG2Uncompressed { + pub input: BlsG2UncompressedInput, + pub output: Vec>, +} + +impl YamlDecode for BlsG2Uncompressed { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl EfTest for Cases { + fn test_results(&self) -> Vec { + self.test_cases + .iter() + .enumerate() + .map(|(i, tc)| { + let result = compressed_hash(&tc.input.message, &tc.input.domain, &tc.output); + + CaseResult::new(i, tc, result) + }) + .collect() + } +} + +/// Execute a `compressed hash to g2` test case. +fn compressed_hash( + message: &String, + domain: &String, + output: &Vec>, +) -> Result<(), Error> { + // Convert message and domain to required types + let msg = + hex::decode(&message[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let d = hex::decode(&domain[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let d = bytes_to_u64(&d); + + // Calculate the point and convert it to compressed bytes + let point = hash_on_g2(&msg, d); + let mut point_bytes = [0 as u8; 288]; + point.getpx().geta().tobytearray(&mut point_bytes, 0); + point.getpx().getb().tobytearray(&mut point_bytes, 48); + point.getpy().geta().tobytearray(&mut point_bytes, 96); + point.getpy().getb().tobytearray(&mut point_bytes, 144); + point.getpz().geta().tobytearray(&mut point_bytes, 192); + point.getpz().getb().tobytearray(&mut point_bytes, 240); + + // Convert the output to one set of bytes (x.a, x.b, y.a, y.b, z.a, z.b) + let mut decoded: Vec = vec![]; + for coordinate in output { + let mut decoded_part = hex::decode(&coordinate[0][2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + decoded.append(&mut decoded_part); + decoded_part = hex::decode(&coordinate[1][2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + decoded.append(&mut decoded_part); + } + + compare_result::, Vec>(&Ok(point_bytes.to_vec()), &Some(decoded)) +} + +// Converts a vector to u64 (from big endian) +fn bytes_to_u64(array: &Vec) -> u64 { + let mut result: u64 = 0; + for (i, value) in array.iter().rev().enumerate() { + if i == 8 { + break; + } + result += u64::pow(2, i as u32 * 8) * (*value as u64); + } + result +} diff --git a/tests/ef_tests/src/cases/bls_priv_to_pub.rs b/tests/ef_tests/src/cases/bls_priv_to_pub.rs new file mode 100644 index 0000000000..b5b4fc997c --- /dev/null +++ b/tests/ef_tests/src/cases/bls_priv_to_pub.rs @@ -0,0 +1,53 @@ +use super::*; +use crate::case_result::compare_result; +use bls::{PublicKey, SecretKey}; +use serde_derive::Deserialize; +use types::EthSpec; + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsPrivToPub { + pub input: String, + pub output: String, +} + +impl YamlDecode for BlsPrivToPub { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl EfTest for Cases { + fn test_results(&self) -> Vec { + self.test_cases + .iter() + .enumerate() + .map(|(i, tc)| { + let result = secret_to_public(&tc.input, &tc.output); + + CaseResult::new(i, tc, result) + }) + .collect() + } +} + +/// Execute a `Private key to public key` test case. +fn secret_to_public(secret: &String, output: &String) -> Result<(), Error> { + // Convert message and domain to required types + let mut sk = + hex::decode(&secret[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + pad_to_48(&mut sk); + let sk = SecretKey::from_bytes(&sk).unwrap(); + let pk = PublicKey::from_secret_key(&sk); + + let decoded = + hex::decode(&output[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + + compare_result::, Vec>(&Ok(pk.as_raw().as_bytes()), &Some(decoded)) +} + +// Increase the size of an array to 48 bytes +fn pad_to_48(array: &mut Vec) { + while array.len() < 48 { + array.insert(0, 0); + } +} diff --git a/tests/ef_tests/src/cases/bls_sign_msg.rs b/tests/ef_tests/src/cases/bls_sign_msg.rs new file mode 100644 index 0000000000..c624313654 --- /dev/null +++ b/tests/ef_tests/src/cases/bls_sign_msg.rs @@ -0,0 +1,88 @@ +use super::*; +use crate::case_result::compare_result; +use bls::{SecretKey, Signature}; +use serde_derive::Deserialize; +use types::EthSpec; + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsSignInput { + pub privkey: String, + pub message: String, + pub domain: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsSign { + pub input: BlsSignInput, + pub output: String, +} + +impl YamlDecode for BlsSign { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl EfTest for Cases { + fn test_results(&self) -> Vec { + self.test_cases + .iter() + .enumerate() + .map(|(i, tc)| { + let result = sign_msg( + &tc.input.privkey, + &tc.input.message, + &tc.input.domain, + &tc.output, + ); + + CaseResult::new(i, tc, result) + }) + .collect() + } +} + +/// Execute a `compressed hash to g2` test case. +fn sign_msg( + private_key: &String, + message: &String, + domain: &String, + output: &String, +) -> Result<(), Error> { + // Convert private_key, message and domain to required types + let mut sk = + hex::decode(&private_key[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + pad_to_48(&mut sk); + let sk = SecretKey::from_bytes(&sk).unwrap(); + let msg = + hex::decode(&message[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let d = hex::decode(&domain[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let d = bytes_to_u64(&d); + + let signature = Signature::new(&msg, d, &sk); + + // Convert the output to one set of bytes + let decoded = + hex::decode(&output[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + + compare_result::, Vec>(&Ok(signature.as_bytes()), &Some(decoded)) +} + +// Converts a vector to u64 (from big endian) +fn bytes_to_u64(array: &Vec) -> u64 { + let mut result: u64 = 0; + for (i, value) in array.iter().rev().enumerate() { + if i == 8 { + break; + } + result += u64::pow(2, i as u32 * 8) * (*value as u64); + } + result +} + +// Increase the size of an array to 48 bytes +fn pad_to_48(array: &mut Vec) { + while array.len() < 48 { + array.insert(0, 0); + } +} diff --git a/tests/ef_tests/src/cases/ssz_generic.rs b/tests/ef_tests/src/cases/ssz_generic.rs new file mode 100644 index 0000000000..9da2162b34 --- /dev/null +++ b/tests/ef_tests/src/cases/ssz_generic.rs @@ -0,0 +1,81 @@ +use super::*; +use crate::case_result::compare_result; +use ethereum_types::{U128, U256}; +use serde_derive::Deserialize; +use ssz::Decode; +use std::fmt::Debug; +use types::EthSpec; + +#[derive(Debug, Clone, Deserialize)] +pub struct SszGeneric { + #[serde(alias = "type")] + pub type_name: String, + pub valid: bool, + pub value: Option, + pub ssz: Option, +} + +impl YamlDecode for SszGeneric { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl EfTest for Cases { + fn test_results(&self) -> Vec { + self.test_cases + .iter() + .enumerate() + .map(|(i, tc)| { + let result = if let Some(ssz) = &tc.ssz { + match tc.type_name.as_ref() { + "uint8" => ssz_generic_test::(tc.valid, ssz, &tc.value), + "uint16" => ssz_generic_test::(tc.valid, ssz, &tc.value), + "uint32" => ssz_generic_test::(tc.valid, ssz, &tc.value), + "uint64" => ssz_generic_test::(tc.valid, ssz, &tc.value), + "uint128" => ssz_generic_test::(tc.valid, ssz, &tc.value), + "uint256" => ssz_generic_test::(tc.valid, ssz, &tc.value), + _ => Err(Error::FailedToParseTest(format!( + "Unknown type: {}", + tc.type_name + ))), + } + } else { + // Skip tests that do not have an ssz field. + // + // See: https://github.com/ethereum/eth2.0-specs/issues/1079 + Ok(()) + }; + + CaseResult::new(i, tc, result) + }) + .collect() + } +} + +/// Execute a `ssz_generic` test case. +fn ssz_generic_test( + should_be_ok: bool, + ssz: &String, + value: &Option, +) -> Result<(), Error> +where + T: Decode + YamlDecode + Debug + PartialEq, +{ + let ssz = hex::decode(&ssz[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + + // We do not cater for the scenario where the test is valid but we are not passed any SSZ. + if should_be_ok && value.is_none() { + panic!("Unexpected test input. Cannot pass without value.") + } + + let expected = if let Some(string) = value { + Some(T::yaml_decode(string)?) + } else { + None + }; + + let decoded = T::from_ssz_bytes(&ssz); + + compare_result(&decoded, &expected) +} diff --git a/tests/ef_tests/src/cases/ssz_static.rs b/tests/ef_tests/src/cases/ssz_static.rs new file mode 100644 index 0000000000..0eacfcceef --- /dev/null +++ b/tests/ef_tests/src/cases/ssz_static.rs @@ -0,0 +1,137 @@ +use super::*; +use crate::case_result::compare_result; +use cached_tree_hash::{CachedTreeHash, TreeHashCache}; +use rayon::prelude::*; +use serde_derive::Deserialize; +use ssz::{Decode, Encode, ssz_encode}; +use std::fmt::Debug; +use tree_hash::TreeHash; +use types::{ + test_utils::{SeedableRng, TestRandom, XorShiftRng}, + Attestation, AttestationData, AttestationDataAndCustodyBit, AttesterSlashing, BeaconBlock, + BeaconBlockBody, BeaconBlockHeader, BeaconState, Crosslink, Deposit, DepositData, Eth1Data, + EthSpec, Fork, Hash256, HistoricalBatch, IndexedAttestation, PendingAttestation, + ProposerSlashing, Transfer, Validator, VoluntaryExit, +}; + +#[derive(Debug, Clone, Deserialize)] +pub struct SszStatic { + pub type_name: String, + pub serialized: String, + pub root: String, + #[serde(skip)] + pub raw_yaml: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Value { + value: T, +} + +impl YamlDecode for SszStatic { + fn yaml_decode(yaml: &String) -> Result { + let mut ssz_static: SszStatic = serde_yaml::from_str(&yaml.as_str()).unwrap(); + + ssz_static.raw_yaml = yaml.clone(); + + Ok(ssz_static) + } +} + +impl SszStatic { + fn value(&self) -> Result { + let wrapper: Value = serde_yaml::from_str(&self.raw_yaml.as_str()).map_err(|e| { + Error::FailedToParseTest(format!("Unable to parse {} YAML: {:?}", self.type_name, e)) + })?; + + Ok(wrapper.value) + } +} + +impl EfTest for Cases { + fn test_results(&self) -> Vec { + self.test_cases + .par_iter() + .enumerate() + .map(|(i, tc)| { + let result = match tc.type_name.as_ref() { + "Fork" => ssz_static_test::(tc), + "Crosslink" => ssz_static_test::(tc), + "Eth1Data" => ssz_static_test::(tc), + "AttestationData" => ssz_static_test::(tc), + "AttestationDataAndCustodyBit" => { + ssz_static_test::(tc) + } + "IndexedAttestation" => ssz_static_test::(tc), + "DepositData" => ssz_static_test::(tc), + "BeaconBlockHeader" => ssz_static_test::(tc), + "Validator" => ssz_static_test::(tc), + "PendingAttestation" => ssz_static_test::(tc), + "HistoricalBatch" => ssz_static_test::>(tc), + "ProposerSlashing" => ssz_static_test::(tc), + "AttesterSlashing" => ssz_static_test::(tc), + "Attestation" => ssz_static_test::(tc), + "Deposit" => ssz_static_test::(tc), + "VoluntaryExit" => ssz_static_test::(tc), + "Transfer" => ssz_static_test::(tc), + "BeaconBlockBody" => ssz_static_test::(tc), + "BeaconBlock" => ssz_static_test::(tc), + "BeaconState" => ssz_static_test::>(tc), + _ => Err(Error::FailedToParseTest(format!( + "Unknown type: {}", + tc.type_name + ))), + }; + + CaseResult::new(i, tc, result) + }) + .collect() + } +} + +fn ssz_static_test(tc: &SszStatic) -> Result<(), Error> +where + T: Clone + + Decode + + Debug + + Encode + + PartialEq + + serde::de::DeserializeOwned + + TreeHash + + CachedTreeHash + + TestRandom, +{ + // Verify we can decode SSZ in the same way we can decode YAML. + let ssz = hex::decode(&tc.serialized[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let expected = tc.value::()?; + let decode_result = T::from_ssz_bytes(&ssz); + compare_result(&decode_result, &Some(expected))?; + + // Verify we can encode the result back into original ssz bytes + let decoded = decode_result.unwrap(); + let encoded_result = decoded.as_ssz_bytes(); + compare_result::, Error>(&Ok(encoded_result), &Some(ssz)); + + // Verify the TreeHash root of the decoded struct matches the test. + let expected_root = + &hex::decode(&tc.root[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let expected_root = Hash256::from_slice(&expected_root); + let tree_hash_root = Hash256::from_slice(&decoded.tree_hash_root()); + compare_result::(&Ok(tree_hash_root), &Some(expected_root))?; + + // Verify a _new_ CachedTreeHash root of the decoded struct matches the test. + let cache = TreeHashCache::new(&decoded).unwrap(); + let cached_tree_hash_root = Hash256::from_slice(cache.tree_hash_root().unwrap()); + compare_result::(&Ok(cached_tree_hash_root), &Some(expected_root))?; + + // Verify the root after an update from a random CachedTreeHash to the decoded struct. + let mut rng = XorShiftRng::from_seed([42; 16]); + let random_instance = T::random_for_test(&mut rng); + let mut cache = TreeHashCache::new(&random_instance).unwrap(); + cache.update(&decoded).unwrap(); + let updated_root = Hash256::from_slice(cache.tree_hash_root().unwrap()); + compare_result::(&Ok(updated_root), &Some(expected_root))?; + + Ok(()) +} diff --git a/tests/ef_tests/src/doc.rs b/tests/ef_tests/src/doc.rs new file mode 100644 index 0000000000..d10f8e9357 --- /dev/null +++ b/tests/ef_tests/src/doc.rs @@ -0,0 +1,117 @@ +use crate::case_result::CaseResult; +use crate::cases::*; +use crate::doc_header::DocHeader; +use crate::eth_specs::{MainnetEthSpec, MinimalEthSpec}; +use crate::yaml_decode::{extract_yaml_by_key, yaml_split_header_and_cases, YamlDecode}; +use crate::EfTest; +use serde_derive::Deserialize; +use std::{fs::File, io::prelude::*, path::PathBuf}; +use types::EthSpec; + +#[derive(Debug, Deserialize)] +pub struct Doc { + pub header_yaml: String, + pub cases_yaml: String, + pub path: PathBuf, +} + +impl Doc { + fn from_path(path: PathBuf) -> Self { + let mut file = File::open(path.clone()).unwrap(); + + let mut yaml = String::new(); + file.read_to_string(&mut yaml).unwrap(); + + let (header_yaml, cases_yaml) = yaml_split_header_and_cases(yaml.clone()); + + Self { + header_yaml, + cases_yaml, + path, + } + } + + pub fn test_results(&self) -> Vec { + let header: DocHeader = serde_yaml::from_str(&self.header_yaml.as_str()).unwrap(); + + match ( + header.runner.as_ref(), + header.handler.as_ref(), + header.config.as_ref(), + ) { + ("ssz", "uint", _) => run_test::(self), + ("ssz", "static", "minimal") => run_test::(self), + ("ssz", "static", "mainnet") => run_test::(self), + ("bls", "aggregate_pubkeys", "mainnet") => { + run_test::(self) + } + ("bls", "aggregate_sigs", "mainnet") => { + run_test::(self) + } + ("bls", "msg_hash_compressed", "mainnet") => { + run_test::(self) + } + // Note this test fails due to a difference in our internal representations. It does + // not effect verification or external representation. + // + // It is skipped. + ("bls", "msg_hash_uncompressed", "mainnet") => vec![], + ("bls", "priv_to_pub", "mainnet") => run_test::(self), + ("bls", "sign_msg", "mainnet") => run_test::(self), + (runner, handler, config) => panic!( + "No implementation for runner: \"{}\", handler: \"{}\", config: \"{}\"", + runner, handler, config + ), + } + } + + pub fn assert_tests_pass(path: PathBuf) { + let doc = Self::from_path(path); + let results = doc.test_results(); + + if results.iter().any(|r| r.result.is_err()) { + print_failures(&doc, &results); + panic!("Tests failed (see above)"); + } else { + println!("Passed {} tests in {:?}", results.len(), doc.path); + } + } +} + +pub fn run_test(doc: &Doc) -> Vec +where + Cases: EfTest + YamlDecode, +{ + // Extract only the "test_cases" YAML as a stand-alone string. + //let test_cases_yaml = extract_yaml_by_key(self., "test_cases"); + + // Pass only the "test_cases" YAML string to `yaml_decode`. + let test_cases: Cases = Cases::yaml_decode(&doc.cases_yaml).unwrap(); + + test_cases.test_results::() +} + +pub fn print_failures(doc: &Doc, results: &[CaseResult]) { + let header: DocHeader = serde_yaml::from_str(&doc.header_yaml).unwrap(); + let failures: Vec<&CaseResult> = results.iter().filter(|r| r.result.is_err()).collect(); + + println!("--------------------------------------------------"); + println!("Test Failure"); + println!("Title: {}", header.title); + println!("File: {:?}", doc.path); + println!(""); + println!( + "{} tests, {} failures, {} passes.", + results.len(), + failures.len(), + results.len() - failures.len() + ); + println!(""); + + for failure in failures { + println!("-------"); + println!("case[{}].result:", failure.case_index); + println!("{:#?}", failure.result); + } + println!(""); +} diff --git a/tests/ef_tests/src/doc_header.rs b/tests/ef_tests/src/doc_header.rs new file mode 100644 index 0000000000..c0d6d3276f --- /dev/null +++ b/tests/ef_tests/src/doc_header.rs @@ -0,0 +1,12 @@ +use serde_derive::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct DocHeader { + pub title: String, + pub summary: String, + pub forks_timeline: String, + pub forks: Vec, + pub config: String, + pub runner: String, + pub handler: String, +} diff --git a/tests/ef_tests/src/error.rs b/tests/ef_tests/src/error.rs new file mode 100644 index 0000000000..58732e83ea --- /dev/null +++ b/tests/ef_tests/src/error.rs @@ -0,0 +1,9 @@ +#[derive(Debug, PartialEq, Clone)] +pub enum Error { + /// The value in the test didn't match our value. + NotEqual(String), + /// The test specified a failure and we did not experience one. + DidntFail(String), + /// Failed to parse the test (internal error). + FailedToParseTest(String), +} diff --git a/tests/ef_tests/src/eth_specs.rs b/tests/ef_tests/src/eth_specs.rs new file mode 100644 index 0000000000..b2d46d8bcb --- /dev/null +++ b/tests/ef_tests/src/eth_specs.rs @@ -0,0 +1,28 @@ +use serde_derive::{Deserialize, Serialize}; +use types::{ + typenum::{U64, U8}, + ChainSpec, EthSpec, FewValidatorsEthSpec, FoundationEthSpec, +}; + +/// "Minimal" testing specification, as defined here: +/// +/// https://github.com/ethereum/eth2.0-specs/blob/v0.6.1/configs/constant_presets/minimal.yaml +/// +/// Spec v0.6.1 +#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)] +pub struct MinimalEthSpec; + +impl EthSpec for MinimalEthSpec { + type ShardCount = U8; + type SlotsPerHistoricalRoot = U64; + type LatestRandaoMixesLength = U64; + type LatestActiveIndexRootsLength = U64; + type LatestSlashedExitLength = U64; + + fn spec() -> ChainSpec { + // TODO: this spec is likely incorrect! + FewValidatorsEthSpec::spec() + } +} + +pub type MainnetEthSpec = FoundationEthSpec; diff --git a/tests/ef_tests/src/lib.rs b/tests/ef_tests/src/lib.rs new file mode 100644 index 0000000000..580d965663 --- /dev/null +++ b/tests/ef_tests/src/lib.rs @@ -0,0 +1,21 @@ +use types::EthSpec; + +pub use case_result::CaseResult; +pub use doc::Doc; +pub use error::Error; +pub use yaml_decode::YamlDecode; + +mod case_result; +mod cases; +mod doc; +mod doc_header; +mod error; +mod eth_specs; +mod yaml_decode; + +/// Defined where an object can return the results of some test(s) adhering to the Ethereum +/// Foundation testing format. +pub trait EfTest { + /// Returns the results of executing one or more tests. + fn test_results(&self) -> Vec; +} diff --git a/tests/ef_tests/src/yaml_decode.rs b/tests/ef_tests/src/yaml_decode.rs new file mode 100644 index 0000000000..974df8311a --- /dev/null +++ b/tests/ef_tests/src/yaml_decode.rs @@ -0,0 +1,59 @@ +use super::*; +use ethereum_types::{U128, U256}; +use types::Fork; + +mod utils; + +pub use utils::*; + +pub trait YamlDecode: Sized { + /// Decode an object from the test specification YAML. + fn yaml_decode(string: &String) -> Result; +} + +/// Basic types can general be decoded with the `parse` fn if they implement `str::FromStr`. +macro_rules! impl_via_parse { + ($ty: ty) => { + impl YamlDecode for $ty { + fn yaml_decode(string: &String) -> Result { + string + .parse::() + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e))) + } + } + }; +} + +impl_via_parse!(u8); +impl_via_parse!(u16); +impl_via_parse!(u32); +impl_via_parse!(u64); + +/// Some `ethereum-types` methods have a `str::FromStr` implementation that expects `0x`-prefixed: +/// hex, so we use `from_dec_str` instead. +macro_rules! impl_via_from_dec_str { + ($ty: ty) => { + impl YamlDecode for $ty { + fn yaml_decode(string: &String) -> Result { + Self::from_dec_str(string).map_err(|e| Error::FailedToParseTest(format!("{:?}", e))) + } + } + }; +} + +impl_via_from_dec_str!(U128); +impl_via_from_dec_str!(U256); + +/// Types that already implement `serde::Deserialize` can be decoded using `serde_yaml`. +macro_rules! impl_via_serde_yaml { + ($ty: ty) => { + impl YamlDecode for $ty { + fn yaml_decode(string: &String) -> Result { + serde_yaml::from_str(string) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e))) + } + } + }; +} + +impl_via_serde_yaml!(Fork); diff --git a/tests/ef_tests/src/yaml_decode/utils.rs b/tests/ef_tests/src/yaml_decode/utils.rs new file mode 100644 index 0000000000..9b7d7ef53e --- /dev/null +++ b/tests/ef_tests/src/yaml_decode/utils.rs @@ -0,0 +1,35 @@ +use yaml_rust::{Yaml, YamlEmitter, YamlLoader}; + +pub fn extract_yaml_by_key(yaml: &str, key: &str) -> String { + let doc = &YamlLoader::load_from_str(yaml).unwrap()[0]; + let subsection = &doc[key]; + + yaml_to_string(subsection) +} + +pub fn extract_yaml_by_index(yaml: &str, index: usize) -> String { + let doc = &YamlLoader::load_from_str(yaml).unwrap()[0]; + let subsection = &doc[index]; + + yaml_to_string(subsection) +} + +pub fn yaml_to_string(yaml: &Yaml) -> String { + let mut out_str = String::new(); + let mut emitter = YamlEmitter::new(&mut out_str); + emitter.escape_all_strings(true); + emitter.dump(yaml).unwrap(); + + out_str +} + +pub fn yaml_split_header_and_cases(mut yaml: String) -> (String, String) { + let test_cases_start = yaml.find("\ntest_cases:\n").unwrap(); + // + 1 to skip the \n we used for matching. + let mut test_cases = yaml.split_off(test_cases_start + 1); + + let end_of_first_line = test_cases.find("\n").unwrap(); + let test_cases = test_cases.split_off(end_of_first_line + 1); + + (yaml, test_cases) +} diff --git a/tests/ef_tests/tests/tests.rs b/tests/ef_tests/tests/tests.rs new file mode 100644 index 0000000000..58aadf4736 --- /dev/null +++ b/tests/ef_tests/tests/tests.rs @@ -0,0 +1,62 @@ +use ef_tests::*; +use rayon::prelude::*; +use std::path::PathBuf; +use walkdir::WalkDir; + +fn yaml_files_in_test_dir(dir: &str) -> Vec { + let mut base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + base_path.push("eth2.0-spec-tests"); + base_path.push("tests"); + base_path.push(dir); + + assert!( + base_path.exists(), + "Unable to locate test files. Did you init git submoules?" + ); + + WalkDir::new(base_path) + .into_iter() + .filter_map(|e| e.ok()) + .filter_map(|entry| { + if entry.file_type().is_file() { + match entry.file_name().to_str() { + Some(f) if f.ends_with(".yaml") => Some(entry.path().to_path_buf()), + Some(f) if f.ends_with(".yml") => Some(entry.path().to_path_buf()), + _ => None, + } + } else { + None + } + }) + .collect() +} + +#[test] +#[cfg(feature = "fake_crypto")] +fn ssz_generic() { + yaml_files_in_test_dir("ssz_generic") + .into_par_iter() + .for_each(|file| { + Doc::assert_tests_pass(file); + }); +} + +#[test] +#[cfg(feature = "fake_crypto")] +fn ssz_static() { + yaml_files_in_test_dir("ssz_static") + .into_par_iter() + .for_each(|file| { + Doc::assert_tests_pass(file); + }); +} + +#[test] +#[cfg(not(feature = "fake_crypto"))] +fn bls() { + yaml_files_in_test_dir("bls") + .into_par_iter() + .for_each(|file| { + Doc::assert_tests_pass(file); + }); +}