From 4a60c06afe58f18cdac13c04b208cdca5795ecc8 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 16 Jan 2020 13:55:33 +1100 Subject: [PATCH] Add binary, re-org crate --- Cargo.lock | 3 + eth2/proto_array_fork_choice/Cargo.toml | 7 + eth2/proto_array_fork_choice/no_votes.yaml | 239 ++++++ eth2/proto_array_fork_choice/src/bin.rs | 19 + eth2/proto_array_fork_choice/src/error.rs | 31 + .../src/fork_choice_test_definition.rs | 6 +- eth2/proto_array_fork_choice/src/lib.rs | 732 +----------------- .../src/proto_array.rs | 2 +- .../src/proto_array_fork_choice.rs | 699 +++++++++++++++++ .../src/ssz_container.rs | 2 +- eth2/proto_array_fork_choice/votes.yaml | 288 +++++++ 11 files changed, 1296 insertions(+), 732 deletions(-) create mode 100644 eth2/proto_array_fork_choice/no_votes.yaml create mode 100644 eth2/proto_array_fork_choice/src/bin.rs create mode 100644 eth2/proto_array_fork_choice/src/error.rs create mode 100644 eth2/proto_array_fork_choice/src/proto_array_fork_choice.rs create mode 100644 eth2/proto_array_fork_choice/votes.yaml diff --git a/Cargo.lock b/Cargo.lock index eb08658396..71d7a96ffc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3026,6 +3026,9 @@ dependencies = [ "eth2_ssz_derive 0.1.0", "itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)", "types 0.1.0", ] diff --git a/eth2/proto_array_fork_choice/Cargo.toml b/eth2/proto_array_fork_choice/Cargo.toml index 0464907d3d..687b6aefcc 100644 --- a/eth2/proto_array_fork_choice/Cargo.toml +++ b/eth2/proto_array_fork_choice/Cargo.toml @@ -4,9 +4,16 @@ version = "0.1.0" authors = ["Paul Hauner "] edition = "2018" +[[bin]] +name = "proto_array_fork_choice" +path = "src/bin.rs" + [dependencies] parking_lot = "0.9.0" types = { path = "../types" } itertools = "0.8.1" eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" +serde = "1.0.102" +serde_derive = "1.0.102" +serde_yaml = "0.8.11" diff --git a/eth2/proto_array_fork_choice/no_votes.yaml b/eth2/proto_array_fork_choice/no_votes.yaml new file mode 100644 index 0000000000..6e434ae34c --- /dev/null +++ b/eth2/proto_array_fork_choice/no_votes.yaml @@ -0,0 +1,239 @@ +--- +finalized_block_slot: 0 +justified_epoch: 1 +finalized_epoch: 1 +finalized_root: 0x0000000000000000000000000000000000000000000000000000000000000000 +operations: + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000000 + - ProcessBlock: + slot: 0 + root: 0x0000000000000000000000000000000000000000000000000000000000000002 + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + justified_epoch: 1 + finalized_epoch: 1 + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000002 + - ProcessBlock: + slot: 0 + root: 0x0000000000000000000000000000000000000000000000000000000000000001 + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + justified_epoch: 1 + finalized_epoch: 1 + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000002 + - ProcessBlock: + slot: 0 + root: 0x0000000000000000000000000000000000000000000000000000000000000003 + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000001 + justified_epoch: 1 + finalized_epoch: 1 + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000002 + - ProcessBlock: + slot: 0 + root: 0x0000000000000000000000000000000000000000000000000000000000000004 + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000002 + justified_epoch: 1 + finalized_epoch: 1 + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000004 + - ProcessBlock: + slot: 0 + root: 0x0000000000000000000000000000000000000000000000000000000000000005 + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000004 + justified_epoch: 2 + finalized_epoch: 1 + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000004 + - InvalidFindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + finalized_epoch: 1 + justified_state_balances: + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - FindHead: + justified_epoch: 2 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + finalized_epoch: 1 + justified_state_balances: + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000005 + - ProcessBlock: + slot: 0 + root: 0x0000000000000000000000000000000000000000000000000000000000000006 + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + justified_epoch: 2 + finalized_epoch: 1 + - FindHead: + justified_epoch: 2 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + finalized_epoch: 1 + justified_state_balances: + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000006 \ No newline at end of file diff --git a/eth2/proto_array_fork_choice/src/bin.rs b/eth2/proto_array_fork_choice/src/bin.rs new file mode 100644 index 0000000000..2a3807ea4e --- /dev/null +++ b/eth2/proto_array_fork_choice/src/bin.rs @@ -0,0 +1,19 @@ +mod error; +mod fork_choice_test_definition; +mod proto_array; +mod proto_array_fork_choice; +mod ssz_container; + +pub use fork_choice_test_definition::*; +use serde_yaml; +use std::fs::File; + +fn main() { + write_test_def_to_yaml("votes.yaml", get_votes_test_definition()); + write_test_def_to_yaml("no_votes.yaml", get_no_votes_test_definition()); +} + +fn write_test_def_to_yaml(filename: &str, def: ForkChoiceTestDefinition) { + let file = File::create(filename).expect("Should be able to open file"); + serde_yaml::to_writer(file, &def).expect("Should be able to write YAML to file"); +} diff --git a/eth2/proto_array_fork_choice/src/error.rs b/eth2/proto_array_fork_choice/src/error.rs new file mode 100644 index 0000000000..ee1e58f5df --- /dev/null +++ b/eth2/proto_array_fork_choice/src/error.rs @@ -0,0 +1,31 @@ +use types::{Epoch, Hash256}; + +#[derive(Clone, PartialEq, Debug)] +pub enum Error { + FinalizedNodeUnknown(Hash256), + JustifiedNodeUnknown(Hash256), + InvalidFinalizedRootChange, + InvalidNodeIndex(usize), + InvalidParentIndex(usize), + InvalidBestChildIndex(usize), + InvalidJustifiedIndex(usize), + InvalidBestDescendant(usize), + InvalidParentDelta(usize), + InvalidNodeDelta(usize), + DeltaOverflow(usize), + IndexOverflow(&'static str), + InvalidDeltaLen { + deltas: usize, + indices: usize, + }, + RevertedFinalizedEpoch { + current_finalized_epoch: Epoch, + new_finalized_epoch: Epoch, + }, + InvalidBestNode { + justified_epoch: Epoch, + finalized_epoch: Epoch, + node_justified_epoch: Epoch, + node_finalized_epoch: Epoch, + }, +} diff --git a/eth2/proto_array_fork_choice/src/fork_choice_test_definition.rs b/eth2/proto_array_fork_choice/src/fork_choice_test_definition.rs index d976579958..501e38ee46 100644 --- a/eth2/proto_array_fork_choice/src/fork_choice_test_definition.rs +++ b/eth2/proto_array_fork_choice/src/fork_choice_test_definition.rs @@ -1,7 +1,8 @@ -use crate::ProtoArrayForkChoice; +use crate::proto_array_fork_choice::ProtoArrayForkChoice; +use serde_derive::{Deserialize, Serialize}; use types::{Epoch, Hash256, Slot}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum Operation { FindHead { justified_epoch: Epoch, @@ -36,6 +37,7 @@ pub enum Operation { }, } +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ForkChoiceTestDefinition { pub finalized_block_slot: Slot, pub justified_epoch: Epoch, diff --git a/eth2/proto_array_fork_choice/src/lib.rs b/eth2/proto_array_fork_choice/src/lib.rs index 19c1948bd8..bf8fc5db28 100644 --- a/eth2/proto_array_fork_choice/src/lib.rs +++ b/eth2/proto_array_fork_choice/src/lib.rs @@ -1,732 +1,8 @@ +mod error; pub mod fork_choice_test_definition; mod proto_array; +mod proto_array_fork_choice; mod ssz_container; -use parking_lot::RwLock; -use proto_array::ProtoArray; -use ssz::{Decode, Encode}; -use ssz_container::SszContainer; -use ssz_derive::{Decode, Encode}; -use std::collections::HashMap; -use types::{Epoch, Hash256, Slot}; - -pub const DEFAULT_PRUNE_THRESHOLD: usize = 256; - -#[derive(Clone, PartialEq, Debug)] -pub enum Error { - FinalizedNodeUnknown(Hash256), - JustifiedNodeUnknown(Hash256), - InvalidFinalizedRootChange, - InvalidNodeIndex(usize), - InvalidParentIndex(usize), - InvalidBestChildIndex(usize), - InvalidJustifiedIndex(usize), - InvalidBestDescendant(usize), - InvalidParentDelta(usize), - InvalidNodeDelta(usize), - DeltaOverflow(usize), - IndexOverflow(&'static str), - InvalidDeltaLen { - deltas: usize, - indices: usize, - }, - RevertedFinalizedEpoch { - current_finalized_epoch: Epoch, - new_finalized_epoch: Epoch, - }, - InvalidBestNode { - justified_epoch: Epoch, - finalized_epoch: Epoch, - node_justified_epoch: Epoch, - node_finalized_epoch: Epoch, - }, -} - -#[derive(Default, PartialEq, Clone, Encode, Decode)] -pub struct VoteTracker { - current_root: Hash256, - next_root: Hash256, - next_epoch: Epoch, -} - -/// 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, Debug, PartialEq)] -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 iter_mut(&mut self) -> impl Iterator { - self.0.iter_mut() - } -} - -pub struct ProtoArrayForkChoice { - proto_array: RwLock, - votes: RwLock>, - balances: RwLock>, -} - -impl PartialEq for ProtoArrayForkChoice { - fn eq(&self, other: &Self) -> bool { - *self.proto_array.read() == *other.proto_array.read() - && *self.votes.read() == *other.votes.read() - && *self.balances.read() == *other.balances.read() - } -} - -impl ProtoArrayForkChoice { - pub fn new( - finalized_block_slot: Slot, - justified_epoch: Epoch, - finalized_epoch: Epoch, - finalized_root: Hash256, - ) -> Result { - let mut proto_array = ProtoArray { - prune_threshold: DEFAULT_PRUNE_THRESHOLD, - justified_epoch, - finalized_epoch, - nodes: Vec::with_capacity(1), - indices: HashMap::with_capacity(1), - }; - - proto_array - .on_new_block( - finalized_block_slot, - finalized_root, - None, - justified_epoch, - finalized_epoch, - ) - .map_err(|e| format!("Failed to add finalized block to proto_array: {:?}", e))?; - - Ok(Self { - proto_array: RwLock::new(proto_array), - votes: RwLock::new(ElasticList::default()), - balances: RwLock::new(vec![]), - }) - } - - pub fn process_attestation( - &self, - validator_index: usize, - block_root: Hash256, - target_epoch: Epoch, - ) -> Result<(), String> { - let mut votes = self.votes.write(); - let vote = votes.get_mut(validator_index); - - if target_epoch > vote.next_epoch || *vote == VoteTracker::default() { - vote.next_root = block_root; - vote.next_epoch = target_epoch; - } - - Ok(()) - } - - pub fn process_block( - &self, - slot: Slot, - block_root: Hash256, - parent_root: Hash256, - justified_epoch: Epoch, - finalized_epoch: Epoch, - ) -> Result<(), String> { - self.proto_array - .write() - .on_new_block( - slot, - block_root, - Some(parent_root), - justified_epoch, - finalized_epoch, - ) - .map_err(|e| format!("process_block_error: {:?}", e)) - } - - pub fn find_head( - &self, - justified_epoch: Epoch, - justified_root: Hash256, - finalized_epoch: Epoch, - justified_state_balances: &[u64], - ) -> Result { - let mut proto_array = self.proto_array.write(); - let mut votes = self.votes.write(); - let mut old_balances = self.balances.write(); - - let new_balances = justified_state_balances; - - let deltas = compute_deltas( - &proto_array.indices, - &mut votes, - &old_balances, - &new_balances, - ) - .map_err(|e| format!("find_head compute_deltas failed: {:?}", e))?; - - proto_array - .apply_score_changes(deltas, justified_epoch, finalized_epoch) - .map_err(|e| format!("find_head apply_score_changes failed: {:?}", e))?; - - *old_balances = new_balances.to_vec(); - - proto_array - .find_head(&justified_root) - .map_err(|e| format!("find_head failed: {:?}", e)) - } - - pub fn update_finalized_root( - &self, - finalized_epoch: Epoch, - finalized_root: Hash256, - ) -> Result<(), String> { - self.proto_array - .write() - .maybe_prune(finalized_epoch, finalized_root) - .map_err(|e| format!("find_head maybe_prune failed: {:?}", e)) - } - - pub fn set_prune_threshold(&self, prune_threshold: usize) { - self.proto_array.write().prune_threshold = prune_threshold; - } - - pub fn len(&self) -> usize { - self.proto_array.read().nodes.len() - } - - pub fn contains_block(&self, block_root: &Hash256) -> bool { - self.proto_array.read().indices.contains_key(block_root) - } - - pub fn block_slot(&self, block_root: &Hash256) -> Option { - let proto_array = self.proto_array.read(); - - let i = proto_array.indices.get(block_root)?; - let block = proto_array.nodes.get(*i)?; - - Some(block.slot) - } - - pub fn latest_message(&self, validator_index: usize) -> Option<(Hash256, Epoch)> { - let votes = self.votes.read(); - - if validator_index < votes.0.len() { - let vote = &votes.0[validator_index]; - - if *vote == VoteTracker::default() { - None - } else { - Some((vote.next_root, vote.next_epoch)) - } - } else { - None - } - } - - pub fn as_bytes(&self) -> Vec { - SszContainer::from(self).as_ssz_bytes() - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - SszContainer::from_ssz_bytes(bytes) - .map(Into::into) - .map_err(|e| format!("Failed to decode ProtoArrayForkChoice: {:?}", e)) - } -} - -/// Returns a list of `deltas`, where there is one delta for each of the indices in -/// `0..indices.len()`. -/// -/// The deltas are formed by a change between `old_balances` and `new_balances`, and/or a change of vote in `votes`. -/// -/// ## Errors -/// -/// - If a value in `indices` is greater to or equal to `indices.len()`. -/// - If some `Hash256` in `votes` is not a key in `indices` (except for `Hash256::zero()`, this is -/// always valid). -fn compute_deltas( - indices: &HashMap, - votes: &mut ElasticList, - old_balances: &[u64], - new_balances: &[u64], -) -> Result, Error> { - let mut deltas = vec![0_i64; indices.len()]; - - for (val_index, vote) in votes.iter_mut().enumerate() { - // There is no need to create a score change if the validator has never voted or both their - // votes are for the zero hash (alias to the genesis block). - if vote.current_root == Hash256::zero() && vote.next_root == Hash256::zero() { - continue; - } - - // If the validator was not included in the _old_ balances (i.e., it did not exist yet) - // then say its balance was zero. - let old_balance = old_balances.get(val_index).copied().unwrap_or_else(|| 0); - - // If the validators vote is not known in the _new_ balances, then use a balance of zero. - // - // It is possible that there is a vote for an unknown validator if we change our justified - // state to a new state with a higher epoch that is on a different fork because that fork may have - // on-boarded less validators than the prior fork. - let new_balance = new_balances.get(val_index).copied().unwrap_or_else(|| 0); - - if vote.current_root != vote.next_root || old_balance != new_balance { - // We ignore the vote if it is not known in `indices`. We assume that it is outside - // of our tree (i.e., pre-finalization) and therefore not interesting. - if let Some(current_delta_index) = indices.get(&vote.current_root).copied() { - let delta = deltas - .get_mut(current_delta_index) - .ok_or_else(|| Error::InvalidNodeDelta(current_delta_index))? - .checked_sub(old_balance as i64) - .ok_or_else(|| Error::DeltaOverflow(current_delta_index))?; - - // Array access safe due to check on previous line. - deltas[current_delta_index] = delta; - } - - // We ignore the vote if it is not known in `indices`. We assume that it is outside - // of our tree (i.e., pre-finalization) and therefore not interesting. - if let Some(next_delta_index) = indices.get(&vote.next_root).copied() { - let delta = deltas - .get(next_delta_index) - .ok_or_else(|| Error::InvalidNodeDelta(next_delta_index))? - .checked_add(new_balance as i64) - .ok_or_else(|| Error::DeltaOverflow(next_delta_index))?; - - // Array access safe due to check on previous line. - deltas[next_delta_index] = delta; - } - - vote.current_root = vote.next_root; - } - } - - Ok(deltas) -} - -#[cfg(test)] -mod test_compute_deltas { - use super::*; - - /// Gives a hash that is not the zero hash (unless i is `usize::max_value)`. - fn hash_from_index(i: usize) -> Hash256 { - Hash256::from_low_u64_be(i as u64 + 1) - } - - #[test] - fn zero_hash() { - let validator_count: usize = 16; - - let mut indices = HashMap::new(); - let mut votes = ElasticList::default(); - let mut old_balances = vec![]; - let mut new_balances = vec![]; - - for i in 0..validator_count { - indices.insert(hash_from_index(i), i); - votes.0.push(VoteTracker { - current_root: Hash256::zero(), - next_root: Hash256::zero(), - next_epoch: Epoch::new(0), - }); - old_balances.push(0); - new_balances.push(0); - } - - let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) - .expect("should compute deltas"); - - assert_eq!( - deltas.len(), - validator_count, - "deltas should have expected length" - ); - assert_eq!( - deltas, - vec![0; validator_count], - "deltas should all be zero" - ); - - for vote in votes.0 { - assert_eq!( - vote.current_root, vote.next_root, - "the vote shoulds should have been updated" - ); - } - } - - #[test] - fn all_voted_the_same() { - const BALANCE: u64 = 42; - - let validator_count: usize = 16; - - let mut indices = HashMap::new(); - let mut votes = ElasticList::default(); - let mut old_balances = vec![]; - let mut new_balances = vec![]; - - for i in 0..validator_count { - indices.insert(hash_from_index(i), i); - votes.0.push(VoteTracker { - current_root: Hash256::zero(), - next_root: hash_from_index(0), - next_epoch: Epoch::new(0), - }); - old_balances.push(BALANCE); - new_balances.push(BALANCE); - } - - let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) - .expect("should compute deltas"); - - assert_eq!( - deltas.len(), - validator_count, - "deltas should have expected length" - ); - - for (i, delta) in deltas.into_iter().enumerate() { - if i == 0 { - assert_eq!( - delta, - BALANCE as i64 * validator_count as i64, - "zero'th root should have a delta" - ); - } else { - assert_eq!(delta, 0, "all other deltas should be zero"); - } - } - - for vote in votes.0 { - assert_eq!( - vote.current_root, vote.next_root, - "the vote shoulds should have been updated" - ); - } - } - - #[test] - fn different_votes() { - const BALANCE: u64 = 42; - - let validator_count: usize = 16; - - let mut indices = HashMap::new(); - let mut votes = ElasticList::default(); - let mut old_balances = vec![]; - let mut new_balances = vec![]; - - for i in 0..validator_count { - indices.insert(hash_from_index(i), i); - votes.0.push(VoteTracker { - current_root: Hash256::zero(), - next_root: hash_from_index(i), - next_epoch: Epoch::new(0), - }); - old_balances.push(BALANCE); - new_balances.push(BALANCE); - } - - let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) - .expect("should compute deltas"); - - assert_eq!( - deltas.len(), - validator_count, - "deltas should have expected length" - ); - - for delta in deltas.into_iter() { - assert_eq!( - delta, BALANCE as i64, - "each root should have the same delta" - ); - } - - for vote in votes.0 { - assert_eq!( - vote.current_root, vote.next_root, - "the vote shoulds should have been updated" - ); - } - } - - #[test] - fn moving_votes() { - const BALANCE: u64 = 42; - - let validator_count: usize = 16; - - let mut indices = HashMap::new(); - let mut votes = ElasticList::default(); - let mut old_balances = vec![]; - let mut new_balances = vec![]; - - for i in 0..validator_count { - indices.insert(hash_from_index(i), i); - votes.0.push(VoteTracker { - current_root: hash_from_index(0), - next_root: hash_from_index(1), - next_epoch: Epoch::new(0), - }); - old_balances.push(BALANCE); - new_balances.push(BALANCE); - } - - let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) - .expect("should compute deltas"); - - assert_eq!( - deltas.len(), - validator_count, - "deltas should have expected length" - ); - - let total_delta = BALANCE as i64 * validator_count as i64; - - for (i, delta) in deltas.into_iter().enumerate() { - if i == 0 { - assert_eq!( - delta, - 0 - total_delta, - "zero'th root should have a negative delta" - ); - } else if i == 1 { - assert_eq!(delta, total_delta, "first root should have positive delta"); - } else { - assert_eq!(delta, 0, "all other deltas should be zero"); - } - } - - for vote in votes.0 { - assert_eq!( - vote.current_root, vote.next_root, - "the vote shoulds should have been updated" - ); - } - } - - #[test] - fn move_out_of_tree() { - const BALANCE: u64 = 42; - - let mut indices = HashMap::new(); - let mut votes = ElasticList::default(); - - // There is only one block. - indices.insert(hash_from_index(1), 0); - - // There are two validators. - let old_balances = vec![BALANCE; 2]; - let new_balances = vec![BALANCE; 2]; - - // One validator moves their vote from the block to the zero hash. - votes.0.push(VoteTracker { - current_root: hash_from_index(1), - next_root: Hash256::zero(), - next_epoch: Epoch::new(0), - }); - - // One validator moves their vote from the block to something outside the tree. - votes.0.push(VoteTracker { - current_root: hash_from_index(1), - next_root: Hash256::from_low_u64_be(1337), - next_epoch: Epoch::new(0), - }); - - let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) - .expect("should compute deltas"); - - assert_eq!(deltas.len(), 1, "deltas should have expected length"); - - assert_eq!( - deltas[0], - 0 - BALANCE as i64 * 2, - "the block should have lost both balances" - ); - - for vote in votes.0 { - assert_eq!( - vote.current_root, vote.next_root, - "the vote shoulds should have been updated" - ); - } - } - - #[test] - fn changing_balances() { - const OLD_BALANCE: u64 = 42; - const NEW_BALANCE: u64 = OLD_BALANCE * 2; - - let validator_count: usize = 16; - - let mut indices = HashMap::new(); - let mut votes = ElasticList::default(); - let mut old_balances = vec![]; - let mut new_balances = vec![]; - - for i in 0..validator_count { - indices.insert(hash_from_index(i), i); - votes.0.push(VoteTracker { - current_root: hash_from_index(0), - next_root: hash_from_index(1), - next_epoch: Epoch::new(0), - }); - old_balances.push(OLD_BALANCE); - new_balances.push(NEW_BALANCE); - } - - let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) - .expect("should compute deltas"); - - assert_eq!( - deltas.len(), - validator_count, - "deltas should have expected length" - ); - - for (i, delta) in deltas.into_iter().enumerate() { - if i == 0 { - assert_eq!( - delta, - 0 - OLD_BALANCE as i64 * validator_count as i64, - "zero'th root should have a negative delta" - ); - } else if i == 1 { - assert_eq!( - delta, - NEW_BALANCE as i64 * validator_count as i64, - "first root should have positive delta" - ); - } else { - assert_eq!(delta, 0, "all other deltas should be zero"); - } - } - - for vote in votes.0 { - assert_eq!( - vote.current_root, vote.next_root, - "the vote shoulds should have been updated" - ); - } - } - - #[test] - fn validator_appears() { - const BALANCE: u64 = 42; - - let mut indices = HashMap::new(); - let mut votes = ElasticList::default(); - - // There are two blocks. - indices.insert(hash_from_index(1), 0); - indices.insert(hash_from_index(2), 1); - - // There is only one validator in the old balances. - let old_balances = vec![BALANCE; 1]; - // There are two validators in the new balances. - let new_balances = vec![BALANCE; 2]; - - // Both validator move votes from block 1 to block 2. - for _ in 0..2 { - votes.0.push(VoteTracker { - current_root: hash_from_index(1), - next_root: hash_from_index(2), - next_epoch: Epoch::new(0), - }); - } - - let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) - .expect("should compute deltas"); - - assert_eq!(deltas.len(), 2, "deltas should have expected length"); - - assert_eq!( - deltas[0], - 0 - BALANCE as i64, - "block 1 should have only lost one balance" - ); - assert_eq!( - deltas[1], - 2 * BALANCE as i64, - "block 2 should have gained two balances" - ); - - for vote in votes.0 { - assert_eq!( - vote.current_root, vote.next_root, - "the vote shoulds should have been updated" - ); - } - } - - #[test] - fn validator_disappears() { - const BALANCE: u64 = 42; - - let mut indices = HashMap::new(); - let mut votes = ElasticList::default(); - - // There are two blocks. - indices.insert(hash_from_index(1), 0); - indices.insert(hash_from_index(2), 1); - - // There are two validators in the old balances. - let old_balances = vec![BALANCE; 2]; - // There is only one validator in the new balances. - let new_balances = vec![BALANCE; 1]; - - // Both validator move votes from block 1 to block 2. - for _ in 0..2 { - votes.0.push(VoteTracker { - current_root: hash_from_index(1), - next_root: hash_from_index(2), - next_epoch: Epoch::new(0), - }); - } - - let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) - .expect("should compute deltas"); - - assert_eq!(deltas.len(), 2, "deltas should have expected length"); - - assert_eq!( - deltas[0], - 0 - BALANCE as i64 * 2, - "block 1 should have lost both balances" - ); - assert_eq!( - deltas[1], BALANCE as i64, - "block 2 should have only gained one balance" - ); - - for vote in votes.0 { - assert_eq!( - vote.current_root, vote.next_root, - "the vote shoulds should have been updated" - ); - } - } -} +pub use error::Error; +pub use proto_array_fork_choice::ProtoArrayForkChoice; diff --git a/eth2/proto_array_fork_choice/src/proto_array.rs b/eth2/proto_array_fork_choice/src/proto_array.rs index 8659a790f1..9bfa6abeb2 100644 --- a/eth2/proto_array_fork_choice/src/proto_array.rs +++ b/eth2/proto_array_fork_choice/src/proto_array.rs @@ -1,4 +1,4 @@ -use crate::Error; +use crate::error::Error; use ssz_derive::{Decode, Encode}; use std::collections::HashMap; use types::{Epoch, Hash256, Slot}; diff --git a/eth2/proto_array_fork_choice/src/proto_array_fork_choice.rs b/eth2/proto_array_fork_choice/src/proto_array_fork_choice.rs new file mode 100644 index 0000000000..74c1253bf1 --- /dev/null +++ b/eth2/proto_array_fork_choice/src/proto_array_fork_choice.rs @@ -0,0 +1,699 @@ +use crate::error::Error; +use crate::proto_array::ProtoArray; +use crate::ssz_container::SszContainer; +use parking_lot::RwLock; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; +use std::collections::HashMap; +use types::{Epoch, Hash256, Slot}; + +pub const DEFAULT_PRUNE_THRESHOLD: usize = 256; + +#[derive(Default, PartialEq, Clone, Encode, Decode)] +pub struct VoteTracker { + current_root: Hash256, + next_root: Hash256, + next_epoch: Epoch, +} + +/// 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, Debug, PartialEq)] +pub struct ElasticList(pub 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 iter_mut(&mut self) -> impl Iterator { + self.0.iter_mut() + } +} + +pub struct ProtoArrayForkChoice { + pub(crate) proto_array: RwLock, + pub(crate) votes: RwLock>, + pub(crate) balances: RwLock>, +} + +impl PartialEq for ProtoArrayForkChoice { + fn eq(&self, other: &Self) -> bool { + *self.proto_array.read() == *other.proto_array.read() + && *self.votes.read() == *other.votes.read() + && *self.balances.read() == *other.balances.read() + } +} + +impl ProtoArrayForkChoice { + pub fn new( + finalized_block_slot: Slot, + justified_epoch: Epoch, + finalized_epoch: Epoch, + finalized_root: Hash256, + ) -> Result { + let mut proto_array = ProtoArray { + prune_threshold: DEFAULT_PRUNE_THRESHOLD, + justified_epoch, + finalized_epoch, + nodes: Vec::with_capacity(1), + indices: HashMap::with_capacity(1), + }; + + proto_array + .on_new_block( + finalized_block_slot, + finalized_root, + None, + justified_epoch, + finalized_epoch, + ) + .map_err(|e| format!("Failed to add finalized block to proto_array: {:?}", e))?; + + Ok(Self { + proto_array: RwLock::new(proto_array), + votes: RwLock::new(ElasticList::default()), + balances: RwLock::new(vec![]), + }) + } + + pub fn process_attestation( + &self, + validator_index: usize, + block_root: Hash256, + target_epoch: Epoch, + ) -> Result<(), String> { + let mut votes = self.votes.write(); + let vote = votes.get_mut(validator_index); + + if target_epoch > vote.next_epoch || *vote == VoteTracker::default() { + vote.next_root = block_root; + vote.next_epoch = target_epoch; + } + + Ok(()) + } + + pub fn process_block( + &self, + slot: Slot, + block_root: Hash256, + parent_root: Hash256, + justified_epoch: Epoch, + finalized_epoch: Epoch, + ) -> Result<(), String> { + self.proto_array + .write() + .on_new_block( + slot, + block_root, + Some(parent_root), + justified_epoch, + finalized_epoch, + ) + .map_err(|e| format!("process_block_error: {:?}", e)) + } + + pub fn find_head( + &self, + justified_epoch: Epoch, + justified_root: Hash256, + finalized_epoch: Epoch, + justified_state_balances: &[u64], + ) -> Result { + let mut proto_array = self.proto_array.write(); + let mut votes = self.votes.write(); + let mut old_balances = self.balances.write(); + + let new_balances = justified_state_balances; + + let deltas = compute_deltas( + &proto_array.indices, + &mut votes, + &old_balances, + &new_balances, + ) + .map_err(|e| format!("find_head compute_deltas failed: {:?}", e))?; + + proto_array + .apply_score_changes(deltas, justified_epoch, finalized_epoch) + .map_err(|e| format!("find_head apply_score_changes failed: {:?}", e))?; + + *old_balances = new_balances.to_vec(); + + proto_array + .find_head(&justified_root) + .map_err(|e| format!("find_head failed: {:?}", e)) + } + + pub fn update_finalized_root( + &self, + finalized_epoch: Epoch, + finalized_root: Hash256, + ) -> Result<(), String> { + self.proto_array + .write() + .maybe_prune(finalized_epoch, finalized_root) + .map_err(|e| format!("find_head maybe_prune failed: {:?}", e)) + } + + pub fn set_prune_threshold(&self, prune_threshold: usize) { + self.proto_array.write().prune_threshold = prune_threshold; + } + + pub fn len(&self) -> usize { + self.proto_array.read().nodes.len() + } + + pub fn contains_block(&self, block_root: &Hash256) -> bool { + self.proto_array.read().indices.contains_key(block_root) + } + + pub fn block_slot(&self, block_root: &Hash256) -> Option { + let proto_array = self.proto_array.read(); + + let i = proto_array.indices.get(block_root)?; + let block = proto_array.nodes.get(*i)?; + + Some(block.slot) + } + + pub fn latest_message(&self, validator_index: usize) -> Option<(Hash256, Epoch)> { + let votes = self.votes.read(); + + if validator_index < votes.0.len() { + let vote = &votes.0[validator_index]; + + if *vote == VoteTracker::default() { + None + } else { + Some((vote.next_root, vote.next_epoch)) + } + } else { + None + } + } + + pub fn as_bytes(&self) -> Vec { + SszContainer::from(self).as_ssz_bytes() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + SszContainer::from_ssz_bytes(bytes) + .map(Into::into) + .map_err(|e| format!("Failed to decode ProtoArrayForkChoice: {:?}", e)) + } +} + +/// Returns a list of `deltas`, where there is one delta for each of the indices in +/// `0..indices.len()`. +/// +/// The deltas are formed by a change between `old_balances` and `new_balances`, and/or a change of vote in `votes`. +/// +/// ## Errors +/// +/// - If a value in `indices` is greater to or equal to `indices.len()`. +/// - If some `Hash256` in `votes` is not a key in `indices` (except for `Hash256::zero()`, this is +/// always valid). +fn compute_deltas( + indices: &HashMap, + votes: &mut ElasticList, + old_balances: &[u64], + new_balances: &[u64], +) -> Result, Error> { + let mut deltas = vec![0_i64; indices.len()]; + + for (val_index, vote) in votes.iter_mut().enumerate() { + // There is no need to create a score change if the validator has never voted or both their + // votes are for the zero hash (alias to the genesis block). + if vote.current_root == Hash256::zero() && vote.next_root == Hash256::zero() { + continue; + } + + // If the validator was not included in the _old_ balances (i.e., it did not exist yet) + // then say its balance was zero. + let old_balance = old_balances.get(val_index).copied().unwrap_or_else(|| 0); + + // If the validators vote is not known in the _new_ balances, then use a balance of zero. + // + // It is possible that there is a vote for an unknown validator if we change our justified + // state to a new state with a higher epoch that is on a different fork because that fork may have + // on-boarded less validators than the prior fork. + let new_balance = new_balances.get(val_index).copied().unwrap_or_else(|| 0); + + if vote.current_root != vote.next_root || old_balance != new_balance { + // We ignore the vote if it is not known in `indices`. We assume that it is outside + // of our tree (i.e., pre-finalization) and therefore not interesting. + if let Some(current_delta_index) = indices.get(&vote.current_root).copied() { + let delta = deltas + .get_mut(current_delta_index) + .ok_or_else(|| Error::InvalidNodeDelta(current_delta_index))? + .checked_sub(old_balance as i64) + .ok_or_else(|| Error::DeltaOverflow(current_delta_index))?; + + // Array access safe due to check on previous line. + deltas[current_delta_index] = delta; + } + + // We ignore the vote if it is not known in `indices`. We assume that it is outside + // of our tree (i.e., pre-finalization) and therefore not interesting. + if let Some(next_delta_index) = indices.get(&vote.next_root).copied() { + let delta = deltas + .get(next_delta_index) + .ok_or_else(|| Error::InvalidNodeDelta(next_delta_index))? + .checked_add(new_balance as i64) + .ok_or_else(|| Error::DeltaOverflow(next_delta_index))?; + + // Array access safe due to check on previous line. + deltas[next_delta_index] = delta; + } + + vote.current_root = vote.next_root; + } + } + + Ok(deltas) +} + +#[cfg(test)] +mod test_compute_deltas { + use super::*; + + /// Gives a hash that is not the zero hash (unless i is `usize::max_value)`. + fn hash_from_index(i: usize) -> Hash256 { + Hash256::from_low_u64_be(i as u64 + 1) + } + + #[test] + fn zero_hash() { + let validator_count: usize = 16; + + let mut indices = HashMap::new(); + let mut votes = ElasticList::default(); + let mut old_balances = vec![]; + let mut new_balances = vec![]; + + for i in 0..validator_count { + indices.insert(hash_from_index(i), i); + votes.0.push(VoteTracker { + current_root: Hash256::zero(), + next_root: Hash256::zero(), + next_epoch: Epoch::new(0), + }); + old_balances.push(0); + new_balances.push(0); + } + + let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) + .expect("should compute deltas"); + + assert_eq!( + deltas.len(), + validator_count, + "deltas should have expected length" + ); + assert_eq!( + deltas, + vec![0; validator_count], + "deltas should all be zero" + ); + + for vote in votes.0 { + assert_eq!( + vote.current_root, vote.next_root, + "the vote shoulds should have been updated" + ); + } + } + + #[test] + fn all_voted_the_same() { + const BALANCE: u64 = 42; + + let validator_count: usize = 16; + + let mut indices = HashMap::new(); + let mut votes = ElasticList::default(); + let mut old_balances = vec![]; + let mut new_balances = vec![]; + + for i in 0..validator_count { + indices.insert(hash_from_index(i), i); + votes.0.push(VoteTracker { + current_root: Hash256::zero(), + next_root: hash_from_index(0), + next_epoch: Epoch::new(0), + }); + old_balances.push(BALANCE); + new_balances.push(BALANCE); + } + + let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) + .expect("should compute deltas"); + + assert_eq!( + deltas.len(), + validator_count, + "deltas should have expected length" + ); + + for (i, delta) in deltas.into_iter().enumerate() { + if i == 0 { + assert_eq!( + delta, + BALANCE as i64 * validator_count as i64, + "zero'th root should have a delta" + ); + } else { + assert_eq!(delta, 0, "all other deltas should be zero"); + } + } + + for vote in votes.0 { + assert_eq!( + vote.current_root, vote.next_root, + "the vote shoulds should have been updated" + ); + } + } + + #[test] + fn different_votes() { + const BALANCE: u64 = 42; + + let validator_count: usize = 16; + + let mut indices = HashMap::new(); + let mut votes = ElasticList::default(); + let mut old_balances = vec![]; + let mut new_balances = vec![]; + + for i in 0..validator_count { + indices.insert(hash_from_index(i), i); + votes.0.push(VoteTracker { + current_root: Hash256::zero(), + next_root: hash_from_index(i), + next_epoch: Epoch::new(0), + }); + old_balances.push(BALANCE); + new_balances.push(BALANCE); + } + + let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) + .expect("should compute deltas"); + + assert_eq!( + deltas.len(), + validator_count, + "deltas should have expected length" + ); + + for delta in deltas.into_iter() { + assert_eq!( + delta, BALANCE as i64, + "each root should have the same delta" + ); + } + + for vote in votes.0 { + assert_eq!( + vote.current_root, vote.next_root, + "the vote shoulds should have been updated" + ); + } + } + + #[test] + fn moving_votes() { + const BALANCE: u64 = 42; + + let validator_count: usize = 16; + + let mut indices = HashMap::new(); + let mut votes = ElasticList::default(); + let mut old_balances = vec![]; + let mut new_balances = vec![]; + + for i in 0..validator_count { + indices.insert(hash_from_index(i), i); + votes.0.push(VoteTracker { + current_root: hash_from_index(0), + next_root: hash_from_index(1), + next_epoch: Epoch::new(0), + }); + old_balances.push(BALANCE); + new_balances.push(BALANCE); + } + + let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) + .expect("should compute deltas"); + + assert_eq!( + deltas.len(), + validator_count, + "deltas should have expected length" + ); + + let total_delta = BALANCE as i64 * validator_count as i64; + + for (i, delta) in deltas.into_iter().enumerate() { + if i == 0 { + assert_eq!( + delta, + 0 - total_delta, + "zero'th root should have a negative delta" + ); + } else if i == 1 { + assert_eq!(delta, total_delta, "first root should have positive delta"); + } else { + assert_eq!(delta, 0, "all other deltas should be zero"); + } + } + + for vote in votes.0 { + assert_eq!( + vote.current_root, vote.next_root, + "the vote shoulds should have been updated" + ); + } + } + + #[test] + fn move_out_of_tree() { + const BALANCE: u64 = 42; + + let mut indices = HashMap::new(); + let mut votes = ElasticList::default(); + + // There is only one block. + indices.insert(hash_from_index(1), 0); + + // There are two validators. + let old_balances = vec![BALANCE; 2]; + let new_balances = vec![BALANCE; 2]; + + // One validator moves their vote from the block to the zero hash. + votes.0.push(VoteTracker { + current_root: hash_from_index(1), + next_root: Hash256::zero(), + next_epoch: Epoch::new(0), + }); + + // One validator moves their vote from the block to something outside the tree. + votes.0.push(VoteTracker { + current_root: hash_from_index(1), + next_root: Hash256::from_low_u64_be(1337), + next_epoch: Epoch::new(0), + }); + + let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) + .expect("should compute deltas"); + + assert_eq!(deltas.len(), 1, "deltas should have expected length"); + + assert_eq!( + deltas[0], + 0 - BALANCE as i64 * 2, + "the block should have lost both balances" + ); + + for vote in votes.0 { + assert_eq!( + vote.current_root, vote.next_root, + "the vote shoulds should have been updated" + ); + } + } + + #[test] + fn changing_balances() { + const OLD_BALANCE: u64 = 42; + const NEW_BALANCE: u64 = OLD_BALANCE * 2; + + let validator_count: usize = 16; + + let mut indices = HashMap::new(); + let mut votes = ElasticList::default(); + let mut old_balances = vec![]; + let mut new_balances = vec![]; + + for i in 0..validator_count { + indices.insert(hash_from_index(i), i); + votes.0.push(VoteTracker { + current_root: hash_from_index(0), + next_root: hash_from_index(1), + next_epoch: Epoch::new(0), + }); + old_balances.push(OLD_BALANCE); + new_balances.push(NEW_BALANCE); + } + + let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) + .expect("should compute deltas"); + + assert_eq!( + deltas.len(), + validator_count, + "deltas should have expected length" + ); + + for (i, delta) in deltas.into_iter().enumerate() { + if i == 0 { + assert_eq!( + delta, + 0 - OLD_BALANCE as i64 * validator_count as i64, + "zero'th root should have a negative delta" + ); + } else if i == 1 { + assert_eq!( + delta, + NEW_BALANCE as i64 * validator_count as i64, + "first root should have positive delta" + ); + } else { + assert_eq!(delta, 0, "all other deltas should be zero"); + } + } + + for vote in votes.0 { + assert_eq!( + vote.current_root, vote.next_root, + "the vote shoulds should have been updated" + ); + } + } + + #[test] + fn validator_appears() { + const BALANCE: u64 = 42; + + let mut indices = HashMap::new(); + let mut votes = ElasticList::default(); + + // There are two blocks. + indices.insert(hash_from_index(1), 0); + indices.insert(hash_from_index(2), 1); + + // There is only one validator in the old balances. + let old_balances = vec![BALANCE; 1]; + // There are two validators in the new balances. + let new_balances = vec![BALANCE; 2]; + + // Both validator move votes from block 1 to block 2. + for _ in 0..2 { + votes.0.push(VoteTracker { + current_root: hash_from_index(1), + next_root: hash_from_index(2), + next_epoch: Epoch::new(0), + }); + } + + let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) + .expect("should compute deltas"); + + assert_eq!(deltas.len(), 2, "deltas should have expected length"); + + assert_eq!( + deltas[0], + 0 - BALANCE as i64, + "block 1 should have only lost one balance" + ); + assert_eq!( + deltas[1], + 2 * BALANCE as i64, + "block 2 should have gained two balances" + ); + + for vote in votes.0 { + assert_eq!( + vote.current_root, vote.next_root, + "the vote shoulds should have been updated" + ); + } + } + + #[test] + fn validator_disappears() { + const BALANCE: u64 = 42; + + let mut indices = HashMap::new(); + let mut votes = ElasticList::default(); + + // There are two blocks. + indices.insert(hash_from_index(1), 0); + indices.insert(hash_from_index(2), 1); + + // There are two validators in the old balances. + let old_balances = vec![BALANCE; 2]; + // There is only one validator in the new balances. + let new_balances = vec![BALANCE; 1]; + + // Both validator move votes from block 1 to block 2. + for _ in 0..2 { + votes.0.push(VoteTracker { + current_root: hash_from_index(1), + next_root: hash_from_index(2), + next_epoch: Epoch::new(0), + }); + } + + let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances) + .expect("should compute deltas"); + + assert_eq!(deltas.len(), 2, "deltas should have expected length"); + + assert_eq!( + deltas[0], + 0 - BALANCE as i64 * 2, + "block 1 should have lost both balances" + ); + assert_eq!( + deltas[1], BALANCE as i64, + "block 2 should have only gained one balance" + ); + + for vote in votes.0 { + assert_eq!( + vote.current_root, vote.next_root, + "the vote shoulds should have been updated" + ); + } + } +} diff --git a/eth2/proto_array_fork_choice/src/ssz_container.rs b/eth2/proto_array_fork_choice/src/ssz_container.rs index 802b9285f5..30c52c08fd 100644 --- a/eth2/proto_array_fork_choice/src/ssz_container.rs +++ b/eth2/proto_array_fork_choice/src/ssz_container.rs @@ -1,6 +1,6 @@ use crate::{ proto_array::{ProtoArray, ProtoNode}, - ElasticList, ProtoArrayForkChoice, VoteTracker, + proto_array_fork_choice::{ElasticList, ProtoArrayForkChoice, VoteTracker}, }; use parking_lot::RwLock; use ssz_derive::{Decode, Encode}; diff --git a/eth2/proto_array_fork_choice/votes.yaml b/eth2/proto_array_fork_choice/votes.yaml new file mode 100644 index 0000000000..458e747645 --- /dev/null +++ b/eth2/proto_array_fork_choice/votes.yaml @@ -0,0 +1,288 @@ +--- +finalized_block_slot: 0 +justified_epoch: 1 +finalized_epoch: 1 +finalized_root: 0x0000000000000000000000000000000000000000000000000000000000000000 +operations: + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000000 + - ProcessBlock: + slot: 0 + root: 0x0000000000000000000000000000000000000000000000000000000000000002 + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + justified_epoch: 1 + finalized_epoch: 1 + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000002 + - ProcessBlock: + slot: 0 + root: 0x0000000000000000000000000000000000000000000000000000000000000001 + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + justified_epoch: 1 + finalized_epoch: 1 + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000002 + - ProcessAttestation: + validator_index: 0 + block_root: 0x0000000000000000000000000000000000000000000000000000000000000001 + target_epoch: 2 + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000001 + - ProcessAttestation: + validator_index: 1 + block_root: 0x0000000000000000000000000000000000000000000000000000000000000002 + target_epoch: 2 + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000002 + - ProcessBlock: + slot: 0 + root: 0x0000000000000000000000000000000000000000000000000000000000000003 + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000001 + justified_epoch: 1 + finalized_epoch: 1 + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000002 + - ProcessAttestation: + validator_index: 0 + block_root: 0x0000000000000000000000000000000000000000000000000000000000000003 + target_epoch: 3 + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000002 + - ProcessAttestation: + validator_index: 1 + block_root: 0x0000000000000000000000000000000000000000000000000000000000000001 + target_epoch: 3 + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000003 + - ProcessBlock: + slot: 0 + root: 0x0000000000000000000000000000000000000000000000000000000000000004 + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000003 + justified_epoch: 1 + finalized_epoch: 1 + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000004 + - ProcessBlock: + slot: 0 + root: 0x0000000000000000000000000000000000000000000000000000000000000005 + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000004 + justified_epoch: 2 + finalized_epoch: 2 + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000004 + - ProcessBlock: + slot: 0 + root: 0x0000000000000000000000000000000000000000000000000000000000000006 + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000004 + justified_epoch: 1 + finalized_epoch: 1 + - ProcessAttestation: + validator_index: 0 + block_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + target_epoch: 4 + - ProcessAttestation: + validator_index: 1 + block_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + target_epoch: 4 + - ProcessBlock: + slot: 0 + root: 0x0000000000000000000000000000000000000000000000000000000000000007 + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + justified_epoch: 2 + finalized_epoch: 2 + - ProcessBlock: + slot: 0 + root: 0x0000000000000000000000000000000000000000000000000000000000000008 + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000007 + justified_epoch: 2 + finalized_epoch: 2 + - ProcessBlock: + slot: 0 + root: 0x0000000000000000000000000000000000000000000000000000000000000009 + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000008 + justified_epoch: 2 + finalized_epoch: 2 + - FindHead: + justified_epoch: 1 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + finalized_epoch: 1 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000006 + - FindHead: + justified_epoch: 2 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + finalized_epoch: 2 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000009 + - ProcessAttestation: + validator_index: 0 + block_root: 0x0000000000000000000000000000000000000000000000000000000000000009 + target_epoch: 5 + - ProcessAttestation: + validator_index: 1 + block_root: 0x0000000000000000000000000000000000000000000000000000000000000009 + target_epoch: 5 + - ProcessBlock: + slot: 0 + root: 0x000000000000000000000000000000000000000000000000000000000000000a + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000008 + justified_epoch: 2 + finalized_epoch: 2 + - FindHead: + justified_epoch: 2 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + finalized_epoch: 2 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000009 + - ProcessAttestation: + validator_index: 2 + block_root: 0x000000000000000000000000000000000000000000000000000000000000000a + target_epoch: 5 + - ProcessAttestation: + validator_index: 3 + block_root: 0x000000000000000000000000000000000000000000000000000000000000000a + target_epoch: 5 + - FindHead: + justified_epoch: 2 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + finalized_epoch: 2 + justified_state_balances: + - 1 + - 1 + - 1 + - 1 + expected_head: 0x000000000000000000000000000000000000000000000000000000000000000a + - FindHead: + justified_epoch: 2 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + finalized_epoch: 2 + justified_state_balances: + - 1 + - 1 + - 0 + - 0 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000009 + - FindHead: + justified_epoch: 2 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + finalized_epoch: 2 + justified_state_balances: + - 1 + - 1 + - 1 + - 1 + expected_head: 0x000000000000000000000000000000000000000000000000000000000000000a + - FindHead: + justified_epoch: 2 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + finalized_epoch: 2 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000009 + - Prune: + finalized_epoch: 2 + finalized_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + prune_threshold: 18446744073709551615 + expected_len: 11 + - FindHead: + justified_epoch: 2 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + finalized_epoch: 2 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000009 + - Prune: + finalized_epoch: 2 + finalized_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + prune_threshold: 1 + expected_len: 6 + - FindHead: + justified_epoch: 2 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + finalized_epoch: 2 + justified_state_balances: + - 1 + - 1 + expected_head: 0x0000000000000000000000000000000000000000000000000000000000000009 + - ProcessBlock: + slot: 0 + root: 0x000000000000000000000000000000000000000000000000000000000000000b + parent_root: 0x0000000000000000000000000000000000000000000000000000000000000009 + justified_epoch: 2 + finalized_epoch: 2 + - FindHead: + justified_epoch: 2 + justified_root: 0x0000000000000000000000000000000000000000000000000000000000000005 + finalized_epoch: 2 + justified_state_balances: + - 1 + - 1 + expected_head: 0x000000000000000000000000000000000000000000000000000000000000000b \ No newline at end of file