diff --git a/Cargo.lock b/Cargo.lock index 4fc8ef8fe97..06cd117028b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7798,6 +7798,7 @@ dependencies = [ "tokio", "tree_hash", "types", + "vec_map", ] [[package]] diff --git a/beacon_node/beacon_chain/src/attestation_rewards.rs b/beacon_node/beacon_chain/src/attestation_rewards.rs index abd676d7389..e86c10a146b 100644 --- a/beacon_node/beacon_chain/src/attestation_rewards.rs +++ b/beacon_node/beacon_chain/src/attestation_rewards.rs @@ -6,7 +6,7 @@ use safe_arith::SafeArith; use serde_utils::quoted_u64::Quoted; use slog::debug; use state_processing::per_epoch_processing::altair::{ - process_inactivity_updates, process_justification_and_finalization, + process_inactivity_updates_slow, process_justification_and_finalization, }; use state_processing::{ common::altair::BaseRewardPerIncrement, @@ -134,10 +134,10 @@ impl BeaconChain { let spec = &self.spec; // Calculate ideal_rewards - let participation_cache = ParticipationCache::new(&state, spec)?; - process_justification_and_finalization(&state, &participation_cache)? - .apply_changes_to_state(&mut state); - process_inactivity_updates(&mut state, &participation_cache, spec)?; + let participation_cache = ParticipationCache::new(&state, spec) + .map_err(|_| BeaconChainError::AttestationRewardsError)?; + process_justification_and_finalization(&state)?.apply_changes_to_state(&mut state); + process_inactivity_updates_slow(&mut state, spec)?; let previous_epoch = state.previous_epoch(); @@ -147,13 +147,9 @@ impl BeaconChain { let weight = get_flag_weight(flag_index) .map_err(|_| BeaconChainError::AttestationRewardsError)?; - let unslashed_participating_indices = participation_cache - .get_unslashed_participating_indices(flag_index, previous_epoch)?; - - let unslashed_participating_balance = - unslashed_participating_indices - .total_balance() - .map_err(|_| BeaconChainError::AttestationRewardsError)?; + let unslashed_participating_balance = participation_cache + .previous_epoch_flag_attesting_balance(flag_index) + .map_err(|_| BeaconChainError::AttestationRewardsError)?; let unslashed_participating_increments = unslashed_participating_balance.safe_div(spec.effective_balance_increment)?; @@ -199,24 +195,41 @@ impl BeaconChain { Self::validators_ids_to_indices(&mut state, validators)? }; - for validator_index in &validators { - let eligible = state.is_eligible_validator(previous_epoch, *validator_index)?; + for &validator_index in &validators { + // Return 0s for unknown/inactive validator indices. This is a bit different from stable + // where we error for unknown pubkeys. + let Ok(validator) = participation_cache.get_validator(validator_index) else { + debug!( + self.log, + "No rewards for inactive/unknown validator"; + "index" => validator_index, + "epoch" => previous_epoch + ); + total_rewards.push(TotalAttestationRewards { + validator_index: validator_index as u64, + head: 0, + target: 0, + source: 0, + inclusion_delay: None, + inactivity: 0, + }); + continue; + }; + let eligible = validator.is_eligible; let mut head_reward = 0i64; let mut target_reward = 0i64; let mut source_reward = 0i64; let mut inactivity_penalty = 0i64; if eligible { - let effective_balance = state.get_effective_balance(*validator_index)?; + let effective_balance = validator.effective_balance; for flag_index in 0..PARTICIPATION_FLAG_WEIGHTS.len() { let (ideal_reward, penalty) = ideal_rewards_hashmap .get(&(flag_index, effective_balance)) .ok_or(BeaconChainError::AttestationRewardsError)?; - let voted_correctly = participation_cache - .get_unslashed_participating_indices(flag_index, previous_epoch) - .map_err(|_| BeaconChainError::AttestationRewardsError)? - .contains(*validator_index) + let voted_correctly = validator + .is_unslashed_participating_index(flag_index) .map_err(|_| BeaconChainError::AttestationRewardsError)?; if voted_correctly { if flag_index == TIMELY_HEAD_FLAG_INDEX { @@ -232,7 +245,7 @@ impl BeaconChain { target_reward = *penalty; let penalty_numerator = effective_balance - .safe_mul(state.get_inactivity_score(*validator_index)?)?; + .safe_mul(state.get_inactivity_score(validator_index)?)?; let penalty_denominator = spec .inactivity_score_bias .safe_mul(spec.inactivity_penalty_quotient_for_state(&state))?; @@ -244,7 +257,7 @@ impl BeaconChain { } } total_rewards.push(TotalAttestationRewards { - validator_index: *validator_index as u64, + validator_index: validator_index as u64, head: head_reward, target: target_reward, source: source_reward, diff --git a/beacon_node/beacon_chain/src/beacon_block_reward.rs b/beacon_node/beacon_chain/src/beacon_block_reward.rs index d05f7cb4ffd..5b70215d225 100644 --- a/beacon_node/beacon_chain/src/beacon_block_reward.rs +++ b/beacon_node/beacon_chain/src/beacon_block_reward.rs @@ -4,9 +4,8 @@ use operation_pool::RewardCache; use safe_arith::SafeArith; use slog::error; use state_processing::{ - common::{ - altair, get_attestation_participation_flag_indices, get_attesting_indices_from_state, - }, + common::{get_attestation_participation_flag_indices, get_attesting_indices_from_state}, + epoch_cache::initialize_epoch_cache, per_block_processing::{ altair::sync_committee::compute_sync_aggregate_rewards, get_slashable_indices, }, @@ -32,6 +31,7 @@ impl BeaconChain { state.build_committee_cache(RelativeEpoch::Previous, &self.spec)?; state.build_committee_cache(RelativeEpoch::Current, &self.spec)?; + initialize_epoch_cache(state, &self.spec)?; self.compute_beacon_block_reward_with_cache(block, block_root, state) } @@ -191,10 +191,6 @@ impl BeaconChain { block: BeaconBlockRef<'_, T::EthSpec, Payload>, state: &BeaconState, ) -> Result { - let total_active_balance = state.get_total_active_balance()?; - let base_reward_per_increment = - altair::BaseRewardPerIncrement::new(total_active_balance, &self.spec)?; - let mut total_proposer_reward = 0; let proposer_reward_denominator = WEIGHT_DENOMINATOR @@ -235,15 +231,8 @@ impl BeaconChain { && !validator_participation.has_flag(flag_index)? { validator_participation.add_flag(flag_index)?; - proposer_reward_numerator.safe_add_assign( - altair::get_base_reward( - state, - index, - base_reward_per_increment, - &self.spec, - )? - .safe_mul(weight)?, - )?; + proposer_reward_numerator + .safe_add_assign(state.get_base_reward(index)?.safe_mul(weight)?)?; } } } diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index b05ba01ee7e..d2b49c86f14 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4967,6 +4967,7 @@ impl BeaconChain { let attestation_packing_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_ATTESTATION_TIMES); + state.build_total_active_balance_cache_at(state.current_epoch(), &self.spec)?; let mut prev_filter_cache = HashMap::new(); let prev_attestation_filter = |att: &AttestationRef| { self.filter_op_pool_attestation(&mut prev_filter_cache, att, &state) diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 9c1ba06f853..2af9c851066 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -55,6 +55,7 @@ pub enum BeaconChainError { SlotClockDidNotStart, NoStateForSlot(Slot), BeaconStateError(BeaconStateError), + EpochCacheError(EpochCacheError), DBInconsistent(String), DBError(store::Error), ForkChoiceError(ForkChoiceError), @@ -250,6 +251,7 @@ easy_from_to!(StateAdvanceError, BeaconChainError); easy_from_to!(BlockReplayError, BeaconChainError); easy_from_to!(InconsistentFork, BeaconChainError); easy_from_to!(AvailabilityCheckError, BeaconChainError); +easy_from_to!(EpochCacheError, BeaconChainError); #[derive(Debug)] pub enum BlockProductionError { diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index 4334f90836f..e27180a002c 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -10,9 +10,7 @@ use beacon_chain::{ }; use lazy_static::lazy_static; use operation_pool::PersistedOperationPool; -use state_processing::{ - per_slot_processing, per_slot_processing::Error as SlotProcessingError, EpochProcessingError, -}; +use state_processing::{per_slot_processing, per_slot_processing::Error as SlotProcessingError}; use types::{ BeaconState, BeaconStateError, EthSpec, Hash256, Keypair, MinimalEthSpec, RelativeEpoch, Slot, }; @@ -59,9 +57,7 @@ fn massive_skips() { assert!(state.slot() > 1, "the state should skip at least one slot"); assert_eq!( error, - SlotProcessingError::EpochProcessingError(EpochProcessingError::BeaconStateError( - BeaconStateError::InsufficientValidators - )), + SlotProcessingError::BeaconStateError(BeaconStateError::InsufficientValidators), "should return error indicating that validators have been slashed out" ) } diff --git a/beacon_node/genesis/src/interop.rs b/beacon_node/genesis/src/interop.rs index d0129834300..5c67e045f8a 100644 --- a/beacon_node/genesis/src/interop.rs +++ b/beacon_node/genesis/src/interop.rs @@ -178,13 +178,14 @@ mod test { } for v in state.validators() { - let creds = v.withdrawal_credentials.as_bytes(); + let creds = v.withdrawal_credentials; assert_eq!( - creds[0], spec.bls_withdrawal_prefix_byte, + creds.as_bytes()[0], + spec.bls_withdrawal_prefix_byte, "first byte of withdrawal creds should be bls prefix" ); assert_eq!( - &creds[1..], + &creds.as_bytes()[1..], &hash(&v.pubkey.as_ssz_bytes())[1..], "rest of withdrawal creds should be pubkey hash" ) @@ -240,7 +241,8 @@ mod test { } for (index, v) in state.validators().iter().enumerate() { - let creds = v.withdrawal_credentials.as_bytes(); + let withdrawal_credientials = v.withdrawal_credentials; + let creds = withdrawal_credientials.as_bytes(); if index % 2 == 0 { assert_eq!( creds[0], spec.bls_withdrawal_prefix_byte, diff --git a/beacon_node/http_api/src/validator_inclusion.rs b/beacon_node/http_api/src/validator_inclusion.rs index f22ced1e693..c58e22e8cf2 100644 --- a/beacon_node/http_api/src/validator_inclusion.rs +++ b/beacon_node/http_api/src/validator_inclusion.rs @@ -4,11 +4,8 @@ use eth2::{ lighthouse::{GlobalValidatorInclusionData, ValidatorInclusionData}, types::ValidatorId, }; -use state_processing::per_epoch_processing::{ - altair::participation_cache::Error as ParticipationCacheError, process_epoch, - EpochProcessingSummary, -}; -use types::{BeaconState, ChainSpec, Epoch, EthSpec}; +use state_processing::per_epoch_processing::{process_epoch, EpochProcessingSummary}; +use types::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec}; /// Returns the state in the last slot of `epoch`. fn end_of_epoch_state( @@ -35,7 +32,7 @@ fn get_epoch_processing_summary( .map_err(|e| warp_utils::reject::custom_server_error(format!("{:?}", e))) } -fn convert_cache_error(error: ParticipationCacheError) -> warp::reject::Rejection { +fn convert_cache_error(error: BeaconStateError) -> warp::reject::Rejection { warp_utils::reject::custom_server_error(format!("{:?}", error)) } diff --git a/beacon_node/http_api/src/validators.rs b/beacon_node/http_api/src/validators.rs index 20af7a680df..5d032e98c43 100644 --- a/beacon_node/http_api/src/validators.rs +++ b/beacon_node/http_api/src/validators.rs @@ -29,7 +29,7 @@ pub fn get_beacon_state_validators( .filter(|(index, (validator, _))| { query_ids.as_ref().map_or(true, |ids| { ids.iter().any(|id| match id { - ValidatorId::PublicKey(pubkey) => &validator.pubkey == pubkey, + ValidatorId::PublicKey(pubkey) => validator.pubkey == *pubkey, ValidatorId::Index(param_index) => { *param_index == *index as u64 } @@ -93,7 +93,7 @@ pub fn get_beacon_state_validator_balances( .filter(|(index, (validator, _))| { optional_ids.map_or(true, |ids| { ids.iter().any(|id| match id { - ValidatorId::PublicKey(pubkey) => &validator.pubkey == pubkey, + ValidatorId::PublicKey(pubkey) => validator.pubkey == *pubkey, ValidatorId::Index(param_index) => { *param_index == *index as u64 } diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 2d946f30921..ed3d6553c13 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -820,7 +820,7 @@ impl ApiTester { ValidatorId::PublicKey( validators .get(i as usize) - .map_or(PublicKeyBytes::empty(), |val| val.pubkey.clone()), + .map_or(PublicKeyBytes::empty(), |val| *val.pubkey), ) }) .collect::>(); @@ -863,7 +863,7 @@ impl ApiTester { if i < state.balances().len() as u64 { validators.push(ValidatorBalanceData { index: i as u64, - balance: state.balances()[i as usize], + balance: *state.balances().get(i as usize).unwrap(), }); } } @@ -905,7 +905,7 @@ impl ApiTester { ValidatorId::PublicKey( validators .get(i as usize) - .map_or(PublicKeyBytes::empty(), |val| val.pubkey.clone()), + .map_or(PublicKeyBytes::empty(), |val| *val.pubkey), ) }) .collect::>(); @@ -999,7 +999,7 @@ impl ApiTester { for (i, validator) in validators.into_iter().enumerate() { let validator_ids = &[ - ValidatorId::PublicKey(validator.pubkey.clone()), + ValidatorId::PublicKey(*validator.pubkey), ValidatorId::Index(i as u64), ]; @@ -2463,7 +2463,7 @@ impl ApiTester { let index = state .get_beacon_proposer_index(slot, &self.chain.spec) .unwrap(); - let pubkey = state.validators()[index].pubkey.clone().into(); + let pubkey = *state.validators().get(index).unwrap().pubkey; ProposerData { pubkey, diff --git a/beacon_node/operation_pool/src/attestation.rs b/beacon_node/operation_pool/src/attestation.rs index 97c291aa855..9133157dbe8 100644 --- a/beacon_node/operation_pool/src/attestation.rs +++ b/beacon_node/operation_pool/src/attestation.rs @@ -47,18 +47,17 @@ impl<'a, T: EthSpec> AttMaxCover<'a, T> { .get_beacon_committee(att.data.slot, att.data.index) .ok()?; let indices = get_attesting_indices::(committee.committee, &fresh_validators).ok()?; + let sqrt_total_active_balance = base::SqrtTotalActiveBalance::new(total_active_balance); let fresh_validators_rewards: HashMap = indices .iter() .copied() .flat_map(|validator_index| { - let reward = base::get_base_reward( - state, - validator_index as usize, - total_active_balance, - spec, - ) - .ok()? - .checked_div(spec.proposer_reward_quotient)?; + let effective_balance = + state.get_effective_balance(validator_index as usize).ok()?; + let reward = + base::get_base_reward(effective_balance, sqrt_total_active_balance, spec) + .ok()? + .checked_div(spec.proposer_reward_quotient)?; Some((validator_index, reward)) }) .collect(); @@ -99,8 +98,11 @@ impl<'a, T: EthSpec> AttMaxCover<'a, T> { let mut proposer_reward_numerator = 0; + // FIXME(sproul): store base_reward in reward cache + // let effective_balance = reward_cache.get_effective_balance(index)?; + let effective_balance = state.get_effective_balance(index as usize).ok()?; let base_reward = - altair::get_base_reward(state, index as usize, base_reward_per_increment, spec) + altair::get_base_reward(effective_balance, base_reward_per_increment, spec) .ok()?; for (flag_index, weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { diff --git a/beacon_node/store/src/partial_beacon_state.rs b/beacon_node/store/src/partial_beacon_state.rs index 1fb5751a0a9..f697f149794 100644 --- a/beacon_node/store/src/partial_beacon_state.rs +++ b/beacon_node/store/src/partial_beacon_state.rs @@ -399,6 +399,8 @@ macro_rules! impl_try_into_beacon_state { committee_caches: <_>::default(), pubkey_cache: <_>::default(), exit_cache: <_>::default(), + slashings_cache: <_>::default(), + epoch_cache: <_>::default(), tree_hash_cache: <_>::default(), // Variant-specific fields diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 865a5affbb9..93dc4e3c4f8 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1,15 +1,10 @@ use crate::{ForkChoiceStore, InvalidationOperation}; -use per_epoch_processing::altair::participation_cache::Error as ParticipationCacheError; use proto_array::{ Block as ProtoBlock, DisallowedReOrgOffsets, ExecutionStatus, ProposerHeadError, ProposerHeadInfo, ProtoArrayForkChoice, ReOrgThreshold, }; -use slog::{crit, debug, error, warn, Logger}; +use slog::{crit, debug, warn, Logger}; use ssz_derive::{Decode, Encode}; -use state_processing::per_epoch_processing::altair::ParticipationCache; -use state_processing::per_epoch_processing::{ - weigh_justification_and_finalization, JustificationAndFinalizationState, -}; use state_processing::{ per_block_processing::errors::AttesterSlashingValidationError, per_epoch_processing, }; @@ -17,13 +12,13 @@ use std::cmp::Ordering; use std::collections::BTreeSet; use std::marker::PhantomData; use std::time::Duration; +use types::ProgressiveBalancesMode; use types::{ consts::merge::INTERVALS_PER_SLOT, AbstractExecPayload, AttestationShufflingId, AttesterSlashing, BeaconBlockRef, BeaconState, BeaconStateError, ChainSpec, Checkpoint, Epoch, EthSpec, ExecPayload, ExecutionBlockHash, Hash256, IndexedAttestation, RelativeEpoch, SignedBeaconBlock, Slot, }; -use types::{ProgressiveBalancesCache, ProgressiveBalancesMode}; #[derive(Debug)] pub enum Error { @@ -77,8 +72,6 @@ pub enum Error { proposer_boost_root: Hash256, }, UnrealizedVoteProcessing(state_processing::EpochProcessingError), - ParticipationCacheBuild(BeaconStateError), - ParticipationCacheError(ParticipationCacheError), ValidatorStatuses(BeaconStateError), ProgressiveBalancesCacheCheckFailed(String), } @@ -107,12 +100,6 @@ impl From for Error { } } -impl From for Error { - fn from(e: ParticipationCacheError) -> Self { - Error::ParticipationCacheError(e) - } -} - #[derive(Debug, Clone, Copy)] /// Controls how fork choice should behave when restoring from a persisted fork choice. pub enum ResetPayloadStatuses { @@ -650,6 +637,7 @@ where /// The supplied block **must** pass the `state_transition` function as it will not be run /// here. #[allow(clippy::too_many_arguments)] + // FIXME(sproul): remove progressive balances mode pub fn on_block>( &mut self, system_time_current_slot: Slot, @@ -658,9 +646,9 @@ where block_delay: Duration, state: &BeaconState, payload_verification_status: PayloadVerificationStatus, - progressive_balances_mode: ProgressiveBalancesMode, + _progressive_balances_mode: ProgressiveBalancesMode, spec: &ChainSpec, - log: &Logger, + _log: &Logger, ) -> Result<(), Error> { // If this block has already been processed we do not need to reprocess it. // We check this immediately in case re-processing the block mutates some property of the @@ -755,84 +743,38 @@ where parent_justified.epoch == block_epoch && parent_finalized.epoch + 1 == block_epoch }); - let (unrealized_justified_checkpoint, unrealized_finalized_checkpoint) = if let Some(( - parent_justified, - parent_finalized, - )) = - parent_checkpoints - { - (parent_justified, parent_finalized) - } else { - let justification_and_finalization_state = match block { - BeaconBlockRef::Deneb(_) - | BeaconBlockRef::Capella(_) - | BeaconBlockRef::Merge(_) - | BeaconBlockRef::Altair(_) => match progressive_balances_mode { - ProgressiveBalancesMode::Disabled => { - let participation_cache = ParticipationCache::new(state, spec) - .map_err(Error::ParticipationCacheBuild)?; - per_epoch_processing::altair::process_justification_and_finalization( + let (unrealized_justified_checkpoint, unrealized_finalized_checkpoint) = + if let Some((parent_justified, parent_finalized)) = parent_checkpoints { + (parent_justified, parent_finalized) + } else { + let justification_and_finalization_state = match block { + BeaconBlockRef::Deneb(_) + | BeaconBlockRef::Capella(_) + | BeaconBlockRef::Merge(_) + | BeaconBlockRef::Altair(_) => { + // FIXME(sproul): initialize progressive balances + per_epoch_processing::altair::process_justification_and_finalization(state)? + } + BeaconBlockRef::Base(_) => { + let mut validator_statuses = + per_epoch_processing::base::ValidatorStatuses::new(state, spec) + .map_err(Error::ValidatorStatuses)?; + validator_statuses + .process_attestations(state) + .map_err(Error::ValidatorStatuses)?; + per_epoch_processing::base::process_justification_and_finalization( state, - &participation_cache, + &validator_statuses.total_balances, + spec, )? } - ProgressiveBalancesMode::Fast - | ProgressiveBalancesMode::Checked - | ProgressiveBalancesMode::Strict => { - let maybe_participation_cache = progressive_balances_mode - .perform_comparative_checks() - .then(|| { - ParticipationCache::new(state, spec) - .map_err(Error::ParticipationCacheBuild) - }) - .transpose()?; - - process_justification_and_finalization_from_progressive_cache::( - state, - maybe_participation_cache.as_ref(), - ) - .or_else(|e| { - if progressive_balances_mode != ProgressiveBalancesMode::Strict { - error!( - log, - "Processing with progressive balances cache failed"; - "info" => "falling back to the non-optimized processing method", - "error" => ?e, - ); - let participation_cache = maybe_participation_cache - .map(Ok) - .unwrap_or_else(|| ParticipationCache::new(state, spec)) - .map_err(Error::ParticipationCacheBuild)?; - per_epoch_processing::altair::process_justification_and_finalization( - state, - &participation_cache, - ).map_err(Error::from) - } else { - Err(e) - } - })? - } - }, - BeaconBlockRef::Base(_) => { - let mut validator_statuses = - per_epoch_processing::base::ValidatorStatuses::new(state, spec) - .map_err(Error::ValidatorStatuses)?; - validator_statuses - .process_attestations(state) - .map_err(Error::ValidatorStatuses)?; - per_epoch_processing::base::process_justification_and_finalization( - state, - &validator_statuses.total_balances, - spec, - )? - } - }; + }; - ( - justification_and_finalization_state.current_justified_checkpoint(), - justification_and_finalization_state.finalized_checkpoint(), - ) - }; + ( + justification_and_finalization_state.current_justified_checkpoint(), + justification_and_finalization_state.finalized_checkpoint(), + ) + }; // Update best known unrealized justified & finalized checkpoints if unrealized_justified_checkpoint.epoch @@ -1558,92 +1500,6 @@ where } } -/// Process justification and finalization using progressive cache. Also performs a comparative -/// check against the `ParticipationCache` if it is supplied. -/// -/// Returns an error if the cache is not initialized or if there is a mismatch on the comparative check. -fn process_justification_and_finalization_from_progressive_cache( - state: &BeaconState, - maybe_participation_cache: Option<&ParticipationCache>, -) -> Result, Error> -where - E: EthSpec, - T: ForkChoiceStore, -{ - let justification_and_finalization_state = JustificationAndFinalizationState::new(state); - if state.current_epoch() <= E::genesis_epoch() + 1 { - return Ok(justification_and_finalization_state); - } - - // Load cached balances - let progressive_balances_cache: &ProgressiveBalancesCache = state.progressive_balances_cache(); - let previous_target_balance = - progressive_balances_cache.previous_epoch_target_attesting_balance()?; - let current_target_balance = - progressive_balances_cache.current_epoch_target_attesting_balance()?; - let total_active_balance = state.get_total_active_balance()?; - - if let Some(participation_cache) = maybe_participation_cache { - check_progressive_balances::( - state, - participation_cache, - previous_target_balance, - current_target_balance, - total_active_balance, - )?; - } - - weigh_justification_and_finalization( - justification_and_finalization_state, - total_active_balance, - previous_target_balance, - current_target_balance, - ) - .map_err(Error::from) -} - -/// Perform comparative checks against `ParticipationCache`, will return error if there's a mismatch. -fn check_progressive_balances( - state: &BeaconState, - participation_cache: &ParticipationCache, - cached_previous_target_balance: u64, - cached_current_target_balance: u64, - cached_total_active_balance: u64, -) -> Result<(), Error> -where - E: EthSpec, - T: ForkChoiceStore, -{ - let slot = state.slot(); - let epoch = state.current_epoch(); - - // Check previous epoch target balances - let previous_target_balance = participation_cache.previous_epoch_target_attesting_balance()?; - if previous_target_balance != cached_previous_target_balance { - return Err(Error::ProgressiveBalancesCacheCheckFailed( - format!("Previous epoch target attesting balance mismatch, slot: {}, epoch: {}, actual: {}, cached: {}", slot, epoch, previous_target_balance, cached_previous_target_balance) - )); - } - - // Check current epoch target balances - let current_target_balance = participation_cache.current_epoch_target_attesting_balance()?; - if current_target_balance != cached_current_target_balance { - return Err(Error::ProgressiveBalancesCacheCheckFailed( - format!("Current epoch target attesting balance mismatch, slot: {}, epoch: {}, actual: {}, cached: {}", slot, epoch, current_target_balance, cached_current_target_balance) - )); - } - - // Check current epoch total balances - let total_active_balance = participation_cache.current_epoch_total_active_balance(); - if total_active_balance != cached_total_active_balance { - return Err(Error::ProgressiveBalancesCacheCheckFailed( - format!("Current epoch total active balance mismatch, slot: {}, epoch: {}, actual: {}, cached: {}", slot, epoch, total_active_balance, cached_total_active_balance) - )); - } - - Ok(()) -} - /// Helper struct that is used to encode/decode the state of the `ForkChoice` as SSZ bytes. /// /// This is used when persisting the state of the fork choice to disk. diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 649fbcc5559..5305c13ae62 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -401,7 +401,7 @@ impl ForkChoiceTest { let state_root = harness .chain .store - .get_blinded_block(&fc.fc_store().justified_checkpoint().root) + .get_blinded_block(&fc.fc_store().justified_checkpoint().root, None) .unwrap() .unwrap() .message() @@ -417,7 +417,7 @@ impl ForkChoiceTest { .into_iter() .map(|v| { if v.is_active_at(state.current_epoch()) { - v.effective_balance + v.effective_balance() } else { 0 } diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index 7279fd28fa2..3daa944979a 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -28,6 +28,7 @@ arbitrary = { workspace = true } lighthouse_metrics = { workspace = true } lazy_static = { workspace = true } derivative = { workspace = true } +vec_map = "0.8.2" [features] default = ["legacy-arith"] diff --git a/consensus/state_processing/src/all_caches.rs b/consensus/state_processing/src/all_caches.rs new file mode 100644 index 00000000000..bd64d9f6643 --- /dev/null +++ b/consensus/state_processing/src/all_caches.rs @@ -0,0 +1,54 @@ +use crate::common::update_progressive_balances_cache::initialize_progressive_balances_cache; +use crate::epoch_cache::initialize_epoch_cache; +use types::{BeaconState, ChainSpec, EpochCacheError, EthSpec, Hash256, RelativeEpoch}; + +/// Mixin trait for the beacon state that provides operations on *all* caches. +/// +/// The reason this trait exists here away from `BeaconState` itself is that some caches are +/// computed by functions in `state_processing`. +pub trait AllCaches { + /// Build all caches. + /// + /// Note that this excludes milhouse's intrinsic tree-hash cache. That needs to be managed + /// separately. + fn build_all_caches(&mut self, spec: &ChainSpec) -> Result<(), EpochCacheError>; + + /// Return true if all caches are built. + /// + /// Note that this excludes milhouse's intrinsic tree-hash cache. That needs to be managed + /// separately. + fn all_caches_built(&self) -> bool; +} + +impl AllCaches for BeaconState { + fn build_all_caches(&mut self, spec: &ChainSpec) -> Result<(), EpochCacheError> { + self.build_caches(spec)?; + initialize_epoch_cache(self, spec)?; + initialize_progressive_balances_cache(self, None, spec)?; + Ok(()) + } + + fn all_caches_built(&self) -> bool { + let current_epoch = self.current_epoch(); + let Ok(epoch_cache_decision_block_root) = + self.proposer_shuffling_decision_root(Hash256::zero()) + else { + return false; + }; + self.get_total_active_balance_at_epoch(current_epoch) + .is_ok() + && self.committee_cache_is_initialized(RelativeEpoch::Previous) + && self.committee_cache_is_initialized(RelativeEpoch::Current) + && self.committee_cache_is_initialized(RelativeEpoch::Next) + && self + .progressive_balances_cache() + .is_initialized_at(current_epoch) + && self.pubkey_cache().len() == self.validators().len() + && self.exit_cache().check_initialized().is_ok() + && self.slashings_cache_is_initialized() + && self + .epoch_cache() + .check_validity::(current_epoch, epoch_cache_decision_block_root) + .is_ok() + } +} diff --git a/consensus/state_processing/src/common/altair.rs b/consensus/state_processing/src/common/altair.rs index 8943ef2f40b..43801541336 100644 --- a/consensus/state_processing/src/common/altair.rs +++ b/consensus/state_processing/src/common/altair.rs @@ -24,14 +24,12 @@ impl BaseRewardPerIncrement { /// shown to be a significant optimisation. /// /// Spec v1.1.0 -pub fn get_base_reward( - state: &BeaconState, - index: usize, +pub fn get_base_reward( + validator_effective_balance: u64, base_reward_per_increment: BaseRewardPerIncrement, spec: &ChainSpec, ) -> Result { - state - .get_effective_balance(index)? + validator_effective_balance .safe_div(spec.effective_balance_increment)? .safe_mul(base_reward_per_increment.as_u64()) .map_err(Into::into) diff --git a/consensus/state_processing/src/common/base.rs b/consensus/state_processing/src/common/base.rs index a8d04ad6cd4..3b08b783228 100644 --- a/consensus/state_processing/src/common/base.rs +++ b/consensus/state_processing/src/common/base.rs @@ -1,21 +1,32 @@ use integer_sqrt::IntegerSquareRoot; -use safe_arith::SafeArith; +use safe_arith::{ArithError, SafeArith}; use types::*; +/// This type exists to avoid confusing `total_active_balance` with `sqrt_total_active_balance`, +/// since they are used in close proximity and the same type (`u64`). +#[derive(Copy, Clone)] +pub struct SqrtTotalActiveBalance(u64); + +impl SqrtTotalActiveBalance { + pub fn new(total_active_balance: u64) -> Self { + Self(total_active_balance.integer_sqrt()) + } + + pub fn as_u64(&self) -> u64 { + self.0 + } +} + /// Returns the base reward for some validator. -pub fn get_base_reward( - state: &BeaconState, - index: usize, - // Should be == get_total_active_balance(state, spec) - total_active_balance: u64, +pub fn get_base_reward( + validator_effective_balance: u64, + sqrt_total_active_balance: SqrtTotalActiveBalance, spec: &ChainSpec, -) -> Result { - state - .get_effective_balance(index)? +) -> Result { + validator_effective_balance .safe_mul(spec.base_reward_factor)? - .safe_div(total_active_balance.integer_sqrt())? + .safe_div(sqrt_total_active_balance.as_u64())? .safe_div(spec.base_rewards_per_epoch) - .map_err(Into::into) } pub fn get_base_reward_from_effective_balance( diff --git a/consensus/state_processing/src/common/initiate_validator_exit.rs b/consensus/state_processing/src/common/initiate_validator_exit.rs index 85e5e1df1db..f8c159b2b64 100644 --- a/consensus/state_processing/src/common/initiate_validator_exit.rs +++ b/consensus/state_processing/src/common/initiate_validator_exit.rs @@ -8,10 +8,10 @@ pub fn initiate_validator_exit( index: usize, spec: &ChainSpec, ) -> Result<(), Error> { - // Return if the validator already initiated exit - if state.get_validator(index)?.exit_epoch != spec.far_future_epoch { - return Ok(()); - } + // We do things in a slightly different order to the spec here. Instead of immediately checking + // whether the validator has already exited, we instead prepare the exit cache and compute the + // cheap-to-calculate values from that. *Then* we look up the validator a single time in the + // validator tree (expensive), make the check and mutate as appropriate. // Ensure the exit cache is built. state.build_exit_cache(spec)?; @@ -28,12 +28,20 @@ pub fn initiate_validator_exit( exit_queue_epoch.safe_add_assign(1)?; } + let validator = state.get_validator_mut(index)?; + + // Return if the validator already initiated exit + if validator.exit_epoch != spec.far_future_epoch { + return Ok(()); + } + + validator.exit_epoch = exit_queue_epoch; + validator.withdrawable_epoch = + exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?; + state .exit_cache_mut() .record_validator_exit(exit_queue_epoch)?; - state.get_validator_mut(index)?.exit_epoch = exit_queue_epoch; - state.get_validator_mut(index)?.withdrawable_epoch = - exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?; Ok(()) } diff --git a/consensus/state_processing/src/common/mod.rs b/consensus/state_processing/src/common/mod.rs index ffe8be3a041..cefc47b0235 100644 --- a/consensus/state_processing/src/common/mod.rs +++ b/consensus/state_processing/src/common/mod.rs @@ -25,8 +25,7 @@ pub fn increase_balance( index: usize, delta: u64, ) -> Result<(), BeaconStateError> { - state.get_balance_mut(index)?.safe_add_assign(delta)?; - Ok(()) + increase_balance_directly(state.get_balance_mut(index)?, delta) } /// Decrease the balance of a validator, saturating upon overflow, as per the spec. @@ -35,7 +34,17 @@ pub fn decrease_balance( index: usize, delta: u64, ) -> Result<(), BeaconStateError> { - let balance = state.get_balance_mut(index)?; + decrease_balance_directly(state.get_balance_mut(index)?, delta) +} + +/// Increase the balance of a validator, erroring upon overflow, as per the spec. +pub fn increase_balance_directly(balance: &mut u64, delta: u64) -> Result<(), BeaconStateError> { + balance.safe_add_assign(delta)?; + Ok(()) +} + +/// Decrease the balance of a validator, saturating upon overflow, as per the spec. +pub fn decrease_balance_directly(balance: &mut u64, delta: u64) -> Result<(), BeaconStateError> { *balance = balance.saturating_sub(delta); Ok(()) } diff --git a/consensus/state_processing/src/common/slash_validator.rs b/consensus/state_processing/src/common/slash_validator.rs index d8b1c1a1076..3d11b1fb3be 100644 --- a/consensus/state_processing/src/common/slash_validator.rs +++ b/consensus/state_processing/src/common/slash_validator.rs @@ -20,6 +20,7 @@ pub fn slash_validator( spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { let epoch = state.current_epoch(); + let latest_block_slot = state.latest_block_header().slot; initiate_validator_exit(state, slashed_index, spec)?; @@ -44,7 +45,10 @@ pub fn slash_validator( .safe_div(spec.min_slashing_penalty_quotient_for_state(state))?, )?; - update_progressive_balances_on_slashing(state, slashed_index)?; + update_progressive_balances_on_slashing(state, slashed_index, validator_effective_balance)?; + state + .slashings_cache_mut() + .record_validator_slashing(latest_block_slot, slashed_index)?; // Apply proposer and whistleblower rewards let proposer_index = ctxt.get_proposer_index(state, spec)? as usize; diff --git a/consensus/state_processing/src/common/update_progressive_balances_cache.rs b/consensus/state_processing/src/common/update_progressive_balances_cache.rs index 45b5d657a6e..fb65e583ba8 100644 --- a/consensus/state_processing/src/common/update_progressive_balances_cache.rs +++ b/consensus/state_processing/src/common/update_progressive_balances_cache.rs @@ -6,12 +6,10 @@ use crate::metrics::{ use crate::per_epoch_processing::altair::ParticipationCache; use crate::{BlockProcessingError, EpochProcessingError}; use lighthouse_metrics::set_gauge; -use ssz_types::VariableList; use std::borrow::Cow; -use types::consts::altair::TIMELY_TARGET_FLAG_INDEX; use types::{ - is_progressive_balances_enabled, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, - ParticipationFlags, ProgressiveBalancesCache, + is_progressive_balances_enabled, BeaconState, BeaconStateError, ChainSpec, Epoch, + EpochTotalBalances, EthSpec, ProgressiveBalancesCache, }; /// Initializes the `ProgressiveBalancesCache` cache using balance values from the @@ -28,24 +26,34 @@ pub fn initialize_progressive_balances_cache( return Ok(()); } + // FIXME(sproul): simplify the participation cache let participation_cache = match maybe_participation_cache { Some(cache) => Cow::Borrowed(cache), - None => Cow::Owned(ParticipationCache::new(state, spec)?), + None => { + state.build_total_active_balance_cache_at(state.current_epoch(), spec)?; + Cow::Owned( + ParticipationCache::new(state, spec) + .map_err(|e| BeaconStateError::ParticipationCacheError(format!("{e:?}")))?, + ) + } }; - let previous_epoch_target_attesting_balance = participation_cache - .previous_epoch_target_attesting_balance_raw() - .map_err(|e| BeaconStateError::ParticipationCacheError(format!("{e:?}")))?; - - let current_epoch_target_attesting_balance = participation_cache - .current_epoch_target_attesting_balance_raw() - .map_err(|e| BeaconStateError::ParticipationCacheError(format!("{e:?}")))?; - let current_epoch = state.current_epoch(); + let previous_epoch_cache = EpochTotalBalances { + total_flag_balances: participation_cache + .previous_epoch_participation + .total_flag_balances, + }; + let current_epoch_cache = EpochTotalBalances { + total_flag_balances: participation_cache + .current_epoch_participation + .total_flag_balances, + }; + state.progressive_balances_cache_mut().initialize( current_epoch, - previous_epoch_target_attesting_balance, - current_epoch_target_attesting_balance, + previous_epoch_cache, + current_epoch_cache, ); update_progressive_balances_metrics(state.progressive_balances_cache())?; @@ -57,16 +65,17 @@ pub fn initialize_progressive_balances_cache( pub fn update_progressive_balances_on_attestation( state: &mut BeaconState, epoch: Epoch, - validator_index: usize, + flag_index: usize, + validator_effective_balance: u64, + validator_slashed: bool, ) -> Result<(), BlockProcessingError> { if is_progressive_balances_enabled(state) { - let validator = state.get_validator(validator_index)?; - if !validator.slashed { - let validator_effective_balance = validator.effective_balance; - state - .progressive_balances_cache_mut() - .on_new_target_attestation(epoch, validator_effective_balance)?; - } + state.progressive_balances_cache_mut().on_new_attestation( + epoch, + validator_slashed, + flag_index, + validator_effective_balance, + )?; } Ok(()) } @@ -75,21 +84,22 @@ pub fn update_progressive_balances_on_attestation( pub fn update_progressive_balances_on_slashing( state: &mut BeaconState, validator_index: usize, + validator_effective_balance: u64, ) -> Result<(), BlockProcessingError> { if is_progressive_balances_enabled(state) { - let previous_epoch_participation = state.previous_epoch_participation()?; - let is_previous_epoch_target_attester = - is_target_attester_in_epoch::(previous_epoch_participation, validator_index)?; - - let current_epoch_participation = state.current_epoch_participation()?; - let is_current_epoch_target_attester = - is_target_attester_in_epoch::(current_epoch_participation, validator_index)?; + let previous_epoch_participation = *state + .previous_epoch_participation()? + .get(validator_index) + .ok_or(BeaconStateError::UnknownValidator(validator_index))?; - let validator_effective_balance = state.get_effective_balance(validator_index)?; + let current_epoch_participation = *state + .current_epoch_participation()? + .get(validator_index) + .ok_or(BeaconStateError::UnknownValidator(validator_index))?; state.progressive_balances_cache_mut().on_slashing( - is_previous_epoch_target_attester, - is_current_epoch_target_attester, + previous_epoch_participation, + current_epoch_participation, validator_effective_balance, )?; } @@ -128,15 +138,3 @@ pub fn update_progressive_balances_metrics( Ok(()) } - -fn is_target_attester_in_epoch( - epoch_participation: &VariableList, - validator_index: usize, -) -> Result { - let participation_flags = epoch_participation - .get(validator_index) - .ok_or(BeaconStateError::UnknownValidator(validator_index))?; - participation_flags - .has_flag(TIMELY_TARGET_FLAG_INDEX) - .map_err(|e| e.into()) -} diff --git a/consensus/state_processing/src/consensus_context.rs b/consensus/state_processing/src/consensus_context.rs index 8e49a0d4983..a2877fca195 100644 --- a/consensus/state_processing/src/consensus_context.rs +++ b/consensus/state_processing/src/consensus_context.rs @@ -1,5 +1,6 @@ use crate::common::get_indexed_attestation; use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError}; +use crate::EpochCacheError; use ssz_derive::{Decode, Encode}; use std::collections::{hash_map::Entry, HashMap}; use tree_hash::TreeHash; @@ -12,6 +13,10 @@ use types::{ pub struct ConsensusContext { /// Slot to act as an identifier/safeguard slot: Slot, + /// Previous epoch of the `slot` precomputed for optimization purpose. + pub(crate) previous_epoch: Epoch, + /// Current epoch of the `slot` precomputed for optimization purpose. + pub(crate) current_epoch: Epoch, /// Proposer index of the block at `slot`. proposer_index: Option, /// Block root of the block at `slot`. @@ -26,6 +31,7 @@ pub struct ConsensusContext { #[derive(Debug, PartialEq, Clone)] pub enum ContextError { BeaconState(BeaconStateError), + EpochCache(EpochCacheError), SlotMismatch { slot: Slot, expected: Slot }, EpochMismatch { epoch: Epoch, expected: Epoch }, } @@ -36,10 +42,20 @@ impl From for ContextError { } } +impl From for ContextError { + fn from(e: EpochCacheError) -> Self { + Self::EpochCache(e) + } +} + impl ConsensusContext { pub fn new(slot: Slot) -> Self { + let current_epoch = slot.epoch(T::slots_per_epoch()); + let previous_epoch = current_epoch.saturating_sub(1u64); Self { slot, + previous_epoch, + current_epoch, proposer_index: None, current_block_root: None, indexed_attestations: HashMap::new(), diff --git a/consensus/state_processing/src/epoch_cache.rs b/consensus/state_processing/src/epoch_cache.rs new file mode 100644 index 00000000000..de0782d762e --- /dev/null +++ b/consensus/state_processing/src/epoch_cache.rs @@ -0,0 +1,121 @@ +use crate::common::altair::BaseRewardPerIncrement; +use crate::common::base::SqrtTotalActiveBalance; +use crate::common::{altair, base}; +use safe_arith::SafeArith; +use types::epoch_cache::{EpochCache, EpochCacheError, EpochCacheKey}; +use types::{ActivationQueue, BeaconState, ChainSpec, EthSpec, ForkName, Hash256}; + +/// Precursor to an `EpochCache`. +pub struct PreEpochCache { + epoch_key: EpochCacheKey, + effective_balances: Vec, +} + +impl PreEpochCache { + pub fn new_for_next_epoch(state: &BeaconState) -> Result { + // The decision block root for the next epoch is the latest block root from this epoch. + let latest_block_header = state.latest_block_header(); + + // State root should already have been filled in by `process_slot`, except in the case + // of a `partial_state_advance`. + let decision_block_root = latest_block_header.canonical_root(); + + let epoch_key = EpochCacheKey { + epoch: state.next_epoch()?, + decision_block_root, + }; + + Ok(Self { + epoch_key, + effective_balances: Vec::with_capacity(state.validators().len()), + }) + } + + pub fn push_effective_balance(&mut self, effective_balance: u64) { + self.effective_balances.push(effective_balance); + } + + pub fn into_epoch_cache( + self, + total_active_balance: u64, + activation_queue: ActivationQueue, + spec: &ChainSpec, + ) -> Result { + let epoch = self.epoch_key.epoch; + let sqrt_total_active_balance = SqrtTotalActiveBalance::new(total_active_balance); + let base_reward_per_increment = BaseRewardPerIncrement::new(total_active_balance, spec)?; + + let effective_balance_increment = spec.effective_balance_increment; + let max_effective_balance_eth = spec + .max_effective_balance + .safe_div(effective_balance_increment)?; + + let mut base_rewards = Vec::with_capacity(max_effective_balance_eth.safe_add(1)? as usize); + + for effective_balance_eth in 0..=max_effective_balance_eth { + let effective_balance = effective_balance_eth.safe_mul(effective_balance_increment)?; + let base_reward = if spec.fork_name_at_epoch(epoch) == ForkName::Base { + base::get_base_reward(effective_balance, sqrt_total_active_balance, spec)? + } else { + altair::get_base_reward(effective_balance, base_reward_per_increment, spec)? + }; + base_rewards.push(base_reward); + } + + Ok(EpochCache::new( + self.epoch_key, + self.effective_balances, + base_rewards, + activation_queue, + spec, + )) + } +} + +pub fn initialize_epoch_cache( + state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), EpochCacheError> { + let current_epoch = state.current_epoch(); + let next_epoch = state.next_epoch().map_err(EpochCacheError::BeaconState)?; + let epoch_cache: &EpochCache = state.epoch_cache(); + let decision_block_root = state + .proposer_shuffling_decision_root(Hash256::zero()) + .map_err(EpochCacheError::BeaconState)?; + + if epoch_cache + .check_validity::(current_epoch, decision_block_root) + .is_ok() + { + // `EpochCache` has already been initialized and is valid, no need to initialize. + return Ok(()); + } + + state.build_total_active_balance_cache_at(current_epoch, spec)?; + let total_active_balance = state.get_total_active_balance_at_epoch(current_epoch)?; + + // Collect effective balances and compute activation queue. + let mut effective_balances = Vec::with_capacity(state.validators().len()); + let mut activation_queue = ActivationQueue::default(); + + for (index, validator) in state.validators().iter().enumerate() { + effective_balances.push(validator.effective_balance); + + // Add to speculative activation queue. + activation_queue + .add_if_could_be_eligible_for_activation(index, validator, next_epoch, spec); + } + + // Compute base rewards. + let pre_epoch_cache = PreEpochCache { + epoch_key: EpochCacheKey { + epoch: current_epoch, + decision_block_root, + }, + effective_balances, + }; + *state.epoch_cache_mut() = + pre_epoch_cache.into_epoch_cache(total_active_balance, activation_queue, spec)?; + + Ok(()) +} diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index 284a7019f34..288f44162a1 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -135,7 +135,8 @@ pub fn process_activations( spec: &ChainSpec, ) -> Result<(), Error> { let (validators, balances, _) = state.validators_and_balances_and_progressive_balances_mut(); - for (index, validator) in validators.iter_mut().enumerate() { + let mut validators_iter = validators.iter_mut().enumerate(); + while let Some((index, validator)) = validators_iter.next() { let balance = balances .get(index) .copied() diff --git a/consensus/state_processing/src/lib.rs b/consensus/state_processing/src/lib.rs index a3ee7254062..7d84c426e8c 100644 --- a/consensus/state_processing/src/lib.rs +++ b/consensus/state_processing/src/lib.rs @@ -16,9 +16,11 @@ mod macros; mod metrics; +pub mod all_caches; pub mod block_replayer; pub mod common; pub mod consensus_context; +pub mod epoch_cache; pub mod genesis; pub mod per_block_processing; pub mod per_epoch_processing; @@ -27,6 +29,7 @@ pub mod state_advance; pub mod upgrade; pub mod verify_operation; +pub use all_caches::AllCaches; pub use block_replayer::{BlockReplayError, BlockReplayer, StateProcessingStrategy}; pub use consensus_context::{ConsensusContext, ContextError}; pub use genesis::{ @@ -41,4 +44,5 @@ pub use per_epoch_processing::{ errors::EpochProcessingError, process_epoch as per_epoch_processing, }; pub use per_slot_processing::{per_slot_processing, Error as SlotProcessingError}; +pub use types::{EpochCache, EpochCacheError, EpochCacheKey}; pub use verify_operation::{SigVerifiedOp, VerifyOperation, VerifyOperationAt}; diff --git a/consensus/state_processing/src/metrics.rs b/consensus/state_processing/src/metrics.rs index d8a51135e85..891689921ad 100644 --- a/consensus/state_processing/src/metrics.rs +++ b/consensus/state_processing/src/metrics.rs @@ -21,6 +21,13 @@ lazy_static! { "beacon_participation_prev_epoch_active_gwei_total", "Total effective balance (gwei) of validators active in the previous epoch" ); + /* + * Processing metrics + */ + pub static ref PROCESS_EPOCH_TIME: Result = try_create_histogram( + "beacon_state_processing_process_epoch", + "Time required for process_epoch", + ); /* * Participation Metrics (progressive balances) */ diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index b9a147a5ad5..c994066c5d9 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -45,6 +45,7 @@ use crate::StateProcessingStrategy; use crate::common::update_progressive_balances_cache::{ initialize_progressive_balances_cache, update_progressive_balances_metrics, }; +use crate::epoch_cache::initialize_epoch_cache; #[cfg(feature = "arbitrary-fuzz")] use arbitrary::Arbitrary; @@ -118,7 +119,10 @@ pub fn per_block_processing>( .fork_name(spec) .map_err(BlockProcessingError::InconsistentStateFork)?; + // Build epoch cache if it hasn't already been built, or if it is no longer valid + initialize_epoch_cache(state, spec)?; initialize_progressive_balances_cache(state, None, spec)?; + state.build_slashings_cache()?; let verify_signatures = match block_signature_strategy { BlockSignatureStrategy::VerifyBulk => { @@ -240,6 +244,9 @@ pub fn process_block_header( ); } + state + .slashings_cache_mut() + .update_latest_block_slot(block_header.slot); *state.latest_block_header_mut() = block_header; // Verify proposer is not slashed diff --git a/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs b/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs index a5dcd6e0b61..7773bc8ac35 100644 --- a/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs +++ b/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs @@ -47,17 +47,20 @@ pub fn process_sync_aggregate( // Apply participant and proposer rewards let committee_indices = state.get_sync_committee_indices(¤t_sync_committee)?; + let mut total_proposer_reward = 0; for (participant_index, participation_bit) in committee_indices .into_iter() .zip(aggregate.sync_committee_bits.iter()) { + // FIXME(sproul): double-check this for Capella, proposer shouldn't have 0 effective balance if participation_bit { increase_balance(state, participant_index, participant_reward)?; - increase_balance(state, proposer_index as usize, proposer_reward)?; + total_proposer_reward.safe_add_assign(proposer_reward)?; } else { decrease_balance(state, participant_index, participant_reward)?; } } + increase_balance(state, proposer_index as usize, total_proposer_reward)?; Ok(()) } diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index de1c132951e..4576853af5c 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -1,6 +1,6 @@ use super::signature_sets::Error as SignatureSetError; use crate::per_epoch_processing::altair::participation_cache; -use crate::ContextError; +use crate::{ContextError, EpochCacheError}; use merkle_proof::MerkleTreeError; use participation_cache::Error as ParticipationCacheError; use safe_arith::ArithError; @@ -84,6 +84,7 @@ pub enum BlockProcessingError { }, ExecutionInvalid, ConsensusContext(ContextError), + EpochCacheError(EpochCacheError), WithdrawalsRootMismatch { expected: Hash256, found: Hash256, @@ -134,6 +135,12 @@ impl From for BlockProcessingError { } } +impl From for BlockProcessingError { + fn from(e: EpochCacheError) -> Self { + BlockProcessingError::EpochCacheError(e) + } +} + impl From> for BlockProcessingError { fn from(e: BlockOperationError) -> BlockProcessingError { match e { @@ -328,9 +335,11 @@ pub enum AttestationInvalid { /// /// `is_current` is `true` if the attestation was compared to the /// `state.current_justified_checkpoint`, `false` if compared to `state.previous_justified_checkpoint`. + /// + /// Checkpoints have been boxed to keep the error size down and prevent clippy failures. WrongJustifiedCheckpoint { - state: Checkpoint, - attestation: Checkpoint, + state: Box, + attestation: Box, is_current: bool, }, /// The aggregation bitfield length is not the smallest possible size to represent the committee. diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index cb24a7ba7ec..13d5873a6ff 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -1,6 +1,5 @@ use super::*; use crate::common::{ - altair::{get_base_reward, BaseRewardPerIncrement}, get_attestation_participation_flag_indices, increase_balance, initiate_validator_exit, slash_validator, }; @@ -98,7 +97,6 @@ pub mod base { pub mod altair_deneb { use super::*; use crate::common::update_progressive_balances_cache::update_progressive_balances_on_attestation; - use types::consts::altair::TIMELY_TARGET_FLAG_INDEX; pub fn process_attestations( state: &mut BeaconState, @@ -115,6 +113,7 @@ pub mod altair_deneb { }) } + #[allow(clippy::too_many_arguments)] pub fn process_attestation( state: &mut BeaconState, attestation: &Attestation, @@ -128,7 +127,7 @@ pub mod altair_deneb { let proposer_index = ctxt.get_proposer_index(state, spec)?; - let attesting_indices = &verify_attestation_for_block_inclusion( + let attesting_indices = verify_attestation_for_block_inclusion( state, attestation, ctxt, @@ -136,7 +135,8 @@ pub mod altair_deneb { spec, ) .map_err(|e| e.into_with_index(att_index))? - .attesting_indices; + .attesting_indices + .clone(); // Matching roots, participation flag indices let data = &attestation.data; @@ -145,32 +145,36 @@ pub mod altair_deneb { get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?; // Update epoch participation flags. - let total_active_balance = state.get_total_active_balance()?; - let base_reward_per_increment = BaseRewardPerIncrement::new(total_active_balance, spec)?; let mut proposer_reward_numerator = 0; - for index in attesting_indices { + for index in &attesting_indices { let index = *index as usize; + let validator_effective_balance = state.epoch_cache().get_effective_balance(index)?; + let validator_slashed = state.slashings_cache().is_slashed(index); + for (flag_index, &weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { - let epoch_participation = state.get_epoch_participation_mut(data.target.epoch)?; - let validator_participation = epoch_participation - .get_mut(index) - .ok_or(BeaconStateError::ParticipationOutOfBounds(index))?; - - if participation_flag_indices.contains(&flag_index) - && !validator_participation.has_flag(flag_index)? - { - validator_participation.add_flag(flag_index)?; - proposer_reward_numerator.safe_add_assign( - get_base_reward(state, index, base_reward_per_increment, spec)? - .safe_mul(weight)?, - )?; - - if flag_index == TIMELY_TARGET_FLAG_INDEX { + let epoch_participation = state.get_epoch_participation_mut( + data.target.epoch, + ctxt.previous_epoch, + ctxt.current_epoch, + )?; + + if participation_flag_indices.contains(&flag_index) { + let validator_participation = epoch_participation + .get_mut(index) + .ok_or(BeaconStateError::ParticipationOutOfBounds(index))?; + + if !validator_participation.has_flag(flag_index)? { + validator_participation.add_flag(flag_index)?; + proposer_reward_numerator + .safe_add_assign(state.get_base_reward(index)?.safe_mul(weight)?)?; + update_progressive_balances_on_attestation( state, data.target.epoch, - index, + flag_index, + validator_effective_balance, + validator_slashed, )?; } } @@ -198,6 +202,8 @@ pub fn process_proposer_slashings( ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { + state.build_slashings_cache()?; + // Verify and apply proposer slashings in series. // We have to verify in series because an invalid block may contain multiple slashings // for the same validator, and we need to correctly detect and reject that. @@ -231,6 +237,8 @@ pub fn process_attester_slashings( ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { + state.build_slashings_cache()?; + for (i, attester_slashing) in attester_slashings.iter().enumerate() { verify_attester_slashing(state, attester_slashing, verify_signatures, spec) .map_err(|e| e.into_with_index(i))?; diff --git a/consensus/state_processing/src/per_block_processing/tests.rs b/consensus/state_processing/src/per_block_processing/tests.rs index 83fd0f232ca..2a2b67e30da 100644 --- a/consensus/state_processing/src/per_block_processing/tests.rs +++ b/consensus/state_processing/src/per_block_processing/tests.rs @@ -451,8 +451,8 @@ async fn invalid_attestation_wrong_justified_checkpoint() { Err(BlockProcessingError::AttestationInvalid { index: 0, reason: AttestationInvalid::WrongJustifiedCheckpoint { - state: old_justified_checkpoint, - attestation: new_justified_checkpoint, + state: Box::new(old_justified_checkpoint), + attestation: Box::new(new_justified_checkpoint), is_current: true, } }) diff --git a/consensus/state_processing/src/per_block_processing/verify_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_attestation.rs index b7aa4643e48..bd598c38710 100644 --- a/consensus/state_processing/src/per_block_processing/verify_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/verify_attestation.rs @@ -102,8 +102,8 @@ fn verify_casper_ffg_vote( verify!( data.source == state.current_justified_checkpoint(), Invalid::WrongJustifiedCheckpoint { - state: state.current_justified_checkpoint(), - attestation: data.source, + state: Box::new(state.current_justified_checkpoint()), + attestation: Box::new(data.source), is_current: true, } ); @@ -112,8 +112,8 @@ fn verify_casper_ffg_vote( verify!( data.source == state.previous_justified_checkpoint(), Invalid::WrongJustifiedCheckpoint { - state: state.previous_justified_checkpoint(), - attestation: data.source, + state: Box::new(state.previous_justified_checkpoint()), + attestation: Box::new(data.source), is_current: false, } ); diff --git a/consensus/state_processing/src/per_epoch_processing.rs b/consensus/state_processing/src/per_epoch_processing.rs index d5d06037cd8..128a67867b8 100644 --- a/consensus/state_processing/src/per_epoch_processing.rs +++ b/consensus/state_processing/src/per_epoch_processing.rs @@ -1,13 +1,14 @@ #![deny(clippy::wildcard_imports)] -pub use epoch_processing_summary::EpochProcessingSummary; +use crate::metrics; +pub use epoch_processing_summary::{EpochProcessingSummary, ParticipationEpochSummary}; use errors::EpochProcessingError as Error; pub use justification_and_finalization_state::JustificationAndFinalizationState; use safe_arith::SafeArith; use types::{BeaconState, ChainSpec, EthSpec}; -pub use registry_updates::process_registry_updates; -pub use slashings::process_slashings; +pub use registry_updates::{process_registry_updates, process_registry_updates_slow}; +pub use slashings::{process_slashings, process_slashings_slow}; pub use weigh_justification_and_finalization::weigh_justification_and_finalization; pub mod altair; @@ -20,6 +21,7 @@ pub mod historical_roots_update; pub mod justification_and_finalization_state; pub mod registry_updates; pub mod resets; +pub mod single_pass; pub mod slashings; pub mod tests; pub mod weigh_justification_and_finalization; @@ -32,6 +34,8 @@ pub fn process_epoch( state: &mut BeaconState, spec: &ChainSpec, ) -> Result, Error> { + let _timer = metrics::start_timer(&metrics::PROCESS_EPOCH_TIME); + // Verify that the `BeaconState` instantiation matches the fork at `state.slot()`. state .fork_name(spec) @@ -39,8 +43,10 @@ pub fn process_epoch( match state { BeaconState::Base(_) => base::process_epoch(state, spec), - BeaconState::Altair(_) | BeaconState::Merge(_) => altair::process_epoch(state, spec), - BeaconState::Capella(_) | BeaconState::Deneb(_) => capella::process_epoch(state, spec), + BeaconState::Altair(_) + | BeaconState::Merge(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) => altair::process_epoch(state, spec), } } diff --git a/consensus/state_processing/src/per_epoch_processing/altair.rs b/consensus/state_processing/src/per_epoch_processing/altair.rs index 0abbd16a989..f5905aaa351 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair.rs @@ -1,17 +1,19 @@ -use super::{process_registry_updates, process_slashings, EpochProcessingSummary, Error}; +use super::{EpochProcessingSummary, Error}; use crate::common::update_progressive_balances_cache::{ initialize_progressive_balances_cache, update_progressive_balances_on_epoch_transition, }; +use crate::epoch_cache::initialize_epoch_cache; +use crate::per_epoch_processing::single_pass::{process_epoch_single_pass, SinglePassConfig}; use crate::per_epoch_processing::{ - effective_balance_updates::process_effective_balance_updates, + capella::process_historical_summaries_update, historical_roots_update::process_historical_roots_update, resets::{process_eth1_data_reset, process_randao_mixes_reset, process_slashings_reset}, }; -pub use inactivity_updates::process_inactivity_updates; +pub use inactivity_updates::process_inactivity_updates_slow; pub use justification_and_finalization::process_justification_and_finalization; pub use participation_cache::ParticipationCache; pub use participation_flag_updates::process_participation_flag_updates; -pub use rewards_and_penalties::process_rewards_and_penalties; +pub use rewards_and_penalties::process_rewards_and_penalties_slow; pub use sync_committee_updates::process_sync_committee_updates; use types::{BeaconState, ChainSpec, EthSpec, RelativeEpoch}; @@ -26,50 +28,51 @@ pub fn process_epoch( state: &mut BeaconState, spec: &ChainSpec, ) -> Result, Error> { - // Ensure the committee caches are built. + // Ensure the required caches are built. state.build_committee_cache(RelativeEpoch::Previous, spec)?; state.build_committee_cache(RelativeEpoch::Current, spec)?; state.build_committee_cache(RelativeEpoch::Next, spec)?; + state.build_total_active_balance_cache_at(state.current_epoch(), spec)?; + initialize_epoch_cache(state, spec)?; + initialize_progressive_balances_cache::(state, None, spec)?; - // Pre-compute participating indices and total balances. - let participation_cache = ParticipationCache::new(state, spec)?; let sync_committee = state.current_sync_committee()?.clone(); - initialize_progressive_balances_cache::(state, Some(&participation_cache), spec)?; // Justification and finalization. - let justification_and_finalization_state = - process_justification_and_finalization(state, &participation_cache)?; + let justification_and_finalization_state = process_justification_and_finalization(state)?; justification_and_finalization_state.apply_changes_to_state(state); - process_inactivity_updates(state, &participation_cache, spec)?; - - // Rewards and Penalties. - process_rewards_and_penalties(state, &participation_cache, spec)?; - - // Registry Updates. - process_registry_updates(state, spec)?; - - // Slashings. - process_slashings( - state, - participation_cache.current_epoch_total_active_balance(), - spec, - )?; + // In a single pass: + // - Inactivity updates + // - Rewards and penalties + // - Registry updates + // - Slashings + // - Effective balance updates + // + // The `process_eth1_data_reset` is not covered in the single pass, but happens afterwards + // without loss of correctness. + let current_epoch_progressive_balances = state.progressive_balances_cache().clone(); + let current_epoch_total_active_balance = state.get_total_active_balance()?; + let participation_summary = + process_epoch_single_pass(state, spec, SinglePassConfig::default())?; // Reset eth1 data votes. process_eth1_data_reset(state)?; - // Update effective balances with hysteresis (lag). - process_effective_balance_updates(state, Some(&participation_cache), spec)?; - // Reset slashings process_slashings_reset(state)?; // Set randao mix process_randao_mixes_reset(state)?; - // Set historical root accumulator - process_historical_roots_update(state)?; + // Set historical summaries accumulator + if state.historical_summaries().is_ok() { + // Post-Capella. + process_historical_summaries_update(state)?; + } else { + // Pre-Capella + process_historical_roots_update(state)?; + } // Rotate current/previous epoch participation process_participation_flag_updates(state)?; @@ -78,11 +81,12 @@ pub fn process_epoch( // Rotate the epoch caches to suit the epoch transition. state.advance_caches(spec)?; - update_progressive_balances_on_epoch_transition(state, spec)?; Ok(EpochProcessingSummary::Altair { - participation_cache, + progressive_balances: current_epoch_progressive_balances, + current_epoch_total_active_balance, + participation: participation_summary, sync_committee, }) } diff --git a/consensus/state_processing/src/per_epoch_processing/altair/inactivity_updates.rs b/consensus/state_processing/src/per_epoch_processing/altair/inactivity_updates.rs index a895567d12c..592ffc09616 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/inactivity_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/inactivity_updates.rs @@ -1,44 +1,23 @@ -use super::ParticipationCache; +use crate::per_epoch_processing::single_pass::{process_epoch_single_pass, SinglePassConfig}; use crate::EpochProcessingError; -use core::result::Result; -use core::result::Result::Ok; -use safe_arith::SafeArith; -use std::cmp::min; use types::beacon_state::BeaconState; use types::chain_spec::ChainSpec; -use types::consts::altair::TIMELY_TARGET_FLAG_INDEX; use types::eth_spec::EthSpec; -pub fn process_inactivity_updates( +/// Slow version of `process_inactivity_updates`. +/// +/// Should only be used for testing. +pub fn process_inactivity_updates_slow( state: &mut BeaconState, - participation_cache: &ParticipationCache, spec: &ChainSpec, ) -> Result<(), EpochProcessingError> { - let previous_epoch = state.previous_epoch(); - // Score updates based on previous epoch participation, skip genesis epoch - if state.current_epoch() == T::genesis_epoch() { - return Ok(()); - } - - let unslashed_indices = participation_cache - .get_unslashed_participating_indices(TIMELY_TARGET_FLAG_INDEX, state.previous_epoch())?; - - for &index in participation_cache.eligible_validator_indices() { - // Increase inactivity score of inactive validators - if unslashed_indices.contains(index)? { - let inactivity_score = state.get_inactivity_score_mut(index)?; - inactivity_score.safe_sub_assign(min(1, *inactivity_score))?; - } else { - state - .get_inactivity_score_mut(index)? - .safe_add_assign(spec.inactivity_score_bias)?; - } - // Decrease the score of all validators for forgiveness when not during a leak - if !state.is_in_inactivity_leak(previous_epoch, spec)? { - let inactivity_score = state.get_inactivity_score_mut(index)?; - inactivity_score - .safe_sub_assign(min(spec.inactivity_score_recovery_rate, *inactivity_score))?; - } - } + process_epoch_single_pass( + state, + spec, + SinglePassConfig { + inactivity_updates: true, + ..SinglePassConfig::disable_all() + }, + )?; Ok(()) } diff --git a/consensus/state_processing/src/per_epoch_processing/altair/justification_and_finalization.rs b/consensus/state_processing/src/per_epoch_processing/altair/justification_and_finalization.rs index 1f17cf56e05..61b5c1ed5ab 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/justification_and_finalization.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/justification_and_finalization.rs @@ -1,32 +1,27 @@ -use super::ParticipationCache; use crate::per_epoch_processing::Error; use crate::per_epoch_processing::{ weigh_justification_and_finalization, JustificationAndFinalizationState, }; use safe_arith::SafeArith; -use types::consts::altair::TIMELY_TARGET_FLAG_INDEX; use types::{BeaconState, EthSpec}; -/// Update the justified and finalized checkpoints for matching target attestations. -pub fn process_justification_and_finalization( - state: &BeaconState, - participation_cache: &ParticipationCache, -) -> Result, Error> { +/// Process justification and finalization using the progressive balances cache. +pub fn process_justification_and_finalization( + state: &BeaconState, +) -> Result, Error> { let justification_and_finalization_state = JustificationAndFinalizationState::new(state); - - if state.current_epoch() <= T::genesis_epoch().safe_add(1)? { + if state.current_epoch() <= E::genesis_epoch().safe_add(1)? { return Ok(justification_and_finalization_state); } - let previous_epoch = state.previous_epoch(); - let current_epoch = state.current_epoch(); - let previous_indices = participation_cache - .get_unslashed_participating_indices(TIMELY_TARGET_FLAG_INDEX, previous_epoch)?; - let current_indices = participation_cache - .get_unslashed_participating_indices(TIMELY_TARGET_FLAG_INDEX, current_epoch)?; - let total_active_balance = participation_cache.current_epoch_total_active_balance(); - let previous_target_balance = previous_indices.total_balance()?; - let current_target_balance = current_indices.total_balance()?; + // Load cached balances + let progressive_balances_cache = state.progressive_balances_cache(); + let previous_target_balance = + progressive_balances_cache.previous_epoch_target_attesting_balance()?; + let current_target_balance = + progressive_balances_cache.current_epoch_target_attesting_balance()?; + let total_active_balance = state.get_total_active_balance()?; + weigh_justification_and_finalization( justification_and_finalization_state, total_active_balance, diff --git a/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs b/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs index d67e7874cb9..689be28adc0 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs @@ -11,51 +11,60 @@ //! Additionally, this cache is returned from the `altair::process_epoch` function and can be used //! to get useful summaries about the validator participation in an epoch. +use std::collections::HashMap; + +use crate::common::altair::{get_base_reward, BaseRewardPerIncrement}; +use safe_arith::{ArithError, SafeArith}; use types::{ consts::altair::{ NUM_FLAG_INDICES, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, }, Balance, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ParticipationFlags, - RelativeEpoch, + RelativeEpoch, Unsigned, Validator, }; #[derive(Debug, PartialEq, Clone)] pub enum Error { InvalidFlagIndex(usize), + NoUnslashedParticipatingIndices, + MissingValidator(usize), + BeaconState(BeaconStateError), + Arith(ArithError), InvalidValidatorIndex(usize), + InconsistentTotalActiveBalance { cached: u64, computed: u64 }, +} + +impl From for Error { + fn from(e: BeaconStateError) -> Self { + Self::BeaconState(e) + } +} + +impl From for Error { + fn from(e: ArithError) -> Self { + Self::Arith(e) + } } /// Caches the participation values for one epoch (either the previous or current). #[derive(PartialEq, Debug, Clone)] -struct SingleEpochParticipationCache { - /// Maps an active validator index to their participation flags. - /// - /// To reiterate, only active and unslashed validator indices are stored in this map. - /// - /// ## Note - /// - /// It would be ideal to maintain a reference to the `BeaconState` here rather than copying the - /// `ParticipationFlags`, however that would cause us to run into mutable reference limitations - /// upstream. - unslashed_participating_indices: Vec>, +pub(crate) struct SingleEpochParticipationCache { /// Stores the sum of the balances for all validators in `self.unslashed_participating_indices` /// for all flags in `NUM_FLAG_INDICES`. /// /// A flag balance is only incremented if a validator is in that flag set. - total_flag_balances: [Balance; NUM_FLAG_INDICES], + pub(crate) total_flag_balances: [Balance; NUM_FLAG_INDICES], /// Stores the sum of all balances of all validators in `self.unslashed_participating_indices` /// (regardless of which flags are set). total_active_balance: Balance, } impl SingleEpochParticipationCache { - fn new(state: &BeaconState, spec: &ChainSpec) -> Self { - let num_validators = state.validators().len(); + fn new(spec: &ChainSpec) -> Self { let zero_balance = Balance::zero(spec.effective_balance_increment); Self { - unslashed_participating_indices: vec![None; num_validators], total_flag_balances: [zero_balance; NUM_FLAG_INDICES], total_active_balance: zero_balance, } @@ -77,76 +86,41 @@ impl SingleEpochParticipationCache { .ok_or(Error::InvalidFlagIndex(flag_index)) } - /// Returns `true` if `val_index` is active, unslashed and has `flag_index` set. - /// - /// ## Errors - /// - /// May return an error if `flag_index` is out-of-bounds. - fn has_flag(&self, val_index: usize, flag_index: usize) -> Result { - let participation_flags = self - .unslashed_participating_indices - .get(val_index) - .ok_or(Error::InvalidValidatorIndex(val_index))?; - if let Some(participation_flags) = participation_flags { - participation_flags - .has_flag(flag_index) - .map_err(|_| Error::InvalidFlagIndex(flag_index)) - } else { - Ok(false) - } - } - - /// Process an **active** validator, reading from the `state` with respect to the + /// Process an **active** validator, reading from the `epoch_participation` with respect to the /// `relative_epoch`. /// /// ## Errors /// - /// - The provided `state` **must** be Altair. An error will be returned otherwise. /// - An error will be returned if the `val_index` validator is inactive at the given /// `relative_epoch`. - fn process_active_validator( + fn process_active_validator( &mut self, val_index: usize, - state: &BeaconState, + validator: &Validator, + epoch_participation: &ParticipationFlags, current_epoch: Epoch, relative_epoch: RelativeEpoch, ) -> Result<(), BeaconStateError> { - let validator = state.get_validator(val_index)?; - let val_balance = validator.effective_balance; - // Sanity check to ensure the validator is active. let epoch = relative_epoch.into_epoch(current_epoch); if !validator.is_active_at(epoch) { return Err(BeaconStateError::ValidatorIsInactive { val_index }); } - let epoch_participation = match relative_epoch { - RelativeEpoch::Current => state.current_epoch_participation(), - RelativeEpoch::Previous => state.previous_epoch_participation(), - _ => Err(BeaconStateError::EpochOutOfBounds), - }? - .get(val_index) - .ok_or(BeaconStateError::ParticipationOutOfBounds(val_index))?; - // All active validators increase the total active balance. - self.total_active_balance.safe_add_assign(val_balance)?; + self.total_active_balance + .safe_add_assign(validator.effective_balance)?; // Only unslashed validators may proceed. if validator.slashed { return Ok(()); } - // Add their `ParticipationFlags` to the map. - *self - .unslashed_participating_indices - .get_mut(val_index) - .ok_or(BeaconStateError::UnknownValidator(val_index))? = Some(*epoch_participation); - // Iterate through all the flags and increment the total flag balances for whichever flags // are set for `val_index`. for (flag, balance) in self.total_flag_balances.iter_mut().enumerate() { if epoch_participation.has_flag(flag)? { - balance.safe_add_assign(val_balance)?; + balance.safe_add_assign(validator.effective_balance)?; } } @@ -154,17 +128,61 @@ impl SingleEpochParticipationCache { } } +#[derive(Debug, PartialEq, Clone)] +pub struct ValidatorInfo { + pub effective_balance: u64, + pub base_reward: u64, + pub is_eligible: bool, + pub is_slashed: bool, + pub is_active_current_epoch: bool, + pub is_active_previous_epoch: bool, + pub previous_epoch_participation: ParticipationFlags, +} + +impl ValidatorInfo { + #[inline] + pub fn is_unslashed_participating_index(&self, flag_index: usize) -> Result { + Ok(self.is_active_previous_epoch + && !self.is_slashed + && self + .previous_epoch_participation + .has_flag(flag_index) + .map_err(|_| Error::InvalidFlagIndex(flag_index))?) + } +} + +/// Single `HashMap` for validator info relevant to `process_epoch`. +#[derive(Debug, PartialEq, Clone)] +struct ValidatorInfoCache { + info: Vec>, +} + +impl ValidatorInfoCache { + pub fn new(capacity: usize) -> Self { + Self { + info: vec![None; capacity], + } + } +} + /// Maintains a cache to be used during `altair::process_epoch`. #[derive(PartialEq, Debug, Clone)] pub struct ParticipationCache { current_epoch: Epoch, /// Caches information about active validators pertaining to `self.current_epoch`. - current_epoch_participation: SingleEpochParticipationCache, + pub(crate) current_epoch_participation: SingleEpochParticipationCache, previous_epoch: Epoch, /// Caches information about active validators pertaining to `self.previous_epoch`. - previous_epoch_participation: SingleEpochParticipationCache, + pub(crate) previous_epoch_participation: SingleEpochParticipationCache, + /// Caches validator information relevant to `process_epoch`. + validators: ValidatorInfoCache, /// Caches the result of the `get_eligible_validator_indices` function. eligible_indices: Vec, + /// Caches the indices and effective balances of validators that need to be processed by + /// `process_slashings`. + process_slashings_indices: Vec<(usize, u64)>, + /// Updates to the inactivity scores if we are definitely not in an inactivity leak. + pub inactivity_score_updates: Option>, } impl ParticipationCache { @@ -173,17 +191,21 @@ impl ParticipationCache { /// ## Errors /// /// - The provided `state` **must** be an Altair state. An error will be returned otherwise. - pub fn new( - state: &BeaconState, - spec: &ChainSpec, - ) -> Result { + pub fn new(state: &BeaconState, spec: &ChainSpec) -> Result { let current_epoch = state.current_epoch(); let previous_epoch = state.previous_epoch(); // Both the current/previous epoch participations are set to a capacity that is slightly // larger than required. The difference will be due slashed-but-active validators. - let mut current_epoch_participation = SingleEpochParticipationCache::new(state, spec); - let mut previous_epoch_participation = SingleEpochParticipationCache::new(state, spec); + let mut current_epoch_participation = SingleEpochParticipationCache::new(spec); + let mut previous_epoch_participation = SingleEpochParticipationCache::new(spec); + + let mut validators = ValidatorInfoCache::new(state.validators().len()); + + let current_epoch_total_active_balance = state.get_total_active_balance()?; + let base_reward_per_increment = + BaseRewardPerIncrement::new(current_epoch_total_active_balance, spec)?; + // Contains the set of validators which are either: // // - Active in the previous epoch. @@ -193,6 +215,19 @@ impl ParticipationCache { // reallocations. let mut eligible_indices = Vec::with_capacity(state.validators().len()); + let mut process_slashings_indices = vec![]; + + // Fast path for inactivity scores update when we are definitely not in an inactivity leak. + // This breaks the dependence of `process_inactivity_updates` on the finalization + // re-calculation. + let definitely_not_in_inactivity_leak = state + .finalized_checkpoint() + .epoch + .safe_add(spec.min_epochs_to_inactivity_penalty)? + .safe_add(1)? + >= state.current_epoch(); + let mut inactivity_score_updates = HashMap::default(); + // Iterate through all validators, updating: // // 1. Validator participation for current and previous epochs. @@ -200,38 +235,112 @@ impl ParticipationCache { // // Care is taken to ensure that the ordering of `eligible_indices` is the same as the // `get_eligible_validator_indices` function in the spec. - for (val_index, val) in state.validators().iter().enumerate() { - if val.is_active_at(current_epoch) { + let iter = state + .validators() + .iter() + .zip(state.current_epoch_participation()?) + .zip(state.previous_epoch_participation()?) + .zip(state.inactivity_scores()?) + .enumerate(); + for (val_index, (((val, curr_epoch_flags), prev_epoch_flags), inactivity_score)) in iter { + let is_active_current_epoch = val.is_active_at(current_epoch); + let is_active_previous_epoch = val.is_active_at(previous_epoch); + let is_eligible = state.is_eligible_validator(previous_epoch, val)?; + + if is_active_current_epoch { current_epoch_participation.process_active_validator( val_index, - state, + val, + curr_epoch_flags, current_epoch, RelativeEpoch::Current, )?; } - if val.is_active_at(previous_epoch) { + if is_active_previous_epoch { + assert!(is_eligible); + previous_epoch_participation.process_active_validator( val_index, - state, + val, + prev_epoch_flags, current_epoch, RelativeEpoch::Previous, )?; } + if val.slashed + && current_epoch.safe_add(T::EpochsPerSlashingsVector::to_u64().safe_div(2)?)? + == val.withdrawable_epoch + { + process_slashings_indices.push((val_index, val.effective_balance)); + } + // Note: a validator might still be "eligible" whilst returning `false` to - // `Validator::is_active_at`. - if state.is_eligible_validator(previous_epoch, val_index)? { - eligible_indices.push(val_index) + // `Validator::is_active_at`. It's also possible for a validator to be active + // in the current epoch without being eligible (if it was just activated). + if is_eligible { + eligible_indices.push(val_index); + } + + let mut validator_info = ValidatorInfo { + effective_balance: val.effective_balance, + base_reward: 0, // not read + is_eligible, + is_slashed: val.slashed, + is_active_current_epoch, + is_active_previous_epoch, + previous_epoch_participation: *prev_epoch_flags, + }; + + // Calculate inactivity updates. + if is_eligible && definitely_not_in_inactivity_leak { + let mut new_inactivity_score = + if validator_info.is_unslashed_participating_index(TIMELY_TARGET_FLAG_INDEX)? { + inactivity_score.saturating_sub(1) + } else { + inactivity_score.safe_add(spec.inactivity_score_bias)? + }; + + // Decrease the score of all validators for forgiveness when not during a leak + new_inactivity_score = + new_inactivity_score.saturating_sub(spec.inactivity_score_recovery_rate); + + if new_inactivity_score != *inactivity_score { + inactivity_score_updates.insert(val_index, new_inactivity_score); + } + } + + #[allow(clippy::indexing_slicing)] + if is_eligible || is_active_current_epoch { + let effective_balance = val.effective_balance; + let base_reward = + get_base_reward(effective_balance, base_reward_per_increment, spec)?; + validator_info.base_reward = base_reward; + validators.info[val_index] = Some(validator_info); } } + // Sanity check total active balance. + if current_epoch_participation.total_active_balance.get() + != current_epoch_total_active_balance + { + return Err(Error::InconsistentTotalActiveBalance { + cached: current_epoch_total_active_balance, + computed: current_epoch_participation.total_active_balance.get(), + }); + } + Ok(Self { current_epoch, current_epoch_participation, previous_epoch, previous_epoch_participation, + validators, eligible_indices, + process_slashings_indices, + inactivity_score_updates: definitely_not_in_inactivity_leak + .then_some(inactivity_score_updates), }) } @@ -240,24 +349,8 @@ impl ParticipationCache { &self.eligible_indices } - /// Equivalent to the `get_unslashed_participating_indices` function in the specification. - pub fn get_unslashed_participating_indices( - &self, - flag_index: usize, - epoch: Epoch, - ) -> Result { - let participation = if epoch == self.current_epoch { - &self.current_epoch_participation - } else if epoch == self.previous_epoch { - &self.previous_epoch_participation - } else { - return Err(BeaconStateError::EpochOutOfBounds); - }; - - Ok(UnslashedParticipatingIndices { - participation, - flag_index, - }) + pub fn process_slashings_indices(&mut self) -> Vec<(usize, u64)> { + std::mem::take(&mut self.process_slashings_indices) } /* @@ -283,8 +376,7 @@ impl ParticipationCache { } pub fn previous_epoch_target_attesting_balance(&self) -> Result { - self.previous_epoch_participation - .total_flag_balance(TIMELY_TARGET_FLAG_INDEX) + self.previous_epoch_flag_attesting_balance(TIMELY_TARGET_FLAG_INDEX) } pub fn previous_epoch_target_attesting_balance_raw(&self) -> Result { @@ -293,46 +385,59 @@ impl ParticipationCache { } pub fn previous_epoch_source_attesting_balance(&self) -> Result { - self.previous_epoch_participation - .total_flag_balance(TIMELY_SOURCE_FLAG_INDEX) + self.previous_epoch_flag_attesting_balance(TIMELY_SOURCE_FLAG_INDEX) } pub fn previous_epoch_head_attesting_balance(&self) -> Result { + self.previous_epoch_flag_attesting_balance(TIMELY_HEAD_FLAG_INDEX) + } + + pub fn previous_epoch_flag_attesting_balance(&self, flag_index: usize) -> Result { self.previous_epoch_participation - .total_flag_balance(TIMELY_HEAD_FLAG_INDEX) + .total_flag_balance(flag_index) } /* * Active/Unslashed */ - /// Returns `None` for an unknown `val_index`. - pub fn is_active_unslashed_in_previous_epoch(&self, val_index: usize) -> Option { - self.previous_epoch_participation - .unslashed_participating_indices - .get(val_index) - .map(|flags| flags.is_some()) + pub fn is_active_unslashed_in_previous_epoch(&self, val_index: usize) -> bool { + self.get_validator(val_index).map_or(false, |validator| { + validator.is_active_previous_epoch && !validator.is_slashed + }) } - /// Returns `None` for an unknown `val_index`. - pub fn is_active_unslashed_in_current_epoch(&self, val_index: usize) -> Option { - self.current_epoch_participation - .unslashed_participating_indices + pub fn is_active_unslashed_in_current_epoch(&self, val_index: usize) -> bool { + self.get_validator(val_index).map_or(false, |validator| { + validator.is_active_current_epoch && !validator.is_slashed + }) + } + + pub fn get_validator(&self, val_index: usize) -> Result<&ValidatorInfo, Error> { + self.validators + .info .get(val_index) - .map(|flags| flags.is_some()) + .ok_or(Error::MissingValidator(val_index))? + .as_ref() + .ok_or(Error::MissingValidator(val_index)) } /* * Flags */ - /// Always returns false for a slashed validator. pub fn is_previous_epoch_timely_source_attester( &self, val_index: usize, ) -> Result { - self.previous_epoch_participation - .has_flag(val_index, TIMELY_SOURCE_FLAG_INDEX) + self.get_validator(val_index) + .map_or(Ok(false), |validator| { + Ok(!validator.is_slashed + && validator + .previous_epoch_participation + .has_flag(TIMELY_SOURCE_FLAG_INDEX) + .map_err(|_| Error::InvalidFlagIndex(TIMELY_SOURCE_FLAG_INDEX))?) + }) } /// Always returns false for a slashed validator. @@ -340,63 +445,35 @@ impl ParticipationCache { &self, val_index: usize, ) -> Result { - self.previous_epoch_participation - .has_flag(val_index, TIMELY_TARGET_FLAG_INDEX) + self.get_validator(val_index) + .map_or(Ok(false), |validator| { + Ok(!validator.is_slashed + && validator + .previous_epoch_participation + .has_flag(TIMELY_TARGET_FLAG_INDEX) + .map_err(|_| Error::InvalidFlagIndex(TIMELY_TARGET_FLAG_INDEX))?) + }) } /// Always returns false for a slashed validator. pub fn is_previous_epoch_timely_head_attester(&self, val_index: usize) -> Result { - self.previous_epoch_participation - .has_flag(val_index, TIMELY_HEAD_FLAG_INDEX) + self.get_validator(val_index) + .map_or(Ok(false), |validator| { + Ok(!validator.is_slashed + && validator + .previous_epoch_participation + .has_flag(TIMELY_HEAD_FLAG_INDEX) + .map_err(|_| Error::InvalidFlagIndex(TIMELY_TARGET_FLAG_INDEX))?) + }) } /// Always returns false for a slashed validator. - pub fn is_current_epoch_timely_source_attester(&self, val_index: usize) -> Result { - self.current_epoch_participation - .has_flag(val_index, TIMELY_SOURCE_FLAG_INDEX) - } - - /// Always returns false for a slashed validator. - pub fn is_current_epoch_timely_target_attester(&self, val_index: usize) -> Result { - self.current_epoch_participation - .has_flag(val_index, TIMELY_TARGET_FLAG_INDEX) - } - - /// Always returns false for a slashed validator. - pub fn is_current_epoch_timely_head_attester(&self, val_index: usize) -> Result { - self.current_epoch_participation - .has_flag(val_index, TIMELY_HEAD_FLAG_INDEX) - } -} - -/// Imitates the return value of the `get_unslashed_participating_indices` in the -/// specification. -/// -/// This struct exists to help make the Lighthouse code read more like the specification. -pub struct UnslashedParticipatingIndices<'a> { - participation: &'a SingleEpochParticipationCache, - flag_index: usize, -} - -impl<'a> UnslashedParticipatingIndices<'a> { - /// Returns `Ok(true)` if the given `val_index` is both: - /// - /// - An active validator. - /// - Has `self.flag_index` set. - pub fn contains(&self, val_index: usize) -> Result { - self.participation.has_flag(val_index, self.flag_index) - } - - /// Returns the sum of all balances of validators which have `self.flag_index` set. - /// - /// ## Notes - /// - /// Respects the `EFFECTIVE_BALANCE_INCREMENT` minimum. - pub fn total_balance(&self) -> Result { - self.participation - .total_flag_balances - .get(self.flag_index) - .ok_or(Error::InvalidFlagIndex(self.flag_index)) - .map(Balance::get) + pub fn is_current_epoch_timely_target_attester( + &self, + _val_index: usize, + ) -> Result { + // FIXME(sproul): decide whether it's worth storing the current epoch participation flags + // *just* for this call. Perhaps the validator API could source it from the state directly. + Ok(false) } } diff --git a/consensus/state_processing/src/per_epoch_processing/altair/rewards_and_penalties.rs b/consensus/state_processing/src/per_epoch_processing/altair/rewards_and_penalties.rs index 19d57130c9b..9510f2eaec4 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/rewards_and_penalties.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/rewards_and_penalties.rs @@ -1,4 +1,8 @@ use super::ParticipationCache; +use crate::per_epoch_processing::{ + single_pass::{process_epoch_single_pass, SinglePassConfig}, + Delta, Error, +}; use safe_arith::SafeArith; use types::consts::altair::{ PARTICIPATION_FLAG_WEIGHTS, TIMELY_HEAD_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, @@ -6,48 +10,21 @@ use types::consts::altair::{ }; use types::{BeaconState, ChainSpec, EthSpec}; -use crate::common::{ - altair::{get_base_reward, BaseRewardPerIncrement}, - decrease_balance, increase_balance, -}; -use crate::per_epoch_processing::{Delta, Error}; - /// Apply attester and proposer rewards. /// -/// Spec v1.1.0 -pub fn process_rewards_and_penalties( +/// This function should only be used for testing. +pub fn process_rewards_and_penalties_slow( state: &mut BeaconState, - participation_cache: &ParticipationCache, spec: &ChainSpec, ) -> Result<(), Error> { - if state.current_epoch() == T::genesis_epoch() { - return Ok(()); - } - - let mut deltas = vec![Delta::default(); state.validators().len()]; - - let total_active_balance = participation_cache.current_epoch_total_active_balance(); - - for flag_index in 0..PARTICIPATION_FLAG_WEIGHTS.len() { - get_flag_index_deltas( - &mut deltas, - state, - flag_index, - total_active_balance, - participation_cache, - spec, - )?; - } - - get_inactivity_penalty_deltas(&mut deltas, state, participation_cache, spec)?; - - // Apply the deltas, erroring on overflow above but not on overflow below (saturating at 0 - // instead). - for (i, delta) in deltas.into_iter().enumerate() { - increase_balance(state, i, delta.rewards)?; - decrease_balance(state, i, delta.penalties)?; - } - + process_epoch_single_pass( + state, + spec, + SinglePassConfig { + rewards_and_penalties: true, + ..SinglePassConfig::disable_all() + }, + )?; Ok(()) } @@ -62,21 +39,21 @@ pub fn get_flag_index_deltas( participation_cache: &ParticipationCache, spec: &ChainSpec, ) -> Result<(), Error> { - let previous_epoch = state.previous_epoch(); - let unslashed_participating_indices = - participation_cache.get_unslashed_participating_indices(flag_index, previous_epoch)?; let weight = get_flag_weight(flag_index)?; - let unslashed_participating_balance = unslashed_participating_indices.total_balance()?; + let unslashed_participating_balance = + participation_cache.previous_epoch_flag_attesting_balance(flag_index)?; let unslashed_participating_increments = unslashed_participating_balance.safe_div(spec.effective_balance_increment)?; let active_increments = total_active_balance.safe_div(spec.effective_balance_increment)?; - let base_reward_per_increment = BaseRewardPerIncrement::new(total_active_balance, spec)?; + let previous_epoch = state.previous_epoch(); for &index in participation_cache.eligible_validator_indices() { - let base_reward = get_base_reward(state, index, base_reward_per_increment, spec)?; + let validator = participation_cache.get_validator(index)?; + let base_reward = validator.base_reward; + let mut delta = Delta::default(); - if unslashed_participating_indices.contains(index)? { + if validator.is_unslashed_participating_index(flag_index)? { if !state.is_in_inactivity_leak(previous_epoch, spec)? { let reward_numerator = base_reward .safe_mul(weight)? @@ -110,15 +87,12 @@ pub fn get_inactivity_penalty_deltas( participation_cache: &ParticipationCache, spec: &ChainSpec, ) -> Result<(), Error> { - let previous_epoch = state.previous_epoch(); - let matching_target_indices = participation_cache - .get_unslashed_participating_indices(TIMELY_TARGET_FLAG_INDEX, previous_epoch)?; for &index in participation_cache.eligible_validator_indices() { + let validator = participation_cache.get_validator(index)?; let mut delta = Delta::default(); - if !matching_target_indices.contains(index)? { - let penalty_numerator = state - .get_validator(index)? + if !validator.is_unslashed_participating_index(TIMELY_TARGET_FLAG_INDEX)? { + let penalty_numerator = validator .effective_balance .safe_mul(state.get_inactivity_score(index)?)?; let penalty_denominator = spec diff --git a/consensus/state_processing/src/per_epoch_processing/base.rs b/consensus/state_processing/src/per_epoch_processing/base.rs index c5864bd1ef2..796379ae756 100644 --- a/consensus/state_processing/src/per_epoch_processing/base.rs +++ b/consensus/state_processing/src/per_epoch_processing/base.rs @@ -1,4 +1,5 @@ use super::{process_registry_updates, process_slashings, EpochProcessingSummary, Error}; +use crate::epoch_cache::initialize_epoch_cache; use crate::per_epoch_processing::{ effective_balance_updates::process_effective_balance_updates, historical_roots_update::process_historical_roots_update, @@ -23,6 +24,8 @@ pub fn process_epoch( state.build_committee_cache(RelativeEpoch::Previous, spec)?; state.build_committee_cache(RelativeEpoch::Current, spec)?; state.build_committee_cache(RelativeEpoch::Next, spec)?; + state.build_total_active_balance_cache_at(state.current_epoch(), spec)?; + initialize_epoch_cache(state, spec)?; // Load the struct we use to assign validators into sets based on their participation. // @@ -52,7 +55,7 @@ pub fn process_epoch( process_eth1_data_reset(state)?; // Update effective balances with hysteresis (lag). - process_effective_balance_updates(state, None, spec)?; + process_effective_balance_updates(state, spec)?; // Reset slashings process_slashings_reset(state)?; diff --git a/consensus/state_processing/src/per_epoch_processing/base/rewards_and_penalties.rs b/consensus/state_processing/src/per_epoch_processing/base/rewards_and_penalties.rs index 74c96d8aee5..57694a2dc7f 100644 --- a/consensus/state_processing/src/per_epoch_processing/base/rewards_and_penalties.rs +++ b/consensus/state_processing/src/per_epoch_processing/base/rewards_and_penalties.rs @@ -1,4 +1,7 @@ -use crate::common::{base::get_base_reward, decrease_balance, increase_balance}; +use crate::common::{ + base::{get_base_reward, SqrtTotalActiveBalance}, + decrease_balance, increase_balance, +}; use crate::per_epoch_processing::{ base::{TotalBalances, ValidatorStatus, ValidatorStatuses}, Delta, Error, @@ -109,7 +112,6 @@ fn get_attestation_deltas( maybe_validators_subset: Option<&Vec>, spec: &ChainSpec, ) -> Result, Error> { - let previous_epoch = state.previous_epoch(); let finality_delay = state .previous_epoch() .safe_sub(state.finalized_checkpoint().epoch)? @@ -118,6 +120,7 @@ fn get_attestation_deltas( let mut deltas = vec![AttestationDelta::default(); state.validators().len()]; let total_balances = &validator_statuses.total_balances; + let sqrt_total_active_balance = SqrtTotalActiveBalance::new(total_balances.current_epoch()); // Ignore validator if a subset is specified and validator is not in the subset let include_validator_delta = |idx| match maybe_validators_subset.as_ref() { @@ -131,11 +134,15 @@ fn get_attestation_deltas( // `get_inclusion_delay_deltas`. It's safe to do so here because any validator that is in // the unslashed indices of the matching source attestations is active, and therefore // eligible. - if !state.is_eligible_validator(previous_epoch, index)? { + if !validator.is_eligible { continue; } - let base_reward = get_base_reward(state, index, total_balances.current_epoch(), spec)?; + let base_reward = get_base_reward( + validator.current_epoch_effective_balance, + sqrt_total_active_balance, + spec, + )?; let (inclusion_delay_delta, proposer_delta) = get_inclusion_delay_delta(validator, base_reward, spec)?; diff --git a/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs b/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs index 26d2536e5fa..8ce85939161 100644 --- a/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs +++ b/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs @@ -53,6 +53,8 @@ impl InclusionInfo { pub struct ValidatorStatus { /// True if the validator has been slashed, ever. pub is_slashed: bool, + /// True if the validator is eligible. + pub is_eligible: bool, /// True if the validator can withdraw in the current epoch. pub is_withdrawable_in_current_epoch: bool, /// True if the validator was active in the state's _current_ epoch. @@ -92,6 +94,7 @@ impl ValidatorStatus { // Update all the bool fields, only updating `self` if `other` is true (never setting // `self` to false). set_self_if_other_is_true!(self, other, is_slashed); + set_self_if_other_is_true!(self, other, is_eligible); set_self_if_other_is_true!(self, other, is_withdrawable_in_current_epoch); set_self_if_other_is_true!(self, other, is_active_in_current_epoch); set_self_if_other_is_true!(self, other, is_active_in_previous_epoch); @@ -195,24 +198,27 @@ impl ValidatorStatuses { let mut statuses = Vec::with_capacity(state.validators().len()); let mut total_balances = TotalBalances::new(spec); - for (i, validator) in state.validators().iter().enumerate() { - let effective_balance = state.get_effective_balance(i)?; + let current_epoch = state.current_epoch(); + let previous_epoch = state.previous_epoch(); + + for validator in state.validators().iter() { + let effective_balance = validator.effective_balance; let mut status = ValidatorStatus { is_slashed: validator.slashed, - is_withdrawable_in_current_epoch: validator - .is_withdrawable_at(state.current_epoch()), + is_eligible: state.is_eligible_validator(previous_epoch, validator)?, + is_withdrawable_in_current_epoch: validator.is_withdrawable_at(current_epoch), current_epoch_effective_balance: effective_balance, ..ValidatorStatus::default() }; - if validator.is_active_at(state.current_epoch()) { + if validator.is_active_at(current_epoch) { status.is_active_in_current_epoch = true; total_balances .current_epoch .safe_add_assign(effective_balance)?; } - if validator.is_active_at(state.previous_epoch()) { + if validator.is_active_at(previous_epoch) { status.is_active_in_previous_epoch = true; total_balances .previous_epoch @@ -285,10 +291,10 @@ impl ValidatorStatuses { } // Compute the total balances - for (index, v) in self.statuses.iter().enumerate() { + for v in self.statuses.iter() { // According to the spec, we only count unslashed validators towards the totals. if !v.is_slashed { - let validator_balance = state.get_effective_balance(index)?; + let validator_balance = v.current_epoch_effective_balance; if v.is_current_epoch_attester { self.total_balances diff --git a/consensus/state_processing/src/per_epoch_processing/capella.rs b/consensus/state_processing/src/per_epoch_processing/capella.rs index 911510ed0ce..161bce54232 100644 --- a/consensus/state_processing/src/per_epoch_processing/capella.rs +++ b/consensus/state_processing/src/per_epoch_processing/capella.rs @@ -1,84 +1,3 @@ -use super::altair::inactivity_updates::process_inactivity_updates; -use super::altair::justification_and_finalization::process_justification_and_finalization; -use super::altair::participation_cache::ParticipationCache; -use super::altair::participation_flag_updates::process_participation_flag_updates; -use super::altair::rewards_and_penalties::process_rewards_and_penalties; -use super::altair::sync_committee_updates::process_sync_committee_updates; -use super::{process_registry_updates, process_slashings, EpochProcessingSummary, Error}; -use crate::per_epoch_processing::{ - effective_balance_updates::process_effective_balance_updates, - resets::{process_eth1_data_reset, process_randao_mixes_reset, process_slashings_reset}, -}; -use types::{BeaconState, ChainSpec, EthSpec, RelativeEpoch}; - -use crate::common::update_progressive_balances_cache::{ - initialize_progressive_balances_cache, update_progressive_balances_on_epoch_transition, -}; pub use historical_summaries_update::process_historical_summaries_update; mod historical_summaries_update; - -pub fn process_epoch( - state: &mut BeaconState, - spec: &ChainSpec, -) -> Result, Error> { - // Ensure the committee caches are built. - state.build_committee_cache(RelativeEpoch::Previous, spec)?; - state.build_committee_cache(RelativeEpoch::Current, spec)?; - state.build_committee_cache(RelativeEpoch::Next, spec)?; - - // Pre-compute participating indices and total balances. - let participation_cache = ParticipationCache::new(state, spec)?; - let sync_committee = state.current_sync_committee()?.clone(); - initialize_progressive_balances_cache(state, Some(&participation_cache), spec)?; - - // Justification and finalization. - let justification_and_finalization_state = - process_justification_and_finalization(state, &participation_cache)?; - justification_and_finalization_state.apply_changes_to_state(state); - - process_inactivity_updates(state, &participation_cache, spec)?; - - // Rewards and Penalties. - process_rewards_and_penalties(state, &participation_cache, spec)?; - - // Registry Updates. - process_registry_updates(state, spec)?; - - // Slashings. - process_slashings( - state, - participation_cache.current_epoch_total_active_balance(), - spec, - )?; - - // Reset eth1 data votes. - process_eth1_data_reset(state)?; - - // Update effective balances with hysteresis (lag). - process_effective_balance_updates(state, Some(&participation_cache), spec)?; - - // Reset slashings - process_slashings_reset(state)?; - - // Set randao mix - process_randao_mixes_reset(state)?; - - // Set historical summaries accumulator - process_historical_summaries_update(state)?; - - // Rotate current/previous epoch participation - process_participation_flag_updates(state)?; - - process_sync_committee_updates(state, spec)?; - - // Rotate the epoch caches to suit the epoch transition. - state.advance_caches(spec)?; - - update_progressive_balances_on_epoch_transition(state, spec)?; - - Ok(EpochProcessingSummary::Altair { - participation_cache, - sync_committee, - }) -} diff --git a/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs b/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs index 1759f7e1402..1817fde7fad 100644 --- a/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs @@ -1,68 +1,70 @@ use super::errors::EpochProcessingError; -use crate::per_epoch_processing::altair::ParticipationCache; +use crate::per_epoch_processing::single_pass::{process_epoch_single_pass, SinglePassConfig}; use safe_arith::SafeArith; use types::beacon_state::BeaconState; use types::chain_spec::ChainSpec; -use types::{BeaconStateError, EthSpec, ProgressiveBalancesCache}; +use types::{BeaconStateError, EthSpec}; pub fn process_effective_balance_updates( state: &mut BeaconState, - maybe_participation_cache: Option<&ParticipationCache>, spec: &ChainSpec, ) -> Result<(), EpochProcessingError> { + // Compute new total active balance for the next epoch as a side-effect of iterating the + // effective balances. + let next_epoch = state.next_epoch()?; + let mut new_total_active_balance = 0; + let hysteresis_increment = spec .effective_balance_increment .safe_div(spec.hysteresis_quotient)?; let downward_threshold = hysteresis_increment.safe_mul(spec.hysteresis_downward_multiplier)?; let upward_threshold = hysteresis_increment.safe_mul(spec.hysteresis_upward_multiplier)?; - let (validators, balances, progressive_balances_cache) = - state.validators_and_balances_and_progressive_balances_mut(); - for (index, validator) in validators.iter_mut().enumerate() { + let (validators, balances, _) = state.validators_and_balances_and_progressive_balances_mut(); + let mut validators_iter = validators.iter_mut().enumerate(); + + while let Some((index, validator)) = validators_iter.next() { let balance = balances .get(index) .copied() .ok_or(BeaconStateError::BalancesOutOfBounds(index))?; - if balance.safe_add(downward_threshold)? < validator.effective_balance + let new_effective_balance = if balance.safe_add(downward_threshold)? + < validator.effective_balance || validator.effective_balance.safe_add(upward_threshold)? < balance { - let old_effective_balance = validator.effective_balance; - let new_effective_balance = std::cmp::min( + std::cmp::min( balance.safe_sub(balance.safe_rem(spec.effective_balance_increment)?)?, spec.max_effective_balance, - ); + ) + } else { + validator.effective_balance + }; - if let Some(participation_cache) = maybe_participation_cache { - update_progressive_balances( - participation_cache, - progressive_balances_cache, - index, - old_effective_balance, - new_effective_balance, - )?; - } + if validator.is_active_at(next_epoch) { + new_total_active_balance.safe_add_assign(new_effective_balance)?; + } + if new_effective_balance != validator.effective_balance { validator.effective_balance = new_effective_balance; } } + + state.set_total_active_balance(next_epoch, new_total_active_balance); + Ok(()) } -fn update_progressive_balances( - participation_cache: &ParticipationCache, - progressive_balances_cache: &mut ProgressiveBalancesCache, - index: usize, - old_effective_balance: u64, - new_effective_balance: u64, +pub fn process_effective_balance_updates_slow( + state: &mut BeaconState, + spec: &ChainSpec, ) -> Result<(), EpochProcessingError> { - if old_effective_balance != new_effective_balance { - let is_current_epoch_target_attester = - participation_cache.is_current_epoch_timely_target_attester(index)?; - progressive_balances_cache.on_effective_balance_change( - is_current_epoch_target_attester, - old_effective_balance, - new_effective_balance, - )?; - } + process_epoch_single_pass( + state, + spec, + SinglePassConfig { + effective_balance_updates: true, + ..SinglePassConfig::disable_all() + }, + )?; Ok(()) } diff --git a/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs b/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs index 89bc4ab5a34..4266f462123 100644 --- a/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs +++ b/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs @@ -1,10 +1,11 @@ -use super::{ - altair::{participation_cache::Error as ParticipationCacheError, ParticipationCache}, - base::{validator_statuses::InclusionInfo, TotalBalances, ValidatorStatus}, -}; +use super::base::{validator_statuses::InclusionInfo, TotalBalances, ValidatorStatus}; use crate::metrics; use std::sync::Arc; -use types::{EthSpec, SyncCommittee}; +use types::{ + consts::altair::{TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX}, + BeaconStateError, Epoch, EthSpec, ParticipationFlags, ProgressiveBalancesCache, SyncCommittee, + Validator, VariableList, +}; /// Provides a summary of validator participation during the epoch. #[derive(PartialEq, Debug)] @@ -14,14 +15,79 @@ pub enum EpochProcessingSummary { statuses: Vec, }, Altair { - participation_cache: ParticipationCache, + progressive_balances: ProgressiveBalancesCache, + current_epoch_total_active_balance: u64, + participation: ParticipationEpochSummary, sync_committee: Arc>, }, } +#[derive(PartialEq, Debug)] +pub struct ParticipationEpochSummary { + /// Copy of the validator registry prior to mutation. + validators: VariableList, + /// Copy of the participation flags for the previous epoch. + previous_epoch_participation: VariableList, + /// Copy of the participation flags for the current epoch. + current_epoch_participation: VariableList, + previous_epoch: Epoch, + current_epoch: Epoch, +} + +impl ParticipationEpochSummary { + pub fn new( + validators: VariableList, + previous_epoch_participation: VariableList, + current_epoch_participation: VariableList, + previous_epoch: Epoch, + current_epoch: Epoch, + ) -> Self { + Self { + validators, + previous_epoch_participation, + current_epoch_participation, + previous_epoch, + current_epoch, + } + } + + pub fn is_active_and_unslashed(&self, val_index: usize, epoch: Epoch) -> bool { + self.validators + .get(val_index) + .map(|validator| !validator.slashed && validator.is_active_at(epoch)) + .unwrap_or(false) + } + + pub fn is_previous_epoch_unslashed_participating_index( + &self, + val_index: usize, + flag_index: usize, + ) -> Result { + Ok(self.is_active_and_unslashed(val_index, self.previous_epoch) + && self + .previous_epoch_participation + .get(val_index) + .ok_or(BeaconStateError::UnknownValidator(val_index))? + .has_flag(flag_index)?) + } + + pub fn is_current_epoch_unslashed_participating_index( + &self, + val_index: usize, + flag_index: usize, + ) -> Result { + Ok(self.is_active_and_unslashed(val_index, self.current_epoch) + && self + .current_epoch_participation + .get(val_index) + .ok_or(BeaconStateError::UnknownValidator(val_index))? + .has_flag(flag_index)?) + } +} + impl EpochProcessingSummary { /// Updates some Prometheus metrics with some values in `self`. - pub fn observe_metrics(&self) -> Result<(), ParticipationCacheError> { + pub fn observe_metrics(&self) -> Result<(), BeaconStateError> { metrics::set_gauge( &metrics::PARTICIPATION_PREV_EPOCH_HEAD_ATTESTING_GWEI_TOTAL, self.previous_epoch_head_attesting_balance()? as i64, @@ -55,35 +121,30 @@ impl EpochProcessingSummary { match self { EpochProcessingSummary::Base { total_balances, .. } => total_balances.current_epoch(), EpochProcessingSummary::Altair { - participation_cache, + current_epoch_total_active_balance, .. - } => participation_cache.current_epoch_total_active_balance(), + } => *current_epoch_total_active_balance, } } /// Returns the sum of the effective balance of all validators in the current epoch who /// included an attestation that matched the target. - pub fn current_epoch_target_attesting_balance(&self) -> Result { + pub fn current_epoch_target_attesting_balance(&self) -> Result { match self { EpochProcessingSummary::Base { total_balances, .. } => { Ok(total_balances.current_epoch_target_attesters()) } EpochProcessingSummary::Altair { - participation_cache, + progressive_balances, .. - } => participation_cache.current_epoch_target_attesting_balance(), + } => progressive_balances.current_epoch_target_attesting_balance(), } } /// Returns the sum of the effective balance of all validators in the previous epoch. pub fn previous_epoch_total_active_balance(&self) -> u64 { - match self { - EpochProcessingSummary::Base { total_balances, .. } => total_balances.previous_epoch(), - EpochProcessingSummary::Altair { - participation_cache, - .. - } => participation_cache.previous_epoch_total_active_balance(), - } + // FIXME(sproul): this is not a useful concept and should be deleted + self.current_epoch_total_active_balance() } /// Returns `true` if `val_index` was included in the active validator indices in the current @@ -97,12 +158,9 @@ impl EpochProcessingSummary { EpochProcessingSummary::Base { statuses, .. } => statuses .get(val_index) .map_or(false, |s| s.is_active_in_current_epoch && !s.is_slashed), - EpochProcessingSummary::Altair { - participation_cache, - .. - } => participation_cache - .is_active_unslashed_in_current_epoch(val_index) - .unwrap_or(false), + EpochProcessingSummary::Altair { participation, .. } => { + participation.is_active_and_unslashed(val_index, participation.current_epoch) + } } } @@ -120,34 +178,30 @@ impl EpochProcessingSummary { pub fn is_current_epoch_target_attester( &self, val_index: usize, - ) -> Result { + ) -> Result { match self { EpochProcessingSummary::Base { statuses, .. } => Ok(statuses .get(val_index) .map_or(false, |s| s.is_current_epoch_target_attester)), - EpochProcessingSummary::Altair { - participation_cache, - .. - } => participation_cache - .is_current_epoch_timely_target_attester(val_index) - .or_else(|e| match e { - ParticipationCacheError::InvalidValidatorIndex(_) => Ok(false), - e => Err(e), - }), + EpochProcessingSummary::Altair { participation, .. } => participation + .is_current_epoch_unslashed_participating_index( + val_index, + TIMELY_TARGET_FLAG_INDEX, + ), } } /// Returns the sum of the effective balance of all validators in the previous epoch who /// included an attestation that matched the target. - pub fn previous_epoch_target_attesting_balance(&self) -> Result { + pub fn previous_epoch_target_attesting_balance(&self) -> Result { match self { EpochProcessingSummary::Base { total_balances, .. } => { Ok(total_balances.previous_epoch_target_attesters()) } EpochProcessingSummary::Altair { - participation_cache, + progressive_balances, .. - } => participation_cache.previous_epoch_target_attesting_balance(), + } => progressive_balances.previous_epoch_target_attesting_balance(), } } @@ -158,15 +212,15 @@ impl EpochProcessingSummary { /// /// - Base: any attestation can match the head. /// - Altair: only "timely" attestations can match the head. - pub fn previous_epoch_head_attesting_balance(&self) -> Result { + pub fn previous_epoch_head_attesting_balance(&self) -> Result { match self { EpochProcessingSummary::Base { total_balances, .. } => { Ok(total_balances.previous_epoch_head_attesters()) } EpochProcessingSummary::Altair { - participation_cache, + progressive_balances, .. - } => participation_cache.previous_epoch_head_attesting_balance(), + } => progressive_balances.previous_epoch_head_attesting_balance(), } } @@ -177,15 +231,15 @@ impl EpochProcessingSummary { /// /// - Base: any attestation can match the source. /// - Altair: only "timely" attestations can match the source. - pub fn previous_epoch_source_attesting_balance(&self) -> Result { + pub fn previous_epoch_source_attesting_balance(&self) -> Result { match self { EpochProcessingSummary::Base { total_balances, .. } => { Ok(total_balances.previous_epoch_attesters()) } EpochProcessingSummary::Altair { - participation_cache, + progressive_balances, .. - } => participation_cache.previous_epoch_source_attesting_balance(), + } => progressive_balances.previous_epoch_source_attesting_balance(), } } @@ -200,12 +254,9 @@ impl EpochProcessingSummary { EpochProcessingSummary::Base { statuses, .. } => statuses .get(val_index) .map_or(false, |s| s.is_active_in_previous_epoch && !s.is_slashed), - EpochProcessingSummary::Altair { - participation_cache, - .. - } => participation_cache - .is_active_unslashed_in_previous_epoch(val_index) - .unwrap_or(false), + EpochProcessingSummary::Altair { participation, .. } => { + participation.is_active_and_unslashed(val_index, participation.previous_epoch) + } } } @@ -218,20 +269,16 @@ impl EpochProcessingSummary { pub fn is_previous_epoch_target_attester( &self, val_index: usize, - ) -> Result { + ) -> Result { match self { EpochProcessingSummary::Base { statuses, .. } => Ok(statuses .get(val_index) .map_or(false, |s| s.is_previous_epoch_target_attester)), - EpochProcessingSummary::Altair { - participation_cache, - .. - } => participation_cache - .is_previous_epoch_timely_target_attester(val_index) - .or_else(|e| match e { - ParticipationCacheError::InvalidValidatorIndex(_) => Ok(false), - e => Err(e), - }), + EpochProcessingSummary::Altair { participation, .. } => participation + .is_previous_epoch_unslashed_participating_index( + val_index, + TIMELY_TARGET_FLAG_INDEX, + ), } } @@ -249,20 +296,13 @@ impl EpochProcessingSummary { pub fn is_previous_epoch_head_attester( &self, val_index: usize, - ) -> Result { + ) -> Result { match self { EpochProcessingSummary::Base { statuses, .. } => Ok(statuses .get(val_index) .map_or(false, |s| s.is_previous_epoch_head_attester)), - EpochProcessingSummary::Altair { - participation_cache, - .. - } => participation_cache - .is_previous_epoch_timely_head_attester(val_index) - .or_else(|e| match e { - ParticipationCacheError::InvalidValidatorIndex(_) => Ok(false), - e => Err(e), - }), + EpochProcessingSummary::Altair { participation, .. } => participation + .is_previous_epoch_unslashed_participating_index(val_index, TIMELY_HEAD_FLAG_INDEX), } } @@ -280,20 +320,16 @@ impl EpochProcessingSummary { pub fn is_previous_epoch_source_attester( &self, val_index: usize, - ) -> Result { + ) -> Result { match self { EpochProcessingSummary::Base { statuses, .. } => Ok(statuses .get(val_index) .map_or(false, |s| s.is_previous_epoch_attester)), - EpochProcessingSummary::Altair { - participation_cache, - .. - } => participation_cache - .is_previous_epoch_timely_source_attester(val_index) - .or_else(|e| match e { - ParticipationCacheError::InvalidValidatorIndex(_) => Ok(false), - e => Err(e), - }), + EpochProcessingSummary::Altair { participation, .. } => participation + .is_previous_epoch_unslashed_participating_index( + val_index, + TIMELY_SOURCE_FLAG_INDEX, + ), } } diff --git a/consensus/state_processing/src/per_epoch_processing/errors.rs b/consensus/state_processing/src/per_epoch_processing/errors.rs index 04797c56342..c1da929c686 100644 --- a/consensus/state_processing/src/per_epoch_processing/errors.rs +++ b/consensus/state_processing/src/per_epoch_processing/errors.rs @@ -1,5 +1,5 @@ use crate::per_epoch_processing::altair::participation_cache::Error as ParticipationCacheError; -use types::{BeaconStateError, InconsistentFork}; +use types::{BeaconStateError, EpochCacheError, InconsistentFork}; #[derive(Debug, PartialEq)] pub enum EpochProcessingError { @@ -25,6 +25,7 @@ pub enum EpochProcessingError { InvalidJustificationBit(ssz_types::Error), InvalidFlagIndex(usize), ParticipationCache(ParticipationCacheError), + EpochCache(EpochCacheError), } impl From for EpochProcessingError { @@ -57,6 +58,12 @@ impl From for EpochProcessingError { } } +impl From for EpochProcessingError { + fn from(e: EpochCacheError) -> Self { + EpochProcessingError::EpochCache(e) + } +} + #[derive(Debug, PartialEq)] pub enum InclusionError { /// The validator did not participate in an attestation in this period. diff --git a/consensus/state_processing/src/per_epoch_processing/registry_updates.rs b/consensus/state_processing/src/per_epoch_processing/registry_updates.rs index 833be413879..a7eaace29c6 100644 --- a/consensus/state_processing/src/per_epoch_processing/registry_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/registry_updates.rs @@ -1,5 +1,5 @@ +use crate::per_epoch_processing::single_pass::{process_epoch_single_pass, SinglePassConfig}; use crate::{common::initiate_validator_exit, per_epoch_processing::Error}; -use itertools::Itertools; use safe_arith::SafeArith; use types::{BeaconState, ChainSpec, EthSpec, Validator}; @@ -40,21 +40,33 @@ pub fn process_registry_updates( } // Queue validators eligible for activation and not dequeued for activation prior to finalized epoch - let activation_queue = state - .validators() - .iter() - .enumerate() - .filter(|(_, validator)| validator.is_eligible_for_activation(state, spec)) - .sorted_by_key(|(index, validator)| (validator.activation_eligibility_epoch, *index)) - .map(|(index, _)| index) - .collect_vec(); - // Dequeue validators for activation up to churn limit - let activation_churn_limit = state.get_activation_churn_limit(spec)? as usize; + let churn_limit = state.get_activation_churn_limit(spec)? as usize; + + let epoch_cache = state.epoch_cache().clone(); + let activation_queue = epoch_cache + .activation_queue()? + .get_validators_eligible_for_activation(state.finalized_checkpoint().epoch, churn_limit); + let delayed_activation_epoch = state.compute_activation_exit_epoch(current_epoch, spec)?; - for index in activation_queue.into_iter().take(activation_churn_limit) { + for index in activation_queue { state.get_validator_mut(index)?.activation_epoch = delayed_activation_epoch; } Ok(()) } + +pub fn process_registry_updates_slow( + state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), Error> { + process_epoch_single_pass( + state, + spec, + SinglePassConfig { + registry_updates: true, + ..SinglePassConfig::disable_all() + }, + )?; + Ok(()) +} diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs new file mode 100644 index 00000000000..4a9d05a5d5b --- /dev/null +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -0,0 +1,628 @@ +use crate::{ + common::update_progressive_balances_cache::initialize_progressive_balances_cache, + epoch_cache::{initialize_epoch_cache, PreEpochCache}, + per_epoch_processing::{Delta, Error, ParticipationEpochSummary}, +}; +use itertools::izip; +use safe_arith::{SafeArith, SafeArithIter}; +use std::cmp::{max, min}; +use std::collections::BTreeSet; +use types::{ + consts::altair::{ + NUM_FLAG_INDICES, PARTICIPATION_FLAG_WEIGHTS, TIMELY_HEAD_FLAG_INDEX, + TIMELY_TARGET_FLAG_INDEX, WEIGHT_DENOMINATOR, + }, + ActivationQueue, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ExitCache, ForkName, + ParticipationFlags, ProgressiveBalancesCache, Unsigned, Validator, +}; + +pub struct SinglePassConfig { + pub inactivity_updates: bool, + pub rewards_and_penalties: bool, + pub registry_updates: bool, + pub slashings: bool, + pub effective_balance_updates: bool, +} + +impl Default for SinglePassConfig { + fn default() -> SinglePassConfig { + Self::enable_all() + } +} + +impl SinglePassConfig { + pub fn enable_all() -> SinglePassConfig { + Self { + inactivity_updates: true, + rewards_and_penalties: true, + registry_updates: true, + slashings: true, + effective_balance_updates: true, + } + } + + pub fn disable_all() -> SinglePassConfig { + SinglePassConfig { + inactivity_updates: false, + rewards_and_penalties: false, + registry_updates: false, + slashings: false, + effective_balance_updates: false, + } + } +} + +/// Values from the state that are immutable throughout epoch processing. +struct StateContext { + current_epoch: Epoch, + next_epoch: Epoch, + is_in_inactivity_leak: bool, + total_active_balance: u64, + churn_limit: u64, + fork_name: ForkName, +} + +struct RewardsAndPenaltiesContext { + unslashed_participating_increments_array: [u64; NUM_FLAG_INDICES], + active_increments: u64, +} + +struct SlashingsContext { + adjusted_total_slashing_balance: u64, + target_withdrawable_epoch: Epoch, +} + +struct EffectiveBalancesContext { + downward_threshold: u64, + upward_threshold: u64, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct ValidatorInfo { + pub index: usize, + pub effective_balance: u64, + pub base_reward: u64, + pub is_eligible: bool, + pub is_slashed: bool, + pub is_active_current_epoch: bool, + pub is_active_previous_epoch: bool, + // Used for determining rewards. + pub previous_epoch_participation: ParticipationFlags, + // Used for updating the progressive balances cache for next epoch. + pub current_epoch_participation: ParticipationFlags, +} + +impl ValidatorInfo { + #[inline] + pub fn is_unslashed_participating_index(&self, flag_index: usize) -> Result { + Ok(self.is_active_previous_epoch + && !self.is_slashed + && self + .previous_epoch_participation + .has_flag(flag_index) + .map_err(|_| Error::InvalidFlagIndex(flag_index))?) + } +} + +pub fn process_epoch_single_pass( + state: &mut BeaconState, + spec: &ChainSpec, + conf: SinglePassConfig, +) -> Result, Error> { + initialize_epoch_cache(state, spec)?; + initialize_progressive_balances_cache(state, None, spec)?; + state.build_exit_cache(spec)?; + + let previous_epoch = state.previous_epoch(); + let current_epoch = state.current_epoch(); + let next_epoch = state.next_epoch()?; + let is_in_inactivity_leak = state.is_in_inactivity_leak(previous_epoch, spec)?; + let total_active_balance = state.get_total_active_balance()?; + let churn_limit = state.get_churn_limit(spec)?; + let activation_churn_limit = state.get_activation_churn_limit(spec)?; + let finalized_checkpoint = state.finalized_checkpoint(); + let fork_name = state.fork_name_unchecked(); + + let state_ctxt = &StateContext { + current_epoch, + next_epoch, + is_in_inactivity_leak, + total_active_balance, + churn_limit, + fork_name, + }; + + // Contexts that require immutable access to `state`. + let slashings_ctxt = &SlashingsContext::new(state, state_ctxt, spec)?; + let mut next_epoch_cache = PreEpochCache::new_for_next_epoch(state)?; + + // Split the state into several disjoint mutable borrows. + let ( + validators, + balances, + previous_epoch_participation, + current_epoch_participation, + inactivity_scores, + progressive_balances, + exit_cache, + epoch_cache, + ) = state.mutable_validator_fields()?; + + let num_validators = validators.len(); + + // Take a snapshot of the validators and participation before mutating. This is used for + // informational purposes (e.g. by the validator monitor). + let summary = ParticipationEpochSummary::new( + validators.clone(), + previous_epoch_participation.clone(), + current_epoch_participation.clone(), + previous_epoch, + current_epoch, + ); + + // Compute shared values required for different parts of epoch processing. + let rewards_ctxt = &RewardsAndPenaltiesContext::new(progressive_balances, state_ctxt, spec)?; + let activation_queue = &epoch_cache + .activation_queue()? + .get_validators_eligible_for_activation( + finalized_checkpoint.epoch, + activation_churn_limit as usize, + ); + let effective_balances_ctxt = &EffectiveBalancesContext::new(spec)?; + + // Iterate over the validators and related fields in one pass. + let mut validators_iter = validators.iter_mut(); + let mut balances_iter = balances.iter_mut(); + let mut inactivity_scores_iter = inactivity_scores.iter_mut(); + + // Values computed for the next epoch transition. + let mut next_epoch_total_active_balance = 0; + let mut next_epoch_activation_queue = ActivationQueue::default(); + + for (index, &previous_epoch_participation, ¤t_epoch_participation) in izip!( + 0..num_validators, + previous_epoch_participation.iter(), + current_epoch_participation.iter(), + ) { + let validator = validators_iter + .next() + .ok_or(BeaconStateError::UnknownValidator(index))?; + let balance = balances_iter + .next() + .ok_or(BeaconStateError::UnknownValidator(index))?; + let inactivity_score = inactivity_scores_iter + .next() + .ok_or(BeaconStateError::UnknownValidator(index))?; + + let is_active_current_epoch = validator.is_active_at(current_epoch); + let is_active_previous_epoch = validator.is_active_at(previous_epoch); + let is_eligible = is_active_previous_epoch + || (validator.slashed && previous_epoch.safe_add(1)? < validator.withdrawable_epoch); + + let base_reward = if is_eligible { + epoch_cache.get_base_reward(index)? + } else { + 0 + }; + + let validator_info = &ValidatorInfo { + index, + effective_balance: validator.effective_balance, + base_reward, + is_eligible, + is_slashed: validator.slashed, + is_active_current_epoch, + is_active_previous_epoch, + previous_epoch_participation, + current_epoch_participation, + }; + + if current_epoch != E::genesis_epoch() { + // `process_inactivity_updates` + if conf.inactivity_updates { + process_single_inactivity_update( + inactivity_score, + validator_info, + state_ctxt, + spec, + )?; + } + + // `process_rewards_and_penalties` + if conf.rewards_and_penalties { + process_single_reward_and_penalty( + balance, + &inactivity_score, + validator_info, + rewards_ctxt, + state_ctxt, + spec, + )?; + } + } + + // `process_registry_updates` + if conf.registry_updates { + process_single_registry_update( + validator, + validator_info, + exit_cache, + activation_queue, + &mut next_epoch_activation_queue, + state_ctxt, + spec, + )?; + } + + // `process_slashings` + if conf.slashings { + process_single_slashing(balance, &validator, slashings_ctxt, state_ctxt, spec)?; + } + + // `process_effective_balance_updates` + if conf.effective_balance_updates { + process_single_effective_balance_update( + *balance, + validator, + validator_info, + &mut next_epoch_total_active_balance, + &mut next_epoch_cache, + progressive_balances, + effective_balances_ctxt, + state_ctxt, + spec, + )?; + } + } + + if conf.effective_balance_updates { + state.set_total_active_balance(next_epoch, next_epoch_total_active_balance); + *state.epoch_cache_mut() = next_epoch_cache.into_epoch_cache( + next_epoch_total_active_balance, + next_epoch_activation_queue, + spec, + )?; + } + + Ok(summary) +} + +fn process_single_inactivity_update( + inactivity_score: &mut u64, + validator_info: &ValidatorInfo, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + if !validator_info.is_eligible { + return Ok(()); + } + + // Increase inactivity score of inactive validators + if validator_info.is_unslashed_participating_index(TIMELY_TARGET_FLAG_INDEX)? { + // Avoid mutating when the inactivity score is 0 and can't go any lower -- the common + // case. + if *inactivity_score == 0 { + return Ok(()); + } + inactivity_score.safe_sub_assign(1)?; + } else { + inactivity_score.safe_add_assign(spec.inactivity_score_bias)?; + } + + // Decrease the score of all validators for forgiveness when not during a leak + if !state_ctxt.is_in_inactivity_leak { + let deduction = min(spec.inactivity_score_recovery_rate, *inactivity_score); + inactivity_score.safe_sub_assign(deduction)?; + } + + Ok(()) +} + +fn process_single_reward_and_penalty( + balance: &mut u64, + inactivity_score: &u64, + validator_info: &ValidatorInfo, + rewards_ctxt: &RewardsAndPenaltiesContext, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + if !validator_info.is_eligible { + return Ok(()); + } + + let mut delta = Delta::default(); + for flag_index in 0..NUM_FLAG_INDICES { + get_flag_index_delta( + &mut delta, + validator_info, + flag_index, + rewards_ctxt, + state_ctxt, + )?; + } + get_inactivity_penalty_delta( + &mut delta, + validator_info, + inactivity_score, + state_ctxt, + spec, + )?; + + if delta.rewards != 0 || delta.penalties != 0 { + balance.safe_add_assign(delta.rewards)?; + *balance = balance.saturating_sub(delta.penalties); + } + + Ok(()) +} + +fn get_flag_index_delta( + delta: &mut Delta, + validator_info: &ValidatorInfo, + flag_index: usize, + rewards_ctxt: &RewardsAndPenaltiesContext, + state_ctxt: &StateContext, +) -> Result<(), Error> { + let base_reward = validator_info.base_reward; + let weight = get_flag_weight(flag_index)?; + let unslashed_participating_increments = + rewards_ctxt.get_unslashed_participating_increments(flag_index)?; + + if validator_info.is_unslashed_participating_index(flag_index)? { + if !state_ctxt.is_in_inactivity_leak { + let reward_numerator = base_reward + .safe_mul(weight)? + .safe_mul(unslashed_participating_increments)?; + delta.reward( + reward_numerator.safe_div( + rewards_ctxt + .active_increments + .safe_mul(WEIGHT_DENOMINATOR)?, + )?, + )?; + } + } else if flag_index != TIMELY_HEAD_FLAG_INDEX { + delta.penalize(base_reward.safe_mul(weight)?.safe_div(WEIGHT_DENOMINATOR)?)?; + } + Ok(()) +} + +/// Get the weight for a `flag_index` from the constant list of all weights. +fn get_flag_weight(flag_index: usize) -> Result { + PARTICIPATION_FLAG_WEIGHTS + .get(flag_index) + .copied() + .ok_or(Error::InvalidFlagIndex(flag_index)) +} + +fn get_inactivity_penalty_delta( + delta: &mut Delta, + validator_info: &ValidatorInfo, + inactivity_score: &u64, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + if !validator_info.is_unslashed_participating_index(TIMELY_TARGET_FLAG_INDEX)? { + let penalty_numerator = validator_info + .effective_balance + .safe_mul(*inactivity_score)?; + let penalty_denominator = spec + .inactivity_score_bias + .safe_mul(spec.inactivity_penalty_quotient_for_fork(state_ctxt.fork_name))?; + delta.penalize(penalty_numerator.safe_div(penalty_denominator)?)?; + } + Ok(()) +} + +impl RewardsAndPenaltiesContext { + fn new( + progressive_balances: &ProgressiveBalancesCache, + state_ctxt: &StateContext, + spec: &ChainSpec, + ) -> Result { + let mut unslashed_participating_increments_array = [0; NUM_FLAG_INDICES]; + for flag_index in 0..NUM_FLAG_INDICES { + let unslashed_participating_balance = + progressive_balances.previous_epoch_flag_attesting_balance(flag_index)?; + let unslashed_participating_increments = + unslashed_participating_balance.safe_div(spec.effective_balance_increment)?; + + *unslashed_participating_increments_array + .get_mut(flag_index) + .ok_or(Error::InvalidFlagIndex(flag_index))? = unslashed_participating_increments; + } + let active_increments = state_ctxt + .total_active_balance + .safe_div(spec.effective_balance_increment)?; + + Ok(Self { + unslashed_participating_increments_array, + active_increments, + }) + } + + fn get_unslashed_participating_increments(&self, flag_index: usize) -> Result { + self.unslashed_participating_increments_array + .get(flag_index) + .copied() + .ok_or(Error::InvalidFlagIndex(flag_index)) + } +} + +fn process_single_registry_update( + validator: &mut Validator, + validator_info: &ValidatorInfo, + exit_cache: &mut ExitCache, + activation_queue: &BTreeSet, + next_epoch_activation_queue: &mut ActivationQueue, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + let current_epoch = state_ctxt.current_epoch; + + if validator.is_eligible_for_activation_queue(spec) { + validator.activation_eligibility_epoch = current_epoch.safe_add(1)?; + } + + if validator.is_active_at(current_epoch) && validator.effective_balance <= spec.ejection_balance + { + initiate_validator_exit(validator, exit_cache, state_ctxt, spec)?; + } + + if activation_queue.contains(&validator_info.index) { + validator.activation_epoch = spec.compute_activation_exit_epoch(current_epoch)?; + } + + // Caching: add to speculative activation queue for next epoch. + next_epoch_activation_queue.add_if_could_be_eligible_for_activation( + validator_info.index, + validator, + state_ctxt.next_epoch, + spec, + ); + + Ok(()) +} + +fn initiate_validator_exit( + validator: &mut Validator, + exit_cache: &mut ExitCache, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + // Return if the validator already initiated exit + if validator.exit_epoch != spec.far_future_epoch { + return Ok(()); + } + + // Compute exit queue epoch + let delayed_epoch = spec.compute_activation_exit_epoch(state_ctxt.current_epoch)?; + let mut exit_queue_epoch = exit_cache + .max_epoch()? + .map_or(delayed_epoch, |epoch| max(epoch, delayed_epoch)); + let exit_queue_churn = exit_cache.get_churn_at(exit_queue_epoch)?; + + if exit_queue_churn >= state_ctxt.churn_limit { + exit_queue_epoch.safe_add_assign(1)?; + } + + validator.exit_epoch = exit_queue_epoch; + validator.withdrawable_epoch = + exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?; + + exit_cache.record_validator_exit(exit_queue_epoch)?; + Ok(()) +} + +impl SlashingsContext { + fn new( + state: &BeaconState, + state_ctxt: &StateContext, + spec: &ChainSpec, + ) -> Result { + let sum_slashings = state.get_all_slashings().iter().copied().safe_sum()?; + let adjusted_total_slashing_balance = min( + sum_slashings.safe_mul(spec.proportional_slashing_multiplier_for_state(state))?, + state_ctxt.total_active_balance, + ); + + let target_withdrawable_epoch = state_ctxt + .current_epoch + .safe_add(E::EpochsPerSlashingsVector::to_u64().safe_div(2)?)?; + + Ok(Self { + adjusted_total_slashing_balance, + target_withdrawable_epoch, + }) + } +} + +fn process_single_slashing( + balance: &mut u64, + validator: &Validator, + slashings_ctxt: &SlashingsContext, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + if validator.slashed && slashings_ctxt.target_withdrawable_epoch == validator.withdrawable_epoch + { + let increment = spec.effective_balance_increment; + let penalty_numerator = validator + .effective_balance + .safe_div(increment)? + .safe_mul(slashings_ctxt.adjusted_total_slashing_balance)?; + let penalty = penalty_numerator + .safe_div(state_ctxt.total_active_balance)? + .safe_mul(increment)?; + + *balance = balance.saturating_sub(penalty); + } + Ok(()) +} + +impl EffectiveBalancesContext { + fn new(spec: &ChainSpec) -> Result { + let hysteresis_increment = spec + .effective_balance_increment + .safe_div(spec.hysteresis_quotient)?; + let downward_threshold = + hysteresis_increment.safe_mul(spec.hysteresis_downward_multiplier)?; + let upward_threshold = hysteresis_increment.safe_mul(spec.hysteresis_upward_multiplier)?; + + Ok(Self { + downward_threshold, + upward_threshold, + }) + } +} + +#[allow(clippy::too_many_arguments)] +fn process_single_effective_balance_update( + balance: u64, + validator: &mut Validator, + validator_info: &ValidatorInfo, + next_epoch_total_active_balance: &mut u64, + next_epoch_cache: &mut PreEpochCache, + progressive_balances: &mut ProgressiveBalancesCache, + eb_ctxt: &EffectiveBalancesContext, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + let old_effective_balance = validator.effective_balance; + let new_effective_balance = if balance.safe_add(eb_ctxt.downward_threshold)? + < validator.effective_balance + || validator + .effective_balance + .safe_add(eb_ctxt.upward_threshold)? + < balance + { + min( + balance.safe_sub(balance.safe_rem(spec.effective_balance_increment)?)?, + spec.max_effective_balance, + ) + } else { + validator.effective_balance + }; + + if validator.is_active_at(state_ctxt.next_epoch) { + next_epoch_total_active_balance.safe_add_assign(new_effective_balance)?; + } + + if new_effective_balance != old_effective_balance { + validator.effective_balance = new_effective_balance; + + // Update progressive balances cache for the *current* epoch, which will soon become the + // previous epoch once the epoch transition completes. + progressive_balances.on_effective_balance_change( + validator.slashed, + validator_info.current_epoch_participation, + old_effective_balance, + new_effective_balance, + )?; + } + + // Caching: update next epoch effective balances. + next_epoch_cache.push_effective_balance(new_effective_balance); + + Ok(()) +} diff --git a/consensus/state_processing/src/per_epoch_processing/slashings.rs b/consensus/state_processing/src/per_epoch_processing/slashings.rs index 2d595491c11..456cb26598d 100644 --- a/consensus/state_processing/src/per_epoch_processing/slashings.rs +++ b/consensus/state_processing/src/per_epoch_processing/slashings.rs @@ -1,6 +1,10 @@ -use crate::per_epoch_processing::Error; +use crate::common::decrease_balance; +use crate::per_epoch_processing::{ + single_pass::{process_epoch_single_pass, SinglePassConfig}, + Error, +}; use safe_arith::{SafeArith, SafeArithIter}; -use types::{BeaconState, BeaconStateError, ChainSpec, EthSpec, Unsigned}; +use types::{BeaconState, ChainSpec, EthSpec, Unsigned}; /// Process slashings. pub fn process_slashings( @@ -16,28 +20,44 @@ pub fn process_slashings( total_balance, ); - let (validators, balances, _) = state.validators_and_balances_and_progressive_balances_mut(); - for (index, validator) in validators.iter().enumerate() { - if validator.slashed - && epoch.safe_add(T::EpochsPerSlashingsVector::to_u64().safe_div(2)?)? - == validator.withdrawable_epoch - { - let increment = spec.effective_balance_increment; - let penalty_numerator = validator - .effective_balance - .safe_div(increment)? - .safe_mul(adjusted_total_slashing_balance)?; - let penalty = penalty_numerator - .safe_div(total_balance)? - .safe_mul(increment)?; + let target_withdrawable_epoch = + epoch.safe_add(T::EpochsPerSlashingsVector::to_u64().safe_div(2)?)?; + let indices = state + .validators() + .iter() + .enumerate() + .filter(|(_, validator)| { + validator.slashed && target_withdrawable_epoch == validator.withdrawable_epoch + }) + .map(|(index, validator)| (index, validator.effective_balance)) + .collect::>(); - // Equivalent to `decrease_balance(state, index, penalty)`, but avoids borrowing `state`. - let balance = balances - .get_mut(index) - .ok_or(BeaconStateError::BalancesOutOfBounds(index))?; - *balance = balance.saturating_sub(penalty); - } + for (index, validator_effective_balance) in indices { + let increment = spec.effective_balance_increment; + let penalty_numerator = validator_effective_balance + .safe_div(increment)? + .safe_mul(adjusted_total_slashing_balance)?; + let penalty = penalty_numerator + .safe_div(total_balance)? + .safe_mul(increment)?; + + decrease_balance(state, index, penalty)?; } Ok(()) } + +pub fn process_slashings_slow( + state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), Error> { + process_epoch_single_pass( + state, + spec, + SinglePassConfig { + slashings: true, + ..SinglePassConfig::disable_all() + }, + )?; + Ok(()) +} diff --git a/consensus/state_processing/src/per_slot_processing.rs b/consensus/state_processing/src/per_slot_processing.rs index e89a78c4d84..322aa2df300 100644 --- a/consensus/state_processing/src/per_slot_processing.rs +++ b/consensus/state_processing/src/per_slot_processing.rs @@ -65,6 +65,11 @@ pub fn per_slot_processing( if spec.deneb_fork_epoch == Some(state.current_epoch()) { upgrade_to_deneb(state, spec)?; } + + // Additionally build all caches so that all valid states that are advanced always have + // committee caches built, and we don't have to worry about initialising them at higher + // layers. + state.build_caches(spec)?; } Ok(summary) diff --git a/consensus/state_processing/src/upgrade/altair.rs b/consensus/state_processing/src/upgrade/altair.rs index 5bb4f0bd592..589e4c7a479 100644 --- a/consensus/state_processing/src/upgrade/altair.rs +++ b/consensus/state_processing/src/upgrade/altair.rs @@ -3,8 +3,8 @@ use crate::common::{get_attestation_participation_flag_indices, get_attesting_in use std::mem; use std::sync::Arc; use types::{ - BeaconState, BeaconStateAltair, BeaconStateError as Error, ChainSpec, EthSpec, Fork, - ParticipationFlags, PendingAttestation, RelativeEpoch, SyncCommittee, VariableList, + BeaconState, BeaconStateAltair, BeaconStateError as Error, ChainSpec, EpochCache, EthSpec, + Fork, ParticipationFlags, PendingAttestation, RelativeEpoch, SyncCommittee, VariableList, }; /// Translate the participation information from the epoch prior to the fork into Altair's format. @@ -106,6 +106,8 @@ pub fn upgrade_to_altair( committee_caches: mem::take(&mut pre.committee_caches), pubkey_cache: mem::take(&mut pre.pubkey_cache), exit_cache: mem::take(&mut pre.exit_cache), + slashings_cache: mem::take(&mut pre.slashings_cache), + epoch_cache: EpochCache::default(), tree_hash_cache: mem::take(&mut pre.tree_hash_cache), }); diff --git a/consensus/state_processing/src/upgrade/capella.rs b/consensus/state_processing/src/upgrade/capella.rs index 5153e35f447..87b40abebdd 100644 --- a/consensus/state_processing/src/upgrade/capella.rs +++ b/consensus/state_processing/src/upgrade/capella.rs @@ -1,6 +1,8 @@ -use ssz_types::VariableList; use std::mem; -use types::{BeaconState, BeaconStateCapella, BeaconStateError as Error, ChainSpec, EthSpec, Fork}; +use types::{ + BeaconState, BeaconStateCapella, BeaconStateError as Error, ChainSpec, EpochCache, EthSpec, + Fork, VariableList, +}; /// Transform a `Merge` state into an `Capella` state. pub fn upgrade_to_capella( @@ -66,6 +68,8 @@ pub fn upgrade_to_capella( committee_caches: mem::take(&mut pre.committee_caches), pubkey_cache: mem::take(&mut pre.pubkey_cache), exit_cache: mem::take(&mut pre.exit_cache), + slashings_cache: mem::take(&mut pre.slashings_cache), + epoch_cache: EpochCache::default(), tree_hash_cache: mem::take(&mut pre.tree_hash_cache), }); diff --git a/consensus/state_processing/src/upgrade/deneb.rs b/consensus/state_processing/src/upgrade/deneb.rs index c253a8c1627..43fe5d9dc3d 100644 --- a/consensus/state_processing/src/upgrade/deneb.rs +++ b/consensus/state_processing/src/upgrade/deneb.rs @@ -1,5 +1,7 @@ use std::mem; -use types::{BeaconState, BeaconStateDeneb, BeaconStateError as Error, ChainSpec, EthSpec, Fork}; +use types::{ + BeaconState, BeaconStateDeneb, BeaconStateError as Error, ChainSpec, EpochCache, EthSpec, Fork, +}; /// Transform a `Capella` state into an `Deneb` state. pub fn upgrade_to_deneb( @@ -67,6 +69,8 @@ pub fn upgrade_to_deneb( committee_caches: mem::take(&mut pre.committee_caches), pubkey_cache: mem::take(&mut pre.pubkey_cache), exit_cache: mem::take(&mut pre.exit_cache), + slashings_cache: mem::take(&mut pre.slashings_cache), + epoch_cache: EpochCache::default(), tree_hash_cache: mem::take(&mut pre.tree_hash_cache), }); diff --git a/consensus/state_processing/src/upgrade/merge.rs b/consensus/state_processing/src/upgrade/merge.rs index eb744501072..147c97ac29e 100644 --- a/consensus/state_processing/src/upgrade/merge.rs +++ b/consensus/state_processing/src/upgrade/merge.rs @@ -1,6 +1,6 @@ use std::mem; use types::{ - BeaconState, BeaconStateError as Error, BeaconStateMerge, ChainSpec, EthSpec, + BeaconState, BeaconStateError as Error, BeaconStateMerge, ChainSpec, EpochCache, EthSpec, ExecutionPayloadHeaderMerge, Fork, }; @@ -64,6 +64,8 @@ pub fn upgrade_to_bellatrix( committee_caches: mem::take(&mut pre.committee_caches), pubkey_cache: mem::take(&mut pre.pubkey_cache), exit_cache: mem::take(&mut pre.exit_cache), + slashings_cache: mem::take(&mut pre.slashings_cache), + epoch_cache: EpochCache::default(), tree_hash_cache: mem::take(&mut pre.tree_hash_cache), }); diff --git a/consensus/types/benches/benches.rs b/consensus/types/benches/benches.rs index bb2b527109f..7a48c058ca5 100644 --- a/consensus/types/benches/benches.rs +++ b/consensus/types/benches/benches.rs @@ -31,7 +31,7 @@ fn get_state(validator_count: usize) -> BeaconState { .collect::>() .par_iter() .map(|&i| Validator { - pubkey: generate_deterministic_keypair(i).pk.into(), + pubkey: generate_deterministic_keypair(i).pk.compress(), withdrawal_credentials: Hash256::from_low_u64_le(i as u64), effective_balance: spec.max_effective_balance, slashed: false, diff --git a/consensus/types/src/activation_queue.rs b/consensus/types/src/activation_queue.rs new file mode 100644 index 00000000000..5ec4ef66895 --- /dev/null +++ b/consensus/types/src/activation_queue.rs @@ -0,0 +1,38 @@ +use crate::{ChainSpec, Epoch, Validator}; +use std::collections::BTreeSet; + +/// Activation queue computed during epoch processing for use in the *next* epoch. +#[derive(Debug, PartialEq, Eq, Default, Clone, arbitrary::Arbitrary)] +pub struct ActivationQueue { + /// Validators represented by `(activation_eligibility_epoch, index)` in sorted order. + queue: BTreeSet<(Epoch, usize)>, +} + +impl ActivationQueue { + pub fn add_if_could_be_eligible_for_activation( + &mut self, + index: usize, + validator: &Validator, + next_epoch: Epoch, + spec: &ChainSpec, + ) { + if validator.could_be_eligible_for_activation_at(next_epoch, spec) { + self.queue + .insert((validator.activation_eligibility_epoch, index)); + } + } + + pub fn get_validators_eligible_for_activation( + &self, + finalized_epoch: Epoch, + churn_limit: usize, + ) -> BTreeSet { + self.queue + .iter() + .filter_map(|&(eligibility_epoch, index)| { + (eligibility_epoch <= finalized_epoch).then_some(index) + }) + .take(churn_limit) + .collect() + } +} diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 4c0ee1bfa20..1adf76d4b20 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -1,5 +1,4 @@ use self::committee_cache::get_active_validator_indices; -use self::exit_cache::ExitCache; use crate::test_utils::TestRandom; use crate::*; use compare_fields::CompareFields; @@ -7,12 +6,12 @@ use compare_fields_derive::CompareFields; use derivative::Derivative; use ethereum_hashing::hash; use int_to_bytes::{int_to_bytes4, int_to_bytes8}; -use pubkey_cache::PubkeyCache; +pub use pubkey_cache::PubkeyCache; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; use ssz::{ssz_encode, Decode, DecodeError, Encode}; use ssz_derive::{Decode, Encode}; -use ssz_types::{typenum::Unsigned, BitVector, FixedVector}; +use ssz_types::{typenum::Unsigned, BitVector}; use std::convert::TryInto; use std::hash::Hash; use std::{fmt, mem, sync::Arc}; @@ -27,7 +26,10 @@ pub use self::committee_cache::{ CommitteeCache, }; pub use crate::beacon_state::balance::Balance; +pub use crate::beacon_state::exit_cache::ExitCache; pub use crate::beacon_state::progressive_balances_cache::*; +pub use crate::beacon_state::slashings_cache::SlashingsCache; +use crate::epoch_cache::EpochCache; use crate::historical_summary::HistoricalSummary; pub use clone_config::CloneConfig; pub use eth_spec::*; @@ -42,12 +44,16 @@ mod exit_cache; mod iter; mod progressive_balances_cache; mod pubkey_cache; +mod slashings_cache; mod tests; mod tree_hash_cache; pub const CACHED_EPOCHS: usize = 3; const MAX_RANDOM_BYTE: u64 = (1 << 8) - 1; +pub type Validators = VariableList::ValidatorRegistryLimit>; +pub type Balances = VariableList::ValidatorRegistryLimit>; + #[derive(Debug, PartialEq, Clone)] pub enum Error { /// A state for a different hard-fork was required -- a severe logic error. @@ -99,6 +105,10 @@ pub enum Error { }, RelativeEpochError(RelativeEpochError), ExitCacheUninitialized, + SlashingsCacheUninitialized { + initialized_slot: Option, + latest_block_slot: Slot, + }, CommitteeCacheUninitialized(Option), SyncCommitteeCacheUninitialized, BlsError(bls::Error), @@ -134,7 +144,21 @@ pub enum Error { current_epoch: Epoch, epoch: Epoch, }, + CommitteeCacheDiffInvalidEpoch { + prev_current_epoch: Epoch, + current_epoch: Epoch, + }, + CommitteeCacheDiffUninitialized { + expected_epoch: Epoch, + }, + DiffAcrossFork { + prev_fork: ForkName, + current_fork: ForkName, + }, + TotalActiveBalanceDiffUninitialized, + MissingImmutableValidator(usize), IndexNotSupported(usize), + InvalidFlagIndex(usize), MerkleTreeError(merkle_proof::MerkleTreeError), } @@ -352,6 +376,17 @@ where #[ssz(skip_serializing, skip_deserializing)] #[tree_hash(skip_hashing)] #[test_random(default)] + pub slashings_cache: SlashingsCache, + /// Epoch cache of values that are useful for block processing that are static over an epoch. + #[serde(skip_serializing, skip_deserializing)] + #[ssz(skip_serializing, skip_deserializing)] + #[tree_hash(skip_hashing)] + #[test_random(default)] + pub epoch_cache: EpochCache, + #[serde(skip_serializing, skip_deserializing)] + #[ssz(skip_serializing, skip_deserializing)] + #[tree_hash(skip_hashing)] + #[test_random(default)] #[derivative(Clone(clone_with = "clone_default"))] pub tree_hash_cache: BeaconTreeHashCache, } @@ -419,6 +454,8 @@ impl BeaconState { ], pubkey_cache: PubkeyCache::default(), exit_cache: ExitCache::default(), + slashings_cache: SlashingsCache::default(), + epoch_cache: EpochCache::default(), tree_hash_cache: <_>::default(), }) } @@ -485,7 +522,7 @@ impl BeaconState { Hash256::from_slice(&self.tree_hash_root()[..]) } - pub fn historical_batch(&self) -> HistoricalBatch { + pub fn historical_batch(&mut self) -> HistoricalBatch { HistoricalBatch { block_roots: self.block_roots().clone(), state_roots: self.state_roots().clone(), @@ -500,6 +537,21 @@ impl BeaconState { Ok(self.pubkey_cache().get(pubkey)) } + /// Immutable variant of `get_validator_index` which errors if the cache is not up to date. + pub fn get_validator_index_read_only( + &self, + pubkey: &PublicKeyBytes, + ) -> Result, Error> { + let pubkey_cache = self.pubkey_cache(); + if pubkey_cache.len() != self.validators().len() { + return Err(Error::PubkeyCacheIncomplete { + cache_len: pubkey_cache.len(), + registry_len: self.validators().len(), + }); + } + Ok(pubkey_cache.get(pubkey)) + } + /// The epoch corresponding to `self.slot()`. pub fn current_epoch(&self) -> Epoch { self.slot().epoch(T::slots_per_epoch()) @@ -510,10 +562,8 @@ impl BeaconState { /// If the current epoch is the genesis epoch, the genesis_epoch is returned. pub fn previous_epoch(&self) -> Epoch { let current_epoch = self.current_epoch(); - if current_epoch > T::genesis_epoch() { - current_epoch - .safe_sub(1) - .expect("current epoch greater than genesis implies greater than 0") + if let Ok(prev_epoch) = current_epoch.safe_sub(1) { + prev_epoch } else { current_epoch } @@ -882,14 +932,16 @@ impl BeaconState { &mut self, sync_committee: &SyncCommittee, ) -> Result, Error> { - let mut indices = Vec::with_capacity(sync_committee.pubkeys.len()); - for pubkey in sync_committee.pubkeys.iter() { - indices.push( - self.get_validator_index(pubkey)? - .ok_or(Error::PubkeyCacheInconsistent)?, - ) - } - Ok(indices) + self.update_pubkey_cache()?; + sync_committee + .pubkeys + .iter() + .map(|pubkey| { + self.pubkey_cache() + .get(pubkey) + .ok_or(Error::PubkeyCacheInconsistent) + }) + .collect() } /// Compute the sync committee indices for the next sync committee. @@ -1202,7 +1254,11 @@ impl BeaconState { /// Convenience accessor for validators and balances simultaneously. pub fn validators_and_balances_and_progressive_balances_mut( &mut self, - ) -> (&mut [Validator], &mut [u64], &mut ProgressiveBalancesCache) { + ) -> ( + &mut Validators, + &mut Balances, + &mut ProgressiveBalancesCache, + ) { match self { BeaconState::Base(state) => ( &mut state.validators, @@ -1232,6 +1288,74 @@ impl BeaconState { } } + #[allow(clippy::type_complexity)] + pub fn mutable_validator_fields( + &mut self, + ) -> Result< + ( + &mut Validators, + &mut Balances, + &VariableList, + &VariableList, + &mut VariableList, + &mut ProgressiveBalancesCache, + &mut ExitCache, + &mut EpochCache, + ), + Error, + > { + match self { + BeaconState::Base(_) => Err(Error::IncorrectStateVariant), + BeaconState::Altair(state) => Ok(( + &mut state.validators, + &mut state.balances, + &state.previous_epoch_participation, + &state.current_epoch_participation, + &mut state.inactivity_scores, + &mut state.progressive_balances_cache, + &mut state.exit_cache, + &mut state.epoch_cache, + )), + BeaconState::Merge(state) => Ok(( + &mut state.validators, + &mut state.balances, + &state.previous_epoch_participation, + &state.current_epoch_participation, + &mut state.inactivity_scores, + &mut state.progressive_balances_cache, + &mut state.exit_cache, + &mut state.epoch_cache, + )), + BeaconState::Capella(state) => Ok(( + &mut state.validators, + &mut state.balances, + &state.previous_epoch_participation, + &state.current_epoch_participation, + &mut state.inactivity_scores, + &mut state.progressive_balances_cache, + &mut state.exit_cache, + &mut state.epoch_cache, + )), + BeaconState::Deneb(state) => Ok(( + &mut state.validators, + &mut state.balances, + &state.previous_epoch_participation, + &state.current_epoch_participation, + &mut state.inactivity_scores, + &mut state.progressive_balances_cache, + &mut state.exit_cache, + &mut state.epoch_cache, + )), + } + } + + /// Get a mutable reference to the balance of a single validator. + pub fn get_balance_mut(&mut self, validator_index: usize) -> Result<&mut u64, Error> { + self.balances_mut() + .get_mut(validator_index) + .ok_or(Error::BalancesOutOfBounds(validator_index)) + } + /// Generate a seed for the given `epoch`. pub fn get_seed( &self, @@ -1306,13 +1430,6 @@ impl BeaconState { .ok_or(Error::InactivityScoresOutOfBounds(validator_index)) } - /// Get a mutable reference to the balance of a single validator. - pub fn get_balance_mut(&mut self, validator_index: usize) -> Result<&mut u64, Error> { - self.balances_mut() - .get_mut(validator_index) - .ok_or(Error::BalancesOutOfBounds(validator_index)) - } - /// Return the epoch at which an activation or exit triggered in ``epoch`` takes effect. /// /// Spec v0.12.1 @@ -1321,7 +1438,7 @@ impl BeaconState { epoch: Epoch, spec: &ChainSpec, ) -> Result { - Ok(epoch.safe_add(1)?.safe_add(spec.max_seed_lookahead)?) + Ok(spec.compute_activation_exit_epoch(epoch)?) } /// Return the churn limit for the current epoch (number of validators who can leave per epoch). @@ -1391,6 +1508,28 @@ impl BeaconState { )) } + pub fn compute_total_active_balance( + &self, + epoch: Epoch, + spec: &ChainSpec, + ) -> Result { + if epoch != self.current_epoch() && epoch != self.next_epoch()? { + return Err(Error::EpochOutOfBounds); + } + + let mut total_active_balance = 0; + + for validator in self.validators() { + if validator.is_active_at(epoch) { + total_active_balance.safe_add_assign(validator.effective_balance)?; + } + } + Ok(std::cmp::max( + total_active_balance, + spec.effective_balance_increment, + )) + } + /// Implementation of `get_total_active_balance`, matching the spec. /// /// Requires the total active balance cache to be initialised, which is initialised whenever @@ -1398,33 +1537,47 @@ impl BeaconState { /// /// Returns minimum `EFFECTIVE_BALANCE_INCREMENT`, to avoid div by 0. pub fn get_total_active_balance(&self) -> Result { + self.get_total_active_balance_at_epoch(self.current_epoch()) + } + + pub fn get_total_active_balance_at_epoch(&self, epoch: Epoch) -> Result { let (initialized_epoch, balance) = self .total_active_balance() .ok_or(Error::TotalActiveBalanceCacheUninitialized)?; - let current_epoch = self.current_epoch(); - if initialized_epoch == current_epoch { + if initialized_epoch == epoch { Ok(balance) } else { Err(Error::TotalActiveBalanceCacheInconsistent { initialized_epoch, - current_epoch, + current_epoch: epoch, }) } } + pub fn set_total_active_balance(&mut self, epoch: Epoch, balance: u64) { + *self.total_active_balance_mut() = Some((epoch, balance)); + } + /// Build the total active balance cache. - /// - /// This function requires the current committee cache to be already built. It is called - /// automatically when `build_committee_cache` is called for the current epoch. - fn build_total_active_balance_cache(&mut self, spec: &ChainSpec) -> Result<(), Error> { - // Order is irrelevant, so use the cached indices. - let current_epoch = self.current_epoch(); - let total_active_balance = self.get_total_balance( - self.get_cached_active_validator_indices(RelativeEpoch::Current)?, - spec, - )?; - *self.total_active_balance_mut() = Some((current_epoch, total_active_balance)); + pub fn build_total_active_balance_cache_at( + &mut self, + epoch: Epoch, + spec: &ChainSpec, + ) -> Result<(), Error> { + if self.get_total_active_balance_at_epoch(epoch).is_err() { + self.force_build_total_active_balance_cache_at(epoch, spec)?; + } + Ok(()) + } + + pub fn force_build_total_active_balance_cache_at( + &mut self, + epoch: Epoch, + spec: &ChainSpec, + ) -> Result<(), Error> { + let total_active_balance = self.compute_total_active_balance(epoch, spec)?; + *self.total_active_balance_mut() = Some((epoch, total_active_balance)); Ok(()) } @@ -1437,8 +1590,10 @@ impl BeaconState { pub fn get_epoch_participation_mut( &mut self, epoch: Epoch, + previous_epoch: Epoch, + current_epoch: Epoch, ) -> Result<&mut VariableList, Error> { - if epoch == self.current_epoch() { + if epoch == current_epoch { match self { BeaconState::Base(_) => Err(BeaconStateError::IncorrectStateVariant), BeaconState::Altair(state) => Ok(&mut state.current_epoch_participation), @@ -1446,7 +1601,7 @@ impl BeaconState { BeaconState::Capella(state) => Ok(&mut state.current_epoch_participation), BeaconState::Deneb(state) => Ok(&mut state.current_epoch_participation), } - } else if epoch == self.previous_epoch() { + } else if epoch == previous_epoch { match self { BeaconState::Base(_) => Err(BeaconStateError::IncorrectStateVariant), BeaconState::Altair(state) => Ok(&mut state.previous_epoch_participation), @@ -1477,6 +1632,7 @@ impl BeaconState { self.build_all_committee_caches(spec)?; self.update_pubkey_cache()?; self.build_exit_cache(spec)?; + self.build_slashings_cache()?; Ok(()) } @@ -1497,6 +1653,20 @@ impl BeaconState { Ok(()) } + /// Build the slashings cache if it needs to be built. + pub fn build_slashings_cache(&mut self) -> Result<(), Error> { + let latest_block_slot = self.latest_block_header().slot; + if !self.slashings_cache().is_initialized(latest_block_slot) { + *self.slashings_cache_mut() = SlashingsCache::new(latest_block_slot, self.validators()); + } + Ok(()) + } + + pub fn slashings_cache_is_initialized(&self) -> bool { + let latest_block_slot = self.latest_block_header().slot; + self.slashings_cache().is_initialized(latest_block_slot) + } + /// Drop all caches on the state. pub fn drop_all_caches(&mut self) -> Result<(), Error> { self.drop_total_active_balance_cache(); @@ -1504,9 +1674,10 @@ impl BeaconState { self.drop_committee_cache(RelativeEpoch::Current)?; self.drop_committee_cache(RelativeEpoch::Next)?; self.drop_pubkey_cache(); - self.drop_tree_hash_cache(); self.drop_progressive_balances_cache(); *self.exit_cache_mut() = ExitCache::default(); + *self.slashings_cache_mut() = SlashingsCache::default(); + *self.epoch_cache_mut() = EpochCache::default(); Ok(()) } @@ -1535,7 +1706,7 @@ impl BeaconState { } if self.total_active_balance().is_none() && relative_epoch == RelativeEpoch::Current { - self.build_total_active_balance_cache(spec)?; + self.build_total_active_balance_cache_at(self.current_epoch(), spec)?; } Ok(()) } @@ -1571,40 +1742,15 @@ impl BeaconState { /// /// Note: this function will not build any new committee caches, but will build the total /// balance cache if the (new) current epoch cache is initialized. - pub fn advance_caches(&mut self, spec: &ChainSpec) -> Result<(), Error> { + pub fn advance_caches(&mut self, _spec: &ChainSpec) -> Result<(), Error> { self.committee_caches_mut().rotate_left(1); - // Re-compute total active balance for current epoch. - // - // This can only be computed once the state's effective balances have been updated - // for the current epoch. I.e. it is not possible to know this value with the same - // lookahead as the committee shuffling. - let curr = Self::committee_cache_index(RelativeEpoch::Current); - let curr_cache = mem::take(self.committee_cache_at_index_mut(curr)?); - - // If current epoch cache is initialized, compute the total active balance from its - // indices. We check that the cache is initialized at the _next_ epoch because the slot has - // not yet been advanced. - let new_current_epoch = self.next_epoch()?; - if curr_cache.is_initialized_at(new_current_epoch) { - *self.total_active_balance_mut() = Some(( - new_current_epoch, - self.get_total_balance(curr_cache.active_validator_indices(), spec)?, - )); - } - // If the cache is not initialized, then the previous cached value for the total balance is - // wrong, so delete it. - else { - self.drop_total_active_balance_cache(); - } - *self.committee_cache_at_index_mut(curr)? = curr_cache; - let next = Self::committee_cache_index(RelativeEpoch::Next); *self.committee_cache_at_index_mut(next)? = CommitteeCache::default(); Ok(()) } - fn committee_cache_index(relative_epoch: RelativeEpoch) -> usize { + pub(crate) fn committee_cache_index(relative_epoch: RelativeEpoch) -> usize { match relative_epoch { RelativeEpoch::Previous => 0, RelativeEpoch::Current => 1, @@ -1794,9 +1940,8 @@ impl BeaconState { pub fn is_eligible_validator( &self, previous_epoch: Epoch, - val_index: usize, + val: &Validator, ) -> Result { - let val = self.get_validator(val_index)?; Ok(val.is_active_at(previous_epoch) || (val.slashed && previous_epoch.safe_add(Epoch::new(1))? < val.withdrawable_epoch)) } @@ -1836,6 +1981,10 @@ impl BeaconState { Ok(sync_committee) } + pub fn get_base_reward(&self, validator_index: usize) -> Result { + self.epoch_cache().get_base_reward(validator_index) + } + pub fn compute_merkle_proof( &mut self, generalized_index: usize, diff --git a/consensus/types/src/beacon_state/committee_cache.rs b/consensus/types/src/beacon_state/committee_cache.rs index 8d29bc22171..dee74b0252c 100644 --- a/consensus/types/src/beacon_state/committee_cache.rs +++ b/consensus/types/src/beacon_state/committee_cache.rs @@ -3,6 +3,7 @@ use super::BeaconState; use crate::*; use core::num::NonZeroUsize; +use derivative::Derivative; use safe_arith::SafeArith; use serde::{Deserialize, Serialize}; use ssz::{four_byte_option_impl, Decode, DecodeError, Encode}; @@ -19,16 +20,44 @@ four_byte_option_impl!(four_byte_option_non_zero_usize, NonZeroUsize); /// Computes and stores the shuffling for an epoch. Provides various getters to allow callers to /// read the committees for the given epoch. -#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Derivative, Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derivative(PartialEq)] pub struct CommitteeCache { #[ssz(with = "four_byte_option_epoch")] initialized_epoch: Option, shuffling: Vec, + #[derivative(PartialEq(compare_with = "compare_shuffling_positions"))] shuffling_positions: Vec, committees_per_slot: u64, slots_per_epoch: u64, } +/// Equivalence function for `shuffling_positions` that ignores trailing `None` entries. +/// +/// It can happen that states from different epochs computing the same cache have different +/// numbers of validators in `state.validators()` due to recent deposits. These new validators +/// cannot be active however and will always be ommitted from the shuffling. This function checks +/// that two lists of shuffling positions are equivalent by ensuring that they are identical on all +/// common entries, and that new entries at the end are all `None`. +/// +/// In practice this is only used in tests. +#[allow(clippy::indexing_slicing)] +fn compare_shuffling_positions(xs: &Vec, ys: &Vec) -> bool { + use std::cmp::Ordering; + + let (shorter, longer) = match xs.len().cmp(&ys.len()) { + Ordering::Equal => { + return xs == ys; + } + Ordering::Less => (xs, ys), + Ordering::Greater => (ys, xs), + }; + shorter == &longer[..shorter.len()] + && longer[shorter.len()..] + .iter() + .all(|new| *new == NonZeroUsizeOption(None)) +} + impl CommitteeCache { /// Return a new, fully initialized cache. /// @@ -322,17 +351,21 @@ pub fn epoch_committee_count(committees_per_slot: usize, slots_per_epoch: usize) /// `epoch`. /// /// Spec v0.12.1 -pub fn get_active_validator_indices(validators: &[Validator], epoch: Epoch) -> Vec { - let mut active = Vec::with_capacity(validators.len()); +pub fn get_active_validator_indices<'a, V, I>(validators: V, epoch: Epoch) -> Vec +where + V: IntoIterator, + I: ExactSizeIterator + Iterator, +{ + let iter = validators.into_iter(); + + let mut active = Vec::with_capacity(iter.len()); - for (index, validator) in validators.iter().enumerate() { + for (index, validator) in iter.enumerate() { if validator.is_active_at(epoch) { active.push(index) } } - active.shrink_to_fit(); - active } diff --git a/consensus/types/src/beacon_state/iter.rs b/consensus/types/src/beacon_state/iter.rs index 2c00913ce96..4b97cae7aed 100644 --- a/consensus/types/src/beacon_state/iter.rs +++ b/consensus/types/src/beacon_state/iter.rs @@ -74,7 +74,7 @@ mod test { let mut state: BeaconState = BeaconState::new(0, <_>::default(), &spec); for i in 0..state.block_roots().len() { - state.block_roots_mut()[i] = root_slot(i).1; + *state.block_roots_mut().get_mut(i).unwrap() = root_slot(i).1; } assert_eq!( @@ -122,7 +122,7 @@ mod test { let mut state: BeaconState = BeaconState::new(0, <_>::default(), &spec); for i in 0..state.block_roots().len() { - state.block_roots_mut()[i] = root_slot(i).1; + *state.block_roots_mut().get_mut(i).unwrap() = root_slot(i).1; } assert_eq!( diff --git a/consensus/types/src/beacon_state/progressive_balances_cache.rs b/consensus/types/src/beacon_state/progressive_balances_cache.rs index 6c0682480bf..d0e0010b93b 100644 --- a/consensus/types/src/beacon_state/progressive_balances_cache.rs +++ b/consensus/types/src/beacon_state/progressive_balances_cache.rs @@ -1,5 +1,11 @@ use crate::beacon_state::balance::Balance; -use crate::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec}; +use crate::{ + consts::altair::{ + NUM_FLAG_INDICES, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, + TIMELY_TARGET_FLAG_INDEX, + }, + BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ParticipationFlags, +}; use arbitrary::Arbitrary; use safe_arith::SafeArith; use serde::{Deserialize, Serialize}; @@ -17,21 +23,120 @@ pub struct ProgressiveBalancesCache { #[derive(Debug, PartialEq, Arbitrary, Clone)] struct Inner { pub current_epoch: Epoch, - pub previous_epoch_target_attesting_balance: Balance, - pub current_epoch_target_attesting_balance: Balance, + pub previous_epoch_cache: EpochTotalBalances, + pub current_epoch_cache: EpochTotalBalances, +} + +/// Caches the participation values for one epoch (either the previous or current). +#[derive(PartialEq, Debug, Clone, Arbitrary)] +pub struct EpochTotalBalances { + /// Stores the sum of the balances for all validators in `self.unslashed_participating_indices` + /// for all flags in `NUM_FLAG_INDICES`. + /// + /// A flag balance is only incremented if a validator is in that flag set. + pub total_flag_balances: [Balance; NUM_FLAG_INDICES], +} + +impl EpochTotalBalances { + pub fn new(spec: &ChainSpec) -> Self { + let zero_balance = Balance::zero(spec.effective_balance_increment); + + Self { + total_flag_balances: [zero_balance; NUM_FLAG_INDICES], + } + } + + /// Returns the total balance of attesters who have `flag_index` set. + pub fn total_flag_balance(&self, flag_index: usize) -> Result { + self.total_flag_balances + .get(flag_index) + .map(Balance::get) + .ok_or(BeaconStateError::InvalidFlagIndex(flag_index)) + } + + /// Returns the raw total balance of attesters who have `flag_index` set. + pub fn total_flag_balance_raw(&self, flag_index: usize) -> Result { + self.total_flag_balances + .get(flag_index) + .copied() + .ok_or(BeaconStateError::InvalidFlagIndex(flag_index)) + } + + pub fn on_new_attestation( + &mut self, + is_slashed: bool, + flag_index: usize, + validator_effective_balance: u64, + ) -> Result<(), BeaconStateError> { + if is_slashed { + return Ok(()); + } + let balance = self + .total_flag_balances + .get_mut(flag_index) + .ok_or(BeaconStateError::InvalidFlagIndex(flag_index))?; + balance.safe_add_assign(validator_effective_balance)?; + Ok(()) + } + + pub fn on_slashing( + &mut self, + participation_flags: ParticipationFlags, + validator_effective_balance: u64, + ) -> Result<(), BeaconStateError> { + for flag_index in 0..NUM_FLAG_INDICES { + if participation_flags.has_flag(flag_index)? { + self.total_flag_balances + .get_mut(flag_index) + .ok_or(BeaconStateError::InvalidFlagIndex(flag_index))? + .safe_sub_assign(validator_effective_balance)?; + } + } + Ok(()) + } + + pub fn on_effective_balance_change( + &mut self, + is_slashed: bool, + current_epoch_participation_flags: ParticipationFlags, + old_effective_balance: u64, + new_effective_balance: u64, + ) -> Result<(), BeaconStateError> { + // If the validator is slashed then we should not update the effective balance, because this + // validator's effective balance has already been removed from the totals. + if is_slashed { + return Ok(()); + } + for flag_index in 0..NUM_FLAG_INDICES { + if current_epoch_participation_flags.has_flag(flag_index)? { + let total = self + .total_flag_balances + .get_mut(flag_index) + .ok_or(BeaconStateError::InvalidFlagIndex(flag_index))?; + if new_effective_balance > old_effective_balance { + total + .safe_add_assign(new_effective_balance.safe_sub(old_effective_balance)?)?; + } else { + total + .safe_sub_assign(old_effective_balance.safe_sub(new_effective_balance)?)?; + } + } + } + Ok(()) + } } impl ProgressiveBalancesCache { pub fn initialize( &mut self, current_epoch: Epoch, - previous_epoch_target_attesting_balance: Balance, - current_epoch_target_attesting_balance: Balance, + previous_epoch_cache: EpochTotalBalances, + current_epoch_cache: EpochTotalBalances, ) { self.inner = Some(Inner { current_epoch, - previous_epoch_target_attesting_balance, - current_epoch_target_attesting_balance, + previous_epoch_cache, + current_epoch_cache, }); } @@ -39,24 +144,36 @@ impl ProgressiveBalancesCache { self.inner.is_some() } + pub fn is_initialized_at(&self, epoch: Epoch) -> bool { + self.inner + .as_ref() + .map_or(false, |inner| inner.current_epoch == epoch) + } + /// When a new target attestation has been processed, we update the cached /// `current_epoch_target_attesting_balance` to include the validator effective balance. /// If the epoch is neither the current epoch nor the previous epoch, an error is returned. - pub fn on_new_target_attestation( + pub fn on_new_attestation( &mut self, epoch: Epoch, + is_slashed: bool, + flag_index: usize, validator_effective_balance: u64, ) -> Result<(), BeaconStateError> { let cache = self.get_inner_mut()?; if epoch == cache.current_epoch { - cache - .current_epoch_target_attesting_balance - .safe_add_assign(validator_effective_balance)?; + cache.current_epoch_cache.on_new_attestation( + is_slashed, + flag_index, + validator_effective_balance, + )?; } else if epoch.safe_add(1)? == cache.current_epoch { - cache - .previous_epoch_target_attesting_balance - .safe_add_assign(validator_effective_balance)?; + cache.previous_epoch_cache.on_new_attestation( + is_slashed, + flag_index, + validator_effective_balance, + )?; } else { return Err(BeaconStateError::ProgressiveBalancesCacheInconsistent); } @@ -68,21 +185,17 @@ impl ProgressiveBalancesCache { /// validator's effective balance to exclude the validator weight. pub fn on_slashing( &mut self, - is_previous_epoch_target_attester: bool, - is_current_epoch_target_attester: bool, + previous_epoch_participation: ParticipationFlags, + current_epoch_participation: ParticipationFlags, effective_balance: u64, ) -> Result<(), BeaconStateError> { let cache = self.get_inner_mut()?; - if is_previous_epoch_target_attester { - cache - .previous_epoch_target_attesting_balance - .safe_sub_assign(effective_balance)?; - } - if is_current_epoch_target_attester { - cache - .current_epoch_target_attesting_balance - .safe_sub_assign(effective_balance)?; - } + cache + .previous_epoch_cache + .on_slashing(previous_epoch_participation, effective_balance)?; + cache + .current_epoch_cache + .on_slashing(current_epoch_participation, effective_balance)?; Ok(()) } @@ -90,22 +203,18 @@ impl ProgressiveBalancesCache { /// its share of the target attesting balance in the cache. pub fn on_effective_balance_change( &mut self, - is_current_epoch_target_attester: bool, + is_slashed: bool, + current_epoch_participation: ParticipationFlags, old_effective_balance: u64, new_effective_balance: u64, ) -> Result<(), BeaconStateError> { let cache = self.get_inner_mut()?; - if is_current_epoch_target_attester { - if new_effective_balance > old_effective_balance { - cache - .current_epoch_target_attesting_balance - .safe_add_assign(new_effective_balance.safe_sub(old_effective_balance)?)?; - } else { - cache - .current_epoch_target_attesting_balance - .safe_sub_assign(old_effective_balance.safe_sub(new_effective_balance)?)?; - } - } + cache.current_epoch_cache.on_effective_balance_change( + is_slashed, + current_epoch_participation, + old_effective_balance, + new_effective_balance, + )?; Ok(()) } @@ -114,25 +223,53 @@ impl ProgressiveBalancesCache { pub fn on_epoch_transition(&mut self, spec: &ChainSpec) -> Result<(), BeaconStateError> { let cache = self.get_inner_mut()?; cache.current_epoch.safe_add_assign(1)?; - cache.previous_epoch_target_attesting_balance = - cache.current_epoch_target_attesting_balance; - cache.current_epoch_target_attesting_balance = - Balance::zero(spec.effective_balance_increment); + cache.previous_epoch_cache = std::mem::replace( + &mut cache.current_epoch_cache, + EpochTotalBalances::new(spec), + ); Ok(()) } + pub fn previous_epoch_flag_attesting_balance( + &self, + flag_index: usize, + ) -> Result { + self.get_inner()? + .previous_epoch_cache + .total_flag_balance(flag_index) + } + + pub fn current_epoch_flag_attesting_balance( + &self, + flag_index: usize, + ) -> Result { + self.get_inner()? + .current_epoch_cache + .total_flag_balance(flag_index) + } + + pub fn previous_epoch_source_attesting_balance(&self) -> Result { + self.previous_epoch_flag_attesting_balance(TIMELY_SOURCE_FLAG_INDEX) + } + pub fn previous_epoch_target_attesting_balance(&self) -> Result { - Ok(self - .get_inner()? - .previous_epoch_target_attesting_balance - .get()) + self.previous_epoch_flag_attesting_balance(TIMELY_TARGET_FLAG_INDEX) + } + + pub fn previous_epoch_head_attesting_balance(&self) -> Result { + self.previous_epoch_flag_attesting_balance(TIMELY_HEAD_FLAG_INDEX) + } + + pub fn current_epoch_source_attesting_balance(&self) -> Result { + self.current_epoch_flag_attesting_balance(TIMELY_SOURCE_FLAG_INDEX) } pub fn current_epoch_target_attesting_balance(&self) -> Result { - Ok(self - .get_inner()? - .current_epoch_target_attesting_balance - .get()) + self.current_epoch_flag_attesting_balance(TIMELY_TARGET_FLAG_INDEX) + } + + pub fn current_epoch_head_attesting_balance(&self) -> Result { + self.current_epoch_flag_attesting_balance(TIMELY_HEAD_FLAG_INDEX) } fn get_inner_mut(&mut self) -> Result<&mut Inner, BeaconStateError> { @@ -158,7 +295,7 @@ pub enum ProgressiveBalancesMode { /// Enable the usage of progressive cache, with checks against the `ParticipationCache` and falls /// back to the existing calculation if there is a balance mismatch. Checked, - /// Enable the usage of progressive cache, with checks against the `ParticipationCache`. Errors + /// Enable the usage of progressive cache, with checks against the `ParticipationCache`. BeaconStateErrors /// if there is a balance mismatch. Used in testing only. Strict, /// Enable the usage of progressive cache, with no comparative checks against the diff --git a/consensus/types/src/beacon_state/slashings_cache.rs b/consensus/types/src/beacon_state/slashings_cache.rs new file mode 100644 index 00000000000..cfdc349f86c --- /dev/null +++ b/consensus/types/src/beacon_state/slashings_cache.rs @@ -0,0 +1,63 @@ +use crate::{BeaconStateError, Slot, Validator}; +use arbitrary::Arbitrary; +use std::collections::HashSet; + +/// Persistent (cheap to clone) cache of all slashed validator indices. +#[derive(Debug, Default, Clone, PartialEq, Arbitrary)] +pub struct SlashingsCache { + latest_block_slot: Option, + #[arbitrary(default)] + slashed_validators: HashSet, +} + +impl SlashingsCache { + /// Initialize a new cache for the given list of validators. + pub fn new<'a, V, I>(latest_block_slot: Slot, validators: V) -> Self + where + V: IntoIterator, + I: ExactSizeIterator + Iterator, + { + let slashed_validators = validators + .into_iter() + .enumerate() + .filter_map(|(i, validator)| validator.slashed.then_some(i)) + .collect(); + Self { + latest_block_slot: Some(latest_block_slot), + slashed_validators, + } + } + + pub fn is_initialized(&self, slot: Slot) -> bool { + self.latest_block_slot == Some(slot) + } + + pub fn check_initialized(&self, latest_block_slot: Slot) -> Result<(), BeaconStateError> { + if self.is_initialized(latest_block_slot) { + Ok(()) + } else { + Err(BeaconStateError::SlashingsCacheUninitialized { + initialized_slot: self.latest_block_slot, + latest_block_slot, + }) + } + } + + pub fn record_validator_slashing( + &mut self, + block_slot: Slot, + validator_index: usize, + ) -> Result<(), BeaconStateError> { + self.check_initialized(block_slot)?; + self.slashed_validators.insert(validator_index); + Ok(()) + } + + pub fn is_slashed(&self, validator_index: usize) -> bool { + self.slashed_validators.contains(&validator_index) + } + + pub fn update_latest_block_slot(&mut self, latest_block_slot: Slot) { + self.latest_block_slot = Some(latest_block_slot); + } +} diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index a182c0f98d6..d3d81ca78a0 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -2,8 +2,8 @@ use crate::application_domain::{ApplicationDomain, APPLICATION_DOMAIN_BUILDER}; use crate::blob_sidecar::BlobIdentifier; use crate::*; use int_to_bytes::int_to_bytes4; -use serde::Deserialize; -use serde::{Deserializer, Serialize, Serializer}; +use safe_arith::{ArithError, SafeArith}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_utils::quoted_u64::MaybeQuoted; use ssz::Encode; use std::fs::File; @@ -297,6 +297,13 @@ impl ChainSpec { } } + /// Return the name of the fork activated at `slot`, if any. + pub fn fork_activated_at_slot(&self, slot: Slot) -> Option { + let prev_slot_fork = self.fork_name_at_slot::(slot.saturating_sub(Slot::new(1))); + let slot_fork = self.fork_name_at_slot::(slot); + (slot_fork != prev_slot_fork).then_some(slot_fork) + } + /// Returns the fork version for a named fork. pub fn fork_version_for_name(&self, fork_name: ForkName) -> [u8; 4] { match fork_name { @@ -320,13 +327,18 @@ impl ChainSpec { } /// For a given `BeaconState`, return the inactivity penalty quotient associated with its variant. + // FIXME(sproul): delete once unused pub fn inactivity_penalty_quotient_for_state(&self, state: &BeaconState) -> u64 { - match state { - BeaconState::Base(_) => self.inactivity_penalty_quotient, - BeaconState::Altair(_) => self.inactivity_penalty_quotient_altair, - BeaconState::Merge(_) => self.inactivity_penalty_quotient_bellatrix, - BeaconState::Capella(_) => self.inactivity_penalty_quotient_bellatrix, - BeaconState::Deneb(_) => self.inactivity_penalty_quotient_bellatrix, + self.inactivity_penalty_quotient_for_fork(state.fork_name_unchecked()) + } + + pub fn inactivity_penalty_quotient_for_fork(&self, fork_name: ForkName) -> u64 { + match fork_name { + ForkName::Base => self.inactivity_penalty_quotient, + ForkName::Altair => self.inactivity_penalty_quotient_altair, + ForkName::Merge => self.inactivity_penalty_quotient_bellatrix, + ForkName::Capella => self.inactivity_penalty_quotient_bellatrix, + ForkName::Deneb => self.inactivity_penalty_quotient_bellatrix, } } @@ -496,6 +508,16 @@ impl ChainSpec { Hash256::from(domain) } + pub fn compute_activation_exit_epoch(&self, epoch: Epoch) -> Result { + epoch.safe_add(1)?.safe_add(self.max_seed_lookahead) + } + + #[allow(clippy::arithmetic_side_effects)] + pub const fn attestation_subnet_prefix_bits(&self) -> u32 { + let attestation_subnet_count_bits = self.attestation_subnet_count.ilog2(); + self.attestation_subnet_extra_bits as u32 + attestation_subnet_count_bits + } + pub fn maximum_gossip_clock_disparity(&self) -> Duration { Duration::from_millis(self.maximum_gossip_clock_disparity_millis) } diff --git a/consensus/types/src/epoch_cache.rs b/consensus/types/src/epoch_cache.rs new file mode 100644 index 00000000000..1f717f2fcaf --- /dev/null +++ b/consensus/types/src/epoch_cache.rs @@ -0,0 +1,142 @@ +use crate::{ActivationQueue, BeaconStateError, ChainSpec, Epoch, EthSpec, Hash256, Slot}; +use safe_arith::{ArithError, SafeArith}; +use std::sync::Arc; + +/// Cache of values which are uniquely determined at the start of an epoch. +/// +/// The values are fixed with respect to the last block of the _prior_ epoch, which we refer +/// to as the "decision block". This cache is very similar to the `BeaconProposerCache` in that +/// beacon proposers are determined at exactly the same time as the values in this cache, so +/// the keys for the two caches are identical. +#[derive(Debug, PartialEq, Eq, Clone, Default, arbitrary::Arbitrary)] +pub struct EpochCache { + inner: Option>, +} + +#[derive(Debug, PartialEq, Eq, Clone, arbitrary::Arbitrary)] +struct Inner { + /// Unique identifier for this cache, which can be used to check its validity before use + /// with any `BeaconState`. + key: EpochCacheKey, + /// Effective balance for every validator in this epoch. + effective_balances: Vec, + /// Base rewards for every effective balance increment (currently 0..32 ETH). + /// + /// Keyed by `effective_balance / effective_balance_increment`. + base_rewards: Vec, + /// Validator activation queue. + activation_queue: ActivationQueue, + /// Effective balance increment. + effective_balance_increment: u64, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, arbitrary::Arbitrary)] +pub struct EpochCacheKey { + pub epoch: Epoch, + pub decision_block_root: Hash256, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum EpochCacheError { + IncorrectEpoch { cache: Epoch, state: Epoch }, + IncorrectDecisionBlock { cache: Hash256, state: Hash256 }, + ValidatorIndexOutOfBounds { validator_index: usize }, + EffectiveBalanceOutOfBounds { effective_balance_eth: usize }, + InvalidSlot { slot: Slot }, + Arith(ArithError), + BeaconState(BeaconStateError), + CacheNotInitialized, +} + +impl From for EpochCacheError { + fn from(e: BeaconStateError) -> Self { + Self::BeaconState(e) + } +} + +impl From for EpochCacheError { + fn from(e: ArithError) -> Self { + Self::Arith(e) + } +} + +impl EpochCache { + pub fn new( + key: EpochCacheKey, + effective_balances: Vec, + base_rewards: Vec, + activation_queue: ActivationQueue, + spec: &ChainSpec, + ) -> EpochCache { + Self { + inner: Some(Arc::new(Inner { + key, + effective_balances, + base_rewards, + activation_queue, + effective_balance_increment: spec.effective_balance_increment, + })), + } + } + + pub fn check_validity( + &self, + current_epoch: Epoch, + state_decision_root: Hash256, + ) -> Result<(), EpochCacheError> { + let cache = self + .inner + .as_ref() + .ok_or(EpochCacheError::CacheNotInitialized)?; + if cache.key.epoch != current_epoch { + return Err(EpochCacheError::IncorrectEpoch { + cache: cache.key.epoch, + state: current_epoch, + }); + } + if cache.key.decision_block_root != state_decision_root { + return Err(EpochCacheError::IncorrectDecisionBlock { + cache: cache.key.decision_block_root, + state: state_decision_root, + }); + } + Ok(()) + } + + #[inline] + pub fn get_effective_balance(&self, validator_index: usize) -> Result { + self.inner + .as_ref() + .ok_or(EpochCacheError::CacheNotInitialized)? + .effective_balances + .get(validator_index) + .copied() + .ok_or(EpochCacheError::ValidatorIndexOutOfBounds { validator_index }) + } + + #[inline] + pub fn get_base_reward(&self, validator_index: usize) -> Result { + let inner = self + .inner + .as_ref() + .ok_or(EpochCacheError::CacheNotInitialized)?; + let effective_balance = self.get_effective_balance(validator_index)?; + let effective_balance_eth = + effective_balance.safe_div(inner.effective_balance_increment)? as usize; + inner + .base_rewards + .get(effective_balance_eth) + .copied() + .ok_or(EpochCacheError::EffectiveBalanceOutOfBounds { + effective_balance_eth, + }) + } + + pub fn activation_queue(&self) -> Result<&ActivationQueue, EpochCacheError> { + let inner = self + .inner + .as_ref() + .ok_or(EpochCacheError::CacheNotInitialized)?; + Ok(&inner.activation_queue) + } +} diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index b07b497a2ae..19fdba54f7a 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -74,6 +74,7 @@ pub mod voluntary_exit; pub mod withdrawal_credentials; #[macro_use] pub mod slot_epoch_macros; +pub mod activation_queue; pub mod config_and_preset; pub mod execution_block_header; pub mod fork_context; @@ -94,6 +95,7 @@ mod tree_hash_impls; pub mod validator_registration_data; pub mod withdrawal; +pub mod epoch_cache; pub mod slot_data; #[cfg(feature = "sqlite")] pub mod sqlite; @@ -105,6 +107,7 @@ pub mod runtime_var_list; use ethereum_types::{H160, H256}; +pub use crate::activation_queue::ActivationQueue; pub use crate::aggregate_and_proof::AggregateAndProof; pub use crate::attestation::{Attestation, Error as AttestationError}; pub use crate::attestation_data::AttestationData; @@ -132,6 +135,7 @@ pub use crate::deposit_data::DepositData; pub use crate::deposit_message::DepositMessage; pub use crate::deposit_tree_snapshot::{DepositTreeSnapshot, FinalizedExecutionBlock}; pub use crate::enr_fork_id::EnrForkId; +pub use crate::epoch_cache::{EpochCache, EpochCacheError, EpochCacheKey}; pub use crate::eth1_data::Eth1Data; pub use crate::eth_spec::EthSpecId; pub use crate::execution_block_hash::ExecutionBlockHash; diff --git a/consensus/types/src/validator.rs b/consensus/types/src/validator.rs index 8fbd9009ea5..98567cd1e6c 100644 --- a/consensus/types/src/validator.rs +++ b/consensus/types/src/validator.rs @@ -77,6 +77,22 @@ impl Validator { && self.activation_epoch == spec.far_future_epoch } + /// Returns `true` if the validator *could* be eligible for activation at `epoch`. + /// + /// Eligibility depends on finalization, so we assume best-possible finalization. This function + /// returning true is a necessary but *not sufficient* condition for a validator to activate in + /// the epoch transition at the end of `epoch`. + pub fn could_be_eligible_for_activation_at(&self, epoch: Epoch, spec: &ChainSpec) -> bool { + // Has not yet been activated + self.activation_epoch == spec.far_future_epoch + // Placement in queue could be finalized. + // + // NOTE: the epoch distance is 1 rather than 2 because we consider the activations that + // occur at the *end* of `epoch`, after `process_justification_and_finalization` has already + // updated the state's checkpoint. + && self.activation_eligibility_epoch < epoch + } + /// Returns `true` if the validator has eth1 withdrawal credential. pub fn has_eth1_withdrawal_credential(&self, spec: &ChainSpec) -> bool { self.withdrawal_credentials diff --git a/crypto/bls/src/generic_public_key.rs b/crypto/bls/src/generic_public_key.rs index 462e4cb2cb0..80b42dfa714 100644 --- a/crypto/bls/src/generic_public_key.rs +++ b/crypto/bls/src/generic_public_key.rs @@ -11,6 +11,9 @@ use tree_hash::TreeHash; /// The byte-length of a BLS public key when serialized in compressed form. pub const PUBLIC_KEY_BYTES_LEN: usize = 48; +/// The byte-length of a BLS public key when serialized in uncompressed form. +pub const PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN: usize = 96; + /// Represents the public key at infinity. pub const INFINITY_PUBLIC_KEY: [u8; PUBLIC_KEY_BYTES_LEN] = [ 0xc0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -23,8 +26,17 @@ pub trait TPublicKey: Sized + Clone { /// Serialize `self` as compressed bytes. fn serialize(&self) -> [u8; PUBLIC_KEY_BYTES_LEN]; + /// Serialize `self` as uncompressed bytes. + fn serialize_uncompressed(&self) -> [u8; PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN]; + /// Deserialize `self` from compressed bytes. fn deserialize(bytes: &[u8]) -> Result; + + /// Deserialize `self` from uncompressed bytes. + /// + /// This function *does not* perform thorough checks of the input bytes and should only be + /// used with bytes output from `Self::serialize_uncompressed`. + fn deserialize_uncompressed(bytes: &[u8]) -> Result; } /// A BLS public key that is generic across some BLS point (`Pub`). @@ -65,6 +77,11 @@ where self.point.serialize() } + /// Serialize `self` as uncompressed bytes. + pub fn serialize_uncompressed(&self) -> [u8; PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN] { + self.point.serialize_uncompressed() + } + /// Deserialize `self` from compressed bytes. pub fn deserialize(bytes: &[u8]) -> Result { if bytes == &INFINITY_PUBLIC_KEY[..] { @@ -75,6 +92,13 @@ where }) } } + + /// Deserialize `self` from compressed bytes. + pub fn deserialize_uncompressed(bytes: &[u8]) -> Result { + Ok(Self { + point: Pub::deserialize_uncompressed(bytes)?, + }) + } } impl Eq for GenericPublicKey {} diff --git a/crypto/bls/src/impls/blst.rs b/crypto/bls/src/impls/blst.rs index e831a175c7a..530d8a09fa2 100644 --- a/crypto/bls/src/impls/blst.rs +++ b/crypto/bls/src/impls/blst.rs @@ -1,10 +1,12 @@ use crate::{ generic_aggregate_public_key::TAggregatePublicKey, generic_aggregate_signature::TAggregateSignature, - generic_public_key::{GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN}, + generic_public_key::{ + GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, + }, generic_secret_key::TSecretKey, generic_signature::{TSignature, SIGNATURE_BYTES_LEN}, - Error, Hash256, ZeroizeHash, INFINITY_SIGNATURE, + BlstError, Error, Hash256, ZeroizeHash, INFINITY_SIGNATURE, }; pub use blst::min_pk as blst_core; use blst::{blst_scalar, BLST_ERROR}; @@ -122,6 +124,10 @@ impl TPublicKey for blst_core::PublicKey { self.compress() } + fn serialize_uncompressed(&self) -> [u8; PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN] { + blst_core::PublicKey::serialize(self) + } + fn deserialize(bytes: &[u8]) -> Result { // key_validate accepts uncompressed bytes too so enforce byte length here. // It also does subgroup checks, noting infinity check is done in `generic_public_key.rs`. @@ -133,6 +139,19 @@ impl TPublicKey for blst_core::PublicKey { } Self::key_validate(bytes).map_err(Into::into) } + + fn deserialize_uncompressed(bytes: &[u8]) -> Result { + if bytes.len() != PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN { + return Err(Error::InvalidByteLength { + got: bytes.len(), + expected: PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, + }); + } + // Ensure we use the `blst` function rather than the one from this trait. + let result: Result = Self::deserialize(bytes); + let key = result?; + Ok(key) + } } /// A wrapper that allows for `PartialEq` and `Clone` impls. diff --git a/crypto/bls/src/impls/fake_crypto.rs b/crypto/bls/src/impls/fake_crypto.rs index f2d8b79b986..a09fb347e6b 100644 --- a/crypto/bls/src/impls/fake_crypto.rs +++ b/crypto/bls/src/impls/fake_crypto.rs @@ -1,7 +1,9 @@ use crate::{ generic_aggregate_public_key::TAggregatePublicKey, generic_aggregate_signature::TAggregateSignature, - generic_public_key::{GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN}, + generic_public_key::{ + GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, + }, generic_secret_key::{TSecretKey, SECRET_KEY_BYTES_LEN}, generic_signature::{TSignature, SIGNATURE_BYTES_LEN}, Error, Hash256, ZeroizeHash, INFINITY_PUBLIC_KEY, INFINITY_SIGNATURE, @@ -46,11 +48,19 @@ impl TPublicKey for PublicKey { self.0 } + fn serialize_uncompressed(&self) -> [u8; PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN] { + panic!("fake_crypto does not support uncompressed keys") + } + fn deserialize(bytes: &[u8]) -> Result { let mut pubkey = Self::infinity(); pubkey.0[..].copy_from_slice(&bytes[0..PUBLIC_KEY_BYTES_LEN]); Ok(pubkey) } + + fn deserialize_uncompressed(_: &[u8]) -> Result { + panic!("fake_crypto does not support uncompressed keys") + } } impl Eq for PublicKey {} diff --git a/crypto/bls/src/impls/milagro.rs b/crypto/bls/src/impls/milagro.rs index eb4767d3c70..4c659331038 100644 --- a/crypto/bls/src/impls/milagro.rs +++ b/crypto/bls/src/impls/milagro.rs @@ -1,7 +1,9 @@ use crate::{ generic_aggregate_public_key::TAggregatePublicKey, generic_aggregate_signature::TAggregateSignature, - generic_public_key::{GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN}, + generic_public_key::{ + GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, + }, generic_secret_key::{TSecretKey, SECRET_KEY_BYTES_LEN}, generic_signature::{TSignature, SIGNATURE_BYTES_LEN}, Error, Hash256, ZeroizeHash, @@ -76,14 +78,20 @@ pub fn verify_signature_sets<'a>( impl TPublicKey for milagro::PublicKey { fn serialize(&self) -> [u8; PUBLIC_KEY_BYTES_LEN] { - let mut bytes = [0; PUBLIC_KEY_BYTES_LEN]; - bytes[..].copy_from_slice(&self.as_bytes()); - bytes + self.as_bytes() + } + + fn serialize_uncompressed(&self) -> [u8; PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN] { + self.as_uncompressed_bytes() } fn deserialize(bytes: &[u8]) -> Result { Self::from_bytes(bytes).map_err(Into::into) } + + fn deserialize_uncompressed(bytes: &[u8]) -> Result { + Self::from_uncompressed_bytes(bytes).map_err(Into::into) + } } impl TAggregatePublicKey for milagro::AggregatePublicKey { diff --git a/crypto/bls/src/lib.rs b/crypto/bls/src/lib.rs index 750e1bd5b80..e588b3b3550 100644 --- a/crypto/bls/src/lib.rs +++ b/crypto/bls/src/lib.rs @@ -35,7 +35,9 @@ mod zeroize_hash; pub mod impls; -pub use generic_public_key::{INFINITY_PUBLIC_KEY, PUBLIC_KEY_BYTES_LEN}; +pub use generic_public_key::{ + INFINITY_PUBLIC_KEY, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, +}; pub use generic_secret_key::SECRET_KEY_BYTES_LEN; pub use generic_signature::{INFINITY_SIGNATURE, SIGNATURE_BYTES_LEN}; pub use get_withdrawal_credentials::get_withdrawal_credentials; diff --git a/crypto/bls/tests/tests.rs b/crypto/bls/tests/tests.rs index ad498dbfa87..40c0a4a7937 100644 --- a/crypto/bls/tests/tests.rs +++ b/crypto/bls/tests/tests.rs @@ -341,6 +341,11 @@ macro_rules! test_suite { .assert_single_message_verify(true) } + #[test] + fn deserialize_infinity_public_key() { + PublicKey::deserialize(&bls::INFINITY_PUBLIC_KEY).unwrap_err(); + } + /// A helper struct to make it easer to deal with `SignatureSet` lifetimes. struct OwnedSignatureSet { signature: AggregateSignature, diff --git a/lcli/src/replace_state_pubkeys.rs b/lcli/src/replace_state_pubkeys.rs index e9e3388c065..4defbe0e962 100644 --- a/lcli/src/replace_state_pubkeys.rs +++ b/lcli/src/replace_state_pubkeys.rs @@ -42,7 +42,8 @@ pub fn run(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), let mut deposit_tree = DepositDataTree::create(&[], 0, DEPOSIT_TREE_DEPTH); let mut deposit_root = Hash256::zero(); - for (index, validator) in state.validators_mut().iter_mut().enumerate() { + let validators = state.validators_mut(); + for index in 0..validators.len() { let (secret, _) = recover_validator_secret_from_mnemonic(seed.as_bytes(), index as u32, KeyType::Voting) .map_err(|e| format!("Unable to generate validator key: {:?}", e))?; @@ -52,11 +53,11 @@ pub fn run(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), eprintln!("{}: {}", index, keypair.pk); - validator.pubkey = keypair.pk.into(); + validators.get_mut(index).unwrap().pubkey = keypair.pk.into(); // Update the deposit tree. let mut deposit_data = DepositData { - pubkey: validator.pubkey, + pubkey: validators.get(index).unwrap().pubkey, // Set this to a junk value since it's very time consuming to generate the withdrawal // keys and it's not useful for the time being. withdrawal_credentials: Hash256::zero(), diff --git a/lcli/src/skip_slots.rs b/lcli/src/skip_slots.rs index cdbacfe4d52..e277486b5d5 100644 --- a/lcli/src/skip_slots.rs +++ b/lcli/src/skip_slots.rs @@ -52,6 +52,7 @@ use eth2::{types::StateId, BeaconNodeHttpClient, SensitiveUrl, Timeouts}; use eth2_network_config::Eth2NetworkConfig; use ssz::Encode; use state_processing::state_advance::{complete_state_advance, partial_state_advance}; +use state_processing::AllCaches; use std::fs::File; use std::io::prelude::*; use std::path::PathBuf; @@ -115,7 +116,7 @@ pub fn run( let target_slot = initial_slot + slots; state - .build_caches(spec) + .build_all_caches(spec) .map_err(|e| format!("Unable to build caches: {:?}", e))?; let state_root = if let Some(root) = cli_state_root.or(state_root) { diff --git a/lcli/src/transition_blocks.rs b/lcli/src/transition_blocks.rs index 23b0ae26206..420a95be51e 100644 --- a/lcli/src/transition_blocks.rs +++ b/lcli/src/transition_blocks.rs @@ -75,8 +75,8 @@ use eth2_network_config::Eth2NetworkConfig; use ssz::Encode; use state_processing::state_advance::complete_state_advance; use state_processing::{ - block_signature_verifier::BlockSignatureVerifier, per_block_processing, BlockSignatureStrategy, - ConsensusContext, StateProcessingStrategy, VerifyBlockRoot, + block_signature_verifier::BlockSignatureVerifier, per_block_processing, AllCaches, + BlockSignatureStrategy, ConsensusContext, StateProcessingStrategy, VerifyBlockRoot, }; use std::borrow::Cow; use std::fs::File; @@ -211,7 +211,7 @@ pub fn run( if config.exclude_cache_builds { pre_state - .build_caches(spec) + .build_all_caches(spec) .map_err(|e| format!("Unable to build caches: {:?}", e))?; let state_root = pre_state .update_tree_hash_cache() @@ -232,6 +232,7 @@ pub fn run( */ let mut output_post_state = None; + let mut saved_ctxt = None; for i in 0..runs { let pre_state = pre_state.clone_with(CloneConfig::all()); let block = block.clone(); @@ -245,6 +246,7 @@ pub fn run( state_root_opt, &config, &validator_pubkey_cache, + &mut saved_ctxt, spec, )?; @@ -294,9 +296,12 @@ pub fn run( .map_err(|e| format!("Unable to write to output file: {:?}", e))?; } + drop(pre_state); + Ok(()) } +#[allow(clippy::too_many_arguments)] fn do_transition( mut pre_state: BeaconState, block_root: Hash256, @@ -304,12 +309,13 @@ fn do_transition( mut state_root_opt: Option, config: &Config, validator_pubkey_cache: &ValidatorPubkeyCache>, + saved_ctxt: &mut Option>, spec: &ChainSpec, ) -> Result, String> { if !config.exclude_cache_builds { let t = Instant::now(); pre_state - .build_caches(spec) + .build_all_caches(spec) .map_err(|e| format!("Unable to build caches: {:?}", e))?; debug!("Build caches: {:?}", t.elapsed()); @@ -337,15 +343,23 @@ fn do_transition( .map_err(|e| format!("Unable to perform complete advance: {e:?}"))?; debug!("Slot processing: {:?}", t.elapsed()); + // Slot and epoch processing should keep the caches fully primed. + assert!(pre_state.all_caches_built()); + let t = Instant::now(); pre_state - .build_caches(spec) + .build_all_caches(spec) .map_err(|e| format!("Unable to build caches: {:?}", e))?; debug!("Build all caches (again): {:?}", t.elapsed()); - let mut ctxt = ConsensusContext::new(pre_state.slot()) - .set_current_block_root(block_root) - .set_proposer_index(block.message().proposer_index()); + let mut ctxt = if let Some(ctxt) = saved_ctxt { + ctxt.clone() + } else { + let ctxt = ConsensusContext::new(pre_state.slot()) + .set_current_block_root(block_root) + .set_proposer_index(block.message().proposer_index()); + ctxt + }; if !config.no_signature_verification { let get_pubkey = move |validator_index| { diff --git a/testing/ef_tests/src/cases/bls_verify_msg.rs b/testing/ef_tests/src/cases/bls_verify_msg.rs index 24b62c5fa1d..dc918309b54 100644 --- a/testing/ef_tests/src/cases/bls_verify_msg.rs +++ b/testing/ef_tests/src/cases/bls_verify_msg.rs @@ -1,7 +1,7 @@ use super::*; use crate::case_result::compare_result; use crate::impl_bls_load_case; -use bls::{PublicKeyBytes, Signature, SignatureBytes}; +use bls::{PublicKey, PublicKeyBytes, Signature, SignatureBytes}; use serde::Deserialize; use std::convert::TryInto; use types::Hash256; @@ -30,6 +30,13 @@ impl Case for BlsVerify { .try_into() .and_then(|signature: Signature| { let pk = self.input.pubkey.decompress()?; + + // Check serialization roundtrip. + let pk_uncompressed = pk.serialize_uncompressed(); + let pk_from_uncompressed = PublicKey::deserialize_uncompressed(&pk_uncompressed) + .expect("uncompressed serialization should round-trip"); + assert_eq!(pk_from_uncompressed, pk); + Ok(signature.verify(&pk, Hash256::from_slice(&message))) }) .unwrap_or(false); diff --git a/testing/ef_tests/src/cases/epoch_processing.rs b/testing/ef_tests/src/cases/epoch_processing.rs index cf182af2b21..7ad38cb6f10 100644 --- a/testing/ef_tests/src/cases/epoch_processing.rs +++ b/testing/ef_tests/src/cases/epoch_processing.rs @@ -5,12 +5,17 @@ use crate::decode::{ssz_decode_state, yaml_decode_file}; use crate::type_name; use crate::type_name::TypeName; use serde::Deserialize; +use state_processing::common::update_progressive_balances_cache::initialize_progressive_balances_cache; +use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::per_epoch_processing::capella::process_historical_summaries_update; -use state_processing::per_epoch_processing::effective_balance_updates::process_effective_balance_updates; +use state_processing::per_epoch_processing::effective_balance_updates::{ + process_effective_balance_updates, process_effective_balance_updates_slow, +}; use state_processing::per_epoch_processing::{ altair, base, historical_roots_update::process_historical_roots_update, - process_registry_updates, process_slashings, + process_registry_updates, process_registry_updates_slow, process_slashings, + process_slashings_slow, resets::{process_eth1_data_reset, process_randao_mixes_reset, process_slashings_reset}, }; use state_processing::EpochProcessingError; @@ -105,11 +110,9 @@ impl EpochTransition for JustificationAndFinalization { | BeaconState::Merge(_) | BeaconState::Capella(_) | BeaconState::Deneb(_) => { + initialize_progressive_balances_cache(state, None, spec)?; let justification_and_finalization_state = - altair::process_justification_and_finalization( - state, - &altair::ParticipationCache::new(state, spec).unwrap(), - )?; + altair::process_justification_and_finalization(state)?; justification_and_finalization_state.apply_changes_to_state(state); Ok(()) } @@ -128,18 +131,20 @@ impl EpochTransition for RewardsAndPenalties { BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) - | BeaconState::Deneb(_) => altair::process_rewards_and_penalties( - state, - &altair::ParticipationCache::new(state, spec).unwrap(), - spec, - ), + | BeaconState::Deneb(_) => altair::process_rewards_and_penalties_slow(state, spec), } } } impl EpochTransition for RegistryUpdates { fn run(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), EpochProcessingError> { - process_registry_updates(state, spec) + initialize_epoch_cache(state, spec)?; + + if let BeaconState::Base(_) = state { + process_registry_updates(state, spec) + } else { + process_registry_updates_slow(state, spec) + } } } @@ -159,13 +164,7 @@ impl EpochTransition for Slashings { | BeaconState::Merge(_) | BeaconState::Capella(_) | BeaconState::Deneb(_) => { - process_slashings( - state, - altair::ParticipationCache::new(state, spec) - .unwrap() - .current_epoch_total_active_balance(), - spec, - )?; + process_slashings_slow(state, spec)?; } }; Ok(()) @@ -180,7 +179,11 @@ impl EpochTransition for Eth1DataReset { impl EpochTransition for EffectiveBalanceUpdates { fn run(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), EpochProcessingError> { - process_effective_balance_updates(state, None, spec) + if let BeaconState::Base(_) = state { + process_effective_balance_updates(state, spec) + } else { + process_effective_balance_updates_slow(state, spec) + } } } @@ -247,11 +250,7 @@ impl EpochTransition for InactivityUpdates { BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) - | BeaconState::Deneb(_) => altair::process_inactivity_updates( - state, - &altair::ParticipationCache::new(state, spec).unwrap(), - spec, - ), + | BeaconState::Deneb(_) => altair::process_inactivity_updates_slow(state, spec), } } } @@ -324,17 +323,20 @@ impl> Case for EpochProcessing { fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { self.metadata.bls_setting.unwrap_or_default().check()?; - let mut state = self.pre.clone(); - let mut expected = self.post.clone(); - let spec = &testing_spec::(fork_name); + let mut pre_state = self.pre.clone(); + + // Processing requires the committee caches. + pre_state.build_all_committee_caches(spec).unwrap(); - let mut result = (|| { - // Processing requires the committee caches. - state.build_all_committee_caches(spec)?; + let mut state = pre_state.clone(); + let mut expected = self.post.clone(); + + if let Some(post_state) = expected.as_mut() { + post_state.build_all_committee_caches(spec).unwrap(); + } - T::run(&mut state, spec).map(|_| state) - })(); + let mut result = T::run(&mut state, spec).map(|_| state); compare_beacon_state_results_without_caches(&mut result, &mut expected) } diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 4c02126d41a..14d047dae88 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -6,6 +6,7 @@ use crate::testing_spec; use serde::Deserialize; use ssz::Decode; use state_processing::common::update_progressive_balances_cache::initialize_progressive_balances_cache; +use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::{ per_block_processing::{ errors::BlockProcessingError, @@ -89,6 +90,7 @@ impl Operation for Attestation { spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { + initialize_epoch_cache(state, spec)?; let mut ctxt = ConsensusContext::new(state.slot()); match state { BeaconState::Base(_) => base::process_attestations( @@ -485,14 +487,22 @@ impl> Case for Operations { fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { let spec = &testing_spec::(fork_name); - let mut state = self.pre.clone(); - let mut expected = self.post.clone(); + let mut pre_state = self.pre.clone(); // Processing requires the committee caches. // NOTE: some of the withdrawals tests have 0 active validators, do not try // to build the commitee cache in this case. if O::handler_name() != "withdrawals" { - state.build_all_committee_caches(spec).unwrap(); + pre_state.build_all_committee_caches(spec).unwrap(); + } + + let mut state = pre_state.clone(); + let mut expected = self.post.clone(); + + if O::handler_name() != "withdrawals" { + if let Some(post_state) = expected.as_mut() { + post_state.build_all_committee_caches(spec).unwrap(); + } } let mut result = self diff --git a/testing/state_transition_vectors/src/exit.rs b/testing/state_transition_vectors/src/exit.rs index 50b98d3066d..8d7062038b1 100644 --- a/testing/state_transition_vectors/src/exit.rs +++ b/testing/state_transition_vectors/src/exit.rs @@ -333,17 +333,17 @@ mod custom_tests { fn assert_exited(state: &BeaconState, validator_index: usize) { let spec = E::default_spec(); - let validator = &state.validators()[validator_index]; + let validator = &state.validators().get(validator_index).unwrap(); assert_eq!( - validator.exit_epoch, + validator.exit_epoch(), // This is correct until we exceed the churn limit. If that happens, we // need to introduce more complex logic. state.current_epoch() + 1 + spec.max_seed_lookahead, "exit epoch" ); assert_eq!( - validator.withdrawable_epoch, - validator.exit_epoch + E::default_spec().min_validator_withdrawability_delay, + validator.withdrawable_epoch(), + validator.exit_epoch() + E::default_spec().min_validator_withdrawability_delay, "withdrawable epoch" ); }