From 5394726caffb75c6fc306d6aa170287c6ff75146 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 7 May 2019 14:57:42 +1000 Subject: [PATCH] spec: initiate_validator_exit v0.6.1 Added a new field `exit_cache` to the BeaconState, which caches the number of validators exiting at each epoch. --- eth2/state_processing/src/common/exit.rs | 39 +++++++++++++ .../src/common/exit_validator.rs | 22 -------- eth2/state_processing/src/common/mod.rs | 4 +- .../src/common/slash_validator.rs | 56 +++++++------------ eth2/types/src/beacon_state.rs | 26 +++++++++ eth2/types/src/beacon_state/exit_cache.rs | 35 ++++++++++++ 6 files changed, 122 insertions(+), 60 deletions(-) create mode 100644 eth2/state_processing/src/common/exit.rs delete mode 100644 eth2/state_processing/src/common/exit_validator.rs create mode 100644 eth2/types/src/beacon_state/exit_cache.rs diff --git a/eth2/state_processing/src/common/exit.rs b/eth2/state_processing/src/common/exit.rs new file mode 100644 index 000000000..a05ce8f9c --- /dev/null +++ b/eth2/state_processing/src/common/exit.rs @@ -0,0 +1,39 @@ +use std::cmp::max; +use types::{BeaconStateError as Error, *}; + +/// Initiate the exit of the validator of the given `index`. +/// +/// Spec v0.6.1 +pub fn initiate_validator_exit( + state: &mut BeaconState, + index: usize, + spec: &ChainSpec, +) -> Result<(), Error> { + if index >= state.validator_registry.len() { + return Err(Error::UnknownValidator); + } + + // Return if the validator already initiated exit + if state.validator_registry[index].exit_epoch != spec.far_future_epoch { + return Ok(()); + } + + // Compute exit queue epoch + let delayed_epoch = state.get_delayed_activation_exit_epoch(state.current_epoch(spec), spec); + let mut exit_queue_epoch = state + .exit_cache + .max_epoch() + .map_or(delayed_epoch, |epoch| max(epoch, delayed_epoch)); + let exit_queue_churn = state.exit_cache.get_churn_at(exit_queue_epoch); + + if exit_queue_churn >= state.get_churn_limit(spec)? { + exit_queue_epoch += 1; + } + + state.exit_cache.record_validator_exit(exit_queue_epoch); + state.validator_registry[index].exit_epoch = exit_queue_epoch; + state.validator_registry[index].withdrawable_epoch = + exit_queue_epoch + spec.min_validator_withdrawability_delay; + + Ok(()) +} diff --git a/eth2/state_processing/src/common/exit_validator.rs b/eth2/state_processing/src/common/exit_validator.rs deleted file mode 100644 index a6cfb395e..000000000 --- a/eth2/state_processing/src/common/exit_validator.rs +++ /dev/null @@ -1,22 +0,0 @@ -use types::{BeaconStateError as Error, *}; - -/// Exit the validator of the given `index`. -/// -/// Spec v0.5.1 -pub fn exit_validator( - state: &mut BeaconState, - validator_index: usize, - spec: &ChainSpec, -) -> Result<(), Error> { - if validator_index >= state.validator_registry.len() { - return Err(Error::UnknownValidator); - } - - let delayed_epoch = state.get_delayed_activation_exit_epoch(state.current_epoch(spec), spec); - - if state.validator_registry[validator_index].exit_epoch > delayed_epoch { - state.validator_registry[validator_index].exit_epoch = delayed_epoch; - } - - Ok(()) -} diff --git a/eth2/state_processing/src/common/mod.rs b/eth2/state_processing/src/common/mod.rs index 49898d10f..896dfad0d 100644 --- a/eth2/state_processing/src/common/mod.rs +++ b/eth2/state_processing/src/common/mod.rs @@ -1,7 +1,7 @@ -mod exit_validator; +mod exit; mod slash_validator; mod verify_bitfield; -pub use exit_validator::exit_validator; +pub use exit::initiate_validator_exit; pub use slash_validator::slash_validator; pub use verify_bitfield::verify_bitfield_length; diff --git a/eth2/state_processing/src/common/slash_validator.rs b/eth2/state_processing/src/common/slash_validator.rs index c1aad7da1..d50631fa1 100644 --- a/eth2/state_processing/src/common/slash_validator.rs +++ b/eth2/state_processing/src/common/slash_validator.rs @@ -1,62 +1,46 @@ -use crate::common::exit_validator; +use crate::common::initiate_validator_exit; use types::{BeaconStateError as Error, *}; /// Slash the validator with index ``index``. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn slash_validator( state: &mut BeaconState, - validator_index: usize, + slashed_index: usize, + opt_whistleblower_index: Option, spec: &ChainSpec, ) -> Result<(), Error> { - let current_epoch = state.current_epoch(spec); - - if (validator_index >= state.validator_registry.len()) - | (validator_index >= state.validator_balances.len()) - { + if slashed_index >= state.validator_registry.len() || slashed_index >= state.balances.len() { return Err(BeaconStateError::UnknownValidator); } - let validator = &state.validator_registry[validator_index]; + let current_epoch = state.current_epoch(spec); - let effective_balance = state.get_effective_balance(validator_index, spec)?; + initiate_validator_exit(state, slashed_index, spec)?; - // A validator that is withdrawn cannot be slashed. - // - // This constraint will be lifted in Phase 0. - if state.slot - >= validator - .withdrawable_epoch - .start_slot(spec.slots_per_epoch) - { - return Err(Error::ValidatorIsWithdrawable); - } - - exit_validator(state, validator_index, spec)?; + state.validator_registry[slashed_index].slashed = true; + state.validator_registry[slashed_index].withdrawable_epoch = + current_epoch + Epoch::from(spec.latest_slashed_exit_length); + let slashed_balance = state.get_effective_balance(slashed_index, spec)?; state.set_slashed_balance( current_epoch, - state.get_slashed_balance(current_epoch, spec)? + effective_balance, + state.get_slashed_balance(current_epoch, spec)? + slashed_balance, spec, )?; - let whistleblower_index = + let proposer_index = state.get_beacon_proposer_index(state.slot, RelativeEpoch::Current, spec)?; - let whistleblower_reward = effective_balance / spec.whistleblower_reward_quotient; + let whistleblower_index = opt_whistleblower_index.unwrap_or(proposer_index); + let whistleblowing_reward = slashed_balance / spec.whistleblowing_reward_quotient; + let proposer_reward = whistleblowing_reward / spec.proposer_reward_quotient; + safe_add_assign!(state.balances[proposer_index], proposer_reward); safe_add_assign!( - state.validator_balances[whistleblower_index as usize], - whistleblower_reward + state.balances[whistleblower_index], + whistleblowing_reward - proposer_reward ); - safe_sub_assign!( - state.validator_balances[validator_index], - whistleblower_reward - ); - - state.validator_registry[validator_index].slashed = true; - - state.validator_registry[validator_index].withdrawable_epoch = - current_epoch + Epoch::from(spec.latest_slashed_exit_length); + safe_sub_assign!(state.balances[slashed_index], whistleblowing_reward); Ok(()) } diff --git a/eth2/types/src/beacon_state.rs b/eth2/types/src/beacon_state.rs index cb37c8448..4ac0c1f71 100644 --- a/eth2/types/src/beacon_state.rs +++ b/eth2/types/src/beacon_state.rs @@ -1,4 +1,5 @@ use self::epoch_cache::{get_active_validator_indices, EpochCache, Error as EpochCacheError}; +use self::exit_cache::ExitCache; use crate::test_utils::TestRandom; use crate::*; use cached_tree_hash::{Error as TreeHashCacheError, TreeHashCache}; @@ -13,6 +14,7 @@ use tree_hash::TreeHash; use tree_hash_derive::{CachedTreeHash, TreeHash}; mod epoch_cache; +mod exit_cache; mod pubkey_cache; mod tests; @@ -126,6 +128,12 @@ pub struct BeaconState { #[tree_hash(skip_hashing)] #[test_random(default)] pub tree_hash_cache: TreeHashCache, + #[serde(skip_serializing, skip_deserializing)] + #[ssz(skip_serializing)] + #[ssz(skip_deserializing)] + #[tree_hash(skip_hashing)] + #[test_random(default)] + pub exit_cache: ExitCache, } impl BeaconState { @@ -198,6 +206,7 @@ impl BeaconState { ], pubkey_cache: PubkeyCache::default(), tree_hash_cache: TreeHashCache::default(), + exit_cache: ExitCache::default(), } } @@ -634,6 +643,21 @@ impl BeaconState { epoch + 1 + spec.activation_exit_delay } + /// Return the churn limit for the current epoch (number of validators who can leave per epoch). + /// + /// Uses the epoch cache, and will error if it isn't initialized. + /// + /// Spec v0.6.1 + pub fn get_churn_limit(&self, spec: &ChainSpec) -> Result { + Ok(std::cmp::max( + spec.min_per_epoch_churn_limit, + self.cache(RelativeEpoch::Current, spec)? + .active_validator_indices + .len() as u64 + / spec.churn_limit_quotient, + )) + } + /// Initiate an exit for the validator of the given `index`. /// /// Spec v0.5.1 @@ -685,6 +709,8 @@ impl BeaconState { self.build_epoch_cache(RelativeEpoch::NextWithRegistryChange, spec)?; self.update_pubkey_cache()?; self.update_tree_hash_cache()?; + self.exit_cache + .build_from_registry(&self.validator_registry, spec); Ok(()) } diff --git a/eth2/types/src/beacon_state/exit_cache.rs b/eth2/types/src/beacon_state/exit_cache.rs new file mode 100644 index 000000000..c129d70a2 --- /dev/null +++ b/eth2/types/src/beacon_state/exit_cache.rs @@ -0,0 +1,35 @@ +use super::{ChainSpec, Epoch, Validator}; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Map from exit epoch to the number of validators known to be exiting/exited at that epoch. +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +pub struct ExitCache(HashMap); + +impl ExitCache { + /// Add all validators with a non-trivial exit epoch to the cache. + pub fn build_from_registry(&mut self, validator_registry: &[Validator], spec: &ChainSpec) { + validator_registry + .iter() + .filter(|validator| validator.exit_epoch != spec.far_future_epoch) + .for_each(|validator| self.record_validator_exit(validator.exit_epoch)); + } + + /// Record the exit of a single validator in the cache. + /// + /// Must only be called once per exiting validator. + pub fn record_validator_exit(&mut self, exit_epoch: Epoch) { + *self.0.entry(exit_epoch).or_insert(0) += 1; + } + + /// Get the greatest epoch for which validator exits are known. + pub fn max_epoch(&self) -> Option { + // This could probably be made even faster by caching the maximum. + self.0.keys().max().cloned() + } + + /// Get the number of validators exiting/exited at a given epoch, or zero if not known. + pub fn get_churn_at(&self, epoch: Epoch) -> u64 { + self.0.get(&epoch).cloned().unwrap_or(0) + } +}