From 4b4c9a98df99c49bbe8c4a9f5508cadecef35621 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Fri, 14 Jun 2019 10:47:51 -0400 Subject: [PATCH 01/40] Copy-paste reduced-tree code --- eth2/fork_choice/src/lib.rs | 1 + eth2/fork_choice/src/reduced_tree.rs | 315 +++++++++++++++++++++++++++ 2 files changed, 316 insertions(+) create mode 100644 eth2/fork_choice/src/reduced_tree.rs diff --git a/eth2/fork_choice/src/lib.rs b/eth2/fork_choice/src/lib.rs index f4a1fa5cb6..8ce8a4945e 100644 --- a/eth2/fork_choice/src/lib.rs +++ b/eth2/fork_choice/src/lib.rs @@ -19,6 +19,7 @@ pub mod bitwise_lmd_ghost; pub mod longest_chain; pub mod optimized_lmd_ghost; +pub mod reduced_tree; pub mod slow_lmd_ghost; pub mod test_utils; diff --git a/eth2/fork_choice/src/reduced_tree.rs b/eth2/fork_choice/src/reduced_tree.rs new file mode 100644 index 0000000000..90eb8958ff --- /dev/null +++ b/eth2/fork_choice/src/reduced_tree.rs @@ -0,0 +1,315 @@ +use std::collections::{BTreeMap, HashMap}; +use std::ops::Range; +use types::Hash256; + +pub const SKIP_LIST_LEN: usize = 16; + +pub type Height = usize; +pub type Slot = u64; + +#[derive(Default, Clone)] +pub struct Node { + pub parent_hash: Option, + pub children: Vec, + pub score: u64, + pub height: Height, + pub block_hash: Hash256, +} + +impl Node { + fn does_not_have_children(&self) -> bool { + self.children.is_empty() + } +} + +pub struct ReducedTree { + store: Store, + nodes: HashMap, + root: Hash256, + slots_at_height: SortedList, + blocks_at_height: HashMap>, +} + +impl ReducedTree { + pub fn new(root: Hash256, height: Height) -> Self { + let mut node: Node = Node::default(); + node.height = 0; + + let mut nodes = HashMap::new(); + nodes.insert(root, Node::default()); + + let mut blocks_at_height = HashMap::new(); + blocks_at_height.insert(height, vec![root]); + + Self { + store: Store::default(), + nodes, + root, + slots_at_height: SortedList::new(), + blocks_at_height, + } + } + + pub fn add_node(&mut self, hash: Hash256, block_hash: Hash256) -> Option<()> { + // TODO: resolve clone. + let mut prev_in_tree = self + .find_prev_in_tree(hash, 0..self.slots_at_height.len())? + .clone(); + + let mut node = Node { + block_hash, + parent_hash: Some(prev_in_tree.block_hash), + ..Node::default() + }; + + if prev_in_tree.does_not_have_children() { + node.parent_hash = Some(prev_in_tree.block_hash); + prev_in_tree.children.push(hash); + } else { + for child_hash in prev_in_tree.children { + let ancestor_hash = self.find_least_common_ancestor(hash, child_hash)?; + if ancestor_hash != prev_in_tree.block_hash { + let child = self.nodes.get_mut(&child_hash)?; + let common_ancestor = Node { + block_hash: ancestor_hash, + parent_hash: Some(prev_in_tree.block_hash), + ..Node::default() + }; + child.parent_hash = Some(common_ancestor.block_hash); + node.parent_hash = Some(common_ancestor.block_hash); + + self.nodes + .insert(common_ancestor.block_hash, common_ancestor); + } + } + } + + self.nodes.insert(hash, node); + + Some(()) + } + + fn find_prev_in_tree(&mut self, hash: Hash256, range: Range) -> Option<&mut Node> { + if range.len() == 0 || range.end > self.slots_at_height.len() { + None + } else { + let mid_height = range.len() / 2; + let mid_slot = self.slot_at_height(mid_height)?; + let mid_ancestor = self.find_ancestor_at_slot(hash, mid_slot)?; + + if self.exists_above_height(hash, mid_height)? { + if self.exists_between_heights(hash, mid_height..mid_height + 1)? { + self.nodes.get_mut(&mid_ancestor) + } else { + self.find_prev_in_tree(hash, mid_height..range.end) + } + } else { + self.find_prev_in_tree(hash, range.start..mid_height) + } + } + } + + fn exists_above_height(&self, hash: Hash256, height: Height) -> Option { + let ancestor_at_height = self.find_ancestor_at_height(hash, height)?; + let blocks_at_height = self.blocks_at_height.get(&height)?; + + Some(blocks_at_height.contains(&ancestor_at_height)) + } + + fn exists_between_heights(&self, hash: Hash256, range: Range) -> Option { + let low_blocks = self.blocks_at_height.get(&range.start)?; + let high_blocks = self.blocks_at_height.get(&range.end)?; + + let low_ancestor = self.find_ancestor_at_height(hash, range.start)?; + let high_ancestor = self.find_ancestor_at_height(hash, range.end)?; + + Some(low_blocks.contains(&low_ancestor) && !high_blocks.contains(&high_ancestor)) + } + + fn find_ancestor_at_height(&self, child: Hash256, height: Height) -> Option { + self.find_ancestor_at_slot(child, self.slot_at_height(height)?) + } + + fn find_ancestor_at_slot(&self, child: Hash256, slot: Slot) -> Option { + get_ancestor_hash_at_slot(slot, child, &self.store) + } + + fn find_least_common_ancestor(&self, a: Hash256, b: Hash256) -> Option { + find_least_common_ancestor(a, b, &self.store) + } + + fn slot_at_height(&self, height: Height) -> Option { + self.slots_at_height.nth(height).cloned() + } +} + +fn get_ancestor_hash_at_slot(slot: Slot, start: Hash256, store: &Store) -> Option { + let mut block = store.get(&start)?; + + loop { + if slot >= block.slot { + break None; + } else { + let delta = block.slot - slot; + + if delta >= 1 << SKIP_LIST_LEN as u64 { + block = store.get(&block.ancestor_skip_list[SKIP_LIST_LEN - 1])?; + } else if delta.is_power_of_two() { + break Some(block.ancestor_skip_list[delta.trailing_zeros() as usize]); + } else { + let i = delta.next_power_of_two().trailing_zeros().saturating_sub(1); + block = store.get(&block.ancestor_skip_list[i as usize])?; + } + } + } +} + +fn find_least_common_ancestor(a_root: Hash256, b_root: Hash256, store: &Store) -> Option { + let mut a = store.get(&a_root)?; + let mut b = store.get(&b_root)?; + + if a.slot > b.slot { + a = store.get(&get_ancestor_hash_at_slot(b.slot, a_root, store)?)?; + } else if b.slot > a.slot { + b = store.get(&get_ancestor_hash_at_slot(a.slot, b_root, store)?)?; + } + + loop { + if a.ancestor_skip_list[0] == b.ancestor_skip_list[0] { + break Some(a.ancestor_skip_list[0]); + } else if a.slot == 0 || b.slot == 0 { + break None; + } else { + a = store.get(&a.ancestor_skip_list[0])?; + b = store.get(&b.ancestor_skip_list[0])?; + } + } +} + +#[derive(Default, Clone, Debug)] +pub struct Block { + pub slot: Slot, + ancestor_skip_list: [Hash256; SKIP_LIST_LEN], +} + +pub type Store = HashMap; + +pub struct SortedList(BTreeMap); + +impl SortedList { + pub fn new() -> Self { + SortedList(BTreeMap::new()) + } + + pub fn insert(&mut self, key: K) { + self.0.insert(key, ()); + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn nth(&self, n: usize) -> Option<&K> { + self.0.iter().nth(n).and_then(|(k, _v)| Some(k)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new() { + let genesis_root = Hash256::random(); + let genesis_slot = 0; + + let _t = Tree::new(genesis_root, genesis_slot); + } + + /// Creates a new "hash" from the `u64`. + /// + /// Does not _actually_ perform a hash, just generates bytes that are some serialization of the + /// the `u64`. + fn get_hash(i: u64) -> Hash256 { + Hash256::from_low_u64_le(i) + } + + fn hash_to_u64(hash: Hash256) -> u64 { + hash.to_low_u64_le() + } + + fn store_chain(store: &mut Store, roots: &[Hash256], slots: &[Slot]) { + for i in 0..roots.len() { + let mut block = Block::default(); + block.slot = slots[i]; + + // Build the skip list. + for j in 0..SKIP_LIST_LEN { + let skip = 2_usize.pow(j as u32); + block.ancestor_skip_list[j as usize] = roots[i.saturating_sub(skip)]; + } + + store.insert(roots[i as usize], block); + } + } + + #[test] + fn common_ancestor() { + let common_chain_len = (2_u64 << SKIP_LIST_LEN) - 3; + let forked_blocks = 2_u64 << SKIP_LIST_LEN; + + let common_roots: Vec = (0..common_chain_len).map(get_hash).collect(); + let common_slots: Vec = (0..common_chain_len).collect(); + + let mut fork_a_roots = common_roots.clone(); + fork_a_roots.append( + &mut (common_chain_len..common_chain_len + forked_blocks) + .map(get_hash) + .collect(), + ); + let mut fork_a_slots = common_slots.clone(); + fork_a_slots.append(&mut (common_chain_len..common_chain_len + forked_blocks).collect()); + + let mut fork_b_roots = common_roots.clone(); + fork_b_roots.append( + &mut (common_chain_len..common_chain_len + forked_blocks) + .map(|i| get_hash(i * 10)) + .collect(), + ); + let mut fork_b_slots = common_slots.clone(); + fork_b_slots.append(&mut (common_chain_len..common_chain_len + forked_blocks).collect()); + + let fork_a_head = *fork_a_roots.iter().last().unwrap(); + let fork_b_head = *fork_b_roots.iter().last().unwrap(); + + let mut store = Store::default(); + store_chain(&mut store, &fork_a_roots, &fork_a_slots); + store_chain(&mut store, &fork_b_roots, &fork_b_slots); + + assert_eq!( + find_least_common_ancestor(fork_a_head, fork_b_head, &store) + .and_then(|i| Some(hash_to_u64(i))), + Some(hash_to_u64(*common_roots.iter().last().unwrap())) + ); + } + + #[test] + fn get_at_slot() { + let n = 2_u64.pow(SKIP_LIST_LEN as u32) * 2; + let mut store = Store::default(); + + let roots: Vec = (0..n).map(get_hash).collect(); + let slots: Vec = (0..n).collect(); + + store_chain(&mut store, &roots, &slots); + + for i in 0..n - 1 { + let key = roots.last().unwrap(); + + assert_eq!( + get_ancestor_hash_at_slot(i as u64, *key, &store), + Some(get_hash(i as u64)) + ); + } + } +} From 4a3d54761a318d03ec8f849a5728e36bd4211ecf Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sat, 15 Jun 2019 09:56:41 -0400 Subject: [PATCH 02/40] Add progress on reduced tree fork choice --- Cargo.toml | 1 + beacon_node/store/src/iter.rs | 135 ++++ beacon_node/store/src/lib.rs | 2 + eth2/fork_choice/fork_choice/Cargo.toml | 25 + .../fork_choice/benches/benches.rs | 75 +++ .../fork_choice/examples/example.rs | 40 ++ .../tests/bitwise_lmd_ghost_test_vectors.yaml | 144 +++++ .../tests/lmd_ghost_test_vectors.yaml | 65 ++ .../tests/longest_chain_test_vectors.yaml | 51 ++ eth2/fork_choice/fork_choice/tests/tests.rs | 231 +++++++ eth2/fork_choice/src/lib.rs | 1 - eth2/fork_choice/src/reduced_tree.rs | 315 ---------- eth2/fork_choice_2/Cargo.toml | 21 + eth2/fork_choice_2/src/lib.rs | 38 ++ eth2/fork_choice_2/src/reduced_tree.rs | 590 ++++++++++++++++++ 15 files changed, 1418 insertions(+), 316 deletions(-) create mode 100644 beacon_node/store/src/iter.rs create mode 100644 eth2/fork_choice/fork_choice/Cargo.toml create mode 100644 eth2/fork_choice/fork_choice/benches/benches.rs create mode 100644 eth2/fork_choice/fork_choice/examples/example.rs create mode 100644 eth2/fork_choice/fork_choice/tests/bitwise_lmd_ghost_test_vectors.yaml create mode 100644 eth2/fork_choice/fork_choice/tests/lmd_ghost_test_vectors.yaml create mode 100644 eth2/fork_choice/fork_choice/tests/longest_chain_test_vectors.yaml create mode 100644 eth2/fork_choice/fork_choice/tests/tests.rs delete mode 100644 eth2/fork_choice/src/reduced_tree.rs create mode 100644 eth2/fork_choice_2/Cargo.toml create mode 100644 eth2/fork_choice_2/src/lib.rs create mode 100644 eth2/fork_choice_2/src/reduced_tree.rs diff --git a/Cargo.toml b/Cargo.toml index 397de70fee..9723d5ce64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "eth2/fork_choice", + "eth2/fork_choice_2", "eth2/operation_pool", "eth2/state_processing", "eth2/types", diff --git a/beacon_node/store/src/iter.rs b/beacon_node/store/src/iter.rs new file mode 100644 index 0000000000..844837983b --- /dev/null +++ b/beacon_node/store/src/iter.rs @@ -0,0 +1,135 @@ +use crate::Store; +use std::sync::Arc; +use types::{BeaconBlock, BeaconState, BeaconStateError, EthSpec, Hash256, Slot}; + +/// Extends `BlockRootsIterator`, returning `BeaconBlock` instances, instead of their roots. +pub struct BlockIterator { + roots: BlockRootsIterator, +} + +impl BlockIterator { + /// Create a new iterator over all blocks in the given `beacon_state` and prior states. + pub fn new(store: Arc, beacon_state: BeaconState, start_slot: Slot) -> Self { + Self { + roots: BlockRootsIterator::new(store, beacon_state, start_slot), + } + } +} + +impl Iterator for BlockIterator { + type Item = BeaconBlock; + + fn next(&mut self) -> Option { + let (root, _slot) = self.roots.next()?; + self.roots.store.get(&root).ok()? + } +} + +/// Iterates backwards through block roots. +/// +/// Uses the `latest_block_roots` field of `BeaconState` to as the source of block roots and will +/// perform a lookup on the `Store` for a prior `BeaconState` if `latest_block_roots` has been +/// exhausted. +/// +/// Returns `None` for roots prior to genesis or when there is an error reading from `Store`. +pub struct BlockRootsIterator { + store: Arc, + beacon_state: BeaconState, + slot: Slot, +} + +impl BlockRootsIterator { + /// Create a new iterator over all block roots in the given `beacon_state` and prior states. + pub fn new(store: Arc, beacon_state: BeaconState, start_slot: Slot) -> Self { + Self { + slot: start_slot, + beacon_state, + store, + } + } +} + +impl Iterator for BlockRootsIterator { + type Item = (Hash256, Slot); + + fn next(&mut self) -> Option { + if (self.slot == 0) || (self.slot > self.beacon_state.slot) { + return None; + } + + self.slot -= 1; + + match self.beacon_state.get_block_root(self.slot) { + Ok(root) => Some((*root, self.slot)), + Err(BeaconStateError::SlotOutOfBounds) => { + // Read a `BeaconState` from the store that has access to prior historical root. + self.beacon_state = { + // Load the earlier state from disk. Skip forward one slot, because a state + // doesn't return it's own state root. + let new_state_root = self.beacon_state.get_state_root(self.slot + 1).ok()?; + + self.store.get(&new_state_root).ok()? + }?; + + let root = self.beacon_state.get_block_root(self.slot).ok()?; + + Some((*root, self.slot)) + } + _ => None, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::MemoryStore; + use types::{test_utils::TestingBeaconStateBuilder, Keypair, MainnetEthSpec}; + + fn get_state() -> BeaconState { + let builder = TestingBeaconStateBuilder::from_single_keypair( + 0, + &Keypair::random(), + &T::default_spec(), + ); + let (state, _keypairs) = builder.build(); + state + } + + #[test] + fn root_iter() { + let store = Arc::new(MemoryStore::open()); + let slots_per_historical_root = MainnetEthSpec::slots_per_historical_root(); + + let mut state_a: BeaconState = get_state(); + let mut state_b: BeaconState = get_state(); + + state_a.slot = Slot::from(slots_per_historical_root); + state_b.slot = Slot::from(slots_per_historical_root * 2); + + let mut hashes = (0..).into_iter().map(|i| Hash256::from(i)); + + for root in &mut state_a.latest_block_roots[..] { + *root = hashes.next().unwrap() + } + for root in &mut state_b.latest_block_roots[..] { + *root = hashes.next().unwrap() + } + + let state_a_root = hashes.next().unwrap(); + state_b.latest_state_roots[0] = state_a_root; + store.put(&state_a_root, &state_a).unwrap(); + + let iter = BlockRootsIterator::new(store.clone(), state_b.clone(), state_b.slot - 1); + let mut collected: Vec<(Hash256, Slot)> = iter.collect(); + collected.reverse(); + + let expected_len = 2 * MainnetEthSpec::slots_per_historical_root() - 1; + + assert_eq!(collected.len(), expected_len); + + for i in 0..expected_len { + assert_eq!(collected[i].0, Hash256::from(i as u64)); + } + } +} diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 59875601a7..24f622fdc0 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -14,6 +14,8 @@ mod impls; mod leveldb_store; mod memory_store; +pub mod iter; + pub use self::leveldb_store::LevelDB as DiskStore; pub use self::memory_store::MemoryStore; pub use errors::Error; diff --git a/eth2/fork_choice/fork_choice/Cargo.toml b/eth2/fork_choice/fork_choice/Cargo.toml new file mode 100644 index 0000000000..e37e415e49 --- /dev/null +++ b/eth2/fork_choice/fork_choice/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "fork_choice" +version = "0.1.0" +authors = ["Age Manning "] +edition = "2018" + +[[bench]] +name = "benches" +harness = false + +[dependencies] +store = { path = "../../beacon_node/store" } +ssz = { path = "../utils/ssz" } +types = { path = "../types" } +log = "0.4.6" +bit-vec = "0.5.0" + +[dev-dependencies] +criterion = "0.2" +hex = "0.3.2" +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/fork_choice/benches/benches.rs b/eth2/fork_choice/fork_choice/benches/benches.rs new file mode 100644 index 0000000000..f311e1ccbb --- /dev/null +++ b/eth2/fork_choice/fork_choice/benches/benches.rs @@ -0,0 +1,75 @@ +use criterion::Criterion; +use criterion::{criterion_group, criterion_main, Benchmark}; +use fork_choice::{test_utils::TestingForkChoiceBuilder, ForkChoice, OptimizedLMDGhost}; +use std::sync::Arc; +use store::MemoryStore; +use types::{ChainSpec, EthSpec, MainnetEthSpec}; + +pub type TestedForkChoice = OptimizedLMDGhost; +pub type TestedEthSpec = MainnetEthSpec; + +/// Helper function to setup a builder and spec. +fn setup( + validator_count: usize, + chain_length: usize, +) -> ( + TestingForkChoiceBuilder, + ChainSpec, +) { + let store = MemoryStore::open(); + let builder: TestingForkChoiceBuilder = + TestingForkChoiceBuilder::new(validator_count, chain_length, Arc::new(store)); + let spec = TestedEthSpec::default_spec(); + + (builder, spec) +} + +/// Benches adding blocks to fork_choice. +fn add_block(c: &mut Criterion) { + let validator_count = 16; + let chain_length = 100; + + let (builder, spec) = setup(validator_count, chain_length); + + c.bench( + &format!("{}_blocks", chain_length), + Benchmark::new("add_blocks", move |b| { + b.iter(|| { + let mut fc = builder.build::>(); + for (root, block) in builder.chain.iter().skip(1) { + fc.add_block(block, root, &spec).unwrap(); + } + }) + }) + .sample_size(10), + ); +} + +/// Benches fork choice head finding. +fn find_head(c: &mut Criterion) { + let validator_count = 16; + let chain_length = 64 * 2; + + let (builder, spec) = setup(validator_count, chain_length); + + let mut fc = builder.build::>(); + for (root, block) in builder.chain.iter().skip(1) { + fc.add_block(block, root, &spec).unwrap(); + } + + let head_root = builder.chain.last().unwrap().0; + for i in 0..validator_count { + fc.add_attestation(i as u64, &head_root, &spec).unwrap(); + } + + c.bench( + &format!("{}_blocks", chain_length), + Benchmark::new("find_head", move |b| { + b.iter(|| fc.find_head(&builder.genesis_root(), &spec).unwrap()) + }) + .sample_size(10), + ); +} + +criterion_group!(benches, add_block, find_head); +criterion_main!(benches); diff --git a/eth2/fork_choice/fork_choice/examples/example.rs b/eth2/fork_choice/fork_choice/examples/example.rs new file mode 100644 index 0000000000..a912c3753c --- /dev/null +++ b/eth2/fork_choice/fork_choice/examples/example.rs @@ -0,0 +1,40 @@ +use fork_choice::{test_utils::TestingForkChoiceBuilder, ForkChoice, OptimizedLMDGhost}; +use std::sync::Arc; +use store::{MemoryStore, Store}; +use types::{BeaconBlock, ChainSpec, EthSpec, Hash256, MainnetEthSpec}; + +fn main() { + let validator_count = 16; + let chain_length = 100; + let repetitions = 50; + + let store = MemoryStore::open(); + let builder: TestingForkChoiceBuilder = + TestingForkChoiceBuilder::new(validator_count, chain_length, Arc::new(store)); + + let fork_choosers: Vec> = (0..repetitions) + .into_iter() + .map(|_| builder.build()) + .collect(); + + let spec = &MainnetEthSpec::default_spec(); + + println!("Running {} times...", repetitions); + for fc in fork_choosers { + do_thing(fc, &builder.chain, builder.genesis_root(), spec); + } +} + +#[inline(never)] +fn do_thing, S: Store>( + mut fc: F, + chain: &[(Hash256, BeaconBlock)], + genesis_root: Hash256, + spec: &ChainSpec, +) { + for (root, block) in chain.iter().skip(1) { + fc.add_block(block, root, spec).unwrap(); + } + + let _head = fc.find_head(&genesis_root, spec).unwrap(); +} diff --git a/eth2/fork_choice/fork_choice/tests/bitwise_lmd_ghost_test_vectors.yaml b/eth2/fork_choice/fork_choice/tests/bitwise_lmd_ghost_test_vectors.yaml new file mode 100644 index 0000000000..61b0b05c40 --- /dev/null +++ b/eth2/fork_choice/fork_choice/tests/bitwise_lmd_ghost_test_vectors.yaml @@ -0,0 +1,144 @@ +title: Fork-choice Tests +summary: A collection of abstract fork-choice tests for bitwise lmd ghost. +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' +# bitwise LMD ghost example. bitwise GHOST gives b2 +- blocks: + - id: 'b0' + parent: 'b0' + - id: 'b1' + parent: 'b0' + - id: 'b2' + parent: 'b0' + - id: 'b3' + parent: 'b0' + weights: + - b1: 5 + - b2: 4 + - b3: 3 + heads: + - id: 'b2' +- blocks: + - id: 'b0' + parent: 'b0' + - id: 'b1' + parent: 'b0' + - id: 'b2' + parent: 'b0' + - id: 'b3' + parent: 'b1' + - id: 'b4' + parent: 'b1' + - id: 'b5' + parent: 'b1' + - id: 'b6' + parent: 'b2' + - id: 'b7' + parent: 'b6' + weights: + - b0: 0 + - b1: 3 + - b2: 2 + - b3: 1 + - b4: 1 + - b5: 1 + - b6: 2 + - b7: 2 + heads: + - id: 'b4' +- blocks: + - id: 'b0' + parent: 'b0' + - id: 'b1' + parent: 'b0' + - id: 'b2' + parent: 'b0' + - id: 'b3' + parent: 'b0' + - id: 'b4' + parent: 'b1' + - id: 'b5' + parent: 'b1' + - id: 'b6' + parent: 'b2' + - id: 'b7' + parent: 'b2' + - id: 'b8' + parent: 'b3' + - id: 'b9' + parent: 'b3' + weights: + - b1: 2 + - b2: 1 + - b3: 1 + - b4: 7 + - b5: 5 + - b6: 2 + - b7: 4 + - b8: 4 + - b9: 2 + heads: + - id: 'b4' +- blocks: + - id: 'b0' + parent: 'b0' + - id: 'b1' + parent: 'b0' + - id: 'b2' + parent: 'b0' + - id: 'b3' + parent: 'b0' + - id: 'b4' + parent: 'b1' + - id: 'b5' + parent: 'b1' + - id: 'b6' + parent: 'b2' + - id: 'b7' + parent: 'b2' + - id: 'b8' + parent: 'b3' + - id: 'b9' + parent: 'b3' + weights: + - b1: 1 + - b2: 1 + - b3: 1 + - b4: 7 + - b5: 5 + - b6: 2 + - b7: 4 + - b8: 4 + - b9: 2 + heads: + - id: 'b7' +- blocks: + - id: 'b0' + parent: 'b0' + - id: 'b1' + parent: 'b0' + - id: 'b2' + parent: 'b0' + weights: + - b1: 0 + - b2: 0 + heads: + - id: 'b1' + diff --git a/eth2/fork_choice/fork_choice/tests/lmd_ghost_test_vectors.yaml b/eth2/fork_choice/fork_choice/tests/lmd_ghost_test_vectors.yaml new file mode 100644 index 0000000000..e7847de11a --- /dev/null +++ b/eth2/fork_choice/fork_choice/tests/lmd_ghost_test_vectors.yaml @@ -0,0 +1,65 @@ +title: Fork-choice Tests +summary: A collection of abstract fork-choice tests for lmd ghost. +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' +# bitwise LMD ghost example. GHOST gives b1 +- blocks: + - id: 'b0' + parent: 'b0' + - id: 'b1' + parent: 'b0' + - id: 'b2' + parent: 'b0' + - id: 'b3' + parent: 'b0' + weights: + - b1: 5 + - b2: 4 + - b3: 3 + heads: + - id: 'b1' +# equal weights children. Should choose lower hash b2 +- blocks: + - id: 'b0' + parent: 'b0' + - id: 'b1' + parent: 'b0' + - id: 'b2' + parent: 'b0' + - id: 'b3' + parent: 'b0' + weights: + - b1: 5 + - b2: 6 + - b3: 6 + heads: + - id: 'b2' +- blocks: + - id: 'b0' + parent: 'b0' + - id: 'b1' + parent: 'b0' + - id: 'b2' + parent: 'b0' + weights: + - b1: 0 + - b2: 0 + heads: + - id: 'b1' diff --git a/eth2/fork_choice/fork_choice/tests/longest_chain_test_vectors.yaml b/eth2/fork_choice/fork_choice/tests/longest_chain_test_vectors.yaml new file mode 100644 index 0000000000..e1cd61f06a --- /dev/null +++ b/eth2/fork_choice/fork_choice/tests/longest_chain_test_vectors.yaml @@ -0,0 +1,51 @@ +title: Fork-choice Tests +summary: A collection of abstract fork-choice tests to verify the longest chain fork-choice rule. +test_suite: Fork-Choice + +test_cases: +- blocks: + - id: 'b0' + parent: 'b0' + - id: 'b1' + parent: 'b0' + - id: 'b2' + parent: 'b1' + - id: 'b3' + parent: 'b1' + - id: 'b4' + parent: 'b3' + weights: + - b0: 0 + - b1: 0 + - b2: 10 + - b3: 1 + heads: + - id: 'b4' +- blocks: + - id: 'b0' + parent: 'b0' + - id: 'b1' + parent: 'b0' + - id: 'b2' + parent: 'b1' + - id: 'b3' + parent: 'b2' + - id: 'b4' + parent: 'b3' + - id: 'b5' + parent: 'b0' + - id: 'b6' + parent: 'b5' + - id: 'b7' + parent: 'b6' + - id: 'b8' + parent: 'b7' + - id: 'b9' + parent: 'b8' + weights: + - b0: 5 + - b1: 20 + - b2: 10 + - b3: 10 + heads: + - id: 'b9' diff --git a/eth2/fork_choice/fork_choice/tests/tests.rs b/eth2/fork_choice/fork_choice/tests/tests.rs new file mode 100644 index 0000000000..39e70a7ddb --- /dev/null +++ b/eth2/fork_choice/fork_choice/tests/tests.rs @@ -0,0 +1,231 @@ +#![cfg(not(debug_assertions))] +/// Tests the available fork-choice algorithms +pub use beacon_chain::BeaconChain; +use bls::Signature; +use store::MemoryStore; +use store::Store; +// use env_logger::{Builder, Env}; +use fork_choice::{BitwiseLMDGhost, ForkChoice, LongestChain, OptimizedLMDGhost, SlowLMDGhost}; +use std::collections::HashMap; +use std::sync::Arc; +use std::{fs::File, io::prelude::*, path::PathBuf}; +use types::test_utils::TestingBeaconStateBuilder; +use types::{ + BeaconBlock, BeaconBlockBody, Eth1Data, EthSpec, Hash256, Keypair, MainnetEthSpec, Slot, +}; +use yaml_rust::yaml; + +// Note: We Assume the block Id's are hex-encoded. + +#[test] +fn test_optimized_lmd_ghost() { + // set up logging + // Builder::from_env(Env::default().default_filter_or("trace")).init(); + + test_yaml_vectors::>( + "tests/lmd_ghost_test_vectors.yaml", + 100, + ); +} + +#[test] +fn test_bitwise_lmd_ghost() { + // set up logging + //Builder::from_env(Env::default().default_filter_or("trace")).init(); + + test_yaml_vectors::>( + "tests/bitwise_lmd_ghost_test_vectors.yaml", + 100, + ); +} + +#[test] +fn test_slow_lmd_ghost() { + test_yaml_vectors::>( + "tests/lmd_ghost_test_vectors.yaml", + 100, + ); +} + +#[test] +fn test_longest_chain() { + test_yaml_vectors::>("tests/longest_chain_test_vectors.yaml", 100); +} + +// run a generic test over given YAML test vectors +fn test_yaml_vectors>( + yaml_file_path: &str, + emulated_validators: usize, // the number of validators used to give weights. +) { + // load test cases from yaml + let test_cases = load_test_cases_from_yaml(yaml_file_path); + + // default vars + let spec = MainnetEthSpec::default_spec(); + let zero_hash = Hash256::zero(); + let eth1_data = Eth1Data { + deposit_count: 0, + deposit_root: zero_hash.clone(), + block_hash: zero_hash.clone(), + }; + let randao_reveal = Signature::empty_signature(); + let signature = Signature::empty_signature(); + let body = BeaconBlockBody { + eth1_data, + randao_reveal, + graffiti: [0; 32], + proposer_slashings: vec![], + attester_slashings: vec![], + attestations: vec![], + deposits: vec![], + voluntary_exits: vec![], + transfers: vec![], + }; + + // process the tests + for test_case in test_cases { + // setup a fresh test + let (mut fork_choice, store, state_root) = setup_inital_state::(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(); + // assume the block tree is given to us in order. + let mut genesis_hash = None; + 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().to_string(); + + // default params for genesis + let block_hash = id_to_hash(&block_id); + let mut slot = spec.genesis_slot; + let previous_block_root = id_to_hash(&parent_id); + + // set the slot and parent based off the YAML. Start with genesis; + // if not the genesis, update slot + if parent_id != block_id { + // find parent slot + slot = *(block_slot + .get(&previous_block_root) + .expect("Parent should have a slot number")) + + 1; + } else { + genesis_hash = Some(block_hash); + } + + // update slot mapping + block_slot.insert(block_hash, slot); + + // build the BeaconBlock + let beacon_block = BeaconBlock { + slot, + previous_block_root, + state_root: state_root.clone(), + signature: signature.clone(), + body: body.clone(), + }; + + // Store the block. + store.put(&block_hash, &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, &spec) + .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 = id_to_hash(&id.to_string()); + 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 <= emulated_validators, + "Not enough validators to emulate weights" + ); + fork_choice + .add_attestation(current_validator as u64, &block_root, &spec) + .unwrap(); + current_validator += 1; + } + } + } + + // everything is set up, run the fork choice, using genesis as the head + let head = fork_choice + .find_head(&genesis_hash.unwrap(), &spec) + .unwrap(); + + // compare the result to the expected test + let success = test_case["heads"] + .clone() + .into_vec() + .unwrap() + .iter() + .find(|heads| id_to_hash(&heads["id"].as_str().unwrap().to_string()) == head) + .is_some(); + + println!("Head found: {}", head); + assert!(success, "Did not find one of the possible heads"); + } +} + +// loads the test_cases from the supplied yaml file +fn load_test_cases_from_yaml(file_path: &str) -> Vec { + // load the yaml + let mut file = { + let mut file_path_buf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + file_path_buf.push(file_path); + 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]; + doc["test_cases"].as_vec().unwrap().clone() +} + +fn setup_inital_state( + // fork_choice_algo: &ForkChoiceAlgorithm, + num_validators: usize +) -> (T, Arc, Hash256) +where + T: ForkChoice, +{ + let store = Arc::new(MemoryStore::open()); + + let fork_choice = ForkChoice::new(store.clone()); + let spec = MainnetEthSpec::default_spec(); + + let mut state_builder: TestingBeaconStateBuilder = + TestingBeaconStateBuilder::from_single_keypair(num_validators, &Keypair::random(), &spec); + state_builder.build_caches(&spec).unwrap(); + let (state, _keypairs) = state_builder.build(); + + let state_root = state.canonical_root(); + store.put(&state_root, &state).unwrap(); + + // return initialised vars + (fork_choice, store, state_root) +} + +// convert a block_id into a Hash256 -- assume input is hex encoded; +fn id_to_hash(id: &String) -> Hash256 { + let bytes = hex::decode(id).expect("Block ID should be hex"); + + let len = std::cmp::min(bytes.len(), 32); + let mut fixed_bytes = [0u8; 32]; + for (index, byte) in bytes.iter().take(32).enumerate() { + fixed_bytes[32 - len + index] = *byte; + } + Hash256::from(fixed_bytes) +} diff --git a/eth2/fork_choice/src/lib.rs b/eth2/fork_choice/src/lib.rs index 8ce8a4945e..f4a1fa5cb6 100644 --- a/eth2/fork_choice/src/lib.rs +++ b/eth2/fork_choice/src/lib.rs @@ -19,7 +19,6 @@ pub mod bitwise_lmd_ghost; pub mod longest_chain; pub mod optimized_lmd_ghost; -pub mod reduced_tree; pub mod slow_lmd_ghost; pub mod test_utils; diff --git a/eth2/fork_choice/src/reduced_tree.rs b/eth2/fork_choice/src/reduced_tree.rs deleted file mode 100644 index 90eb8958ff..0000000000 --- a/eth2/fork_choice/src/reduced_tree.rs +++ /dev/null @@ -1,315 +0,0 @@ -use std::collections::{BTreeMap, HashMap}; -use std::ops::Range; -use types::Hash256; - -pub const SKIP_LIST_LEN: usize = 16; - -pub type Height = usize; -pub type Slot = u64; - -#[derive(Default, Clone)] -pub struct Node { - pub parent_hash: Option, - pub children: Vec, - pub score: u64, - pub height: Height, - pub block_hash: Hash256, -} - -impl Node { - fn does_not_have_children(&self) -> bool { - self.children.is_empty() - } -} - -pub struct ReducedTree { - store: Store, - nodes: HashMap, - root: Hash256, - slots_at_height: SortedList, - blocks_at_height: HashMap>, -} - -impl ReducedTree { - pub fn new(root: Hash256, height: Height) -> Self { - let mut node: Node = Node::default(); - node.height = 0; - - let mut nodes = HashMap::new(); - nodes.insert(root, Node::default()); - - let mut blocks_at_height = HashMap::new(); - blocks_at_height.insert(height, vec![root]); - - Self { - store: Store::default(), - nodes, - root, - slots_at_height: SortedList::new(), - blocks_at_height, - } - } - - pub fn add_node(&mut self, hash: Hash256, block_hash: Hash256) -> Option<()> { - // TODO: resolve clone. - let mut prev_in_tree = self - .find_prev_in_tree(hash, 0..self.slots_at_height.len())? - .clone(); - - let mut node = Node { - block_hash, - parent_hash: Some(prev_in_tree.block_hash), - ..Node::default() - }; - - if prev_in_tree.does_not_have_children() { - node.parent_hash = Some(prev_in_tree.block_hash); - prev_in_tree.children.push(hash); - } else { - for child_hash in prev_in_tree.children { - let ancestor_hash = self.find_least_common_ancestor(hash, child_hash)?; - if ancestor_hash != prev_in_tree.block_hash { - let child = self.nodes.get_mut(&child_hash)?; - let common_ancestor = Node { - block_hash: ancestor_hash, - parent_hash: Some(prev_in_tree.block_hash), - ..Node::default() - }; - child.parent_hash = Some(common_ancestor.block_hash); - node.parent_hash = Some(common_ancestor.block_hash); - - self.nodes - .insert(common_ancestor.block_hash, common_ancestor); - } - } - } - - self.nodes.insert(hash, node); - - Some(()) - } - - fn find_prev_in_tree(&mut self, hash: Hash256, range: Range) -> Option<&mut Node> { - if range.len() == 0 || range.end > self.slots_at_height.len() { - None - } else { - let mid_height = range.len() / 2; - let mid_slot = self.slot_at_height(mid_height)?; - let mid_ancestor = self.find_ancestor_at_slot(hash, mid_slot)?; - - if self.exists_above_height(hash, mid_height)? { - if self.exists_between_heights(hash, mid_height..mid_height + 1)? { - self.nodes.get_mut(&mid_ancestor) - } else { - self.find_prev_in_tree(hash, mid_height..range.end) - } - } else { - self.find_prev_in_tree(hash, range.start..mid_height) - } - } - } - - fn exists_above_height(&self, hash: Hash256, height: Height) -> Option { - let ancestor_at_height = self.find_ancestor_at_height(hash, height)?; - let blocks_at_height = self.blocks_at_height.get(&height)?; - - Some(blocks_at_height.contains(&ancestor_at_height)) - } - - fn exists_between_heights(&self, hash: Hash256, range: Range) -> Option { - let low_blocks = self.blocks_at_height.get(&range.start)?; - let high_blocks = self.blocks_at_height.get(&range.end)?; - - let low_ancestor = self.find_ancestor_at_height(hash, range.start)?; - let high_ancestor = self.find_ancestor_at_height(hash, range.end)?; - - Some(low_blocks.contains(&low_ancestor) && !high_blocks.contains(&high_ancestor)) - } - - fn find_ancestor_at_height(&self, child: Hash256, height: Height) -> Option { - self.find_ancestor_at_slot(child, self.slot_at_height(height)?) - } - - fn find_ancestor_at_slot(&self, child: Hash256, slot: Slot) -> Option { - get_ancestor_hash_at_slot(slot, child, &self.store) - } - - fn find_least_common_ancestor(&self, a: Hash256, b: Hash256) -> Option { - find_least_common_ancestor(a, b, &self.store) - } - - fn slot_at_height(&self, height: Height) -> Option { - self.slots_at_height.nth(height).cloned() - } -} - -fn get_ancestor_hash_at_slot(slot: Slot, start: Hash256, store: &Store) -> Option { - let mut block = store.get(&start)?; - - loop { - if slot >= block.slot { - break None; - } else { - let delta = block.slot - slot; - - if delta >= 1 << SKIP_LIST_LEN as u64 { - block = store.get(&block.ancestor_skip_list[SKIP_LIST_LEN - 1])?; - } else if delta.is_power_of_two() { - break Some(block.ancestor_skip_list[delta.trailing_zeros() as usize]); - } else { - let i = delta.next_power_of_two().trailing_zeros().saturating_sub(1); - block = store.get(&block.ancestor_skip_list[i as usize])?; - } - } - } -} - -fn find_least_common_ancestor(a_root: Hash256, b_root: Hash256, store: &Store) -> Option { - let mut a = store.get(&a_root)?; - let mut b = store.get(&b_root)?; - - if a.slot > b.slot { - a = store.get(&get_ancestor_hash_at_slot(b.slot, a_root, store)?)?; - } else if b.slot > a.slot { - b = store.get(&get_ancestor_hash_at_slot(a.slot, b_root, store)?)?; - } - - loop { - if a.ancestor_skip_list[0] == b.ancestor_skip_list[0] { - break Some(a.ancestor_skip_list[0]); - } else if a.slot == 0 || b.slot == 0 { - break None; - } else { - a = store.get(&a.ancestor_skip_list[0])?; - b = store.get(&b.ancestor_skip_list[0])?; - } - } -} - -#[derive(Default, Clone, Debug)] -pub struct Block { - pub slot: Slot, - ancestor_skip_list: [Hash256; SKIP_LIST_LEN], -} - -pub type Store = HashMap; - -pub struct SortedList(BTreeMap); - -impl SortedList { - pub fn new() -> Self { - SortedList(BTreeMap::new()) - } - - pub fn insert(&mut self, key: K) { - self.0.insert(key, ()); - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn nth(&self, n: usize) -> Option<&K> { - self.0.iter().nth(n).and_then(|(k, _v)| Some(k)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn new() { - let genesis_root = Hash256::random(); - let genesis_slot = 0; - - let _t = Tree::new(genesis_root, genesis_slot); - } - - /// Creates a new "hash" from the `u64`. - /// - /// Does not _actually_ perform a hash, just generates bytes that are some serialization of the - /// the `u64`. - fn get_hash(i: u64) -> Hash256 { - Hash256::from_low_u64_le(i) - } - - fn hash_to_u64(hash: Hash256) -> u64 { - hash.to_low_u64_le() - } - - fn store_chain(store: &mut Store, roots: &[Hash256], slots: &[Slot]) { - for i in 0..roots.len() { - let mut block = Block::default(); - block.slot = slots[i]; - - // Build the skip list. - for j in 0..SKIP_LIST_LEN { - let skip = 2_usize.pow(j as u32); - block.ancestor_skip_list[j as usize] = roots[i.saturating_sub(skip)]; - } - - store.insert(roots[i as usize], block); - } - } - - #[test] - fn common_ancestor() { - let common_chain_len = (2_u64 << SKIP_LIST_LEN) - 3; - let forked_blocks = 2_u64 << SKIP_LIST_LEN; - - let common_roots: Vec = (0..common_chain_len).map(get_hash).collect(); - let common_slots: Vec = (0..common_chain_len).collect(); - - let mut fork_a_roots = common_roots.clone(); - fork_a_roots.append( - &mut (common_chain_len..common_chain_len + forked_blocks) - .map(get_hash) - .collect(), - ); - let mut fork_a_slots = common_slots.clone(); - fork_a_slots.append(&mut (common_chain_len..common_chain_len + forked_blocks).collect()); - - let mut fork_b_roots = common_roots.clone(); - fork_b_roots.append( - &mut (common_chain_len..common_chain_len + forked_blocks) - .map(|i| get_hash(i * 10)) - .collect(), - ); - let mut fork_b_slots = common_slots.clone(); - fork_b_slots.append(&mut (common_chain_len..common_chain_len + forked_blocks).collect()); - - let fork_a_head = *fork_a_roots.iter().last().unwrap(); - let fork_b_head = *fork_b_roots.iter().last().unwrap(); - - let mut store = Store::default(); - store_chain(&mut store, &fork_a_roots, &fork_a_slots); - store_chain(&mut store, &fork_b_roots, &fork_b_slots); - - assert_eq!( - find_least_common_ancestor(fork_a_head, fork_b_head, &store) - .and_then(|i| Some(hash_to_u64(i))), - Some(hash_to_u64(*common_roots.iter().last().unwrap())) - ); - } - - #[test] - fn get_at_slot() { - let n = 2_u64.pow(SKIP_LIST_LEN as u32) * 2; - let mut store = Store::default(); - - let roots: Vec = (0..n).map(get_hash).collect(); - let slots: Vec = (0..n).collect(); - - store_chain(&mut store, &roots, &slots); - - for i in 0..n - 1 { - let key = roots.last().unwrap(); - - assert_eq!( - get_ancestor_hash_at_slot(i as u64, *key, &store), - Some(get_hash(i as u64)) - ); - } - } -} diff --git a/eth2/fork_choice_2/Cargo.toml b/eth2/fork_choice_2/Cargo.toml new file mode 100644 index 0000000000..f71523f0a0 --- /dev/null +++ b/eth2/fork_choice_2/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "fork_choice_2" +version = "0.1.0" +authors = ["Age Manning ", "Paul Hauner "] +edition = "2018" + +[dependencies] +store = { path = "../../beacon_node/store" } +ssz = { path = "../utils/ssz" } +types = { path = "../types" } +log = "0.4.6" +bit-vec = "0.5.0" + +[dev-dependencies] +criterion = "0.2" +hex = "0.3.2" +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_2/src/lib.rs b/eth2/fork_choice_2/src/lib.rs new file mode 100644 index 0000000000..0731f301ba --- /dev/null +++ b/eth2/fork_choice_2/src/lib.rs @@ -0,0 +1,38 @@ +pub mod reduced_tree; + +use std::sync::Arc; +use store::Error as DBError; +use store::Store; +use types::{BeaconBlock, ChainSpec, Hash256, Slot}; + +type Result = std::result::Result; + +#[derive(Debug, PartialEq)] +pub enum Error { + BackendError(String), +} + +pub trait LmdGhostBackend { + fn new(store: Arc) -> Self; + + fn process_message( + &mut self, + validator_index: usize, + block_hash: Hash256, + block_slot: Slot, + ) -> Result<()>; + + fn find_head(&mut self) -> Result; +} + +pub struct ForkChoice { + algorithm: T, +} + +impl> ForkChoice { + fn new(store: Arc) -> Self { + Self { + algorithm: T::new(store), + } + } +} diff --git a/eth2/fork_choice_2/src/reduced_tree.rs b/eth2/fork_choice_2/src/reduced_tree.rs new file mode 100644 index 0000000000..4cc0368cc9 --- /dev/null +++ b/eth2/fork_choice_2/src/reduced_tree.rs @@ -0,0 +1,590 @@ +use super::{Error as SuperError, LmdGhostBackend}; +use std::collections::{BTreeMap, HashMap}; +use std::marker::PhantomData; +use std::sync::Arc; +use store::{iter::BlockRootsIterator, Error as StoreError, Store}; +use types::{BeaconBlock, BeaconState, EthSpec, Hash256, Slot}; + +type Result = std::result::Result; + +pub const SKIP_LIST_LEN: usize = 16; + +#[derive(Debug, PartialEq)] +pub enum Error { + MissingNode(Hash256), + MissingBlock(Hash256), + MissingState(Hash256), + NotInTree(Hash256), + NoCommonAncestor((Hash256, Hash256)), + StoreError(StoreError), +} + +impl From for Error { + fn from(e: StoreError) -> Error { + Error::StoreError(e) + } +} + +pub type Height = usize; + +#[derive(Default, Clone)] +pub struct Node { + pub parent_hash: Option, + pub children: Vec, + pub score: u64, + pub height: Height, + pub block_hash: Hash256, + pub voters: Vec, +} + +impl Node { + pub fn remove_voter(&mut self, voter: usize) -> Option { + let i = self.voters.iter().position(|&v| v == voter)?; + Some(self.voters.remove(i)) + } + + pub fn add_voter(&mut self, voter: usize) { + self.voters.push(voter); + } + + pub fn has_votes(&self) -> bool { + !self.voters.is_empty() + } + + pub fn is_genesis(&self) -> bool { + self.parent_hash.is_some() + } +} + +impl Node { + fn does_not_have_children(&self) -> bool { + self.children.is_empty() + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Vote { + hash: Hash256, + slot: Slot, +} + +pub struct ReducedTree { + store: Arc, + nodes: HashMap, + slots_at_height: SortedList, + blocks_at_height: HashMap>, + /// Maps validator indices to their latest votes. + latest_votes: ElasticList>, + _phantom: PhantomData, +} + +impl LmdGhostBackend for ReducedTree +where + T: Store, + E: EthSpec, +{ + fn new(store: Arc) -> Self { + Self::new(store) + } + + fn process_message( + &mut self, + validator_index: usize, + block_hash: Hash256, + block_slot: Slot, + ) -> std::result::Result<(), SuperError> { + self.process_message(validator_index, block_hash, block_slot) + .map_err(Into::into) + } + + fn find_head(&mut self) -> std::result::Result { + unimplemented!(); + } +} + +impl From for SuperError { + fn from(e: Error) -> SuperError { + SuperError::BackendError(format!("{:?}", e)) + } +} + +impl ReducedTree +where + T: Store, + E: EthSpec, +{ + pub fn new(store: Arc) -> Self { + Self { + store, + nodes: HashMap::new(), + slots_at_height: SortedList::new(), + blocks_at_height: HashMap::new(), + latest_votes: ElasticList::default(), + _phantom: PhantomData, + } + } + + pub fn process_message( + &mut self, + validator_index: usize, + block_hash: Hash256, + slot: Slot, + ) -> Result<()> { + if let Some(previous_vote) = self.latest_votes.get(validator_index) { + if previous_vote.slot > slot { + // Given vote is earier than known vote, nothing to do. + return Ok(()); + } else if previous_vote.slot == slot && previous_vote.hash == block_hash { + // Given vote is identical to known vote, nothing to do. + return Ok(()); + } else if previous_vote.slot == slot && previous_vote.hash != block_hash { + // Vote is an equivocation (double-vote), ignore it. + // + // TODO: flag this as slashable. + return Ok(()); + } else { + // Given vote is newer or different to current vote, replace the current vote. + self.remove_latest_message(validator_index)?; + } + } + + // TODO: add new vote. + + Ok(()) + } + + pub fn remove_latest_message(&mut self, validator_index: usize) -> Result<()> { + if let Some(vote) = self.latest_votes.get(validator_index) { + let should_delete = { + let node = self.get_mut_node(vote.hash)?; + + node.remove_voter(validator_index); + + if let Some(parent_hash) = node.parent_hash { + if node.has_votes() { + // A node with votes is never removed. + false + } else if node.children.len() > 1 { + // A node with more than one child is never removed. + false + } else if node.children.len() == 1 { + // A node which has only one child may be removed. + // + // Load the child of the node and set it's parent to be the parent of this + // node (viz., graft the node's child to the node's parent) + let child = self + .nodes + .get_mut(&node.children[0]) + .ok_or_else(|| Error::MissingNode(node.children[0]))?; + + child.parent_hash = node.parent_hash; + + true + } else if node.children.len() == 0 { + // A node which has no children may be deleted and potentially it's parent + // too. + self.maybe_delete_node(parent_hash)?; + + true + } else { + // It is impossible for a node to have a number of children that is not 0, 1 or + // greater than one. + // + // This code is strictly unnecessary, however we keep it for readability. + unreachable!(); + } + } else { + // A node without a parent is the genesis/finalized node and should never be removed. + false + } + }; + + if should_delete { + self.nodes.remove(&vote.hash); + } + } + + Ok(()) + } + + fn maybe_delete_node(&mut self, hash: Hash256) -> Result<()> { + let should_delete = { + let node = self.get_node(hash)?; + + if let Some(parent_hash) = node.parent_hash { + if (node.children.len() == 1) && !node.has_votes() { + let child_node = self.get_mut_node(node.children[0])?; + + child_node.parent_hash = node.parent_hash; + + true + } else { + false + } + } else { + // A node without a parent is the genesis node and should not be deleted. + false + } + }; + + if should_delete { + self.nodes.remove(&hash); + } + + Ok(()) + } + + pub fn add_latest_message(&mut self, validator_index: usize, hash: Hash256) -> Result<()> { + if let Ok(node) = self.get_mut_node(hash) { + node.add_voter(validator_index); + } else { + self.add_node(hash, vec![validator_index])?; + } + + Ok(()) + } + + pub fn add_node(&mut self, hash: Hash256, voters: Vec) -> Result<()> { + // Find the highest (by slot) ancestor of the given hash/block that is in the reduced tree. + let mut prev_in_tree = { + let hash = self + .find_prev_in_tree(hash) + .ok_or_else(|| Error::NotInTree(hash))?; + self.get_mut_node(hash)?.clone() + }; + + let mut node = Node { + block_hash: hash, + parent_hash: Some(prev_in_tree.block_hash), + voters, + ..Node::default() + }; + + if prev_in_tree.does_not_have_children() { + node.parent_hash = Some(prev_in_tree.block_hash); + prev_in_tree.children.push(hash); + } else { + for &child_hash in &prev_in_tree.children { + let ancestor_hash = self.find_least_common_ancestor(hash, child_hash)?; + if ancestor_hash != prev_in_tree.block_hash { + let child = self.get_mut_node(child_hash)?; + let common_ancestor = Node { + block_hash: ancestor_hash, + parent_hash: Some(prev_in_tree.block_hash), + ..Node::default() + }; + child.parent_hash = Some(common_ancestor.block_hash); + node.parent_hash = Some(common_ancestor.block_hash); + + self.nodes + .insert(common_ancestor.block_hash, common_ancestor); + } + } + } + + // Update `prev_in_tree`. A mutable reference was not maintained to satisfy the borrow + // checker. + // + // This is not an ideal solution and results in unnecessary memory copies -- a better + // solution is certainly possible. + self.nodes.insert(prev_in_tree.block_hash, prev_in_tree); + self.nodes.insert(hash, node); + + Ok(()) + } + + /// For the given block `hash`, find it's highest (by slot) ancestor that exists in the reduced + /// tree. + fn find_prev_in_tree(&mut self, hash: Hash256) -> Option { + self.iter_ancestors(hash) + .ok()? + .find(|(root, _slit)| self.get_node(*root).is_ok()) + .and_then(|(root, _slot)| Some(root)) + } + + /// For the given `child` block hash, return the block's ancestor at the given `target` slot. + fn find_ancestor_at_slot(&self, child: Hash256, target: Slot) -> Result { + let (root, slot) = self + .iter_ancestors(child)? + .find(|(_block, slot)| *slot <= target) + .ok_or_else(|| Error::NotInTree(child))?; + + // Explicitly check that the slot is the target in the case that the given child has a slot + // above target. + if slot == target { + Ok(root) + } else { + Err(Error::NotInTree(child)) + } + } + + /// For the two given block roots (`a_root` and `b_root`), find the first block they share in + /// the tree. Viz, find the block that these two distinct blocks forked from. + fn find_least_common_ancestor(&self, a_root: Hash256, b_root: Hash256) -> Result { + // If the blocks behind `a_root` and `b_root` are not at the same slot, take the highest + // block (by slot) down to be equal with the lower slot. + // + // The result is two roots which identify two blocks at the same height. + let (a_root, b_root) = { + let a = self.get_block(a_root)?; + let b = self.get_block(b_root)?; + + if a.slot > b.slot { + (self.find_ancestor_at_slot(a_root, b.slot)?, b_root) + } else if b.slot > a.slot { + (a_root, self.find_ancestor_at_slot(b_root, a.slot)?) + } else { + (a_root, b_root) + } + }; + + let ((a_root, _a_slot), (_b_root, _b_slot)) = self + .iter_ancestors(a_root)? + .zip(self.iter_ancestors(b_root)?) + .find(|((a_root, _), (b_root, _))| a_root == b_root) + .ok_or_else(|| Error::NoCommonAncestor((a_root, b_root)))?; + + Ok(a_root) + } + + fn iter_ancestors(&self, child: Hash256) -> Result> { + let block = self.get_block(child)?; + let state = self.get_state(block.state_root)?; + + Ok(BlockRootsIterator::new( + self.store.clone(), + state, + block.slot, + )) + } + + fn get_node(&self, hash: Hash256) -> Result<&Node> { + self.nodes + .get(&hash) + .ok_or_else(|| Error::MissingNode(hash)) + } + + fn get_mut_node(&mut self, hash: Hash256) -> Result<&mut Node> { + self.nodes + .get_mut(&hash) + .ok_or_else(|| Error::MissingNode(hash)) + } + + fn get_block(&self, block_root: Hash256) -> Result { + self.store + .get::(&block_root)? + .ok_or_else(|| Error::MissingBlock(block_root)) + } + + fn get_state(&self, state_root: Hash256) -> Result> { + self.store + .get::>(&state_root)? + .ok_or_else(|| Error::MissingState(state_root)) + } + + /* + fn exists_above_height(&self, hash: Hash256, height: Height) -> Option { + let ancestor_at_height = self.find_ancestor_at_height(hash, height)?; + let blocks_at_height = self.blocks_at_height.get(&height)?; + + Some(blocks_at_height.contains(&ancestor_at_height)) + } + + fn exists_between_heights(&self, hash: Hash256, range: Range) -> Option { + let low_blocks = self.blocks_at_height.get(&range.start)?; + let high_blocks = self.blocks_at_height.get(&range.end)?; + + let low_ancestor = self.find_ancestor_at_height(hash, range.start)?; + let high_ancestor = self.find_ancestor_at_height(hash, range.end)?; + + Some(low_blocks.contains(&low_ancestor) && !high_blocks.contains(&high_ancestor)) + } + + fn find_ancestor_at_height(&self, child: Hash256, height: Height) -> Option { + self.find_ancestor_at_slot(child, self.slot_at_height(height)?) + } + + fn slot_at_height(&self, height: Height) -> Option { + self.slots_at_height.nth(height).cloned() + } + */ +} + +pub struct SortedList(BTreeMap); + +impl SortedList { + pub fn new() -> Self { + SortedList(BTreeMap::new()) + } + + pub fn insert(&mut self, key: K) { + self.0.insert(key, ()); + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn nth(&self, n: usize) -> Option<&K> { + self.0.iter().nth(n).and_then(|(k, _v)| Some(k)) + } +} + +#[derive(Default, Clone)] +pub struct ElasticList(Vec); + +impl ElasticList +where + T: Default, +{ + fn ensure(&mut self, i: usize) { + if self.0.len() <= i { + self.0.resize_with(i + 1, Default::default); + } + } + + pub fn get(&mut self, i: usize) -> &T { + self.ensure(i); + &self.0[i] + } + + pub fn get_mut(&mut self, i: usize) -> &mut T { + self.ensure(i); + &mut self.0[i] + } + + pub fn insert(&mut self, i: usize, element: T) { + self.ensure(i); + self.0[i] = element; + } +} + +/* +#[derive(Default, Clone, Debug)] +pub struct Block { + pub slot: Slot, + ancestor_skip_list: [Hash256; SKIP_LIST_LEN], +} + +pub type Store = HashMap; + +pub struct SortedList(BTreeMap); + +impl SortedList { + pub fn new() -> Self { + SortedList(BTreeMap::new()) + } + + pub fn insert(&mut self, key: K) { + self.0.insert(key, ()); + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn nth(&self, n: usize) -> Option<&K> { + self.0.iter().nth(n).and_then(|(k, _v)| Some(k)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new() { + let genesis_root = Hash256::random(); + let genesis_slot = 0; + + let _t = Tree::new(genesis_root, genesis_slot); + } + + /// Creates a new "hash" from the `u64`. + /// + /// Does not _actually_ perform a hash, just generates bytes that are some serialization of the + /// the `u64`. + fn get_hash(i: u64) -> Hash256 { + Hash256::from_low_u64_le(i) + } + + fn hash_to_u64(hash: Hash256) -> u64 { + hash.to_low_u64_le() + } + + fn store_chain(store: &mut Store, roots: &[Hash256], slots: &[Slot]) { + for i in 0..roots.len() { + let mut block = Block::default(); + block.slot = slots[i]; + + // Build the skip list. + for j in 0..SKIP_LIST_LEN { + let skip = 2_usize.pow(j as u32); + block.ancestor_skip_list[j as usize] = roots[i.saturating_sub(skip)]; + } + + store.insert(roots[i as usize], block); + } + } + + #[test] + fn common_ancestor() { + let common_chain_len = (2_u64 << SKIP_LIST_LEN) - 3; + let forked_blocks = 2_u64 << SKIP_LIST_LEN; + + let common_roots: Vec = (0..common_chain_len).map(get_hash).collect(); + let common_slots: Vec = (0..common_chain_len).collect(); + + let mut fork_a_roots = common_roots.clone(); + fork_a_roots.append( + &mut (common_chain_len..common_chain_len + forked_blocks) + .map(get_hash) + .collect(), + ); + let mut fork_a_slots = common_slots.clone(); + fork_a_slots.append(&mut (common_chain_len..common_chain_len + forked_blocks).collect()); + + let mut fork_b_roots = common_roots.clone(); + fork_b_roots.append( + &mut (common_chain_len..common_chain_len + forked_blocks) + .map(|i| get_hash(i * 10)) + .collect(), + ); + let mut fork_b_slots = common_slots.clone(); + fork_b_slots.append(&mut (common_chain_len..common_chain_len + forked_blocks).collect()); + + let fork_a_head = *fork_a_roots.iter().last().unwrap(); + let fork_b_head = *fork_b_roots.iter().last().unwrap(); + + let mut store = Store::default(); + store_chain(&mut store, &fork_a_roots, &fork_a_slots); + store_chain(&mut store, &fork_b_roots, &fork_b_slots); + + assert_eq!( + find_least_common_ancestor(fork_a_head, fork_b_head, &store) + .and_then(|i| Some(hash_to_u64(i))), + Some(hash_to_u64(*common_roots.iter().last().unwrap())) + ); + } + + #[test] + fn get_at_slot() { + let n = 2_u64.pow(SKIP_LIST_LEN as u32) * 2; + let mut store = Store::default(); + + let roots: Vec = (0..n).map(get_hash).collect(); + let slots: Vec = (0..n).collect(); + + store_chain(&mut store, &roots, &slots); + + for i in 0..n - 1 { + let key = roots.last().unwrap(); + + assert_eq!( + get_ancestor_hash_at_slot(i as u64, *key, &store), + Some(get_hash(i as u64)) + ); + } + } +} +*/ From a62c5711339f6885f076542ead50c12ceb355013 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sat, 15 Jun 2019 10:11:55 -0400 Subject: [PATCH 03/40] Fix compile errors in reduced tree --- eth2/fork_choice_2/src/reduced_tree.rs | 203 ++----------------------- 1 file changed, 15 insertions(+), 188 deletions(-) diff --git a/eth2/fork_choice_2/src/reduced_tree.rs b/eth2/fork_choice_2/src/reduced_tree.rs index 4cc0368cc9..a7316c9978 100644 --- a/eth2/fork_choice_2/src/reduced_tree.rs +++ b/eth2/fork_choice_2/src/reduced_tree.rs @@ -1,5 +1,5 @@ use super::{Error as SuperError, LmdGhostBackend}; -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; use std::marker::PhantomData; use std::sync::Arc; use store::{iter::BlockRootsIterator, Error as StoreError, Store}; @@ -71,8 +71,6 @@ pub struct Vote { pub struct ReducedTree { store: Arc, nodes: HashMap, - slots_at_height: SortedList, - blocks_at_height: HashMap>, /// Maps validator indices to their latest votes. latest_votes: ElasticList>, _phantom: PhantomData, @@ -117,8 +115,6 @@ where Self { store, nodes: HashMap::new(), - slots_at_height: SortedList::new(), - blocks_at_height: HashMap::new(), latest_votes: ElasticList::default(), _phantom: PhantomData, } @@ -154,11 +150,13 @@ where } pub fn remove_latest_message(&mut self, validator_index: usize) -> Result<()> { - if let Some(vote) = self.latest_votes.get(validator_index) { - let should_delete = { - let node = self.get_mut_node(vote.hash)?; + if self.latest_votes.get(validator_index).is_some() { + // Unwrap is safe as prior `if` statements ensures the result is `Some`. + let vote = self.latest_votes.get(validator_index).unwrap(); - node.remove_voter(validator_index); + let should_delete = { + self.get_mut_node(vote.hash)?.remove_voter(validator_index); + let node = self.get_node(vote.hash)?.clone(); if let Some(parent_hash) = node.parent_hash { if node.has_votes() { @@ -202,6 +200,8 @@ where if should_delete { self.nodes.remove(&vote.hash); } + + self.latest_votes.insert(validator_index, Some(vote)); } Ok(()) @@ -209,9 +209,9 @@ where fn maybe_delete_node(&mut self, hash: Hash256) -> Result<()> { let should_delete = { - let node = self.get_node(hash)?; + let node = self.get_node(hash)?.clone(); - if let Some(parent_hash) = node.parent_hash { + if node.parent_hash.is_some() { if (node.children.len() == 1) && !node.has_votes() { let child_node = self.get_mut_node(node.children[0])?; @@ -381,55 +381,12 @@ where .get::>(&state_root)? .ok_or_else(|| Error::MissingState(state_root)) } - - /* - fn exists_above_height(&self, hash: Hash256, height: Height) -> Option { - let ancestor_at_height = self.find_ancestor_at_height(hash, height)?; - let blocks_at_height = self.blocks_at_height.get(&height)?; - - Some(blocks_at_height.contains(&ancestor_at_height)) - } - - fn exists_between_heights(&self, hash: Hash256, range: Range) -> Option { - let low_blocks = self.blocks_at_height.get(&range.start)?; - let high_blocks = self.blocks_at_height.get(&range.end)?; - - let low_ancestor = self.find_ancestor_at_height(hash, range.start)?; - let high_ancestor = self.find_ancestor_at_height(hash, range.end)?; - - Some(low_blocks.contains(&low_ancestor) && !high_blocks.contains(&high_ancestor)) - } - - fn find_ancestor_at_height(&self, child: Hash256, height: Height) -> Option { - self.find_ancestor_at_slot(child, self.slot_at_height(height)?) - } - - fn slot_at_height(&self, height: Height) -> Option { - self.slots_at_height.nth(height).cloned() - } - */ -} - -pub struct SortedList(BTreeMap); - -impl SortedList { - pub fn new() -> Self { - SortedList(BTreeMap::new()) - } - - pub fn insert(&mut self, key: K) { - self.0.insert(key, ()); - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn nth(&self, n: usize) -> Option<&K> { - self.0.iter().nth(n).and_then(|(k, _v)| Some(k)) - } } +/// A Vec-wrapper which will grow to match any request. +/// +/// E.g., a `get` or `insert` to an out-of-bounds element will cause the Vec to grow (using +/// Default) to the smallest size required to fulfill the request. #[derive(Default, Clone)] pub struct ElasticList(Vec); @@ -458,133 +415,3 @@ where self.0[i] = element; } } - -/* -#[derive(Default, Clone, Debug)] -pub struct Block { - pub slot: Slot, - ancestor_skip_list: [Hash256; SKIP_LIST_LEN], -} - -pub type Store = HashMap; - -pub struct SortedList(BTreeMap); - -impl SortedList { - pub fn new() -> Self { - SortedList(BTreeMap::new()) - } - - pub fn insert(&mut self, key: K) { - self.0.insert(key, ()); - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn nth(&self, n: usize) -> Option<&K> { - self.0.iter().nth(n).and_then(|(k, _v)| Some(k)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn new() { - let genesis_root = Hash256::random(); - let genesis_slot = 0; - - let _t = Tree::new(genesis_root, genesis_slot); - } - - /// Creates a new "hash" from the `u64`. - /// - /// Does not _actually_ perform a hash, just generates bytes that are some serialization of the - /// the `u64`. - fn get_hash(i: u64) -> Hash256 { - Hash256::from_low_u64_le(i) - } - - fn hash_to_u64(hash: Hash256) -> u64 { - hash.to_low_u64_le() - } - - fn store_chain(store: &mut Store, roots: &[Hash256], slots: &[Slot]) { - for i in 0..roots.len() { - let mut block = Block::default(); - block.slot = slots[i]; - - // Build the skip list. - for j in 0..SKIP_LIST_LEN { - let skip = 2_usize.pow(j as u32); - block.ancestor_skip_list[j as usize] = roots[i.saturating_sub(skip)]; - } - - store.insert(roots[i as usize], block); - } - } - - #[test] - fn common_ancestor() { - let common_chain_len = (2_u64 << SKIP_LIST_LEN) - 3; - let forked_blocks = 2_u64 << SKIP_LIST_LEN; - - let common_roots: Vec = (0..common_chain_len).map(get_hash).collect(); - let common_slots: Vec = (0..common_chain_len).collect(); - - let mut fork_a_roots = common_roots.clone(); - fork_a_roots.append( - &mut (common_chain_len..common_chain_len + forked_blocks) - .map(get_hash) - .collect(), - ); - let mut fork_a_slots = common_slots.clone(); - fork_a_slots.append(&mut (common_chain_len..common_chain_len + forked_blocks).collect()); - - let mut fork_b_roots = common_roots.clone(); - fork_b_roots.append( - &mut (common_chain_len..common_chain_len + forked_blocks) - .map(|i| get_hash(i * 10)) - .collect(), - ); - let mut fork_b_slots = common_slots.clone(); - fork_b_slots.append(&mut (common_chain_len..common_chain_len + forked_blocks).collect()); - - let fork_a_head = *fork_a_roots.iter().last().unwrap(); - let fork_b_head = *fork_b_roots.iter().last().unwrap(); - - let mut store = Store::default(); - store_chain(&mut store, &fork_a_roots, &fork_a_slots); - store_chain(&mut store, &fork_b_roots, &fork_b_slots); - - assert_eq!( - find_least_common_ancestor(fork_a_head, fork_b_head, &store) - .and_then(|i| Some(hash_to_u64(i))), - Some(hash_to_u64(*common_roots.iter().last().unwrap())) - ); - } - - #[test] - fn get_at_slot() { - let n = 2_u64.pow(SKIP_LIST_LEN as u32) * 2; - let mut store = Store::default(); - - let roots: Vec = (0..n).map(get_hash).collect(); - let slots: Vec = (0..n).collect(); - - store_chain(&mut store, &roots, &slots); - - for i in 0..n - 1 { - let key = roots.last().unwrap(); - - assert_eq!( - get_ancestor_hash_at_slot(i as u64, *key, &store), - Some(get_hash(i as u64)) - ); - } - } -} -*/ From 8f44402691f33cac477cb0768e26e56ba76af04b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sat, 15 Jun 2019 10:36:16 -0400 Subject: [PATCH 04/40] Add RwLock to reduced tree --- eth2/fork_choice_2/Cargo.toml | 1 + eth2/fork_choice_2/src/lib.rs | 12 +++---- eth2/fork_choice_2/src/reduced_tree.rs | 44 ++++++++++++++++---------- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/eth2/fork_choice_2/Cargo.toml b/eth2/fork_choice_2/Cargo.toml index f71523f0a0..61b3f7dcff 100644 --- a/eth2/fork_choice_2/Cargo.toml +++ b/eth2/fork_choice_2/Cargo.toml @@ -13,6 +13,7 @@ bit-vec = "0.5.0" [dev-dependencies] criterion = "0.2" +parking_lot = "0.7" hex = "0.3.2" yaml-rust = "0.4.2" bls = { path = "../utils/bls" } diff --git a/eth2/fork_choice_2/src/lib.rs b/eth2/fork_choice_2/src/lib.rs index 0731f301ba..1c24516661 100644 --- a/eth2/fork_choice_2/src/lib.rs +++ b/eth2/fork_choice_2/src/lib.rs @@ -1,9 +1,7 @@ pub mod reduced_tree; use std::sync::Arc; -use store::Error as DBError; -use store::Store; -use types::{BeaconBlock, ChainSpec, Hash256, Slot}; +use types::{Hash256, Slot}; type Result = std::result::Result; @@ -12,17 +10,17 @@ pub enum Error { BackendError(String), } -pub trait LmdGhostBackend { +pub trait LmdGhostBackend: Send + Sync { fn new(store: Arc) -> Self; fn process_message( - &mut self, + &self, validator_index: usize, block_hash: Hash256, block_slot: Slot, ) -> Result<()>; - fn find_head(&mut self) -> Result; + fn find_head(&self) -> Result; } pub struct ForkChoice { @@ -30,7 +28,7 @@ pub struct ForkChoice { } impl> ForkChoice { - fn new(store: Arc) -> Self { + pub fn new(store: Arc) -> Self { Self { algorithm: T::new(store), } diff --git a/eth2/fork_choice_2/src/reduced_tree.rs b/eth2/fork_choice_2/src/reduced_tree.rs index a7316c9978..ca32c17f66 100644 --- a/eth2/fork_choice_2/src/reduced_tree.rs +++ b/eth2/fork_choice_2/src/reduced_tree.rs @@ -1,4 +1,5 @@ use super::{Error as SuperError, LmdGhostBackend}; +use parking_lot::RwLock; use std::collections::HashMap; use std::marker::PhantomData; use std::sync::Arc; @@ -68,34 +69,30 @@ pub struct Vote { slot: Slot, } -pub struct ReducedTree { - store: Arc, - nodes: HashMap, - /// Maps validator indices to their latest votes. - latest_votes: ElasticList>, - _phantom: PhantomData, -} - -impl LmdGhostBackend for ReducedTree +impl LmdGhostBackend for ThreadSafeReducedTree where T: Store, E: EthSpec, { fn new(store: Arc) -> Self { - Self::new(store) + ThreadSafeReducedTree { + core: RwLock::new(ReducedTree::new(store)), + } } fn process_message( - &mut self, + &self, validator_index: usize, block_hash: Hash256, block_slot: Slot, ) -> std::result::Result<(), SuperError> { - self.process_message(validator_index, block_hash, block_slot) + self.core + .write() + .process_message(validator_index, block_hash, block_slot) .map_err(Into::into) } - fn find_head(&mut self) -> std::result::Result { + fn find_head(&self) -> std::result::Result { unimplemented!(); } } @@ -106,6 +103,19 @@ impl From for SuperError { } } +pub struct ThreadSafeReducedTree { + pub core: RwLock>, +} + +pub struct ReducedTree { + store: Arc, + /// Stores all nodes of the tree, keyed by the block hash contained in the node. + nodes: HashMap, + /// Maps validator indices to their latest votes. + latest_votes: ElasticList>, + _phantom: PhantomData, +} + impl ReducedTree where T: Store, @@ -144,12 +154,12 @@ where } } - // TODO: add new vote. + self.add_latest_message(validator_index, block_hash)?; Ok(()) } - pub fn remove_latest_message(&mut self, validator_index: usize) -> Result<()> { + fn remove_latest_message(&mut self, validator_index: usize) -> Result<()> { if self.latest_votes.get(validator_index).is_some() { // Unwrap is safe as prior `if` statements ensures the result is `Some`. let vote = self.latest_votes.get(validator_index).unwrap(); @@ -234,7 +244,7 @@ where Ok(()) } - pub fn add_latest_message(&mut self, validator_index: usize, hash: Hash256) -> Result<()> { + fn add_latest_message(&mut self, validator_index: usize, hash: Hash256) -> Result<()> { if let Ok(node) = self.get_mut_node(hash) { node.add_voter(validator_index); } else { @@ -244,7 +254,7 @@ where Ok(()) } - pub fn add_node(&mut self, hash: Hash256, voters: Vec) -> Result<()> { + fn add_node(&mut self, hash: Hash256, voters: Vec) -> Result<()> { // Find the highest (by slot) ancestor of the given hash/block that is in the reduced tree. let mut prev_in_tree = { let hash = self From c43bbfe183ccad51bb7c77e5d1d42bb46de51bdd Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sat, 15 Jun 2019 11:26:56 -0400 Subject: [PATCH 05/40] Rename fork_choice_2 to lmd_ghost --- Cargo.toml | 2 +- eth2/fork_choice_2/src/lib.rs | 36 ------- eth2/{fork_choice_2 => lmd_ghost}/Cargo.toml | 5 +- eth2/lmd_ghost/src/lib.rs | 99 +++++++++++++++++++ .../src/reduced_tree.rs | 2 +- 5 files changed, 104 insertions(+), 40 deletions(-) delete mode 100644 eth2/fork_choice_2/src/lib.rs rename eth2/{fork_choice_2 => lmd_ghost}/Cargo.toml (88%) create mode 100644 eth2/lmd_ghost/src/lib.rs rename eth2/{fork_choice_2 => lmd_ghost}/src/reduced_tree.rs (99%) diff --git a/Cargo.toml b/Cargo.toml index 9723d5ce64..27c473bb9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] members = [ "eth2/fork_choice", - "eth2/fork_choice_2", + "eth2/lmd_ghost", "eth2/operation_pool", "eth2/state_processing", "eth2/types", diff --git a/eth2/fork_choice_2/src/lib.rs b/eth2/fork_choice_2/src/lib.rs deleted file mode 100644 index 1c24516661..0000000000 --- a/eth2/fork_choice_2/src/lib.rs +++ /dev/null @@ -1,36 +0,0 @@ -pub mod reduced_tree; - -use std::sync::Arc; -use types::{Hash256, Slot}; - -type Result = std::result::Result; - -#[derive(Debug, PartialEq)] -pub enum Error { - BackendError(String), -} - -pub trait LmdGhostBackend: Send + Sync { - fn new(store: Arc) -> Self; - - fn process_message( - &self, - validator_index: usize, - block_hash: Hash256, - block_slot: Slot, - ) -> Result<()>; - - fn find_head(&self) -> Result; -} - -pub struct ForkChoice { - algorithm: T, -} - -impl> ForkChoice { - pub fn new(store: Arc) -> Self { - Self { - algorithm: T::new(store), - } - } -} diff --git a/eth2/fork_choice_2/Cargo.toml b/eth2/lmd_ghost/Cargo.toml similarity index 88% rename from eth2/fork_choice_2/Cargo.toml rename to eth2/lmd_ghost/Cargo.toml index 61b3f7dcff..788708faa2 100644 --- a/eth2/fork_choice_2/Cargo.toml +++ b/eth2/lmd_ghost/Cargo.toml @@ -1,19 +1,20 @@ [package] -name = "fork_choice_2" +name = "lmd_ghost" version = "0.1.0" authors = ["Age Manning ", "Paul Hauner "] edition = "2018" [dependencies] +parking_lot = "0.7" store = { path = "../../beacon_node/store" } ssz = { path = "../utils/ssz" } +state_processing = { path = "../state_processing" } types = { path = "../types" } log = "0.4.6" bit-vec = "0.5.0" [dev-dependencies] criterion = "0.2" -parking_lot = "0.7" hex = "0.3.2" yaml-rust = "0.4.2" bls = { path = "../utils/bls" } diff --git a/eth2/lmd_ghost/src/lib.rs b/eth2/lmd_ghost/src/lib.rs new file mode 100644 index 0000000000..27801595a2 --- /dev/null +++ b/eth2/lmd_ghost/src/lib.rs @@ -0,0 +1,99 @@ +pub mod reduced_tree; + +use state_processing::common::get_attesting_indices_unsorted; +use std::marker::PhantomData; +use std::sync::Arc; +use types::{Attestation, BeaconBlock, BeaconState, BeaconStateError, EthSpec, Hash256, Slot}; + +type Result = std::result::Result; + +#[derive(Debug, PartialEq)] +pub enum Error { + BackendError(String), + BeaconStateError(BeaconStateError), +} + +pub trait LmdGhostBackend: Send + Sync { + fn new(store: Arc) -> Self; + + fn process_message( + &self, + validator_index: usize, + block_hash: Hash256, + block_slot: Slot, + ) -> Result<()>; + + fn find_head(&self) -> Result; +} + +pub struct ForkChoice { + backend: T, + _phantom: PhantomData, +} + +impl ForkChoice +where + T: LmdGhostBackend, + E: EthSpec, +{ + pub fn new(store: Arc) -> Self { + Self { + backend: T::new(store), + _phantom: PhantomData, + } + } + + pub fn find_head(&self) -> Result { + self.backend.find_head() + } + + pub fn process_attestation( + &self, + state: &BeaconState, + attestation: &Attestation, + ) -> Result<()> { + let validator_indices = get_attesting_indices_unsorted( + state, + &attestation.data, + &attestation.aggregation_bitfield, + )?; + + let block_hash = attestation.data.target_root; + + // TODO: what happens when the target root is not the same slot as the block? + let block_slot = attestation + .data + .target_epoch + .start_slot(E::slots_per_epoch()); + + for validator_index in validator_indices { + self.backend + .process_message(validator_index, block_hash, block_slot)?; + } + + Ok(()) + } + + pub fn process_block( + &self, + state: &BeaconState, + block: &BeaconBlock, + block_hash: Hash256, + block_proposer: usize, + ) -> Result<()> { + self.backend + .process_message(block_proposer, block_hash, block.slot)?; + + for attestation in &block.body.attestations { + self.process_attestation(state, attestation)?; + } + + Ok(()) + } +} + +impl From for Error { + fn from(e: BeaconStateError) -> Error { + Error::BeaconStateError(e) + } +} diff --git a/eth2/fork_choice_2/src/reduced_tree.rs b/eth2/lmd_ghost/src/reduced_tree.rs similarity index 99% rename from eth2/fork_choice_2/src/reduced_tree.rs rename to eth2/lmd_ghost/src/reduced_tree.rs index ca32c17f66..b0e82d9847 100644 --- a/eth2/fork_choice_2/src/reduced_tree.rs +++ b/eth2/lmd_ghost/src/reduced_tree.rs @@ -69,7 +69,7 @@ pub struct Vote { slot: Slot, } -impl LmdGhostBackend for ThreadSafeReducedTree +impl LmdGhostBackend for ThreadSafeReducedTree where T: Store, E: EthSpec, From 2ee71aa80819da94489675e01e1b905abe5677a5 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sat, 15 Jun 2019 14:03:29 -0400 Subject: [PATCH 06/40] Add new fork choice struct to beacon chain --- beacon_node/beacon_chain/Cargo.toml | 2 +- beacon_node/beacon_chain/src/beacon_chain.rs | 36 +++---- beacon_node/beacon_chain/src/errors.rs | 2 +- beacon_node/beacon_chain/src/fork_choice.rs | 98 ++++++++++++++++++++ beacon_node/beacon_chain/src/lib.rs | 2 +- eth2/lmd_ghost/src/lib.rs | 91 ++---------------- eth2/lmd_ghost/src/reduced_tree.rs | 25 ++--- 7 files changed, 127 insertions(+), 129 deletions(-) create mode 100644 beacon_node/beacon_chain/src/fork_choice.rs diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index 4f007cbb75..abf1bc6472 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -11,7 +11,6 @@ store = { path = "../store" } failure = "0.1" failure_derive = "0.1" hashing = { path = "../../eth2/utils/hashing" } -fork_choice = { path = "../../eth2/fork_choice" } parking_lot = "0.7" prometheus = "^0.6" log = "0.4" @@ -26,3 +25,4 @@ ssz_derive = { path = "../../eth2/utils/ssz_derive" } state_processing = { path = "../../eth2/state_processing" } tree_hash = { path = "../../eth2/utils/tree_hash" } types = { path = "../../eth2/types" } +lmd_ghost = { path = "../../eth2/lmd_ghost" } diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index dc2cc16df9..b0fe9278ca 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1,9 +1,10 @@ use crate::checkpoint::CheckPoint; use crate::errors::{BeaconChainError as Error, BlockProductionError}; +use crate::fork_choice::{Error as ForkChoiceError, ForkChoice}; use crate::iter::{BlockIterator, BlockRootsIterator}; use crate::metrics::Metrics; use crate::persisted_beacon_chain::{PersistedBeaconChain, BEACON_CHAIN_DB_KEY}; -use fork_choice::{ForkChoice, ForkChoiceError}; +use lmd_ghost::LmdGhost; use log::{debug, trace}; use operation_pool::DepositInsertStatus; use operation_pool::OperationPool; @@ -48,7 +49,7 @@ pub enum BlockProcessingOutcome { pub trait BeaconChainTypes { type Store: store::Store; type SlotClock: slot_clock::SlotClock; - type ForkChoice: fork_choice::ForkChoice; + type LmdGhost: LmdGhost; type EthSpec: types::EthSpec; } @@ -73,7 +74,7 @@ pub struct BeaconChain { genesis_block_root: Hash256, /// A state-machine that is updated with information from the network and chooses a canonical /// head block. - pub fork_choice: RwLock, + pub fork_choice: ForkChoice, /// Stores metrics about this `BeaconChain`. pub metrics: Metrics, } @@ -86,7 +87,7 @@ impl BeaconChain { mut genesis_state: BeaconState, genesis_block: BeaconBlock, spec: ChainSpec, - fork_choice: T::ForkChoice, + fork_choice: ForkChoice, ) -> Result { let state_root = genesis_state.canonical_root(); store.put(&state_root, &genesis_state)?; @@ -115,7 +116,7 @@ impl BeaconChain { state: RwLock::new(genesis_state), canonical_head, genesis_block_root, - fork_choice: RwLock::new(fork_choice), + fork_choice, metrics: Metrics::new()?, }) } @@ -138,18 +139,19 @@ impl BeaconChain { spec.seconds_per_slot, ); - let fork_choice = T::ForkChoice::new(store.clone()); + // let fork_choice = T::ForkChoice::new(store.clone()); + // let fork_choice: ForkChoice = ForkChoice::new(store.clone()); Ok(Some(BeaconChain { spec, - store, slot_clock, op_pool: OperationPool::default(), canonical_head: RwLock::new(p.canonical_head), state: RwLock::new(p.state), - fork_choice: RwLock::new(fork_choice), + fork_choice: ForkChoice::new(store.clone()), genesis_block_root: p.genesis_block_root, metrics: Metrics::new()?, + store, })) } @@ -613,9 +615,7 @@ impl BeaconChain { self.store.put(&state_root, &state)?; // Register the new block with the fork choice service. - self.fork_choice - .write() - .add_block(&block, &block_root, &self.spec)?; + self.fork_choice.process_block(&state, &block)?; // Execute the fork choice algorithm, enthroning a new head if discovered. // @@ -713,20 +713,8 @@ impl BeaconChain { // Start fork choice metrics timer. let timer = self.metrics.fork_choice_times.start_timer(); - let justified_root = { - let root = self.head().beacon_state.current_justified_root; - if root == self.spec.zero_hash { - self.genesis_block_root - } else { - root - } - }; - // Determine the root of the block that is the head of the chain. - let beacon_block_root = self - .fork_choice - .write() - .find_head(&justified_root, &self.spec)?; + let beacon_block_root = self.fork_choice.find_head()?; // End fork choice metrics timer. timer.observe_duration(); diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 75f2fd84dd..8e04948df0 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -1,5 +1,5 @@ +use crate::fork_choice::Error as ForkChoiceError; use crate::metrics::Error as MetricsError; -use fork_choice::ForkChoiceError; use state_processing::BlockProcessingError; use state_processing::SlotProcessingError; use types::*; diff --git a/beacon_node/beacon_chain/src/fork_choice.rs b/beacon_node/beacon_chain/src/fork_choice.rs new file mode 100644 index 0000000000..b2a45baa26 --- /dev/null +++ b/beacon_node/beacon_chain/src/fork_choice.rs @@ -0,0 +1,98 @@ +use crate::BeaconChain; +use lmd_ghost::LmdGhost; +use state_processing::common::get_attesting_indices_unsorted; +use std::marker::PhantomData; +use std::sync::Arc; +use store::Store; +use types::{Attestation, BeaconBlock, BeaconState, BeaconStateError, EthSpec, Hash256}; + +type Result = std::result::Result; + +#[derive(Debug, PartialEq)] +pub enum Error { + BackendError(String), + BeaconStateError(BeaconStateError), +} + +pub struct ForkChoice { + backend: L, + _phantom_a: PhantomData, + _phantom_b: PhantomData, +} + +impl ForkChoice +where + L: LmdGhost, + S: Store, + E: EthSpec, +{ + pub fn new(store: Arc) -> Self { + Self { + backend: L::new(store), + _phantom_a: PhantomData, + _phantom_b: PhantomData, + } + } + + pub fn find_head(&self) -> Result { + self.backend.find_head().map_err(Into::into) + } + + pub fn process_attestation( + &self, + state: &BeaconState, + attestation: &Attestation, + ) -> Result<()> { + // Note: `get_attesting_indices_unsorted` requires that the beacon state caches be built. + let validator_indices = get_attesting_indices_unsorted( + state, + &attestation.data, + &attestation.aggregation_bitfield, + )?; + + let block_hash = attestation.data.target_root; + + // TODO: what happens when the target root is not the same slot as the block? + let block_slot = attestation + .data + .target_epoch + .start_slot(E::slots_per_epoch()); + + for validator_index in validator_indices { + self.backend + .process_message(validator_index, block_hash, block_slot)?; + } + + Ok(()) + } + + /// A helper function which runs `self.process_attestation` on all `Attestation` in the given `BeaconBlock`. + /// + /// Assumes the block (and therefore it's attestations) are valid. It is a logic error to + /// provide an invalid block. + pub fn process_block(&self, state: &BeaconState, block: &BeaconBlock) -> Result<()> { + // Note: we never count the block as a latest message, only attestations. + // + // I (Paul H) do not have an explicit reference to this, however I derive it from this + // document: + // + // https://github.com/ethereum/eth2.0-specs/blob/v0.7.0/specs/core/0_fork-choice.md + for attestation in &block.body.attestations { + self.process_attestation(state, attestation)?; + } + + Ok(()) + } +} + +impl From for Error { + fn from(e: BeaconStateError) -> Error { + Error::BeaconStateError(e) + } +} + +impl From for Error { + fn from(e: String) -> Error { + Error::BackendError(e) + } +} diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 21edb78598..b4250fe0fc 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -1,6 +1,7 @@ mod beacon_chain; mod checkpoint; mod errors; +mod fork_choice; pub mod iter; mod metrics; mod persisted_beacon_chain; @@ -8,7 +9,6 @@ mod persisted_beacon_chain; pub use self::beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessingOutcome}; pub use self::checkpoint::CheckPoint; pub use self::errors::{BeaconChainError, BlockProductionError}; -pub use fork_choice; pub use parking_lot; pub use slot_clock; pub use state_processing::per_block_processing::errors::{ diff --git a/eth2/lmd_ghost/src/lib.rs b/eth2/lmd_ghost/src/lib.rs index 27801595a2..5cd1b18929 100644 --- a/eth2/lmd_ghost/src/lib.rs +++ b/eth2/lmd_ghost/src/lib.rs @@ -1,20 +1,15 @@ -pub mod reduced_tree; +mod reduced_tree; -use state_processing::common::get_attesting_indices_unsorted; -use std::marker::PhantomData; use std::sync::Arc; -use types::{Attestation, BeaconBlock, BeaconState, BeaconStateError, EthSpec, Hash256, Slot}; +use store::Store; +use types::{EthSpec, Hash256, Slot}; -type Result = std::result::Result; +pub use reduced_tree::ThreadSafeReducedTree; -#[derive(Debug, PartialEq)] -pub enum Error { - BackendError(String), - BeaconStateError(BeaconStateError), -} +pub type Result = std::result::Result; -pub trait LmdGhostBackend: Send + Sync { - fn new(store: Arc) -> Self; +pub trait LmdGhost: Send + Sync { + fn new(store: Arc) -> Self; fn process_message( &self, @@ -25,75 +20,3 @@ pub trait LmdGhostBackend: Send + Sync { fn find_head(&self) -> Result; } - -pub struct ForkChoice { - backend: T, - _phantom: PhantomData, -} - -impl ForkChoice -where - T: LmdGhostBackend, - E: EthSpec, -{ - pub fn new(store: Arc) -> Self { - Self { - backend: T::new(store), - _phantom: PhantomData, - } - } - - pub fn find_head(&self) -> Result { - self.backend.find_head() - } - - pub fn process_attestation( - &self, - state: &BeaconState, - attestation: &Attestation, - ) -> Result<()> { - let validator_indices = get_attesting_indices_unsorted( - state, - &attestation.data, - &attestation.aggregation_bitfield, - )?; - - let block_hash = attestation.data.target_root; - - // TODO: what happens when the target root is not the same slot as the block? - let block_slot = attestation - .data - .target_epoch - .start_slot(E::slots_per_epoch()); - - for validator_index in validator_indices { - self.backend - .process_message(validator_index, block_hash, block_slot)?; - } - - Ok(()) - } - - pub fn process_block( - &self, - state: &BeaconState, - block: &BeaconBlock, - block_hash: Hash256, - block_proposer: usize, - ) -> Result<()> { - self.backend - .process_message(block_proposer, block_hash, block.slot)?; - - for attestation in &block.body.attestations { - self.process_attestation(state, attestation)?; - } - - Ok(()) - } -} - -impl From for Error { - fn from(e: BeaconStateError) -> Error { - Error::BeaconStateError(e) - } -} diff --git a/eth2/lmd_ghost/src/reduced_tree.rs b/eth2/lmd_ghost/src/reduced_tree.rs index b0e82d9847..6f2b31c990 100644 --- a/eth2/lmd_ghost/src/reduced_tree.rs +++ b/eth2/lmd_ghost/src/reduced_tree.rs @@ -1,4 +1,4 @@ -use super::{Error as SuperError, LmdGhostBackend}; +use super::{LmdGhost, Result as SuperResult}; use parking_lot::RwLock; use std::collections::HashMap; use std::marker::PhantomData; @@ -8,8 +8,6 @@ use types::{BeaconBlock, BeaconState, EthSpec, Hash256, Slot}; type Result = std::result::Result; -pub const SKIP_LIST_LEN: usize = 16; - #[derive(Debug, PartialEq)] pub enum Error { MissingNode(Hash256), @@ -51,10 +49,6 @@ impl Node { pub fn has_votes(&self) -> bool { !self.voters.is_empty() } - - pub fn is_genesis(&self) -> bool { - self.parent_hash.is_some() - } } impl Node { @@ -69,7 +63,7 @@ pub struct Vote { slot: Slot, } -impl LmdGhostBackend for ThreadSafeReducedTree +impl LmdGhost for ThreadSafeReducedTree where T: Store, E: EthSpec, @@ -85,21 +79,21 @@ where validator_index: usize, block_hash: Hash256, block_slot: Slot, - ) -> std::result::Result<(), SuperError> { + ) -> SuperResult<()> { self.core .write() .process_message(validator_index, block_hash, block_slot) .map_err(Into::into) } - fn find_head(&self) -> std::result::Result { + fn find_head(&self) -> SuperResult { unimplemented!(); } } -impl From for SuperError { - fn from(e: Error) -> SuperError { - SuperError::BackendError(format!("{:?}", e)) +impl From for String { + fn from(e: Error) -> String { + format!("{:?}", e) } } @@ -415,11 +409,6 @@ where &self.0[i] } - pub fn get_mut(&mut self, i: usize) -> &mut T { - self.ensure(i); - &mut self.0[i] - } - pub fn insert(&mut self, i: usize, element: T) { self.ensure(i); self.0[i] = element; From 7756a658a752e2d8e8b83e646075abf441d3306e Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sat, 15 Jun 2019 15:05:34 -0400 Subject: [PATCH 07/40] Update fork choice find head fn --- beacon_node/beacon_chain/src/beacon_chain.rs | 6 +- beacon_node/beacon_chain/src/fork_choice.rs | 83 ++++++++++++++------ eth2/lmd_ghost/src/lib.rs | 4 +- eth2/lmd_ghost/src/reduced_tree.rs | 5 +- 4 files changed, 71 insertions(+), 27 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index b0fe9278ca..31b1759e79 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -74,7 +74,7 @@ pub struct BeaconChain { genesis_block_root: Hash256, /// A state-machine that is updated with information from the network and chooses a canonical /// head block. - pub fork_choice: ForkChoice, + pub fork_choice: ForkChoice, /// Stores metrics about this `BeaconChain`. pub metrics: Metrics, } @@ -87,7 +87,7 @@ impl BeaconChain { mut genesis_state: BeaconState, genesis_block: BeaconBlock, spec: ChainSpec, - fork_choice: ForkChoice, + fork_choice: ForkChoice, ) -> Result { let state_root = genesis_state.canonical_root(); store.put(&state_root, &genesis_state)?; @@ -714,7 +714,7 @@ impl BeaconChain { let timer = self.metrics.fork_choice_times.start_timer(); // Determine the root of the block that is the head of the chain. - let beacon_block_root = self.fork_choice.find_head()?; + let beacon_block_root = self.fork_choice.find_head(&self)?; // End fork choice metrics timer. timer.observe_duration(); diff --git a/beacon_node/beacon_chain/src/fork_choice.rs b/beacon_node/beacon_chain/src/fork_choice.rs index b2a45baa26..da20e934ff 100644 --- a/beacon_node/beacon_chain/src/fork_choice.rs +++ b/beacon_node/beacon_chain/src/fork_choice.rs @@ -1,46 +1,75 @@ -use crate::BeaconChain; +use crate::{BeaconChain, BeaconChainTypes}; use lmd_ghost::LmdGhost; use state_processing::common::get_attesting_indices_unsorted; -use std::marker::PhantomData; use std::sync::Arc; -use store::Store; +use store::{Error as StoreError, Store}; use types::{Attestation, BeaconBlock, BeaconState, BeaconStateError, EthSpec, Hash256}; type Result = std::result::Result; #[derive(Debug, PartialEq)] pub enum Error { + MissingBlock(Hash256), + MissingState(Hash256), BackendError(String), BeaconStateError(BeaconStateError), + StoreError(StoreError), } -pub struct ForkChoice { - backend: L, - _phantom_a: PhantomData, - _phantom_b: PhantomData, +pub struct ForkChoice { + backend: T::LmdGhost, } -impl ForkChoice -where - L: LmdGhost, - S: Store, - E: EthSpec, -{ - pub fn new(store: Arc) -> Self { +impl ForkChoice { + pub fn new(store: Arc) -> Self { Self { - backend: L::new(store), - _phantom_a: PhantomData, - _phantom_b: PhantomData, + backend: T::LmdGhost::new(store), } } - pub fn find_head(&self) -> Result { - self.backend.find_head().map_err(Into::into) + pub fn find_head(&self, chain: &BeaconChain) -> Result { + // From the specification: + // + // Let justified_head be the descendant of finalized_head with the highest epoch that has + // been justified for at least 1 epoch ... If no such descendant exists, + // set justified_head to finalized_head. + let (start_state, start_block_root) = { + let state = chain.current_state(); + + let block_root = if state.current_epoch() + 1 > state.current_justified_epoch { + state.current_justified_root + } else { + state.finalized_root + }; + let block = chain + .store + .get::(&block_root)? + .ok_or_else(|| Error::MissingBlock(block_root))?; + + let state = chain + .store + .get::>(&block.state_root)? + .ok_or_else(|| Error::MissingState(block.state_root))?; + + (state, block_root) + }; + + // A function that returns the weight for some validator index. + let weight = |validator_index: usize| -> Option { + start_state + .validator_registry + .get(validator_index) + .and_then(|v| Some(v.effective_balance)) + }; + + self.backend + .find_head(start_block_root, weight) + .map_err(Into::into) } pub fn process_attestation( &self, - state: &BeaconState, + state: &BeaconState, attestation: &Attestation, ) -> Result<()> { // Note: `get_attesting_indices_unsorted` requires that the beacon state caches be built. @@ -56,7 +85,7 @@ where let block_slot = attestation .data .target_epoch - .start_slot(E::slots_per_epoch()); + .start_slot(T::EthSpec::slots_per_epoch()); for validator_index in validator_indices { self.backend @@ -70,7 +99,11 @@ where /// /// Assumes the block (and therefore it's attestations) are valid. It is a logic error to /// provide an invalid block. - pub fn process_block(&self, state: &BeaconState, block: &BeaconBlock) -> Result<()> { + pub fn process_block( + &self, + state: &BeaconState, + block: &BeaconBlock, + ) -> Result<()> { // Note: we never count the block as a latest message, only attestations. // // I (Paul H) do not have an explicit reference to this, however I derive it from this @@ -91,6 +124,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: StoreError) -> Error { + Error::StoreError(e) + } +} + impl From for Error { fn from(e: String) -> Error { Error::BackendError(e) diff --git a/eth2/lmd_ghost/src/lib.rs b/eth2/lmd_ghost/src/lib.rs index 5cd1b18929..a50532758d 100644 --- a/eth2/lmd_ghost/src/lib.rs +++ b/eth2/lmd_ghost/src/lib.rs @@ -18,5 +18,7 @@ pub trait LmdGhost: Send + Sync { block_slot: Slot, ) -> Result<()>; - fn find_head(&self) -> Result; + fn find_head(&self, start_block_root: Hash256, weight: F) -> Result + where + F: Fn(usize) -> Option; } diff --git a/eth2/lmd_ghost/src/reduced_tree.rs b/eth2/lmd_ghost/src/reduced_tree.rs index 6f2b31c990..44b34b070c 100644 --- a/eth2/lmd_ghost/src/reduced_tree.rs +++ b/eth2/lmd_ghost/src/reduced_tree.rs @@ -86,7 +86,10 @@ where .map_err(Into::into) } - fn find_head(&self) -> SuperResult { + fn find_head(&self, _start_block_root: Hash256, _weight: F) -> SuperResult + where + F: Fn(usize) -> Option, + { unimplemented!(); } } From f4621a9f1ae6bc6ae0c645583a27e3e4a60cfe92 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sat, 15 Jun 2019 18:19:08 -0400 Subject: [PATCH 08/40] Improve reduced tree fork choice --- beacon_node/beacon_chain/src/beacon_chain.rs | 23 +- beacon_node/beacon_chain/src/fork_choice.rs | 50 ++-- beacon_node/beacon_chain/src/lib.rs | 1 + beacon_node/client/src/beacon_chain_types.rs | 26 +-- eth2/lmd_ghost/src/lib.rs | 6 +- eth2/lmd_ghost/src/reduced_tree.rs | 226 ++++++++++++++----- 6 files changed, 225 insertions(+), 107 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 31b1759e79..9e3f6b52cf 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -87,7 +87,6 @@ impl BeaconChain { mut genesis_state: BeaconState, genesis_block: BeaconBlock, spec: ChainSpec, - fork_choice: ForkChoice, ) -> Result { let state_root = genesis_state.canonical_root(); store.put(&state_root, &genesis_state)?; @@ -110,14 +109,14 @@ impl BeaconChain { Ok(Self { spec, - store, slot_clock, op_pool: OperationPool::new(), state: RwLock::new(genesis_state), canonical_head, genesis_block_root, - fork_choice, + fork_choice: ForkChoice::new(store.clone(), genesis_block_root), metrics: Metrics::new()?, + store, }) } @@ -139,16 +138,15 @@ impl BeaconChain { spec.seconds_per_slot, ); - // let fork_choice = T::ForkChoice::new(store.clone()); - // let fork_choice: ForkChoice = ForkChoice::new(store.clone()); + let last_finalized_root = p.canonical_head.beacon_state.finalized_root; Ok(Some(BeaconChain { spec, slot_clock, + fork_choice: ForkChoice::new(store.clone(), last_finalized_root), op_pool: OperationPool::default(), canonical_head: RwLock::new(p.canonical_head), state: RwLock::new(p.state), - fork_choice: ForkChoice::new(store.clone()), genesis_block_root: p.genesis_block_root, metrics: Metrics::new()?, store, @@ -476,11 +474,22 @@ impl BeaconChain { .op_pool .insert_attestation(attestation, &*self.state.read(), &self.spec); + timer.observe_duration(); + if result.is_ok() { self.metrics.attestation_processing_successes.inc(); } - timer.observe_duration(); + // TODO: process attestation. Please consider: + // + // - Because a block was not added to the op pool does not mean it's invalid (it might + // just be old). + // - The attestation should be rejected if we don't know the block (ideally it should be + // queued, but this may be overkill). + // - The attestation _must_ be validated against it's state before being added to fork + // choice. + // - You can avoid verifying some attestations by first checking if they're a latest + // message. This would involve expanding the `LmdGhost` API. result } diff --git a/beacon_node/beacon_chain/src/fork_choice.rs b/beacon_node/beacon_chain/src/fork_choice.rs index da20e934ff..eadb69ce95 100644 --- a/beacon_node/beacon_chain/src/fork_choice.rs +++ b/beacon_node/beacon_chain/src/fork_choice.rs @@ -21,9 +21,9 @@ pub struct ForkChoice { } impl ForkChoice { - pub fn new(store: Arc) -> Self { + pub fn new(store: Arc, genesis_block_root: Hash256) -> Self { Self { - backend: T::LmdGhost::new(store), + backend: T::LmdGhost::new(store, genesis_block_root), } } @@ -67,7 +67,29 @@ impl ForkChoice { .map_err(Into::into) } - pub fn process_attestation( + /// Process all attestations in the given `block`. + /// + /// Assumes the block (and therefore it's attestations) are valid. It is a logic error to + /// provide an invalid block. + pub fn process_block( + &self, + state: &BeaconState, + block: &BeaconBlock, + ) -> Result<()> { + // Note: we never count the block as a latest message, only attestations. + // + // I (Paul H) do not have an explicit reference to this, but I derive it from this + // document: + // + // https://github.com/ethereum/eth2.0-specs/blob/v0.7.0/specs/core/0_fork-choice.md + for attestation in &block.body.attestations { + self.process_attestation_from_block(state, attestation)?; + } + + Ok(()) + } + + fn process_attestation_from_block( &self, state: &BeaconState, attestation: &Attestation, @@ -94,28 +116,6 @@ impl ForkChoice { Ok(()) } - - /// A helper function which runs `self.process_attestation` on all `Attestation` in the given `BeaconBlock`. - /// - /// Assumes the block (and therefore it's attestations) are valid. It is a logic error to - /// provide an invalid block. - pub fn process_block( - &self, - state: &BeaconState, - block: &BeaconBlock, - ) -> Result<()> { - // Note: we never count the block as a latest message, only attestations. - // - // I (Paul H) do not have an explicit reference to this, however I derive it from this - // document: - // - // https://github.com/ethereum/eth2.0-specs/blob/v0.7.0/specs/core/0_fork-choice.md - for attestation in &block.body.attestations { - self.process_attestation(state, attestation)?; - } - - Ok(()) - } } impl From for Error { diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index b4250fe0fc..4aa1370d97 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -9,6 +9,7 @@ mod persisted_beacon_chain; pub use self::beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessingOutcome}; pub use self::checkpoint::CheckPoint; pub use self::errors::{BeaconChainError, BlockProductionError}; +pub use lmd_ghost; pub use parking_lot; pub use slot_clock; pub use state_processing::per_block_processing::errors::{ diff --git a/beacon_node/client/src/beacon_chain_types.rs b/beacon_node/client/src/beacon_chain_types.rs index c55c04b443..c923f724c8 100644 --- a/beacon_node/client/src/beacon_chain_types.rs +++ b/beacon_node/client/src/beacon_chain_types.rs @@ -1,8 +1,9 @@ use beacon_chain::{ - fork_choice::OptimizedLMDGhost, slot_clock::SystemTimeSlotClock, store::Store, BeaconChain, - BeaconChainTypes, + lmd_ghost::{LmdGhost, ThreadSafeReducedTree}, + slot_clock::SystemTimeSlotClock, + store::Store, + BeaconChain, BeaconChainTypes, }; -use fork_choice::ForkChoice; use slog::{info, Logger}; use slot_clock::SlotClock; use std::marker::PhantomData; @@ -33,7 +34,7 @@ pub struct ClientType { impl BeaconChainTypes for ClientType { type Store = S; type SlotClock = SystemTimeSlotClock; - type ForkChoice = OptimizedLMDGhost; + type LmdGhost = ThreadSafeReducedTree; type EthSpec = E; } impl InitialiseBeaconChain for ClientType {} @@ -45,8 +46,8 @@ fn maybe_load_from_store_for_testnet( log: Logger, ) -> BeaconChain where - T: BeaconChainTypes, - T::ForkChoice: ForkChoice, + T: BeaconChainTypes, + T::LmdGhost: LmdGhost, { if let Ok(Some(beacon_chain)) = BeaconChain::from_store(store.clone(), spec.clone()) { info!( @@ -74,19 +75,10 @@ where genesis_state.genesis_time, spec.seconds_per_slot, ); - // Choose the fork choice - let fork_choice = T::ForkChoice::new(store.clone()); // Genesis chain //TODO: Handle error correctly - BeaconChain::from_genesis( - store, - slot_clock, - genesis_state, - genesis_block, - spec, - fork_choice, - ) - .expect("Terminate if beacon chain generation fails") + BeaconChain::from_genesis(store, slot_clock, genesis_state, genesis_block, spec) + .expect("Terminate if beacon chain generation fails") } } diff --git a/eth2/lmd_ghost/src/lib.rs b/eth2/lmd_ghost/src/lib.rs index a50532758d..59495e382c 100644 --- a/eth2/lmd_ghost/src/lib.rs +++ b/eth2/lmd_ghost/src/lib.rs @@ -9,7 +9,7 @@ pub use reduced_tree::ThreadSafeReducedTree; pub type Result = std::result::Result; pub trait LmdGhost: Send + Sync { - fn new(store: Arc) -> Self; + fn new(store: Arc, genesis_root: Hash256) -> Self; fn process_message( &self, @@ -20,5 +20,7 @@ pub trait LmdGhost: Send + Sync { fn find_head(&self, start_block_root: Hash256, weight: F) -> Result where - F: Fn(usize) -> Option; + F: Fn(usize) -> Option + Copy; + + fn update_finalized_root(&self, new_root: Hash256) -> Result<()>; } diff --git a/eth2/lmd_ghost/src/reduced_tree.rs b/eth2/lmd_ghost/src/reduced_tree.rs index 44b34b070c..84249b603f 100644 --- a/eth2/lmd_ghost/src/reduced_tree.rs +++ b/eth2/lmd_ghost/src/reduced_tree.rs @@ -16,6 +16,7 @@ pub enum Error { NotInTree(Hash256), NoCommonAncestor((Hash256, Hash256)), StoreError(StoreError), + ValidatorWeightUnknown(usize), } impl From for Error { @@ -24,43 +25,8 @@ impl From for Error { } } -pub type Height = usize; - -#[derive(Default, Clone)] -pub struct Node { - pub parent_hash: Option, - pub children: Vec, - pub score: u64, - pub height: Height, - pub block_hash: Hash256, - pub voters: Vec, -} - -impl Node { - pub fn remove_voter(&mut self, voter: usize) -> Option { - let i = self.voters.iter().position(|&v| v == voter)?; - Some(self.voters.remove(i)) - } - - pub fn add_voter(&mut self, voter: usize) { - self.voters.push(voter); - } - - pub fn has_votes(&self) -> bool { - !self.voters.is_empty() - } -} - -impl Node { - fn does_not_have_children(&self) -> bool { - self.children.is_empty() - } -} - -#[derive(Debug, Clone, Copy)] -pub struct Vote { - hash: Hash256, - slot: Slot, +pub struct ThreadSafeReducedTree { + core: RwLock>, } impl LmdGhost for ThreadSafeReducedTree @@ -68,9 +34,9 @@ where T: Store, E: EthSpec, { - fn new(store: Arc) -> Self { + fn new(store: Arc, genesis_root: Hash256) -> Self { ThreadSafeReducedTree { - core: RwLock::new(ReducedTree::new(store)), + core: RwLock::new(ReducedTree::new(store, genesis_root)), } } @@ -86,30 +52,29 @@ where .map_err(Into::into) } - fn find_head(&self, _start_block_root: Hash256, _weight: F) -> SuperResult + fn find_head(&self, start_block_root: Hash256, weight_fn: F) -> SuperResult where - F: Fn(usize) -> Option, + F: Fn(usize) -> Option + Copy, { - unimplemented!(); + self.core + .write() + .update_weights_and_find_head(start_block_root, weight_fn) + .map_err(Into::into) + } + + fn update_finalized_root(&self, new_root: Hash256) -> SuperResult<()> { + self.core.write().update_root(new_root).map_err(Into::into) } } -impl From for String { - fn from(e: Error) -> String { - format!("{:?}", e) - } -} - -pub struct ThreadSafeReducedTree { - pub core: RwLock>, -} - -pub struct ReducedTree { +struct ReducedTree { store: Arc, /// Stores all nodes of the tree, keyed by the block hash contained in the node. nodes: HashMap, /// Maps validator indices to their latest votes. latest_votes: ElasticList>, + /// Stores the root of the tree, used for pruning. + root: Hash256, _phantom: PhantomData, } @@ -118,15 +83,54 @@ where T: Store, E: EthSpec, { - pub fn new(store: Arc) -> Self { + pub fn new(store: Arc, genesis_root: Hash256) -> Self { + let mut nodes = HashMap::new(); + + // Insert the genesis node. + nodes.insert( + genesis_root, + Node { + block_hash: genesis_root, + ..Node::default() + }, + ); + Self { store, - nodes: HashMap::new(), + nodes, latest_votes: ElasticList::default(), + root: genesis_root, _phantom: PhantomData, } } + pub fn update_root(&mut self, new_root: Hash256) -> Result<()> { + if !self.nodes.contains_key(&new_root) { + self.add_node(new_root, vec![])?; + } + + self.retain_subtree(self.root, new_root)?; + + self.root = new_root; + + Ok(()) + } + + fn retain_subtree(&mut self, current_hash: Hash256, subtree_hash: Hash256) -> Result<()> { + if current_hash != subtree_hash { + // Clone satisifies the borrow checker. + let children = self.get_node(current_hash)?.children.clone(); + + for child_hash in children { + self.retain_subtree(child_hash, subtree_hash)?; + } + + self.nodes.remove(¤t_hash); + } + + Ok(()) + } + pub fn process_message( &mut self, validator_index: usize, @@ -143,7 +147,7 @@ where } else if previous_vote.slot == slot && previous_vote.hash != block_hash { // Vote is an equivocation (double-vote), ignore it. // - // TODO: flag this as slashable. + // TODO: this is slashable. return Ok(()); } else { // Given vote is newer or different to current vote, replace the current vote. @@ -156,6 +160,76 @@ where Ok(()) } + pub fn update_weights_and_find_head( + &mut self, + start_block_root: Hash256, + weight_fn: F, + ) -> Result + where + F: Fn(usize) -> Option + Copy, + { + let _root_weight = self.update_weight(start_block_root, weight_fn)?; + + let start_node = self.get_node(start_block_root)?; + let head_node = self.find_head_from(start_node)?; + + Ok(head_node.block_hash) + } + + fn find_head_from<'a>(&'a self, start_node: &'a Node) -> Result<&'a Node> { + if start_node.does_not_have_children() { + Ok(start_node) + } else { + let children = start_node + .children + .iter() + .map(|hash| self.get_node(*hash)) + .collect::>>()?; + + // TODO: check if `max_by` is `O(n^2)`. + let best_child = children + .iter() + .max_by(|a, b| { + if a.weight != b.weight { + a.weight.cmp(&b.weight) + } else { + a.block_hash.cmp(&b.block_hash) + } + }) + // There can only be no maximum if there are no children. This code path is guarded + // against that condition. + .expect("There must be a maximally weighted node."); + + self.find_head_from(best_child) + } + } + + fn update_weight(&mut self, start_block_root: Hash256, weight_fn: F) -> Result + where + F: Fn(usize) -> Option + Copy, + { + let weight = { + let node = self.get_node(start_block_root)?.clone(); + + let mut weight = 0; + + for &child in &node.children { + weight += self.update_weight(child, weight_fn)?; + } + + for &voter in &node.voters { + weight += weight_fn(voter).ok_or_else(|| Error::ValidatorWeightUnknown(voter))?; + } + + weight + }; + + let node = self.get_mut_node(start_block_root)?; + node.weight = weight; + + Ok(weight) + } + fn remove_latest_message(&mut self, validator_index: usize) -> Result<()> { if self.latest_votes.get(validator_index).is_some() { // Unwrap is safe as prior `if` statements ensures the result is `Some`. @@ -390,6 +464,40 @@ where } } +#[derive(Default, Clone)] +pub struct Node { + pub parent_hash: Option, + pub children: Vec, + pub weight: u64, + pub block_hash: Hash256, + pub voters: Vec, +} + +impl Node { + pub fn does_not_have_children(&self) -> bool { + self.children.is_empty() + } + + pub fn remove_voter(&mut self, voter: usize) -> Option { + let i = self.voters.iter().position(|&v| v == voter)?; + Some(self.voters.remove(i)) + } + + pub fn add_voter(&mut self, voter: usize) { + self.voters.push(voter); + } + + pub fn has_votes(&self) -> bool { + !self.voters.is_empty() + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Vote { + hash: Hash256, + slot: Slot, +} + /// A Vec-wrapper which will grow to match any request. /// /// E.g., a `get` or `insert` to an out-of-bounds element will cause the Vec to grow (using @@ -417,3 +525,9 @@ where self.0[i] = element; } } + +impl From for String { + fn from(e: Error) -> String { + format!("{:?}", e) + } +} From 1128de535dd346f729e1d500ea7951271ca3750b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sat, 15 Jun 2019 19:11:02 -0400 Subject: [PATCH 09/40] Ignore attestations to zero hash --- beacon_node/beacon_chain/src/fork_choice.rs | 30 +++++++++++++++------ 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/beacon_node/beacon_chain/src/fork_choice.rs b/beacon_node/beacon_chain/src/fork_choice.rs index eadb69ce95..fbe8e3996d 100644 --- a/beacon_node/beacon_chain/src/fork_choice.rs +++ b/beacon_node/beacon_chain/src/fork_choice.rs @@ -103,15 +103,29 @@ impl ForkChoice { let block_hash = attestation.data.target_root; - // TODO: what happens when the target root is not the same slot as the block? - let block_slot = attestation - .data - .target_epoch - .start_slot(T::EthSpec::slots_per_epoch()); + // Ignore any attestations to the zero hash. + // + // This is an edge case that results from the spec aliasing the zero hash to the genesis + // block. Attesters may attest to the zero hash if they have never seen a block. + // + // We have two options here: + // + // 1. Apply all zero-hash attestations to the zero hash. + // 2. Ignore all attestations to the zero hash. + // + // (1) becomes weird once we hit finality and fork choice drops the genesis block. (2) is + // fine becuase votes to the genesis block are not usefully, all validators already + // implicitly attest to genesis just by being present in the chain. + if block_hash != Hash256::zero() { + let block_slot = attestation + .data + .target_epoch + .start_slot(T::EthSpec::slots_per_epoch()); - for validator_index in validator_indices { - self.backend - .process_message(validator_index, block_hash, block_slot)?; + for validator_index in validator_indices { + self.backend + .process_message(validator_index, block_hash, block_slot)?; + } } Ok(()) From 8fb6ffffe2135047e86f545170ce69cc96fe0f17 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sun, 16 Jun 2019 02:24:33 -0400 Subject: [PATCH 10/40] Add new beacon chain test harness --- beacon_node/beacon_chain/src/beacon_chain.rs | 4 +- beacon_node/beacon_chain/src/lib.rs | 1 + beacon_node/beacon_chain/src/test_utils.rs | 147 ++++++++++++++++++ .../slot_clock/src/testing_slot_clock.rs | 4 + 4 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 beacon_node/beacon_chain/src/test_utils.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 9e3f6b52cf..8602d32c77 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -88,6 +88,8 @@ impl BeaconChain { genesis_block: BeaconBlock, spec: ChainSpec, ) -> Result { + genesis_state.build_all_caches(&spec)?; + let state_root = genesis_state.canonical_root(); store.put(&state_root, &genesis_state)?; @@ -105,8 +107,6 @@ impl BeaconChain { state_root, )); - genesis_state.build_all_caches(&spec)?; - Ok(Self { spec, slot_clock, diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 4aa1370d97..67b0cee479 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -5,6 +5,7 @@ mod fork_choice; pub mod iter; mod metrics; mod persisted_beacon_chain; +mod test_utils; pub use self::beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessingOutcome}; pub use self::checkpoint::CheckPoint; diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs new file mode 100644 index 0000000000..be370ff651 --- /dev/null +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -0,0 +1,147 @@ +use crate::{BeaconChain, BeaconChainTypes, BlockProcessingOutcome}; +use lmd_ghost::{LmdGhost, ThreadSafeReducedTree}; +use slot_clock::SlotClock; +use slot_clock::TestingSlotClock; +use std::marker::PhantomData; +use std::sync::Arc; +use store::MemoryStore; +use tree_hash::{SignedRoot, TreeHash}; +use types::{ + test_utils::TestingBeaconStateBuilder, BeaconBlock, ChainSpec, Domain, EthSpec, Hash256, + Keypair, MinimalEthSpec, Signature, +}; + +pub struct CommonTypes +where + L: LmdGhost, + E: EthSpec, +{ + _phantom_l: PhantomData, + _phantom_e: PhantomData, +} + +impl BeaconChainTypes for CommonTypes +where + L: LmdGhost, + E: EthSpec, +{ + type Store = MemoryStore; + type SlotClock = TestingSlotClock; + type LmdGhost = L; + type EthSpec = E; +} + +pub struct BeaconChainHarness +where + L: LmdGhost, + E: EthSpec, +{ + chain: BeaconChain>, + keypairs: Vec, + spec: ChainSpec, +} + +impl BeaconChainHarness +where + L: LmdGhost, + E: EthSpec, +{ + pub fn new(validator_count: usize) -> Self { + let spec = E::default_spec(); + + let store = Arc::new(MemoryStore::open()); + + let state_builder = + TestingBeaconStateBuilder::from_default_keypairs_file_if_exists(validator_count, &spec); + let (genesis_state, keypairs) = state_builder.build(); + + let mut genesis_block = BeaconBlock::empty(&spec); + genesis_block.state_root = Hash256::from_slice(&genesis_state.tree_hash_root()); + + // Slot clock + let slot_clock = TestingSlotClock::new( + spec.genesis_slot, + genesis_state.genesis_time, + spec.seconds_per_slot, + ); + + let chain = BeaconChain::from_genesis( + store, + slot_clock, + genesis_state, + genesis_block, + spec.clone(), + ) + .expect("Terminate if beacon chain generation fails"); + + Self { + chain, + keypairs, + spec, + } + } + + pub fn extend_canonical_chain(&self) { + self.chain.slot_clock.advance_slot(); + self.chain.catchup_state().expect("should catchup state"); + + let block = self.build_block(); + let outcome = self + .chain + .process_block(block) + .expect("should process block"); + assert_eq!(outcome, BlockProcessingOutcome::Processed); + } + + fn build_block(&self) -> BeaconBlock { + let slot = self.chain.read_slot_clock().unwrap(); + + let sk = { + let proposer = self + .chain + .block_proposer(slot) + .expect("should get block propoer"); + &self.keypairs[proposer].sk + }; + + let fork = &self.chain.head().beacon_state.fork; + + let randao_reveal = { + let epoch = slot.epoch(E::slots_per_epoch()); + let message = epoch.tree_hash_root(); + let domain = self.spec.get_domain(epoch, Domain::Randao, fork); + Signature::new(&message, domain, sk) + }; + + let (mut block, _state) = self + .chain + .produce_block(randao_reveal) + .expect("should producer block"); + + block.signature = { + let message = block.signed_root(); + let epoch = block.slot.epoch(E::slots_per_epoch()); + let domain = self.spec.get_domain(epoch, Domain::BeaconProposer, fork); + Signature::new(&message, domain, sk) + }; + + block + } +} + +#[cfg(test)] +mod test { + use super::*; + + pub const VALIDATOR_COUNT: usize = 16; + + #[test] + fn build_on_genesis() { + let harness: BeaconChainHarness< + ThreadSafeReducedTree, + MinimalEthSpec, + > = BeaconChainHarness::new(VALIDATOR_COUNT); + + harness.extend_canonical_chain(); + } +} diff --git a/eth2/utils/slot_clock/src/testing_slot_clock.rs b/eth2/utils/slot_clock/src/testing_slot_clock.rs index fc9b7201bb..ab00d2baa7 100644 --- a/eth2/utils/slot_clock/src/testing_slot_clock.rs +++ b/eth2/utils/slot_clock/src/testing_slot_clock.rs @@ -15,6 +15,10 @@ impl TestingSlotClock { pub fn set_slot(&self, slot: u64) { *self.slot.write().expect("TestingSlotClock poisoned.") = Slot::from(slot); } + + pub fn advance_slot(&self) { + self.set_slot(self.present_slot().unwrap().unwrap().as_u64() + 1) + } } impl SlotClock for TestingSlotClock { From 1638a7aa6235942b6c07f020ec92e05ca8aec6d8 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sun, 16 Jun 2019 09:22:05 -0400 Subject: [PATCH 11/40] Alias zero hash to genesis in find head --- beacon_node/beacon_chain/src/fork_choice.rs | 14 ++++++++++++++ beacon_node/beacon_chain/src/test_utils.rs | 4 +++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/fork_choice.rs b/beacon_node/beacon_chain/src/fork_choice.rs index fbe8e3996d..0d132386d5 100644 --- a/beacon_node/beacon_chain/src/fork_choice.rs +++ b/beacon_node/beacon_chain/src/fork_choice.rs @@ -18,12 +18,18 @@ pub enum Error { pub struct ForkChoice { backend: T::LmdGhost, + /// Used for resolving the `0x00..00` alias back to genesis. + /// + /// Does not necessarily need to be the _actual_ genesis, it suffices to be the finalized root + /// whenever the struct was instantiated. + genesis_block_root: Hash256, } impl ForkChoice { pub fn new(store: Arc, genesis_block_root: Hash256) -> Self { Self { backend: T::LmdGhost::new(store, genesis_block_root), + genesis_block_root, } } @@ -41,11 +47,19 @@ impl ForkChoice { } else { state.finalized_root }; + let block = chain .store .get::(&block_root)? .ok_or_else(|| Error::MissingBlock(block_root))?; + // Resolve the `0x00.. 00` alias back to genesis + let block_root = if block_root == Hash256::zero() { + self.genesis_block_root + } else { + block_root + }; + let state = chain .store .get::>(&block.state_root)? diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index be370ff651..ece0a2fa21 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -142,6 +142,8 @@ mod test { MinimalEthSpec, > = BeaconChainHarness::new(VALIDATOR_COUNT); - harness.extend_canonical_chain(); + for _ in 0..MinimalEthSpec::slots_per_epoch() * 2 { + harness.extend_canonical_chain(); + } } } From f6c86d0f7ff53f277cc807dc4e78ed1bfbe16701 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sun, 16 Jun 2019 15:55:59 -0400 Subject: [PATCH 12/40] Add attestations to beacon chain harness --- beacon_node/beacon_chain/src/lib.rs | 2 +- beacon_node/beacon_chain/src/test_utils.rs | 76 ++++++++++++++++++++-- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 67b0cee479..cad8cc5e07 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -5,7 +5,7 @@ mod fork_choice; pub mod iter; mod metrics; mod persisted_beacon_chain; -mod test_utils; +pub mod test_utils; pub use self::beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessingOutcome}; pub use self::checkpoint::CheckPoint; diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index ece0a2fa21..b2eee12e18 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -1,5 +1,5 @@ use crate::{BeaconChain, BeaconChainTypes, BlockProcessingOutcome}; -use lmd_ghost::{LmdGhost, ThreadSafeReducedTree}; +use lmd_ghost::LmdGhost; use slot_clock::SlotClock; use slot_clock::TestingSlotClock; use std::marker::PhantomData; @@ -7,8 +7,9 @@ use std::sync::Arc; use store::MemoryStore; use tree_hash::{SignedRoot, TreeHash}; use types::{ - test_utils::TestingBeaconStateBuilder, BeaconBlock, ChainSpec, Domain, EthSpec, Hash256, - Keypair, MinimalEthSpec, Signature, + test_utils::TestingBeaconStateBuilder, AggregateSignature, Attestation, + AttestationDataAndCustodyBit, BeaconBlock, Bitfield, ChainSpec, Domain, EthSpec, Hash256, + Keypair, SecretKey, Signature, }; pub struct CommonTypes @@ -91,6 +92,8 @@ where .process_block(block) .expect("should process block"); assert_eq!(outcome, BlockProcessingOutcome::Processed); + + self.add_attestations_to_op_pool(); } fn build_block(&self) -> BeaconBlock { @@ -127,16 +130,81 @@ where block } + + fn add_attestations_to_op_pool(&self) { + let state = &self.chain.current_state(); + let spec = &self.spec; + let fork = &state.fork; + + state + .get_crosslink_committees_at_slot(state.slot) + .expect("should get committees") + .iter() + .for_each(|cc| { + let committee_size = cc.committee.len(); + + for (i, validator_index) in cc.committee.iter().enumerate() { + let data = self + .chain + .produce_attestation_data(cc.shard) + .expect("should produce attestation data"); + + let mut aggregation_bitfield = Bitfield::new(); + aggregation_bitfield.set(i, true); + aggregation_bitfield.set(committee_size, false); + + let mut custody_bitfield = Bitfield::new(); + custody_bitfield.set(committee_size, false); + + let signature = { + let message = AttestationDataAndCustodyBit { + data: data.clone(), + custody_bit: false, + } + .tree_hash_root(); + + let domain = spec.get_domain(data.target_epoch, Domain::Attestation, fork); + + let mut agg_sig = AggregateSignature::new(); + agg_sig.add(&Signature::new( + &message, + domain, + self.get_sk(*validator_index), + )); + + agg_sig + }; + + let attestation = Attestation { + aggregation_bitfield, + data, + custody_bitfield, + signature, + }; + + self.chain + .process_attestation(attestation) + .expect("should process attestation"); + } + }); + } + + fn get_sk(&self, validator_index: usize) -> &SecretKey { + &self.keypairs[validator_index].sk + } } #[cfg(test)] +#[cfg(not(debug_assertions))] mod test { use super::*; + use lmd_ghost::ThreadSafeReducedTree; + use types::MinimalEthSpec; pub const VALIDATOR_COUNT: usize = 16; #[test] - fn build_on_genesis() { + fn build_two_epochs_on_genesis() { let harness: BeaconChainHarness< ThreadSafeReducedTree, MinimalEthSpec, From 9c2bbb6c056372171db7f6204fe901c0d2b15be4 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sun, 16 Jun 2019 16:39:48 -0400 Subject: [PATCH 13/40] Add stubbed-out block processing to fork choice --- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- beacon_node/beacon_chain/src/fork_choice.rs | 5 ++++- eth2/lmd_ghost/src/lib.rs | 15 ++++++++++++--- eth2/lmd_ghost/src/reduced_tree.rs | 7 ++++++- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 8602d32c77..d7ba67a37d 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -624,7 +624,7 @@ impl BeaconChain { self.store.put(&state_root, &state)?; // Register the new block with the fork choice service. - self.fork_choice.process_block(&state, &block)?; + self.fork_choice.process_block(&state, &block, block_root)?; // Execute the fork choice algorithm, enthroning a new head if discovered. // diff --git a/beacon_node/beacon_chain/src/fork_choice.rs b/beacon_node/beacon_chain/src/fork_choice.rs index 0d132386d5..7aba6fdf5d 100644 --- a/beacon_node/beacon_chain/src/fork_choice.rs +++ b/beacon_node/beacon_chain/src/fork_choice.rs @@ -89,6 +89,7 @@ impl ForkChoice { &self, state: &BeaconState, block: &BeaconBlock, + block_root: Hash256, ) -> Result<()> { // Note: we never count the block as a latest message, only attestations. // @@ -100,6 +101,8 @@ impl ForkChoice { self.process_attestation_from_block(state, attestation)?; } + self.backend.process_block(block_root, block.slot)?; + Ok(()) } @@ -138,7 +141,7 @@ impl ForkChoice { for validator_index in validator_indices { self.backend - .process_message(validator_index, block_hash, block_slot)?; + .process_attestation(validator_index, block_hash, block_slot)?; } } diff --git a/eth2/lmd_ghost/src/lib.rs b/eth2/lmd_ghost/src/lib.rs index 59495e382c..8f5d0b5299 100644 --- a/eth2/lmd_ghost/src/lib.rs +++ b/eth2/lmd_ghost/src/lib.rs @@ -9,18 +9,27 @@ pub use reduced_tree::ThreadSafeReducedTree; pub type Result = std::result::Result; pub trait LmdGhost: Send + Sync { - fn new(store: Arc, genesis_root: Hash256) -> Self; + /// Create a new instance, with the given `store` and `finalized_root`. + fn new(store: Arc, finalized_root: Hash256) -> Self; - fn process_message( + /// Process an attestation message from some validator that attests to some `block_hash` + /// representing a block at some `block_slot`. + fn process_attestation( &self, validator_index: usize, block_hash: Hash256, block_slot: Slot, ) -> Result<()>; + /// Process a block that was seen on the network. + fn process_block(&self, block_hash: Hash256, block_slot: Slot) -> Result<()>; + + /// Returns the head of the chain, starting the search at `start_block_root` and moving upwards + /// (in block height). fn find_head(&self, start_block_root: Hash256, weight: F) -> Result where F: Fn(usize) -> Option + Copy; - fn update_finalized_root(&self, new_root: Hash256) -> Result<()>; + /// Provide an indication that the blockchain has been finalized at the given `finalized_root`. + fn update_finalized_root(&self, finalized_root: Hash256) -> Result<()>; } diff --git a/eth2/lmd_ghost/src/reduced_tree.rs b/eth2/lmd_ghost/src/reduced_tree.rs index 84249b603f..f6db34e36c 100644 --- a/eth2/lmd_ghost/src/reduced_tree.rs +++ b/eth2/lmd_ghost/src/reduced_tree.rs @@ -40,7 +40,7 @@ where } } - fn process_message( + fn process_attestation( &self, validator_index: usize, block_hash: Hash256, @@ -52,6 +52,11 @@ where .map_err(Into::into) } + /// Process a block that was seen on the network. + fn process_block(&self, block_hash: Hash256, block_slot: Slot) -> SuperResult<()> { + unimplemented!(); + } + fn find_head(&self, start_block_root: Hash256, weight_fn: F) -> SuperResult where F: Fn(usize) -> Option + Copy, From fd384e54f4219adbf265a11eaf21e668929d3555 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 17 Jun 2019 14:31:35 -0700 Subject: [PATCH 14/40] Add weightless blocks to reduced tree, fix bugs --- eth2/lmd_ghost/src/reduced_tree.rs | 114 ++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 27 deletions(-) diff --git a/eth2/lmd_ghost/src/reduced_tree.rs b/eth2/lmd_ghost/src/reduced_tree.rs index f6db34e36c..c48fd11d0d 100644 --- a/eth2/lmd_ghost/src/reduced_tree.rs +++ b/eth2/lmd_ghost/src/reduced_tree.rs @@ -13,6 +13,7 @@ pub enum Error { MissingNode(Hash256), MissingBlock(Hash256), MissingState(Hash256), + MissingChild(Hash256), NotInTree(Hash256), NoCommonAncestor((Hash256, Hash256)), StoreError(StoreError), @@ -49,12 +50,15 @@ where self.core .write() .process_message(validator_index, block_hash, block_slot) - .map_err(Into::into) + .map_err(|e| format!("process_attestation failed: {:?}", e)) } /// Process a block that was seen on the network. - fn process_block(&self, block_hash: Hash256, block_slot: Slot) -> SuperResult<()> { - unimplemented!(); + fn process_block(&self, block_hash: Hash256, _block_slot: Slot) -> SuperResult<()> { + self.core + .write() + .add_weightless_node(block_hash) + .map_err(|e| format!("process_block failed: {:?}", e)) } fn find_head(&self, start_block_root: Hash256, weight_fn: F) -> SuperResult @@ -64,11 +68,14 @@ where self.core .write() .update_weights_and_find_head(start_block_root, weight_fn) - .map_err(Into::into) + .map_err(|e| format!("find_head failed: {:?}", e)) } fn update_finalized_root(&self, new_root: Hash256) -> SuperResult<()> { - self.core.write().update_root(new_root).map_err(Into::into) + self.core + .write() + .update_root(new_root) + .map_err(|e| format!("update_finalized_root failed: {:?}", e)) } } @@ -111,7 +118,13 @@ where pub fn update_root(&mut self, new_root: Hash256) -> Result<()> { if !self.nodes.contains_key(&new_root) { - self.add_node(new_root, vec![])?; + let node = Node { + block_hash: new_root, + voters: vec![], + ..Node::default() + }; + + self.add_node(node)?; } self.retain_subtree(self.root, new_root)?; @@ -297,11 +310,19 @@ where let should_delete = { let node = self.get_node(hash)?.clone(); - if node.parent_hash.is_some() { + if let Some(parent_hash) = node.parent_hash { if (node.children.len() == 1) && !node.has_votes() { - let child_node = self.get_mut_node(node.children[0])?; + // Graft the child to it's grandparent. + let child_hash = { + let child_node = self.get_mut_node(node.children[0])?; + child_node.parent_hash = node.parent_hash; - child_node.parent_hash = node.parent_hash; + child_node.block_hash + }; + + // Graft the grandparent to it's grandchild. + let parent_node = self.get_mut_node(parent_hash)?; + parent_node.replace_child(node.block_hash, child_hash)?; true } else { @@ -324,57 +345,85 @@ where if let Ok(node) = self.get_mut_node(hash) { node.add_voter(validator_index); } else { - self.add_node(hash, vec![validator_index])?; + let node = Node { + block_hash: hash, + voters: vec![validator_index], + ..Node::default() + }; + + self.add_node(node)?; } Ok(()) } - fn add_node(&mut self, hash: Hash256, voters: Vec) -> Result<()> { + fn add_weightless_node(&mut self, hash: Hash256) -> Result<()> { + if !self.nodes.contains_key(&hash) { + let node = Node { + block_hash: hash, + ..Node::default() + }; + + self.add_node(node)?; + + if let Some(parent_hash) = self.get_node(hash)?.parent_hash { + self.maybe_delete_node(parent_hash)?; + } + } + + Ok(()) + } + + fn add_node(&mut self, mut node: Node) -> Result<()> { // Find the highest (by slot) ancestor of the given hash/block that is in the reduced tree. let mut prev_in_tree = { let hash = self - .find_prev_in_tree(hash) - .ok_or_else(|| Error::NotInTree(hash))?; + .find_prev_in_tree(node.block_hash) + .ok_or_else(|| Error::NotInTree(node.block_hash))?; self.get_mut_node(hash)?.clone() }; - let mut node = Node { - block_hash: hash, - parent_hash: Some(prev_in_tree.block_hash), - voters, - ..Node::default() - }; + let mut added_new_ancestor = false; - if prev_in_tree.does_not_have_children() { - node.parent_hash = Some(prev_in_tree.block_hash); - prev_in_tree.children.push(hash); - } else { + if !prev_in_tree.children.is_empty() { for &child_hash in &prev_in_tree.children { - let ancestor_hash = self.find_least_common_ancestor(hash, child_hash)?; + let ancestor_hash = self.find_least_common_ancestor(node.block_hash, child_hash)?; + if ancestor_hash != prev_in_tree.block_hash { let child = self.get_mut_node(child_hash)?; let common_ancestor = Node { block_hash: ancestor_hash, parent_hash: Some(prev_in_tree.block_hash), + children: vec![node.block_hash, child_hash], ..Node::default() }; child.parent_hash = Some(common_ancestor.block_hash); node.parent_hash = Some(common_ancestor.block_hash); + prev_in_tree.replace_child(child_hash, ancestor_hash)?; + self.nodes .insert(common_ancestor.block_hash, common_ancestor); + + added_new_ancestor = true; + + break; } } } + if !added_new_ancestor { + node.parent_hash = Some(prev_in_tree.block_hash); + prev_in_tree.children.push(node.block_hash); + } + // Update `prev_in_tree`. A mutable reference was not maintained to satisfy the borrow // checker. // // This is not an ideal solution and results in unnecessary memory copies -- a better // solution is certainly possible. self.nodes.insert(prev_in_tree.block_hash, prev_in_tree); - self.nodes.insert(hash, node); + self.nodes.insert(node.block_hash, node); Ok(()) } @@ -384,7 +433,7 @@ where fn find_prev_in_tree(&mut self, hash: Hash256) -> Option { self.iter_ancestors(hash) .ok()? - .find(|(root, _slit)| self.get_node(*root).is_ok()) + .find(|(root, _slot)| self.nodes.contains_key(root)) .and_then(|(root, _slot)| Some(root)) } @@ -469,7 +518,7 @@ where } } -#[derive(Default, Clone)] +#[derive(Default, Clone, Debug)] pub struct Node { pub parent_hash: Option, pub children: Vec, @@ -483,6 +532,17 @@ impl Node { self.children.is_empty() } + pub fn replace_child(&mut self, old: Hash256, new: Hash256) -> Result<()> { + let i = self + .children + .iter() + .position(|&c| c == old) + .ok_or_else(|| Error::MissingChild(old))?; + self.children[i] = new; + + Ok(()) + } + pub fn remove_voter(&mut self, voter: usize) -> Option { let i = self.voters.iter().position(|&v| v == voter)?; Some(self.voters.remove(i)) From c6e24572c7d68dc4b4a6097b1773635f8482ab30 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 17 Jun 2019 17:39:17 -0700 Subject: [PATCH 15/40] Improve chain harness tests --- beacon_node/beacon_chain/src/test_utils.rs | 26 +++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index b2eee12e18..e7a54580df 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -205,13 +205,37 @@ mod test { #[test] fn build_two_epochs_on_genesis() { + let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; + let harness: BeaconChainHarness< ThreadSafeReducedTree, MinimalEthSpec, > = BeaconChainHarness::new(VALIDATOR_COUNT); - for _ in 0..MinimalEthSpec::slots_per_epoch() * 2 { + for _ in 0..num_blocks_produced { harness.extend_canonical_chain(); } + + let state = &harness.chain.head().beacon_state; + + assert_eq!( + state.slot, num_blocks_produced, + "head should be at the current slot" + ); + assert_eq!( + state.current_epoch(), + num_blocks_produced / MinimalEthSpec::slots_per_epoch(), + "head should be at the expected epoch" + ); + assert_eq!( + state.current_justified_epoch, + state.current_epoch() - 1, + "the head should be justified one behind the current epoch" + ); + assert_eq!( + state.finalized_epoch, + state.current_epoch() - 1, + "the head should be finalized one behind the current epoch" + ); } } From 2b5c70711d8059eaa24bc42ec534d26ed05336ec Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 17 Jun 2019 17:39:33 -0700 Subject: [PATCH 16/40] Cover edge case in reduced tree --- eth2/lmd_ghost/src/reduced_tree.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/eth2/lmd_ghost/src/reduced_tree.rs b/eth2/lmd_ghost/src/reduced_tree.rs index c48fd11d0d..a0fc2d96e7 100644 --- a/eth2/lmd_ghost/src/reduced_tree.rs +++ b/eth2/lmd_ghost/src/reduced_tree.rs @@ -186,6 +186,13 @@ where where F: Fn(usize) -> Option + Copy, { + // It is possible that the given `start_block_root` is not in the reduced tree. + // + // In this case, we add a weightless node at `start_block_root`. + if !self.nodes.contains_key(&start_block_root) { + self.add_weightless_node(start_block_root)?; + }; + let _root_weight = self.update_weight(start_block_root, weight_fn)?; let start_node = self.get_node(start_block_root)?; @@ -389,6 +396,9 @@ where for &child_hash in &prev_in_tree.children { let ancestor_hash = self.find_least_common_ancestor(node.block_hash, child_hash)?; + // TODO: handle the case where the new block is a child of an existing node and a + // parent of an existing node. + if ancestor_hash != prev_in_tree.block_hash { let child = self.get_mut_node(child_hash)?; let common_ancestor = Node { From 952e08ba3898194a07243d7a87d4b8d2571fe2df Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 19 Jun 2019 01:47:21 +1000 Subject: [PATCH 17/40] Add state roots iter to store --- beacon_node/store/src/iter.rs | 156 +++++++++++++++++++++++++++++---- eth2/types/src/beacon_state.rs | 16 +++- 2 files changed, 154 insertions(+), 18 deletions(-) diff --git a/beacon_node/store/src/iter.rs b/beacon_node/store/src/iter.rs index 844837983b..2326052123 100644 --- a/beacon_node/store/src/iter.rs +++ b/beacon_node/store/src/iter.rs @@ -1,22 +1,73 @@ use crate::Store; +use std::borrow::Cow; use std::sync::Arc; use types::{BeaconBlock, BeaconState, BeaconStateError, EthSpec, Hash256, Slot}; -/// Extends `BlockRootsIterator`, returning `BeaconBlock` instances, instead of their roots. -pub struct BlockIterator { - roots: BlockRootsIterator, +#[derive(Clone)] +pub struct StateRootsIterator<'a, T: EthSpec, U> { + store: Arc, + beacon_state: Cow<'a, BeaconState>, + slot: Slot, } -impl BlockIterator { +impl<'a, T: EthSpec, U: Store> StateRootsIterator<'a, T, U> { /// Create a new iterator over all blocks in the given `beacon_state` and prior states. - pub fn new(store: Arc, beacon_state: BeaconState, start_slot: Slot) -> Self { + pub fn new(store: Arc, beacon_state: &'a BeaconState, start_slot: Slot) -> Self { + Self { + store, + beacon_state: Cow::Borrowed(beacon_state), + slot: start_slot, + } + } +} + +impl<'a, T: EthSpec, U: Store> Iterator for StateRootsIterator<'a, T, U> { + type Item = (Hash256, Slot); + + fn next(&mut self) -> Option { + if (self.slot == 0) || (self.slot > self.beacon_state.slot) { + return None; + } + + self.slot -= 1; + + match self.beacon_state.get_state_root(self.slot) { + Ok(root) => Some((*root, self.slot)), + Err(BeaconStateError::SlotOutOfBounds) => { + // Read a `BeaconState` from the store that has access to prior historical root. + let beacon_state: BeaconState = { + let new_state_root = self.beacon_state.get_oldest_state_root().ok()?; + + self.store.get(&new_state_root).ok()? + }?; + + self.beacon_state = Cow::Owned(beacon_state); + + let root = self.beacon_state.get_state_root(self.slot).ok()?; + + Some((*root, self.slot)) + } + _ => None, + } + } +} + +#[derive(Clone)] +/// Extends `BlockRootsIterator`, returning `BeaconBlock` instances, instead of their roots. +pub struct BlockIterator<'a, T: EthSpec, U> { + roots: BlockRootsIterator<'a, T, U>, +} + +impl<'a, T: EthSpec, U: Store> BlockIterator<'a, T, U> { + /// Create a new iterator over all blocks in the given `beacon_state` and prior states. + pub fn new(store: Arc, beacon_state: &'a BeaconState, start_slot: Slot) -> Self { Self { roots: BlockRootsIterator::new(store, beacon_state, start_slot), } } } -impl Iterator for BlockIterator { +impl<'a, T: EthSpec, U: Store> Iterator for BlockIterator<'a, T, U> { type Item = BeaconBlock; fn next(&mut self) -> Option { @@ -32,24 +83,25 @@ impl Iterator for BlockIterator { /// exhausted. /// /// Returns `None` for roots prior to genesis or when there is an error reading from `Store`. -pub struct BlockRootsIterator { +#[derive(Clone)] +pub struct BlockRootsIterator<'a, T: EthSpec, U> { store: Arc, - beacon_state: BeaconState, + beacon_state: Cow<'a, BeaconState>, slot: Slot, } -impl BlockRootsIterator { +impl<'a, T: EthSpec, U: Store> BlockRootsIterator<'a, T, U> { /// Create a new iterator over all block roots in the given `beacon_state` and prior states. - pub fn new(store: Arc, beacon_state: BeaconState, start_slot: Slot) -> Self { + pub fn new(store: Arc, beacon_state: &'a BeaconState, start_slot: Slot) -> Self { Self { slot: start_slot, - beacon_state, + beacon_state: Cow::Borrowed(beacon_state), store, } } } -impl Iterator for BlockRootsIterator { +impl<'a, T: EthSpec, U: Store> Iterator for BlockRootsIterator<'a, T, U> { type Item = (Hash256, Slot); fn next(&mut self) -> Option { @@ -63,14 +115,16 @@ impl Iterator for BlockRootsIterator { Ok(root) => Some((*root, self.slot)), Err(BeaconStateError::SlotOutOfBounds) => { // Read a `BeaconState` from the store that has access to prior historical root. - self.beacon_state = { + let beacon_state: BeaconState = { // Load the earlier state from disk. Skip forward one slot, because a state // doesn't return it's own state root. - let new_state_root = self.beacon_state.get_state_root(self.slot + 1).ok()?; + let new_state_root = self.beacon_state.get_oldest_state_root().ok()?; self.store.get(&new_state_root).ok()? }?; + self.beacon_state = Cow::Owned(beacon_state); + let root = self.beacon_state.get_block_root(self.slot).ok()?; Some((*root, self.slot)) @@ -97,7 +151,7 @@ mod test { } #[test] - fn root_iter() { + fn block_root_iter() { let store = Arc::new(MemoryStore::open()); let slots_per_historical_root = MainnetEthSpec::slots_per_historical_root(); @@ -120,7 +174,13 @@ mod test { state_b.latest_state_roots[0] = state_a_root; store.put(&state_a_root, &state_a).unwrap(); - let iter = BlockRootsIterator::new(store.clone(), state_b.clone(), state_b.slot - 1); + let iter = BlockRootsIterator::new(store.clone(), &state_b, state_b.slot - 1); + + assert!( + iter.clone().find(|(_root, slot)| *slot == 0).is_some(), + "iter should contain zero slot" + ); + let mut collected: Vec<(Hash256, Slot)> = iter.collect(); collected.reverse(); @@ -132,4 +192,68 @@ mod test { assert_eq!(collected[i].0, Hash256::from(i as u64)); } } + + #[test] + fn state_root_iter() { + let store = Arc::new(MemoryStore::open()); + let slots_per_historical_root = MainnetEthSpec::slots_per_historical_root(); + + let mut state_a: BeaconState = get_state(); + let mut state_b: BeaconState = get_state(); + + state_a.slot = Slot::from(slots_per_historical_root); + state_b.slot = Slot::from(slots_per_historical_root * 2); + + let mut hashes = (0..).into_iter().map(|i| Hash256::from(i)); + + for slot in 0..slots_per_historical_root { + state_a + .set_state_root(Slot::from(slot), hashes.next().unwrap()) + .expect(&format!("should set state_a slot {}", slot)); + } + for slot in slots_per_historical_root..slots_per_historical_root * 2 { + state_b + .set_state_root(Slot::from(slot), hashes.next().unwrap()) + .expect(&format!("should set state_b slot {}", slot)); + } + + /* + for root in &mut state_a.latest_state_roots[..] { + state_a.set_state_root(slots.next().unwrap(), hashes.next().unwrap()); + // *root = hashes.next().unwrap() + } + for root in &mut state_b.latest_state_roots[..] { + state_b.set_state_root(slots.next().unwrap(), hashes.next().unwrap()); + *root = hashes.next().unwrap() + } + */ + + let state_a_root = Hash256::from(slots_per_historical_root as u64); + let state_b_root = Hash256::from(slots_per_historical_root as u64 * 2); + + store.put(&state_a_root, &state_a).unwrap(); + store.put(&state_b_root, &state_b).unwrap(); + + let iter = StateRootsIterator::new(store.clone(), &state_b, state_b.slot - 1); + + assert!( + iter.clone().find(|(_root, slot)| *slot == 0).is_some(), + "iter should contain zero slot" + ); + + let mut collected: Vec<(Hash256, Slot)> = iter.collect(); + collected.reverse(); + + let expected_len = MainnetEthSpec::slots_per_historical_root() * 2 - 1; + + assert_eq!(collected.len(), expected_len, "collection length incorrect"); + + for i in 0..expected_len { + let (hash, slot) = collected[i]; + + assert_eq!(slot, i as u64, "slot mismatch at {}: {} vs {}", i, slot, i); + + assert_eq!(hash, Hash256::from(i as u64), "hash mismatch at {}", i); + } + } } diff --git a/eth2/types/src/beacon_state.rs b/eth2/types/src/beacon_state.rs index f76676a2b5..58237fe6f3 100644 --- a/eth2/types/src/beacon_state.rs +++ b/eth2/types/src/beacon_state.rs @@ -569,7 +569,7 @@ impl BeaconState { /// /// Spec v0.6.3 fn get_latest_state_roots_index(&self, slot: Slot) -> Result { - if (slot < self.slot) && (self.slot <= slot + self.latest_state_roots.len() as u64) { + if (slot < self.slot) && (self.slot <= slot + Slot::from(self.latest_state_roots.len())) { Ok(slot.as_usize() % self.latest_state_roots.len()) } else { Err(BeaconStateError::SlotOutOfBounds) @@ -579,11 +579,23 @@ impl BeaconState { /// Gets the state root for some slot. /// /// Spec v0.6.3 - pub fn get_state_root(&mut self, slot: Slot) -> Result<&Hash256, Error> { + pub fn get_state_root(&self, slot: Slot) -> Result<&Hash256, Error> { let i = self.get_latest_state_roots_index(slot)?; Ok(&self.latest_state_roots[i]) } + /// Gets the oldest (earliest slot) state root. + /// + /// Spec v0.5.1 + pub fn get_oldest_state_root(&self) -> Result<&Hash256, Error> { + let lookback = std::cmp::min( + self.slot - Slot::from(self.latest_state_roots.len()), + self.slot, + ); + let i = self.get_latest_state_roots_index(self.slot - lookback)?; + Ok(&self.latest_state_roots[i]) + } + /// Sets the latest state root for slot. /// /// Spec v0.6.3 From 55196dff6424e83e968c83ff75d2ba65754c8ddb Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 19 Jun 2019 02:06:23 +1000 Subject: [PATCH 18/40] Remove iter mod from beacon chain Now the iter mod in store is the only implementation --- beacon_node/beacon_chain/src/beacon_chain.rs | 14 +- beacon_node/beacon_chain/src/iter.rs | 133 ------------------- beacon_node/beacon_chain/src/lib.rs | 1 - beacon_node/store/src/iter.rs | 25 +++- eth2/lmd_ghost/src/reduced_tree.rs | 2 +- 5 files changed, 36 insertions(+), 139 deletions(-) delete mode 100644 beacon_node/beacon_chain/src/iter.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index d7ba67a37d..920d8c3201 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1,7 +1,6 @@ use crate::checkpoint::CheckPoint; use crate::errors::{BeaconChainError as Error, BlockProductionError}; use crate::fork_choice::{Error as ForkChoiceError, ForkChoice}; -use crate::iter::{BlockIterator, BlockRootsIterator}; use crate::metrics::Metrics; use crate::persisted_beacon_chain::{PersistedBeaconChain, BEACON_CHAIN_DB_KEY}; use lmd_ghost::LmdGhost; @@ -19,6 +18,7 @@ use state_processing::{ per_slot_processing, BlockProcessingError, }; use std::sync::Arc; +use store::iter::{BlockIterator, BlockRootsIterator, StateRootsIterator}; use store::{Error as DBError, Store}; use tree_hash::TreeHash; use types::*; @@ -203,7 +203,7 @@ impl BeaconChain { /// /// Contains duplicate headers when skip slots are encountered. pub fn rev_iter_blocks(&self, slot: Slot) -> BlockIterator { - BlockIterator::new(self.store.clone(), self.state.read().clone(), slot) + BlockIterator::owned(self.store.clone(), self.state.read().clone(), slot) } /// Iterates in reverse (highest to lowest slot) through all block roots from `slot` through to @@ -213,7 +213,15 @@ impl BeaconChain { /// /// Contains duplicate roots when skip slots are encountered. pub fn rev_iter_block_roots(&self, slot: Slot) -> BlockRootsIterator { - BlockRootsIterator::new(self.store.clone(), self.state.read().clone(), slot) + BlockRootsIterator::owned(self.store.clone(), self.state.read().clone(), slot) + } + + /// Iterates in reverse (highest to lowest slot) through all state roots from `slot` through to + /// genesis. + /// + /// Returns `None` for roots prior to genesis or when there is an error reading from `Store`. + pub fn rev_iter_state_roots(&self, slot: Slot) -> StateRootsIterator { + StateRootsIterator::owned(self.store.clone(), self.state.read().clone(), slot) } /// Returns the block at the given root, if any. diff --git a/beacon_node/beacon_chain/src/iter.rs b/beacon_node/beacon_chain/src/iter.rs deleted file mode 100644 index 1b5e382b02..0000000000 --- a/beacon_node/beacon_chain/src/iter.rs +++ /dev/null @@ -1,133 +0,0 @@ -use std::sync::Arc; -use store::Store; -use types::{BeaconBlock, BeaconState, BeaconStateError, EthSpec, Hash256, Slot}; - -/// Extends `BlockRootsIterator`, returning `BeaconBlock` instances, instead of their roots. -pub struct BlockIterator { - roots: BlockRootsIterator, -} - -impl BlockIterator { - /// Create a new iterator over all blocks in the given `beacon_state` and prior states. - pub fn new(store: Arc, beacon_state: BeaconState, start_slot: Slot) -> Self { - Self { - roots: BlockRootsIterator::new(store, beacon_state, start_slot), - } - } -} - -impl Iterator for BlockIterator { - type Item = BeaconBlock; - - fn next(&mut self) -> Option { - let root = self.roots.next()?; - self.roots.store.get(&root).ok()? - } -} - -/// Iterates backwards through block roots. -/// -/// Uses the `latest_block_roots` field of `BeaconState` to as the source of block roots and will -/// perform a lookup on the `Store` for a prior `BeaconState` if `latest_block_roots` has been -/// exhausted. -/// -/// Returns `None` for roots prior to genesis or when there is an error reading from `Store`. -pub struct BlockRootsIterator { - store: Arc, - beacon_state: BeaconState, - slot: Slot, -} - -impl BlockRootsIterator { - /// Create a new iterator over all block roots in the given `beacon_state` and prior states. - pub fn new(store: Arc, beacon_state: BeaconState, start_slot: Slot) -> Self { - Self { - slot: start_slot, - beacon_state, - store, - } - } -} - -impl Iterator for BlockRootsIterator { - type Item = Hash256; - - fn next(&mut self) -> Option { - if (self.slot == 0) || (self.slot > self.beacon_state.slot) { - return None; - } - - self.slot -= 1; - - match self.beacon_state.get_block_root(self.slot) { - Ok(root) => Some(*root), - Err(BeaconStateError::SlotOutOfBounds) => { - // Read a `BeaconState` from the store that has access to prior historical root. - self.beacon_state = { - // Load the earlier state from disk. Skip forward one slot, because a state - // doesn't return it's own state root. - let new_state_root = self.beacon_state.get_state_root(self.slot + 1).ok()?; - - self.store.get(&new_state_root).ok()? - }?; - - self.beacon_state.get_block_root(self.slot).ok().cloned() - } - _ => None, - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use store::MemoryStore; - use types::{test_utils::TestingBeaconStateBuilder, Keypair, MainnetEthSpec}; - - fn get_state() -> BeaconState { - let builder = TestingBeaconStateBuilder::from_single_keypair( - 0, - &Keypair::random(), - &T::default_spec(), - ); - let (state, _keypairs) = builder.build(); - state - } - - #[test] - fn root_iter() { - let store = Arc::new(MemoryStore::open()); - let slots_per_historical_root = MainnetEthSpec::slots_per_historical_root(); - - let mut state_a: BeaconState = get_state(); - let mut state_b: BeaconState = get_state(); - - state_a.slot = Slot::from(slots_per_historical_root); - state_b.slot = Slot::from(slots_per_historical_root * 2); - - let mut hashes = (0..).into_iter().map(|i| Hash256::from(i)); - - for root in &mut state_a.latest_block_roots[..] { - *root = hashes.next().unwrap() - } - for root in &mut state_b.latest_block_roots[..] { - *root = hashes.next().unwrap() - } - - let state_a_root = hashes.next().unwrap(); - state_b.latest_state_roots[0] = state_a_root; - store.put(&state_a_root, &state_a).unwrap(); - - let iter = BlockRootsIterator::new(store.clone(), state_b.clone(), state_b.slot - 1); - let mut collected: Vec = iter.collect(); - collected.reverse(); - - let expected_len = 2 * MainnetEthSpec::slots_per_historical_root() - 1; - - assert_eq!(collected.len(), expected_len); - - for i in 0..expected_len { - assert_eq!(collected[i], Hash256::from(i as u64)); - } - } -} diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index cad8cc5e07..df1de153a2 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -2,7 +2,6 @@ mod beacon_chain; mod checkpoint; mod errors; mod fork_choice; -pub mod iter; mod metrics; mod persisted_beacon_chain; pub mod test_utils; diff --git a/beacon_node/store/src/iter.rs b/beacon_node/store/src/iter.rs index 2326052123..e8b9a27e97 100644 --- a/beacon_node/store/src/iter.rs +++ b/beacon_node/store/src/iter.rs @@ -11,7 +11,6 @@ pub struct StateRootsIterator<'a, T: EthSpec, U> { } impl<'a, T: EthSpec, U: Store> StateRootsIterator<'a, T, U> { - /// Create a new iterator over all blocks in the given `beacon_state` and prior states. pub fn new(store: Arc, beacon_state: &'a BeaconState, start_slot: Slot) -> Self { Self { store, @@ -19,6 +18,14 @@ impl<'a, T: EthSpec, U: Store> StateRootsIterator<'a, T, U> { slot: start_slot, } } + + pub fn owned(store: Arc, beacon_state: BeaconState, start_slot: Slot) -> Self { + Self { + slot: start_slot, + beacon_state: Cow::Owned(beacon_state), + store, + } + } } impl<'a, T: EthSpec, U: Store> Iterator for StateRootsIterator<'a, T, U> { @@ -65,6 +72,13 @@ impl<'a, T: EthSpec, U: Store> BlockIterator<'a, T, U> { roots: BlockRootsIterator::new(store, beacon_state, start_slot), } } + + /// Create a new iterator over all blocks in the given `beacon_state` and prior states. + pub fn owned(store: Arc, beacon_state: BeaconState, start_slot: Slot) -> Self { + Self { + roots: BlockRootsIterator::owned(store, beacon_state, start_slot), + } + } } impl<'a, T: EthSpec, U: Store> Iterator for BlockIterator<'a, T, U> { @@ -99,6 +113,15 @@ impl<'a, T: EthSpec, U: Store> BlockRootsIterator<'a, T, U> { store, } } + + /// Create a new iterator over all block roots in the given `beacon_state` and prior states. + pub fn owned(store: Arc, beacon_state: BeaconState, start_slot: Slot) -> Self { + Self { + slot: start_slot, + beacon_state: Cow::Owned(beacon_state), + store, + } + } } impl<'a, T: EthSpec, U: Store> Iterator for BlockRootsIterator<'a, T, U> { diff --git a/eth2/lmd_ghost/src/reduced_tree.rs b/eth2/lmd_ghost/src/reduced_tree.rs index a0fc2d96e7..9a869c1ebf 100644 --- a/eth2/lmd_ghost/src/reduced_tree.rs +++ b/eth2/lmd_ghost/src/reduced_tree.rs @@ -496,7 +496,7 @@ where let block = self.get_block(child)?; let state = self.get_state(block.state_root)?; - Ok(BlockRootsIterator::new( + Ok(BlockRootsIterator::owned( self.store.clone(), state, block.slot, From 55818e285a96fc086ab162c57c01ec28d6798c26 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 19 Jun 2019 03:01:58 +1000 Subject: [PATCH 19/40] Refactor block prod. to produce for forks --- beacon_node/beacon_chain/src/beacon_chain.rs | 47 +++++++++++-- beacon_node/beacon_chain/src/errors.rs | 3 + beacon_node/beacon_chain/src/test_utils.rs | 70 +++++++++++++++----- beacon_node/network/src/sync/simple_sync.rs | 16 ++--- 4 files changed, 100 insertions(+), 36 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 920d8c3201..c680498dca 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -23,6 +23,12 @@ use store::{Error as DBError, Store}; use tree_hash::TreeHash; use types::*; +// Text included in blocks. +// Must be 32-bytes or panic. +// +// |-------must be this long------| +pub const GRAFFITI: &str = "sigp/lighthouse-0.0.0-prerelease"; + #[derive(Debug, PartialEq)] pub enum BlockProcessingOutcome { /// Block was valid and imported into the block graph. @@ -657,16 +663,41 @@ impl BeaconChain { &self, randao_reveal: Signature, ) -> Result<(BeaconBlock, BeaconState), BlockProductionError> { - debug!("Producing block at slot {}...", self.state.read().slot); + let state = self.state.read().clone(); + let slot = self + .read_slot_clock() + .ok_or_else(|| BlockProductionError::UnableToReadSlot)?; + + self.produce_block_on_state(state, slot, randao_reveal) + } + + /// Produce a block for some `slot` upon the given `state`. + /// + /// Typically the `self.produce_block()` function should be used, instead of calling this + /// function directly. This function is useful for purposefully creating forks or blocks at + /// non-current slots. + /// + /// The given state will be advanced to the given `produce_at_slot`, then a block will be + /// produced at that slot height. + pub fn produce_block_on_state( + &self, + mut state: BeaconState, + produce_at_slot: Slot, + randao_reveal: Signature, + ) -> Result<(BeaconBlock, BeaconState), BlockProductionError> { self.metrics.block_production_requests.inc(); let timer = self.metrics.block_production_times.start_timer(); - let mut state = self.state.read().clone(); + // If required, transition the new state to the present slot. + for _ in state.slot.as_u64()..produce_at_slot.as_u64() { + // Ensure the next epoch state caches are built in case of an epoch transition. + state.build_committee_cache(RelativeEpoch::Next, &self.spec)?; + + per_slot_processing(&mut state, &self.spec)?; + } state.build_committee_cache(RelativeEpoch::Current, &self.spec)?; - trace!("Finding attestations for new block..."); - let previous_block_root = if state.slot > 0 { *state .get_block_root(state.slot - 1) @@ -675,8 +706,11 @@ impl BeaconChain { state.latest_block_header.canonical_root() }; + let mut graffiti: [u8; 32] = [0; 32]; + graffiti.copy_from_slice(GRAFFITI.as_bytes()); + let (proposer_slashings, attester_slashings) = - self.op_pool.get_slashings(&*self.state.read(), &self.spec); + self.op_pool.get_slashings(&state, &self.spec); let mut block = BeaconBlock { slot: state.slot, @@ -691,8 +725,7 @@ impl BeaconChain { deposit_root: Hash256::zero(), block_hash: Hash256::zero(), }, - // TODO: badass Lighthouse graffiti - graffiti: [0; 32], + graffiti, proposer_slashings, attester_slashings, attestations: self diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 8e04948df0..157d774c6f 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -40,9 +40,12 @@ impl From for BeaconChainError { #[derive(Debug, PartialEq)] pub enum BlockProductionError { UnableToGetBlockRootFromState, + UnableToReadSlot, + SlotProcessingError(SlotProcessingError), BlockProcessingError(BlockProcessingError), BeaconStateError(BeaconStateError), } easy_from_to!(BlockProcessingError, BlockProductionError); easy_from_to!(BeaconStateError, BlockProductionError); +easy_from_to!(SlotProcessingError, BlockProductionError); diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index e7a54580df..fe731ac3e0 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -5,13 +5,20 @@ use slot_clock::TestingSlotClock; use std::marker::PhantomData; use std::sync::Arc; use store::MemoryStore; +use store::Store; use tree_hash::{SignedRoot, TreeHash}; use types::{ test_utils::TestingBeaconStateBuilder, AggregateSignature, Attestation, - AttestationDataAndCustodyBit, BeaconBlock, Bitfield, ChainSpec, Domain, EthSpec, Hash256, - Keypair, SecretKey, Signature, + AttestationDataAndCustodyBit, BeaconBlock, BeaconState, Bitfield, ChainSpec, Domain, EthSpec, + Hash256, Keypair, RelativeEpoch, SecretKey, Signature, Slot, }; +#[derive(Clone, Copy)] +pub enum BuildStrategy { + OnCanonicalHead, + ForkCanonicalChainAt(Slot), +} + pub struct CommonTypes where L: LmdGhost, @@ -82,11 +89,11 @@ where } } - pub fn extend_canonical_chain(&self) { + pub fn extend_chain(&self, build_strategy: BuildStrategy) { self.chain.slot_clock.advance_slot(); self.chain.catchup_state().expect("should catchup state"); - let block = self.build_block(); + let block = self.build_block(build_strategy); let outcome = self .chain .process_block(block) @@ -96,18 +103,47 @@ where self.add_attestations_to_op_pool(); } - fn build_block(&self) -> BeaconBlock { - let slot = self.chain.read_slot_clock().unwrap(); + fn get_state(&self, build_strategy: BuildStrategy) -> BeaconState { + match build_strategy { + BuildStrategy::OnCanonicalHead => self.chain.current_state().clone(), + BuildStrategy::ForkCanonicalChainAt(fork_slot) => { + let state_root = self + .chain + .rev_iter_state_roots(self.chain.head().beacon_state.slot - 1) + .find(|(_hash, slot)| *slot == fork_slot) + .map(|(hash, _slot)| hash) + .expect("could not find state root for fork"); - let sk = { - let proposer = self - .chain - .block_proposer(slot) - .expect("should get block propoer"); - &self.keypairs[proposer].sk + self.chain + .store + .get(&state_root) + .expect("should read db") + .expect("should find state root") + } + } + } + + fn build_block(&self, build_strategy: BuildStrategy) -> BeaconBlock { + let mut state = self.get_state(build_strategy); + state.build_all_caches(&self.spec).unwrap(); + + let slot = match build_strategy { + BuildStrategy::OnCanonicalHead => self.chain.read_slot_clock().unwrap(), + BuildStrategy::ForkCanonicalChainAt(slot) => slot, }; - let fork = &self.chain.head().beacon_state.fork; + let proposer_index = match build_strategy { + BuildStrategy::OnCanonicalHead => self + .chain + .block_proposer(slot) + .expect("should get block proposer from chain"), + _ => state + .get_beacon_proposer_index(slot, RelativeEpoch::Current, &self.spec) + .expect("should get block proposer from state"), + }; + + let sk = &self.keypairs[proposer_index].sk; + let fork = &state.fork.clone(); let randao_reveal = { let epoch = slot.epoch(E::slots_per_epoch()); @@ -118,8 +154,8 @@ where let (mut block, _state) = self .chain - .produce_block(randao_reveal) - .expect("should producer block"); + .produce_block_on_state(state, slot, randao_reveal) + .expect("should produce block"); block.signature = { let message = block.signed_root(); @@ -195,7 +231,7 @@ where } #[cfg(test)] -#[cfg(not(debug_assertions))] +// #[cfg(not(debug_assertions))] mod test { use super::*; use lmd_ghost::ThreadSafeReducedTree; @@ -213,7 +249,7 @@ mod test { > = BeaconChainHarness::new(VALIDATOR_COUNT); for _ in 0..num_blocks_produced { - harness.extend_canonical_chain(); + harness.extend_chain(BuildStrategy::OnCanonicalHead); } let state = &harness.chain.head().beacon_state; diff --git a/beacon_node/network/src/sync/simple_sync.rs b/beacon_node/network/src/sync/simple_sync.rs index 403a8c54b8..528edca98e 100644 --- a/beacon_node/network/src/sync/simple_sync.rs +++ b/beacon_node/network/src/sync/simple_sync.rs @@ -182,7 +182,7 @@ impl SimpleSync { && (!self .chain .rev_iter_block_roots(local.best_slot) - .any(|root| root == remote.latest_finalized_root)) + .any(|(root, _slot)| root == remote.latest_finalized_root)) && (local.latest_finalized_root != spec.zero_hash) && (remote.latest_finalized_root != spec.zero_hash) { @@ -266,11 +266,12 @@ impl SimpleSync { "start_slot" => req.start_slot, ); - let mut roots: Vec = self + let mut roots: Vec = self .chain .rev_iter_block_roots(req.start_slot + req.count) .skip(1) .take(req.count as usize) + .map(|(block_root, slot)| BlockRootSlot { slot, block_root }) .collect(); if roots.len() as u64 != req.count { @@ -285,16 +286,6 @@ impl SimpleSync { } roots.reverse(); - - let mut roots: Vec = roots - .iter() - .enumerate() - .map(|(i, block_root)| BlockRootSlot { - slot: req.start_slot + Slot::from(i), - block_root: *block_root, - }) - .collect(); - roots.dedup_by_key(|brs| brs.block_root); network.send_rpc_response( @@ -392,6 +383,7 @@ impl SimpleSync { .chain .rev_iter_block_roots(req.start_slot + (count - 1)) .take(count as usize) + .map(|(root, _slot)| root) .collect(); roots.reverse(); From 5a98502ad6f30c192da16f7765bd276b31a9acf8 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 19 Jun 2019 03:40:30 +1000 Subject: [PATCH 20/40] Fix bug in epoch trans. finalization --- eth2/state_processing/src/per_epoch_processing.rs | 6 +++--- .../src/per_epoch_processing/validator_statuses.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eth2/state_processing/src/per_epoch_processing.rs b/eth2/state_processing/src/per_epoch_processing.rs index 26091d49ef..be42134081 100644 --- a/eth2/state_processing/src/per_epoch_processing.rs +++ b/eth2/state_processing/src/per_epoch_processing.rs @@ -122,17 +122,17 @@ pub fn process_justification_and_finalization( state.finalized_root = *state.get_block_root_at_epoch(state.finalized_epoch)?; } // The 2nd/3rd most recent epochs are both justified, the 2nd using the 3rd as source. - if (bitfield >> 1) % 4 == 0b11 && state.previous_justified_epoch == current_epoch - 2 { + if (bitfield >> 1) % 4 == 0b11 && old_previous_justified_epoch == current_epoch - 2 { state.finalized_epoch = old_previous_justified_epoch; state.finalized_root = *state.get_block_root_at_epoch(state.finalized_epoch)?; } // The 1st/2nd/3rd most recent epochs are all justified, the 1st using the 2nd as source. - if bitfield % 8 == 0b111 && state.current_justified_epoch == current_epoch - 2 { + if bitfield % 8 == 0b111 && old_current_justified_epoch == current_epoch - 2 { state.finalized_epoch = old_current_justified_epoch; state.finalized_root = *state.get_block_root_at_epoch(state.finalized_epoch)?; } // The 1st/2nd most recent epochs are both justified, the 1st using the 2nd as source. - if bitfield % 4 == 0b11 && state.current_justified_epoch == current_epoch - 1 { + if bitfield % 4 == 0b11 && old_current_justified_epoch == current_epoch - 1 { state.finalized_epoch = old_current_justified_epoch; state.finalized_root = *state.get_block_root_at_epoch(state.finalized_epoch)?; } diff --git a/eth2/state_processing/src/per_epoch_processing/validator_statuses.rs b/eth2/state_processing/src/per_epoch_processing/validator_statuses.rs index ece9174ca6..e20aa6cf25 100644 --- a/eth2/state_processing/src/per_epoch_processing/validator_statuses.rs +++ b/eth2/state_processing/src/per_epoch_processing/validator_statuses.rs @@ -125,7 +125,7 @@ impl ValidatorStatus { /// The total effective balances for different sets of validators during the previous and current /// epochs. -#[derive(Default, Clone)] +#[derive(Default, Clone, Debug)] pub struct TotalBalances { /// The total effective balance of all active validators during the _current_ epoch. pub current_epoch: u64, From 41a36da765c7cdccde0c5e43b1988b7ab149d314 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 19 Jun 2019 03:40:45 +1000 Subject: [PATCH 21/40] Update `BeaconChain` tests --- beacon_node/beacon_chain/src/test_utils.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index fe731ac3e0..097ad656cf 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -240,7 +240,7 @@ mod test { pub const VALIDATOR_COUNT: usize = 16; #[test] - fn build_two_epochs_on_genesis() { + fn can_finalize() { let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; let harness: BeaconChainHarness< @@ -270,8 +270,8 @@ mod test { ); assert_eq!( state.finalized_epoch, - state.current_epoch() - 1, - "the head should be finalized one behind the current epoch" + state.current_epoch() - 2, + "the head should be finalized two behind the current epoch" ); } } From 85b23f9f1b7bcc035dd688b9769a835c99a466bd Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 20 Jun 2019 10:59:19 +1000 Subject: [PATCH 22/40] Add incomplete progress on fork choice --- beacon_node/beacon_chain/src/beacon_chain.rs | 45 +++---- beacon_node/beacon_chain/src/test_utils.rs | 119 +++++++++++++++---- eth2/types/src/beacon_state.rs | 3 + 3 files changed, 119 insertions(+), 48 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index c680498dca..86e0ded3ce 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4,7 +4,7 @@ use crate::fork_choice::{Error as ForkChoiceError, ForkChoice}; use crate::metrics::Metrics; use crate::persisted_beacon_chain::{PersistedBeaconChain, BEACON_CHAIN_DB_KEY}; use lmd_ghost::LmdGhost; -use log::{debug, trace}; +use log::trace; use operation_pool::DepositInsertStatus; use operation_pool::OperationPool; use parking_lot::{RwLock, RwLockReadGuard}; @@ -300,18 +300,20 @@ impl BeaconChain { _ => return Err(Error::UnableToReadSlot), }; - let mut state = self.state.write(); + if self.state.read().slot < present_slot { + let mut state = self.state.write(); - // If required, transition the new state to the present slot. - for _ in state.slot.as_u64()..present_slot.as_u64() { - // Ensure the next epoch state caches are built in case of an epoch transition. - state.build_committee_cache(RelativeEpoch::Next, spec)?; + // If required, transition the new state to the present slot. + for _ in state.slot.as_u64()..present_slot.as_u64() { + // Ensure the next epoch state caches are built in case of an epoch transition. + state.build_committee_cache(RelativeEpoch::Next, spec)?; - per_slot_processing(&mut *state, spec)?; + per_slot_processing(&mut *state, spec)?; + } + + state.build_all_caches(spec)?; } - state.build_all_caches(spec)?; - Ok(()) } @@ -382,13 +384,14 @@ impl BeaconChain { /// Returns the block proposer for a given slot. /// - /// Information is read from the present `beacon_state` shuffling, so only information from the - /// present and prior epoch is available. - pub fn block_proposer(&self, slot: Slot) -> Result { - self.state - .write() - .build_committee_cache(RelativeEpoch::Current, &self.spec)?; + /// Information is read from the present `beacon_state` shuffling, only information from the + /// present epoch is available. + pub fn block_proposer(&self, slot: Slot) -> Result { + // Ensures that the present state has been advanced to the present slot, skipping slots if + // blocks are not present. + self.catchup_state()?; + // TODO: permit lookups of the proposer at any slot. let index = self.state.read().get_beacon_proposer_index( slot, RelativeEpoch::Current, @@ -559,6 +562,7 @@ impl BeaconChain { .read() .finalized_epoch .start_slot(T::EthSpec::slots_per_epoch()); + if block.slot <= finalized_slot { return Ok(BlockProcessingOutcome::FinalizedSlot); } @@ -573,7 +577,9 @@ impl BeaconChain { return Ok(BlockProcessingOutcome::GenesisBlock); } - let present_slot = self.present_slot(); + let present_slot = self + .read_slot_clock() + .ok_or_else(|| Error::UnableToReadSlot)?; if block.slot > present_slot { return Ok(BlockProcessingOutcome::FutureSlot { @@ -719,8 +725,8 @@ impl BeaconChain { signature: Signature::empty_signature(), // To be completed by a validator. body: BeaconBlockBody { randao_reveal, + // TODO: replace with real data. eth1_data: Eth1Data { - // TODO: replace with real data deposit_count: 0, deposit_root: Hash256::zero(), block_hash: Hash256::zero(), @@ -739,11 +745,6 @@ impl BeaconChain { }, }; - debug!( - "Produced block with {} attestations, updating state.", - block.body.attestations.len() - ); - per_block_processing_without_verifying_block_signature(&mut state, &block, &self.spec)?; let state_root = state.canonical_root(); diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 097ad656cf..64e482a508 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2,6 +2,7 @@ use crate::{BeaconChain, BeaconChainTypes, BlockProcessingOutcome}; use lmd_ghost::LmdGhost; use slot_clock::SlotClock; use slot_clock::TestingSlotClock; +use state_processing::per_slot_processing; use std::marker::PhantomData; use std::sync::Arc; use store::MemoryStore; @@ -13,7 +14,7 @@ use types::{ Hash256, Keypair, RelativeEpoch, SecretKey, Signature, Slot, }; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum BuildStrategy { OnCanonicalHead, ForkCanonicalChainAt(Slot), @@ -89,22 +90,14 @@ where } } - pub fn extend_chain(&self, build_strategy: BuildStrategy) { + pub fn advance_slot(&self) { self.chain.slot_clock.advance_slot(); self.chain.catchup_state().expect("should catchup state"); - - let block = self.build_block(build_strategy); - let outcome = self - .chain - .process_block(block) - .expect("should process block"); - assert_eq!(outcome, BlockProcessingOutcome::Processed); - - self.add_attestations_to_op_pool(); } - fn get_state(&self, build_strategy: BuildStrategy) -> BeaconState { - match build_strategy { + pub fn extend_chain(&self, build_strategy: BuildStrategy, blocks: usize) { + // Get an initial state to build the block upon, based on the build strategy. + let mut state = match build_strategy { BuildStrategy::OnCanonicalHead => self.chain.current_state().clone(), BuildStrategy::ForkCanonicalChainAt(fork_slot) => { let state_root = self @@ -120,18 +113,80 @@ where .expect("should read db") .expect("should find state root") } - } - } + }; - fn build_block(&self, build_strategy: BuildStrategy) -> BeaconBlock { - let mut state = self.get_state(build_strategy); - state.build_all_caches(&self.spec).unwrap(); - - let slot = match build_strategy { + // Get an initial slot to build upon, based on the build strategy. + let mut slot = match build_strategy { BuildStrategy::OnCanonicalHead => self.chain.read_slot_clock().unwrap(), BuildStrategy::ForkCanonicalChainAt(slot) => slot, }; + for _ in 0..blocks { + while self.chain.read_slot_clock().expect("should have a slot") < slot { + self.advance_slot(); + } + + let (block, new_state) = self.build_block(state.clone(), slot, build_strategy); + + let outcome = self + .chain + .process_block(block) + .expect("should process block"); + + assert_eq!(outcome, BlockProcessingOutcome::Processed); + + state = new_state; + slot += 1; + } + + self.add_attestations_to_op_pool(); + } + + fn build_block( + &self, + mut state: BeaconState, + slot: Slot, + build_strategy: BuildStrategy, + ) -> (BeaconBlock, BeaconState) { + if slot < state.slot { + panic!("produce slot cannot be prior to the state slot"); + } + + for _ in 0..slot.as_u64() - state.slot.as_u64() { + // Ensure the next epoch state caches are built in case of an epoch transition. + state + .build_committee_cache(RelativeEpoch::Next, &self.spec) + .expect("should be able to build caches"); + + per_slot_processing(&mut state, &self.spec) + .expect("should be able to advance state to slot"); + } + + state.drop_all_caches(); + state.build_all_caches(&self.spec).unwrap(); + + // dbg!(slot); + // dbg!(state.generate_seed(state.current_epoch(), &self.spec)); + dbg!(state.generate_seed(state.next_epoch(), &self.spec)); + /* + dbg!(self + .chain + .current_state() + .generate_seed(state.current_epoch(), &self.spec)); + // dbg!(state.generate_seed(state.next_epoch(), &self.spec)); + dbg!(state.canonical_root()); + dbg!(&state.committee_caches[0]); + dbg!(self.chain.current_state().canonical_root()); + dbg!(&self.chain.current_state().committee_caches[0]); + + dbg!(state.get_beacon_proposer_index(slot, RelativeEpoch::Current, &self.spec)); + dbg!(self.chain.current_state().get_beacon_proposer_index( + slot, + RelativeEpoch::Current, + &self.spec + )); + */ + let proposer_index = match build_strategy { BuildStrategy::OnCanonicalHead => self .chain @@ -152,7 +207,7 @@ where Signature::new(&message, domain, sk) }; - let (mut block, _state) = self + let (mut block, state) = self .chain .produce_block_on_state(state, slot, randao_reveal) .expect("should produce block"); @@ -164,7 +219,7 @@ where Signature::new(&message, domain, sk) }; - block + (block, state) } fn add_attestations_to_op_pool(&self) { @@ -239,18 +294,30 @@ mod test { pub const VALIDATOR_COUNT: usize = 16; + fn get_harness( + validator_count: usize, + ) -> BeaconChainHarness, MinimalEthSpec> + { + let harness = BeaconChainHarness::new(validator_count); + + // Move past the zero slot. + harness.advance_slot(); + + harness + } + #[test] fn can_finalize() { let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; - let harness: BeaconChainHarness< - ThreadSafeReducedTree, - MinimalEthSpec, - > = BeaconChainHarness::new(VALIDATOR_COUNT); + let harness = get_harness(VALIDATOR_COUNT); + harness.extend_chain(BuildStrategy::OnCanonicalHead, num_blocks_produced as usize); + /* for _ in 0..num_blocks_produced { harness.extend_chain(BuildStrategy::OnCanonicalHead); } + */ let state = &harness.chain.head().beacon_state; diff --git a/eth2/types/src/beacon_state.rs b/eth2/types/src/beacon_state.rs index 58237fe6f3..4790d95e7f 100644 --- a/eth2/types/src/beacon_state.rs +++ b/eth2/types/src/beacon_state.rs @@ -703,6 +703,9 @@ impl BeaconState { let active_index_root = self.get_active_index_root(epoch, spec)?; let epoch_bytes = int_to_bytes32(epoch.as_u64()); + dbg!(randao); + dbg!(active_index_root); + let mut preimage = [0; 32 * 3]; preimage[0..32].copy_from_slice(&randao[..]); preimage[32..64].copy_from_slice(&active_index_root[..]); From e485f3ee75ff3e34da479a93b8d773ea52fd2e08 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 20 Jun 2019 18:44:50 +1000 Subject: [PATCH 23/40] Fix bug in reduced tree fork choice --- eth2/lmd_ghost/src/reduced_tree.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/eth2/lmd_ghost/src/reduced_tree.rs b/eth2/lmd_ghost/src/reduced_tree.rs index 9a869c1ebf..f35f50e3ae 100644 --- a/eth2/lmd_ghost/src/reduced_tree.rs +++ b/eth2/lmd_ghost/src/reduced_tree.rs @@ -173,6 +173,14 @@ where } } + self.latest_votes.insert( + validator_index, + Some(Vote { + slot, + hash: block_hash, + }), + ); + self.add_latest_message(validator_index, block_hash)?; Ok(()) From 5a8cde0598c06a9b65a9cc96ecf15a947acd02f0 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 20 Jun 2019 18:45:19 +1000 Subject: [PATCH 24/40] Change "canonical_root" of block to be signed root --- eth2/types/src/beacon_block.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eth2/types/src/beacon_block.rs b/eth2/types/src/beacon_block.rs index f823f234e5..709bf65e09 100644 --- a/eth2/types/src/beacon_block.rs +++ b/eth2/types/src/beacon_block.rs @@ -61,11 +61,11 @@ impl BeaconBlock { } } - /// Returns the `tree_hash_root | update` of the block. + /// Returns the `signed_root` of the block. /// /// Spec v0.6.3 pub fn canonical_root(&self) -> Hash256 { - Hash256::from_slice(&self.tree_hash_root()[..]) + Hash256::from_slice(&self.signed_root()[..]) } /// Returns a full `BeaconBlockHeader` of this block. From d0037f49d857d382110a90b5727e47d73b984164 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 20 Jun 2019 18:46:03 +1000 Subject: [PATCH 25/40] Add progress on debugging fork choice --- beacon_node/beacon_chain/src/beacon_chain.rs | 83 +++++++++++++++++--- beacon_node/beacon_chain/src/test_utils.rs | 46 +++-------- eth2/lmd_ghost/src/reduced_tree.rs | 4 +- eth2/types/src/beacon_block.rs | 2 +- eth2/types/src/beacon_state.rs | 5 +- 5 files changed, 92 insertions(+), 48 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 86e0ded3ce..47cbde8ef8 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -32,7 +32,7 @@ pub const GRAFFITI: &str = "sigp/lighthouse-0.0.0-prerelease"; #[derive(Debug, PartialEq)] pub enum BlockProcessingOutcome { /// Block was valid and imported into the block graph. - Processed, + Processed { block_root: Hash256 }, /// The blocks parent_root is unknown. ParentUnknown { parent: Hash256 }, /// The block slot is greater than the present slot. @@ -426,6 +426,12 @@ impl BeaconChain { /// Produce an `AttestationData` that is valid for the present `slot` and given `shard`. pub fn produce_attestation_data(&self, shard: u64) -> Result { + let state = self.state.read(); + let head_block_root = self.head().beacon_block_root; + let head_block_slot = self.head().beacon_block.slot; + + self.produce_attestation_data_for_block(shard, head_block_root, head_block_slot, &*state) + /* let slots_per_epoch = T::EthSpec::slots_per_epoch(); self.metrics.attestation_production_requests.inc(); @@ -474,6 +480,65 @@ impl BeaconChain { previous_crosslink_root, crosslink_data_root: Hash256::zero(), }) + */ + } + + /// Produce an `AttestationData` that attests to the chain denoted by `block_root` and `state`. + pub fn produce_attestation_data_for_block( + &self, + shard: u64, + head_block_root: Hash256, + head_block_slot: Slot, + state: &BeaconState, + ) -> Result { + // Collect some metrics. + self.metrics.attestation_production_requests.inc(); + let timer = self.metrics.attestation_production_times.start_timer(); + + let slots_per_epoch = T::EthSpec::slots_per_epoch(); + let current_epoch_start_slot = state.current_epoch().start_slot(slots_per_epoch); + + // The `target_root` is the root of the first block of the current epoch. + // + // The `state` does not know the root of the block for it's current slot (it only knows + // about blocks from prior slots). This creates an edge-case when the state is on the first + // slot of the epoch -- we're unable to obtain the `target_root` because it is not a prior + // root. + // + // This edge case is handled in two ways: + // + // - If the head block is on the same slot as the state, we use it's root. + // - Otherwise, assume the current slot has been skipped and use the block root from the + // prior slot. + // + // For all other cases, we simply read the `target_root` from `state.latest_block_roots`. + let target_root = if state.slot == current_epoch_start_slot { + if head_block_slot == current_epoch_start_slot { + head_block_root + } else { + *state.get_block_root(current_epoch_start_slot - 1)? + } + } else { + *state.get_block_root(current_epoch_start_slot)? + }; + + let previous_crosslink_root = + Hash256::from_slice(&state.get_current_crosslink(shard)?.tree_hash_root()); + + // Collect some metrics. + self.metrics.attestation_production_successes.inc(); + timer.observe_duration(); + + Ok(AttestationData { + beacon_block_root: head_block_root, + source_epoch: state.current_justified_epoch, + source_root: state.current_justified_root, + target_epoch: state.current_epoch(), + target_root, + shard, + previous_crosslink_root, + crosslink_data_root: Hash256::zero(), + }) } /// Accept a new attestation from the network. @@ -612,9 +677,6 @@ impl BeaconChain { .get(&parent_state_root)? .ok_or_else(|| Error::DBInconsistent(format!("Missing state {}", parent_state_root)))?; - // TODO: check the block proposer signature BEFORE doing a state transition. This will - // significantly lower exposure surface to DoS attacks. - // Transition the parent state to the block slot. let mut state: BeaconState = parent_state; for _ in state.slot.as_u64()..block.slot.as_u64() { @@ -658,7 +720,7 @@ impl BeaconChain { .observe(block.body.attestations.len() as f64); timer.observe_duration(); - Ok(BlockProcessingOutcome::Processed) + Ok(BlockProcessingOutcome::Processed { block_root }) } /// Produce a new block at the present slot. @@ -695,10 +757,7 @@ impl BeaconChain { let timer = self.metrics.block_production_times.start_timer(); // If required, transition the new state to the present slot. - for _ in state.slot.as_u64()..produce_at_slot.as_u64() { - // Ensure the next epoch state caches are built in case of an epoch transition. - state.build_committee_cache(RelativeEpoch::Next, &self.spec)?; - + while state.slot < produce_at_slot { per_slot_processing(&mut state, &self.spec)?; } @@ -711,6 +770,7 @@ impl BeaconChain { } else { state.latest_block_header.canonical_root() }; + dbg!(previous_block_root); let mut graffiti: [u8; 32] = [0; 32]; graffiti.copy_from_slice(GRAFFITI.as_bytes()); @@ -754,6 +814,8 @@ impl BeaconChain { self.metrics.block_production_successes.inc(); timer.observe_duration(); + dbg!(block.canonical_root()); + Ok((block, state)) } @@ -787,6 +849,9 @@ impl BeaconChain { // If we switched to a new chain (instead of building atop the present chain). if self.head().beacon_block_root != beacon_block.previous_block_root { + dbg!("switched head"); + dbg!(self.head().beacon_block.slot); + dbg!(beacon_block.slot); self.metrics.fork_choice_reorg_count.inc(); }; diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 64e482a508..bb308c4076 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -131,15 +131,19 @@ where let outcome = self .chain .process_block(block) - .expect("should process block"); + .expect("should not error during block processing"); - assert_eq!(outcome, BlockProcessingOutcome::Processed); + if let BlockProcessingOutcome::Processed { block_root } = outcome { + // + } else { + panic!("block should be successfully processed"); + } + + self.add_attestations_to_op_pool(); state = new_state; slot += 1; } - - self.add_attestations_to_op_pool(); } fn build_block( @@ -152,41 +156,13 @@ where panic!("produce slot cannot be prior to the state slot"); } - for _ in 0..slot.as_u64() - state.slot.as_u64() { - // Ensure the next epoch state caches are built in case of an epoch transition. - state - .build_committee_cache(RelativeEpoch::Next, &self.spec) - .expect("should be able to build caches"); - + while state.slot < slot { per_slot_processing(&mut state, &self.spec) .expect("should be able to advance state to slot"); } - state.drop_all_caches(); state.build_all_caches(&self.spec).unwrap(); - // dbg!(slot); - // dbg!(state.generate_seed(state.current_epoch(), &self.spec)); - dbg!(state.generate_seed(state.next_epoch(), &self.spec)); - /* - dbg!(self - .chain - .current_state() - .generate_seed(state.current_epoch(), &self.spec)); - // dbg!(state.generate_seed(state.next_epoch(), &self.spec)); - dbg!(state.canonical_root()); - dbg!(&state.committee_caches[0]); - dbg!(self.chain.current_state().canonical_root()); - dbg!(&self.chain.current_state().committee_caches[0]); - - dbg!(state.get_beacon_proposer_index(slot, RelativeEpoch::Current, &self.spec)); - dbg!(self.chain.current_state().get_beacon_proposer_index( - slot, - RelativeEpoch::Current, - &self.spec - )); - */ - let proposer_index = match build_strategy { BuildStrategy::OnCanonicalHead => self .chain @@ -308,7 +284,7 @@ mod test { #[test] fn can_finalize() { - let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; + let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 1 + 2; let harness = get_harness(VALIDATOR_COUNT); @@ -340,5 +316,7 @@ mod test { state.current_epoch() - 2, "the head should be finalized two behind the current epoch" ); + + panic!(); } } diff --git a/eth2/lmd_ghost/src/reduced_tree.rs b/eth2/lmd_ghost/src/reduced_tree.rs index f35f50e3ae..888b20bf27 100644 --- a/eth2/lmd_ghost/src/reduced_tree.rs +++ b/eth2/lmd_ghost/src/reduced_tree.rs @@ -210,6 +210,8 @@ where } fn find_head_from<'a>(&'a self, start_node: &'a Node) -> Result<&'a Node> { + dbg!(&self.nodes); + if start_node.does_not_have_children() { Ok(start_node) } else { @@ -585,7 +587,7 @@ pub struct Vote { /// /// E.g., a `get` or `insert` to an out-of-bounds element will cause the Vec to grow (using /// Default) to the smallest size required to fulfill the request. -#[derive(Default, Clone)] +#[derive(Default, Clone, Debug)] pub struct ElasticList(Vec); impl ElasticList diff --git a/eth2/types/src/beacon_block.rs b/eth2/types/src/beacon_block.rs index 709bf65e09..18e5a37ec5 100644 --- a/eth2/types/src/beacon_block.rs +++ b/eth2/types/src/beacon_block.rs @@ -5,7 +5,7 @@ use bls::Signature; use serde_derive::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; -use tree_hash::TreeHash; +use tree_hash::{SignedRoot, TreeHash}; use tree_hash_derive::{CachedTreeHash, SignedRoot, TreeHash}; /// A block of the `BeaconChain`. diff --git a/eth2/types/src/beacon_state.rs b/eth2/types/src/beacon_state.rs index 4790d95e7f..f82340017f 100644 --- a/eth2/types/src/beacon_state.rs +++ b/eth2/types/src/beacon_state.rs @@ -703,9 +703,6 @@ impl BeaconState { let active_index_root = self.get_active_index_root(epoch, spec)?; let epoch_bytes = int_to_bytes32(epoch.as_u64()); - dbg!(randao); - dbg!(active_index_root); - let mut preimage = [0; 32 * 3]; preimage[0..32].copy_from_slice(&randao[..]); preimage[32..64].copy_from_slice(&active_index_root[..]); @@ -838,10 +835,12 @@ impl BeaconState { /// Note: whilst this function will preserve already-built caches, it will not build any. pub fn advance_caches(&mut self) { let next = Self::cache_index(RelativeEpoch::Previous); + let current = Self::cache_index(RelativeEpoch::Current); let caches = &mut self.committee_caches[..]; caches.rotate_left(1); caches[next] = CommitteeCache::default(); + caches[current] = CommitteeCache::default(); } fn cache_index(relative_epoch: RelativeEpoch) -> usize { From 0b2ad4d0a10af6d4ebe8a64e502925b9ce183b48 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Fri, 21 Jun 2019 09:18:02 +1000 Subject: [PATCH 26/40] Mark reduced_tree fork choice as incomplete --- beacon_node/beacon_chain/src/beacon_chain.rs | 6 ----- beacon_node/client/src/lib.rs | 1 - eth2/lmd_ghost/src/reduced_tree.rs | 27 ++++++++++++++++---- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 47cbde8ef8..b8fff96111 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -770,7 +770,6 @@ impl BeaconChain { } else { state.latest_block_header.canonical_root() }; - dbg!(previous_block_root); let mut graffiti: [u8; 32] = [0; 32]; graffiti.copy_from_slice(GRAFFITI.as_bytes()); @@ -814,8 +813,6 @@ impl BeaconChain { self.metrics.block_production_successes.inc(); timer.observe_duration(); - dbg!(block.canonical_root()); - Ok((block, state)) } @@ -849,9 +846,6 @@ impl BeaconChain { // If we switched to a new chain (instead of building atop the present chain). if self.head().beacon_block_root != beacon_block.previous_block_root { - dbg!("switched head"); - dbg!(self.head().beacon_block.slot); - dbg!(beacon_block.slot); self.metrics.fork_choice_reorg_count.inc(); }; diff --git a/beacon_node/client/src/lib.rs b/beacon_node/client/src/lib.rs index 92ed6e0227..18ddef7bb3 100644 --- a/beacon_node/client/src/lib.rs +++ b/beacon_node/client/src/lib.rs @@ -186,7 +186,6 @@ impl Drop for Client { fn drop(&mut self) { // Save the beacon chain to it's store before dropping. let _result = self.beacon_chain.persist(); - dbg!("Saved BeaconChain to store"); } } diff --git a/eth2/lmd_ghost/src/reduced_tree.rs b/eth2/lmd_ghost/src/reduced_tree.rs index 888b20bf27..411c3bc1c2 100644 --- a/eth2/lmd_ghost/src/reduced_tree.rs +++ b/eth2/lmd_ghost/src/reduced_tree.rs @@ -1,3 +1,8 @@ +/// An implementation of "reduced tree" LMD GHOST fork choice. +/// +/// This algorithm was concieved at IC3 Cornell, 2019. +/// +/// This implementation is incomplete and has known bugs. use super::{LmdGhost, Result as SuperResult}; use parking_lot::RwLock; use std::collections::HashMap; @@ -210,8 +215,6 @@ where } fn find_head_from<'a>(&'a self, start_node: &'a Node) -> Result<&'a Node> { - dbg!(&self.nodes); - if start_node.does_not_have_children() { Ok(start_node) } else { @@ -404,10 +407,24 @@ where if !prev_in_tree.children.is_empty() { for &child_hash in &prev_in_tree.children { - let ancestor_hash = self.find_least_common_ancestor(node.block_hash, child_hash)?; + if self + .iter_ancestors(child_hash)? + .any(|(ancestor, _slot)| ancestor == node.block_hash) + { + let child = self.get_mut_node(child_hash)?; - // TODO: handle the case where the new block is a child of an existing node and a - // parent of an existing node. + child.parent_hash = Some(node.block_hash); + node.children.push(child_hash); + prev_in_tree.replace_child(child_hash, node.block_hash)?; + + added_new_ancestor = true; + + break; + } + } + + for &child_hash in &prev_in_tree.children { + let ancestor_hash = self.find_least_common_ancestor(node.block_hash, child_hash)?; if ancestor_hash != prev_in_tree.block_hash { let child = self.get_mut_node(child_hash)?; From 7a4c3e26acb332418e0dc530d161a68c8b5ab3e0 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Fri, 21 Jun 2019 11:30:25 +1000 Subject: [PATCH 27/40] Fix bug in reduced tree fork choice --- beacon_node/beacon_chain/src/test_utils.rs | 9 +--- eth2/lmd_ghost/src/reduced_tree.rs | 51 ++++++++++++---------- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index bb308c4076..8da68bbdc3 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -284,16 +284,11 @@ mod test { #[test] fn can_finalize() { - let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 1 + 2; + let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; let harness = get_harness(VALIDATOR_COUNT); harness.extend_chain(BuildStrategy::OnCanonicalHead, num_blocks_produced as usize); - /* - for _ in 0..num_blocks_produced { - harness.extend_chain(BuildStrategy::OnCanonicalHead); - } - */ let state = &harness.chain.head().beacon_state; @@ -316,7 +311,5 @@ mod test { state.current_epoch() - 2, "the head should be finalized two behind the current epoch" ); - - panic!(); } } diff --git a/eth2/lmd_ghost/src/reduced_tree.rs b/eth2/lmd_ghost/src/reduced_tree.rs index 411c3bc1c2..2d70ea1453 100644 --- a/eth2/lmd_ghost/src/reduced_tree.rs +++ b/eth2/lmd_ghost/src/reduced_tree.rs @@ -1,8 +1,8 @@ -/// An implementation of "reduced tree" LMD GHOST fork choice. -/// -/// This algorithm was concieved at IC3 Cornell, 2019. -/// -/// This implementation is incomplete and has known bugs. +//! An implementation of "reduced tree" LMD GHOST fork choice. +//! +//! This algorithm was concieved at IC3 Cornell, 2019. +//! +//! This implementation is incomplete and has known bugs. Do not use in production. use super::{LmdGhost, Result as SuperResult}; use parking_lot::RwLock; use std::collections::HashMap; @@ -406,6 +406,8 @@ where let mut added_new_ancestor = false; if !prev_in_tree.children.is_empty() { + let mut added = false; + for &child_hash in &prev_in_tree.children { if self .iter_ancestors(child_hash)? @@ -417,34 +419,37 @@ where node.children.push(child_hash); prev_in_tree.replace_child(child_hash, node.block_hash)?; - added_new_ancestor = true; + added = true; break; } } - for &child_hash in &prev_in_tree.children { - let ancestor_hash = self.find_least_common_ancestor(node.block_hash, child_hash)?; + if !added { + for &child_hash in &prev_in_tree.children { + let ancestor_hash = + self.find_least_common_ancestor(node.block_hash, child_hash)?; - if ancestor_hash != prev_in_tree.block_hash { - let child = self.get_mut_node(child_hash)?; - let common_ancestor = Node { - block_hash: ancestor_hash, - parent_hash: Some(prev_in_tree.block_hash), - children: vec![node.block_hash, child_hash], - ..Node::default() - }; - child.parent_hash = Some(common_ancestor.block_hash); - node.parent_hash = Some(common_ancestor.block_hash); + if ancestor_hash != prev_in_tree.block_hash { + let child = self.get_mut_node(child_hash)?; + let common_ancestor = Node { + block_hash: ancestor_hash, + parent_hash: Some(prev_in_tree.block_hash), + children: vec![node.block_hash, child_hash], + ..Node::default() + }; + child.parent_hash = Some(common_ancestor.block_hash); + node.parent_hash = Some(common_ancestor.block_hash); - prev_in_tree.replace_child(child_hash, ancestor_hash)?; + prev_in_tree.replace_child(child_hash, ancestor_hash)?; - self.nodes - .insert(common_ancestor.block_hash, common_ancestor); + self.nodes + .insert(common_ancestor.block_hash, common_ancestor); - added_new_ancestor = true; + added_new_ancestor = true; - break; + break; + } } } } From 46c0e176827be48cec31394391f0b981179adff4 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Fri, 21 Jun 2019 11:37:02 +1000 Subject: [PATCH 28/40] Add arbitrary attestation for beacon chain harness --- beacon_node/beacon_chain/src/beacon_chain.rs | 55 ++------------------ beacon_node/beacon_chain/src/test_utils.rs | 19 ++++--- 2 files changed, 18 insertions(+), 56 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index b8fff96111..235ec5f4c2 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -425,65 +425,20 @@ impl BeaconChain { } /// Produce an `AttestationData` that is valid for the present `slot` and given `shard`. + /// + /// Attests to the canonical chain. pub fn produce_attestation_data(&self, shard: u64) -> Result { let state = self.state.read(); let head_block_root = self.head().beacon_block_root; let head_block_slot = self.head().beacon_block.slot; self.produce_attestation_data_for_block(shard, head_block_root, head_block_slot, &*state) - /* - let slots_per_epoch = T::EthSpec::slots_per_epoch(); - - self.metrics.attestation_production_requests.inc(); - let timer = self.metrics.attestation_production_times.start_timer(); - - let state = self.state.read(); - - let current_epoch_start_slot = self - .state - .read() - .slot - .epoch(slots_per_epoch) - .start_slot(slots_per_epoch); - - let target_root = if state.slot == current_epoch_start_slot { - // If we're on the first slot of the state's epoch. - if self.head().beacon_block.slot == state.slot { - // If the current head block is from the current slot, use its block root. - self.head().beacon_block_root - } else { - // If the current head block is not from this slot, use the slot from the previous - // epoch. - *self - .state - .read() - .get_block_root(current_epoch_start_slot - slots_per_epoch)? - } - } else { - // If we're not on the first slot of the epoch. - *self.state.read().get_block_root(current_epoch_start_slot)? - }; - - let previous_crosslink_root = - Hash256::from_slice(&state.get_current_crosslink(shard)?.tree_hash_root()); - - self.metrics.attestation_production_successes.inc(); - timer.observe_duration(); - - Ok(AttestationData { - beacon_block_root: self.head().beacon_block_root, - source_epoch: state.current_justified_epoch, - source_root: state.current_justified_root, - target_epoch: state.current_epoch(), - target_root, - shard, - previous_crosslink_root, - crosslink_data_root: Hash256::zero(), - }) - */ } /// Produce an `AttestationData` that attests to the chain denoted by `block_root` and `state`. + /// + /// Permits attesting to any arbitrary chain. Generally, the `produce_attestation_data` + /// function should be used as it attests to the canonical chain. pub fn produce_attestation_data_for_block( &self, shard: u64, diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 8da68bbdc3..b6873fa571 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -134,13 +134,11 @@ where .expect("should not error during block processing"); if let BlockProcessingOutcome::Processed { block_root } = outcome { - // + self.add_attestations_to_op_pool(&new_state, block_root, slot); } else { panic!("block should be successfully processed"); } - self.add_attestations_to_op_pool(); - state = new_state; slot += 1; } @@ -198,8 +196,12 @@ where (block, state) } - fn add_attestations_to_op_pool(&self) { - let state = &self.chain.current_state(); + fn add_attestations_to_op_pool( + &self, + state: &BeaconState, + head_block_root: Hash256, + head_block_slot: Slot, + ) { let spec = &self.spec; let fork = &state.fork; @@ -213,7 +215,12 @@ where for (i, validator_index) in cc.committee.iter().enumerate() { let data = self .chain - .produce_attestation_data(cc.shard) + .produce_attestation_data_for_block( + cc.shard, + head_block_root, + head_block_slot, + state, + ) .expect("should produce attestation data"); let mut aggregation_bitfield = Bitfield::new(); From 723283bd0125218c4e7852203dcb097f6dc88fd2 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Fri, 21 Jun 2019 11:44:15 +1000 Subject: [PATCH 29/40] Add attestation strategy to chain harness --- beacon_node/beacon_chain/src/test_utils.rs | 111 +++++++++++++-------- 1 file changed, 70 insertions(+), 41 deletions(-) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index b6873fa571..65be6fba54 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -20,6 +20,11 @@ pub enum BuildStrategy { ForkCanonicalChainAt(Slot), } +#[derive(Clone, Copy, Debug)] +pub enum AttestationStrategy { + AllValidators, +} + pub struct CommonTypes where L: LmdGhost, @@ -95,7 +100,12 @@ where self.chain.catchup_state().expect("should catchup state"); } - pub fn extend_chain(&self, build_strategy: BuildStrategy, blocks: usize) { + pub fn extend_chain( + &self, + build_strategy: BuildStrategy, + blocks: usize, + attestation_strategy: AttestationStrategy, + ) { // Get an initial state to build the block upon, based on the build strategy. let mut state = match build_strategy { BuildStrategy::OnCanonicalHead => self.chain.current_state().clone(), @@ -134,7 +144,12 @@ where .expect("should not error during block processing"); if let BlockProcessingOutcome::Processed { block_root } = outcome { - self.add_attestations_to_op_pool(&new_state, block_root, slot); + self.add_attestations_to_op_pool( + attestation_strategy, + &new_state, + block_root, + slot, + ); } else { panic!("block should be successfully processed"); } @@ -198,6 +213,7 @@ where fn add_attestations_to_op_pool( &self, + attestation_strategy: AttestationStrategy, state: &BeaconState, head_block_root: Hash256, head_block_slot: Slot, @@ -205,6 +221,10 @@ where let spec = &self.spec; let fork = &state.fork; + let attesting_validators: Vec = match attestation_strategy { + AttestationStrategy::AllValidators => (0..self.keypairs.len()).collect(), + }; + state .get_crosslink_committees_at_slot(state.slot) .expect("should get committees") @@ -213,52 +233,57 @@ where let committee_size = cc.committee.len(); for (i, validator_index) in cc.committee.iter().enumerate() { - let data = self - .chain - .produce_attestation_data_for_block( - cc.shard, - head_block_root, - head_block_slot, - state, - ) - .expect("should produce attestation data"); + // Note: searching this array is worst-case `O(n)`. A hashset could be a better + // alternative. + if attesting_validators.contains(validator_index) { + let data = self + .chain + .produce_attestation_data_for_block( + cc.shard, + head_block_root, + head_block_slot, + state, + ) + .expect("should produce attestation data"); - let mut aggregation_bitfield = Bitfield::new(); - aggregation_bitfield.set(i, true); - aggregation_bitfield.set(committee_size, false); + let mut aggregation_bitfield = Bitfield::new(); + aggregation_bitfield.set(i, true); + aggregation_bitfield.set(committee_size, false); - let mut custody_bitfield = Bitfield::new(); - custody_bitfield.set(committee_size, false); + let mut custody_bitfield = Bitfield::new(); + custody_bitfield.set(committee_size, false); - let signature = { - let message = AttestationDataAndCustodyBit { - data: data.clone(), - custody_bit: false, - } - .tree_hash_root(); + let signature = { + let message = AttestationDataAndCustodyBit { + data: data.clone(), + custody_bit: false, + } + .tree_hash_root(); - let domain = spec.get_domain(data.target_epoch, Domain::Attestation, fork); + let domain = + spec.get_domain(data.target_epoch, Domain::Attestation, fork); - let mut agg_sig = AggregateSignature::new(); - agg_sig.add(&Signature::new( - &message, - domain, - self.get_sk(*validator_index), - )); + let mut agg_sig = AggregateSignature::new(); + agg_sig.add(&Signature::new( + &message, + domain, + self.get_sk(*validator_index), + )); - agg_sig - }; + agg_sig + }; - let attestation = Attestation { - aggregation_bitfield, - data, - custody_bitfield, - signature, - }; + let attestation = Attestation { + aggregation_bitfield, + data, + custody_bitfield, + signature, + }; - self.chain - .process_attestation(attestation) - .expect("should process attestation"); + self.chain + .process_attestation(attestation) + .expect("should process attestation"); + } } }); } @@ -295,7 +320,11 @@ mod test { let harness = get_harness(VALIDATOR_COUNT); - harness.extend_chain(BuildStrategy::OnCanonicalHead, num_blocks_produced as usize); + harness.extend_chain( + BuildStrategy::OnCanonicalHead, + num_blocks_produced as usize, + AttestationStrategy::AllValidators, + ); let state = &harness.chain.head().beacon_state; From 299b4cb2071b62c572ce2fbb550ece4127b095cc Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Fri, 21 Jun 2019 11:55:37 +1000 Subject: [PATCH 30/40] Document beacon chain harness --- beacon_node/beacon_chain/src/test_utils.rs | 57 ++++++++++++++++------ 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 65be6fba54..be9030ccd3 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -14,17 +14,25 @@ use types::{ Hash256, Keypair, RelativeEpoch, SecretKey, Signature, Slot, }; +/// Indicates how the `BeaconChainHarness` should produce blocks. #[derive(Clone, Copy, Debug)] -pub enum BuildStrategy { +pub enum BlockStrategy { + /// Produce blocks upon the canonical head (normal case). OnCanonicalHead, + /// Ignore the canonical head and produce blocks upon the block at the given slot. + /// + /// Useful for simulating forks. ForkCanonicalChainAt(Slot), } +/// Indicates how the `BeaconChainHarness` should produce attestations. #[derive(Clone, Copy, Debug)] pub enum AttestationStrategy { + /// All validators attest to whichever block the `BeaconChainHarness` has produced. AllValidators, } +/// Used to make the `BeaconChainHarness` generic over some types. pub struct CommonTypes where L: LmdGhost, @@ -45,6 +53,8 @@ where type EthSpec = E; } +/// A testing harness which can instantiate a `BeaconChain` and populate it with blocks and +/// attestations. pub struct BeaconChainHarness where L: LmdGhost, @@ -60,6 +70,7 @@ where L: LmdGhost, E: EthSpec, { + /// Instantiate a new harness with `validator_count` initial validators. pub fn new(validator_count: usize) -> Self { let spec = E::default_spec(); @@ -95,21 +106,32 @@ where } } + /// Advance the slot of the `BeaconChain`. + /// + /// Does not produce blocks or attestations. pub fn advance_slot(&self) { self.chain.slot_clock.advance_slot(); self.chain.catchup_state().expect("should catchup state"); } + /// Extend the `BeaconChain` with some blocks and attestations. + /// + /// Chain will be extended by `num_blocks` blocks. + /// + /// The `block_strategy` dictates where the new blocks will be placed. + /// + /// The `attestation_strategy` dictates which validators will attest to the newly created + /// blocks. pub fn extend_chain( &self, - build_strategy: BuildStrategy, - blocks: usize, + num_blocks: usize, + block_strategy: BlockStrategy, attestation_strategy: AttestationStrategy, ) { // Get an initial state to build the block upon, based on the build strategy. - let mut state = match build_strategy { - BuildStrategy::OnCanonicalHead => self.chain.current_state().clone(), - BuildStrategy::ForkCanonicalChainAt(fork_slot) => { + let mut state = match block_strategy { + BlockStrategy::OnCanonicalHead => self.chain.current_state().clone(), + BlockStrategy::ForkCanonicalChainAt(fork_slot) => { let state_root = self .chain .rev_iter_state_roots(self.chain.head().beacon_state.slot - 1) @@ -126,17 +148,17 @@ where }; // Get an initial slot to build upon, based on the build strategy. - let mut slot = match build_strategy { - BuildStrategy::OnCanonicalHead => self.chain.read_slot_clock().unwrap(), - BuildStrategy::ForkCanonicalChainAt(slot) => slot, + let mut slot = match block_strategy { + BlockStrategy::OnCanonicalHead => self.chain.read_slot_clock().unwrap(), + BlockStrategy::ForkCanonicalChainAt(slot) => slot, }; - for _ in 0..blocks { + for _ in 0..num_blocks { while self.chain.read_slot_clock().expect("should have a slot") < slot { self.advance_slot(); } - let (block, new_state) = self.build_block(state.clone(), slot, build_strategy); + let (block, new_state) = self.build_block(state.clone(), slot, block_strategy); let outcome = self .chain @@ -159,11 +181,12 @@ where } } + /// Returns a newly created block, signed by the proposer for the given slot. fn build_block( &self, mut state: BeaconState, slot: Slot, - build_strategy: BuildStrategy, + block_strategy: BlockStrategy, ) -> (BeaconBlock, BeaconState) { if slot < state.slot { panic!("produce slot cannot be prior to the state slot"); @@ -176,8 +199,8 @@ where state.build_all_caches(&self.spec).unwrap(); - let proposer_index = match build_strategy { - BuildStrategy::OnCanonicalHead => self + let proposer_index = match block_strategy { + BlockStrategy::OnCanonicalHead => self .chain .block_proposer(slot) .expect("should get block proposer from chain"), @@ -211,6 +234,9 @@ where (block, state) } + /// Adds attestations to the `BeaconChain` operations pool to be included in future blocks. + /// + /// The `attestation_strategy` dictates which validators should attest. fn add_attestations_to_op_pool( &self, attestation_strategy: AttestationStrategy, @@ -288,6 +314,7 @@ where }); } + /// Returns the secret key for the given validator index. fn get_sk(&self, validator_index: usize) -> &SecretKey { &self.keypairs[validator_index].sk } @@ -321,8 +348,8 @@ mod test { let harness = get_harness(VALIDATOR_COUNT); harness.extend_chain( - BuildStrategy::OnCanonicalHead, num_blocks_produced as usize, + BlockStrategy::OnCanonicalHead, AttestationStrategy::AllValidators, ); From fbb40485c168df0aa72326094295ddbdf41e2a62 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Fri, 21 Jun 2019 18:54:37 +1000 Subject: [PATCH 31/40] Add additional chain tests --- beacon_node/beacon_chain/src/test_utils.rs | 127 ++++++++++++++++++++- 1 file changed, 122 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index be9030ccd3..36da030242 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -26,10 +26,12 @@ pub enum BlockStrategy { } /// Indicates how the `BeaconChainHarness` should produce attestations. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub enum AttestationStrategy { /// All validators attest to whichever block the `BeaconChainHarness` has produced. AllValidators, + /// Only the given validators should attest. All others should fail to produce attestations. + SomeValidators(Vec), } /// Used to make the `BeaconChainHarness` generic over some types. @@ -167,7 +169,7 @@ where if let BlockProcessingOutcome::Processed { block_root } = outcome { self.add_attestations_to_op_pool( - attestation_strategy, + &attestation_strategy, &new_state, block_root, slot, @@ -239,7 +241,7 @@ where /// The `attestation_strategy` dictates which validators should attest. fn add_attestations_to_op_pool( &self, - attestation_strategy: AttestationStrategy, + attestation_strategy: &AttestationStrategy, state: &BeaconState, head_block_root: Hash256, head_block_slot: Slot, @@ -249,6 +251,7 @@ where let attesting_validators: Vec = match attestation_strategy { AttestationStrategy::AllValidators => (0..self.keypairs.len()).collect(), + AttestationStrategy::SomeValidators(vec) => vec.clone(), }; state @@ -327,7 +330,8 @@ mod test { use lmd_ghost::ThreadSafeReducedTree; use types::MinimalEthSpec; - pub const VALIDATOR_COUNT: usize = 16; + // Should ideally be divisible by 3. + pub const VALIDATOR_COUNT: usize = 24; fn get_harness( validator_count: usize, @@ -342,7 +346,7 @@ mod test { } #[test] - fn can_finalize() { + fn finalizes_with_full_participation() { let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; let harness = get_harness(VALIDATOR_COUNT); @@ -375,4 +379,117 @@ mod test { "the head should be finalized two behind the current epoch" ); } + + #[test] + fn finalizes_with_two_thirds_participation() { + let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; + + let harness = get_harness(VALIDATOR_COUNT); + + let two_thirds = (VALIDATOR_COUNT / 3) * 2; + let attesters = (0..two_thirds).collect(); + + harness.extend_chain( + num_blocks_produced as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(attesters), + ); + + let state = &harness.chain.head().beacon_state; + + assert_eq!( + state.slot, num_blocks_produced, + "head should be at the current slot" + ); + assert_eq!( + state.current_epoch(), + num_blocks_produced / MinimalEthSpec::slots_per_epoch(), + "head should be at the expected epoch" + ); + + // Note: the 2/3rds tests are not justifying the immediately prior epochs because the + // `MIN_ATTESTATION_INCLUSION_DELAY` is preventing an adequate number of attestations being + // included in blocks during that epoch. + + assert_eq!( + state.current_justified_epoch, + state.current_epoch() - 2, + "the head should be justified two behind the current epoch" + ); + assert_eq!( + state.finalized_epoch, + state.current_epoch() - 4, + "the head should be finalized three behind the current epoch" + ); + } + + #[test] + fn does_not_finalize_with_less_than_two_thirds_participation() { + let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; + + let harness = get_harness(VALIDATOR_COUNT); + + let two_thirds = (VALIDATOR_COUNT / 3) * 2; + let less_than_two_thirds = two_thirds - 1; + let attesters = (0..less_than_two_thirds).collect(); + + harness.extend_chain( + num_blocks_produced as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(attesters), + ); + + let state = &harness.chain.head().beacon_state; + + assert_eq!( + state.slot, num_blocks_produced, + "head should be at the current slot" + ); + assert_eq!( + state.current_epoch(), + num_blocks_produced / MinimalEthSpec::slots_per_epoch(), + "head should be at the expected epoch" + ); + assert_eq!( + state.current_justified_epoch, 0, + "no epoch should have been justified" + ); + assert_eq!( + state.finalized_epoch, 0, + "no epoch should have been finalized" + ); + } + + #[test] + fn does_not_finalize_without_attestation() { + let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; + + let harness = get_harness(VALIDATOR_COUNT); + + harness.extend_chain( + num_blocks_produced as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(vec![]), + ); + + let state = &harness.chain.head().beacon_state; + + assert_eq!( + state.slot, num_blocks_produced, + "head should be at the current slot" + ); + assert_eq!( + state.current_epoch(), + num_blocks_produced / MinimalEthSpec::slots_per_epoch(), + "head should be at the expected epoch" + ); + assert_eq!( + state.current_justified_epoch, 0, + "no epoch should have been justified" + ); + assert_eq!( + state.finalized_epoch, 0, + "no epoch should have been finalized" + ); + } } From e904e0a5a88d0eb48b61ce1e8d90c4f7cd75586d Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sun, 23 Jun 2019 10:04:52 +1000 Subject: [PATCH 32/40] Fix bug in operations for block production --- beacon_node/beacon_chain/src/beacon_chain.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 235ec5f4c2..2a4df66f1d 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -748,14 +748,10 @@ impl BeaconChain { graffiti, proposer_slashings, attester_slashings, - attestations: self - .op_pool - .get_attestations(&*self.state.read(), &self.spec), - deposits: self.op_pool.get_deposits(&*self.state.read(), &self.spec), - voluntary_exits: self - .op_pool - .get_voluntary_exits(&*self.state.read(), &self.spec), - transfers: self.op_pool.get_transfers(&*self.state.read(), &self.spec), + attestations: self.op_pool.get_attestations(&state, &self.spec), + deposits: self.op_pool.get_deposits(&state, &self.spec), + voluntary_exits: self.op_pool.get_voluntary_exits(&state, &self.spec), + transfers: self.op_pool.get_transfers(&state, &self.spec), }, }; From 8ed03e391d7b69321712b0ef4a83d45d0803269d Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sun, 23 Jun 2019 10:05:12 +1000 Subject: [PATCH 33/40] Allow harness to produce fork blocks --- beacon_node/beacon_chain/src/test_utils.rs | 224 +++------------------ 1 file changed, 32 insertions(+), 192 deletions(-) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 36da030242..19d0f8fb15 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -22,7 +22,12 @@ pub enum BlockStrategy { /// Ignore the canonical head and produce blocks upon the block at the given slot. /// /// Useful for simulating forks. - ForkCanonicalChainAt(Slot), + ForkCanonicalChainAt { + /// The slot of the parent of the first block produced. + previous_slot: Slot, + /// The slot of the first block produced (must be higher than `previous_slot`. + first_slot: Slot, + }, } /// Indicates how the `BeaconChainHarness` should produce attestations. @@ -62,7 +67,7 @@ where L: LmdGhost, E: EthSpec, { - chain: BeaconChain>, + pub chain: BeaconChain>, keypairs: Vec, spec: ChainSpec, } @@ -130,29 +135,20 @@ where block_strategy: BlockStrategy, attestation_strategy: AttestationStrategy, ) { - // Get an initial state to build the block upon, based on the build strategy. - let mut state = match block_strategy { - BlockStrategy::OnCanonicalHead => self.chain.current_state().clone(), - BlockStrategy::ForkCanonicalChainAt(fork_slot) => { - let state_root = self - .chain - .rev_iter_state_roots(self.chain.head().beacon_state.slot - 1) - .find(|(_hash, slot)| *slot == fork_slot) - .map(|(hash, _slot)| hash) - .expect("could not find state root for fork"); + let mut state = { + // Determine the slot for the first block (or skipped block). + let state_slot = match block_strategy { + BlockStrategy::OnCanonicalHead => self.chain.read_slot_clock().unwrap() - 1, + BlockStrategy::ForkCanonicalChainAt { previous_slot, .. } => previous_slot, + }; - self.chain - .store - .get(&state_root) - .expect("should read db") - .expect("should find state root") - } + self.get_state_at_slot(state_slot) }; - // Get an initial slot to build upon, based on the build strategy. + // Determine the first slot where a block should be built. let mut slot = match block_strategy { BlockStrategy::OnCanonicalHead => self.chain.read_slot_clock().unwrap(), - BlockStrategy::ForkCanonicalChainAt(slot) => slot, + BlockStrategy::ForkCanonicalChainAt { first_slot, .. } => first_slot, }; for _ in 0..num_blocks { @@ -175,7 +171,7 @@ where slot, ); } else { - panic!("block should be successfully processed"); + panic!("block should be successfully processed: {:?}", outcome); } state = new_state; @@ -183,6 +179,21 @@ where } } + fn get_state_at_slot(&self, state_slot: Slot) -> BeaconState { + let state_root = self + .chain + .rev_iter_state_roots(self.chain.current_state().slot) + .find(|(_hash, slot)| *slot == state_slot) + .map(|(hash, _slot)| hash) + .expect("could not find state root"); + + self.chain + .store + .get(&state_root) + .expect("should read db") + .expect("should find state root") + } + /// Returns a newly created block, signed by the proposer for the given slot. fn build_block( &self, @@ -322,174 +333,3 @@ where &self.keypairs[validator_index].sk } } - -#[cfg(test)] -// #[cfg(not(debug_assertions))] -mod test { - use super::*; - use lmd_ghost::ThreadSafeReducedTree; - use types::MinimalEthSpec; - - // Should ideally be divisible by 3. - pub const VALIDATOR_COUNT: usize = 24; - - fn get_harness( - validator_count: usize, - ) -> BeaconChainHarness, MinimalEthSpec> - { - let harness = BeaconChainHarness::new(validator_count); - - // Move past the zero slot. - harness.advance_slot(); - - harness - } - - #[test] - fn finalizes_with_full_participation() { - let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; - - let harness = get_harness(VALIDATOR_COUNT); - - harness.extend_chain( - num_blocks_produced as usize, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::AllValidators, - ); - - let state = &harness.chain.head().beacon_state; - - assert_eq!( - state.slot, num_blocks_produced, - "head should be at the current slot" - ); - assert_eq!( - state.current_epoch(), - num_blocks_produced / MinimalEthSpec::slots_per_epoch(), - "head should be at the expected epoch" - ); - assert_eq!( - state.current_justified_epoch, - state.current_epoch() - 1, - "the head should be justified one behind the current epoch" - ); - assert_eq!( - state.finalized_epoch, - state.current_epoch() - 2, - "the head should be finalized two behind the current epoch" - ); - } - - #[test] - fn finalizes_with_two_thirds_participation() { - let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; - - let harness = get_harness(VALIDATOR_COUNT); - - let two_thirds = (VALIDATOR_COUNT / 3) * 2; - let attesters = (0..two_thirds).collect(); - - harness.extend_chain( - num_blocks_produced as usize, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::SomeValidators(attesters), - ); - - let state = &harness.chain.head().beacon_state; - - assert_eq!( - state.slot, num_blocks_produced, - "head should be at the current slot" - ); - assert_eq!( - state.current_epoch(), - num_blocks_produced / MinimalEthSpec::slots_per_epoch(), - "head should be at the expected epoch" - ); - - // Note: the 2/3rds tests are not justifying the immediately prior epochs because the - // `MIN_ATTESTATION_INCLUSION_DELAY` is preventing an adequate number of attestations being - // included in blocks during that epoch. - - assert_eq!( - state.current_justified_epoch, - state.current_epoch() - 2, - "the head should be justified two behind the current epoch" - ); - assert_eq!( - state.finalized_epoch, - state.current_epoch() - 4, - "the head should be finalized three behind the current epoch" - ); - } - - #[test] - fn does_not_finalize_with_less_than_two_thirds_participation() { - let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; - - let harness = get_harness(VALIDATOR_COUNT); - - let two_thirds = (VALIDATOR_COUNT / 3) * 2; - let less_than_two_thirds = two_thirds - 1; - let attesters = (0..less_than_two_thirds).collect(); - - harness.extend_chain( - num_blocks_produced as usize, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::SomeValidators(attesters), - ); - - let state = &harness.chain.head().beacon_state; - - assert_eq!( - state.slot, num_blocks_produced, - "head should be at the current slot" - ); - assert_eq!( - state.current_epoch(), - num_blocks_produced / MinimalEthSpec::slots_per_epoch(), - "head should be at the expected epoch" - ); - assert_eq!( - state.current_justified_epoch, 0, - "no epoch should have been justified" - ); - assert_eq!( - state.finalized_epoch, 0, - "no epoch should have been finalized" - ); - } - - #[test] - fn does_not_finalize_without_attestation() { - let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; - - let harness = get_harness(VALIDATOR_COUNT); - - harness.extend_chain( - num_blocks_produced as usize, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::SomeValidators(vec![]), - ); - - let state = &harness.chain.head().beacon_state; - - assert_eq!( - state.slot, num_blocks_produced, - "head should be at the current slot" - ); - assert_eq!( - state.current_epoch(), - num_blocks_produced / MinimalEthSpec::slots_per_epoch(), - "head should be at the expected epoch" - ); - assert_eq!( - state.current_justified_epoch, 0, - "no epoch should have been justified" - ); - assert_eq!( - state.finalized_epoch, 0, - "no epoch should have been finalized" - ); - } -} From f8fb011d6c491769951fb5ea3bbe63f778fdd613 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sun, 23 Jun 2019 10:33:35 +1000 Subject: [PATCH 34/40] Finish fork test for beacon chain --- beacon_node/beacon_chain/src/test_utils.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 19d0f8fb15..164857e5d8 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -121,7 +121,8 @@ where self.chain.catchup_state().expect("should catchup state"); } - /// Extend the `BeaconChain` with some blocks and attestations. + /// Extend the `BeaconChain` with some blocks and attestations. Returns the root of the + /// last-produced block (the head of the chain). /// /// Chain will be extended by `num_blocks` blocks. /// @@ -134,7 +135,7 @@ where num_blocks: usize, block_strategy: BlockStrategy, attestation_strategy: AttestationStrategy, - ) { + ) -> Hash256 { let mut state = { // Determine the slot for the first block (or skipped block). let state_slot = match block_strategy { @@ -151,6 +152,8 @@ where BlockStrategy::ForkCanonicalChainAt { first_slot, .. } => first_slot, }; + let mut head_block_root = None; + for _ in 0..num_blocks { while self.chain.read_slot_clock().expect("should have a slot") < slot { self.advance_slot(); @@ -164,6 +167,8 @@ where .expect("should not error during block processing"); if let BlockProcessingOutcome::Processed { block_root } = outcome { + head_block_root = Some(block_root); + self.add_attestations_to_op_pool( &attestation_strategy, &new_state, @@ -177,6 +182,8 @@ where state = new_state; slot += 1; } + + head_block_root.expect("did not produce any blocks") } fn get_state_at_slot(&self, state_slot: Slot) -> BeaconState { From 77fba0b98ed8a60de23ff23c70857338f559a94c Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sun, 23 Jun 2019 14:47:23 +1000 Subject: [PATCH 35/40] Fix bugs in fork choice, add more tests --- beacon_node/beacon_chain/src/beacon_chain.rs | 125 +++++++---- beacon_node/beacon_chain/src/errors.rs | 4 + beacon_node/beacon_chain/src/fork_choice.rs | 56 +++-- beacon_node/beacon_chain/tests/tests.rs | 225 +++++++++++++++++++ eth2/lmd_ghost/src/lib.rs | 23 +- eth2/lmd_ghost/src/reduced_tree.rs | 141 +++++++----- 6 files changed, 455 insertions(+), 119 deletions(-) create mode 100644 beacon_node/beacon_chain/tests/tests.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 2a4df66f1d..0137a0746b 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -120,7 +120,7 @@ impl BeaconChain { state: RwLock::new(genesis_state), canonical_head, genesis_block_root, - fork_choice: ForkChoice::new(store.clone(), genesis_block_root), + fork_choice: ForkChoice::new(store.clone(), &genesis_block, genesis_block_root), metrics: Metrics::new()?, store, }) @@ -145,11 +145,12 @@ impl BeaconChain { ); let last_finalized_root = p.canonical_head.beacon_state.finalized_root; + let last_finalized_block = &p.canonical_head.beacon_block; Ok(Some(BeaconChain { spec, slot_clock, - fork_choice: ForkChoice::new(store.clone(), last_finalized_root), + fork_choice: ForkChoice::new(store.clone(), last_finalized_block, last_finalized_root), op_pool: OperationPool::default(), canonical_head: RwLock::new(p.canonical_head), state: RwLock::new(p.state), @@ -239,37 +240,6 @@ impl BeaconChain { Ok(self.store.get(block_root)?) } - /// Update the canonical head to `new_head`. - fn update_canonical_head(&self, new_head: CheckPoint) -> Result<(), Error> { - // Update the checkpoint that stores the head of the chain at the time it received the - // block. - *self.canonical_head.write() = new_head; - - // Update the always-at-the-present-slot state we keep around for performance gains. - *self.state.write() = { - let mut state = self.canonical_head.read().beacon_state.clone(); - - let present_slot = match self.slot_clock.present_slot() { - Ok(Some(slot)) => slot, - _ => return Err(Error::UnableToReadSlot), - }; - - // If required, transition the new state to the present slot. - for _ in state.slot.as_u64()..present_slot.as_u64() { - per_slot_processing(&mut state, &self.spec)?; - } - - state.build_all_caches(&self.spec)?; - - state - }; - - // Save `self` to `self.store`. - self.persist()?; - - Ok(()) - } - /// Returns a read-lock guarded `BeaconState` which is the `canonical_head` that has been /// updated to match the current slot clock. pub fn current_state(&self) -> RwLockReadGuard> { @@ -800,17 +770,94 @@ impl BeaconChain { self.metrics.fork_choice_reorg_count.inc(); }; - self.update_canonical_head(CheckPoint { - beacon_block, - beacon_block_root, - beacon_state, - beacon_state_root, - })?; + let old_finalized_epoch = self.head().beacon_state.finalized_epoch; + let new_finalized_epoch = beacon_state.finalized_epoch; + let finalized_root = beacon_state.finalized_root; + + // Never revert back past a finalized epoch. + if new_finalized_epoch < old_finalized_epoch { + Err(Error::RevertedFinalizedEpoch { + previous_epoch: old_finalized_epoch, + new_epoch: new_finalized_epoch, + }) + } else { + self.update_canonical_head(CheckPoint { + beacon_block: beacon_block, + beacon_block_root, + beacon_state, + beacon_state_root, + })?; + + if new_finalized_epoch != old_finalized_epoch { + self.after_finalization(old_finalized_epoch, finalized_root)?; + } + + Ok(()) + } + } else { + Ok(()) } + } + + /// Update the canonical head to `new_head`. + fn update_canonical_head(&self, new_head: CheckPoint) -> Result<(), Error> { + // Update the checkpoint that stores the head of the chain at the time it received the + // block. + *self.canonical_head.write() = new_head; + + // Update the always-at-the-present-slot state we keep around for performance gains. + *self.state.write() = { + let mut state = self.canonical_head.read().beacon_state.clone(); + + let present_slot = match self.slot_clock.present_slot() { + Ok(Some(slot)) => slot, + _ => return Err(Error::UnableToReadSlot), + }; + + // If required, transition the new state to the present slot. + for _ in state.slot.as_u64()..present_slot.as_u64() { + per_slot_processing(&mut state, &self.spec)?; + } + + state.build_all_caches(&self.spec)?; + + state + }; + + // Save `self` to `self.store`. + self.persist()?; Ok(()) } + /// Called after `self` has had a new block finalized. + /// + /// Performs pruning and finality-based optimizations. + fn after_finalization( + &self, + old_finalized_epoch: Epoch, + finalized_block_root: Hash256, + ) -> Result<(), Error> { + let finalized_block = self + .store + .get::(&finalized_block_root)? + .ok_or_else(|| Error::MissingBeaconBlock(finalized_block_root))?; + + let new_finalized_epoch = finalized_block.slot.epoch(T::EthSpec::slots_per_epoch()); + + if new_finalized_epoch < old_finalized_epoch { + Err(Error::RevertedFinalizedEpoch { + previous_epoch: old_finalized_epoch, + new_epoch: new_finalized_epoch, + }) + } else { + self.fork_choice + .process_finalization(&finalized_block, finalized_block_root)?; + + Ok(()) + } + } + /// Returns `true` if the given block root has not been processed. pub fn is_new_block_root(&self, beacon_block_root: &Hash256) -> Result { Ok(!self.store.exists::(beacon_block_root)?) diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 157d774c6f..0d619d7f2d 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -19,6 +19,10 @@ pub enum BeaconChainError { InsufficientValidators, BadRecentBlockRoots, UnableToReadSlot, + RevertedFinalizedEpoch { + previous_epoch: Epoch, + new_epoch: Epoch, + }, BeaconStateError(BeaconStateError), DBInconsistent(String), DBError(store::Error), diff --git a/beacon_node/beacon_chain/src/fork_choice.rs b/beacon_node/beacon_chain/src/fork_choice.rs index 7aba6fdf5d..389e5b46bf 100644 --- a/beacon_node/beacon_chain/src/fork_choice.rs +++ b/beacon_node/beacon_chain/src/fork_choice.rs @@ -3,7 +3,7 @@ use lmd_ghost::LmdGhost; use state_processing::common::get_attesting_indices_unsorted; use std::sync::Arc; use store::{Error as StoreError, Store}; -use types::{Attestation, BeaconBlock, BeaconState, BeaconStateError, EthSpec, Hash256}; +use types::{Attestation, BeaconBlock, BeaconState, BeaconStateError, Epoch, EthSpec, Hash256}; type Result = std::result::Result; @@ -26,27 +26,41 @@ pub struct ForkChoice { } impl ForkChoice { - pub fn new(store: Arc, genesis_block_root: Hash256) -> Self { + /// Instantiate a new fork chooser. + /// + /// "Genesis" does not necessarily need to be the absolute genesis, it can be some finalized + /// block. + pub fn new( + store: Arc, + genesis_block: &BeaconBlock, + genesis_block_root: Hash256, + ) -> Self { Self { - backend: T::LmdGhost::new(store, genesis_block_root), + backend: T::LmdGhost::new(store, genesis_block, genesis_block_root), genesis_block_root, } } pub fn find_head(&self, chain: &BeaconChain) -> Result { + let start_slot = |epoch: Epoch| epoch.start_slot(T::EthSpec::slots_per_epoch()); + // From the specification: // // Let justified_head be the descendant of finalized_head with the highest epoch that has // been justified for at least 1 epoch ... If no such descendant exists, // set justified_head to finalized_head. - let (start_state, start_block_root) = { + let (start_state, start_block_root, start_block_slot) = { let state = chain.current_state(); - let block_root = if state.current_epoch() + 1 > state.current_justified_epoch { - state.current_justified_root - } else { - state.finalized_root - }; + let (block_root, block_slot) = + if state.current_epoch() + 1 > state.current_justified_epoch { + ( + state.current_justified_root, + start_slot(state.current_justified_epoch), + ) + } else { + (state.finalized_root, start_slot(state.finalized_epoch)) + }; let block = chain .store @@ -65,7 +79,7 @@ impl ForkChoice { .get::>(&block.state_root)? .ok_or_else(|| Error::MissingState(block.state_root))?; - (state, block_root) + (state, block_root, block_slot) }; // A function that returns the weight for some validator index. @@ -77,7 +91,7 @@ impl ForkChoice { }; self.backend - .find_head(start_block_root, weight) + .find_head(start_block_slot, start_block_root, weight) .map_err(Into::into) } @@ -101,7 +115,7 @@ impl ForkChoice { self.process_attestation_from_block(state, attestation)?; } - self.backend.process_block(block_root, block.slot)?; + self.backend.process_block(block, block_root)?; Ok(()) } @@ -131,8 +145,8 @@ impl ForkChoice { // 2. Ignore all attestations to the zero hash. // // (1) becomes weird once we hit finality and fork choice drops the genesis block. (2) is - // fine becuase votes to the genesis block are not usefully, all validators already - // implicitly attest to genesis just by being present in the chain. + // fine becuase votes to the genesis block are not useful; all validators implicitly attest + // to genesis just by being present in the chain. if block_hash != Hash256::zero() { let block_slot = attestation .data @@ -147,6 +161,20 @@ impl ForkChoice { Ok(()) } + + /// Inform the fork choice that the given block (and corresponding root) have been finalized so + /// it may prune it's storage. + /// + /// `finalized_block_root` must be the root of `finalized_block`. + pub fn process_finalization( + &self, + finalized_block: &BeaconBlock, + finalized_block_root: Hash256, + ) -> Result<()> { + self.backend + .update_finalized_root(finalized_block, finalized_block_root) + .map_err(Into::into) + } } impl From for Error { diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs new file mode 100644 index 0000000000..5181f1e768 --- /dev/null +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -0,0 +1,225 @@ +use beacon_chain::test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy}; +use lmd_ghost::ThreadSafeReducedTree; +use store::MemoryStore; +use types::{EthSpec, MinimalEthSpec, Slot}; + +// Should ideally be divisible by 3. +pub const VALIDATOR_COUNT: usize = 24; + +fn get_harness( + validator_count: usize, +) -> BeaconChainHarness, MinimalEthSpec> { + let harness = BeaconChainHarness::new(validator_count); + + // Move past the zero slot. + harness.advance_slot(); + + harness +} + +#[test] +fn fork() { + let harness = get_harness(VALIDATOR_COUNT); + + let two_thirds = (VALIDATOR_COUNT / 3) * 2; + let delay = MinimalEthSpec::default_spec().min_attestation_inclusion_delay as usize; + + let honest_validators: Vec = (0..two_thirds).collect(); + let faulty_validators: Vec = (two_thirds..VALIDATOR_COUNT).collect(); + + let initial_blocks = delay + 1; + let honest_fork_blocks = delay + 1; + let faulty_fork_blocks = delay + 2; + + // Build an initial chain were all validators agree. + harness.extend_chain( + initial_blocks, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ); + + // Move to the next slot so we may produce some more blocks on the head. + harness.advance_slot(); + + // Extend the chain with blocks where only honest validators agree. + let honest_head = harness.extend_chain( + honest_fork_blocks, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(honest_validators.clone()), + ); + + // Go back to the last block where all agreed, and build blocks upon it where only faulty nodes + // agree. + let faulty_head = harness.extend_chain( + faulty_fork_blocks, + BlockStrategy::ForkCanonicalChainAt { + previous_slot: Slot::from(initial_blocks), + first_slot: Slot::from(initial_blocks + 2), + }, + AttestationStrategy::SomeValidators(faulty_validators.clone()), + ); + + assert!(honest_head != faulty_head, "forks should be distinct"); + + let state = &harness.chain.head().beacon_state; + + assert_eq!( + state.slot, + Slot::from(initial_blocks + honest_fork_blocks), + "head should be at the current slot" + ); + + assert_eq!( + harness.chain.head().beacon_block_root, + honest_head, + "the honest chain should be the canonical chain" + ); +} + +#[test] +fn finalizes_with_full_participation() { + let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; + + let harness = get_harness(VALIDATOR_COUNT); + + harness.extend_chain( + num_blocks_produced as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ); + + let state = &harness.chain.head().beacon_state; + + assert_eq!( + state.slot, num_blocks_produced, + "head should be at the current slot" + ); + assert_eq!( + state.current_epoch(), + num_blocks_produced / MinimalEthSpec::slots_per_epoch(), + "head should be at the expected epoch" + ); + assert_eq!( + state.current_justified_epoch, + state.current_epoch() - 1, + "the head should be justified one behind the current epoch" + ); + assert_eq!( + state.finalized_epoch, + state.current_epoch() - 2, + "the head should be finalized two behind the current epoch" + ); +} + +#[test] +fn finalizes_with_two_thirds_participation() { + let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; + + let harness = get_harness(VALIDATOR_COUNT); + + let two_thirds = (VALIDATOR_COUNT / 3) * 2; + let attesters = (0..two_thirds).collect(); + + harness.extend_chain( + num_blocks_produced as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(attesters), + ); + + let state = &harness.chain.head().beacon_state; + + assert_eq!( + state.slot, num_blocks_produced, + "head should be at the current slot" + ); + assert_eq!( + state.current_epoch(), + num_blocks_produced / MinimalEthSpec::slots_per_epoch(), + "head should be at the expected epoch" + ); + + // Note: the 2/3rds tests are not justifying the immediately prior epochs because the + // `MIN_ATTESTATION_INCLUSION_DELAY` is preventing an adequate number of attestations being + // included in blocks during that epoch. + + assert_eq!( + state.current_justified_epoch, + state.current_epoch() - 2, + "the head should be justified two behind the current epoch" + ); + assert_eq!( + state.finalized_epoch, + state.current_epoch() - 4, + "the head should be finalized three behind the current epoch" + ); +} + +#[test] +fn does_not_finalize_with_less_than_two_thirds_participation() { + let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; + + let harness = get_harness(VALIDATOR_COUNT); + + let two_thirds = (VALIDATOR_COUNT / 3) * 2; + let less_than_two_thirds = two_thirds - 1; + let attesters = (0..less_than_two_thirds).collect(); + + harness.extend_chain( + num_blocks_produced as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(attesters), + ); + + let state = &harness.chain.head().beacon_state; + + assert_eq!( + state.slot, num_blocks_produced, + "head should be at the current slot" + ); + assert_eq!( + state.current_epoch(), + num_blocks_produced / MinimalEthSpec::slots_per_epoch(), + "head should be at the expected epoch" + ); + assert_eq!( + state.current_justified_epoch, 0, + "no epoch should have been justified" + ); + assert_eq!( + state.finalized_epoch, 0, + "no epoch should have been finalized" + ); +} + +#[test] +fn does_not_finalize_without_attestation() { + let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; + + let harness = get_harness(VALIDATOR_COUNT); + + harness.extend_chain( + num_blocks_produced as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(vec![]), + ); + + let state = &harness.chain.head().beacon_state; + + assert_eq!( + state.slot, num_blocks_produced, + "head should be at the current slot" + ); + assert_eq!( + state.current_epoch(), + num_blocks_produced / MinimalEthSpec::slots_per_epoch(), + "head should be at the expected epoch" + ); + assert_eq!( + state.current_justified_epoch, 0, + "no epoch should have been justified" + ); + assert_eq!( + state.finalized_epoch, 0, + "no epoch should have been finalized" + ); +} diff --git a/eth2/lmd_ghost/src/lib.rs b/eth2/lmd_ghost/src/lib.rs index 8f5d0b5299..dd413e2eb5 100644 --- a/eth2/lmd_ghost/src/lib.rs +++ b/eth2/lmd_ghost/src/lib.rs @@ -2,7 +2,7 @@ mod reduced_tree; use std::sync::Arc; use store::Store; -use types::{EthSpec, Hash256, Slot}; +use types::{BeaconBlock, EthSpec, Hash256, Slot}; pub use reduced_tree::ThreadSafeReducedTree; @@ -10,7 +10,7 @@ pub type Result = std::result::Result; pub trait LmdGhost: Send + Sync { /// Create a new instance, with the given `store` and `finalized_root`. - fn new(store: Arc, finalized_root: Hash256) -> Self; + fn new(store: Arc, finalized_block: &BeaconBlock, finalized_root: Hash256) -> Self; /// Process an attestation message from some validator that attests to some `block_hash` /// representing a block at some `block_slot`. @@ -22,14 +22,25 @@ pub trait LmdGhost: Send + Sync { ) -> Result<()>; /// Process a block that was seen on the network. - fn process_block(&self, block_hash: Hash256, block_slot: Slot) -> Result<()>; + fn process_block(&self, block: &BeaconBlock, block_hash: Hash256) -> Result<()>; /// Returns the head of the chain, starting the search at `start_block_root` and moving upwards /// (in block height). - fn find_head(&self, start_block_root: Hash256, weight: F) -> Result + fn find_head( + &self, + start_block_slot: Slot, + start_block_root: Hash256, + weight: F, + ) -> Result where F: Fn(usize) -> Option + Copy; - /// Provide an indication that the blockchain has been finalized at the given `finalized_root`. - fn update_finalized_root(&self, finalized_root: Hash256) -> Result<()>; + /// Provide an indication that the blockchain has been finalized at the given `finalized_block`. + /// + /// `finalized_block_root` must be the root of `finalized_block`. + fn update_finalized_root( + &self, + finalized_block: &BeaconBlock, + finalized_block_root: Hash256, + ) -> Result<()>; } diff --git a/eth2/lmd_ghost/src/reduced_tree.rs b/eth2/lmd_ghost/src/reduced_tree.rs index 2d70ea1453..82547bbf6b 100644 --- a/eth2/lmd_ghost/src/reduced_tree.rs +++ b/eth2/lmd_ghost/src/reduced_tree.rs @@ -40,9 +40,9 @@ where T: Store, E: EthSpec, { - fn new(store: Arc, genesis_root: Hash256) -> Self { + fn new(store: Arc, genesis_block: &BeaconBlock, genesis_root: Hash256) -> Self { ThreadSafeReducedTree { - core: RwLock::new(ReducedTree::new(store, genesis_root)), + core: RwLock::new(ReducedTree::new(store, genesis_block, genesis_root)), } } @@ -59,27 +59,32 @@ where } /// Process a block that was seen on the network. - fn process_block(&self, block_hash: Hash256, _block_slot: Slot) -> SuperResult<()> { + fn process_block(&self, block: &BeaconBlock, block_hash: Hash256) -> SuperResult<()> { self.core .write() - .add_weightless_node(block_hash) + .add_weightless_node(block.slot, block_hash) .map_err(|e| format!("process_block failed: {:?}", e)) } - fn find_head(&self, start_block_root: Hash256, weight_fn: F) -> SuperResult + fn find_head( + &self, + start_block_slot: Slot, + start_block_root: Hash256, + weight_fn: F, + ) -> SuperResult where F: Fn(usize) -> Option + Copy, { self.core .write() - .update_weights_and_find_head(start_block_root, weight_fn) + .update_weights_and_find_head(start_block_slot, start_block_root, weight_fn) .map_err(|e| format!("find_head failed: {:?}", e)) } - fn update_finalized_root(&self, new_root: Hash256) -> SuperResult<()> { + fn update_finalized_root(&self, new_block: &BeaconBlock, new_root: Hash256) -> SuperResult<()> { self.core .write() - .update_root(new_root) + .update_root(new_block.slot, new_root) .map_err(|e| format!("update_finalized_root failed: {:?}", e)) } } @@ -91,7 +96,7 @@ struct ReducedTree { /// Maps validator indices to their latest votes. latest_votes: ElasticList>, /// Stores the root of the tree, used for pruning. - root: Hash256, + root: (Hash256, Slot), _phantom: PhantomData, } @@ -100,7 +105,7 @@ where T: Store, E: EthSpec, { - pub fn new(store: Arc, genesis_root: Hash256) -> Self { + pub fn new(store: Arc, genesis_block: &BeaconBlock, genesis_root: Hash256) -> Self { let mut nodes = HashMap::new(); // Insert the genesis node. @@ -116,12 +121,12 @@ where store, nodes, latest_votes: ElasticList::default(), - root: genesis_root, + root: (genesis_root, genesis_block.slot), _phantom: PhantomData, } } - pub fn update_root(&mut self, new_root: Hash256) -> Result<()> { + pub fn update_root(&mut self, new_slot: Slot, new_root: Hash256) -> Result<()> { if !self.nodes.contains_key(&new_root) { let node = Node { block_hash: new_root, @@ -132,16 +137,22 @@ where self.add_node(node)?; } - self.retain_subtree(self.root, new_root)?; + self.retain_subtree(self.root.0, new_root)?; - self.root = new_root; + self.root = (new_root, new_slot); + + let root_node = self.get_mut_node(new_root)?; + root_node.parent_hash = None; Ok(()) } + /// Removes `current_hash` and all decendants, except `subtree_hash` and all nodes + /// which have `subtree_hash` as an ancestor. + /// + /// In effect, prunes the tree so that only decendants of `subtree_hash` exist. fn retain_subtree(&mut self, current_hash: Hash256, subtree_hash: Hash256) -> Result<()> { if current_hash != subtree_hash { - // Clone satisifies the borrow checker. let children = self.get_node(current_hash)?.children.clone(); for child_hash in children { @@ -160,39 +171,42 @@ where block_hash: Hash256, slot: Slot, ) -> Result<()> { - if let Some(previous_vote) = self.latest_votes.get(validator_index) { - if previous_vote.slot > slot { - // Given vote is earier than known vote, nothing to do. - return Ok(()); - } else if previous_vote.slot == slot && previous_vote.hash == block_hash { - // Given vote is identical to known vote, nothing to do. - return Ok(()); - } else if previous_vote.slot == slot && previous_vote.hash != block_hash { - // Vote is an equivocation (double-vote), ignore it. - // - // TODO: this is slashable. - return Ok(()); - } else { - // Given vote is newer or different to current vote, replace the current vote. - self.remove_latest_message(validator_index)?; + if slot >= self.root_slot() { + if let Some(previous_vote) = self.latest_votes.get(validator_index) { + if previous_vote.slot > slot { + // Given vote is earier than known vote, nothing to do. + return Ok(()); + } else if previous_vote.slot == slot && previous_vote.hash == block_hash { + // Given vote is identical to known vote, nothing to do. + return Ok(()); + } else if previous_vote.slot == slot && previous_vote.hash != block_hash { + // Vote is an equivocation (double-vote), ignore it. + // + // TODO: this is slashable. + return Ok(()); + } else { + // Given vote is newer or different to current vote, replace the current vote. + self.remove_latest_message(validator_index)?; + } } + + self.latest_votes.insert( + validator_index, + Some(Vote { + slot, + hash: block_hash, + }), + ); + + self.add_latest_message(validator_index, block_hash)?; } - self.latest_votes.insert( - validator_index, - Some(Vote { - slot, - hash: block_hash, - }), - ); - - self.add_latest_message(validator_index, block_hash)?; - Ok(()) } pub fn update_weights_and_find_head( &mut self, + start_block_slot: Slot, start_block_root: Hash256, weight_fn: F, ) -> Result @@ -203,7 +217,7 @@ where // // In this case, we add a weightless node at `start_block_root`. if !self.nodes.contains_key(&start_block_root) { - self.add_weightless_node(start_block_root)?; + self.add_weightless_node(start_block_slot, start_block_root)?; }; let _root_weight = self.update_weight(start_block_root, weight_fn)?; @@ -289,13 +303,15 @@ where // // Load the child of the node and set it's parent to be the parent of this // node (viz., graft the node's child to the node's parent) - let child = self - .nodes - .get_mut(&node.children[0]) - .ok_or_else(|| Error::MissingNode(node.children[0]))?; - + let child = self.get_mut_node(node.children[0])?; child.parent_hash = node.parent_hash; + // Graft the parent of this node to it's child. + if let Some(parent_hash) = node.parent_hash { + let parent = self.get_mut_node(parent_hash)?; + parent.replace_child(node.block_hash, node.children[0])?; + } + true } else if node.children.len() == 0 { // A node which has no children may be deleted and potentially it's parent @@ -377,17 +393,19 @@ where Ok(()) } - fn add_weightless_node(&mut self, hash: Hash256) -> Result<()> { - if !self.nodes.contains_key(&hash) { - let node = Node { - block_hash: hash, - ..Node::default() - }; + fn add_weightless_node(&mut self, slot: Slot, hash: Hash256) -> Result<()> { + if slot >= self.root_slot() { + if !self.nodes.contains_key(&hash) { + let node = Node { + block_hash: hash, + ..Node::default() + }; - self.add_node(node)?; + self.add_node(node)?; - if let Some(parent_hash) = self.get_node(hash)?.parent_hash { - self.maybe_delete_node(parent_hash)?; + if let Some(parent_hash) = self.get_node(hash)?.parent_hash { + self.maybe_delete_node(parent_hash)?; + } } } @@ -403,11 +421,9 @@ where self.get_mut_node(hash)?.clone() }; - let mut added_new_ancestor = false; + let mut added = false; if !prev_in_tree.children.is_empty() { - let mut added = false; - for &child_hash in &prev_in_tree.children { if self .iter_ancestors(child_hash)? @@ -418,6 +434,7 @@ where child.parent_hash = Some(node.block_hash); node.children.push(child_hash); prev_in_tree.replace_child(child_hash, node.block_hash)?; + node.parent_hash = Some(prev_in_tree.block_hash); added = true; @@ -446,7 +463,7 @@ where self.nodes .insert(common_ancestor.block_hash, common_ancestor); - added_new_ancestor = true; + added = true; break; } @@ -454,7 +471,7 @@ where } } - if !added_new_ancestor { + if !added { node.parent_hash = Some(prev_in_tree.block_hash); prev_in_tree.children.push(node.block_hash); } @@ -558,6 +575,10 @@ where .get::>(&state_root)? .ok_or_else(|| Error::MissingState(state_root)) } + + fn root_slot(&self) -> Slot { + self.root.1 + } } #[derive(Default, Clone, Debug)] From 075c98937510f650ddfcfa1b71e43a7ab42b938b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sun, 23 Jun 2019 15:05:16 +1000 Subject: [PATCH 36/40] Remove old fork_choice crate --- Cargo.toml | 1 - beacon_node/client/Cargo.toml | 1 - beacon_node/http_server/Cargo.toml | 1 - beacon_node/network/src/sync/simple_sync.rs | 7 +- beacon_node/rpc/src/beacon_block.rs | 3 +- eth2/fork_choice/Cargo.toml | 25 - eth2/fork_choice/benches/benches.rs | 75 --- eth2/fork_choice/examples/example.rs | 40 -- eth2/fork_choice/fork_choice/Cargo.toml | 25 - .../fork_choice/benches/benches.rs | 75 --- .../fork_choice/examples/example.rs | 40 -- .../tests/bitwise_lmd_ghost_test_vectors.yaml | 144 ------ .../tests/lmd_ghost_test_vectors.yaml | 65 --- .../tests/longest_chain_test_vectors.yaml | 51 -- eth2/fork_choice/fork_choice/tests/tests.rs | 231 --------- eth2/fork_choice/src/bitwise_lmd_ghost.rs | 476 ------------------ eth2/fork_choice/src/lib.rs | 95 ---- eth2/fork_choice/src/longest_chain.rs | 100 ---- eth2/fork_choice/src/optimized_lmd_ghost.rs | 447 ---------------- eth2/fork_choice/src/slow_lmd_ghost.rs | 212 -------- eth2/fork_choice/src/test_utils.rs | 91 ---- .../tests/bitwise_lmd_ghost_test_vectors.yaml | 144 ------ .../tests/lmd_ghost_test_vectors.yaml | 65 --- .../tests/longest_chain_test_vectors.yaml | 51 -- eth2/fork_choice/tests/tests.rs | 231 --------- 25 files changed, 6 insertions(+), 2690 deletions(-) delete mode 100644 eth2/fork_choice/Cargo.toml delete mode 100644 eth2/fork_choice/benches/benches.rs delete mode 100644 eth2/fork_choice/examples/example.rs delete mode 100644 eth2/fork_choice/fork_choice/Cargo.toml delete mode 100644 eth2/fork_choice/fork_choice/benches/benches.rs delete mode 100644 eth2/fork_choice/fork_choice/examples/example.rs delete mode 100644 eth2/fork_choice/fork_choice/tests/bitwise_lmd_ghost_test_vectors.yaml delete mode 100644 eth2/fork_choice/fork_choice/tests/lmd_ghost_test_vectors.yaml delete mode 100644 eth2/fork_choice/fork_choice/tests/longest_chain_test_vectors.yaml delete mode 100644 eth2/fork_choice/fork_choice/tests/tests.rs delete mode 100644 eth2/fork_choice/src/bitwise_lmd_ghost.rs delete mode 100644 eth2/fork_choice/src/lib.rs delete mode 100644 eth2/fork_choice/src/longest_chain.rs delete mode 100644 eth2/fork_choice/src/optimized_lmd_ghost.rs delete mode 100644 eth2/fork_choice/src/slow_lmd_ghost.rs delete mode 100644 eth2/fork_choice/src/test_utils.rs delete mode 100644 eth2/fork_choice/tests/bitwise_lmd_ghost_test_vectors.yaml delete mode 100644 eth2/fork_choice/tests/lmd_ghost_test_vectors.yaml delete mode 100644 eth2/fork_choice/tests/longest_chain_test_vectors.yaml delete mode 100644 eth2/fork_choice/tests/tests.rs diff --git a/Cargo.toml b/Cargo.toml index 27c473bb9a..ef17b431ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,5 @@ [workspace] members = [ - "eth2/fork_choice", "eth2/lmd_ghost", "eth2/operation_pool", "eth2/state_processing", diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index 2b6f44e949..f97302a7c0 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -10,7 +10,6 @@ network = { path = "../network" } store = { path = "../store" } http_server = { path = "../http_server" } rpc = { path = "../rpc" } -fork_choice = { path = "../../eth2/fork_choice" } prometheus = "^0.6" types = { path = "../../eth2/types" } tree_hash = { path = "../../eth2/utils/tree_hash" } diff --git a/beacon_node/http_server/Cargo.toml b/beacon_node/http_server/Cargo.toml index 098c3e1c9d..45e0349f55 100644 --- a/beacon_node/http_server/Cargo.toml +++ b/beacon_node/http_server/Cargo.toml @@ -16,7 +16,6 @@ types = { path = "../../eth2/types" } ssz = { path = "../../eth2/utils/ssz" } slot_clock = { path = "../../eth2/utils/slot_clock" } protos = { path = "../../protos" } -fork_choice = { path = "../../eth2/fork_choice" } grpcio = { version = "0.4", default-features = false, features = ["protobuf-codec"] } persistent = "^0.4" protobuf = "2.0.2" diff --git a/beacon_node/network/src/sync/simple_sync.rs b/beacon_node/network/src/sync/simple_sync.rs index 528edca98e..0a082afcf7 100644 --- a/beacon_node/network/src/sync/simple_sync.rs +++ b/beacon_node/network/src/sync/simple_sync.rs @@ -517,7 +517,7 @@ impl SimpleSync { self.process_block(peer_id.clone(), block.clone(), network, &"gossip") { match outcome { - BlockProcessingOutcome::Processed => SHOULD_FORWARD_GOSSIP_BLOCK, + BlockProcessingOutcome::Processed { .. } => SHOULD_FORWARD_GOSSIP_BLOCK, BlockProcessingOutcome::ParentUnknown { .. } => { self.import_queue .enqueue_full_blocks(vec![block], peer_id.clone()); @@ -582,7 +582,7 @@ impl SimpleSync { _ => true, }; - if processing_result == Some(BlockProcessingOutcome::Processed) { + if processing_result == Some(BlockProcessingOutcome::Processed { block_root }) { successful += 1; } @@ -695,11 +695,12 @@ impl SimpleSync { if let Ok(outcome) = processing_result { match outcome { - BlockProcessingOutcome::Processed => { + BlockProcessingOutcome::Processed { block_root } => { info!( self.log, "Imported block from network"; "source" => source, "slot" => block.slot, + "block_root" => format!("{}", block_root), "peer" => format!("{:?}", peer_id), ); } diff --git a/beacon_node/rpc/src/beacon_block.rs b/beacon_node/rpc/src/beacon_block.rs index d36cb1f313..533fd285a3 100644 --- a/beacon_node/rpc/src/beacon_block.rs +++ b/beacon_node/rpc/src/beacon_block.rs @@ -95,12 +95,13 @@ impl BeaconBlockService for BeaconBlockServiceInstance { Ok(block) => { match self.chain.process_block(block.clone()) { Ok(outcome) => { - if outcome == BlockProcessingOutcome::Processed { + if let BlockProcessingOutcome::Processed { block_root } = outcome { // Block was successfully processed. info!( self.log, "Valid block from RPC"; "block_slot" => block.slot, + "block_root" => format!("{}", block_root), ); // TODO: Obtain topics from the network service properly. diff --git a/eth2/fork_choice/Cargo.toml b/eth2/fork_choice/Cargo.toml deleted file mode 100644 index e37e415e49..0000000000 --- a/eth2/fork_choice/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "fork_choice" -version = "0.1.0" -authors = ["Age Manning "] -edition = "2018" - -[[bench]] -name = "benches" -harness = false - -[dependencies] -store = { path = "../../beacon_node/store" } -ssz = { path = "../utils/ssz" } -types = { path = "../types" } -log = "0.4.6" -bit-vec = "0.5.0" - -[dev-dependencies] -criterion = "0.2" -hex = "0.3.2" -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/benches/benches.rs b/eth2/fork_choice/benches/benches.rs deleted file mode 100644 index f311e1ccbb..0000000000 --- a/eth2/fork_choice/benches/benches.rs +++ /dev/null @@ -1,75 +0,0 @@ -use criterion::Criterion; -use criterion::{criterion_group, criterion_main, Benchmark}; -use fork_choice::{test_utils::TestingForkChoiceBuilder, ForkChoice, OptimizedLMDGhost}; -use std::sync::Arc; -use store::MemoryStore; -use types::{ChainSpec, EthSpec, MainnetEthSpec}; - -pub type TestedForkChoice = OptimizedLMDGhost; -pub type TestedEthSpec = MainnetEthSpec; - -/// Helper function to setup a builder and spec. -fn setup( - validator_count: usize, - chain_length: usize, -) -> ( - TestingForkChoiceBuilder, - ChainSpec, -) { - let store = MemoryStore::open(); - let builder: TestingForkChoiceBuilder = - TestingForkChoiceBuilder::new(validator_count, chain_length, Arc::new(store)); - let spec = TestedEthSpec::default_spec(); - - (builder, spec) -} - -/// Benches adding blocks to fork_choice. -fn add_block(c: &mut Criterion) { - let validator_count = 16; - let chain_length = 100; - - let (builder, spec) = setup(validator_count, chain_length); - - c.bench( - &format!("{}_blocks", chain_length), - Benchmark::new("add_blocks", move |b| { - b.iter(|| { - let mut fc = builder.build::>(); - for (root, block) in builder.chain.iter().skip(1) { - fc.add_block(block, root, &spec).unwrap(); - } - }) - }) - .sample_size(10), - ); -} - -/// Benches fork choice head finding. -fn find_head(c: &mut Criterion) { - let validator_count = 16; - let chain_length = 64 * 2; - - let (builder, spec) = setup(validator_count, chain_length); - - let mut fc = builder.build::>(); - for (root, block) in builder.chain.iter().skip(1) { - fc.add_block(block, root, &spec).unwrap(); - } - - let head_root = builder.chain.last().unwrap().0; - for i in 0..validator_count { - fc.add_attestation(i as u64, &head_root, &spec).unwrap(); - } - - c.bench( - &format!("{}_blocks", chain_length), - Benchmark::new("find_head", move |b| { - b.iter(|| fc.find_head(&builder.genesis_root(), &spec).unwrap()) - }) - .sample_size(10), - ); -} - -criterion_group!(benches, add_block, find_head); -criterion_main!(benches); diff --git a/eth2/fork_choice/examples/example.rs b/eth2/fork_choice/examples/example.rs deleted file mode 100644 index a912c3753c..0000000000 --- a/eth2/fork_choice/examples/example.rs +++ /dev/null @@ -1,40 +0,0 @@ -use fork_choice::{test_utils::TestingForkChoiceBuilder, ForkChoice, OptimizedLMDGhost}; -use std::sync::Arc; -use store::{MemoryStore, Store}; -use types::{BeaconBlock, ChainSpec, EthSpec, Hash256, MainnetEthSpec}; - -fn main() { - let validator_count = 16; - let chain_length = 100; - let repetitions = 50; - - let store = MemoryStore::open(); - let builder: TestingForkChoiceBuilder = - TestingForkChoiceBuilder::new(validator_count, chain_length, Arc::new(store)); - - let fork_choosers: Vec> = (0..repetitions) - .into_iter() - .map(|_| builder.build()) - .collect(); - - let spec = &MainnetEthSpec::default_spec(); - - println!("Running {} times...", repetitions); - for fc in fork_choosers { - do_thing(fc, &builder.chain, builder.genesis_root(), spec); - } -} - -#[inline(never)] -fn do_thing, S: Store>( - mut fc: F, - chain: &[(Hash256, BeaconBlock)], - genesis_root: Hash256, - spec: &ChainSpec, -) { - for (root, block) in chain.iter().skip(1) { - fc.add_block(block, root, spec).unwrap(); - } - - let _head = fc.find_head(&genesis_root, spec).unwrap(); -} diff --git a/eth2/fork_choice/fork_choice/Cargo.toml b/eth2/fork_choice/fork_choice/Cargo.toml deleted file mode 100644 index e37e415e49..0000000000 --- a/eth2/fork_choice/fork_choice/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "fork_choice" -version = "0.1.0" -authors = ["Age Manning "] -edition = "2018" - -[[bench]] -name = "benches" -harness = false - -[dependencies] -store = { path = "../../beacon_node/store" } -ssz = { path = "../utils/ssz" } -types = { path = "../types" } -log = "0.4.6" -bit-vec = "0.5.0" - -[dev-dependencies] -criterion = "0.2" -hex = "0.3.2" -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/fork_choice/benches/benches.rs b/eth2/fork_choice/fork_choice/benches/benches.rs deleted file mode 100644 index f311e1ccbb..0000000000 --- a/eth2/fork_choice/fork_choice/benches/benches.rs +++ /dev/null @@ -1,75 +0,0 @@ -use criterion::Criterion; -use criterion::{criterion_group, criterion_main, Benchmark}; -use fork_choice::{test_utils::TestingForkChoiceBuilder, ForkChoice, OptimizedLMDGhost}; -use std::sync::Arc; -use store::MemoryStore; -use types::{ChainSpec, EthSpec, MainnetEthSpec}; - -pub type TestedForkChoice = OptimizedLMDGhost; -pub type TestedEthSpec = MainnetEthSpec; - -/// Helper function to setup a builder and spec. -fn setup( - validator_count: usize, - chain_length: usize, -) -> ( - TestingForkChoiceBuilder, - ChainSpec, -) { - let store = MemoryStore::open(); - let builder: TestingForkChoiceBuilder = - TestingForkChoiceBuilder::new(validator_count, chain_length, Arc::new(store)); - let spec = TestedEthSpec::default_spec(); - - (builder, spec) -} - -/// Benches adding blocks to fork_choice. -fn add_block(c: &mut Criterion) { - let validator_count = 16; - let chain_length = 100; - - let (builder, spec) = setup(validator_count, chain_length); - - c.bench( - &format!("{}_blocks", chain_length), - Benchmark::new("add_blocks", move |b| { - b.iter(|| { - let mut fc = builder.build::>(); - for (root, block) in builder.chain.iter().skip(1) { - fc.add_block(block, root, &spec).unwrap(); - } - }) - }) - .sample_size(10), - ); -} - -/// Benches fork choice head finding. -fn find_head(c: &mut Criterion) { - let validator_count = 16; - let chain_length = 64 * 2; - - let (builder, spec) = setup(validator_count, chain_length); - - let mut fc = builder.build::>(); - for (root, block) in builder.chain.iter().skip(1) { - fc.add_block(block, root, &spec).unwrap(); - } - - let head_root = builder.chain.last().unwrap().0; - for i in 0..validator_count { - fc.add_attestation(i as u64, &head_root, &spec).unwrap(); - } - - c.bench( - &format!("{}_blocks", chain_length), - Benchmark::new("find_head", move |b| { - b.iter(|| fc.find_head(&builder.genesis_root(), &spec).unwrap()) - }) - .sample_size(10), - ); -} - -criterion_group!(benches, add_block, find_head); -criterion_main!(benches); diff --git a/eth2/fork_choice/fork_choice/examples/example.rs b/eth2/fork_choice/fork_choice/examples/example.rs deleted file mode 100644 index a912c3753c..0000000000 --- a/eth2/fork_choice/fork_choice/examples/example.rs +++ /dev/null @@ -1,40 +0,0 @@ -use fork_choice::{test_utils::TestingForkChoiceBuilder, ForkChoice, OptimizedLMDGhost}; -use std::sync::Arc; -use store::{MemoryStore, Store}; -use types::{BeaconBlock, ChainSpec, EthSpec, Hash256, MainnetEthSpec}; - -fn main() { - let validator_count = 16; - let chain_length = 100; - let repetitions = 50; - - let store = MemoryStore::open(); - let builder: TestingForkChoiceBuilder = - TestingForkChoiceBuilder::new(validator_count, chain_length, Arc::new(store)); - - let fork_choosers: Vec> = (0..repetitions) - .into_iter() - .map(|_| builder.build()) - .collect(); - - let spec = &MainnetEthSpec::default_spec(); - - println!("Running {} times...", repetitions); - for fc in fork_choosers { - do_thing(fc, &builder.chain, builder.genesis_root(), spec); - } -} - -#[inline(never)] -fn do_thing, S: Store>( - mut fc: F, - chain: &[(Hash256, BeaconBlock)], - genesis_root: Hash256, - spec: &ChainSpec, -) { - for (root, block) in chain.iter().skip(1) { - fc.add_block(block, root, spec).unwrap(); - } - - let _head = fc.find_head(&genesis_root, spec).unwrap(); -} diff --git a/eth2/fork_choice/fork_choice/tests/bitwise_lmd_ghost_test_vectors.yaml b/eth2/fork_choice/fork_choice/tests/bitwise_lmd_ghost_test_vectors.yaml deleted file mode 100644 index 61b0b05c40..0000000000 --- a/eth2/fork_choice/fork_choice/tests/bitwise_lmd_ghost_test_vectors.yaml +++ /dev/null @@ -1,144 +0,0 @@ -title: Fork-choice Tests -summary: A collection of abstract fork-choice tests for bitwise lmd ghost. -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' -# bitwise LMD ghost example. bitwise GHOST gives b2 -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - - id: 'b3' - parent: 'b0' - weights: - - b1: 5 - - b2: 4 - - b3: 3 - heads: - - id: 'b2' -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - - id: 'b3' - parent: 'b1' - - id: 'b4' - parent: 'b1' - - id: 'b5' - parent: 'b1' - - id: 'b6' - parent: 'b2' - - id: 'b7' - parent: 'b6' - weights: - - b0: 0 - - b1: 3 - - b2: 2 - - b3: 1 - - b4: 1 - - b5: 1 - - b6: 2 - - b7: 2 - heads: - - id: 'b4' -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - - id: 'b3' - parent: 'b0' - - id: 'b4' - parent: 'b1' - - id: 'b5' - parent: 'b1' - - id: 'b6' - parent: 'b2' - - id: 'b7' - parent: 'b2' - - id: 'b8' - parent: 'b3' - - id: 'b9' - parent: 'b3' - weights: - - b1: 2 - - b2: 1 - - b3: 1 - - b4: 7 - - b5: 5 - - b6: 2 - - b7: 4 - - b8: 4 - - b9: 2 - heads: - - id: 'b4' -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - - id: 'b3' - parent: 'b0' - - id: 'b4' - parent: 'b1' - - id: 'b5' - parent: 'b1' - - id: 'b6' - parent: 'b2' - - id: 'b7' - parent: 'b2' - - id: 'b8' - parent: 'b3' - - id: 'b9' - parent: 'b3' - weights: - - b1: 1 - - b2: 1 - - b3: 1 - - b4: 7 - - b5: 5 - - b6: 2 - - b7: 4 - - b8: 4 - - b9: 2 - heads: - - id: 'b7' -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - weights: - - b1: 0 - - b2: 0 - heads: - - id: 'b1' - diff --git a/eth2/fork_choice/fork_choice/tests/lmd_ghost_test_vectors.yaml b/eth2/fork_choice/fork_choice/tests/lmd_ghost_test_vectors.yaml deleted file mode 100644 index e7847de11a..0000000000 --- a/eth2/fork_choice/fork_choice/tests/lmd_ghost_test_vectors.yaml +++ /dev/null @@ -1,65 +0,0 @@ -title: Fork-choice Tests -summary: A collection of abstract fork-choice tests for lmd ghost. -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' -# bitwise LMD ghost example. GHOST gives b1 -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - - id: 'b3' - parent: 'b0' - weights: - - b1: 5 - - b2: 4 - - b3: 3 - heads: - - id: 'b1' -# equal weights children. Should choose lower hash b2 -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - - id: 'b3' - parent: 'b0' - weights: - - b1: 5 - - b2: 6 - - b3: 6 - heads: - - id: 'b2' -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - weights: - - b1: 0 - - b2: 0 - heads: - - id: 'b1' diff --git a/eth2/fork_choice/fork_choice/tests/longest_chain_test_vectors.yaml b/eth2/fork_choice/fork_choice/tests/longest_chain_test_vectors.yaml deleted file mode 100644 index e1cd61f06a..0000000000 --- a/eth2/fork_choice/fork_choice/tests/longest_chain_test_vectors.yaml +++ /dev/null @@ -1,51 +0,0 @@ -title: Fork-choice Tests -summary: A collection of abstract fork-choice tests to verify the longest chain fork-choice rule. -test_suite: Fork-Choice - -test_cases: -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b1' - - id: 'b3' - parent: 'b1' - - id: 'b4' - parent: 'b3' - weights: - - b0: 0 - - b1: 0 - - b2: 10 - - b3: 1 - heads: - - id: 'b4' -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b1' - - id: 'b3' - parent: 'b2' - - id: 'b4' - parent: 'b3' - - id: 'b5' - parent: 'b0' - - id: 'b6' - parent: 'b5' - - id: 'b7' - parent: 'b6' - - id: 'b8' - parent: 'b7' - - id: 'b9' - parent: 'b8' - weights: - - b0: 5 - - b1: 20 - - b2: 10 - - b3: 10 - heads: - - id: 'b9' diff --git a/eth2/fork_choice/fork_choice/tests/tests.rs b/eth2/fork_choice/fork_choice/tests/tests.rs deleted file mode 100644 index 39e70a7ddb..0000000000 --- a/eth2/fork_choice/fork_choice/tests/tests.rs +++ /dev/null @@ -1,231 +0,0 @@ -#![cfg(not(debug_assertions))] -/// Tests the available fork-choice algorithms -pub use beacon_chain::BeaconChain; -use bls::Signature; -use store::MemoryStore; -use store::Store; -// use env_logger::{Builder, Env}; -use fork_choice::{BitwiseLMDGhost, ForkChoice, LongestChain, OptimizedLMDGhost, SlowLMDGhost}; -use std::collections::HashMap; -use std::sync::Arc; -use std::{fs::File, io::prelude::*, path::PathBuf}; -use types::test_utils::TestingBeaconStateBuilder; -use types::{ - BeaconBlock, BeaconBlockBody, Eth1Data, EthSpec, Hash256, Keypair, MainnetEthSpec, Slot, -}; -use yaml_rust::yaml; - -// Note: We Assume the block Id's are hex-encoded. - -#[test] -fn test_optimized_lmd_ghost() { - // set up logging - // Builder::from_env(Env::default().default_filter_or("trace")).init(); - - test_yaml_vectors::>( - "tests/lmd_ghost_test_vectors.yaml", - 100, - ); -} - -#[test] -fn test_bitwise_lmd_ghost() { - // set up logging - //Builder::from_env(Env::default().default_filter_or("trace")).init(); - - test_yaml_vectors::>( - "tests/bitwise_lmd_ghost_test_vectors.yaml", - 100, - ); -} - -#[test] -fn test_slow_lmd_ghost() { - test_yaml_vectors::>( - "tests/lmd_ghost_test_vectors.yaml", - 100, - ); -} - -#[test] -fn test_longest_chain() { - test_yaml_vectors::>("tests/longest_chain_test_vectors.yaml", 100); -} - -// run a generic test over given YAML test vectors -fn test_yaml_vectors>( - yaml_file_path: &str, - emulated_validators: usize, // the number of validators used to give weights. -) { - // load test cases from yaml - let test_cases = load_test_cases_from_yaml(yaml_file_path); - - // default vars - let spec = MainnetEthSpec::default_spec(); - let zero_hash = Hash256::zero(); - let eth1_data = Eth1Data { - deposit_count: 0, - deposit_root: zero_hash.clone(), - block_hash: zero_hash.clone(), - }; - let randao_reveal = Signature::empty_signature(); - let signature = Signature::empty_signature(); - let body = BeaconBlockBody { - eth1_data, - randao_reveal, - graffiti: [0; 32], - proposer_slashings: vec![], - attester_slashings: vec![], - attestations: vec![], - deposits: vec![], - voluntary_exits: vec![], - transfers: vec![], - }; - - // process the tests - for test_case in test_cases { - // setup a fresh test - let (mut fork_choice, store, state_root) = setup_inital_state::(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(); - // assume the block tree is given to us in order. - let mut genesis_hash = None; - 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().to_string(); - - // default params for genesis - let block_hash = id_to_hash(&block_id); - let mut slot = spec.genesis_slot; - let previous_block_root = id_to_hash(&parent_id); - - // set the slot and parent based off the YAML. Start with genesis; - // if not the genesis, update slot - if parent_id != block_id { - // find parent slot - slot = *(block_slot - .get(&previous_block_root) - .expect("Parent should have a slot number")) - + 1; - } else { - genesis_hash = Some(block_hash); - } - - // update slot mapping - block_slot.insert(block_hash, slot); - - // build the BeaconBlock - let beacon_block = BeaconBlock { - slot, - previous_block_root, - state_root: state_root.clone(), - signature: signature.clone(), - body: body.clone(), - }; - - // Store the block. - store.put(&block_hash, &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, &spec) - .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 = id_to_hash(&id.to_string()); - 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 <= emulated_validators, - "Not enough validators to emulate weights" - ); - fork_choice - .add_attestation(current_validator as u64, &block_root, &spec) - .unwrap(); - current_validator += 1; - } - } - } - - // everything is set up, run the fork choice, using genesis as the head - let head = fork_choice - .find_head(&genesis_hash.unwrap(), &spec) - .unwrap(); - - // compare the result to the expected test - let success = test_case["heads"] - .clone() - .into_vec() - .unwrap() - .iter() - .find(|heads| id_to_hash(&heads["id"].as_str().unwrap().to_string()) == head) - .is_some(); - - println!("Head found: {}", head); - assert!(success, "Did not find one of the possible heads"); - } -} - -// loads the test_cases from the supplied yaml file -fn load_test_cases_from_yaml(file_path: &str) -> Vec { - // load the yaml - let mut file = { - let mut file_path_buf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - file_path_buf.push(file_path); - 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]; - doc["test_cases"].as_vec().unwrap().clone() -} - -fn setup_inital_state( - // fork_choice_algo: &ForkChoiceAlgorithm, - num_validators: usize -) -> (T, Arc, Hash256) -where - T: ForkChoice, -{ - let store = Arc::new(MemoryStore::open()); - - let fork_choice = ForkChoice::new(store.clone()); - let spec = MainnetEthSpec::default_spec(); - - let mut state_builder: TestingBeaconStateBuilder = - TestingBeaconStateBuilder::from_single_keypair(num_validators, &Keypair::random(), &spec); - state_builder.build_caches(&spec).unwrap(); - let (state, _keypairs) = state_builder.build(); - - let state_root = state.canonical_root(); - store.put(&state_root, &state).unwrap(); - - // return initialised vars - (fork_choice, store, state_root) -} - -// convert a block_id into a Hash256 -- assume input is hex encoded; -fn id_to_hash(id: &String) -> Hash256 { - let bytes = hex::decode(id).expect("Block ID should be hex"); - - let len = std::cmp::min(bytes.len(), 32); - let mut fixed_bytes = [0u8; 32]; - for (index, byte) in bytes.iter().take(32).enumerate() { - fixed_bytes[32 - len + index] = *byte; - } - Hash256::from(fixed_bytes) -} diff --git a/eth2/fork_choice/src/bitwise_lmd_ghost.rs b/eth2/fork_choice/src/bitwise_lmd_ghost.rs deleted file mode 100644 index 3ed57bf4dd..0000000000 --- a/eth2/fork_choice/src/bitwise_lmd_ghost.rs +++ /dev/null @@ -1,476 +0,0 @@ -//! The optimised bitwise LMD-GHOST fork choice rule. -use crate::{ForkChoice, ForkChoiceError}; -use bit_vec::BitVec; -use log::{debug, trace}; -use std::collections::HashMap; -use std::marker::PhantomData; -use std::sync::Arc; -use store::Store; -use types::{BeaconBlock, BeaconState, ChainSpec, EthSpec, Hash256, Slot, SlotHeight}; - -//TODO: Pruning - Children -//TODO: Handle Syncing - -// NOTE: This uses u32 to represent difference between block heights. Thus this is only -// applicable for block height differences in the range of a u32. -// This can potentially be parallelized in some parts. - -/// Compute the base-2 logarithm of an integer, floored (rounded down) -#[inline] -fn log2_int(x: u64) -> u32 { - if x == 0 { - return 0; - } - 63 - x.leading_zeros() -} - -fn power_of_2_below(x: u64) -> u64 { - 2u64.pow(log2_int(x)) -} - -/// Stores the necessary data structures to run the optimised bitwise lmd ghost algorithm. -pub struct BitwiseLMDGhost { - /// A cache of known ancestors at given heights for a specific block. - //TODO: Consider FnvHashMap - cache: HashMap, Hash256>, - /// Log lookup table for blocks to their ancestors. - //TODO: Verify we only want/need a size 16 log lookup - ancestors: Vec>, - /// Stores the children for any given parent. - children: HashMap>, - /// The latest attestation targets as a map of validator index to block hash. - //TODO: Could this be a fixed size vec - latest_attestation_targets: HashMap, - /// Block and state storage. - store: Arc, - max_known_height: SlotHeight, - _phantom: PhantomData, -} - -impl BitwiseLMDGhost { - /// Finds the latest votes weighted by validator balance. Returns a hashmap of block_hash to - /// weighted votes. - pub fn get_latest_votes( - &self, - state_root: &Hash256, - block_slot: Slot, - spec: &ChainSpec, - ) -> Result, ForkChoiceError> { - // get latest votes - // Note: Votes are weighted by min(balance, MAX_DEPOSIT_AMOUNT) // - // FORK_CHOICE_BALANCE_INCREMENT - // build a hashmap of block_hash to weighted votes - let mut latest_votes: HashMap = HashMap::new(); - // gets the current weighted votes - let current_state: BeaconState = self - .store - .get(&state_root)? - .ok_or_else(|| ForkChoiceError::MissingBeaconState(*state_root))?; - - let active_validator_indices = - current_state.get_active_validator_indices(block_slot.epoch(E::slots_per_epoch())); - - for index in active_validator_indices { - let balance = std::cmp::min(current_state.balances[index], spec.max_effective_balance) - / spec.effective_balance_increment; - if balance > 0 { - if let Some(target) = self.latest_attestation_targets.get(&(index as u64)) { - *latest_votes.entry(*target).or_insert_with(|| 0) += balance; - } - } - } - trace!("Latest votes: {:?}", latest_votes); - Ok(latest_votes) - } - - /// Gets the ancestor at a given height `at_height` of a block specified by `block_hash`. - fn get_ancestor( - &mut self, - block_hash: Hash256, - target_height: SlotHeight, - spec: &ChainSpec, - ) -> Option { - // return None if we can't get the block from the db. - let block_height = { - let block_slot = self - .store - .get::(&block_hash) - .ok()? - .expect("Should have returned already if None") - .slot; - - block_slot.height(spec.genesis_slot) - }; - - // verify we haven't exceeded the block height - if target_height >= block_height { - if target_height > block_height { - return None; - } else { - return Some(block_hash); - } - } - // check if the result is stored in our cache - let cache_key = CacheKey::new(&block_hash, target_height.as_u64()); - if let Some(ancestor) = self.cache.get(&cache_key) { - return Some(*ancestor); - } - - // not in the cache recursively search for ancestors using a log-lookup - if let Some(ancestor) = { - let ancestor_lookup = *self.ancestors - [log2_int((block_height - target_height - 1u64).as_u64()) as usize] - .get(&block_hash) - //TODO: Panic if we can't lookup and fork choice fails - .expect("All blocks should be added to the ancestor log lookup table"); - self.get_ancestor(ancestor_lookup, target_height, &spec) - } { - // add the result to the cache - self.cache.insert(cache_key, ancestor); - return Some(ancestor); - } - - None - } - - // looks for an obvious block winner given the latest votes for a specific height - fn get_clear_winner( - &mut self, - latest_votes: &HashMap, - block_height: SlotHeight, - spec: &ChainSpec, - ) -> Option { - // map of vote counts for every hash at this height - let mut current_votes: HashMap = HashMap::new(); - let mut total_vote_count = 0; - - trace!("Clear winner at block height: {}", block_height); - // loop through the latest votes and count all votes - // these have already been weighted by balance - for (hash, votes) in latest_votes.iter() { - if let Some(ancestor) = self.get_ancestor(*hash, block_height, spec) { - let current_vote_value = *current_votes.get(&ancestor).unwrap_or_else(|| &0); - current_votes.insert(ancestor, current_vote_value + *votes); - total_vote_count += votes; - } - } - // Check if there is a clear block winner at this height. If so return it. - for (hash, votes) in current_votes.iter() { - if *votes > total_vote_count / 2 { - // we have a clear winner, return it - return Some(*hash); - } - } - // didn't find a clear winner - None - } - - // Finds the best child, splitting children into a binary tree, based on their hashes (Bitwise - // LMD Ghost) - fn choose_best_child(&self, votes: &HashMap) -> Option { - if votes.is_empty() { - return None; - } - let mut bitmask: BitVec = BitVec::new(); - // loop through all bits - for bit in 0..=256 { - let mut zero_votes = 0; - let mut one_votes = 0; - let mut single_candidate = (None, false); - - trace!("Child vote length: {}", votes.len()); - for (candidate, votes) in votes.iter() { - let candidate_bit: BitVec = BitVec::from_bytes(candidate.as_bytes()); - - // if the bitmasks don't match, exclude candidate - if !bitmask.iter().eq(candidate_bit.iter().take(bit)) { - trace!( - "Child: {} was removed in bit: {} with the bitmask: {:?}", - candidate, - bit, - bitmask - ); - continue; - } - if candidate_bit.get(bit) == Some(false) { - zero_votes += votes; - } else { - one_votes += votes; - } - - if single_candidate.0.is_none() { - single_candidate.0 = Some(candidate); - single_candidate.1 = true; - } else { - single_candidate.1 = false; - } - } - bitmask.push(one_votes > zero_votes); - if single_candidate.1 { - return Some(*single_candidate.0.expect("Cannot reach this")); - } - } - // should never reach here - None - } -} - -impl ForkChoice for BitwiseLMDGhost { - fn new(store: Arc) -> Self { - BitwiseLMDGhost { - cache: HashMap::new(), - ancestors: vec![HashMap::new(); 16], - latest_attestation_targets: HashMap::new(), - children: HashMap::new(), - max_known_height: SlotHeight::new(0), - store, - _phantom: PhantomData, - } - } - - fn add_block( - &mut self, - block: &BeaconBlock, - block_hash: &Hash256, - spec: &ChainSpec, - ) -> Result<(), ForkChoiceError> { - // get the height of the parent - let parent_height = self - .store - .get::(&block.previous_block_root)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(block.previous_block_root))? - .slot - .height(spec.genesis_slot); - - let parent_hash = &block.previous_block_root; - - // add the new block to the children of parent - (*self - .children - .entry(block.previous_block_root) - .or_insert_with(|| vec![])) - .push(block_hash.clone()); - - // build the ancestor data structure - for index in 0..16 { - if parent_height % (1 << index) == 0 { - self.ancestors[index].insert(*block_hash, *parent_hash); - } else { - // TODO: This is unsafe. Will panic if parent_hash doesn't exist. Using it for debugging - let parent_ancestor = self.ancestors[index][parent_hash]; - self.ancestors[index].insert(*block_hash, parent_ancestor); - } - } - // update the max height - self.max_known_height = std::cmp::max(self.max_known_height, parent_height + 1); - Ok(()) - } - - fn add_attestation( - &mut self, - validator_index: u64, - target_block_root: &Hash256, - spec: &ChainSpec, - ) -> Result<(), ForkChoiceError> { - // simply add the attestation to the latest_attestation_target if the block_height is - // larger - trace!( - "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!("Old attestation found: {:?}", attestation_target); - // get the height of the target block - let block_height = self - .store - .get::(&target_block_root)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(*target_block_root))? - .slot - .height(spec.genesis_slot); - - // get the height of the past target block - let past_block_height = self - .store - .get::(&attestation_target)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(*attestation_target))? - .slot - .height(spec.genesis_slot); - // update the attestation only if the new target is higher - if past_block_height < block_height { - trace!("Updating old attestation"); - *attestation_target = *target_block_root; - } - } - Ok(()) - } - - /// Perform lmd_ghost on the current chain to find the head. - fn find_head( - &mut self, - justified_block_start: &Hash256, - spec: &ChainSpec, - ) -> Result { - debug!( - "Starting optimised fork choice at block: {}", - justified_block_start - ); - let block = self - .store - .get::(&justified_block_start)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(*justified_block_start))?; - - let block_slot = block.slot; - let state_root = block.state_root; - let mut block_height = block_slot.height(spec.genesis_slot); - - let mut current_head = *justified_block_start; - - let mut latest_votes = self.get_latest_votes(&state_root, block_slot, spec)?; - - // remove any votes that don't relate to our current head. - latest_votes - .retain(|hash, _| self.get_ancestor(*hash, block_height, spec) == Some(current_head)); - - // begin searching for the head - loop { - debug!( - "Iteration for block: {} with vote length: {}", - current_head, - latest_votes.len() - ); - // if there are no children, we are done, return the current_head - let children = match self.children.get(¤t_head) { - Some(children) => children.clone(), - None => { - debug!("Head found: {}", current_head); - return Ok(current_head); - } - }; - - // logarithmic lookup blocks to see if there are obvious winners, if so, - // progress to the next iteration. - let mut step = - power_of_2_below(self.max_known_height.saturating_sub(block_height).as_u64()) / 2; - while step > 0 { - trace!("Current Step: {}", step); - if let Some(clear_winner) = self.get_clear_winner( - &latest_votes, - block_height - (block_height % step) + step, - spec, - ) { - current_head = clear_winner; - break; - } - step /= 2; - } - if step > 0 { - trace!("Found clear winner: {}", current_head); - } - // 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!( - "Lookup failed, only one child, proceeding to child: {}", - current_head - ); - } - // we need to find the best child path to progress down. - else { - trace!("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 - if let Some(child) = self.get_ancestor(*voted_hash, block_height + 1, spec) { - // add up the votes for each child - *child_votes.entry(child).or_insert_with(|| 0) += vote; - } - } - // check if we have votes of children, if not select the smallest hash child - if child_votes.is_empty() { - current_head = *children - .iter() - .min_by(|child1, child2| child1.cmp(child2)) - .expect("Must be children here"); - trace!( - "Children have no votes - smallest hash chosen: {}", - current_head - ); - } else { - // given the votes on the children, find the best child - current_head = self - .choose_best_child(&child_votes) - .ok_or(ForkChoiceError::CannotFindBestChild)?; - trace!("Best child found: {}", current_head); - } - } - - // didn't find head yet, proceed to next iteration - // update block height - block_height = self - .store - .get::(¤t_head)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(current_head))? - .slot - .height(spec.genesis_slot); - // prune the latest votes for votes that are not part of current chosen chain - // more specifically, only keep votes that have head as an ancestor - for hash in latest_votes.keys() { - trace!( - "Ancestor for vote: {} at height: {} is: {:?}", - hash, - block_height, - self.get_ancestor(*hash, block_height, spec) - ); - } - latest_votes.retain(|hash, _| { - self.get_ancestor(*hash, block_height, spec) == Some(current_head) - }); - } - } -} - -/// Type for storing blocks in a memory cache. Key is comprised of block-hash plus the height. -#[derive(PartialEq, Eq, Hash)] -pub struct CacheKey { - block_hash: Hash256, - block_height: T, -} - -impl CacheKey { - pub fn new(block_hash: &Hash256, block_height: T) -> Self { - CacheKey { - block_hash: *block_hash, - block_height, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - pub fn test_power_of_2_below() { - assert_eq!(power_of_2_below(4), 4); - assert_eq!(power_of_2_below(5), 4); - assert_eq!(power_of_2_below(7), 4); - assert_eq!(power_of_2_below(24), 16); - assert_eq!(power_of_2_below(32), 32); - assert_eq!(power_of_2_below(33), 32); - assert_eq!(power_of_2_below(63), 32); - } - - #[test] - pub fn test_power_of_2_below_large() { - let pow: u64 = 1 << 24; - for x in (pow - 20)..(pow + 20) { - assert!(power_of_2_below(x) <= x, "{}", x); - } - } -} diff --git a/eth2/fork_choice/src/lib.rs b/eth2/fork_choice/src/lib.rs deleted file mode 100644 index f4a1fa5cb6..0000000000 --- a/eth2/fork_choice/src/lib.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! This crate stores the various implementations of fork-choice rules that can be used for the -//! beacon blockchain. -//! -//! There are three implementations. One is the naive longest chain rule (primarily for testing -//! purposes). The other two are proposed implementations of the LMD-GHOST fork-choice rule with various forms of optimisation. -//! -//! The current implementations are: -//! - [`longest-chain`]: Simplistic longest-chain fork choice - primarily for testing, **not for -//! production**. -//! - [`slow_lmd_ghost`]: This is a simple and very inefficient implementation given in the ethereum 2.0 -//! specifications (https://github.com/ethereum/eth2.0-specs/blob/v0.1/specs/core/0_beacon-chain.md#get_block_root). -//! - [`bitwise_lmd_ghost`]: This is an optimised version of bitwise LMD-GHOST as proposed -//! by Vitalik. The reference implementation can be found at: https://github.com/ethereum/research/blob/master/ghost/ghost.py -//! -//! [`longest-chain`]: struct.LongestChain.html -//! [`slow_lmd_ghost`]: struct.SlowLmdGhost.html -//! [`bitwise_lmd_ghost`]: struct.OptimisedLmdGhost.html - -pub mod bitwise_lmd_ghost; -pub mod longest_chain; -pub mod optimized_lmd_ghost; -pub mod slow_lmd_ghost; -pub mod test_utils; - -use std::sync::Arc; -use store::Error as DBError; -use types::{BeaconBlock, ChainSpec, Hash256}; - -pub use bitwise_lmd_ghost::BitwiseLMDGhost; -pub use longest_chain::LongestChain; -pub use optimized_lmd_ghost::OptimizedLMDGhost; -pub use slow_lmd_ghost::SlowLMDGhost; - -/// Defines the interface for Fork Choices. Each Fork choice will define their own data structures -/// which can be built in block processing through the `add_block` and `add_attestation` functions. -/// The main fork choice algorithm is specified in `find_head -pub trait ForkChoice: Send + Sync { - /// Create a new `ForkChoice` which reads from `store`. - fn new(store: Arc) -> Self; - - /// Called when a block has been added. Allows generic block-level data structures to be - /// built for a given fork-choice. - fn add_block( - &mut self, - block: &BeaconBlock, - block_hash: &Hash256, - spec: &ChainSpec, - ) -> Result<(), ForkChoiceError>; - /// Called when an attestation has been added. Allows generic attestation-level data structures to be built for a given fork choice. - // This can be generalised to a full attestation if required later. - fn add_attestation( - &mut self, - validator_index: u64, - target_block_hash: &Hash256, - spec: &ChainSpec, - ) -> Result<(), ForkChoiceError>; - /// The fork-choice algorithm to find the current canonical head of the chain. - // TODO: Remove the justified_start_block parameter and make it internal - fn find_head( - &mut self, - justified_start_block: &Hash256, - spec: &ChainSpec, - ) -> Result; -} - -/// Possible fork choice errors that can occur. -#[derive(Debug, PartialEq)] -pub enum ForkChoiceError { - MissingBeaconBlock(Hash256), - MissingBeaconState(Hash256), - IncorrectBeaconState(Hash256), - CannotFindBestChild, - ChildrenNotFound, - StorageError(String), - HeadNotFound, -} - -impl From for ForkChoiceError { - fn from(e: DBError) -> ForkChoiceError { - ForkChoiceError::StorageError(format!("{:?}", e)) - } -} - -/// Fork choice options that are currently implemented. -#[derive(Debug, Clone)] -pub enum ForkChoiceAlgorithm { - /// Chooses the longest chain becomes the head. Not for production. - LongestChain, - /// A simple and highly inefficient implementation of LMD ghost. - SlowLMDGhost, - /// An optimised version of bitwise LMD-GHOST by Vitalik. - BitwiseLMDGhost, - /// An optimised implementation of LMD ghost. - OptimizedLMDGhost, -} diff --git a/eth2/fork_choice/src/longest_chain.rs b/eth2/fork_choice/src/longest_chain.rs deleted file mode 100644 index 08e47cf393..0000000000 --- a/eth2/fork_choice/src/longest_chain.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::{ForkChoice, ForkChoiceError}; -use std::sync::Arc; -use store::Store; -use types::{BeaconBlock, ChainSpec, Hash256, Slot}; - -pub struct LongestChain { - /// List of head block hashes - head_block_hashes: Vec, - /// Block storage. - store: Arc, -} - -impl ForkChoice for LongestChain { - fn new(store: Arc) -> Self { - LongestChain { - head_block_hashes: Vec::new(), - store, - } - } - - fn add_block( - &mut self, - block: &BeaconBlock, - block_hash: &Hash256, - _: &ChainSpec, - ) -> Result<(), ForkChoiceError> { - // add the block hash to head_block_hashes removing the parent if it exists - self.head_block_hashes - .retain(|hash| *hash != block.previous_block_root); - self.head_block_hashes.push(*block_hash); - Ok(()) - } - - fn add_attestation( - &mut self, - _: u64, - _: &Hash256, - _: &ChainSpec, - ) -> Result<(), ForkChoiceError> { - // do nothing - Ok(()) - } - - fn find_head(&mut self, _: &Hash256, _: &ChainSpec) -> Result { - let mut head_blocks: Vec<(usize, BeaconBlock)> = vec![]; - /* - * Load all the head_block hashes from the DB as SszBeaconBlocks. - */ - for (index, block_hash) in self.head_block_hashes.iter().enumerate() { - let block: BeaconBlock = self - .store - .get(&block_hash)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(*block_hash))?; - head_blocks.push((index, block)); - } - - /* - * Loop through all the head blocks and find the highest slot. - */ - let highest_slot = head_blocks - .iter() - .fold(Slot::from(0u64), |highest, (_, block)| { - std::cmp::max(block.slot, highest) - }); - - // if we find no blocks, return Error - if highest_slot == 0 { - return Err(ForkChoiceError::HeadNotFound); - } - - /* - * Loop through all the highest blocks and sort them by highest hash. - * - * Ultimately, the index of the head_block hash with the highest slot and highest block - * hash will be the winner. - */ - - let head_index: Option = - head_blocks - .iter() - .fold(None, |smallest_index, (index, block)| { - if block.slot == highest_slot { - if smallest_index.is_none() { - return Some(*index); - } - return Some(std::cmp::min( - *index, - smallest_index.expect("Cannot be None"), - )); - } - smallest_index - }); - - if head_index.is_none() { - return Err(ForkChoiceError::HeadNotFound); - } - - Ok(self.head_block_hashes[head_index.unwrap()]) - } -} diff --git a/eth2/fork_choice/src/optimized_lmd_ghost.rs b/eth2/fork_choice/src/optimized_lmd_ghost.rs deleted file mode 100644 index 7a48c461e4..0000000000 --- a/eth2/fork_choice/src/optimized_lmd_ghost.rs +++ /dev/null @@ -1,447 +0,0 @@ -//! The optimised bitwise LMD-GHOST fork choice rule. -use crate::{ForkChoice, ForkChoiceError}; -use log::{debug, trace}; -use std::cmp::Ordering; -use std::collections::HashMap; -use std::marker::PhantomData; -use std::sync::Arc; -use store::Store; -use types::{BeaconBlock, BeaconState, ChainSpec, EthSpec, Hash256, Slot, SlotHeight}; - -//TODO: Pruning - Children -//TODO: Handle Syncing - -// NOTE: This uses u32 to represent difference between block heights. Thus this is only -// applicable for block height differences in the range of a u32. -// This can potentially be parallelized in some parts. - -/// Compute the base-2 logarithm of an integer, floored (rounded down) -#[inline] -fn log2_int(x: u64) -> u32 { - if x == 0 { - return 0; - } - 63 - x.leading_zeros() -} - -fn power_of_2_below(x: u64) -> u64 { - 2u64.pow(log2_int(x)) -} - -/// Stores the necessary data structures to run the optimised lmd ghost algorithm. -pub struct OptimizedLMDGhost { - /// A cache of known ancestors at given heights for a specific block. - //TODO: Consider FnvHashMap - cache: HashMap, Hash256>, - /// Log lookup table for blocks to their ancestors. - //TODO: Verify we only want/need a size 16 log lookup - ancestors: Vec>, - /// Stores the children for any given parent. - children: HashMap>, - /// The latest attestation targets as a map of validator index to block hash. - //TODO: Could this be a fixed size vec - latest_attestation_targets: HashMap, - /// Block and state storage. - store: Arc, - max_known_height: SlotHeight, - _phantom: PhantomData, -} - -impl OptimizedLMDGhost { - /// Finds the latest votes weighted by validator balance. Returns a hashmap of block_hash to - /// weighted votes. - pub fn get_latest_votes( - &self, - state_root: &Hash256, - block_slot: Slot, - spec: &ChainSpec, - ) -> Result, ForkChoiceError> { - // get latest votes - // Note: Votes are weighted by min(balance, MAX_DEPOSIT_AMOUNT) // - // FORK_CHOICE_BALANCE_INCREMENT - // build a hashmap of block_hash to weighted votes - let mut latest_votes: HashMap = HashMap::new(); - // gets the current weighted votes - let current_state: BeaconState = self - .store - .get(&state_root)? - .ok_or_else(|| ForkChoiceError::MissingBeaconState(*state_root))?; - - let active_validator_indices = - current_state.get_active_validator_indices(block_slot.epoch(E::slots_per_epoch())); - - for index in active_validator_indices { - let balance = std::cmp::min(current_state.balances[index], spec.max_effective_balance) - / spec.effective_balance_increment; - if balance > 0 { - if let Some(target) = self.latest_attestation_targets.get(&(index as u64)) { - *latest_votes.entry(*target).or_insert_with(|| 0) += balance; - } - } - } - trace!("Latest votes: {:?}", latest_votes); - Ok(latest_votes) - } - - /// Gets the ancestor at a given height `at_height` of a block specified by `block_hash`. - fn get_ancestor( - &mut self, - block_hash: Hash256, - target_height: SlotHeight, - spec: &ChainSpec, - ) -> Option { - // return None if we can't get the block from the db. - let block_height = { - let block_slot = self - .store - .get::(&block_hash) - .ok()? - .expect("Should have returned already if None") - .slot; - - block_slot.height(spec.genesis_slot) - }; - - // verify we haven't exceeded the block height - if target_height >= block_height { - if target_height > block_height { - return None; - } else { - return Some(block_hash); - } - } - // check if the result is stored in our cache - let cache_key = CacheKey::new(&block_hash, target_height.as_u64()); - if let Some(ancestor) = self.cache.get(&cache_key) { - return Some(*ancestor); - } - - // not in the cache recursively search for ancestors using a log-lookup - if let Some(ancestor) = { - let ancestor_lookup = *self.ancestors - [log2_int((block_height - target_height - 1u64).as_u64()) as usize] - .get(&block_hash) - //TODO: Panic if we can't lookup and fork choice fails - .expect("All blocks should be added to the ancestor log lookup table"); - self.get_ancestor(ancestor_lookup, target_height, &spec) - } { - // add the result to the cache - self.cache.insert(cache_key, ancestor); - return Some(ancestor); - } - - None - } - - // looks for an obvious block winner given the latest votes for a specific height - fn get_clear_winner( - &mut self, - latest_votes: &HashMap, - block_height: SlotHeight, - spec: &ChainSpec, - ) -> Option { - // map of vote counts for every hash at this height - let mut current_votes: HashMap = HashMap::new(); - let mut total_vote_count = 0; - - trace!("Clear winner at block height: {}", block_height); - // loop through the latest votes and count all votes - // these have already been weighted by balance - for (hash, votes) in latest_votes.iter() { - if let Some(ancestor) = self.get_ancestor(*hash, block_height, spec) { - let current_vote_value = *current_votes.get(&ancestor).unwrap_or_else(|| &0); - current_votes.insert(ancestor, current_vote_value + *votes); - total_vote_count += votes; - } - } - // Check if there is a clear block winner at this height. If so return it. - for (hash, votes) in current_votes.iter() { - if *votes > total_vote_count / 2 { - // we have a clear winner, return it - return Some(*hash); - } - } - // didn't find a clear winner - None - } - - // Finds the best child (one with highest votes) - fn choose_best_child(&self, votes: &HashMap) -> Option { - if votes.is_empty() { - return None; - } - - // Iterate through hashmap to get child with maximum votes - let best_child = votes.iter().max_by(|(child1, v1), (child2, v2)| { - let mut result = v1.cmp(v2); - // If votes are equal, choose smaller hash to break ties deterministically - if result == Ordering::Equal { - // Reverse so that max_by chooses smaller hash - result = child1.cmp(child2).reverse(); - } - result - }); - - Some(*best_child.unwrap().0) - } -} - -impl ForkChoice for OptimizedLMDGhost { - fn new(store: Arc) -> Self { - OptimizedLMDGhost { - cache: HashMap::new(), - ancestors: vec![HashMap::new(); 16], - latest_attestation_targets: HashMap::new(), - children: HashMap::new(), - max_known_height: SlotHeight::new(0), - store, - _phantom: PhantomData, - } - } - - fn add_block( - &mut self, - block: &BeaconBlock, - block_hash: &Hash256, - spec: &ChainSpec, - ) -> Result<(), ForkChoiceError> { - // get the height of the parent - let parent_height = self - .store - .get::(&block.previous_block_root)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(block.previous_block_root))? - .slot - .height(spec.genesis_slot); - - let parent_hash = &block.previous_block_root; - - // add the new block to the children of parent - (*self - .children - .entry(block.previous_block_root) - .or_insert_with(|| vec![])) - .push(block_hash.clone()); - - // build the ancestor data structure - for index in 0..16 { - if parent_height % (1 << index) == 0 { - self.ancestors[index].insert(*block_hash, *parent_hash); - } else { - // TODO: This is unsafe. Will panic if parent_hash doesn't exist. Using it for debugging - let parent_ancestor = self.ancestors[index][parent_hash]; - self.ancestors[index].insert(*block_hash, parent_ancestor); - } - } - // update the max height - self.max_known_height = std::cmp::max(self.max_known_height, parent_height + 1); - Ok(()) - } - - fn add_attestation( - &mut self, - validator_index: u64, - target_block_root: &Hash256, - spec: &ChainSpec, - ) -> Result<(), ForkChoiceError> { - // simply add the attestation to the latest_attestation_target if the block_height is - // larger - trace!( - "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!("Old attestation found: {:?}", attestation_target); - // get the height of the target block - let block_height = self - .store - .get::(&target_block_root)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(*target_block_root))? - .slot - .height(spec.genesis_slot); - - // get the height of the past target block - let past_block_height = self - .store - .get::(&attestation_target)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(*attestation_target))? - .slot - .height(spec.genesis_slot); - // update the attestation only if the new target is higher - if past_block_height < block_height { - trace!("Updating old attestation"); - *attestation_target = *target_block_root; - } - } - Ok(()) - } - - /// Perform lmd_ghost on the current chain to find the head. - fn find_head( - &mut self, - justified_block_start: &Hash256, - spec: &ChainSpec, - ) -> Result { - debug!( - "Starting optimised fork choice at block: {}", - justified_block_start - ); - let block = self - .store - .get::(&justified_block_start)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(*justified_block_start))?; - - let block_slot = block.slot; - let state_root = block.state_root; - let mut block_height = block_slot.height(spec.genesis_slot); - - let mut current_head = *justified_block_start; - - let mut latest_votes = self.get_latest_votes(&state_root, block_slot, spec)?; - - // remove any votes that don't relate to our current head. - latest_votes - .retain(|hash, _| self.get_ancestor(*hash, block_height, spec) == Some(current_head)); - - // begin searching for the head - loop { - debug!( - "Iteration for block: {} with vote length: {}", - current_head, - latest_votes.len() - ); - // if there are no children, we are done, return the current_head - let children = match self.children.get(¤t_head) { - Some(children) => children.clone(), - None => { - debug!("Head found: {}", current_head); - return Ok(current_head); - } - }; - - // logarithmic lookup blocks to see if there are obvious winners, if so, - // progress to the next iteration. - let mut step = - power_of_2_below(self.max_known_height.saturating_sub(block_height).as_u64()) / 2; - while step > 0 { - trace!("Current Step: {}", step); - if let Some(clear_winner) = self.get_clear_winner( - &latest_votes, - block_height - (block_height % step) + step, - spec, - ) { - current_head = clear_winner; - break; - } - step /= 2; - } - if step > 0 { - trace!("Found clear winner: {}", current_head); - } - // 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!( - "Lookup failed, only one child, proceeding to child: {}", - current_head - ); - } - // we need to find the best child path to progress down. - else { - trace!("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 - if let Some(child) = self.get_ancestor(*voted_hash, block_height + 1, spec) { - // add up the votes for each child - *child_votes.entry(child).or_insert_with(|| 0) += vote; - } - } - // check if we have votes of children, if not select the smallest hash child - if child_votes.is_empty() { - current_head = *children - .iter() - .min_by(|child1, child2| child1.cmp(child2)) - .expect("Must be children here"); - trace!( - "Children have no votes - smallest hash chosen: {}", - current_head - ); - } else { - // given the votes on the children, find the best child - current_head = self - .choose_best_child(&child_votes) - .ok_or(ForkChoiceError::CannotFindBestChild)?; - trace!("Best child found: {}", current_head); - } - } - - // didn't find head yet, proceed to next iteration - // update block height - block_height = self - .store - .get::(¤t_head)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(current_head))? - .slot - .height(spec.genesis_slot); - // prune the latest votes for votes that are not part of current chosen chain - // more specifically, only keep votes that have head as an ancestor - for hash in latest_votes.keys() { - trace!( - "Ancestor for vote: {} at height: {} is: {:?}", - hash, - block_height, - self.get_ancestor(*hash, block_height, spec) - ); - } - latest_votes.retain(|hash, _| { - self.get_ancestor(*hash, block_height, spec) == Some(current_head) - }); - } - } -} - -/// Type for storing blocks in a memory cache. Key is comprised of block-hash plus the height. -#[derive(PartialEq, Eq, Hash)] -pub struct CacheKey { - block_hash: Hash256, - block_height: T, -} - -impl CacheKey { - pub fn new(block_hash: &Hash256, block_height: T) -> Self { - CacheKey { - block_hash: *block_hash, - block_height, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - pub fn test_power_of_2_below() { - assert_eq!(power_of_2_below(4), 4); - assert_eq!(power_of_2_below(5), 4); - assert_eq!(power_of_2_below(7), 4); - assert_eq!(power_of_2_below(24), 16); - assert_eq!(power_of_2_below(32), 32); - assert_eq!(power_of_2_below(33), 32); - assert_eq!(power_of_2_below(63), 32); - } - - #[test] - pub fn test_power_of_2_below_large() { - let pow: u64 = 1 << 24; - for x in (pow - 20)..(pow + 20) { - assert!(power_of_2_below(x) <= x, "{}", x); - } - } -} diff --git a/eth2/fork_choice/src/slow_lmd_ghost.rs b/eth2/fork_choice/src/slow_lmd_ghost.rs deleted file mode 100644 index 9b7a204002..0000000000 --- a/eth2/fork_choice/src/slow_lmd_ghost.rs +++ /dev/null @@ -1,212 +0,0 @@ -use crate::{ForkChoice, ForkChoiceError}; -use log::{debug, trace}; -use std::collections::HashMap; -use std::marker::PhantomData; -use std::sync::Arc; -use store::Store; -use types::{BeaconBlock, BeaconState, ChainSpec, EthSpec, Hash256, Slot}; - -//TODO: Pruning and syncing - -pub struct SlowLMDGhost { - /// The latest attestation targets as a map of validator index to block hash. - //TODO: Could this be a fixed size vec - latest_attestation_targets: HashMap, - /// Stores the children for any given parent. - children: HashMap>, - /// Block and state storage. - store: Arc, - _phantom: PhantomData, -} - -impl SlowLMDGhost { - /// Finds the latest votes weighted by validator balance. Returns a hashmap of block_hash to - /// weighted votes. - pub fn get_latest_votes( - &self, - state_root: &Hash256, - block_slot: Slot, - spec: &ChainSpec, - ) -> Result, ForkChoiceError> { - // get latest votes - // Note: Votes are weighted by min(balance, MAX_DEPOSIT_AMOUNT) // - // FORK_CHOICE_BALANCE_INCREMENT - // build a hashmap of block_hash to weighted votes - let mut latest_votes: HashMap = HashMap::new(); - // gets the current weighted votes - let current_state: BeaconState = self - .store - .get(state_root)? - .ok_or_else(|| ForkChoiceError::MissingBeaconState(*state_root))?; - - let active_validator_indices = - current_state.get_active_validator_indices(block_slot.epoch(E::slots_per_epoch())); - - for index in active_validator_indices { - let balance = std::cmp::min(current_state.balances[index], spec.max_effective_balance) - / spec.effective_balance_increment; - if balance > 0 { - if let Some(target) = self.latest_attestation_targets.get(&(index as u64)) { - *latest_votes.entry(*target).or_insert_with(|| 0) += balance; - } - } - } - trace!("Latest votes: {:?}", latest_votes); - Ok(latest_votes) - } - - /// Get the total number of votes for some given block root. - /// - /// The vote count is incremented each time an attestation target votes for a block root. - fn get_vote_count( - &self, - latest_votes: &HashMap, - block_root: &Hash256, - ) -> Result { - let mut count = 0; - let block_slot = self - .store - .get::(&block_root)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(*block_root))? - .slot; - - for (vote_hash, votes) in latest_votes.iter() { - let (root_at_slot, _) = self - .store - .get_block_at_preceeding_slot(*vote_hash, block_slot)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(*block_root))?; - if root_at_slot == *block_root { - count += votes; - } - } - Ok(count) - } -} - -impl ForkChoice for SlowLMDGhost { - fn new(store: Arc) -> Self { - SlowLMDGhost { - latest_attestation_targets: HashMap::new(), - children: HashMap::new(), - store, - _phantom: PhantomData, - } - } - - /// Process when a block is added - fn add_block( - &mut self, - block: &BeaconBlock, - block_hash: &Hash256, - _: &ChainSpec, - ) -> Result<(), ForkChoiceError> { - // build the children hashmap - // add the new block to the children of parent - (*self - .children - .entry(block.previous_block_root) - .or_insert_with(|| vec![])) - .push(block_hash.clone()); - - // complete - Ok(()) - } - - fn add_attestation( - &mut self, - validator_index: u64, - target_block_root: &Hash256, - spec: &ChainSpec, - ) -> Result<(), ForkChoiceError> { - // simply add the attestation to the latest_attestation_target if the block_height is - // larger - trace!( - "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!("Old attestation found: {:?}", attestation_target); - // get the height of the target block - let block_height = self - .store - .get::(&target_block_root)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(*target_block_root))? - .slot - .height(spec.genesis_slot); - - // get the height of the past target block - let past_block_height = self - .store - .get::(&attestation_target)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(*attestation_target))? - .slot - .height(spec.genesis_slot); - // update the attestation only if the new target is higher - if past_block_height < block_height { - trace!("Updating old attestation"); - *attestation_target = *target_block_root; - } - } - Ok(()) - } - - /// A very inefficient implementation of LMD ghost. - fn find_head( - &mut self, - justified_block_start: &Hash256, - spec: &ChainSpec, - ) -> Result { - debug!("Running LMD Ghost Fork-choice rule"); - let start = self - .store - .get::(&justified_block_start)? - .ok_or_else(|| ForkChoiceError::MissingBeaconBlock(*justified_block_start))?; - - let start_state_root = start.state_root; - - let latest_votes = self.get_latest_votes(&start_state_root, start.slot, spec)?; - - let mut head_hash = *justified_block_start; - - loop { - debug!("Iteration for block: {}", head_hash); - - let children = match self.children.get(&head_hash) { - Some(children) => children, - // we have found the head, exit - None => break, - }; - - // if we only have one child, use it - if children.len() == 1 { - trace!("Single child found."); - head_hash = children[0]; - continue; - } - trace!("Children found: {:?}", children); - - let mut head_vote_count = 0; - head_hash = children[0]; - for child_hash in children { - let vote_count = self.get_vote_count(&latest_votes, &child_hash)?; - trace!("Vote count for child: {} is: {}", child_hash, vote_count); - - if vote_count > head_vote_count { - head_hash = *child_hash; - head_vote_count = vote_count; - } - // resolve ties - choose smaller hash - else if vote_count == head_vote_count && *child_hash < head_hash { - head_hash = *child_hash; - } - } - } - Ok(head_hash) - } -} diff --git a/eth2/fork_choice/src/test_utils.rs b/eth2/fork_choice/src/test_utils.rs deleted file mode 100644 index 8ef20108aa..0000000000 --- a/eth2/fork_choice/src/test_utils.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::ForkChoice; -use std::marker::PhantomData; -use std::sync::Arc; -use store::Store; -use types::{ - test_utils::{SeedableRng, TestRandom, TestingBeaconStateBuilder, XorShiftRng}, - BeaconBlock, BeaconState, EthSpec, Hash256, Keypair, MainnetEthSpec, -}; - -/// Creates a chain of blocks and produces `ForkChoice` instances with pre-filled stores. -pub struct TestingForkChoiceBuilder { - store: Arc, - pub chain: Vec<(Hash256, BeaconBlock)>, - _phantom: PhantomData, -} - -impl TestingForkChoiceBuilder { - pub fn new(validator_count: usize, chain_length: usize, store: Arc) -> Self { - let chain = - get_chain_of_blocks::(chain_length, validator_count, store.clone()); - - Self { - store, - chain, - _phantom: PhantomData, - } - } - - pub fn genesis_root(&self) -> Hash256 { - self.chain[0].0 - } - - /// Return a new `ForkChoice` instance with a chain stored in it's `Store`. - pub fn build>(&self) -> F { - F::new(self.store.clone()) - } -} - -fn get_state(validator_count: usize) -> BeaconState { - let spec = T::default_spec(); - - let builder: TestingBeaconStateBuilder = - TestingBeaconStateBuilder::from_single_keypair(validator_count, &Keypair::random(), &spec); - let (state, _keypairs) = builder.build(); - state -} - -/// Generates a chain of blocks of length `len`. -/// -/// Creates a `BeaconState` for the block and stores it in `Store`, along with the block. -/// -/// Returns the chain of blocks. -fn get_chain_of_blocks( - len: usize, - validator_count: usize, - store: Arc, -) -> Vec<(Hash256, BeaconBlock)> { - let spec = T::default_spec(); - let mut blocks_and_roots: Vec<(Hash256, BeaconBlock)> = vec![]; - let mut unique_hashes = (0..).map(Hash256::from); - let mut random_block = BeaconBlock::random_for_test(&mut XorShiftRng::from_seed([42; 16])); - random_block.previous_block_root = Hash256::zero(); - let beacon_state = get_state::(validator_count); - - for i in 0..len { - let slot = spec.genesis_slot + i as u64; - - // Generate and store the state. - let mut state = beacon_state.clone(); - state.slot = slot; - let state_root = unique_hashes.next().unwrap(); - store.put(&state_root, &state).unwrap(); - - // Generate the block. - let mut block = random_block.clone(); - block.slot = slot; - block.state_root = state_root; - - // Chain all the blocks to their parents. - if i > 0 { - block.previous_block_root = blocks_and_roots[i - 1].0; - } - - // Store the block. - let block_root = unique_hashes.next().unwrap(); - store.put(&block_root, &block).unwrap(); - blocks_and_roots.push((block_root, block)); - } - - blocks_and_roots -} diff --git a/eth2/fork_choice/tests/bitwise_lmd_ghost_test_vectors.yaml b/eth2/fork_choice/tests/bitwise_lmd_ghost_test_vectors.yaml deleted file mode 100644 index 61b0b05c40..0000000000 --- a/eth2/fork_choice/tests/bitwise_lmd_ghost_test_vectors.yaml +++ /dev/null @@ -1,144 +0,0 @@ -title: Fork-choice Tests -summary: A collection of abstract fork-choice tests for bitwise lmd ghost. -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' -# bitwise LMD ghost example. bitwise GHOST gives b2 -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - - id: 'b3' - parent: 'b0' - weights: - - b1: 5 - - b2: 4 - - b3: 3 - heads: - - id: 'b2' -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - - id: 'b3' - parent: 'b1' - - id: 'b4' - parent: 'b1' - - id: 'b5' - parent: 'b1' - - id: 'b6' - parent: 'b2' - - id: 'b7' - parent: 'b6' - weights: - - b0: 0 - - b1: 3 - - b2: 2 - - b3: 1 - - b4: 1 - - b5: 1 - - b6: 2 - - b7: 2 - heads: - - id: 'b4' -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - - id: 'b3' - parent: 'b0' - - id: 'b4' - parent: 'b1' - - id: 'b5' - parent: 'b1' - - id: 'b6' - parent: 'b2' - - id: 'b7' - parent: 'b2' - - id: 'b8' - parent: 'b3' - - id: 'b9' - parent: 'b3' - weights: - - b1: 2 - - b2: 1 - - b3: 1 - - b4: 7 - - b5: 5 - - b6: 2 - - b7: 4 - - b8: 4 - - b9: 2 - heads: - - id: 'b4' -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - - id: 'b3' - parent: 'b0' - - id: 'b4' - parent: 'b1' - - id: 'b5' - parent: 'b1' - - id: 'b6' - parent: 'b2' - - id: 'b7' - parent: 'b2' - - id: 'b8' - parent: 'b3' - - id: 'b9' - parent: 'b3' - weights: - - b1: 1 - - b2: 1 - - b3: 1 - - b4: 7 - - b5: 5 - - b6: 2 - - b7: 4 - - b8: 4 - - b9: 2 - heads: - - id: 'b7' -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - weights: - - b1: 0 - - b2: 0 - heads: - - id: 'b1' - diff --git a/eth2/fork_choice/tests/lmd_ghost_test_vectors.yaml b/eth2/fork_choice/tests/lmd_ghost_test_vectors.yaml deleted file mode 100644 index e7847de11a..0000000000 --- a/eth2/fork_choice/tests/lmd_ghost_test_vectors.yaml +++ /dev/null @@ -1,65 +0,0 @@ -title: Fork-choice Tests -summary: A collection of abstract fork-choice tests for lmd ghost. -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' -# bitwise LMD ghost example. GHOST gives b1 -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - - id: 'b3' - parent: 'b0' - weights: - - b1: 5 - - b2: 4 - - b3: 3 - heads: - - id: 'b1' -# equal weights children. Should choose lower hash b2 -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - - id: 'b3' - parent: 'b0' - weights: - - b1: 5 - - b2: 6 - - b3: 6 - heads: - - id: 'b2' -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b0' - weights: - - b1: 0 - - b2: 0 - heads: - - id: 'b1' diff --git a/eth2/fork_choice/tests/longest_chain_test_vectors.yaml b/eth2/fork_choice/tests/longest_chain_test_vectors.yaml deleted file mode 100644 index e1cd61f06a..0000000000 --- a/eth2/fork_choice/tests/longest_chain_test_vectors.yaml +++ /dev/null @@ -1,51 +0,0 @@ -title: Fork-choice Tests -summary: A collection of abstract fork-choice tests to verify the longest chain fork-choice rule. -test_suite: Fork-Choice - -test_cases: -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b1' - - id: 'b3' - parent: 'b1' - - id: 'b4' - parent: 'b3' - weights: - - b0: 0 - - b1: 0 - - b2: 10 - - b3: 1 - heads: - - id: 'b4' -- blocks: - - id: 'b0' - parent: 'b0' - - id: 'b1' - parent: 'b0' - - id: 'b2' - parent: 'b1' - - id: 'b3' - parent: 'b2' - - id: 'b4' - parent: 'b3' - - id: 'b5' - parent: 'b0' - - id: 'b6' - parent: 'b5' - - id: 'b7' - parent: 'b6' - - id: 'b8' - parent: 'b7' - - id: 'b9' - parent: 'b8' - weights: - - b0: 5 - - b1: 20 - - b2: 10 - - b3: 10 - heads: - - id: 'b9' diff --git a/eth2/fork_choice/tests/tests.rs b/eth2/fork_choice/tests/tests.rs deleted file mode 100644 index 4fa266d306..0000000000 --- a/eth2/fork_choice/tests/tests.rs +++ /dev/null @@ -1,231 +0,0 @@ -#![cfg(not(debug_assertions))] -/// Tests the available fork-choice algorithms -pub use beacon_chain::BeaconChain; -use bls::Signature; -use store::MemoryStore; -use store::Store; -// use env_logger::{Builder, Env}; -use fork_choice::{BitwiseLMDGhost, ForkChoice, LongestChain, OptimizedLMDGhost, SlowLMDGhost}; -use std::collections::HashMap; -use std::sync::Arc; -use std::{fs::File, io::prelude::*, path::PathBuf}; -use types::test_utils::TestingBeaconStateBuilder; -use types::{ - BeaconBlock, BeaconBlockBody, Eth1Data, EthSpec, Hash256, Keypair, MainnetEthSpec, Slot, -}; -use yaml_rust::yaml; - -// Note: We Assume the block Id's are hex-encoded. - -#[test] -fn test_optimized_lmd_ghost() { - // set up logging - // Builder::from_env(Env::default().default_filter_or("trace")).init(); - - test_yaml_vectors::>( - "tests/lmd_ghost_test_vectors.yaml", - 100, - ); -} - -#[test] -fn test_bitwise_lmd_ghost() { - // set up logging - //Builder::from_env(Env::default().default_filter_or("trace")).init(); - - test_yaml_vectors::>( - "tests/bitwise_lmd_ghost_test_vectors.yaml", - 100, - ); -} - -#[test] -fn test_slow_lmd_ghost() { - test_yaml_vectors::>( - "tests/lmd_ghost_test_vectors.yaml", - 100, - ); -} - -#[test] -fn test_longest_chain() { - test_yaml_vectors::>("tests/longest_chain_test_vectors.yaml", 100); -} - -// run a generic test over given YAML test vectors -fn test_yaml_vectors>( - yaml_file_path: &str, - emulated_validators: usize, // the number of validators used to give weights. -) { - // load test cases from yaml - let test_cases = load_test_cases_from_yaml(yaml_file_path); - - // default vars - let spec = MainnetEthSpec::default_spec(); - let zero_hash = Hash256::zero(); - let eth1_data = Eth1Data { - deposit_count: 0, - deposit_root: zero_hash.clone(), - block_hash: zero_hash.clone(), - }; - let randao_reveal = Signature::empty_signature(); - let signature = Signature::empty_signature(); - let body = BeaconBlockBody { - eth1_data, - randao_reveal, - graffiti: [0; 32], - proposer_slashings: vec![], - attester_slashings: vec![], - attestations: vec![], - deposits: vec![], - voluntary_exits: vec![], - transfers: vec![], - }; - - // process the tests - for test_case in test_cases { - // setup a fresh test - let (mut fork_choice, store, state_root) = setup_inital_state::(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(); - // assume the block tree is given to us in order. - let mut genesis_hash = None; - 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().to_string(); - - // default params for genesis - let block_hash = id_to_hash(&block_id); - let mut slot = spec.genesis_slot; - let previous_block_root = id_to_hash(&parent_id); - - // set the slot and parent based off the YAML. Start with genesis; - // if not the genesis, update slot - if parent_id != block_id { - // find parent slot - slot = *(block_slot - .get(&previous_block_root) - .expect("Parent should have a slot number")) - + 1; - } else { - genesis_hash = Some(block_hash); - } - - // update slot mapping - block_slot.insert(block_hash, slot); - - // build the BeaconBlock - let beacon_block = BeaconBlock { - slot, - previous_block_root, - state_root: state_root.clone(), - signature: signature.clone(), - body: body.clone(), - }; - - // Store the block. - store.put(&block_hash, &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, &spec) - .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 = id_to_hash(&id.to_string()); - 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 <= emulated_validators, - "Not enough validators to emulate weights" - ); - fork_choice - .add_attestation(current_validator as u64, &block_root, &spec) - .unwrap(); - current_validator += 1; - } - } - } - - // everything is set up, run the fork choice, using genesis as the head - let head = fork_choice - .find_head(&genesis_hash.unwrap(), &spec) - .unwrap(); - - // compare the result to the expected test - let success = test_case["heads"] - .clone() - .into_vec() - .unwrap() - .iter() - .find(|heads| id_to_hash(&heads["id"].as_str().unwrap().to_string()) == head) - .is_some(); - - println!("Head found: {}", head); - assert!(success, "Did not find one of the possible heads"); - } -} - -// loads the test_cases from the supplied yaml file -fn load_test_cases_from_yaml(file_path: &str) -> Vec { - // load the yaml - let mut file = { - let mut file_path_buf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - file_path_buf.push(file_path); - 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]; - doc["test_cases"].as_vec().unwrap().clone() -} - -fn setup_inital_state( - // fork_choice_algo: &ForkChoiceAlgorithm, - num_validators: usize, -) -> (T, Arc, Hash256) -where - T: ForkChoice, -{ - let store = Arc::new(MemoryStore::open()); - - let fork_choice = ForkChoice::new(store.clone()); - let spec = MainnetEthSpec::default_spec(); - - let mut state_builder: TestingBeaconStateBuilder = - TestingBeaconStateBuilder::from_single_keypair(num_validators, &Keypair::random(), &spec); - state_builder.build_caches(&spec).unwrap(); - let (state, _keypairs) = state_builder.build(); - - let state_root = state.canonical_root(); - store.put(&state_root, &state).unwrap(); - - // return initialised vars - (fork_choice, store, state_root) -} - -// convert a block_id into a Hash256 -- assume input is hex encoded; -fn id_to_hash(id: &String) -> Hash256 { - let bytes = hex::decode(id).expect("Block ID should be hex"); - - let len = std::cmp::min(bytes.len(), 32); - let mut fixed_bytes = [0u8; 32]; - for (index, byte) in bytes.iter().take(32).enumerate() { - fixed_bytes[32 - len + index] = *byte; - } - Hash256::from(fixed_bytes) -} From 7115961d848eb0be8f21699b127fd1180509412d Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 24 Jun 2019 07:25:01 +1000 Subject: [PATCH 37/40] Ensure current epoch committee is built in test --- tests/ef_tests/src/cases/sanity_blocks.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/ef_tests/src/cases/sanity_blocks.rs b/tests/ef_tests/src/cases/sanity_blocks.rs index 91824f48d9..bbd4abbad1 100644 --- a/tests/ef_tests/src/cases/sanity_blocks.rs +++ b/tests/ef_tests/src/cases/sanity_blocks.rs @@ -3,7 +3,7 @@ use crate::bls_setting::BlsSetting; use crate::case_result::compare_beacon_state_results_without_caches; use serde_derive::Deserialize; use state_processing::{per_block_processing, per_slot_processing}; -use types::{BeaconBlock, BeaconState, EthSpec}; +use types::{BeaconBlock, BeaconState, EthSpec, RelativeEpoch}; #[derive(Debug, Clone, Deserialize)] pub struct SanityBlocks { @@ -54,6 +54,11 @@ impl Case for SanityBlocks { while state.slot < block.slot { per_slot_processing(&mut state, spec).unwrap(); } + + state + .build_committee_cache(RelativeEpoch::Current, spec) + .unwrap(); + per_block_processing(&mut state, block, spec) }) .map(|_| state); From 8e13237b7f11bca102a4e1150fd9f0aaa3435aeb Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 24 Jun 2019 07:45:34 +1000 Subject: [PATCH 38/40] Only perform beacon chain tests on release --- beacon_node/beacon_chain/tests/tests.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index 5181f1e768..42b6cb6a21 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -1,3 +1,5 @@ +#![cfg(not(debug_assertions))] + use beacon_chain::test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy}; use lmd_ghost::ThreadSafeReducedTree; use store::MemoryStore; From 3a196f3fdc6c51e3e66d4454f092d9eed5a5d705 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 24 Jun 2019 09:22:40 +1000 Subject: [PATCH 39/40] Tidy, fix clippy lints --- beacon_node/store/src/iter.rs | 11 ------- eth2/lmd_ghost/src/reduced_tree.rs | 47 ++++++++++++------------------ 2 files changed, 18 insertions(+), 40 deletions(-) diff --git a/beacon_node/store/src/iter.rs b/beacon_node/store/src/iter.rs index e8b9a27e97..cf50d671bf 100644 --- a/beacon_node/store/src/iter.rs +++ b/beacon_node/store/src/iter.rs @@ -240,17 +240,6 @@ mod test { .expect(&format!("should set state_b slot {}", slot)); } - /* - for root in &mut state_a.latest_state_roots[..] { - state_a.set_state_root(slots.next().unwrap(), hashes.next().unwrap()); - // *root = hashes.next().unwrap() - } - for root in &mut state_b.latest_state_roots[..] { - state_b.set_state_root(slots.next().unwrap(), hashes.next().unwrap()); - *root = hashes.next().unwrap() - } - */ - let state_a_root = Hash256::from(slots_per_historical_root as u64); let state_b_root = Hash256::from(slots_per_historical_root as u64 * 2); diff --git a/eth2/lmd_ghost/src/reduced_tree.rs b/eth2/lmd_ghost/src/reduced_tree.rs index 82547bbf6b..49e9000765 100644 --- a/eth2/lmd_ghost/src/reduced_tree.rs +++ b/eth2/lmd_ghost/src/reduced_tree.rs @@ -173,20 +173,14 @@ where ) -> Result<()> { if slot >= self.root_slot() { if let Some(previous_vote) = self.latest_votes.get(validator_index) { - if previous_vote.slot > slot { - // Given vote is earier than known vote, nothing to do. - return Ok(()); - } else if previous_vote.slot == slot && previous_vote.hash == block_hash { - // Given vote is identical to known vote, nothing to do. - return Ok(()); - } else if previous_vote.slot == slot && previous_vote.hash != block_hash { - // Vote is an equivocation (double-vote), ignore it. - // - // TODO: this is slashable. - return Ok(()); - } else { - // Given vote is newer or different to current vote, replace the current vote. + // Note: it is possible to do a cheap equivocation check here: + // + // slashable = (previous_vote.slot == slot) && (previous_vote.hash != block_hash) + + if previous_vote.slot < slot { self.remove_latest_message(validator_index)?; + } else { + return Ok(()); } } @@ -292,11 +286,8 @@ where let node = self.get_node(vote.hash)?.clone(); if let Some(parent_hash) = node.parent_hash { - if node.has_votes() { - // A node with votes is never removed. - false - } else if node.children.len() > 1 { - // A node with more than one child is never removed. + if node.has_votes() || node.children.len() > 1 { + // A node with votes or more than one child is never removed. false } else if node.children.len() == 1 { // A node which has only one child may be removed. @@ -313,7 +304,7 @@ where } true - } else if node.children.len() == 0 { + } else if node.children.is_empty() { // A node which has no children may be deleted and potentially it's parent // too. self.maybe_delete_node(parent_hash)?; @@ -394,18 +385,16 @@ where } fn add_weightless_node(&mut self, slot: Slot, hash: Hash256) -> Result<()> { - if slot >= self.root_slot() { - if !self.nodes.contains_key(&hash) { - let node = Node { - block_hash: hash, - ..Node::default() - }; + if slot >= self.root_slot() && !self.nodes.contains_key(&hash) { + let node = Node { + block_hash: hash, + ..Node::default() + }; - self.add_node(node)?; + self.add_node(node)?; - if let Some(parent_hash) = self.get_node(hash)?.parent_hash { - self.maybe_delete_node(parent_hash)?; - } + if let Some(parent_hash) = self.get_node(hash)?.parent_hash { + self.maybe_delete_node(parent_hash)?; } } From 8afe8b3569085d05eba78a0cb11399485320405b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 24 Jun 2019 15:31:36 +1000 Subject: [PATCH 40/40] Implement fixes from PR review --- beacon_node/beacon_chain/src/fork_choice.rs | 2 +- beacon_node/beacon_chain/tests/tests.rs | 2 +- eth2/types/src/beacon_state.rs | 9 +++------ 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/beacon_node/beacon_chain/src/fork_choice.rs b/beacon_node/beacon_chain/src/fork_choice.rs index 389e5b46bf..c693145ea6 100644 --- a/beacon_node/beacon_chain/src/fork_choice.rs +++ b/beacon_node/beacon_chain/src/fork_choice.rs @@ -87,7 +87,7 @@ impl ForkChoice { start_state .validator_registry .get(validator_index) - .and_then(|v| Some(v.effective_balance)) + .map(|v| v.effective_balance) }; self.backend diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index 42b6cb6a21..17e373ad6a 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -33,7 +33,7 @@ fn fork() { let honest_fork_blocks = delay + 1; let faulty_fork_blocks = delay + 2; - // Build an initial chain were all validators agree. + // Build an initial chain where all validators agree. harness.extend_chain( initial_blocks, BlockStrategy::OnCanonicalHead, diff --git a/eth2/types/src/beacon_state.rs b/eth2/types/src/beacon_state.rs index f82340017f..1be6eac236 100644 --- a/eth2/types/src/beacon_state.rs +++ b/eth2/types/src/beacon_state.rs @@ -586,13 +586,10 @@ impl BeaconState { /// Gets the oldest (earliest slot) state root. /// - /// Spec v0.5.1 + /// Spec v0.6.3 pub fn get_oldest_state_root(&self) -> Result<&Hash256, Error> { - let lookback = std::cmp::min( - self.slot - Slot::from(self.latest_state_roots.len()), - self.slot, - ); - let i = self.get_latest_state_roots_index(self.slot - lookback)?; + let i = self + .get_latest_state_roots_index(self.slot - Slot::from(self.latest_state_roots.len()))?; Ok(&self.latest_state_roots[i]) }