From 8b34bc490b60e0534a1a10c0f736070e0ff06d03 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Mon, 18 Feb 2019 17:32:13 +1100 Subject: [PATCH] Add fork-choice yaml tests. --- eth2/fork_choice/Cargo.toml | 2 + eth2/fork_choice/src/lib.rs | 2 + eth2/fork_choice/src/optimised_lmd_ghost.rs | 51 ++-- .../tests/fork_choice_test_vectors.yaml | 21 ++ .../tests/optimised_lmd_ghost_test.rs | 248 ++++++++++++++++++ 5 files changed, 302 insertions(+), 22 deletions(-) create mode 100644 eth2/fork_choice/tests/fork_choice_test_vectors.yaml create mode 100644 eth2/fork_choice/tests/optimised_lmd_ghost_test.rs diff --git a/eth2/fork_choice/Cargo.toml b/eth2/fork_choice/Cargo.toml index 72a653032..2765622d1 100644 --- a/eth2/fork_choice/Cargo.toml +++ b/eth2/fork_choice/Cargo.toml @@ -10,9 +10,11 @@ ssz = { path = "../utils/ssz" } types = { path = "../types" } fast-math = "0.1.1" byteorder = "1.3.1" +log = "0.4.6" [dev-dependencies] yaml-rust = "0.4.2" bls = { path = "../utils/bls" } slot_clock = { path = "../utils/slot_clock" } beacon_chain = { path = "../../beacon_node/beacon_chain" } +env_logger = "0.6.0" diff --git a/eth2/fork_choice/src/lib.rs b/eth2/fork_choice/src/lib.rs index c0df820c6..7b97fdc0e 100644 --- a/eth2/fork_choice/src/lib.rs +++ b/eth2/fork_choice/src/lib.rs @@ -41,6 +41,8 @@ extern crate db; extern crate ssz; extern crate types; +#[macro_use] +extern crate log; pub mod longest_chain; pub mod optimised_lmd_ghost; diff --git a/eth2/fork_choice/src/optimised_lmd_ghost.rs b/eth2/fork_choice/src/optimised_lmd_ghost.rs index 6b21e39f8..8f538da0a 100644 --- a/eth2/fork_choice/src/optimised_lmd_ghost.rs +++ b/eth2/fork_choice/src/optimised_lmd_ghost.rs @@ -1,23 +1,3 @@ -// Copyright 2019 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - extern crate byteorder; extern crate fast_math; use crate::{ForkChoice, ForkChoiceError}; @@ -108,6 +88,7 @@ where // Note: Votes are weighted by min(balance, MAX_DEPOSIT_AMOUNT) // // FORK_CHOICE_BALANCE_INCREMENT // build a hashmap of block_hash to weighted votes + trace!("FORKCHOICE: Getting the latest votes"); let mut latest_votes: HashMap = HashMap::new(); // gets the current weighted votes let current_state = self @@ -119,6 +100,10 @@ where ¤t_state.validator_registry[..], block_slot.epoch(EPOCH_LENGTH), ); + trace!( + "FORKCHOICE: Active validator indicies: {:?}", + active_validator_indices + ); for index in active_validator_indices { let balance = @@ -130,7 +115,7 @@ where } } } - + trace!("FORKCHOICE: Latest votes: {:?}", latest_votes); Ok(latest_votes) } @@ -212,6 +197,7 @@ where // Finds the best child, splitting children into a binary tree, based on their hashes fn choose_best_child(&self, votes: &HashMap) -> Option { + println!("Votes: {:?}", votes); let mut bitmask = 0; for bit in (0..=255).rev() { let mut zero_votes = 0; @@ -298,12 +284,21 @@ impl ForkChoice for OptimisedLMDGhost { ) -> Result<(), ForkChoiceError> { // simply add the attestation to the latest_attestation_target if the block_height is // larger + trace!( + "FORKCHOICE: Adding attestation of validator: {:?} for block: {:?}", + validator_index, + target_block_root + ); let attestation_target = self .latest_attestation_targets .entry(validator_index) .or_insert_with(|| *target_block_root); // if we already have a value if attestation_target != target_block_root { + trace!( + "FORKCHOICE: Old attestation found: {:?}", + attestation_target + ); // get the height of the target block let block_height = self .block_store @@ -319,8 +314,11 @@ impl ForkChoice for OptimisedLMDGhost { .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(*attestation_target))? .slot() .height(Slot::from(GENESIS_SLOT)); + trace!("FORKCHOICE: Old block height: {:?}", past_block_height); + trace!("FORKCHOICE: New block height: {:?}", block_height); // update the attestation only if the new target is higher if past_block_height < block_height { + trace!("FORKCHOICE: Updating old attestation"); *attestation_target = *target_block_root; } } @@ -329,6 +327,7 @@ impl ForkChoice for OptimisedLMDGhost { /// Perform lmd_ghost on the current chain to find the head. fn find_head(&mut self, justified_block_start: &Hash256) -> Result { + trace!("Starting optimised fork choice"); let block = self .block_store .get_deserialized(&justified_block_start)? @@ -344,6 +343,7 @@ impl ForkChoice for OptimisedLMDGhost { // remove any votes that don't relate to our current head. latest_votes.retain(|hash, _| self.get_ancestor(*hash, block_height) == Some(current_head)); + trace!("FORKCHOICE: Latest votes: {:?}", latest_votes); // begin searching for the head loop { @@ -368,13 +368,19 @@ impl ForkChoice for OptimisedLMDGhost { step /= 2; } if step > 0 { + trace!("FORKCHOICE: Found clear winner in log lookup"); } // if our skip lookup failed and we only have one child, progress to that child else if children.len() == 1 { current_head = children[0]; + trace!( + "FORKCHOICE: Lookup failed, only one child, proceeding to child: {}", + current_head + ); } // we need to find the best child path to progress down. else { + trace!("FORKCHOICE: Searching for best child"); let mut child_votes = HashMap::new(); for (voted_hash, vote) in latest_votes.iter() { // if the latest votes correspond to a child @@ -383,14 +389,15 @@ impl ForkChoice for OptimisedLMDGhost { *child_votes.entry(child).or_insert_with(|| 0) += vote; } } + println!("Child votes: {:?}", child_votes); // given the votes on the children, find the best child current_head = self .choose_best_child(&child_votes) .ok_or(ForkChoiceError::CannotFindBestChild)?; + trace!("FORKCHOICE: Best child found: {}", current_head); } // No head was found, re-iterate - // update the block height for the next iteration let block_height = self .block_store diff --git a/eth2/fork_choice/tests/fork_choice_test_vectors.yaml b/eth2/fork_choice/tests/fork_choice_test_vectors.yaml new file mode 100644 index 000000000..5a8869e8b --- /dev/null +++ b/eth2/fork_choice/tests/fork_choice_test_vectors.yaml @@ -0,0 +1,21 @@ +title: Fork-choice Tests +summary: A collection of abstract fork-choice tests. +test_suite: Fork-Choice + +test_cases: +- blocks: + - id: 'b0' + parent: 'b0' + - id: 'b1' + parent: 'b0' + - id: 'b2' + parent: 'b1' + - id: 'b3' + parent: 'b1' + weights: + - b0: 0 + - b1: 0 + - b2: 5 + - b3: 10 + heads: + - id: 'b3' diff --git a/eth2/fork_choice/tests/optimised_lmd_ghost_test.rs b/eth2/fork_choice/tests/optimised_lmd_ghost_test.rs new file mode 100644 index 000000000..ac0b6888c --- /dev/null +++ b/eth2/fork_choice/tests/optimised_lmd_ghost_test.rs @@ -0,0 +1,248 @@ +// Tests the optimised LMD Ghost Algorithm + +extern crate beacon_chain; +extern crate bls; +extern crate db; +extern crate env_logger; +extern crate fork_choice; +extern crate log; +extern crate slot_clock; +extern crate types; +extern crate yaml_rust; + +pub use beacon_chain::BeaconChain; +use bls::{PublicKey, Signature}; +use db::stores::{BeaconBlockStore, BeaconStateStore}; +use db::MemoryDB; +use env_logger::{Builder, Env}; +use fork_choice::{ForkChoice, ForkChoiceError, OptimisedLMDGhost}; +use ssz::ssz_encode; +use std::collections::HashMap; +use std::sync::Arc; +use std::{fs::File, io::prelude::*, path::PathBuf, str::FromStr}; +use types::test_utils::{SeedableRng, TestRandom, XorShiftRng}; +use types::validator_registry::get_active_validator_indices; +use types::{ + BeaconBlock, BeaconBlockBody, BeaconState, ChainSpec, Deposit, DepositData, DepositInput, + Eth1Data, Hash256, Slot, Validator, +}; +use yaml_rust::yaml; + +// initialise a single validator and state. All blocks will reference this state root. +fn setup_inital_state( + no_validators: usize, +) -> (impl ForkChoice, Arc>, Hash256) { + let zero_hash = Hash256::zero(); + + let db = Arc::new(MemoryDB::open()); + let block_store = Arc::new(BeaconBlockStore::new(db.clone())); + let state_store = Arc::new(BeaconStateStore::new(db.clone())); + + // the fork choice instantiation + let optimised_lmd_ghost = OptimisedLMDGhost::new(block_store.clone(), state_store.clone()); + + // misc vars for setting up the state + let genesis_time = 1_550_381_159; + + let latest_eth1_data = Eth1Data { + deposit_root: zero_hash.clone(), + block_hash: zero_hash.clone(), + }; + + let initial_validator_deposits = vec![]; + + /* + (0..no_validators) + .map(|_| Deposit { + branch: vec![], + index: 0, + deposit_data: DepositData { + amount: 32_000_000_000, // 32 ETH (in Gwei) + timestamp: genesis_time - 1, + deposit_input: DepositInput { + pubkey: PublicKey::default(), + withdrawal_credentials: zero_hash.clone(), + proof_of_possession: Signature::empty_signature(), + }, + }, + }) + .collect(); + */ + + let spec = ChainSpec::foundation(); + + // create the state + let mut state = BeaconState::genesis( + genesis_time, + initial_validator_deposits, + latest_eth1_data, + &spec, + ) + .unwrap(); + + // activate the validators + for _ in 0..no_validators { + let validator = Validator { + pubkey: PublicKey::default(), + withdrawal_credentials: zero_hash, + activation_epoch: spec.far_future_epoch, + exit_epoch: spec.far_future_epoch, + withdrawal_epoch: spec.far_future_epoch, + penalized_epoch: spec.far_future_epoch, + status_flags: None, + }; + state.validator_registry.push(validator); + state.validator_balances.push(32_000_000_000); + } + + let state_root = state.canonical_root(); + state_store + .put(&state_root, &ssz_encode(&state)[..]) + .unwrap(); + + println!( + "Active: {:?}", + get_active_validator_indices( + &state.validator_registry, + Slot::from(5u64).epoch(spec.EPOCH_LENGTH) + ) + ); + // return initialised vars + (optimised_lmd_ghost, block_store, state_root) +} + +// YAML test vectors +#[test] +fn test_optimised_lmd_ghost() { + // set up logging + Builder::from_env(Env::default().default_filter_or("trace")).init(); + + // load the yaml + let mut file = { + let mut file_path_buf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + file_path_buf.push("tests/fork_choice_test_vectors.yaml"); + + File::open(file_path_buf).unwrap() + }; + + let mut yaml_str = String::new(); + file.read_to_string(&mut yaml_str).unwrap(); + let docs = yaml::YamlLoader::load_from_str(&yaml_str).unwrap(); + let doc = &docs[0]; + let test_cases = doc["test_cases"].as_vec().unwrap(); + + // set up the test + let total_emulated_validators = 20; // the number of validators used to give weights. + let (mut fork_choice, block_store, state_root) = setup_inital_state(total_emulated_validators); + + // keep a hashmap of block_id's to block_hashes (random hashes to abstract block_id) + let mut block_id_map: HashMap = HashMap::new(); + // keep a list of hash to slot + let mut block_slot: HashMap = HashMap::new(); + + // default vars + let zero_hash = Hash256::zero(); + let eth1_data = Eth1Data { + deposit_root: zero_hash.clone(), + block_hash: zero_hash.clone(), + }; + let randao_reveal = Signature::empty_signature(); + let signature = Signature::empty_signature(); + let body = BeaconBlockBody { + proposer_slashings: vec![], + attester_slashings: vec![], + attestations: vec![], + deposits: vec![], + exits: vec![], + }; + + // process the tests + for test_case in test_cases { + // assume the block tree is given to us in order. + for block in test_case["blocks"].clone().into_vec().unwrap() { + let block_id = block["id"].as_str().unwrap().to_string(); + let parent_id = block["parent"].as_str().unwrap(); + + // default params for genesis + let mut block_hash = zero_hash.clone(); + let mut slot = Slot::from(0u64); + let mut parent_root = zero_hash; + + // set the slot and parent based off the YAML. Start with genesis; + // if not the genesis, update slot and parent + if parent_id != block_id { + // generate a random hash for the block_hash + block_hash = Hash256::random(); + // find the parent hash + parent_root = *block_id_map + .get(parent_id) + .expect(&format!("Parent not found: {}", parent_id)); + slot = *(block_slot + .get(&parent_root) + .expect("Parent should have a slot number")) + + 1; + } + + block_id_map.insert(block_id.clone(), block_hash.clone()); + + // update slot mapping + block_slot.insert(block_hash, slot); + + // build the BeaconBlock + let beacon_block = BeaconBlock { + slot, + parent_root, + state_root: state_root.clone(), + randao_reveal: randao_reveal.clone(), + eth1_data: eth1_data.clone(), + signature: signature.clone(), + body: body.clone(), + }; + + // Store the block and state. + block_store + .put(&block_hash, &ssz_encode(&beacon_block)[..]) + .unwrap(); + + // run add block for fork choice if not genesis + if parent_id != block_id { + fork_choice.add_block(&beacon_block, &block_hash).unwrap(); + } + } + + // add the weights (attestations) + let mut current_validator = 0; + for id_map in test_case["weights"].clone().into_vec().unwrap() { + // get the block id and weights + for (map_id, map_weight) in id_map.as_hash().unwrap().iter() { + let id = map_id.as_str().unwrap(); + let block_root = block_id_map + .get(id) + .expect(&format!("Cannot find block id: {} in weights", id)); + let weight = map_weight.as_i64().unwrap(); + // we assume a validator has a value 1 and add an attestation for to achieve the + // correct weight + for _ in 0..weight { + assert!(current_validator <= total_emulated_validators); + fork_choice + .add_attestation(current_validator as u64, &block_root) + .unwrap(); + current_validator += 1; + } + } + } + + // everything is set up, run the fork choice, using genesis as the head + println!("Running fork choice"); + let head = fork_choice.find_head(&zero_hash).unwrap(); + + let mut found_id = None; + for (id, block_hash) in block_id_map.iter() { + if *block_hash == head { + found_id = Some(id); + } + } + + println!("Head Block ID: {:?}", found_id); + } +}