diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 1065f661d6..e6fd2a1342 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -15,9 +15,7 @@ use state_processing::{ use std::sync::Arc; use types::{ readers::{BeaconBlockReader, BeaconStateReader}, - AttestationData, BeaconBlock, BeaconBlockBody, BeaconState, BeaconStateError, ChainSpec, - Crosslink, Deposit, Epoch, Eth1Data, FreeAttestation, Hash256, PublicKey, RelativeEpoch, - Signature, Slot, + *, }; #[derive(Debug, PartialEq)] @@ -66,6 +64,9 @@ pub struct BeaconChain { pub state_store: Arc>, pub slot_clock: U, pub attestation_aggregator: RwLock, + pub deposits_for_inclusion: RwLock>, + pub proposer_slashings_for_inclusion: RwLock>, + pub attester_slashings_for_inclusion: RwLock>, canonical_head: RwLock, finalized_head: RwLock, pub state: RwLock, @@ -132,6 +133,9 @@ where state_store, slot_clock, attestation_aggregator, + deposits_for_inclusion: RwLock::new(vec![]), + proposer_slashings_for_inclusion: RwLock::new(vec![]), + attester_slashings_for_inclusion: RwLock::new(vec![]), state: RwLock::new(genesis_state), finalized_head, canonical_head, @@ -364,6 +368,128 @@ where Ok(aggregation_outcome) } + /// Accept some deposit and queue it for inclusion in an appropriate block. + pub fn receive_deposit_for_inclusion(&self, deposit: Deposit) { + // TODO: deposits are not check for validity; check them. + self.deposits_for_inclusion.write().push(deposit); + } + + /// Return a vec of deposits suitable for inclusion in some block. + pub fn get_deposits_for_block(&self) -> Vec { + // TODO: deposits are indiscriminately included; check them for validity. + self.deposits_for_inclusion.read().clone() + } + + /// Takes a list of `Deposits` that were included in recent blocks and removes them from the + /// inclusion queue. + /// + /// This ensures that `Deposits` are not included twice in successive blocks. + pub fn set_deposits_as_included(&self, included_deposits: &[Deposit]) { + // TODO: method does not take forks into account; consider this. + let mut indices_to_delete = vec![]; + + for included in included_deposits { + for (i, for_inclusion) in self.deposits_for_inclusion.read().iter().enumerate() { + if included == for_inclusion { + indices_to_delete.push(i); + } + } + } + + let deposits_for_inclusion = &mut self.deposits_for_inclusion.write(); + for i in indices_to_delete { + deposits_for_inclusion.remove(i); + } + } + + /// Accept some proposer slashing and queue it for inclusion in an appropriate block. + pub fn receive_proposer_slashing_for_inclusion(&self, proposer_slashing: ProposerSlashing) { + // TODO: proposer_slashings are not check for validity; check them. + self.proposer_slashings_for_inclusion + .write() + .push(proposer_slashing); + } + + /// Return a vec of proposer slashings suitable for inclusion in some block. + pub fn get_proposer_slashings_for_block(&self) -> Vec { + // TODO: proposer_slashings are indiscriminately included; check them for validity. + self.proposer_slashings_for_inclusion.read().clone() + } + + /// Takes a list of `ProposerSlashings` that were included in recent blocks and removes them + /// from the inclusion queue. + /// + /// This ensures that `ProposerSlashings` are not included twice in successive blocks. + pub fn set_proposer_slashings_as_included( + &self, + included_proposer_slashings: &[ProposerSlashing], + ) { + // TODO: method does not take forks into account; consider this. + let mut indices_to_delete = vec![]; + + for included in included_proposer_slashings { + for (i, for_inclusion) in self + .proposer_slashings_for_inclusion + .read() + .iter() + .enumerate() + { + if included == for_inclusion { + indices_to_delete.push(i); + } + } + } + + let proposer_slashings_for_inclusion = &mut self.proposer_slashings_for_inclusion.write(); + for i in indices_to_delete { + proposer_slashings_for_inclusion.remove(i); + } + } + + /// Accept some attester slashing and queue it for inclusion in an appropriate block. + pub fn receive_attester_slashing_for_inclusion(&self, attester_slashing: AttesterSlashing) { + // TODO: attester_slashings are not check for validity; check them. + self.attester_slashings_for_inclusion + .write() + .push(attester_slashing); + } + + /// Return a vec of attester slashings suitable for inclusion in some block. + pub fn get_attester_slashings_for_block(&self) -> Vec { + // TODO: attester_slashings are indiscriminately included; check them for validity. + self.attester_slashings_for_inclusion.read().clone() + } + + /// Takes a list of `AttesterSlashings` that were included in recent blocks and removes them + /// from the inclusion queue. + /// + /// This ensures that `AttesterSlashings` are not included twice in successive blocks. + pub fn set_attester_slashings_as_included( + &self, + included_attester_slashings: &[AttesterSlashing], + ) { + // TODO: method does not take forks into account; consider this. + let mut indices_to_delete = vec![]; + + for included in included_attester_slashings { + for (i, for_inclusion) in self + .attester_slashings_for_inclusion + .read() + .iter() + .enumerate() + { + if included == for_inclusion { + indices_to_delete.push(i); + } + } + } + + let attester_slashings_for_inclusion = &mut self.attester_slashings_for_inclusion.write(); + for i in indices_to_delete { + attester_slashings_for_inclusion.remove(i); + } + } + /// Dumps the entire canonical chain, from the head to genesis to a vector for analysis. /// /// This could be a very expensive operation and should only be done in testing/analysis @@ -412,6 +538,8 @@ where last_slot = slot; } + dump.reverse(); + Ok(dump) } @@ -488,6 +616,11 @@ where self.block_store.put(&block_root, &ssz_encode(&block)[..])?; self.state_store.put(&state_root, &ssz_encode(&state)[..])?; + // Update the inclusion queues so they aren't re-submitted. + self.set_deposits_as_included(&block.body.deposits[..]); + self.set_proposer_slashings_as_included(&block.body.proposer_slashings[..]); + self.set_attester_slashings_as_included(&block.body.attester_slashings[..]); + // run the fork_choice add_block logic self.fork_choice .write() @@ -500,7 +633,7 @@ where if self.head().beacon_block_root == parent_block_root { self.update_canonical_head(block.clone(), block_root, state.clone(), state_root); // Update the local state variable. - *self.state.write() = state.clone(); + *self.state.write() = state; } Ok(BlockProcessingOutcome::ValidBlock(ValidBlock::Processed)) @@ -541,10 +674,10 @@ where }, signature: self.spec.empty_signature.clone(), // To be completed by a validator. body: BeaconBlockBody { - proposer_slashings: vec![], - attester_slashings: vec![], + proposer_slashings: self.get_proposer_slashings_for_block(), + attester_slashings: self.get_attester_slashings_for_block(), attestations, - deposits: vec![], + deposits: self.get_deposits_for_block(), exits: vec![], }, }; @@ -553,7 +686,7 @@ where let result = state.per_block_processing_without_verifying_block_signature(&block, &self.spec); - trace!( + debug!( "BeaconNode::produce_block: state processing result: {:?}", result ); diff --git a/beacon_node/beacon_chain/test_harness/Cargo.toml b/beacon_node/beacon_chain/test_harness/Cargo.toml index 657cc79552..bd7a58270b 100644 --- a/beacon_node/beacon_chain/test_harness/Cargo.toml +++ b/beacon_node/beacon_chain/test_harness/Cargo.toml @@ -4,6 +4,14 @@ version = "0.1.0" authors = ["Paul Hauner "] edition = "2018" +[[bin]] +name = "test_harness" +path = "src/bin.rs" + +[lib] +name = "test_harness" +path = "src/lib.rs" + [[bench]] name = "state_transition" harness = false @@ -18,6 +26,7 @@ beacon_chain = { path = "../../beacon_chain" } block_proposer = { path = "../../../eth2/block_proposer" } bls = { path = "../../../eth2/utils/bls" } boolean-bitfield = { path = "../../../eth2/utils/boolean-bitfield" } +clap = "2.32.0" db = { path = "../../db" } parking_lot = "0.7" failure = "0.1" @@ -33,3 +42,4 @@ serde_json = "1.0" slot_clock = { path = "../../../eth2/utils/slot_clock" } ssz = { path = "../../../eth2/utils/ssz" } types = { path = "../../../eth2/types" } +yaml-rust = "0.4.2" diff --git a/beacon_node/beacon_chain/test_harness/README.md b/beacon_node/beacon_chain/test_harness/README.md new file mode 100644 index 0000000000..12cbbe0627 --- /dev/null +++ b/beacon_node/beacon_chain/test_harness/README.md @@ -0,0 +1,150 @@ +# Test Harness + +Provides a testing environment for the `BeaconChain`, `Attester` and `BlockProposer` objects. + +This environment bypasses networking and client run-times and connects the `Attester` and `Proposer` +directly to the `BeaconChain` via an `Arc`. + +The `BeaconChainHarness` contains a single `BeaconChain` instance and many `ValidatorHarness` +instances. All of the `ValidatorHarness` instances work to advance the `BeaconChain` by +producing blocks and attestations. + +The crate consists of a library and binary, examples for using both are +described below. + +## YAML + +Both the library and the binary are capable of parsing tests from a YAML file, +in fact this is the sole purpose of the binary. + +You can find YAML test cases [here](specs/). An example is included below: + +```yaml +title: Validator Registry Tests +summary: Tests deposit and slashing effects on validator registry. +test_suite: validator_registry +fork: tchaikovsky +version: 1.0 +test_cases: + - config: + epoch_length: 64 + deposits_for_chain_start: 1000 + num_slots: 64 + skip_slots: [2, 3] + deposits: + # At slot 1, create a new validator deposit of 32 ETH. + - slot: 1 + amount: 32 + # Trigger more deposits... + - slot: 3 + amount: 32 + - slot: 5 + amount: 32 + proposer_slashings: + # At slot 2, trigger a proposer slashing for validator #42. + - slot: 2 + validator_index: 42 + # Trigger another slashing... + - slot: 8 + validator_index: 13 + attester_slashings: + # At slot 2, trigger an attester slashing for validators #11 and #12. + - slot: 2 + validator_indices: [11, 12] + # Trigger another slashing... + - slot: 5 + validator_indices: [14] + results: + num_skipped_slots: 2 + states: + - slot: 63 + num_validators: 1003 + slashed_validators: [11, 12, 13, 14, 42] + exited_validators: [] + +``` + +Thanks to [prsym](http://github.com/prysmaticlabs/prysm) for coming up with the +base YAML format. + +### Notes + +Wherever `slot` is used, it is actually the "slot height", or slots since +genesis. This allows the tests to disregard the `GENESIS_EPOCH`. + +### Differences from Prysmatic's format + +1. The detail for `deposits`, `proposer_slashings` and `attester_slashings` is + ommitted from the test specification. It assumed they should be valid + objects. +2. There is a `states` list in `results` that runs checks against any state + specified by a `slot` number. This is in contrast to the variables in + `results` that assume the last (highest) state should be inspected. + +#### Reasoning + +Respective reasonings for above changes: + +1. This removes the concerns of the actual object structure from the tests. + This allows for more variation in the deposits/slashings objects without + needing to update the tests. Also, it makes it makes it easier to create + tests. +2. This gives more fine-grained control over the tests. It allows for checking + that certain events happened at certain times whilst making the tests only + slightly more verbose. + +_Notes: it may be useful to add an extra field to each slashing type to +indicate if it should be valid or not. It also may be useful to add an option +for double-vote/surround-vote attester slashings. The `amount` field was left +on `deposits` as it changes the behaviour of state significantly._ + +## Binary Usage Example + +Follow these steps to run as a binary: + +1. Navigate to the root of this crate (where this readme is located) +2. Run `$ cargo run --release -- --yaml examples/validator_registry.yaml` + +_Note: the `--release` flag builds the binary without all the debugging +instrumentation. The test is much faster built using `--release`. As is +customary in cargo, the flags before `--` are passed to cargo and the flags +after are passed to the binary._ + +### CLI Options + +``` +Lighthouse Test Harness Runner 0.0.1 +Sigma Prime +Runs `test_harness` using a YAML test_case. + +USAGE: + test_harness --log-level --yaml + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + --log-level Logging level. [default: debug] [possible values: error, warn, info, debug, trace] + --yaml YAML file test_case. +``` + + +## Library Usage Example + +```rust +use test_harness::BeaconChainHarness; +use types::ChainSpec; + +let validator_count = 8; +let spec = ChainSpec::few_validators(); + +let mut harness = BeaconChainHarness::new(spec, validator_count); + +harness.advance_chain_with_block(); + +let chain = harness.chain_dump().unwrap(); + +// One block should have been built on top of the genesis block. +assert_eq!(chain.len(), 2); +``` diff --git a/beacon_node/beacon_chain/test_harness/specs/validator_registry.yaml b/beacon_node/beacon_chain/test_harness/specs/validator_registry.yaml new file mode 100644 index 0000000000..b7fdda9bf7 --- /dev/null +++ b/beacon_node/beacon_chain/test_harness/specs/validator_registry.yaml @@ -0,0 +1,42 @@ +title: Validator Registry Tests +summary: Tests deposit and slashing effects on validator registry. +test_suite: validator_registry +fork: tchaikovsky +version: 1.0 +test_cases: + - config: + epoch_length: 64 + deposits_for_chain_start: 1000 + num_slots: 64 + skip_slots: [2, 3] + deposits: + # At slot 1, create a new validator deposit of 32 ETH. + - slot: 1 + amount: 32 + # Trigger more deposits... + - slot: 3 + amount: 32 + - slot: 5 + amount: 32 + proposer_slashings: + # At slot 2, trigger a proposer slashing for validator #42. + - slot: 2 + validator_index: 42 + # Trigger another slashing... + - slot: 8 + validator_index: 13 + attester_slashings: + # At slot 2, trigger an attester slashing for validators #11 and #12. + - slot: 2 + validator_indices: [11, 12] + # Trigger another slashing... + - slot: 5 + validator_indices: [14] + results: + num_skipped_slots: 2 + states: + - slot: 63 + num_validators: 1003 + slashed_validators: [11, 12, 13, 14, 42] + exited_validators: [] + diff --git a/beacon_node/beacon_chain/test_harness/src/beacon_chain_harness.rs b/beacon_node/beacon_chain/test_harness/src/beacon_chain_harness.rs index d3bd444d1a..2f375f7faf 100644 --- a/beacon_node/beacon_chain/test_harness/src/beacon_chain_harness.rs +++ b/beacon_node/beacon_chain/test_harness/src/beacon_chain_harness.rs @@ -11,14 +11,9 @@ use log::debug; use rayon::prelude::*; use slot_clock::TestingSlotClock; use std::collections::HashSet; -use std::fs::File; -use std::io::prelude::*; use std::iter::FromIterator; use std::sync::Arc; -use types::{ - BeaconBlock, ChainSpec, Deposit, DepositData, DepositInput, Eth1Data, FreeAttestation, Hash256, - Keypair, Slot, -}; +use types::*; /// The beacon chain harness simulates a single beacon node with `validator_count` validators connected /// to it. Each validator is provided a borrow to the beacon chain, where it may read @@ -245,6 +240,59 @@ impl BeaconChainHarness { debug!("Free attestations processed."); } + /// Signs a message using some validators secret key with the `Fork` info from the latest state + /// of the `BeaconChain`. + /// + /// Useful for producing slashable messages and other objects that `BeaconChainHarness` does + /// not produce naturally. + pub fn validator_sign( + &self, + validator_index: usize, + message: &[u8], + epoch: Epoch, + domain_type: u64, + ) -> Option { + let validator = self.validators.get(validator_index)?; + + let domain = self + .beacon_chain + .state + .read() + .fork + .get_domain(epoch, domain_type); + + Some(Signature::new(message, domain, &validator.keypair.sk)) + } + + /// Submit a deposit to the `BeaconChain` and, if given a keypair, create a new + /// `ValidatorHarness` instance for this validator. + /// + /// If a new `ValidatorHarness` was created, the validator should become fully operational as + /// if the validator were created during `BeaconChainHarness` instantiation. + pub fn add_deposit(&mut self, deposit: Deposit, keypair: Option) { + self.beacon_chain.receive_deposit_for_inclusion(deposit); + + // If a keypair is present, add a new `ValidatorHarness` to the rig. + if let Some(keypair) = keypair { + let validator = + ValidatorHarness::new(keypair, self.beacon_chain.clone(), self.spec.clone()); + self.validators.push(validator); + } + } + + /// Submit a proposer slashing to the `BeaconChain` for inclusion in some block. + pub fn add_proposer_slashing(&mut self, proposer_slashing: ProposerSlashing) { + self.beacon_chain + .receive_proposer_slashing_for_inclusion(proposer_slashing); + } + + /// Submit an attester slashing to the `BeaconChain` for inclusion in some block. + pub fn add_attester_slashing(&mut self, attester_slashing: AttesterSlashing) { + self.beacon_chain + .receive_attester_slashing_for_inclusion(attester_slashing); + } + + /// Executes the fork choice rule on the `BeaconChain`, selecting a new canonical head. pub fn run_fork_choice(&mut self) { self.beacon_chain.fork_choice().unwrap() } @@ -253,12 +301,4 @@ impl BeaconChainHarness { pub fn chain_dump(&self) -> Result, BeaconChainError> { self.beacon_chain.chain_dump() } - - /// Write the output of `chain_dump` to a JSON file. - pub fn dump_to_file(&self, filename: String, chain_dump: &[CheckPoint]) { - let json = serde_json::to_string(chain_dump).unwrap(); - let mut file = File::create(filename).unwrap(); - file.write_all(json.as_bytes()) - .expect("Failed writing dump to file."); - } } diff --git a/beacon_node/beacon_chain/test_harness/src/bin.rs b/beacon_node/beacon_chain/test_harness/src/bin.rs new file mode 100644 index 0000000000..283cb0dfa3 --- /dev/null +++ b/beacon_node/beacon_chain/test_harness/src/bin.rs @@ -0,0 +1,69 @@ +use clap::{App, Arg}; +use env_logger::{Builder, Env}; +use std::{fs::File, io::prelude::*}; +use test_case::TestCase; +use yaml_rust::YamlLoader; + +mod beacon_chain_harness; +mod test_case; +mod validator_harness; + +use validator_harness::ValidatorHarness; + +fn main() { + let matches = App::new("Lighthouse Test Harness Runner") + .version("0.0.1") + .author("Sigma Prime ") + .about("Runs `test_harness` using a YAML test_case.") + .arg( + Arg::with_name("yaml") + .long("yaml") + .value_name("FILE") + .help("YAML file test_case.") + .required(true), + ) + .arg( + Arg::with_name("log") + .long("log-level") + .value_name("LOG_LEVEL") + .help("Logging level.") + .possible_values(&["error", "warn", "info", "debug", "trace"]) + .default_value("debug") + .required(true), + ) + .get_matches(); + + if let Some(log_level) = matches.value_of("log") { + Builder::from_env(Env::default().default_filter_or(log_level)).init(); + } + + if let Some(yaml_file) = matches.value_of("yaml") { + let docs = { + let mut file = File::open(yaml_file).unwrap(); + + let mut yaml_str = String::new(); + file.read_to_string(&mut yaml_str).unwrap(); + + YamlLoader::load_from_str(&yaml_str).unwrap() + }; + + for doc in &docs { + // For each `test_cases` YAML in the document, build a `TestCase`, execute it and + // assert that the execution result matches the test_case description. + // + // In effect, for each `test_case` a new `BeaconChainHarness` is created from genesis + // and a new `BeaconChain` is built as per the test_case. + // + // After the `BeaconChain` has been built out as per the test_case, a dump of all blocks + // and states in the chain is obtained and checked against the `results` specified in + // the `test_case`. + // + // If any of the expectations in the results are not met, the process + // panics with a message. + for test_case in doc["test_cases"].as_vec().unwrap() { + let test_case = TestCase::from_yaml(test_case); + test_case.assert_result_valid(test_case.execute()) + } + } + } +} diff --git a/beacon_node/beacon_chain/test_harness/src/lib.rs b/beacon_node/beacon_chain/test_harness/src/lib.rs index b04fc69963..0703fd4a5a 100644 --- a/beacon_node/beacon_chain/test_harness/src/lib.rs +++ b/beacon_node/beacon_chain/test_harness/src/lib.rs @@ -1,4 +1,32 @@ +//! Provides a testing environment for the `BeaconChain`, `Attester` and `BlockProposer` objects. +//! +//! This environment bypasses networking and client run-times and connects the `Attester` and `Proposer` +//! directly to the `BeaconChain` via an `Arc`. +//! +//! The `BeaconChainHarness` contains a single `BeaconChain` instance and many `ValidatorHarness` +//! instances. All of the `ValidatorHarness` instances work to advance the `BeaconChain` by +//! producing blocks and attestations. +//! +//! Example: +//! ``` +//! use test_harness::BeaconChainHarness; +//! use types::ChainSpec; +//! +//! let validator_count = 8; +//! let spec = ChainSpec::few_validators(); +//! +//! let mut harness = BeaconChainHarness::new(spec, validator_count); +//! +//! harness.advance_chain_with_block(); +//! +//! let chain = harness.chain_dump().unwrap(); +//! +//! // One block should have been built on top of the genesis block. +//! assert_eq!(chain.len(), 2); +//! ``` + mod beacon_chain_harness; +pub mod test_case; mod validator_harness; pub use self::beacon_chain_harness::BeaconChainHarness; diff --git a/beacon_node/beacon_chain/test_harness/src/test_case/config.rs b/beacon_node/beacon_chain/test_harness/src/test_case/config.rs new file mode 100644 index 0000000000..8c88ee5d19 --- /dev/null +++ b/beacon_node/beacon_chain/test_harness/src/test_case/config.rs @@ -0,0 +1,109 @@ +use super::yaml_helpers::{as_u64, as_usize, as_vec_u64}; +use bls::create_proof_of_possession; +use types::*; +use yaml_rust::Yaml; + +pub type DepositTuple = (u64, Deposit, Keypair); +pub type ProposerSlashingTuple = (u64, u64); +pub type AttesterSlashingTuple = (u64, Vec); + +/// Defines the execution of a `BeaconStateHarness` across a series of slots. +#[derive(Debug)] +pub struct Config { + /// Initial validators. + pub deposits_for_chain_start: usize, + /// Number of slots in an epoch. + pub epoch_length: Option, + /// Number of slots to build before ending execution. + pub num_slots: u64, + /// Number of slots that should be skipped due to inactive validator. + pub skip_slots: Option>, + /// Deposits to be included during execution. + pub deposits: Option>, + /// Proposer slashings to be included during execution. + pub proposer_slashings: Option>, + /// Attester slashings to be including during execution. + pub attester_slashings: Option>, +} + +impl Config { + /// Load from a YAML document. + /// + /// Expects to receive the `config` section of the document. + pub fn from_yaml(yaml: &Yaml) -> Self { + Self { + deposits_for_chain_start: as_usize(&yaml, "deposits_for_chain_start") + .expect("Must specify validator count"), + epoch_length: as_u64(&yaml, "epoch_length"), + num_slots: as_u64(&yaml, "num_slots").expect("Must specify `config.num_slots`"), + skip_slots: as_vec_u64(yaml, "skip_slots"), + deposits: parse_deposits(&yaml), + proposer_slashings: parse_proposer_slashings(&yaml), + attester_slashings: parse_attester_slashings(&yaml), + } + } +} + +/// Parse the `attester_slashings` section of the YAML document. +fn parse_attester_slashings(yaml: &Yaml) -> Option> { + let mut slashings = vec![]; + + for slashing in yaml["attester_slashings"].as_vec()? { + let slot = as_u64(slashing, "slot").expect("Incomplete attester_slashing (slot)"); + let validator_indices = as_vec_u64(slashing, "validator_indices") + .expect("Incomplete attester_slashing (validator_indices)"); + + slashings.push((slot, validator_indices)); + } + + Some(slashings) +} + +/// Parse the `proposer_slashings` section of the YAML document. +fn parse_proposer_slashings(yaml: &Yaml) -> Option> { + let mut slashings = vec![]; + + for slashing in yaml["proposer_slashings"].as_vec()? { + let slot = as_u64(slashing, "slot").expect("Incomplete proposer slashing (slot)_"); + let validator_index = as_u64(slashing, "validator_index") + .expect("Incomplete proposer slashing (validator_index)"); + + slashings.push((slot, validator_index)); + } + + Some(slashings) +} + +/// Parse the `deposits` section of the YAML document. +fn parse_deposits(yaml: &Yaml) -> Option> { + let mut deposits = vec![]; + + for deposit in yaml["deposits"].as_vec()? { + let keypair = Keypair::random(); + let proof_of_possession = create_proof_of_possession(&keypair); + + let slot = as_u64(deposit, "slot").expect("Incomplete deposit (slot)"); + let amount = + as_u64(deposit, "amount").expect("Incomplete deposit (amount)") * 1_000_000_000; + + let deposit = Deposit { + // Note: `branch` and `index` will need to be updated once the spec defines their + // validity. + branch: vec![], + index: 0, + deposit_data: DepositData { + amount, + timestamp: 1, + deposit_input: DepositInput { + pubkey: keypair.pk.clone(), + withdrawal_credentials: Hash256::zero(), + proof_of_possession, + }, + }, + }; + + deposits.push((slot, deposit, keypair)); + } + + Some(deposits) +} diff --git a/beacon_node/beacon_chain/test_harness/src/test_case/mod.rs b/beacon_node/beacon_chain/test_harness/src/test_case/mod.rs new file mode 100644 index 0000000000..f6d8d42e8d --- /dev/null +++ b/beacon_node/beacon_chain/test_harness/src/test_case/mod.rs @@ -0,0 +1,215 @@ +//! Defines execution and testing specs for a `BeaconChainHarness` instance. Supports loading from +//! a YAML file. + +use crate::beacon_chain_harness::BeaconChainHarness; +use beacon_chain::CheckPoint; +use log::{info, warn}; +use types::*; +use types::{ + attester_slashing::AttesterSlashingBuilder, proposer_slashing::ProposerSlashingBuilder, +}; +use yaml_rust::Yaml; + +mod config; +mod results; +mod state_check; +mod yaml_helpers; + +pub use config::Config; +pub use results::Results; +pub use state_check::StateCheck; + +/// Defines the execution and testing of a `BeaconChainHarness` instantiation. +/// +/// Typical workflow is: +/// +/// 1. Instantiate the `TestCase` from YAML: `let test_case = TestCase::from_yaml(&my_yaml);` +/// 2. Execute the test_case: `let result = test_case.execute();` +/// 3. Test the results against the test_case: `test_case.assert_result_valid(result);` +#[derive(Debug)] +pub struct TestCase { + /// Defines the execution. + pub config: Config, + /// Defines tests to run against the execution result. + pub results: Results, +} + +/// The result of executing a `TestCase`. +/// +pub struct ExecutionResult { + /// The canonical beacon chain generated from the execution. + pub chain: Vec, + /// The spec used for execution. + pub spec: ChainSpec, +} + +impl TestCase { + /// Load the test case from a YAML document. + pub fn from_yaml(test_case: &Yaml) -> Self { + Self { + results: Results::from_yaml(&test_case["results"]), + config: Config::from_yaml(&test_case["config"]), + } + } + + /// Return a `ChainSpec::foundation()`. + /// + /// If specified in `config`, returns it with a modified `epoch_length`. + fn spec(&self) -> ChainSpec { + let mut spec = ChainSpec::foundation(); + + if let Some(n) = self.config.epoch_length { + spec.epoch_length = n; + } + + spec + } + + /// Executes the test case, returning an `ExecutionResult`. + pub fn execute(&self) -> ExecutionResult { + let spec = self.spec(); + let validator_count = self.config.deposits_for_chain_start; + let slots = self.config.num_slots; + + info!( + "Building BeaconChainHarness with {} validators...", + validator_count + ); + + let mut harness = BeaconChainHarness::new(spec, validator_count); + + info!("Starting simulation across {} slots...", slots); + + // -1 slots because genesis counts as a slot. + for slot_height in 0..slots - 1 { + // Feed deposits to the BeaconChain. + if let Some(ref deposits) = self.config.deposits { + for (slot, deposit, keypair) in deposits { + if *slot == slot_height { + info!("Including deposit at slot height {}.", slot_height); + harness.add_deposit(deposit.clone(), Some(keypair.clone())); + } + } + } + + // Feed proposer slashings to the BeaconChain. + if let Some(ref slashings) = self.config.proposer_slashings { + for (slot, validator_index) in slashings { + if *slot == slot_height { + info!( + "Including proposer slashing at slot height {} for validator #{}.", + slot_height, validator_index + ); + let slashing = build_proposer_slashing(&harness, *validator_index); + harness.add_proposer_slashing(slashing); + } + } + } + + // Feed attester slashings to the BeaconChain. + if let Some(ref slashings) = self.config.attester_slashings { + for (slot, validator_indices) in slashings { + if *slot == slot_height { + info!( + "Including attester slashing at slot height {} for validators {:?}.", + slot_height, validator_indices + ); + let slashing = + build_double_vote_attester_slashing(&harness, &validator_indices[..]); + harness.add_attester_slashing(slashing); + } + } + } + + // Build a block or skip a slot. + match self.config.skip_slots { + Some(ref skip_slots) if skip_slots.contains(&slot_height) => { + warn!("Skipping slot at height {}.", slot_height); + harness.increment_beacon_chain_slot(); + } + _ => { + info!("Producing block at slot height {}.", slot_height); + harness.advance_chain_with_block(); + } + } + } + + harness.run_fork_choice(); + + info!("Test execution complete!"); + + info!("Building chain dump for analysis..."); + + ExecutionResult { + chain: harness.chain_dump().expect("Chain dump failed."), + spec: (*harness.spec).clone(), + } + } + + /// Checks that the `ExecutionResult` is consistent with the specifications in `self.results`. + /// + /// # Panics + /// + /// Panics with a message if any result does not match exepectations. + pub fn assert_result_valid(&self, execution_result: ExecutionResult) { + info!("Verifying test results..."); + let spec = &execution_result.spec; + + if let Some(num_skipped_slots) = self.results.num_skipped_slots { + assert_eq!( + execution_result.chain.len(), + self.config.num_slots as usize - num_skipped_slots, + "actual skipped slots != expected." + ); + info!( + "OK: Chain length is {} ({} skipped slots).", + execution_result.chain.len(), + num_skipped_slots + ); + } + + if let Some(ref state_checks) = self.results.state_checks { + for checkpoint in &execution_result.chain { + let state = &checkpoint.beacon_state; + + for state_check in state_checks { + let adjusted_state_slot = + state.slot - spec.genesis_epoch.start_slot(spec.epoch_length); + + if state_check.slot == adjusted_state_slot { + state_check.assert_valid(state, spec); + } + } + } + } + } +} + +/// Builds an `AttesterSlashing` for some `validator_indices`. +/// +/// Signs the message using a `BeaconChainHarness`. +fn build_double_vote_attester_slashing( + harness: &BeaconChainHarness, + validator_indices: &[u64], +) -> AttesterSlashing { + let signer = |validator_index: u64, message: &[u8], epoch: Epoch, domain: u64| { + harness + .validator_sign(validator_index as usize, message, epoch, domain) + .expect("Unable to sign AttesterSlashing") + }; + + AttesterSlashingBuilder::double_vote(validator_indices, signer, &harness.spec) +} + +/// Builds an `ProposerSlashing` for some `validator_index`. +/// +/// Signs the message using a `BeaconChainHarness`. +fn build_proposer_slashing(harness: &BeaconChainHarness, validator_index: u64) -> ProposerSlashing { + let signer = |validator_index: u64, message: &[u8], epoch: Epoch, domain: u64| { + harness + .validator_sign(validator_index as usize, message, epoch, domain) + .expect("Unable to sign AttesterSlashing") + }; + + ProposerSlashingBuilder::double_vote(validator_index, signer, &harness.spec) +} diff --git a/beacon_node/beacon_chain/test_harness/src/test_case/results.rs b/beacon_node/beacon_chain/test_harness/src/test_case/results.rs new file mode 100644 index 0000000000..596418c0fd --- /dev/null +++ b/beacon_node/beacon_chain/test_harness/src/test_case/results.rs @@ -0,0 +1,34 @@ +use super::state_check::StateCheck; +use super::yaml_helpers::as_usize; +use yaml_rust::Yaml; + +/// A series of tests to be carried out upon an `ExecutionResult`, returned from executing a +/// `TestCase`. +#[derive(Debug)] +pub struct Results { + pub num_skipped_slots: Option, + pub state_checks: Option>, +} + +impl Results { + /// Load from a YAML document. + /// + /// Expects the `results` section of the YAML document. + pub fn from_yaml(yaml: &Yaml) -> Self { + Self { + num_skipped_slots: as_usize(yaml, "num_skipped_slots"), + state_checks: parse_state_checks(yaml), + } + } +} + +/// Parse the `state_checks` section of the YAML document. +fn parse_state_checks(yaml: &Yaml) -> Option> { + let mut states = vec![]; + + for state_yaml in yaml["states"].as_vec()? { + states.push(StateCheck::from_yaml(state_yaml)); + } + + Some(states) +} diff --git a/beacon_node/beacon_chain/test_harness/src/test_case/state_check.rs b/beacon_node/beacon_chain/test_harness/src/test_case/state_check.rs new file mode 100644 index 0000000000..90c6228947 --- /dev/null +++ b/beacon_node/beacon_chain/test_harness/src/test_case/state_check.rs @@ -0,0 +1,98 @@ +use super::yaml_helpers::{as_u64, as_usize, as_vec_u64}; +use log::info; +use types::*; +use yaml_rust::Yaml; + +/// Tests to be conducted upon a `BeaconState` object generated during the execution of a +/// `TestCase`. +#[derive(Debug)] +pub struct StateCheck { + /// Checked against `beacon_state.slot`. + pub slot: Slot, + /// Checked against `beacon_state.validator_registry.len()`. + pub num_validators: Option, + /// A list of validator indices which have been penalized. Must be in ascending order. + pub slashed_validators: Option>, + /// A list of validator indices which have been exited. Must be in ascending order. + pub exited_validators: Option>, +} + +impl StateCheck { + /// Load from a YAML document. + /// + /// Expects the `state_check` section of the YAML document. + pub fn from_yaml(yaml: &Yaml) -> Self { + Self { + slot: Slot::from(as_u64(&yaml, "slot").expect("State must specify slot")), + num_validators: as_usize(&yaml, "num_validators"), + slashed_validators: as_vec_u64(&yaml, "slashed_validators"), + exited_validators: as_vec_u64(&yaml, "exited_validators"), + } + } + + /// Performs all checks against a `BeaconState` + /// + /// # Panics + /// + /// Panics with an error message if any test fails. + pub fn assert_valid(&self, state: &BeaconState, spec: &ChainSpec) { + let state_epoch = state.slot.epoch(spec.epoch_length); + + info!("Running state check for slot height {}.", self.slot); + + assert_eq!( + self.slot, + state.slot - spec.genesis_epoch.start_slot(spec.epoch_length), + "State slot is invalid." + ); + + if let Some(num_validators) = self.num_validators { + assert_eq!( + state.validator_registry.len(), + num_validators, + "State validator count != expected." + ); + info!("OK: num_validators = {}.", num_validators); + } + + if let Some(ref slashed_validators) = self.slashed_validators { + let actually_slashed_validators: Vec = state + .validator_registry + .iter() + .enumerate() + .filter_map(|(i, validator)| { + if validator.is_penalized_at(state_epoch) { + Some(i as u64) + } else { + None + } + }) + .collect(); + assert_eq!( + actually_slashed_validators, *slashed_validators, + "Slashed validators != expected." + ); + info!("OK: slashed_validators = {:?}.", slashed_validators); + } + + if let Some(ref exited_validators) = self.exited_validators { + let actually_exited_validators: Vec = state + .validator_registry + .iter() + .enumerate() + .filter_map(|(i, validator)| { + if validator.is_exited_at(state_epoch) { + Some(i as u64) + } else { + None + } + }) + .collect(); + assert_eq!( + actually_exited_validators, *exited_validators, + "Exited validators != expected." + ); + info!("OK: exited_validators = {:?}.", exited_validators); + } + } +} diff --git a/beacon_node/beacon_chain/test_harness/src/test_case/yaml_helpers.rs b/beacon_node/beacon_chain/test_harness/src/test_case/yaml_helpers.rs new file mode 100644 index 0000000000..c499b3c0f9 --- /dev/null +++ b/beacon_node/beacon_chain/test_harness/src/test_case/yaml_helpers.rs @@ -0,0 +1,19 @@ +use yaml_rust::Yaml; + +pub fn as_usize(yaml: &Yaml, key: &str) -> Option { + yaml[key].as_i64().and_then(|n| Some(n as usize)) +} + +pub fn as_u64(yaml: &Yaml, key: &str) -> Option { + yaml[key].as_i64().and_then(|n| Some(n as u64)) +} + +pub fn as_vec_u64(yaml: &Yaml, key: &str) -> Option> { + yaml[key].clone().into_vec().and_then(|vec| { + Some( + vec.iter() + .map(|item| item.as_i64().unwrap() as u64) + .collect(), + ) + }) +} diff --git a/beacon_node/beacon_chain/test_harness/src/validator_harness/local_signer.rs b/beacon_node/beacon_chain/test_harness/src/validator_harness/local_signer.rs index 3f249cb199..803af5045d 100644 --- a/beacon_node/beacon_chain/test_harness/src/validator_harness/local_signer.rs +++ b/beacon_node/beacon_chain/test_harness/src/validator_harness/local_signer.rs @@ -1,27 +1,16 @@ use attester::Signer as AttesterSigner; use block_proposer::Signer as BlockProposerSigner; -use std::sync::RwLock; use types::{Keypair, Signature}; /// A test-only struct used to perform signing for a proposer or attester. pub struct LocalSigner { keypair: Keypair, - should_sign: RwLock, } impl LocalSigner { /// Produce a new TestSigner with signing enabled by default. pub fn new(keypair: Keypair) -> Self { - Self { - keypair, - should_sign: RwLock::new(true), - } - } - - /// If set to `false`, the service will refuse to sign all messages. Otherwise, all messages - /// will be signed. - pub fn enable_signing(&self, enabled: bool) { - *self.should_sign.write().unwrap() = enabled; + Self { keypair } } /// Sign some message. diff --git a/beacon_node/beacon_chain/test_harness/tests/chain.rs b/beacon_node/beacon_chain/test_harness/tests/chain.rs index 1b29a412fe..238e567ad2 100644 --- a/beacon_node/beacon_chain/test_harness/tests/chain.rs +++ b/beacon_node/beacon_chain/test_harness/tests/chain.rs @@ -41,6 +41,4 @@ fn it_can_produce_past_first_epoch_boundary() { let dump = harness.chain_dump().expect("Chain dump failed."); assert_eq!(dump.len() as u64, blocks + 1); // + 1 for genesis block. - - harness.dump_to_file("/tmp/chaindump.json".to_string(), &dump); } diff --git a/eth2/state_processing/src/block_processable.rs b/eth2/state_processing/src/block_processable.rs index effaa65079..32327aad3b 100644 --- a/eth2/state_processing/src/block_processable.rs +++ b/eth2/state_processing/src/block_processable.rs @@ -1,19 +1,14 @@ +use self::verify_slashable_attestation::verify_slashable_attestation; use crate::SlotProcessingError; use hashing::hash; use int_to_bytes::int_to_bytes32; use log::{debug, trace}; use ssz::{ssz_encode, TreeHash}; -use types::{ - AggregatePublicKey, Attestation, BeaconBlock, BeaconState, BeaconStateError, ChainSpec, - Crosslink, Epoch, Exit, Fork, Hash256, PendingAttestation, PublicKey, RelativeEpoch, Signature, -}; +use types::*; + +mod verify_slashable_attestation; -// TODO: define elsehwere. -const DOMAIN_PROPOSAL: u64 = 2; -const DOMAIN_EXIT: u64 = 3; -const DOMAIN_RANDAO: u64 = 4; const PHASE_0_CUSTODY_BIT: bool = false; -const DOMAIN_ATTESTATION: u64 = 1; #[derive(Debug, PartialEq)] pub enum Error { @@ -31,10 +26,13 @@ pub enum Error { BadRandaoSignature, MaxProposerSlashingsExceeded, BadProposerSlashing, + MaxAttesterSlashingsExceed, MaxAttestationsExceeded, + BadAttesterSlashing, InvalidAttestation(AttestationValidationError), NoBlockRoot, MaxDepositsExceeded, + BadDeposit, MaxExitsExceeded, BadExit, BadCustodyReseeds, @@ -89,7 +87,7 @@ impl BlockProcessable for BeaconState { } fn per_block_processing_signature_optional( - state: &mut BeaconState, + mut state: &mut BeaconState, block: &BeaconBlock, verify_block_signature: bool, spec: &ChainSpec, @@ -113,7 +111,7 @@ fn per_block_processing_signature_optional( &block_proposer.pubkey, &block.proposal_root(spec)[..], &block.signature, - get_domain(&state.fork, state.current_epoch(spec), DOMAIN_PROPOSAL) + get_domain(&state.fork, state.current_epoch(spec), spec.domain_proposal) ), Error::BadBlockSignature ); @@ -127,7 +125,7 @@ fn per_block_processing_signature_optional( &block_proposer.pubkey, &int_to_bytes32(state.current_epoch(spec).as_u64()), &block.randao_reveal, - get_domain(&state.fork, state.current_epoch(spec), DOMAIN_RANDAO) + get_domain(&state.fork, state.current_epoch(spec), spec.domain_randao) ), Error::BadRandaoSignature ); @@ -188,7 +186,7 @@ fn per_block_processing_signature_optional( .proposal_data_1 .slot .epoch(spec.epoch_length), - DOMAIN_PROPOSAL + spec.domain_proposal ) ), Error::BadProposerSlashing @@ -204,7 +202,7 @@ fn per_block_processing_signature_optional( .proposal_data_2 .slot .epoch(spec.epoch_length), - DOMAIN_PROPOSAL + spec.domain_proposal ) ), Error::BadProposerSlashing @@ -212,6 +210,17 @@ fn per_block_processing_signature_optional( state.penalize_validator(proposer_slashing.proposer_index as usize, spec)?; } + /* + * Attester slashings + */ + ensure!( + block.body.attester_slashings.len() as u64 <= spec.max_attester_slashings, + Error::MaxAttesterSlashingsExceed + ); + for attester_slashing in &block.body.attester_slashings { + verify_slashable_attestation(&mut state, &attester_slashing, spec)?; + } + /* * Attestations */ @@ -242,7 +251,27 @@ fn per_block_processing_signature_optional( Error::MaxDepositsExceeded ); - // TODO: process deposits. + // TODO: verify deposit merkle branches. + for deposit in &block.body.deposits { + debug!( + "Processing deposit for pubkey {:?}", + deposit.deposit_data.deposit_input.pubkey + ); + state + .process_deposit( + deposit.deposit_data.deposit_input.pubkey.clone(), + deposit.deposit_data.amount, + deposit + .deposit_data + .deposit_input + .proof_of_possession + .clone(), + deposit.deposit_data.deposit_input.withdrawal_credentials, + None, + spec, + ) + .map_err(|_| Error::BadDeposit)?; + } /* * Exits @@ -276,7 +305,7 @@ fn per_block_processing_signature_optional( &validator.pubkey, &exit_message, &exit.signature, - get_domain(&state.fork, exit.epoch, DOMAIN_EXIT) + get_domain(&state.fork, exit.epoch, spec.domain_exit) ), Error::BadProposerSlashing ); @@ -370,11 +399,7 @@ fn validate_attestation_signature_optional( ); let mut group_public_key = AggregatePublicKey::new(); for participant in participants { - group_public_key.add( - state.validator_registry[participant as usize] - .pubkey - .as_raw(), - ) + group_public_key.add(&state.validator_registry[participant as usize].pubkey) } ensure!( attestation.verify_signature( @@ -383,7 +408,7 @@ fn validate_attestation_signature_optional( get_domain( &state.fork, attestation.data.slot.epoch(spec.epoch_length), - DOMAIN_ATTESTATION, + spec.domain_attestation, ) ), AttestationValidationError::BadSignature diff --git a/eth2/state_processing/src/block_processable/verify_slashable_attestation.rs b/eth2/state_processing/src/block_processable/verify_slashable_attestation.rs new file mode 100644 index 0000000000..a406af24ee --- /dev/null +++ b/eth2/state_processing/src/block_processable/verify_slashable_attestation.rs @@ -0,0 +1,61 @@ +use super::Error; +use types::*; + +macro_rules! ensure { + ($condition: expr, $result: expr) => { + if !$condition { + return Err($result); + } + }; +} + +/// Returns `Ok(())` if some `AttesterSlashing` is valid to be included in some `BeaconState`, +/// otherwise returns an `Err`. +pub fn verify_slashable_attestation( + state: &mut BeaconState, + attester_slashing: &AttesterSlashing, + spec: &ChainSpec, +) -> Result<(), Error> { + let slashable_attestation_1 = &attester_slashing.slashable_attestation_1; + let slashable_attestation_2 = &attester_slashing.slashable_attestation_2; + + ensure!( + slashable_attestation_1.data != slashable_attestation_2.data, + Error::BadAttesterSlashing + ); + ensure!( + slashable_attestation_1.is_double_vote(slashable_attestation_2, spec) + | slashable_attestation_1.is_surround_vote(slashable_attestation_2, spec), + Error::BadAttesterSlashing + ); + ensure!( + state.verify_slashable_attestation(&slashable_attestation_1, spec), + Error::BadAttesterSlashing + ); + ensure!( + state.verify_slashable_attestation(&slashable_attestation_2, spec), + Error::BadAttesterSlashing + ); + + let mut slashable_indices = vec![]; + for i in &slashable_attestation_1.validator_indices { + let validator = state + .validator_registry + .get(*i as usize) + .ok_or_else(|| Error::BadAttesterSlashing)?; + + if slashable_attestation_1.validator_indices.contains(&i) + & !validator.is_penalized_at(state.current_epoch(spec)) + { + slashable_indices.push(i); + } + } + + ensure!(!slashable_indices.is_empty(), Error::BadAttesterSlashing); + + for i in slashable_indices { + state.penalize_validator(*i as usize, spec)?; + } + + Ok(()) +} diff --git a/eth2/types/src/attester_slashing.rs b/eth2/types/src/attester_slashing.rs index 2a1df9e0cc..ac75a25626 100644 --- a/eth2/types/src/attester_slashing.rs +++ b/eth2/types/src/attester_slashing.rs @@ -4,6 +4,10 @@ use serde_derive::Serialize; use ssz_derive::{Decode, Encode, TreeHash}; use test_random_derive::TestRandom; +mod builder; + +pub use builder::AttesterSlashingBuilder; + #[derive(Debug, PartialEq, Clone, Serialize, Encode, Decode, TreeHash, TestRandom)] pub struct AttesterSlashing { pub slashable_attestation_1: SlashableAttestation, diff --git a/eth2/types/src/attester_slashing/builder.rs b/eth2/types/src/attester_slashing/builder.rs new file mode 100644 index 0000000000..ed203d6e1f --- /dev/null +++ b/eth2/types/src/attester_slashing/builder.rs @@ -0,0 +1,96 @@ +use crate::*; +use ssz::TreeHash; + +/// Builds an `AttesterSlashing`. +pub struct AttesterSlashingBuilder(); + +impl AttesterSlashingBuilder { + /// Builds an `AttesterSlashing` that is a double vote. + /// + /// The `signer` function is used to sign the double-vote and accepts: + /// + /// - `validator_index: u64` + /// - `message: &[u8]` + /// - `epoch: Epoch` + /// - `domain: u64` + /// + /// Where domain is a domain "constant" (e.g., `spec.domain_attestation`). + pub fn double_vote( + validator_indices: &[u64], + signer: F, + spec: &ChainSpec, + ) -> AttesterSlashing + where + F: Fn(u64, &[u8], Epoch, u64) -> Signature, + { + let double_voted_slot = Slot::new(0); + let shard = 0; + let justified_epoch = Epoch::new(0); + let epoch = Epoch::new(0); + let hash_1 = Hash256::from(&[1][..]); + let hash_2 = Hash256::from(&[2][..]); + + let mut slashable_attestation_1 = SlashableAttestation { + validator_indices: validator_indices.to_vec(), + data: AttestationData { + slot: double_voted_slot, + shard, + beacon_block_root: hash_1, + epoch_boundary_root: hash_1, + shard_block_root: hash_1, + latest_crosslink: Crosslink { + epoch, + shard_block_root: hash_1, + }, + justified_epoch, + justified_block_root: hash_1, + }, + custody_bitfield: Bitfield::new(), + aggregate_signature: AggregateSignature::new(), + }; + + let mut slashable_attestation_2 = SlashableAttestation { + validator_indices: validator_indices.to_vec(), + data: AttestationData { + slot: double_voted_slot, + shard, + beacon_block_root: hash_2, + epoch_boundary_root: hash_2, + shard_block_root: hash_2, + latest_crosslink: Crosslink { + epoch, + shard_block_root: hash_2, + }, + justified_epoch, + justified_block_root: hash_2, + }, + custody_bitfield: Bitfield::new(), + aggregate_signature: AggregateSignature::new(), + }; + + let add_signatures = |attestation: &mut SlashableAttestation| { + for (i, validator_index) in validator_indices.iter().enumerate() { + let attestation_data_and_custody_bit = AttestationDataAndCustodyBit { + data: attestation.data.clone(), + custody_bit: attestation.custody_bitfield.get(i).unwrap(), + }; + let message = attestation_data_and_custody_bit.hash_tree_root(); + let signature = signer( + *validator_index, + &message[..], + epoch, + spec.domain_attestation, + ); + attestation.aggregate_signature.add(&signature); + } + }; + + add_signatures(&mut slashable_attestation_1); + add_signatures(&mut slashable_attestation_2); + + AttesterSlashing { + slashable_attestation_1, + slashable_attestation_2, + } + } +} diff --git a/eth2/types/src/beacon_state.rs b/eth2/types/src/beacon_state.rs index 67cf2341a5..a9e2f2673c 100644 --- a/eth2/types/src/beacon_state.rs +++ b/eth2/types/src/beacon_state.rs @@ -1,14 +1,11 @@ use self::epoch_cache::EpochCache; use crate::test_utils::TestRandom; -use crate::{ - validator::StatusFlags, validator_registry::get_active_validator_indices, AttestationData, - Bitfield, ChainSpec, Crosslink, Deposit, DepositData, DepositInput, Epoch, Eth1Data, - Eth1DataVote, Fork, Hash256, PendingAttestation, PublicKey, Signature, Slot, Validator, -}; +use crate::{validator::StatusFlags, validator_registry::get_active_validator_indices, *}; use bls::verify_proof_of_possession; use honey_badger_split::SplitExt; use log::{debug, error, trace}; use rand::RngCore; +use rayon::prelude::*; use serde_derive::Serialize; use ssz::{hash, Decodable, DecodeError, Encodable, SszStream, TreeHash}; use std::collections::HashMap; @@ -199,10 +196,10 @@ impl BeaconState { let mut genesis_state = BeaconState::genesis_without_validators(genesis_time, latest_eth1_data, spec)?; - trace!("Processing genesis deposits..."); + debug!("Processing genesis deposits..."); let deposit_data = initial_validator_deposits - .iter() + .par_iter() .map(|deposit| &deposit.deposit_data) .collect(); @@ -411,6 +408,8 @@ impl BeaconState { return Err(Error::InsufficientValidators); } + debug!("Shuffling {} validators...", active_validator_indices.len()); + let committees_per_epoch = self.get_epoch_committee_count(active_validator_indices.len(), spec); @@ -420,8 +419,7 @@ impl BeaconState { committees_per_epoch ); - let active_validator_indices: Vec = - active_validator_indices.iter().cloned().collect(); + let active_validator_indices: Vec = active_validator_indices.to_vec(); let shuffled_active_validator_indices = shuffle_list( active_validator_indices, @@ -1000,6 +998,10 @@ impl BeaconState { whistleblower_reward ); self.validator_registry[validator_index].penalized_epoch = current_epoch; + debug!( + "Whistleblower {} penalized validator {}.", + whistleblower_index, validator_index + ); Ok(()) } @@ -1138,6 +1140,114 @@ impl BeaconState { ) } + /// Verify ``bitfield`` against the ``committee_size``. + /// + /// Spec v0.2.0 + pub fn verify_bitfield(&self, bitfield: &Bitfield, committee_size: usize) -> bool { + if bitfield.num_bytes() != ((committee_size + 7) / 8) { + return false; + } + + for i in committee_size..(bitfield.num_bytes() * 8) { + match bitfield.get(i) { + Ok(bit) => { + if bit { + return false; + } + } + Err(_) => unreachable!(), + } + } + + true + } + + /// Verify validity of ``slashable_attestation`` fields. + /// + /// Spec v0.2.0 + pub fn verify_slashable_attestation( + &self, + slashable_attestation: &SlashableAttestation, + spec: &ChainSpec, + ) -> bool { + if slashable_attestation.custody_bitfield.num_set_bits() > 0 { + return false; + } + + if slashable_attestation.validator_indices.is_empty() { + return false; + } + + for i in 0..(slashable_attestation.validator_indices.len() - 1) { + if slashable_attestation.validator_indices[i] + >= slashable_attestation.validator_indices[i + 1] + { + return false; + } + } + + if !self.verify_bitfield( + &slashable_attestation.custody_bitfield, + slashable_attestation.validator_indices.len(), + ) { + return false; + } + + if slashable_attestation.validator_indices.len() + > spec.max_indices_per_slashable_vote as usize + { + return false; + } + + let mut aggregate_pubs = vec![AggregatePublicKey::new(); 2]; + let mut message_exists = vec![false; 2]; + + for (i, v) in slashable_attestation.validator_indices.iter().enumerate() { + let custody_bit = match slashable_attestation.custody_bitfield.get(i) { + Ok(bit) => bit, + Err(_) => unreachable!(), + }; + + message_exists[custody_bit as usize] = true; + + match self.validator_registry.get(*v as usize) { + Some(validator) => { + aggregate_pubs[custody_bit as usize].add(&validator.pubkey); + } + None => return false, + }; + } + + let message_0 = AttestationDataAndCustodyBit { + data: slashable_attestation.data.clone(), + custody_bit: false, + } + .hash_tree_root(); + let message_1 = AttestationDataAndCustodyBit { + data: slashable_attestation.data.clone(), + custody_bit: true, + } + .hash_tree_root(); + + let mut messages = vec![]; + let mut keys = vec![]; + + if message_exists[0] { + messages.push(&message_0[..]); + keys.push(&aggregate_pubs[0]); + } + if message_exists[1] { + messages.push(&message_1[..]); + keys.push(&aggregate_pubs[1]); + } + + slashable_attestation.aggregate_signature.verify_multiple( + &messages[..], + spec.domain_attestation, + &keys[..], + ) + } + /// Return the block root at a recent `slot`. /// /// Spec v0.2.0 diff --git a/eth2/types/src/proposer_slashing.rs b/eth2/types/src/proposer_slashing.rs index 610017c0cb..ea30d46ece 100644 --- a/eth2/types/src/proposer_slashing.rs +++ b/eth2/types/src/proposer_slashing.rs @@ -6,6 +6,10 @@ use serde_derive::Serialize; use ssz_derive::{Decode, Encode, TreeHash}; use test_random_derive::TestRandom; +mod builder; + +pub use builder::ProposerSlashingBuilder; + #[derive(Debug, PartialEq, Clone, Serialize, Encode, Decode, TreeHash, TestRandom)] pub struct ProposerSlashing { pub proposer_index: u64, diff --git a/eth2/types/src/proposer_slashing/builder.rs b/eth2/types/src/proposer_slashing/builder.rs new file mode 100644 index 0000000000..7923ff74db --- /dev/null +++ b/eth2/types/src/proposer_slashing/builder.rs @@ -0,0 +1,59 @@ +use crate::*; +use ssz::TreeHash; + +/// Builds a `ProposerSlashing`. +pub struct ProposerSlashingBuilder(); + +impl ProposerSlashingBuilder { + /// Builds a `ProposerSlashing` that is a double vote. + /// + /// The `signer` function is used to sign the double-vote and accepts: + /// + /// - `validator_index: u64` + /// - `message: &[u8]` + /// - `epoch: Epoch` + /// - `domain: u64` + /// + /// Where domain is a domain "constant" (e.g., `spec.domain_attestation`). + pub fn double_vote(proposer_index: u64, signer: F, spec: &ChainSpec) -> ProposerSlashing + where + F: Fn(u64, &[u8], Epoch, u64) -> Signature, + { + let slot = Slot::new(0); + let shard = 0; + + let proposal_data_1 = ProposalSignedData { + slot, + shard, + block_root: Hash256::from(&[1][..]), + }; + + let proposal_data_2 = ProposalSignedData { + slot, + shard, + block_root: Hash256::from(&[2][..]), + }; + + let proposal_signature_1 = { + let message = proposal_data_1.hash_tree_root(); + let epoch = slot.epoch(spec.epoch_length); + let domain = spec.domain_proposal; + signer(proposer_index, &message[..], epoch, domain) + }; + + let proposal_signature_2 = { + let message = proposal_data_2.hash_tree_root(); + let epoch = slot.epoch(spec.epoch_length); + let domain = spec.domain_proposal; + signer(proposer_index, &message[..], epoch, domain) + }; + + ProposerSlashing { + proposer_index, + proposal_data_1, + proposal_signature_1, + proposal_data_2, + proposal_signature_2, + } + } +} diff --git a/eth2/types/src/slashable_attestation.rs b/eth2/types/src/slashable_attestation.rs index c4a12338a5..8ad582ce64 100644 --- a/eth2/types/src/slashable_attestation.rs +++ b/eth2/types/src/slashable_attestation.rs @@ -1,4 +1,4 @@ -use crate::{test_utils::TestRandom, AggregateSignature, AttestationData, Bitfield}; +use crate::{test_utils::TestRandom, AggregateSignature, AttestationData, Bitfield, ChainSpec}; use rand::RngCore; use serde_derive::Serialize; use ssz_derive::{Decode, Encode, TreeHash}; @@ -12,12 +12,107 @@ pub struct SlashableAttestation { pub aggregate_signature: AggregateSignature, } +impl SlashableAttestation { + /// Check if ``attestation_data_1`` and ``attestation_data_2`` have the same target. + /// + /// Spec v0.3.0 + pub fn is_double_vote(&self, other: &SlashableAttestation, spec: &ChainSpec) -> bool { + self.data.slot.epoch(spec.epoch_length) == other.data.slot.epoch(spec.epoch_length) + } + + /// Check if ``attestation_data_1`` surrounds ``attestation_data_2``. + /// + /// Spec v0.3.0 + pub fn is_surround_vote(&self, other: &SlashableAttestation, spec: &ChainSpec) -> bool { + let source_epoch_1 = self.data.justified_epoch; + let source_epoch_2 = other.data.justified_epoch; + let target_epoch_1 = self.data.slot.epoch(spec.epoch_length); + let target_epoch_2 = other.data.slot.epoch(spec.epoch_length); + + (source_epoch_1 < source_epoch_2) && (target_epoch_2 < target_epoch_1) + } +} + #[cfg(test)] mod tests { use super::*; + use crate::chain_spec::ChainSpec; + use crate::slot_epoch::{Epoch, Slot}; use crate::test_utils::{SeedableRng, TestRandom, XorShiftRng}; use ssz::{ssz_encode, Decodable, TreeHash}; + #[test] + pub fn test_is_double_vote_true() { + let spec = ChainSpec::foundation(); + let slashable_vote_first = create_slashable_attestation(1, 1, &spec); + let slashable_vote_second = create_slashable_attestation(1, 1, &spec); + + assert_eq!( + slashable_vote_first.is_double_vote(&slashable_vote_second, &spec), + true + ) + } + + #[test] + pub fn test_is_double_vote_false() { + let spec = ChainSpec::foundation(); + let slashable_vote_first = create_slashable_attestation(1, 1, &spec); + let slashable_vote_second = create_slashable_attestation(2, 1, &spec); + + assert_eq!( + slashable_vote_first.is_double_vote(&slashable_vote_second, &spec), + false + ); + } + + #[test] + pub fn test_is_surround_vote_true() { + let spec = ChainSpec::foundation(); + let slashable_vote_first = create_slashable_attestation(2, 1, &spec); + let slashable_vote_second = create_slashable_attestation(1, 2, &spec); + + assert_eq!( + slashable_vote_first.is_surround_vote(&slashable_vote_second, &spec), + true + ); + } + + #[test] + pub fn test_is_surround_vote_true_realistic() { + let spec = ChainSpec::foundation(); + let slashable_vote_first = create_slashable_attestation(4, 1, &spec); + let slashable_vote_second = create_slashable_attestation(3, 2, &spec); + + assert_eq!( + slashable_vote_first.is_surround_vote(&slashable_vote_second, &spec), + true + ); + } + + #[test] + pub fn test_is_surround_vote_false_source_epoch_fails() { + let spec = ChainSpec::foundation(); + let slashable_vote_first = create_slashable_attestation(2, 2, &spec); + let slashable_vote_second = create_slashable_attestation(1, 1, &spec); + + assert_eq!( + slashable_vote_first.is_surround_vote(&slashable_vote_second, &spec), + false + ); + } + + #[test] + pub fn test_is_surround_vote_false_target_epoch_fails() { + let spec = ChainSpec::foundation(); + let slashable_vote_first = create_slashable_attestation(1, 1, &spec); + let slashable_vote_second = create_slashable_attestation(2, 2, &spec); + + assert_eq!( + slashable_vote_first.is_surround_vote(&slashable_vote_second, &spec), + false + ); + } + #[test] pub fn test_ssz_round_trip() { let mut rng = XorShiftRng::from_seed([42; 16]); @@ -40,4 +135,17 @@ mod tests { // TODO: Add further tests // https://github.com/sigp/lighthouse/issues/170 } + + fn create_slashable_attestation( + slot_factor: u64, + justified_epoch: u64, + spec: &ChainSpec, + ) -> SlashableAttestation { + let mut rng = XorShiftRng::from_seed([42; 16]); + let mut slashable_vote = SlashableAttestation::random_for_test(&mut rng); + + slashable_vote.data.slot = Slot::new(slot_factor * spec.epoch_length); + slashable_vote.data.justified_epoch = Epoch::new(justified_epoch); + slashable_vote + } } diff --git a/eth2/types/src/validator.rs b/eth2/types/src/validator.rs index b832283a0d..587a48a1f3 100644 --- a/eth2/types/src/validator.rs +++ b/eth2/types/src/validator.rs @@ -54,9 +54,19 @@ pub struct Validator { } impl Validator { - /// This predicate indicates if the validator represented by this record is considered "active" at `slot`. - pub fn is_active_at(&self, slot: Epoch) -> bool { - self.activation_epoch <= slot && slot < self.exit_epoch + /// Returns `true` if the validator is considered active at some epoch. + pub fn is_active_at(&self, epoch: Epoch) -> bool { + self.activation_epoch <= epoch && epoch < self.exit_epoch + } + + /// Returns `true` if the validator is considered exited at some epoch. + pub fn is_exited_at(&self, epoch: Epoch) -> bool { + self.exit_epoch <= epoch + } + + /// Returns `true` if the validator is considered penalized at some epoch. + pub fn is_penalized_at(&self, epoch: Epoch) -> bool { + self.penalized_epoch <= epoch } } diff --git a/eth2/utils/bls/src/aggregate_public_key.rs b/eth2/utils/bls/src/aggregate_public_key.rs new file mode 100644 index 0000000000..2174a43cb0 --- /dev/null +++ b/eth2/utils/bls/src/aggregate_public_key.rs @@ -0,0 +1,24 @@ +use super::PublicKey; +use bls_aggregates::AggregatePublicKey as RawAggregatePublicKey; + +/// A single BLS signature. +/// +/// This struct is a wrapper upon a base type and provides helper functions (e.g., SSZ +/// serialization). +#[derive(Debug, Clone, Default)] +pub struct AggregatePublicKey(RawAggregatePublicKey); + +impl AggregatePublicKey { + pub fn new() -> Self { + AggregatePublicKey(RawAggregatePublicKey::new()) + } + + pub fn add(&mut self, public_key: &PublicKey) { + self.0.add(public_key.as_raw()) + } + + /// Returns the underlying signature. + pub fn as_raw(&self) -> &RawAggregatePublicKey { + &self.0 + } +} diff --git a/eth2/utils/bls/src/aggregate_signature.rs b/eth2/utils/bls/src/aggregate_signature.rs index 4ee79d0aa8..2d8776353f 100644 --- a/eth2/utils/bls/src/aggregate_signature.rs +++ b/eth2/utils/bls/src/aggregate_signature.rs @@ -1,5 +1,7 @@ use super::{AggregatePublicKey, Signature}; -use bls_aggregates::AggregateSignature as RawAggregateSignature; +use bls_aggregates::{ + AggregatePublicKey as RawAggregatePublicKey, AggregateSignature as RawAggregateSignature, +}; use serde::ser::{Serialize, Serializer}; use ssz::{ decode_ssz_list, hash, ssz_encode, Decodable, DecodeError, Encodable, SszStream, TreeHash, @@ -33,7 +35,38 @@ impl AggregateSignature { domain: u64, aggregate_public_key: &AggregatePublicKey, ) -> bool { - self.0.verify(msg, domain, aggregate_public_key) + self.0.verify(msg, domain, aggregate_public_key.as_raw()) + } + + /// Verify this AggregateSignature against multiple AggregatePublickeys with multiple Messages. + /// + /// All PublicKeys related to a Message should be aggregated into one AggregatePublicKey. + /// Each AggregatePublicKey has a 1:1 ratio with a 32 byte Message. + pub fn verify_multiple( + &self, + messages: &[&[u8]], + domain: u64, + aggregate_public_keys: &[&AggregatePublicKey], + ) -> bool { + // TODO: the API for `RawAggregatePublicKey` shoudn't need to take an owned + // `AggregatePublicKey`. There is an issue to fix this, but in the meantime we need to + // clone. + // + // https://github.com/sigp/signature-schemes/issues/10 + let aggregate_public_keys: Vec = aggregate_public_keys + .iter() + .map(|pk| pk.as_raw()) + .cloned() + .collect(); + + // Messages are concatenated into one long message. + let mut msg: Vec = vec![]; + for message in messages { + msg.extend_from_slice(message); + } + + self.0 + .verify_multiple(&msg[..], domain, &aggregate_public_keys[..]) } } diff --git a/eth2/utils/bls/src/lib.rs b/eth2/utils/bls/src/lib.rs index 8f2e9fac03..865b8d82d2 100644 --- a/eth2/utils/bls/src/lib.rs +++ b/eth2/utils/bls/src/lib.rs @@ -1,20 +1,20 @@ extern crate bls_aggregates; extern crate ssz; +mod aggregate_public_key; mod aggregate_signature; mod keypair; mod public_key; mod secret_key; mod signature; +pub use crate::aggregate_public_key::AggregatePublicKey; pub use crate::aggregate_signature::AggregateSignature; pub use crate::keypair::Keypair; pub use crate::public_key::PublicKey; pub use crate::secret_key::SecretKey; pub use crate::signature::Signature; -pub use self::bls_aggregates::AggregatePublicKey; - pub const BLS_AGG_SIG_BYTE_SIZE: usize = 96; use ssz::ssz_encode;