From 117a207d495244377e56f2c2fd308726ed7ef1bd Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 2 Oct 2018 17:35:03 +1000 Subject: [PATCH] Add pre-written validation code This adds block and attestation validation code that was written previously. There were many non-validation specific changes made whilst building these functions (e.g., db, hashing, etc) -- these changes have already been merged into master and this branch has been created just to make it easy to review this code. --- Cargo.toml | 1 + beacon_chain/validation/Cargo.toml | 13 + .../validation/benches/block_validation.rs | 148 +++++++ beacon_chain/validation/benches/main.rs | 5 + .../src/attestation_parent_hashes.rs | 226 +++++++++++ .../validation/src/attestation_validation.rs | 210 ++++++++++ .../validation/src/block_validation.rs | 370 ++++++++++++++++++ beacon_chain/validation/src/lib.rs | 12 + .../validation/src/message_generation.rs | 70 ++++ .../validation/src/mod_attestation.rs | 16 + beacon_chain/validation/src/mod_block.rs | 21 + .../validation/src/signature_verification.rs | 183 +++++++++ .../tests/attestation_validation/helpers.rs | 192 +++++++++ .../tests/attestation_validation/mod.rs | 9 + .../tests/attestation_validation/tests.rs | 127 ++++++ .../tests/block_validation/helpers.rs | 226 +++++++++++ .../validation/tests/block_validation/mod.rs | 12 + .../tests/block_validation/tests.rs | 243 ++++++++++++ beacon_chain/validation/tests/main.rs | 20 + 19 files changed, 2104 insertions(+) create mode 100644 beacon_chain/validation/Cargo.toml create mode 100644 beacon_chain/validation/benches/block_validation.rs create mode 100644 beacon_chain/validation/benches/main.rs create mode 100644 beacon_chain/validation/src/attestation_parent_hashes.rs create mode 100644 beacon_chain/validation/src/attestation_validation.rs create mode 100644 beacon_chain/validation/src/block_validation.rs create mode 100644 beacon_chain/validation/src/lib.rs create mode 100644 beacon_chain/validation/src/message_generation.rs create mode 100644 beacon_chain/validation/src/mod_attestation.rs create mode 100644 beacon_chain/validation/src/mod_block.rs create mode 100644 beacon_chain/validation/src/signature_verification.rs create mode 100644 beacon_chain/validation/tests/attestation_validation/helpers.rs create mode 100644 beacon_chain/validation/tests/attestation_validation/mod.rs create mode 100644 beacon_chain/validation/tests/attestation_validation/tests.rs create mode 100644 beacon_chain/validation/tests/block_validation/helpers.rs create mode 100644 beacon_chain/validation/tests/block_validation/mod.rs create mode 100644 beacon_chain/validation/tests/block_validation/tests.rs create mode 100644 beacon_chain/validation/tests/main.rs diff --git a/Cargo.toml b/Cargo.toml index d9f9d14bc8..2d9f7d0925 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,5 +39,6 @@ members = [ "beacon_chain/utils/hashing", "beacon_chain/utils/ssz", "beacon_chain/utils/ssz_helpers", + "beacon_chain/validation", "lighthouse/db", ] diff --git a/beacon_chain/validation/Cargo.toml b/beacon_chain/validation/Cargo.toml new file mode 100644 index 0000000000..a202fb5ce5 --- /dev/null +++ b/beacon_chain/validation/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "validation" +version = "0.1.0" +authors = ["Paul Hauner "] + +[dependencies] +bls = { path = "../utils/bls" } +db = { path = "../../lighthouse/db" } +hashing = { path = "../utils/hashing" } +rayon = "1.0.2" +ssz = { path = "../utils/ssz" } +ssz_helpers = { path = "../utils/ssz_helpers" } +types = { path = "../types" } diff --git a/beacon_chain/validation/benches/block_validation.rs b/beacon_chain/validation/benches/block_validation.rs new file mode 100644 index 0000000000..03e9df4984 --- /dev/null +++ b/beacon_chain/validation/benches/block_validation.rs @@ -0,0 +1,148 @@ +#![feature(test)] + +extern crate test; + +use self::test::Bencher; + +use std::sync::Arc; + +use super::{ + BlockValidationContext, + AttesterMap, + ProposerMap, +}; + +use super::tests::{ + TestStore, + TestParams, + setup_block_validation_scenario, + serialize_block, +}; + +use super::super::{ + Block, + SszBlock, +}; + +fn bench_block_validation_scenario( + b: &mut Bencher, + params: &TestParams, + mutator_func: F) + where F: FnOnce(Block, AttesterMap, ProposerMap, TestStore) + -> (Block, AttesterMap, ProposerMap, TestStore) +{ + let (block, + parent_hashes, + attester_map, + proposer_map, + stores) = setup_block_validation_scenario(¶ms); + + let (block, + attester_map, + proposer_map, + stores) = mutator_func(block, attester_map, proposer_map, stores); + + let ssz_bytes = serialize_block(&block); + let ssz_block = SszBlock::from_slice(&ssz_bytes[..]) + .unwrap(); + + let parent_hashes = Arc::new(parent_hashes); + let proposer_map = Arc::new(proposer_map); + let attester_map = Arc::new(attester_map); + b.iter(|| { + let context = BlockValidationContext { + present_slot: params.validation_context_slot, + cycle_length: params.cycle_length, + last_justified_slot: params.validation_context_justified_slot, + last_finalized_slot: params.validation_context_finalized_slot, + parent_hashes: parent_hashes.clone(), + proposer_map: proposer_map.clone(), + attester_map: attester_map.clone(), + block_store: stores.block.clone(), + validator_store: stores.validator.clone(), + pow_store: stores.pow_chain.clone() + }; + let result = context.validate_ssz_block(&ssz_block); + assert!(result.is_ok()); + }); +} + +#[bench] +#[ignore] +fn bench_block_validation_10m_eth(b: &mut Bencher) { + let total_validators: usize = 10_000_000 / 32; + let cycle_length: u8 = 64; + let shard_count: u16 = 1024; + let shards_per_slot: u16 = 1024 / u16::from(cycle_length); + let validators_per_shard: usize = total_validators / usize::from(shard_count); + let block_slot = u64::from(cycle_length) * 10000; + let attestations_justified_slot = block_slot - u64::from(cycle_length); + let parent_proposer_index = 0; + + let validation_context_slot = block_slot; + let validation_context_justified_slot = attestations_justified_slot; + let validation_context_finalized_slot = 0; + + let params = TestParams { + total_validators, + cycle_length, + shard_count, + shards_per_slot, + validators_per_shard, + parent_proposer_index, + block_slot, + attestations_justified_slot, + validation_context_slot, + validation_context_justified_slot, + validation_context_finalized_slot, + }; + + let no_mutate = |block, attester_map, proposer_map, stores| { + (block, attester_map, proposer_map, stores) + }; + + bench_block_validation_scenario( + b, + ¶ms, + no_mutate); +} + +#[bench] +#[ignore] +fn bench_block_validation_100m_eth(b: &mut Bencher) { + let total_validators: usize = 100_000_000 / 32; + let cycle_length: u8 = 64; + let shard_count: u16 = 1024; + let shards_per_slot: u16 = 1024 / u16::from(cycle_length); + let validators_per_shard: usize = total_validators / usize::from(shard_count); + let block_slot = u64::from(cycle_length) * 10000; + let attestations_justified_slot = block_slot - u64::from(cycle_length); + let parent_proposer_index = 0; + + let validation_context_slot = block_slot; + let validation_context_justified_slot = attestations_justified_slot; + let validation_context_finalized_slot = 0; + + let params = TestParams { + total_validators, + cycle_length, + shard_count, + shards_per_slot, + validators_per_shard, + parent_proposer_index, + block_slot, + attestations_justified_slot, + validation_context_slot, + validation_context_justified_slot, + validation_context_finalized_slot, + }; + + let no_mutate = |block, attester_map, proposer_map, stores| { + (block, attester_map, proposer_map, stores) + }; + + bench_block_validation_scenario( + b, + ¶ms, + no_mutate); +} diff --git a/beacon_chain/validation/benches/main.rs b/beacon_chain/validation/benches/main.rs new file mode 100644 index 0000000000..0faf89f6d8 --- /dev/null +++ b/beacon_chain/validation/benches/main.rs @@ -0,0 +1,5 @@ +#![feature(test)] +extern crate lighthouse; +extern crate tests; + +mod block_validation; diff --git a/beacon_chain/validation/src/attestation_parent_hashes.rs b/beacon_chain/validation/src/attestation_parent_hashes.rs new file mode 100644 index 0000000000..20bcf6ce2f --- /dev/null +++ b/beacon_chain/validation/src/attestation_parent_hashes.rs @@ -0,0 +1,226 @@ +use super::types::Hash256; + +#[derive(Debug)] +pub enum ParentHashesError { + BadCurrentHashes, + BadObliqueHashes, + SlotTooHigh, + SlotTooLow, + IntWrapping, +} + +/// This function is used to select the hashes used in +/// the signing of an AttestationRecord. +/// +/// It either returns Result with a vector of length `cycle_length,` or +/// returns an Error. +/// +/// This function corresponds to the `get_signed_parent_hashes` function +/// in the Python reference implentation. +/// +/// See this slide for more information: +/// https://tinyurl.com/ybzn2spw +pub fn attestation_parent_hashes( + cycle_length: u8, + block_slot: u64, + attestation_slot: u64, + current_hashes: &[Hash256], + oblique_hashes: &[Hash256]) + -> Result, ParentHashesError> +{ + // This cast places a limit on cycle_length. If you change it, check math + // for overflow. + let cycle_length: u64 = u64::from(cycle_length); + + if current_hashes.len() as u64 != (cycle_length * 2) { + return Err(ParentHashesError::BadCurrentHashes); + } + if oblique_hashes.len() as u64 > cycle_length { + return Err(ParentHashesError::BadObliqueHashes); + } + if attestation_slot >= block_slot { + return Err(ParentHashesError::SlotTooHigh); + } + + /* + * Cannot underflow as block_slot cannot be less + * than attestation_slot. + */ + let attestation_distance = block_slot - attestation_slot; + + if attestation_distance > cycle_length { + return Err(ParentHashesError::SlotTooLow); + } + + /* + * Cannot underflow because attestation_distance cannot + * be larger than cycle_length. + */ + let start = cycle_length - attestation_distance; + + /* + * Overflow is potentially impossible, but proof is complicated + * enough to just use checked math. + * + * Arithmetic is: + * start + cycle_length - oblique_hashes.len() + */ + let end = start.checked_add(cycle_length) + .and_then(|x| x.checked_sub(oblique_hashes.len() as u64)) + .ok_or(ParentHashesError::IntWrapping)?; + + + let mut hashes = Vec::new(); + hashes.extend_from_slice( + ¤t_hashes[(start as usize)..(end as usize)]); + hashes.extend_from_slice(oblique_hashes); + + Ok(hashes) +} + + +#[cfg(test)] +mod tests { + use super::*; + + fn get_range_of_hashes(from: usize, to: usize) -> Vec { + (from..to).map(|i| get_hash(&vec![i as u8])).collect() + } + + fn get_hash(value: &[u8]) -> Hash256 { + Hash256::from_slice(value) + } + + #[test] + fn test_get_signed_hashes_oblique_scenario_1() { + /* + * Two oblique hashes. + */ + let cycle_length: u8 = 8; + let block_slot: u64 = 19; + let attestation_slot: u64 = 15; + let current_hashes = get_range_of_hashes(3, 19); + let oblique_hashes = get_range_of_hashes(100, 102); + let result = attestation_parent_hashes( + cycle_length, + block_slot, + attestation_slot, + ¤t_hashes, + &oblique_hashes); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(result.len(), cycle_length as usize); + + let mut expected_result = get_range_of_hashes(7, 13); + expected_result.append(&mut get_range_of_hashes(100, 102)); + assert_eq!(result, expected_result); + } + + #[test] + fn test_get_signed_hashes_oblique_scenario_2() { + /* + * All oblique hashes. + */ + let cycle_length: u8 = 8; + let block_slot: u64 = 19; + let attestation_slot: u64 = 15; + let current_hashes = get_range_of_hashes(3, 19); + let oblique_hashes = get_range_of_hashes(100, 108); + let result = attestation_parent_hashes( + cycle_length, + block_slot, + attestation_slot, + ¤t_hashes, + &oblique_hashes); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(result.len(), cycle_length as usize); + + let expected_result = get_range_of_hashes(100, 108); + assert_eq!(result, expected_result); + } + + #[test] + fn test_get_signed_hashes_scenario_1() { + /* + * Google Slides example. + * https://tinyurl.com/ybzn2spw + */ + let cycle_length: u8 = 8; + let block_slot: u64 = 19; + let attestation_slot: u64 = 15; + let current_hashes = get_range_of_hashes(3, 19); + let oblique_hashes = vec![]; + let result = attestation_parent_hashes( + cycle_length, + block_slot, + attestation_slot, + ¤t_hashes, + &oblique_hashes); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(result.len(), cycle_length as usize); + let expected_result = get_range_of_hashes(7, 15); + assert_eq!(result, expected_result); + } + + #[test] + fn test_get_signed_hashes_scenario_2() { + /* + * Block 1, attestation 0. + */ + let cycle_length: u8 = 8; + let block_slot: u64 = 1; + let attestation_slot: u64 = 0; + let current_hashes = get_range_of_hashes(0, 16); + let oblique_hashes = vec![]; + let result = attestation_parent_hashes( + cycle_length, + block_slot, + attestation_slot, + ¤t_hashes, + &oblique_hashes); + let result = result.unwrap(); + assert_eq!(result.len(), cycle_length as usize); + let expected_result = get_range_of_hashes(7, 15); + assert_eq!(result, expected_result); + } + + #[test] + fn test_get_signed_hashes_scenario_3() { + /* + * attestation_slot too large + */ + let cycle_length: u8 = 8; + let block_slot: u64 = 100; + let attestation_slot: u64 = 100; + let current_hashes = get_range_of_hashes(0, 16); + let oblique_hashes = vec![]; + let result = attestation_parent_hashes( + cycle_length, + block_slot, + attestation_slot, + ¤t_hashes, + &oblique_hashes); + assert!(result.is_err()); + } + + #[test] + fn test_get_signed_hashes_scenario_4() { + /* + * Current hashes too small + */ + let cycle_length: u8 = 8; + let block_slot: u64 = 100; + let attestation_slot: u64 = 99; + let current_hashes = get_range_of_hashes(0, 15); + let oblique_hashes = vec![]; + let result = attestation_parent_hashes( + cycle_length, + block_slot, + attestation_slot, + ¤t_hashes, + &oblique_hashes); + assert!(result.is_err()); + } +} diff --git a/beacon_chain/validation/src/attestation_validation.rs b/beacon_chain/validation/src/attestation_validation.rs new file mode 100644 index 0000000000..e04450a154 --- /dev/null +++ b/beacon_chain/validation/src/attestation_validation.rs @@ -0,0 +1,210 @@ +use std::collections::HashSet; +use std::sync::Arc; +use super::types::{ + AttestationRecord, + AttesterMap, +}; +use super::attestation_parent_hashes::{ + attestation_parent_hashes, + ParentHashesError, +}; +use super::db::{ + ClientDB, + DBError +}; +use super::db::stores::{ + BlockStore, + ValidatorStore, +}; +use super::types::{ + Hash256, +}; +use super::message_generation::generate_signed_message; +use super::signature_verification::{ + verify_aggregate_signature_for_indices, + SignatureVerificationError, +}; + +#[derive(Debug,PartialEq)] +pub enum AttestationValidationError { + SlotTooHigh, + SlotTooLow, + JustifiedSlotIncorrect, + UnknownJustifiedBlock, + TooManyObliqueHashes, + BadCurrentHashes, + BadObliqueHashes, + BadAttesterMap, + IntWrapping, + PublicKeyCorrupt, + NoPublicKeyForValidator, + BadBitfieldLength, + InvalidBitfield, + InvalidBitfieldEndBits, + NoSignatures, + NonZeroTrailingBits, + BadAggregateSignature, + DBError(String), +} + +pub struct AttestationValidationContext + where T: ClientDB + Sized +{ + pub block_slot: u64, + pub cycle_length: u8, + pub last_justified_slot: u64, + pub parent_hashes: Arc>, + pub block_store: Arc>, + pub validator_store: Arc>, + pub attester_map: Arc, +} + +impl AttestationValidationContext + where T: ClientDB +{ + pub fn validate_attestation(&self, a: &AttestationRecord) + -> Result, AttestationValidationError> + { + /* + * The attesation slot must not be higher than the block that contained it. + */ + if a.slot > self.block_slot { + return Err(AttestationValidationError::SlotTooHigh); + } + + /* + * The slot of this attestation must not be more than cycle_length + 1 distance + * from the block that contained it. + */ + if a.slot < self.block_slot + .saturating_sub(u64::from(self.cycle_length).saturating_add(1)) { + return Err(AttestationValidationError::SlotTooLow); + } + + /* + * The attestation must indicate that its last justified slot is the same as the last justified + * slot known to us. + */ + if a.justified_slot != self.last_justified_slot { + return Err(AttestationValidationError::JustifiedSlotIncorrect); + } + + /* + * There is no need to include more oblique parents hashes than there are blocks + * in a cycle. + */ + if a.oblique_parent_hashes.len() > usize::from(self.cycle_length) { + return Err(AttestationValidationError::TooManyObliqueHashes); + } + + /* + * Retrieve the set of attestation indices for this slot and shard id. + * + * This is an array mapping the order that validators will appear in the bitfield to the + * canonincal index of a validator. + */ + let attestation_indices = self.attester_map.get(&(a.slot, a.shard_id)) + .ok_or(AttestationValidationError::BadAttesterMap)?; + + /* + * The bitfield must be no longer than the minimum required to represent each validator in the + * attestation indicies for this slot and shard id. + */ + if a.attester_bitfield.num_bytes() != + bytes_for_bits(attestation_indices.len()) + { + return Err(AttestationValidationError::BadBitfieldLength); + } + + /* + * If there are excess bits in the bitfield because the number of a validators in not a + * multiple of 8, reject this attestation record. + * + * Allow extra set bits would permit mutliple different byte layouts (and therefore hashes) to + * refer to the same AttesationRecord. + */ + if a.attester_bitfield.len() > attestation_indices.len() { + return Err(AttestationValidationError::InvalidBitfieldEndBits) + } + + /* + * The specified justified block hash must be known to us + */ + if !self.block_store.block_exists(&a.justified_block_hash)? { + return Err(AttestationValidationError::UnknownJustifiedBlock) + } + + let signed_message = { + let parent_hashes = attestation_parent_hashes( + self.cycle_length, + self.block_slot, + a.slot, + &self.parent_hashes, + &a.oblique_parent_hashes)?; + generate_signed_message( + a.slot, + &parent_hashes, + a.shard_id, + &a.shard_block_hash, + a.justified_slot) + }; + + let voted_hashmap = + verify_aggregate_signature_for_indices( + &signed_message, + &a.aggregate_sig, + &attestation_indices, + &a.attester_bitfield, + &self.validator_store)?; + + /* + * If the hashmap of voters is None, the signature verification failed. + */ + match voted_hashmap { + None => Err(AttestationValidationError::BadAggregateSignature), + Some(hashmap) => Ok(hashmap), + } + } +} + +fn bytes_for_bits(bits: usize) -> usize { + (bits.saturating_sub(1) / 8) + 1 +} + +impl From for AttestationValidationError { + fn from(e: ParentHashesError) -> Self { + match e { + ParentHashesError::BadCurrentHashes + => AttestationValidationError::BadCurrentHashes, + ParentHashesError::BadObliqueHashes + => AttestationValidationError::BadObliqueHashes, + ParentHashesError::SlotTooLow + => AttestationValidationError::SlotTooLow, + ParentHashesError::SlotTooHigh + => AttestationValidationError::SlotTooHigh, + ParentHashesError::IntWrapping + => AttestationValidationError::IntWrapping + } + } +} + +impl From for AttestationValidationError { + fn from(e: DBError) -> Self { + AttestationValidationError::DBError(e.message) + } +} + +impl From for AttestationValidationError { + fn from(e: SignatureVerificationError) -> Self { + match e { + SignatureVerificationError::BadValidatorIndex + => AttestationValidationError::BadAttesterMap, + SignatureVerificationError::PublicKeyCorrupt + => AttestationValidationError::PublicKeyCorrupt, + SignatureVerificationError::NoPublicKeyForValidator + => AttestationValidationError::NoPublicKeyForValidator, + SignatureVerificationError::DBError(s) + => AttestationValidationError::DBError(s), + } + } +} diff --git a/beacon_chain/validation/src/block_validation.rs b/beacon_chain/validation/src/block_validation.rs new file mode 100644 index 0000000000..6b11da801e --- /dev/null +++ b/beacon_chain/validation/src/block_validation.rs @@ -0,0 +1,370 @@ +extern crate rayon; + +use self::rayon::prelude::*; + +use std::sync::{ + Arc, + RwLock, +}; +use super::attestation_validation::{ + AttestationValidationContext, + AttestationValidationError, +}; +use super::types::{ + AttestationRecord, + AttesterMap, + Block, + ProposerMap, +}; +use super::ssz_helpers::attestation_ssz_splitter::{ + split_one_attestation, + split_all_attestations, + AttestationSplitError, +}; +use super::ssz_helpers::ssz_block::{ + SszBlock, + SszBlockError, +}; +use super::db::{ + ClientDB, + DBError, +}; +use super::db::stores::{ + BlockStore, + PoWChainStore, + ValidatorStore, +}; +use super::ssz::{ + Decodable, + DecodeError, +}; +use super::types::Hash256; + +#[derive(Debug, PartialEq)] +pub enum BlockStatus { + NewBlock, + KnownBlock, +} + +#[derive(Debug, PartialEq)] +pub enum SszBlockValidationError { + FutureSlot, + SlotAlreadyFinalized, + UnknownPoWChainRef, + UnknownParentHash, + BadAttestationSsz, + AttestationValidationError(AttestationValidationError), + AttestationSignatureFailed, + ProposerAttestationHasObliqueHashes, + NoProposerSignature, + BadProposerMap, + RwLockPoisoned, + DBError(String), +} + +/// The context against which a block should be validated. +pub struct BlockValidationContext + where T: ClientDB + Sized +{ + /// The slot as determined by the system time. + pub present_slot: u64, + /// The cycle_length as determined by the chain configuration. + pub cycle_length: u8, + /// The last justified slot as per the client's view of the canonical chain. + pub last_justified_slot: u64, + /// The last finalized slot as per the client's view of the canonical chain. + pub last_finalized_slot: u64, + /// A vec of the hashes of the blocks preceeding the present slot. + pub parent_hashes: Arc>, + /// A map of slots to a block proposer validation index. + pub proposer_map: Arc, + /// A map of (slot, shard_id) to the attestation set of validation indices. + pub attester_map: Arc, + /// The store containing block information. + pub block_store: Arc>, + /// The store containing validator information. + pub validator_store: Arc>, + /// The store containing information about the proof-of-work chain. + pub pow_store: Arc>, +} + +impl BlockValidationContext + where T: ClientDB +{ + /// Validate some SszBlock against a block validation context. An SszBlock varies from a Block in + /// that is a read-only structure that reads directly from encoded SSZ. + /// + /// The reason to validate an SzzBlock is to avoid decoding it in its entirety if there is + /// a suspicion that the block might be invalid. Such a suspicion should be applied to + /// all blocks coming from the network. + /// + /// This function will determine if the block is new, already known or invalid (either + /// intrinsically or due to some application error.) + /// + /// Note: this function does not implement randao_reveal checking as it is not in the + /// specification. + #[allow(dead_code)] + pub fn validate_ssz_block(&self, b: &SszBlock) + -> Result<(BlockStatus, Option), SszBlockValidationError> + where T: ClientDB + Sized + { + + /* + * If this block is already known, return immediately and indicate the the block is + * known. Don't attempt to deserialize the block. + */ + let block_hash = &b.block_hash(); + if self.block_store.block_exists(&block_hash)? { + return Ok((BlockStatus::KnownBlock, None)); + } + + /* + * If the block slot corresponds to a slot in the future, drop it. + */ + let block_slot = b.slot_number(); + if block_slot > self.present_slot { + return Err(SszBlockValidationError::FutureSlot); + } + + /* + * If the block is unknown (assumed unknown because we checked the db earlier in this + * function) and it comes from a slot that is already finalized, drop the block. + * + * If a slot is finalized, there's no point in considering any other blocks for that slot. + */ + if block_slot <= self.last_finalized_slot { + return Err(SszBlockValidationError::SlotAlreadyFinalized); + } + + /* + * If the PoW chain hash is not known to us, drop it. + * + * We only accept blocks that reference a known PoW hash. + * + * Note: it is not clear what a "known" PoW chain ref is. Likely it means the block hash is + * "sufficienty deep in the canonical PoW chain". This should be clarified as the spec + * crystallizes. + */ + let pow_chain_ref = b.pow_chain_ref(); + if !self.pow_store.block_hash_exists(b.pow_chain_ref())? { + return Err(SszBlockValidationError::UnknownPoWChainRef); + } + + /* + * Store a slice of the serialized attestations from the block SSZ. + */ + let attestations_ssz = &b.attestations(); + + /* + * Get a slice of the first serialized attestation (the 0'th) and decode it into + * a full AttestationRecord object. + * + * The first attestation must be validated separately as it must contain a signature of the + * proposer of the previous block (this is checked later in this function). + */ + let (first_attestation_ssz, next_index) = split_one_attestation( + &attestations_ssz, + 0)?; + let (first_attestation, _) = AttestationRecord::ssz_decode( + &first_attestation_ssz, 0)?; + + /* + * The first attestation may not have oblique hashes. + * + * The presence of oblique hashes in the first attestation would indicate that the proposer + * of the previous block is attesting to some other block than the one they produced. + */ + if first_attestation.oblique_parent_hashes.len() > 0 { + return Err(SszBlockValidationError::ProposerAttestationHasObliqueHashes); + } + + /* + * Generate the context in which attestations will be validated. + */ + let attestation_validation_context = Arc::new(AttestationValidationContext { + block_slot, + cycle_length: self.cycle_length, + last_justified_slot: self.last_justified_slot, + parent_hashes: self.parent_hashes.clone(), + block_store: self.block_store.clone(), + validator_store: self.validator_store.clone(), + attester_map: self.attester_map.clone(), + }); + + /* + * Validate this first attestation. + */ + let attestation_voters = attestation_validation_context + .validate_attestation(&first_attestation)?; + + /* + * Read the parent hash from the block we are validating then attempt to load + * the parent block ssz from the database. If that parent doesn't exist in + * the database, reject the block. + * + * If the parent does exist in the database, read the slot of that parent. Then, + * determine the proposer of that slot (the parent slot) by looking it up + * in the proposer map. + * + * If that proposer (the proposer of the parent block) was not present in the first (0'th) + * attestation of this block, reject the block. + */ + let parent_hash = b.parent_hash(); + match self.block_store.get_serialized_block(&parent_hash)? { + None => return Err(SszBlockValidationError::UnknownParentHash), + Some(ssz) => { + let parent_block = SszBlock::from_slice(&ssz[..])?; + let proposer = self.proposer_map.get(&parent_block.slot_number()) + .ok_or(SszBlockValidationError::BadProposerMap)?; + if !attestation_voters.contains(&proposer) { + return Err(SszBlockValidationError::NoProposerSignature); + } + } + } + + /* + * Split the remaining attestations into a vector of slices, each containing + * a single serialized attestation record. + */ + let other_attestations = split_all_attestations(attestations_ssz, + next_index)?; + + /* + * Verify each other AttestationRecord. + * + * This uses the `rayon` library to do "sometimes" parallelization. Put simply, + * if there are some spare threads, the verification of attestation records will happen + * concurrently. + * + * There is a thread-safe `failure` variable which is set whenever an attestation fails + * validation. This is so all attestation validation is halted if a single bad attestation + * is found. + */ + let failure: RwLock> = RwLock::new(None); + let mut deserialized_attestations: Vec = other_attestations + .par_iter() + .filter_map(|attestation_ssz| { + /* + * If some thread has set the `failure` variable to `Some(error)` the abandon + * attestation serialization and validation. + */ + if let Some(_) = *failure.read().unwrap() { + return None; + } + /* + * If there has not been a failure yet, attempt to serialize and validate the + * attestation. + */ + match AttestationRecord::ssz_decode(&attestation_ssz, 0) { + /* + * Deserialization failed, therefore the block is invalid. + */ + Err(e) => { + let mut failure = failure.write().unwrap(); + *failure = Some(SszBlockValidationError::from(e)); + None + } + /* + * Deserialization succeeded and the attestation should be validated. + */ + Ok((attestation, _)) => { + match attestation_validation_context.validate_attestation(&attestation) { + /* + * Attestation validation failed with some error. + */ + Err(e) => { + let mut failure = failure.write().unwrap(); + *failure = Some(SszBlockValidationError::from(e)); + None + } + /* + * Attestation validation succeded. + */ + Ok(_) => Some(attestation) + } + } + } + }) + .collect(); + + match failure.into_inner() { + Err(_) => return Err(SszBlockValidationError::RwLockPoisoned), + Ok(failure) => { + match failure { + Some(error) => return Err(error), + _ => () + } + + } + } + + /* + * Add the first attestation to the vec of deserialized attestations at + * index 0. + */ + deserialized_attestations.insert(0, first_attestation); + + /* + * If we have reached this point, the block is a new valid block that is worthy of + * processing. + */ + let block = Block { + parent_hash: Hash256::from(parent_hash), + slot_number: block_slot, + randao_reveal: Hash256::from(b.randao_reveal()), + attestations: deserialized_attestations, + pow_chain_ref: Hash256::from(pow_chain_ref), + active_state_root: Hash256::from(b.act_state_root()), + crystallized_state_root: Hash256::from(b.cry_state_root()), + }; + Ok((BlockStatus::NewBlock, Some(block))) + } +} + +impl From for SszBlockValidationError { + fn from(e: DBError) -> Self { + SszBlockValidationError::DBError(e.message) + } +} + +impl From for SszBlockValidationError { + fn from(e: AttestationSplitError) -> Self { + match e { + AttestationSplitError::TooShort => + SszBlockValidationError::BadAttestationSsz + } + } +} + +impl From for SszBlockValidationError { + fn from(e: SszBlockError) -> Self { + match e { + SszBlockError::TooShort => + SszBlockValidationError::DBError("Bad parent block in db.".to_string()), + SszBlockError::TooLong => + SszBlockValidationError::DBError("Bad parent block in db.".to_string()), + } + } +} + +impl From for SszBlockValidationError { + fn from(e: DecodeError) -> Self { + match e { + DecodeError::TooShort => + SszBlockValidationError::BadAttestationSsz, + DecodeError::TooLong => + SszBlockValidationError::BadAttestationSsz, + } + } +} + +impl From for SszBlockValidationError { + fn from(e: AttestationValidationError) -> Self { + SszBlockValidationError::AttestationValidationError(e) + } +} + +/* + * Tests for block validation are contained in the root directory "tests" directory (AKA + * "integration tests directory"). + */ diff --git a/beacon_chain/validation/src/lib.rs b/beacon_chain/validation/src/lib.rs new file mode 100644 index 0000000000..7ddf143f63 --- /dev/null +++ b/beacon_chain/validation/src/lib.rs @@ -0,0 +1,12 @@ +extern crate db; +extern crate bls; +extern crate hashing; +extern crate ssz; +extern crate ssz_helpers; +extern crate types; + +pub mod attestation_validation; +mod attestation_parent_hashes; +pub mod block_validation; +mod message_generation; +mod signature_verification; diff --git a/beacon_chain/validation/src/message_generation.rs b/beacon_chain/validation/src/message_generation.rs new file mode 100644 index 0000000000..e831eabcfe --- /dev/null +++ b/beacon_chain/validation/src/message_generation.rs @@ -0,0 +1,70 @@ +use super::ssz::SszStream; +use super::hashing::canonical_hash; +use super::types::Hash256; + +/// Generates the message used to validate the signature provided with an AttestationRecord. +/// +/// Ensures that the signer of the message has a view of the chain that is compatible with ours. +pub fn generate_signed_message( + slot: u64, + parent_hashes: &[Hash256], + shard_id: u16, + shard_block_hash: &Hash256, + justified_slot: u64) + -> Vec +{ + /* + * Note: it's a little risky here to use SSZ, because the encoding is not necessarily SSZ + * (for example, SSZ might change whilst this doesn't). + * + * I have suggested switching this to ssz here: + * https://github.com/ethereum/eth2.0-specs/issues/5 + * + * If this doesn't happen, it would be safer to not use SSZ at all. + */ + let mut ssz_stream = SszStream::new(); + ssz_stream.append(&slot); + ssz_stream.append_vec(&parent_hashes.to_vec()); + ssz_stream.append(&shard_id); + ssz_stream.append(shard_block_hash); + ssz_stream.append(&justified_slot); + let bytes = ssz_stream.drain(); + canonical_hash(&bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_signed_message() { + let slot = 93; + let parent_hashes: Vec = (0..12) + .map(|i| Hash256::from(i as u64)) + .collect(); + let shard_id = 15; + let shard_block_hash = Hash256::from("shard_block_hash".as_bytes()); + let justified_slot = 18; + + let output = generate_signed_message( + slot, + &parent_hashes, + shard_id, + &shard_block_hash, + justified_slot); + + /* + * Note: this is not some well-known test vector, it's simply the result of running + * this and printing the output. + * + * Once well-known test vectors are established, they should be placed here. + */ + let expected = vec![ + 149, 99, 94, 229, 72, 144, 233, 14, 164, 16, 143, 53, 94, 48, + 118, 179, 33, 181, 172, 215, 2, 191, 176, 18, 188, 172, 137, + 178, 236, 66, 74, 120 + ]; + + assert_eq!(output, expected); + } +} diff --git a/beacon_chain/validation/src/mod_attestation.rs b/beacon_chain/validation/src/mod_attestation.rs new file mode 100644 index 0000000000..ea7bdbc71d --- /dev/null +++ b/beacon_chain/validation/src/mod_attestation.rs @@ -0,0 +1,16 @@ +use super::common::maps::AttesterMap; +use super::db; +use super::bls; +use super::ssz; +use super::types; +use super::common::attestation_parent_hashes; +use super::utils; + +mod attestation_validation; +mod signature_verification; +mod message_generation; + +pub use self::attestation_validation::{ + AttestationValidationContext, + AttestationValidationError, +}; diff --git a/beacon_chain/validation/src/mod_block.rs b/beacon_chain/validation/src/mod_block.rs new file mode 100644 index 0000000000..a18d3934f5 --- /dev/null +++ b/beacon_chain/validation/src/mod_block.rs @@ -0,0 +1,21 @@ +mod block_validation; + +use super::attestation_record; +use super::{ + SszBlock, + SszBlockError, + Block, +}; +use super::db; +use super::ssz; +use super::utils; + +pub use super::common::maps::{ + AttesterMap, + ProposerMap, +}; +pub use self::block_validation::{ + BlockValidationContext, + SszBlockValidationError, + BlockStatus, +}; diff --git a/beacon_chain/validation/src/signature_verification.rs b/beacon_chain/validation/src/signature_verification.rs new file mode 100644 index 0000000000..3fdd4c4827 --- /dev/null +++ b/beacon_chain/validation/src/signature_verification.rs @@ -0,0 +1,183 @@ +use std::collections::HashSet; +use super::bls::{ + AggregateSignature, + AggregatePublicKey, +}; +use super::db::ClientDB; +use super::db::stores::{ + ValidatorStore, + ValidatorStoreError, +}; +use super::types::Bitfield; + +#[derive(Debug, PartialEq)] +pub enum SignatureVerificationError { + BadValidatorIndex, + PublicKeyCorrupt, + NoPublicKeyForValidator, + DBError(String), +} + +/// Verify an aggregate signature across the supplied message. +/// +/// The public keys used for verification are collected by mapping +/// each true bitfield bit to canonical ValidatorRecord index through +/// the attestation_indicies map. +/// +/// Each public key is loaded from the store on-demand. +pub fn verify_aggregate_signature_for_indices( + message: &[u8], + agg_sig: &AggregateSignature, + attestation_indices: &[usize], + bitfield: &Bitfield, + validator_store: &ValidatorStore) + -> Result>, SignatureVerificationError> + where T: ClientDB + Sized +{ + let mut voters = HashSet::new(); + let mut agg_pub_key = AggregatePublicKey::new(); + + for i in 0..attestation_indices.len() { + let voted = bitfield.get_bit(i); + if voted { + /* + * De-reference the attestation index into a canonical ValidatorRecord index. + */ + let validator = *attestation_indices.get(i) + .ok_or(SignatureVerificationError::BadValidatorIndex)?; + /* + * Load the validators public key from our store. + */ + let pub_key = validator_store + .get_public_key_by_index(validator)? + .ok_or(SignatureVerificationError::NoPublicKeyForValidator)?; + /* + * Add the validators public key to the aggregate public key. + */ + agg_pub_key.add(&pub_key); + /* + * Add to the validator to the set of voters for this attestation record. + */ + voters.insert(validator); + } + } + /* + * Verify the aggregate public key against the aggregate signature. + * + * This verification will only succeed if the exact set of public keys + * were added to the aggregate public key as those that signed the aggregate signature. + */ + if agg_sig.verify(&message, &agg_pub_key) { + Ok(Some(voters)) + } else { + Ok(None) + } +} + +impl From for SignatureVerificationError { + fn from(error: ValidatorStoreError) -> Self { + match error { + ValidatorStoreError::DBError(s) => + SignatureVerificationError::DBError(s), + ValidatorStoreError::DecodeError => + SignatureVerificationError::PublicKeyCorrupt, + } + } +} + + +#[cfg(test)] +mod tests { + use super::*; + use super::super::bls::{ + Keypair, + Signature, + }; + use super::super::db::MemoryDB; + use std::sync::Arc; + + /* + * Cases that still need testing: + * + * - No signatures. + * - Database failure. + * - Unknown validator index. + * - Extra validator on signature. + */ + + #[test] + fn test_signature_verification() { + let message = "cats".as_bytes(); + let signing_keypairs = vec![ + Keypair::random(), + Keypair::random(), + Keypair::random(), + Keypair::random(), + Keypair::random(), + Keypair::random(), + ]; + let non_signing_keypairs = vec![ + Keypair::random(), + Keypair::random(), + Keypair::random(), + Keypair::random(), + Keypair::random(), + Keypair::random(), + ]; + /* + * Signing keypairs first, then non-signing + */ + let mut all_keypairs = signing_keypairs.clone(); + all_keypairs.append(&mut non_signing_keypairs.clone()); + + let attestation_indices: Vec = (0..all_keypairs.len()) + .collect(); + let mut bitfield = Bitfield::new(); + for i in 0..signing_keypairs.len() { + bitfield.set_bit(i, true); + } + + let db = Arc::new(MemoryDB::open()); + let store = ValidatorStore::new(db); + + for (i, keypair) in all_keypairs.iter().enumerate() { + store.put_public_key_by_index(i, &keypair.pk).unwrap(); + } + + let mut agg_sig = AggregateSignature::new(); + for keypair in &signing_keypairs { + let sig = Signature::new(&message, &keypair.sk); + agg_sig.add(&sig); + } + + /* + * Test using all valid parameters. + */ + let voters = verify_aggregate_signature_for_indices( + &message, + &agg_sig, + &attestation_indices, + &bitfield, + &store).unwrap(); + + let voters = voters.unwrap(); + (0..signing_keypairs.len()) + .for_each(|i| assert!(voters.contains(&i))); + (signing_keypairs.len()..non_signing_keypairs.len()) + .for_each(|i| assert!(!voters.contains(&i))); + + /* + * Add another validator to the bitfield, run validation will all other + * parameters the same and assert that it fails. + */ + bitfield.set_bit(signing_keypairs.len() + 1, true); + let voters = verify_aggregate_signature_for_indices( + &message, + &agg_sig, + &attestation_indices, + &bitfield, + &store).unwrap(); + + assert_eq!(voters, None); + } +} diff --git a/beacon_chain/validation/tests/attestation_validation/helpers.rs b/beacon_chain/validation/tests/attestation_validation/helpers.rs new file mode 100644 index 0000000000..7c8ba20858 --- /dev/null +++ b/beacon_chain/validation/tests/attestation_validation/helpers.rs @@ -0,0 +1,192 @@ +use std::sync::Arc; + +use super::db::{ + MemoryDB, +}; +use super::db::stores::{ + BlockStore, + ValidatorStore, +}; +use super::types::{ + AttestationRecord, + AttesterMap, + Bitfield, + Hash256, +}; +use super::validation::attestation_validation::{ + AttestationValidationContext, +}; +use super::bls::{ + AggregateSignature, + Keypair, + SecretKey, + Signature, +}; +use super::ssz::SszStream; +use super::hashing::{ + canonical_hash, +}; + + +pub struct TestStore { + pub db: Arc, + pub block: Arc>, + pub validator: Arc>, +} + +impl TestStore { + pub fn new() -> Self { + let db = Arc::new(MemoryDB::open()); + let block = Arc::new(BlockStore::new(db.clone())); + let validator = Arc::new(ValidatorStore::new(db.clone())); + Self { + db, + block, + validator, + } + } +} + +pub struct TestRig { + pub attestation: AttestationRecord, + pub context: AttestationValidationContext, + pub stores: TestStore, + pub attester_count: usize, +} + +fn generate_message_hash(slot: u64, + parent_hashes: &[Hash256], + shard_id: u16, + shard_block_hash: &Hash256, + justified_slot: u64) + -> Vec +{ + let mut stream = SszStream::new(); + stream.append(&slot); + stream.append_vec(&parent_hashes.to_vec()); + stream.append(&shard_id); + stream.append(shard_block_hash); + stream.append(&justified_slot); + let bytes = stream.drain(); + canonical_hash(&bytes) +} + +pub fn generate_attestation(shard_id: u16, + shard_block_hash: &Hash256, + block_slot: u64, + attestation_slot: u64, + justified_slot: u64, + justified_block_hash: &Hash256, + cycle_length: u8, + parent_hashes: &[Hash256], + signing_keys: &[Option]) + -> AttestationRecord +{ + let mut attester_bitfield = Bitfield::new(); + let mut aggregate_sig = AggregateSignature::new(); + + let parent_hashes_slice = { + let distance: usize = (block_slot - attestation_slot) as usize; + let last: usize = parent_hashes.len() - distance; + let first: usize = last - usize::from(cycle_length); + &parent_hashes[first..last] + }; + + /* + * Generate the message that will be signed across for this attr record. + */ + let attestation_message = generate_message_hash( + attestation_slot, + parent_hashes_slice, + shard_id, + shard_block_hash, + justified_slot); + + for (i, secret_key) in signing_keys.iter().enumerate() { + /* + * If the signing key is Some, set the bitfield bit to true + * and sign the aggregate sig. + */ + if let Some(sk) = secret_key { + attester_bitfield.set_bit(i, true); + let sig = Signature::new(&attestation_message, sk); + aggregate_sig.add(&sig); + } + } + + AttestationRecord { + slot: attestation_slot, + shard_id, + oblique_parent_hashes: vec![], + shard_block_hash: shard_block_hash.clone(), + attester_bitfield, + justified_slot, + justified_block_hash: justified_block_hash.clone(), + aggregate_sig, + } +} + +pub fn setup_attestation_validation_test(shard_id: u16, attester_count: usize) + -> TestRig +{ + let stores = TestStore::new(); + + let block_slot = 10000; + let cycle_length: u8 = 64; + let last_justified_slot = block_slot - u64::from(cycle_length); + let parent_hashes: Vec = (0..(cycle_length * 2)) + .map(|i| Hash256::from(i as u64)) + .collect(); + let parent_hashes = Arc::new(parent_hashes); + let justified_block_hash = Hash256::from("justified_block".as_bytes()); + let shard_block_hash = Hash256::from("shard_block".as_bytes()); + + stores.block.put_serialized_block(&justified_block_hash.as_ref(), &[42]).unwrap(); + + let attestation_slot = block_slot - 1; + + let mut keypairs = vec![]; + let mut signing_keys = vec![]; + let mut attester_map = AttesterMap::new(); + let mut attesters = vec![]; + + /* + * Generate a random keypair for each validator and clone it into the + * list of keypairs. Store it in the database. + */ + for i in 0..attester_count { + let keypair = Keypair::random(); + keypairs.push(keypair.clone()); + stores.validator.put_public_key_by_index(i, &keypair.pk).unwrap(); + signing_keys.push(Some(keypair.sk.clone())); + attesters.push(i); + } + attester_map.insert((attestation_slot, shard_id), attesters); + + let context: AttestationValidationContext = AttestationValidationContext { + block_slot, + cycle_length, + last_justified_slot, + parent_hashes: parent_hashes.clone(), + block_store: stores.block.clone(), + validator_store: stores.validator.clone(), + attester_map: Arc::new(attester_map), + }; + let attestation = generate_attestation( + shard_id, + &shard_block_hash, + block_slot, + attestation_slot, + last_justified_slot, + &justified_block_hash, + cycle_length, + &parent_hashes.clone(), + &signing_keys); + + TestRig { + attestation, + context, + stores, + attester_count, + } +} diff --git a/beacon_chain/validation/tests/attestation_validation/mod.rs b/beacon_chain/validation/tests/attestation_validation/mod.rs new file mode 100644 index 0000000000..f6fe5ddae6 --- /dev/null +++ b/beacon_chain/validation/tests/attestation_validation/mod.rs @@ -0,0 +1,9 @@ +pub mod helpers; +mod tests; + +use super::bls; +use super::db; +use super::ssz; +use super::types; +use super::hashing; +use super::validation; diff --git a/beacon_chain/validation/tests/attestation_validation/tests.rs b/beacon_chain/validation/tests/attestation_validation/tests.rs new file mode 100644 index 0000000000..1e2fc388c5 --- /dev/null +++ b/beacon_chain/validation/tests/attestation_validation/tests.rs @@ -0,0 +1,127 @@ +use std::sync::Arc; + +use super::helpers::{ + TestRig, + setup_attestation_validation_test, +}; +use super::validation::attestation_validation::{ + AttestationValidationError, +}; +use super::types::AttesterMap; +use super::bls::{ + AggregateSignature, +}; +use super::types::{ + Hash256, +}; + +fn generic_rig() -> TestRig { + let shard_id = 10; + let validator_count = 2; + setup_attestation_validation_test(shard_id, validator_count) +} + +#[test] +fn test_attestation_validation_valid() { + let rig = generic_rig(); + + let result = rig.context.validate_attestation(&rig.attestation); + + let voter_map = result.unwrap(); + assert_eq!(voter_map.len(), 2); +} + +#[test] +fn test_attestation_validation_invalid_slot_too_high() { + let mut rig = generic_rig(); + + rig.attestation.slot = rig.context.block_slot + 1; + + let result = rig.context.validate_attestation(&rig.attestation); + assert_eq!(result, Err(AttestationValidationError::SlotTooHigh)); +} + +#[test] +fn test_attestation_validation_invalid_slot_too_low() { + let mut rig = generic_rig(); + + rig.attestation.slot = rig.context.block_slot - u64::from(rig.context.cycle_length) - 2; + let result = rig.context.validate_attestation(&rig.attestation); + assert_eq!(result, Err(AttestationValidationError::SlotTooLow)); +} + +#[test] +fn test_attestation_validation_invalid_justified_slot_incorrect() { + let mut rig = generic_rig(); + + let original = rig.attestation.justified_slot; + rig.attestation.justified_slot = original - 1; + let result = rig.context.validate_attestation(&rig.attestation); + assert_eq!(result, Err(AttestationValidationError::JustifiedSlotIncorrect)); + + rig.attestation.justified_slot = original + 1; + let result = rig.context.validate_attestation(&rig.attestation); + assert_eq!(result, Err(AttestationValidationError::JustifiedSlotIncorrect)); +} + +#[test] +fn test_attestation_validation_invalid_too_many_oblique() { + let mut rig = generic_rig(); + + let obliques: Vec = (0..(rig.context.cycle_length + 1)) + .map(|i| Hash256::from((i * 2) as u64)) + .collect(); + + rig.attestation.oblique_parent_hashes = obliques; + + let result = rig.context.validate_attestation(&rig.attestation); + assert_eq!(result, Err(AttestationValidationError::TooManyObliqueHashes)); +} + +#[test] +fn test_attestation_validation_invalid_bad_attester_map() { + let mut rig = generic_rig(); + + rig.context.attester_map = Arc::new(AttesterMap::new()); + + let result = rig.context.validate_attestation(&rig.attestation); + assert_eq!(result, Err(AttestationValidationError::BadAttesterMap)); +} + +#[test] +fn test_attestation_validation_invalid_bad_bitfield_length() { + let mut rig = generic_rig(); + + /* + * Extend the bitfield by one byte + * + * This is a little hacky and makes assumptions about the internals + * of the bitfield. + */ + let one_byte_higher = rig.attester_count + 8; + rig.attestation.attester_bitfield.set_bit(one_byte_higher, true); + rig.attestation.attester_bitfield.set_bit(one_byte_higher, false); + + let result = rig.context.validate_attestation(&rig.attestation); + assert_eq!(result, Err(AttestationValidationError::BadBitfieldLength)); +} + +#[test] +fn test_attestation_validation_invalid_unknown_justfied_block_hash() { + let mut rig = generic_rig(); + + rig.attestation.justified_block_hash = Hash256::from("unknown block hash".as_bytes()); + + let result = rig.context.validate_attestation(&rig.attestation); + assert_eq!(result, Err(AttestationValidationError::UnknownJustifiedBlock)); +} + +#[test] +fn test_attestation_validation_invalid_empty_signature() { + let mut rig = generic_rig(); + + rig.attestation.aggregate_sig = AggregateSignature::new(); + + let result = rig.context.validate_attestation(&rig.attestation); + assert_eq!(result, Err(AttestationValidationError::BadAggregateSignature)); +} diff --git a/beacon_chain/validation/tests/block_validation/helpers.rs b/beacon_chain/validation/tests/block_validation/helpers.rs new file mode 100644 index 0000000000..e04e6fac80 --- /dev/null +++ b/beacon_chain/validation/tests/block_validation/helpers.rs @@ -0,0 +1,226 @@ +use std::sync::Arc; + +use super::generate_attestation; +use super::bls::{ + Keypair, +}; +use super::db::{ + MemoryDB, +}; +use super::db::stores::{ + BlockStore, + PoWChainStore, + ValidatorStore, +}; +use super::types::{ + AttestationRecord, + AttesterMap, + Block, + Hash256, + ProposerMap, +}; +use super::ssz_helpers::ssz_block::SszBlock; +use super::validation::block_validation::{ + BlockValidationContext, + SszBlockValidationError, + BlockStatus, +}; +use super::ssz::{ + SszStream, +}; + +#[derive(Debug)] +pub struct BlockTestParams { + pub total_validators: usize, + pub cycle_length: u8, + pub shard_count: u16, + pub shards_per_slot: u16, + pub validators_per_shard: usize, + pub block_slot: u64, + pub attestations_justified_slot: u64, + pub parent_proposer_index: usize, + pub validation_context_slot: u64, + pub validation_context_justified_slot: u64, + pub validation_context_finalized_slot: u64, +} + +pub struct TestStore { + pub db: Arc, + pub block: Arc>, + pub pow_chain: Arc>, + pub validator: Arc>, +} + +impl TestStore { + pub fn new() -> Self { + let db = Arc::new(MemoryDB::open()); + let block = Arc::new(BlockStore::new(db.clone())); + let pow_chain = Arc::new(PoWChainStore::new(db.clone())); + let validator = Arc::new(ValidatorStore::new(db.clone())); + Self { + db, + block, + pow_chain, + validator, + } + } +} + +type ParentHashes = Vec; + +/// Setup for a block validation function, without actually executing the +/// block validation function. +pub fn setup_block_validation_scenario(params: &BlockTestParams) + -> (Block, ParentHashes, AttesterMap, ProposerMap, TestStore) +{ + let stores = TestStore::new(); + + let cycle_length = params.cycle_length; + let shards_per_slot = params.shards_per_slot; + let validators_per_shard = params.validators_per_shard; + let block_slot = params.block_slot; + let attestations_justified_slot = params.attestations_justified_slot; + + let parent_hashes: Vec = (0..(cycle_length * 2)) + .map(|i| Hash256::from(i as u64)) + .collect(); + let parent_hash = Hash256::from("parent_hash".as_bytes()); + let randao_reveal = Hash256::from("randao_reveal".as_bytes()); + let justified_block_hash = Hash256::from("justified_hash".as_bytes()); + let pow_chain_ref = Hash256::from("pow_chain".as_bytes()); + let active_state_root = Hash256::from("active_state".as_bytes()); + let crystallized_state_root = Hash256::from("cry_state".as_bytes()); + let shard_block_hash = Hash256::from("shard_block_hash".as_bytes()); + + stores.pow_chain.put_block_hash(pow_chain_ref.as_ref()).unwrap(); + stores.block.put_serialized_block(justified_block_hash.as_ref(), &vec![42]).unwrap(); + + /* + * Generate a minimum viable parent block and store it in the database. + */ + let mut parent_block = Block::zero(); + let parent_attestation = AttestationRecord::zero(); + parent_block.slot_number = block_slot - 1; + parent_block.attestations.push(parent_attestation); + let parent_block_ssz = serialize_block(&parent_block); + stores.block.put_serialized_block(parent_hash.as_ref(), &parent_block_ssz).unwrap(); + + let proposer_map = { + let mut proposer_map = ProposerMap::new(); + proposer_map.insert(parent_block.slot_number, params.parent_proposer_index); + proposer_map + }; + + let (attester_map, attestations, _keypairs) = { + let mut i = 0; + let attestation_slot = block_slot - 1; + let mut attester_map = AttesterMap::new(); + let mut attestations = vec![]; + let mut keypairs = vec![]; + /* + * For each shard in this slot, generate an attestation. + */ + for shard in 0..shards_per_slot { + let mut signing_keys = vec![]; + let mut attesters = vec![]; + /* + * Generate a random keypair for each validator and clone it into the + * list of keypairs. Store it in the database. + */ + for _ in 0..validators_per_shard { + let keypair = Keypair::random(); + keypairs.push(keypair.clone()); + stores.validator.put_public_key_by_index(i, &keypair.pk).unwrap(); + signing_keys.push(Some(keypair.sk.clone())); + attesters.push(i); + i += 1; + } + attester_map.insert((attestation_slot, shard), attesters); + + let attestation = generate_attestation( + shard, + &shard_block_hash, + block_slot, + attestation_slot, + attestations_justified_slot, + &justified_block_hash, + cycle_length, + &parent_hashes, + &signing_keys[..]); + attestations.push(attestation); + } + (attester_map, attestations, keypairs) + }; + + let block = Block { + parent_hash, + slot_number: block_slot, + randao_reveal, + attestations, + pow_chain_ref, + active_state_root, + crystallized_state_root, + }; + + (block, + parent_hashes, + attester_map, + proposer_map, + stores) +} + +/// Helper function to take some Block and SSZ serialize it. +pub fn serialize_block(b: &Block) -> Vec { + let mut stream = SszStream::new(); + stream.append(b); + stream.drain() +} + +/// Setup and run a block validation scenario, given some parameters. +/// +/// Returns the Result returned from the block validation function. +pub fn run_block_validation_scenario( + params: &BlockTestParams, + mutator_func: F) + -> Result<(BlockStatus, Option), SszBlockValidationError> + where F: FnOnce(Block, AttesterMap, ProposerMap, TestStore) + -> (Block, AttesterMap, ProposerMap, TestStore) +{ + let (block, + parent_hashes, + attester_map, + proposer_map, + stores) = setup_block_validation_scenario(¶ms); + + let (block, + attester_map, + proposer_map, + stores) = mutator_func(block, attester_map, proposer_map, stores); + + let ssz_bytes = serialize_block(&block); + let ssz_block = SszBlock::from_slice(&ssz_bytes[..]) + .unwrap(); + + let context = BlockValidationContext { + present_slot: params.validation_context_slot, + cycle_length: params.cycle_length, + last_justified_slot: params.validation_context_justified_slot, + last_finalized_slot: params.validation_context_finalized_slot, + parent_hashes: Arc::new(parent_hashes), + proposer_map: Arc::new(proposer_map), + attester_map: Arc::new(attester_map), + block_store: stores.block.clone(), + validator_store: stores.validator.clone(), + pow_store: stores.pow_chain.clone() + }; + let validation_status = context.validate_ssz_block(&ssz_block); + /* + * If validation returned a block, make sure it's the same block we supplied to it. + * + * I.e., there were no errors during the serialization -> deserialization process. + */ + if let Ok((_, Some(returned_block))) = &validation_status { + assert_eq!(*returned_block, block); + }; + validation_status +} diff --git a/beacon_chain/validation/tests/block_validation/mod.rs b/beacon_chain/validation/tests/block_validation/mod.rs new file mode 100644 index 0000000000..83431606c3 --- /dev/null +++ b/beacon_chain/validation/tests/block_validation/mod.rs @@ -0,0 +1,12 @@ +mod helpers; +mod tests; + +use super::bls; +use super::db; +use super::hashing; +use super::ssz; +use super::ssz_helpers; +use super::types; +use super::validation; + +use super::attestation_validation::helpers::generate_attestation; diff --git a/beacon_chain/validation/tests/block_validation/tests.rs b/beacon_chain/validation/tests/block_validation/tests.rs new file mode 100644 index 0000000000..95ddd4222e --- /dev/null +++ b/beacon_chain/validation/tests/block_validation/tests.rs @@ -0,0 +1,243 @@ +use super::bls::{ + AggregateSignature, +}; +use super::helpers::{ + BlockTestParams, + TestStore, + run_block_validation_scenario, + serialize_block, +}; +use super::types::{ + Block, + Hash256, + ProposerMap, +}; +use super::ssz_helpers::ssz_block::SszBlock; +use super::validation::block_validation::{ + SszBlockValidationError, + BlockStatus, +}; +use super::validation::attestation_validation::{ + AttestationValidationError, +}; +use super::hashing::canonical_hash; + +fn get_simple_params() -> BlockTestParams { + let validators_per_shard: usize = 5; + let cycle_length: u8 = 2; + let shard_count: u16 = 4; + let shards_per_slot: u16 = shard_count / u16::from(cycle_length); + let total_validators: usize = validators_per_shard * shard_count as usize; + let block_slot = u64::from(cycle_length) * 10000; + let attestations_justified_slot = block_slot - u64::from(cycle_length); + let parent_proposer_index = 0; + + let validation_context_slot = block_slot; + let validation_context_justified_slot = attestations_justified_slot; + let validation_context_finalized_slot = 0; + + BlockTestParams { + total_validators, + cycle_length, + shard_count, + shards_per_slot, + validators_per_shard, + parent_proposer_index, + block_slot, + attestations_justified_slot, + validation_context_slot, + validation_context_justified_slot, + validation_context_finalized_slot, + } +} + +// TODO: test bad ssz serialization + +#[test] +fn test_block_validation_valid() { + let params = get_simple_params(); + + let mutator = |block: Block, attester_map, proposer_map, stores| { + /* + * Do not mutate + */ + (block, attester_map, proposer_map, stores) + }; + + let status = run_block_validation_scenario( + ¶ms, + mutator); + + assert_eq!(status.unwrap().0, BlockStatus::NewBlock); +} + +#[test] +fn test_block_validation_valid_known_block() { + let params = get_simple_params(); + + let mutator = |block: Block, attester_map, proposer_map, stores: TestStore| { + /* + * Pre-store the block in the database + */ + let block_ssz = serialize_block(&block); + let block_hash = canonical_hash(&block_ssz); + stores.block.put_serialized_block(&block_hash, &block_ssz).unwrap(); + (block, attester_map, proposer_map, stores) + }; + + let status = run_block_validation_scenario( + ¶ms, + mutator); + + assert_eq!(status.unwrap(), (BlockStatus::KnownBlock, None)); +} + +#[test] +fn test_block_validation_invalid_future_slot() { + let params = get_simple_params(); + + let mutator = |mut block: Block, attester_map, proposer_map, stores| { + block.slot_number = block.slot_number + 1; + (block, attester_map, proposer_map, stores) + }; + + let status = run_block_validation_scenario( + ¶ms, + mutator); + + assert_eq!(status, Err(SszBlockValidationError::FutureSlot)); +} + +#[test] +fn test_block_validation_invalid_slot_already_finalized() { + let mut params = get_simple_params(); + + params.validation_context_finalized_slot = params.block_slot; + params.validation_context_justified_slot = params.validation_context_finalized_slot + + u64::from(params.cycle_length); + + let mutator = |block, attester_map, proposer_map, stores| { + /* + * Do not mutate + */ + (block, attester_map, proposer_map, stores) + }; + + let status = run_block_validation_scenario( + ¶ms, + mutator); + + assert_eq!(status, Err(SszBlockValidationError::SlotAlreadyFinalized)); +} + +#[test] +fn test_block_validation_invalid_unknown_pow_hash() { + let params = get_simple_params(); + + let mutator = |mut block: Block, attester_map, proposer_map, stores| { + block.pow_chain_ref = Hash256::from("unknown pow hash".as_bytes()); + (block, attester_map, proposer_map, stores) + }; + + let status = run_block_validation_scenario( + ¶ms, + mutator); + + assert_eq!(status, Err(SszBlockValidationError::UnknownPoWChainRef)); +} + +#[test] +fn test_block_validation_invalid_unknown_parent_hash() { + let params = get_simple_params(); + + let mutator = |mut block: Block, attester_map, proposer_map, stores| { + block.parent_hash = Hash256::from("unknown parent block".as_bytes()); + (block, attester_map, proposer_map, stores) + }; + + let status = run_block_validation_scenario( + ¶ms, + mutator); + + assert_eq!(status, Err(SszBlockValidationError::UnknownParentHash)); +} + +#[test] +fn test_block_validation_invalid_1st_attestation_signature() { + let params = get_simple_params(); + + let mutator = |mut block: Block, attester_map, proposer_map, stores| { + /* + * Set the second attestaion record to have an invalid signature. + */ + block.attestations[0].aggregate_sig = AggregateSignature::new(); + (block, attester_map, proposer_map, stores) + }; + + let status = run_block_validation_scenario( + ¶ms, + mutator); + + assert_eq!(status, Err(SszBlockValidationError::AttestationValidationError( + AttestationValidationError::BadAggregateSignature))); +} + +#[test] +fn test_block_validation_invalid_no_parent_proposer_signature() { + let params = get_simple_params(); + + let mutator = |block: Block, attester_map, mut proposer_map: ProposerMap, stores: TestStore| { + /* + * Set the proposer for this slot to be a validator that does not exist. + */ + let ssz = stores.block.get_serialized_block(&block.parent_hash.as_ref()).unwrap().unwrap(); + let parent_block_slot = SszBlock::from_slice(&ssz[..]).unwrap().slot_number(); + proposer_map.insert(parent_block_slot, params.total_validators + 1); + (block, attester_map, proposer_map, stores) + }; + + let status = run_block_validation_scenario( + ¶ms, + mutator); + + assert_eq!(status, Err(SszBlockValidationError::NoProposerSignature)); +} + +#[test] +fn test_block_validation_invalid_bad_proposer_map() { + let params = get_simple_params(); + + let mutator = |block, attester_map, _, stores| { + /* + * Initialize a new, empty proposer map + */ + let proposer_map = ProposerMap::new(); + (block, attester_map, proposer_map, stores) + }; + + let status = run_block_validation_scenario( + ¶ms, + mutator); + + assert_eq!(status, Err(SszBlockValidationError::BadProposerMap)); +} + +#[test] +fn test_block_validation_invalid_2nd_attestation_signature() { + let params = get_simple_params(); + + let mutator = |mut block: Block, attester_map, proposer_map, stores| { + /* + * Set the second attestaion record to have an invalid signature. + */ + block.attestations[1].aggregate_sig = AggregateSignature::new(); + (block, attester_map, proposer_map, stores) + }; + + let status = run_block_validation_scenario( + ¶ms, + mutator); + + assert_eq!(status, Err(SszBlockValidationError::AttestationValidationError( + AttestationValidationError::BadAggregateSignature))); +} diff --git a/beacon_chain/validation/tests/main.rs b/beacon_chain/validation/tests/main.rs new file mode 100644 index 0000000000..a68931ed74 --- /dev/null +++ b/beacon_chain/validation/tests/main.rs @@ -0,0 +1,20 @@ +extern crate validation; + +extern crate bls; +extern crate db; +extern crate hashing; +extern crate ssz; +extern crate ssz_helpers; +extern crate types; + +#[cfg(test)] +mod attestation_validation; +#[cfg(test)] +mod block_validation; + +/* +use lighthouse::bls; +use lighthouse::db; +use lighthouse::state; +use lighthouse::utils; +*/