From 8cb05de03ab82c42ac886f7b0f5dd0edfc20ed5f Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 7 Feb 2024 08:34:28 +0800 Subject: [PATCH 01/21] Single-pass epoch processing (#4483, #4573) Co-authored-by: Michael Sproul --- .../beacon_chain/src/attestation_rewards.rs | 55 +- .../beacon_chain/src/beacon_block_reward.rs | 21 +- beacon_node/beacon_chain/src/beacon_chain.rs | 1 + beacon_node/beacon_chain/src/errors.rs | 2 + beacon_node/beacon_chain/tests/tests.rs | 8 +- beacon_node/genesis/src/interop.rs | 10 +- .../http_api/src/validator_inclusion.rs | 9 +- beacon_node/operation_pool/src/attestation.rs | 20 +- beacon_node/store/src/partial_beacon_state.rs | 2 + consensus/fork_choice/src/fork_choice.rs | 210 +----- consensus/state_processing/Cargo.toml | 2 +- consensus/state_processing/src/all_caches.rs | 54 ++ .../state_processing/src/common/altair.rs | 8 +- consensus/state_processing/src/common/base.rs | 33 +- .../src/common/initiate_validator_exit.rs | 22 +- consensus/state_processing/src/common/mod.rs | 15 +- .../src/common/slash_validator.rs | 6 +- .../update_progressive_balances_cache.rs | 88 ++- .../state_processing/src/consensus_context.rs | 16 + consensus/state_processing/src/epoch_cache.rs | 121 ++++ consensus/state_processing/src/lib.rs | 4 + consensus/state_processing/src/metrics.rs | 7 + .../src/per_block_processing.rs | 7 + .../altair/sync_committee.rs | 5 +- .../src/per_block_processing/errors.rs | 15 +- .../process_operations.rs | 49 +- .../src/per_block_processing/tests.rs | 4 +- .../verify_attestation.rs | 8 +- .../src/per_epoch_processing.rs | 16 +- .../src/per_epoch_processing/altair.rs | 66 +- .../altair/inactivity_updates.rs | 47 +- .../altair/justification_and_finalization.rs | 31 +- .../altair/participation_cache.rs | 401 ++++++----- .../altair/rewards_and_penalties.rs | 74 +-- .../src/per_epoch_processing/base.rs | 5 +- .../base/rewards_and_penalties.rs | 15 +- .../base/validator_statuses.rs | 22 +- .../src/per_epoch_processing/capella.rs | 81 --- .../effective_balance_updates.rs | 66 +- .../epoch_processing_summary.rs | 196 +++--- .../src/per_epoch_processing/errors.rs | 9 +- .../per_epoch_processing/registry_updates.rs | 36 +- .../src/per_epoch_processing/single_pass.rs | 628 ++++++++++++++++++ .../src/per_epoch_processing/slashings.rs | 64 +- .../src/per_slot_processing.rs | 5 + .../state_processing/src/upgrade/altair.rs | 6 +- .../state_processing/src/upgrade/capella.rs | 8 +- .../state_processing/src/upgrade/deneb.rs | 6 +- .../state_processing/src/upgrade/merge.rs | 4 +- consensus/types/src/activation_queue.rs | 38 ++ consensus/types/src/beacon_state.rs | 269 ++++++-- .../types/src/beacon_state/clone_config.rs | 4 + .../types/src/beacon_state/committee_cache.rs | 45 +- .../progressive_balances_cache.rs | 239 +++++-- .../types/src/beacon_state/pubkey_cache.rs | 1 + .../types/src/beacon_state/slashings_cache.rs | 63 ++ consensus/types/src/beacon_state/tests.rs | 8 +- consensus/types/src/chain_spec.rs | 31 +- consensus/types/src/epoch_cache.rs | 142 ++++ consensus/types/src/lib.rs | 4 + consensus/types/src/validator.rs | 16 + lcli/src/skip_slots.rs | 3 +- lcli/src/transition_blocks.rs | 30 +- .../ef_tests/src/cases/epoch_processing.rs | 68 +- testing/ef_tests/src/cases/operations.rs | 16 +- 65 files changed, 2499 insertions(+), 1066 deletions(-) create mode 100644 consensus/state_processing/src/all_caches.rs create mode 100644 consensus/state_processing/src/epoch_cache.rs create mode 100644 consensus/state_processing/src/per_epoch_processing/single_pass.rs create mode 100644 consensus/types/src/activation_queue.rs create mode 100644 consensus/types/src/beacon_state/slashings_cache.rs create mode 100644 consensus/types/src/epoch_cache.rs 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/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/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index 7279fd28fa2..be5367eb08f 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -40,4 +40,4 @@ arbitrary-fuzz = [ "ssz_types/arbitrary", "tree_hash/arbitrary", ] -portable = ["bls/supranational-portable"] \ No newline at end of file +portable = ["bls/supranational-portable"] 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/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..d83b3434cde 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, @@ -127,6 +126,8 @@ pub mod altair_deneb { state.build_committee_cache(RelativeEpoch::Current, spec)?; let proposer_index = ctxt.get_proposer_index(state, spec)?; + let previous_epoch = ctxt.previous_epoch; + let current_epoch = ctxt.current_epoch; let attesting_indices = &verify_attestation_for_block_inclusion( state, @@ -145,32 +146,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 { 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, + previous_epoch, + 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 +203,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 +238,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..920199908d5 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,68 @@ 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(); + let (validators, balances, _) = state.validators_and_balances_and_progressive_balances_mut(); for (index, validator) in validators.iter_mut().enumerate() { 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..fd0825f2e5a --- /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/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..67ba34d0e29 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), } @@ -353,6 +377,19 @@ where #[tree_hash(skip_hashing)] #[test_random(default)] #[derivative(Clone(clone_with = "clone_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)] + #[derivative(Clone(clone_with = "clone_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 +456,8 @@ impl BeaconState { ], pubkey_cache: PubkeyCache::default(), exit_cache: ExitCache::default(), + slashings_cache: SlashingsCache::default(), + epoch_cache: EpochCache::default(), tree_hash_cache: <_>::default(), }) } @@ -510,10 +549,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 +919,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 +1241,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 +1275,67 @@ 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, + )), + } + } + /// Generate a seed for the given `epoch`. pub fn get_seed( &self, @@ -1321,7 +1425,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 +1495,28 @@ impl BeaconState { )) } + pub fn compute_total_active_balance_slow( + &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 +1524,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_slow(epoch, spec)?; + *self.total_active_balance_mut() = Some((epoch, total_active_balance)); Ok(()) } @@ -1437,8 +1577,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 +1588,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 +1619,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 +1640,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(); @@ -1507,6 +1664,8 @@ impl BeaconState { 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 +1694,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 +1730,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, @@ -1776,6 +1910,12 @@ impl BeaconState { if config.exit_cache { *res.exit_cache_mut() = self.exit_cache().clone(); } + if config.slashings_cache { + *res.slashings_cache_mut() = self.slashings_cache().clone(); + } + if config.epoch_cache { + *res.epoch_cache_mut() = self.epoch_cache().clone(); + } if config.tree_hash_cache { *res.tree_hash_cache_mut() = self.tree_hash_cache().clone(); } @@ -1794,9 +1934,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 +1975,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/clone_config.rs b/consensus/types/src/beacon_state/clone_config.rs index c6e7f47421f..a43070a194b 100644 --- a/consensus/types/src/beacon_state/clone_config.rs +++ b/consensus/types/src/beacon_state/clone_config.rs @@ -4,6 +4,8 @@ pub struct CloneConfig { pub committee_caches: bool, pub pubkey_cache: bool, pub exit_cache: bool, + pub slashings_cache: bool, + pub epoch_cache: bool, pub tree_hash_cache: bool, pub progressive_balances_cache: bool, } @@ -14,6 +16,8 @@ impl CloneConfig { committee_caches: true, pubkey_cache: true, exit_cache: true, + slashings_cache: true, + epoch_cache: true, tree_hash_cache: true, progressive_balances_cache: true, } 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/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/pubkey_cache.rs b/consensus/types/src/beacon_state/pubkey_cache.rs index c56c9077e1a..0b61ea3c5f8 100644 --- a/consensus/types/src/beacon_state/pubkey_cache.rs +++ b/consensus/types/src/beacon_state/pubkey_cache.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; type ValidatorIndex = usize; +#[allow(clippy::len_without_is_empty)] #[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)] pub struct PubkeyCache { /// Maintain the number of keys added to the map. It is not sufficient to just use the HashMap 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/beacon_state/tests.rs b/consensus/types/src/beacon_state/tests.rs index 6cd9c1dbf88..b2baafb3a4b 100644 --- a/consensus/types/src/beacon_state/tests.rs +++ b/consensus/types/src/beacon_state/tests.rs @@ -224,13 +224,15 @@ async fn clone_config() { .update_tree_hash_cache() .expect("should update tree hash cache"); - let num_caches = 5; + let num_caches = 7; let all_configs = (0..2u8.pow(num_caches)).map(|i| CloneConfig { committee_caches: (i & 1) != 0, pubkey_cache: ((i >> 1) & 1) != 0, exit_cache: ((i >> 2) & 1) != 0, - tree_hash_cache: ((i >> 3) & 1) != 0, - progressive_balances_cache: ((i >> 4) & 1) != 0, + slashings_cache: ((i >> 3) & 1) != 0, + epoch_cache: ((i >> 4) & 1) != 0, + tree_hash_cache: ((i >> 5) & 1) != 0, + progressive_balances_cache: ((i >> 6) & 1) != 0, }); for config in all_configs { diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index a182c0f98d6..44846402ba3 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; @@ -320,13 +320,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 +501,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/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/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 From 94bb079e947f6dd19f81eb8138f03a7e15161604 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 8 Feb 2024 09:20:28 +1100 Subject: [PATCH 02/21] Delete unused epoch processing code (#5170) * Delete unused epoch processing code * Compare total deltas * Remove unnecessary apply_pending * cargo fmt * Remove newline --- .../altair/rewards_and_penalties.rs | 81 +--------- testing/ef_tests/src/cases/rewards.rs | 149 +++++++++--------- 2 files changed, 79 insertions(+), 151 deletions(-) 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 9510f2eaec4..6b36346847b 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,13 +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, - WEIGHT_DENOMINATOR, + Error, }; +use types::consts::altair::PARTICIPATION_FLAG_WEIGHTS; use types::{BeaconState, ChainSpec, EthSpec}; /// Apply attester and proposer rewards. @@ -28,51 +23,6 @@ pub fn process_rewards_and_penalties_slow( Ok(()) } -/// Return the deltas for a given flag index by scanning through the participation flags. -/// -/// Spec v1.1.0 -pub fn get_flag_index_deltas( - deltas: &mut [Delta], - state: &BeaconState, - flag_index: usize, - total_active_balance: u64, - participation_cache: &ParticipationCache, - spec: &ChainSpec, -) -> Result<(), Error> { - let weight = get_flag_weight(flag_index)?; - 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 previous_epoch = state.previous_epoch(); - - for &index in participation_cache.eligible_validator_indices() { - let validator = participation_cache.get_validator(index)?; - let base_reward = validator.base_reward; - - let mut delta = Delta::default(); - - 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)? - .safe_mul(unslashed_participating_increments)?; - delta.reward( - reward_numerator.safe_div(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)?)?; - } - deltas - .get_mut(index) - .ok_or(Error::DeltaOutOfBounds(index))? - .combine(delta)?; - } - Ok(()) -} - /// Get the weight for a `flag_index` from the constant list of all weights. pub fn get_flag_weight(flag_index: usize) -> Result { PARTICIPATION_FLAG_WEIGHTS @@ -80,30 +30,3 @@ pub fn get_flag_weight(flag_index: usize) -> Result { .copied() .ok_or(Error::InvalidFlagIndex(flag_index)) } - -pub fn get_inactivity_penalty_deltas( - deltas: &mut [Delta], - state: &BeaconState, - participation_cache: &ParticipationCache, - spec: &ChainSpec, -) -> Result<(), Error> { - for &index in participation_cache.eligible_validator_indices() { - let validator = participation_cache.get_validator(index)?; - let mut delta = Delta::default(); - - 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 - .inactivity_score_bias - .safe_mul(spec.inactivity_penalty_quotient_for_state(state))?; - delta.penalize(penalty_numerator.safe_div(penalty_denominator)?)?; - } - deltas - .get_mut(index) - .ok_or(Error::DeltaOutOfBounds(index))? - .combine(delta)?; - } - Ok(()) -} diff --git a/testing/ef_tests/src/cases/rewards.rs b/testing/ef_tests/src/cases/rewards.rs index bb41f6fe12f..1a8d5b0f539 100644 --- a/testing/ef_tests/src/cases/rewards.rs +++ b/testing/ef_tests/src/cases/rewards.rs @@ -7,17 +7,14 @@ use ssz::four_byte_option_impl; use ssz_derive::{Decode, Encode}; use state_processing::{ per_epoch_processing::{ - altair::{self, rewards_and_penalties::get_flag_index_deltas, ParticipationCache}, + altair, base::{self, rewards_and_penalties::AttestationDelta, ValidatorStatuses}, Delta, }, EpochProcessingError, }; use std::path::{Path, PathBuf}; -use types::{ - consts::altair::{TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX}, - BeaconState, EthSpec, ForkName, -}; +use types::{BeaconState, EthSpec, ForkName}; #[derive(Debug, Clone, PartialEq, Decode, Encode, CompareFields)] pub struct Deltas { @@ -41,6 +38,11 @@ pub struct AllDeltas { inactivity_penalty_deltas: Deltas, } +#[derive(Debug, Clone, PartialEq, CompareFields)] +pub struct TotalDeltas { + deltas: Vec, +} + #[derive(Debug, Clone, Default, Deserialize)] pub struct Metadata { pub description: Option, @@ -110,11 +112,19 @@ impl Case for RewardsTest { let mut state = self.pre.clone(); let spec = &testing_spec::(fork_name); - let deltas: Result = (|| { - // Processing requires the committee caches. - state.build_all_committee_caches(spec)?; + // Single-pass epoch processing doesn't compute rewards in the genesis epoch because that's + // what the spec for `process_rewards_and_penalties` says to do. We skip these tests for now. + // + // See: https://github.com/ethereum/consensus-specs/issues/3593 + if fork_name != ForkName::Base && state.current_epoch() == 0 { + return Err(Error::SkippedKnownFailure); + } + + if let BeaconState::Base(_) = state { + let deltas: Result = (|| { + // Processing requires the committee caches. + state.build_all_committee_caches(spec)?; - if let BeaconState::Base(_) = state { let mut validator_statuses = ValidatorStatuses::new(&state, spec)?; validator_statuses.process_attestations(&state)?; @@ -125,39 +135,19 @@ impl Case for RewardsTest { )?; Ok(convert_all_base_deltas(&deltas)) - } else { - let total_active_balance = state.get_total_active_balance()?; + })(); + compare_result_detailed(&deltas, &Some(self.deltas.clone()))?; + } else { + let deltas: Result = (|| { + // Processing requires the committee caches. + state.build_all_committee_caches(spec)?; + compute_altair_deltas(&mut state, spec) + })(); - let source_deltas = compute_altair_flag_deltas( - &state, - TIMELY_SOURCE_FLAG_INDEX, - total_active_balance, - spec, - )?; - let target_deltas = compute_altair_flag_deltas( - &state, - TIMELY_TARGET_FLAG_INDEX, - total_active_balance, - spec, - )?; - let head_deltas = compute_altair_flag_deltas( - &state, - TIMELY_HEAD_FLAG_INDEX, - total_active_balance, - spec, - )?; - let inactivity_penalty_deltas = compute_altair_inactivity_deltas(&state, spec)?; - Ok(AllDeltas { - source_deltas, - target_deltas, - head_deltas, - inclusion_delay_deltas: None, - inactivity_penalty_deltas, - }) - } - })(); - - compare_result_detailed(&deltas, &Some(self.deltas.clone()))?; + let expected = all_deltas_to_total_deltas(&self.deltas); + + compare_result_detailed(&deltas, &Some(expected))?; + }; Ok(()) } @@ -182,39 +172,54 @@ fn convert_base_deltas(attestation_deltas: &[AttestationDelta], accessor: Access Deltas { rewards, penalties } } -fn compute_altair_flag_deltas( - state: &BeaconState, - flag_index: usize, - total_active_balance: u64, - spec: &ChainSpec, -) -> Result { - let mut deltas = vec![Delta::default(); state.validators().len()]; - get_flag_index_deltas( - &mut deltas, - state, - flag_index, - total_active_balance, - &ParticipationCache::new(state, spec).unwrap(), - spec, - )?; - Ok(convert_altair_deltas(deltas)) +fn deltas_to_total_deltas(d: &Deltas) -> impl Iterator + '_ { + d.rewards + .iter() + .zip(&d.penalties) + .map(|(&reward, &penalty)| reward as i64 - penalty as i64) } -fn compute_altair_inactivity_deltas( - state: &BeaconState, - spec: &ChainSpec, -) -> Result { - let mut deltas = vec![Delta::default(); state.validators().len()]; - altair::rewards_and_penalties::get_inactivity_penalty_deltas( - &mut deltas, - state, - &ParticipationCache::new(state, spec).unwrap(), - spec, - )?; - Ok(convert_altair_deltas(deltas)) +fn optional_deltas_to_total_deltas(d: &Option, len: usize) -> TotalDeltas { + let deltas = if let Some(d) = d { + deltas_to_total_deltas(d).collect() + } else { + vec![0i64; len] + }; + TotalDeltas { deltas } } -fn convert_altair_deltas(deltas: Vec) -> Deltas { - let (rewards, penalties) = deltas.into_iter().map(|d| (d.rewards, d.penalties)).unzip(); - Deltas { rewards, penalties } +fn all_deltas_to_total_deltas(d: &AllDeltas) -> TotalDeltas { + let len = d.source_deltas.rewards.len(); + let deltas = deltas_to_total_deltas(&d.source_deltas) + .zip(deltas_to_total_deltas(&d.target_deltas)) + .zip(deltas_to_total_deltas(&d.head_deltas)) + .zip(optional_deltas_to_total_deltas(&d.inclusion_delay_deltas, len).deltas) + .zip(deltas_to_total_deltas(&d.inactivity_penalty_deltas)) + .map( + |((((source, target), head), inclusion_delay), inactivity_penalty)| { + source + target + head + inclusion_delay + inactivity_penalty + }, + ) + .collect::>(); + TotalDeltas { deltas } +} + +fn compute_altair_deltas( + state: &mut BeaconState, + spec: &ChainSpec, +) -> Result { + // Initialise deltas to pre-state balances. + let mut deltas = state + .balances() + .iter() + .map(|x| *x as i64) + .collect::>(); + altair::process_rewards_and_penalties_slow(state, spec)?; + + for (delta, new_balance) in deltas.iter_mut().zip(state.balances()) { + let old_balance = *delta; + *delta = *new_balance as i64 - old_balance; + } + + Ok(TotalDeltas { deltas }) } From 82d733bd7ad46fe83735d3ac64b6f5cd1bb1e3d3 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 14 Feb 2024 11:55:11 +1100 Subject: [PATCH 03/21] Use epoch cache in block packing (#5223) --- beacon_node/beacon_chain/src/beacon_chain.rs | 4 ++++ beacon_node/beacon_chain/src/errors.rs | 2 ++ beacon_node/operation_pool/src/attestation.rs | 14 +++----------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index d2b49c86f14..34e5b546052 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -95,6 +95,7 @@ use slot_clock::SlotClock; use ssz::Encode; use state_processing::{ common::get_attesting_indices_from_state, + epoch_cache::initialize_epoch_cache, per_block_processing, per_block_processing::{ errors::AttestationValidationError, get_expected_withdrawals, @@ -4967,7 +4968,10 @@ impl BeaconChain { let attestation_packing_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_ATTESTATION_TIMES); + // Epoch cache and total balance cache are required for op pool packing. state.build_total_active_balance_cache_at(state.current_epoch(), &self.spec)?; + initialize_epoch_cache(&mut state, &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 2af9c851066..96c57560628 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -260,6 +260,7 @@ pub enum BlockProductionError { UnableToProduceAtSlot(Slot), SlotProcessingError(SlotProcessingError), BlockProcessingError(BlockProcessingError), + EpochCacheError(EpochCacheError), ForkChoiceError(ForkChoiceError), Eth1ChainError(Eth1ChainError), BeaconStateError(BeaconStateError), @@ -299,3 +300,4 @@ easy_from_to!(SlotProcessingError, BlockProductionError); easy_from_to!(Eth1ChainError, BlockProductionError); easy_from_to!(StateAdvanceError, BlockProductionError); easy_from_to!(ForkChoiceError, BlockProductionError); +easy_from_to!(EpochCacheError, BlockProductionError); diff --git a/beacon_node/operation_pool/src/attestation.rs b/beacon_node/operation_pool/src/attestation.rs index 9133157dbe8..8d8e3796745 100644 --- a/beacon_node/operation_pool/src/attestation.rs +++ b/beacon_node/operation_pool/src/attestation.rs @@ -2,7 +2,7 @@ use crate::attestation_storage::AttestationRef; use crate::max_cover::MaxCover; use crate::reward_cache::RewardCache; use state_processing::common::{ - altair, base, get_attestation_participation_flag_indices, get_attesting_indices, + base, get_attestation_participation_flag_indices, get_attesting_indices, }; use std::collections::HashMap; use types::{ @@ -30,7 +30,7 @@ impl<'a, T: EthSpec> AttMaxCover<'a, T> { if let BeaconState::Base(ref base_state) = state { Self::new_for_base(att, state, base_state, total_active_balance, spec) } else { - Self::new_for_altair_deneb(att, state, reward_cache, total_active_balance, spec) + Self::new_for_altair_deneb(att, state, reward_cache, spec) } } @@ -72,7 +72,6 @@ impl<'a, T: EthSpec> AttMaxCover<'a, T> { att: AttestationRef<'a, T>, state: &BeaconState, reward_cache: &'a RewardCache, - total_active_balance: u64, spec: &ChainSpec, ) -> Option { let att_data = att.attestation_data(); @@ -81,8 +80,6 @@ impl<'a, T: EthSpec> AttMaxCover<'a, T> { let att_participation_flags = get_attestation_participation_flag_indices(state, &att_data, inclusion_delay, spec) .ok()?; - let base_reward_per_increment = - altair::BaseRewardPerIncrement::new(total_active_balance, spec).ok()?; let fresh_validators_rewards = att .indexed @@ -98,12 +95,7 @@ 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(effective_balance, base_reward_per_increment, spec) - .ok()?; + let base_reward = state.get_base_reward(index as usize).ok()?; for (flag_index, weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { if att_participation_flags.contains(&flag_index) { From 10e7fe4cae986a5f5ab3482dc34778931e44ee00 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 14 Feb 2024 11:55:27 +1100 Subject: [PATCH 04/21] Remove progressive balances mode (#5224) --- beacon_node/beacon_chain/src/beacon_chain.rs | 2 -- beacon_node/beacon_chain/src/builder.rs | 2 -- beacon_node/beacon_chain/src/chain_config.rs | 5 +-- beacon_node/beacon_chain/src/fork_revert.rs | 9 +---- .../tests/payload_invalidation.rs | 2 -- beacon_node/src/cli.rs | 10 ++---- beacon_node/src/config.rs | 10 +++--- consensus/fork_choice/src/fork_choice.rs | 6 ---- consensus/fork_choice/tests/tests.rs | 36 +++++-------------- .../progressive_balances_cache.rs | 29 --------------- lighthouse/tests/beacon_node.rs | 26 ++------------ testing/ef_tests/src/cases/fork_choice.rs | 4 +-- 12 files changed, 23 insertions(+), 118 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 34e5b546052..a7e3813b142 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3369,9 +3369,7 @@ impl BeaconChain { block_delay, &state, payload_verification_status, - self.config.progressive_balances_mode, &self.spec, - &self.log, ) .map_err(|e| BlockError::BeaconChainError(e.into()))?; } diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index abd0e6b6a82..368a0105dd8 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -751,8 +751,6 @@ where store.clone(), Some(current_slot), &self.spec, - self.chain_config.progressive_balances_mode, - &log, )?; } diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index 23e17a6efad..eb73f68bce0 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -1,7 +1,7 @@ pub use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold}; use serde::{Deserialize, Serialize}; use std::time::Duration; -use types::{Checkpoint, Epoch, ProgressiveBalancesMode}; +use types::{Checkpoint, Epoch}; pub const DEFAULT_RE_ORG_THRESHOLD: ReOrgThreshold = ReOrgThreshold(20); pub const DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION: Epoch = Epoch::new(2); @@ -79,8 +79,6 @@ pub struct ChainConfig { /// /// This is useful for block builders and testing. pub always_prepare_payload: bool, - /// Whether to use `ProgressiveBalancesCache` in unrealized FFG progression calculation. - pub progressive_balances_mode: ProgressiveBalancesMode, /// Number of epochs between each migration of data from the hot database to the freezer. pub epochs_per_migration: u64, /// When set to true Light client server computes and caches state proofs for serving updates @@ -114,7 +112,6 @@ impl Default for ChainConfig { shuffling_cache_size: crate::shuffling_cache::DEFAULT_CACHE_SIZE, genesis_backfill: false, always_prepare_payload: false, - progressive_balances_mode: ProgressiveBalancesMode::Fast, epochs_per_migration: crate::migrate::DEFAULT_EPOCHS_PER_MIGRATION, enable_light_client_server: false, } diff --git a/beacon_node/beacon_chain/src/fork_revert.rs b/beacon_node/beacon_chain/src/fork_revert.rs index dc0e34277c9..084ae95e096 100644 --- a/beacon_node/beacon_chain/src/fork_revert.rs +++ b/beacon_node/beacon_chain/src/fork_revert.rs @@ -10,10 +10,7 @@ use state_processing::{ use std::sync::Arc; use std::time::Duration; use store::{iter::ParentRootBlockIterator, HotColdDB, ItemStore}; -use types::{ - BeaconState, ChainSpec, EthSpec, ForkName, Hash256, ProgressiveBalancesMode, SignedBeaconBlock, - Slot, -}; +use types::{BeaconState, ChainSpec, EthSpec, ForkName, Hash256, SignedBeaconBlock, Slot}; const CORRUPT_DB_MESSAGE: &str = "The database could be corrupt. Check its file permissions or \ consider deleting it by running with the --purge-db flag."; @@ -103,8 +100,6 @@ pub fn reset_fork_choice_to_finalization, Cold: It store: Arc>, current_slot: Option, spec: &ChainSpec, - progressive_balances_mode: ProgressiveBalancesMode, - log: &Logger, ) -> Result, E>, String> { // Fetch finalized block. let finalized_checkpoint = head_state.finalized_checkpoint(); @@ -202,9 +197,7 @@ pub fn reset_fork_choice_to_finalization, Cold: It Duration::from_secs(0), &state, payload_verification_status, - progressive_balances_mode, spec, - log, ) .map_err(|e| format!("Error applying replayed block to fork choice: {:?}", e))?; } diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index a0b7fbd365a..22d3f35ef10 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -1077,9 +1077,7 @@ async fn invalid_parent() { Duration::from_secs(0), &state, PayloadVerificationStatus::Optimistic, - rig.harness.chain.config.progressive_balances_mode, &rig.harness.chain.spec, - rig.harness.logger() ), Err(ForkChoiceError::ProtoArrayStringError(message)) if message.contains(&format!( diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 4ed60d27b0f..fd8054e0ff7 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1,6 +1,5 @@ use clap::{App, Arg, ArgGroup}; use strum::VariantNames; -use types::ProgressiveBalancesMode; pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new("beacon_node") @@ -1208,14 +1207,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("progressive-balances") .long("progressive-balances") .value_name("MODE") - .help("Control the progressive balances cache mode. The default `fast` mode uses \ - the cache to speed up fork choice. A more conservative `checked` mode \ - compares the cache's results against results without the cache. If \ - there is a mismatch, it falls back to the cache-free result. Using the \ - default `fast` mode is recommended unless advised otherwise by the \ - Lighthouse team.") + .help("Deprecated. This optimisation is now the default and cannot be disabled.") .takes_value(true) - .possible_values(ProgressiveBalancesMode::VARIANTS) + .possible_values(&["fast", "disabled", "checked", "strict"]) ) .arg( Arg::with_name("beacon-processor-max-workers") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index b485775dbfe..da293fcbf37 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -833,10 +833,12 @@ pub fn get_config( client_config.network.invalid_block_storage = Some(path); } - if let Some(progressive_balances_mode) = - clap_utils::parse_optional(cli_args, "progressive-balances")? - { - client_config.chain.progressive_balances_mode = progressive_balances_mode; + if cli_args.is_present("progressive-balances") { + warn!( + log, + "Progressive balances mode is deprecated"; + "info" => "please remove --progressive-balances" + ); } if let Some(max_workers) = clap_utils::parse_optional(cli_args, "beacon-processor-max-workers")? diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 93dc4e3c4f8..f2684a4d9ff 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -12,7 +12,6 @@ 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, @@ -73,7 +72,6 @@ pub enum Error { }, UnrealizedVoteProcessing(state_processing::EpochProcessingError), ValidatorStatuses(BeaconStateError), - ProgressiveBalancesCacheCheckFailed(String), } impl From for Error { @@ -636,8 +634,6 @@ 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, @@ -646,9 +642,7 @@ where block_delay: Duration, state: &BeaconState, payload_verification_status: PayloadVerificationStatus, - _progressive_balances_mode: ProgressiveBalancesMode, spec: &ChainSpec, - _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 diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 649fbcc5559..29f296b1b41 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -16,8 +16,8 @@ use std::time::Duration; use store::MemoryStore; use types::{ test_utils::generate_deterministic_keypair, BeaconBlockRef, BeaconState, ChainSpec, Checkpoint, - Epoch, EthSpec, ForkName, Hash256, IndexedAttestation, MainnetEthSpec, ProgressiveBalancesMode, - RelativeEpoch, SignedBeaconBlock, Slot, SubnetId, + Epoch, EthSpec, ForkName, Hash256, IndexedAttestation, MainnetEthSpec, RelativeEpoch, + SignedBeaconBlock, Slot, SubnetId, }; pub type E = MainnetEthSpec; @@ -47,8 +47,10 @@ impl fmt::Debug for ForkChoiceTest { impl ForkChoiceTest { /// Creates a new tester. pub fn new() -> Self { + // Run fork choice tests against the latest fork. + let spec = ForkName::latest().make_genesis_spec(ChainSpec::default()); let harness = BeaconChainHarness::builder(MainnetEthSpec) - .default_spec() + .spec(spec) .deterministic_keypairs(VALIDATOR_COUNT) .fresh_ephemeral_store() .build(); @@ -58,29 +60,13 @@ impl ForkChoiceTest { /// Creates a new tester with a custom chain config. pub fn new_with_chain_config(chain_config: ChainConfig) -> Self { - let harness = BeaconChainHarness::builder(MainnetEthSpec) - .default_spec() - .chain_config(chain_config) - .deterministic_keypairs(VALIDATOR_COUNT) - .fresh_ephemeral_store() - .build(); - - Self { harness } - } - - /// Creates a new tester with the specified `ProgressiveBalancesMode` and genesis from latest fork. - fn new_with_progressive_balances_mode(mode: ProgressiveBalancesMode) -> ForkChoiceTest { - // genesis with latest fork (at least altair required to test the cache) + // Run fork choice tests against the latest fork. let spec = ForkName::latest().make_genesis_spec(ChainSpec::default()); let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec) - .chain_config(ChainConfig { - progressive_balances_mode: mode, - ..ChainConfig::default() - }) + .chain_config(chain_config) .deterministic_keypairs(VALIDATOR_COUNT) .fresh_ephemeral_store() - .mock_execution_layer() .build(); Self { harness } @@ -338,9 +324,7 @@ impl ForkChoiceTest { Duration::from_secs(0), &state, PayloadVerificationStatus::Verified, - self.harness.chain.config.progressive_balances_mode, &self.harness.chain.spec, - self.harness.logger(), ) .unwrap(); self @@ -383,9 +367,7 @@ impl ForkChoiceTest { Duration::from_secs(0), &state, PayloadVerificationStatus::Verified, - self.harness.chain.config.progressive_balances_mode, &self.harness.chain.spec, - self.harness.logger(), ) .expect_err("on_block did not return an error"); comparison_func(err); @@ -1348,7 +1330,7 @@ async fn weak_subjectivity_check_epoch_boundary_is_skip_slot_failure() { /// where the slashed validator is a target attester in previous / current epoch. #[tokio::test] async fn progressive_balances_cache_attester_slashing() { - ForkChoiceTest::new_with_progressive_balances_mode(ProgressiveBalancesMode::Strict) + ForkChoiceTest::new() // first two epochs .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0) .await @@ -1379,7 +1361,7 @@ async fn progressive_balances_cache_attester_slashing() { /// where the slashed validator is a target attester in previous / current epoch. #[tokio::test] async fn progressive_balances_cache_proposer_slashing() { - ForkChoiceTest::new_with_progressive_balances_mode(ProgressiveBalancesMode::Strict) + ForkChoiceTest::new() // first two epochs .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0) .await diff --git a/consensus/types/src/beacon_state/progressive_balances_cache.rs b/consensus/types/src/beacon_state/progressive_balances_cache.rs index d0e0010b93b..69c76447b46 100644 --- a/consensus/types/src/beacon_state/progressive_balances_cache.rs +++ b/consensus/types/src/beacon_state/progressive_balances_cache.rs @@ -8,8 +8,6 @@ use crate::{ }; use arbitrary::Arbitrary; use safe_arith::SafeArith; -use serde::{Deserialize, Serialize}; -use strum::{Display, EnumString, EnumVariantNames}; /// This cache keeps track of the accumulated target attestation balance for the current & previous /// epochs. The cached values can be utilised by fork choice to calculate unrealized justification @@ -285,33 +283,6 @@ impl ProgressiveBalancesCache { } } -#[derive( - Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize, Display, EnumString, EnumVariantNames, -)] -#[strum(serialize_all = "lowercase")] -pub enum ProgressiveBalancesMode { - /// Disable the usage of progressive cache, and use the existing `ParticipationCache` calculation. - Disabled, - /// 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`. BeaconStateErrors - /// if there is a balance mismatch. Used in testing only. - Strict, - /// Enable the usage of progressive cache, with no comparative checks against the - /// `ParticipationCache`. This is fast but an experimental mode, use with caution. - Fast, -} - -impl ProgressiveBalancesMode { - pub fn perform_comparative_checks(&self) -> bool { - match self { - Self::Disabled | Self::Fast => false, - Self::Checked | Self::Strict => true, - } - } -} - /// `ProgressiveBalancesCache` is only enabled from `Altair` as it requires `ParticipationCache`. pub fn is_progressive_balances_enabled(state: &BeaconState) -> bool { match state { diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index f97f17a6677..90533f5ba43 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -19,10 +19,7 @@ use std::string::ToString; use std::time::Duration; use tempfile::TempDir; use types::non_zero_usize::new_non_zero_usize; -use types::{ - Address, Checkpoint, Epoch, ExecutionBlockHash, ForkName, Hash256, MainnetEthSpec, - ProgressiveBalancesMode, -}; +use types::{Address, Checkpoint, Epoch, ExecutionBlockHash, ForkName, Hash256, MainnetEthSpec}; const DEFAULT_ETH1_ENDPOINT: &str = "http://localhost:8545/"; const DUMMY_ENR_TCP_PORT: u16 = 7777; @@ -2462,29 +2459,12 @@ fn invalid_gossip_verified_blocks_path() { }); } -#[test] -fn progressive_balances_default() { - CommandLineTest::new() - .run_with_zero_port() - .with_config(|config| { - assert_eq!( - config.chain.progressive_balances_mode, - ProgressiveBalancesMode::Fast - ) - }); -} - #[test] fn progressive_balances_checked() { + // Flag is deprecated but supplying it should not crash until we remove it completely. CommandLineTest::new() .flag("progressive-balances", Some("checked")) - .run_with_zero_port() - .with_config(|config| { - assert_eq!( - config.chain.progressive_balances_mode, - ProgressiveBalancesMode::Checked - ) - }); + .run_with_zero_port(); } #[test] diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 9884a709eb9..467d71b8591 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -25,7 +25,7 @@ use std::time::Duration; use types::{ Attestation, AttesterSlashing, BeaconBlock, BeaconState, BlobSidecar, BlobsList, Checkpoint, EthSpec, ExecutionBlockHash, ForkName, Hash256, IndexedAttestation, KzgProof, - ProgressiveBalancesMode, ProposerPreparationData, SignedBeaconBlock, Slot, Uint256, + ProposerPreparationData, SignedBeaconBlock, Slot, Uint256, }; #[derive(Default, Debug, PartialEq, Clone, Deserialize, Decode)] @@ -557,9 +557,7 @@ impl Tester { block_delay, &state, PayloadVerificationStatus::Irrelevant, - ProgressiveBalancesMode::Strict, &self.harness.chain.spec, - self.harness.logger(), ); if result.is_ok() { From 90ee50fbf37b1fcf543d3528786dfdb77e4808fe Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 14 Feb 2024 13:02:51 +1100 Subject: [PATCH 05/21] inline inactivity_penalty_quotient_for_state --- beacon_node/beacon_chain/src/attestation_rewards.rs | 6 +++--- consensus/types/src/chain_spec.rs | 6 ------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/beacon_node/beacon_chain/src/attestation_rewards.rs b/beacon_node/beacon_chain/src/attestation_rewards.rs index e86c10a146b..25f05b32ff4 100644 --- a/beacon_node/beacon_chain/src/attestation_rewards.rs +++ b/beacon_node/beacon_chain/src/attestation_rewards.rs @@ -246,9 +246,9 @@ impl BeaconChain { let penalty_numerator = effective_balance .safe_mul(state.get_inactivity_score(validator_index)?)?; - let penalty_denominator = spec - .inactivity_score_bias - .safe_mul(spec.inactivity_penalty_quotient_for_state(&state))?; + let penalty_denominator = spec.inactivity_score_bias.safe_mul( + spec.inactivity_penalty_quotient_for_fork(state.fork_name_unchecked()), + )?; inactivity_penalty = -(penalty_numerator.safe_div(penalty_denominator)? as i64); } else if flag_index == TIMELY_SOURCE_FLAG_INDEX { diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 44846402ba3..df305efc688 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -319,12 +319,6 @@ 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 { - 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, From f420033e72ae316e18d79564734e97c7ce694568 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:47:06 +1100 Subject: [PATCH 06/21] drop previous_epoch_total_active_balance --- beacon_node/http_api/src/validator_inclusion.rs | 1 - common/eth2/src/lighthouse.rs | 2 -- .../per_epoch_processing/epoch_processing_summary.rs | 10 ---------- 3 files changed, 13 deletions(-) diff --git a/beacon_node/http_api/src/validator_inclusion.rs b/beacon_node/http_api/src/validator_inclusion.rs index c58e22e8cf2..cbc5470e3e2 100644 --- a/beacon_node/http_api/src/validator_inclusion.rs +++ b/beacon_node/http_api/src/validator_inclusion.rs @@ -47,7 +47,6 @@ pub fn global_validator_inclusion_data( Ok(GlobalValidatorInclusionData { current_epoch_active_gwei: summary.current_epoch_total_active_balance(), - previous_epoch_active_gwei: summary.previous_epoch_total_active_balance(), current_epoch_target_attesting_gwei: summary .current_epoch_target_attesting_balance() .map_err(convert_cache_error)?, diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs index 538f1a42d1c..dcb460b55a7 100644 --- a/common/eth2/src/lighthouse.rs +++ b/common/eth2/src/lighthouse.rs @@ -54,8 +54,6 @@ pub struct Peer { pub struct GlobalValidatorInclusionData { /// The total effective balance of all active validators during the _current_ epoch. pub current_epoch_active_gwei: u64, - /// The total effective balance of all active validators during the _previous_ epoch. - pub previous_epoch_active_gwei: u64, /// The total effective balance of all validators who attested during the _current_ epoch and /// agreed with the state about the beacon block at the first slot of the _current_ epoch. pub current_epoch_target_attesting_gwei: u64, 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 4266f462123..7a9316648dc 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 @@ -100,10 +100,6 @@ impl EpochProcessingSummary { &metrics::PARTICIPATION_PREV_EPOCH_SOURCE_ATTESTING_GWEI_TOTAL, self.previous_epoch_source_attesting_balance()? as i64, ); - metrics::set_gauge( - &metrics::PARTICIPATION_PREV_EPOCH_ACTIVE_GWEI_TOTAL, - self.previous_epoch_total_active_balance() as i64, - ); Ok(()) } @@ -141,12 +137,6 @@ impl EpochProcessingSummary { } } - /// Returns the sum of the effective balance of all validators in the previous epoch. - pub fn previous_epoch_total_active_balance(&self) -> u64 { - // 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 /// epoch *and* the validator is not slashed. /// From ff625e715b78b348702d2eabc8c9ba311e288068 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Sun, 18 Feb 2024 16:56:04 +1100 Subject: [PATCH 07/21] fc lint --- consensus/fork_choice/src/fork_choice.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index f2684a4d9ff..cbc08c79afc 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -634,6 +634,7 @@ where /// /// The supplied block **must** pass the `state_transition` function as it will not be run /// here. + #[allow(clippy::too_many_arguments)] pub fn on_block>( &mut self, system_time_current_slot: Slot, From a4427daa54ae6b24e4aaca7d905c17994038d475 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 20 Feb 2024 09:31:00 +1100 Subject: [PATCH 08/21] spec compliant process_sync_aggregate (#15) * spec compliant process_sync_aggregate * Update consensus/state_processing/src/per_block_processing/altair/sync_committee.rs Co-authored-by: Michael Sproul --------- Co-authored-by: Michael Sproul --- .../altair/sync_committee.rs | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) 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 7773bc8ac35..99653d75618 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 @@ -4,7 +4,9 @@ use crate::{signature_sets::sync_aggregate_signature_set, VerifySignatures}; use safe_arith::SafeArith; use std::borrow::Cow; use types::consts::altair::{PROPOSER_WEIGHT, SYNC_REWARD_WEIGHT, WEIGHT_DENOMINATOR}; -use types::{BeaconState, ChainSpec, EthSpec, PublicKeyBytes, SyncAggregate, Unsigned}; +use types::{ + BeaconState, BeaconStateError, ChainSpec, EthSpec, PublicKeyBytes, SyncAggregate, Unsigned, +}; pub fn process_sync_aggregate( state: &mut BeaconState, @@ -47,20 +49,35 @@ 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; + let proposer_index = proposer_index as usize; + let mut proposer_balance = *state + .balances() + .get(proposer_index) + .ok_or(BeaconStateError::BalancesOutOfBounds(proposer_index))?; + 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)?; - total_proposer_reward.safe_add_assign(proposer_reward)?; + // Accumulate proposer rewards in a temp var in case the proposer has very low balance, is + // part of the sync committee, does not participate and its penalties saturate. + if participant_index == proposer_index { + proposer_balance.safe_add_assign(participant_reward)?; + } else { + increase_balance(state, participant_index, participant_reward)?; + } + proposer_balance.safe_add_assign(proposer_reward)?; } else { - decrease_balance(state, participant_index, participant_reward)?; + if participant_index == proposer_index { + proposer_balance = proposer_balance.saturating_sub(participant_reward); + } else { + decrease_balance(state, participant_index, participant_reward)?; + } } } - increase_balance(state, proposer_index as usize, total_proposer_reward)?; + + *state.get_balance_mut(proposer_index)? = proposer_balance; Ok(()) } From 4380e0550299c78fb6a55be3f80c6b5a2cda9b3d Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 22 Feb 2024 13:22:29 +1100 Subject: [PATCH 09/21] Delete the participation cache (#16) --- .../beacon_chain/src/attestation_rewards.rs | 46 +- .../http_api/src/attestation_performance.rs | 8 - consensus/fork_choice/src/fork_choice.rs | 6 +- consensus/state_processing/src/all_caches.rs | 2 +- .../update_progressive_balances_cache.rs | 79 ++- .../src/per_block_processing.rs | 2 +- .../altair/sync_committee.rs | 8 +- .../src/per_block_processing/errors.rs | 9 - .../src/per_epoch_processing/altair.rs | 4 +- .../altair/participation_cache.rs | 479 ------------------ .../src/per_epoch_processing/errors.rs | 8 - .../src/per_epoch_processing/single_pass.rs | 2 +- .../state_processing/src/upgrade/altair.rs | 2 +- consensus/types/src/beacon_state.rs | 1 - .../progressive_balances_cache.rs | 2 +- .../ef_tests/src/cases/epoch_processing.rs | 2 +- testing/ef_tests/src/cases/operations.rs | 6 +- 17 files changed, 101 insertions(+), 565 deletions(-) delete mode 100644 consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs diff --git a/beacon_node/beacon_chain/src/attestation_rewards.rs b/beacon_node/beacon_chain/src/attestation_rewards.rs index 25f05b32ff4..5c3ddb9b3b9 100644 --- a/beacon_node/beacon_chain/src/attestation_rewards.rs +++ b/beacon_node/beacon_chain/src/attestation_rewards.rs @@ -1,7 +1,6 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2::lighthouse::attestation_rewards::{IdealAttestationRewards, TotalAttestationRewards}; use eth2::lighthouse::StandardAttestationRewards; -use participation_cache::ParticipationCache; use safe_arith::SafeArith; use serde_utils::quoted_u64::Quoted; use slog::debug; @@ -10,7 +9,7 @@ use state_processing::per_epoch_processing::altair::{ }; use state_processing::{ common::altair::BaseRewardPerIncrement, - per_epoch_processing::altair::{participation_cache, rewards_and_penalties::get_flag_weight}, + per_epoch_processing::altair::rewards_and_penalties::get_flag_weight, }; use std::collections::HashMap; use store::consts::altair::{ @@ -134,8 +133,6 @@ impl BeaconChain { let spec = &self.spec; // Calculate ideal_rewards - 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)?; @@ -147,14 +144,14 @@ impl BeaconChain { let weight = get_flag_weight(flag_index) .map_err(|_| BeaconChainError::AttestationRewardsError)?; - let unslashed_participating_balance = participation_cache - .previous_epoch_flag_attesting_balance(flag_index) - .map_err(|_| BeaconChainError::AttestationRewardsError)?; + let unslashed_participating_balance = state + .progressive_balances_cache() + .previous_epoch_flag_attesting_balance(flag_index)?; let unslashed_participating_increments = unslashed_participating_balance.safe_div(spec.effective_balance_increment)?; - let total_active_balance = participation_cache.current_epoch_total_active_balance(); + let total_active_balance = state.get_total_active_balance()?; let active_increments = total_active_balance.safe_div(spec.effective_balance_increment)?; @@ -190,7 +187,7 @@ impl BeaconChain { let mut total_rewards: Vec = Vec::new(); let validators = if validators.is_empty() { - participation_cache.eligible_validator_indices().to_vec() + Self::all_eligible_validator_indices(&state, previous_epoch)? } else { Self::validators_ids_to_indices(&mut state, validators)? }; @@ -198,7 +195,7 @@ impl BeaconChain { 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 { + let Ok(validator) = state.get_validator(validator_index) else { debug!( self.log, "No rewards for inactive/unknown validator"; @@ -215,7 +212,11 @@ impl BeaconChain { }); continue; }; - let eligible = validator.is_eligible; + let previous_epoch_participation_flags = state + .previous_epoch_participation()? + .get(validator_index) + .ok_or(BeaconChainError::AttestationRewardsError)?; + let eligible = state.is_eligible_validator(previous_epoch, validator)?; let mut head_reward = 0i64; let mut target_reward = 0i64; let mut source_reward = 0i64; @@ -228,9 +229,8 @@ impl BeaconChain { let (ideal_reward, penalty) = ideal_rewards_hashmap .get(&(flag_index, effective_balance)) .ok_or(BeaconChainError::AttestationRewardsError)?; - let voted_correctly = validator - .is_unslashed_participating_index(flag_index) - .map_err(|_| BeaconChainError::AttestationRewardsError)?; + let voted_correctly = !validator.slashed + && previous_epoch_participation_flags.has_flag(flag_index)?; if voted_correctly { if flag_index == TIMELY_HEAD_FLAG_INDEX { head_reward += *ideal_reward as i64; @@ -314,6 +314,24 @@ impl BeaconChain { Ok(max_steps) } + fn all_eligible_validator_indices( + state: &BeaconState, + previous_epoch: Epoch, + ) -> Result, BeaconChainError> { + state + .validators() + .iter() + .enumerate() + .filter_map(|(i, validator)| { + state + .is_eligible_validator(previous_epoch, validator) + .map(|eligible| eligible.then_some(i)) + .map_err(BeaconChainError::BeaconStateError) + .transpose() + }) + .collect() + } + fn validators_ids_to_indices( state: &mut BeaconState, validators: Vec, diff --git a/beacon_node/http_api/src/attestation_performance.rs b/beacon_node/http_api/src/attestation_performance.rs index 6e3ebcccec5..d4f9916814a 100644 --- a/beacon_node/http_api/src/attestation_performance.rs +++ b/beacon_node/http_api/src/attestation_performance.rs @@ -3,7 +3,6 @@ use eth2::lighthouse::{ AttestationPerformance, AttestationPerformanceQuery, AttestationPerformanceStatistics, }; use state_processing::{ - per_epoch_processing::altair::participation_cache::Error as ParticipationCacheError, per_epoch_processing::EpochProcessingSummary, BlockReplayError, BlockReplayer, }; use std::sync::Arc; @@ -18,7 +17,6 @@ const BLOCK_ROOT_CHUNK_SIZE: usize = 100; enum AttestationPerformanceError { BlockReplay(#[allow(dead_code)] BlockReplayError), BeaconState(#[allow(dead_code)] BeaconStateError), - ParticipationCache(#[allow(dead_code)] ParticipationCacheError), UnableToFindValidator(#[allow(dead_code)] usize), } @@ -34,12 +32,6 @@ impl From for AttestationPerformanceError { } } -impl From for AttestationPerformanceError { - fn from(e: ParticipationCacheError) -> Self { - Self::ParticipationCache(e) - } -} - pub fn get_attestation_performance( target: String, query: AttestationPerformanceQuery, diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index cbc08c79afc..faa34834b11 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -747,7 +747,11 @@ where | BeaconBlockRef::Capella(_) | BeaconBlockRef::Merge(_) | BeaconBlockRef::Altair(_) => { - // FIXME(sproul): initialize progressive balances + // NOTE: Processing justification & finalization requires the progressive + // balances cache, but we cannot initialize it here as we only have an + // immutable reference. The state *should* have come straight from block + // processing, which initialises the cache, but if we add other `on_block` + // calls in future it could be worth passing a mutable reference. per_epoch_processing::altair::process_justification_and_finalization(state)? } BeaconBlockRef::Base(_) => { diff --git a/consensus/state_processing/src/all_caches.rs b/consensus/state_processing/src/all_caches.rs index bd64d9f6643..af0e394221c 100644 --- a/consensus/state_processing/src/all_caches.rs +++ b/consensus/state_processing/src/all_caches.rs @@ -24,7 +24,7 @@ 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)?; + initialize_progressive_balances_cache(self, spec)?; Ok(()) } 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 fb65e583ba8..3fc21ed41d8 100644 --- a/consensus/state_processing/src/common/update_progressive_balances_cache.rs +++ b/consensus/state_processing/src/common/update_progressive_balances_cache.rs @@ -3,21 +3,16 @@ use crate::metrics::{ PARTICIPATION_CURR_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, PARTICIPATION_PREV_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, }; -use crate::per_epoch_processing::altair::ParticipationCache; use crate::{BlockProcessingError, EpochProcessingError}; use lighthouse_metrics::set_gauge; -use std::borrow::Cow; use types::{ is_progressive_balances_enabled, BeaconState, BeaconStateError, ChainSpec, Epoch, - EpochTotalBalances, EthSpec, ProgressiveBalancesCache, + EpochTotalBalances, EthSpec, ParticipationFlags, ProgressiveBalancesCache, Validator, }; -/// Initializes the `ProgressiveBalancesCache` cache using balance values from the -/// `ParticipationCache`. If the optional `&ParticipationCache` is not supplied, it will be computed -/// from the `BeaconState`. +/// Initializes the `ProgressiveBalancesCache` if it is unbuilt. pub fn initialize_progressive_balances_cache( state: &mut BeaconState, - maybe_participation_cache: Option<&ParticipationCache>, spec: &ChainSpec, ) -> Result<(), BeaconStateError> { if !is_progressive_balances_enabled(state) @@ -26,29 +21,37 @@ 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 => { - state.build_total_active_balance_cache_at(state.current_epoch(), spec)?; - Cow::Owned( - ParticipationCache::new(state, spec) - .map_err(|e| BeaconStateError::ParticipationCacheError(format!("{e:?}")))?, - ) + // Calculate the total flag balances for previous & current epoch in a single iteration. + // This calculates `get_total_balance(unslashed_participating_indices(..))` for each flag in + // the current and previous epoch. + let current_epoch = state.current_epoch(); + let previous_epoch = state.previous_epoch(); + let mut previous_epoch_cache = EpochTotalBalances::new(spec); + let mut current_epoch_cache = EpochTotalBalances::new(spec); + for ((validator, current_epoch_flags), previous_epoch_flags) in state + .validators() + .iter() + .zip(state.current_epoch_participation()?) + .zip(state.previous_epoch_participation()?) + { + // Exclude slashed validators. We are calculating *unslashed* participating totals. + if validator.slashed { + continue; } - }; - 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, - }; + // Update current epoch flag balances. + if validator.is_active_at(current_epoch) { + update_flag_total_balances(&mut current_epoch_cache, *current_epoch_flags, validator)?; + } + // Update previous epoch flag balances. + if validator.is_active_at(previous_epoch) { + update_flag_total_balances( + &mut previous_epoch_cache, + *previous_epoch_flags, + validator, + )?; + } + } state.progressive_balances_cache_mut().initialize( current_epoch, @@ -61,6 +64,26 @@ pub fn initialize_progressive_balances_cache( Ok(()) } +/// During the initialization of the progressive balances for a single epoch, add +/// `validator.effective_balance` to the flag total, for each flag present in `participation_flags`. +/// +/// Pre-conditions: +/// +/// - `validator` must not be slashed +/// - the `participation_flags` must be for `validator` in the same epoch as the `total_balances` +fn update_flag_total_balances( + total_balances: &mut EpochTotalBalances, + participation_flags: ParticipationFlags, + validator: &Validator, +) -> Result<(), BeaconStateError> { + for (flag, balance) in total_balances.total_flag_balances.iter_mut().enumerate() { + if participation_flags.has_flag(flag)? { + balance.safe_add_assign(validator.effective_balance)?; + } + } + Ok(()) +} + /// Updates the `ProgressiveBalancesCache` when a new target attestation has been processed. pub fn update_progressive_balances_on_attestation( state: &mut BeaconState, diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index c994066c5d9..b358a16447f 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -121,7 +121,7 @@ pub fn per_block_processing>( // 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)?; + initialize_progressive_balances_cache(state, spec)?; state.build_slashings_cache()?; let verify_signatures = match block_signature_strategy { 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 99653d75618..3faa0d1754f 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 @@ -68,12 +68,10 @@ pub fn process_sync_aggregate( increase_balance(state, participant_index, participant_reward)?; } proposer_balance.safe_add_assign(proposer_reward)?; + } else if participant_index == proposer_index { + proposer_balance = proposer_balance.saturating_sub(participant_reward); } else { - if participant_index == proposer_index { - proposer_balance = proposer_balance.saturating_sub(participant_reward); - } else { - decrease_balance(state, participant_index, participant_reward)?; - } + decrease_balance(state, participant_index, participant_reward)?; } } diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index 4576853af5c..15c2b800912 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -1,8 +1,6 @@ use super::signature_sets::Error as SignatureSetError; -use crate::per_epoch_processing::altair::participation_cache; use crate::{ContextError, EpochCacheError}; use merkle_proof::MerkleTreeError; -use participation_cache::Error as ParticipationCacheError; use safe_arith::ArithError; use ssz::DecodeError; use types::*; @@ -90,7 +88,6 @@ pub enum BlockProcessingError { found: Hash256, }, WithdrawalCredentialsInvalid, - ParticipationCacheError(ParticipationCacheError), } impl From for BlockProcessingError { @@ -154,12 +151,6 @@ impl From> for BlockProcessingError { } } -impl From for BlockProcessingError { - fn from(e: ParticipationCacheError) -> Self { - BlockProcessingError::ParticipationCacheError(e) - } -} - /// A conversion that consumes `self` and adds an `index` variable to resulting struct. /// /// Used here to allow converting an error into an upstream error that points to the object that diff --git a/consensus/state_processing/src/per_epoch_processing/altair.rs b/consensus/state_processing/src/per_epoch_processing/altair.rs index f5905aaa351..be2bba405bf 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair.rs @@ -11,7 +11,6 @@ use crate::per_epoch_processing::{ }; 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_slow; pub use sync_committee_updates::process_sync_committee_updates; @@ -19,7 +18,6 @@ use types::{BeaconState, ChainSpec, EthSpec, RelativeEpoch}; pub mod inactivity_updates; pub mod justification_and_finalization; -pub mod participation_cache; pub mod participation_flag_updates; pub mod rewards_and_penalties; pub mod sync_committee_updates; @@ -34,7 +32,7 @@ pub fn process_epoch( 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)?; + initialize_progressive_balances_cache::(state, spec)?; let sync_committee = state.current_sync_committee()?.clone(); 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 deleted file mode 100644 index 689be28adc0..00000000000 --- a/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs +++ /dev/null @@ -1,479 +0,0 @@ -//! Provides the `ParticipationCache`, a custom Lighthouse cache which attempts to reduce CPU and -//! memory usage by: -//! -//! - Caching a map of `validator_index -> participation_flags` for all active validators in the -//! previous and current epochs. -//! - Caching the total balances of: -//! - All active validators. -//! - All active validators matching each of the three "timely" flags. -//! - Caching the "eligible" validators. -//! -//! 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, 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)] -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. - 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(spec: &ChainSpec) -> Self { - let zero_balance = Balance::zero(spec.effective_balance_increment); - - Self { - total_flag_balances: [zero_balance; NUM_FLAG_INDICES], - total_active_balance: zero_balance, - } - } - - /// Returns the total balance of attesters who have `flag_index` set. - fn total_flag_balance(&self, flag_index: usize) -> Result { - self.total_flag_balances - .get(flag_index) - .map(Balance::get) - .ok_or(Error::InvalidFlagIndex(flag_index)) - } - - /// Returns the raw total balance of attesters who have `flag_index` set. - fn total_flag_balance_raw(&self, flag_index: usize) -> Result { - self.total_flag_balances - .get(flag_index) - .copied() - .ok_or(Error::InvalidFlagIndex(flag_index)) - } - - /// Process an **active** validator, reading from the `epoch_participation` with respect to the - /// `relative_epoch`. - /// - /// ## Errors - /// - /// - An error will be returned if the `val_index` validator is inactive at the given - /// `relative_epoch`. - fn process_active_validator( - &mut self, - val_index: usize, - validator: &Validator, - epoch_participation: &ParticipationFlags, - current_epoch: Epoch, - relative_epoch: RelativeEpoch, - ) -> Result<(), BeaconStateError> { - // 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 }); - } - - // All active validators increase the total active balance. - self.total_active_balance - .safe_add_assign(validator.effective_balance)?; - - // Only unslashed validators may proceed. - if validator.slashed { - return Ok(()); - } - - // 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(validator.effective_balance)?; - } - } - - Ok(()) - } -} - -#[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`. - pub(crate) current_epoch_participation: SingleEpochParticipationCache, - previous_epoch: Epoch, - /// Caches information about active validators pertaining to `self.previous_epoch`. - 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 { - /// Instantiate `Self`, returning a fully initialized cache. - /// - /// ## Errors - /// - /// - The provided `state` **must** be an Altair state. An error will be returned otherwise. - 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(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. - // - Slashed, but not yet withdrawable. - // - // Using the full length of `state.validators` is almost always overkill, but it ensures no - // 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. - // 2. The "eligible indices". - // - // Care is taken to ensure that the ordering of `eligible_indices` is the same as the - // `get_eligible_validator_indices` function in the spec. - 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, - val, - curr_epoch_flags, - current_epoch, - RelativeEpoch::Current, - )?; - } - - if is_active_previous_epoch { - assert!(is_eligible); - - previous_epoch_participation.process_active_validator( - val_index, - 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`. 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), - }) - } - - /// Equivalent to the specification `get_eligible_validator_indices` function. - pub fn eligible_validator_indices(&self) -> &[usize] { - &self.eligible_indices - } - - pub fn process_slashings_indices(&mut self) -> Vec<(usize, u64)> { - std::mem::take(&mut self.process_slashings_indices) - } - - /* - * Balances - */ - - pub fn current_epoch_total_active_balance(&self) -> u64 { - self.current_epoch_participation.total_active_balance.get() - } - - pub fn current_epoch_target_attesting_balance(&self) -> Result { - self.current_epoch_participation - .total_flag_balance(TIMELY_TARGET_FLAG_INDEX) - } - - pub fn current_epoch_target_attesting_balance_raw(&self) -> Result { - self.current_epoch_participation - .total_flag_balance_raw(TIMELY_TARGET_FLAG_INDEX) - } - - pub fn previous_epoch_total_active_balance(&self) -> u64 { - self.previous_epoch_participation.total_active_balance.get() - } - - pub fn previous_epoch_target_attesting_balance(&self) -> Result { - self.previous_epoch_flag_attesting_balance(TIMELY_TARGET_FLAG_INDEX) - } - - pub fn previous_epoch_target_attesting_balance_raw(&self) -> Result { - self.previous_epoch_participation - .total_flag_balance_raw(TIMELY_TARGET_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_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(flag_index) - } - - /* - * Active/Unslashed - */ - - 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 - }) - } - - 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) - .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.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. - pub fn is_previous_epoch_timely_target_attester( - &self, - val_index: usize, - ) -> Result { - 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.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_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/errors.rs b/consensus/state_processing/src/per_epoch_processing/errors.rs index c1da929c686..c18e1303b26 100644 --- a/consensus/state_processing/src/per_epoch_processing/errors.rs +++ b/consensus/state_processing/src/per_epoch_processing/errors.rs @@ -1,4 +1,3 @@ -use crate::per_epoch_processing::altair::participation_cache::Error as ParticipationCacheError; use types::{BeaconStateError, EpochCacheError, InconsistentFork}; #[derive(Debug, PartialEq)] @@ -24,7 +23,6 @@ pub enum EpochProcessingError { InconsistentStateFork(InconsistentFork), InvalidJustificationBit(ssz_types::Error), InvalidFlagIndex(usize), - ParticipationCache(ParticipationCacheError), EpochCache(EpochCacheError), } @@ -52,12 +50,6 @@ impl From for EpochProcessingError { } } -impl From for EpochProcessingError { - fn from(e: ParticipationCacheError) -> EpochProcessingError { - EpochProcessingError::ParticipationCache(e) - } -} - impl From for EpochProcessingError { fn from(e: EpochCacheError) -> Self { EpochProcessingError::EpochCache(e) diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index fd0825f2e5a..5f23ce0081c 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -110,7 +110,7 @@ pub fn process_epoch_single_pass( conf: SinglePassConfig, ) -> Result, Error> { initialize_epoch_cache(state, spec)?; - initialize_progressive_balances_cache(state, None, spec)?; + initialize_progressive_balances_cache(state, spec)?; state.build_exit_cache(spec)?; let previous_epoch = state.previous_epoch(); diff --git a/consensus/state_processing/src/upgrade/altair.rs b/consensus/state_processing/src/upgrade/altair.rs index 589e4c7a479..cfbc6eba9e9 100644 --- a/consensus/state_processing/src/upgrade/altair.rs +++ b/consensus/state_processing/src/upgrade/altair.rs @@ -114,7 +114,7 @@ pub fn upgrade_to_altair( // Fill in previous epoch participation from the pre state's pending attestations. translate_participation(&mut post, &pre.previous_epoch_attestations, spec)?; - initialize_progressive_balances_cache(&mut post, None, spec)?; + initialize_progressive_balances_cache(&mut post, spec)?; // Fill in sync committees // Note: A duplicate committee is assigned for the current and next committee at the fork diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 67ba34d0e29..77f15c0ed1f 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -115,7 +115,6 @@ pub enum Error { SszTypesError(ssz_types::Error), TreeHashCacheNotInitialized, NonLinearTreeHashCacheHistory, - ParticipationCacheError(String), ProgressiveBalancesCacheNotInitialized, ProgressiveBalancesCacheInconsistent, TreeHashCacheSkippedSlot { diff --git a/consensus/types/src/beacon_state/progressive_balances_cache.rs b/consensus/types/src/beacon_state/progressive_balances_cache.rs index 69c76447b46..a7620540bb4 100644 --- a/consensus/types/src/beacon_state/progressive_balances_cache.rs +++ b/consensus/types/src/beacon_state/progressive_balances_cache.rs @@ -283,7 +283,7 @@ impl ProgressiveBalancesCache { } } -/// `ProgressiveBalancesCache` is only enabled from `Altair` as it requires `ParticipationCache`. +/// `ProgressiveBalancesCache` is only enabled from `Altair` as it uses Altair-specific logic. pub fn is_progressive_balances_enabled(state: &BeaconState) -> bool { match state { BeaconState::Base(_) => false, diff --git a/testing/ef_tests/src/cases/epoch_processing.rs b/testing/ef_tests/src/cases/epoch_processing.rs index 7ad38cb6f10..af695eb94e0 100644 --- a/testing/ef_tests/src/cases/epoch_processing.rs +++ b/testing/ef_tests/src/cases/epoch_processing.rs @@ -110,7 +110,7 @@ impl EpochTransition for JustificationAndFinalization { | BeaconState::Merge(_) | BeaconState::Capella(_) | BeaconState::Deneb(_) => { - initialize_progressive_balances_cache(state, None, spec)?; + initialize_progressive_balances_cache(state, spec)?; let justification_and_finalization_state = altair::process_justification_and_finalization(state)?; justification_and_finalization_state.apply_changes_to_state(state); diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 14d047dae88..177fa301471 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -104,7 +104,7 @@ impl Operation for Attestation { | BeaconState::Merge(_) | BeaconState::Capella(_) | BeaconState::Deneb(_) => { - initialize_progressive_balances_cache(state, None, spec)?; + initialize_progressive_balances_cache(state, spec)?; altair_deneb::process_attestation( state, self, @@ -134,7 +134,7 @@ impl Operation for AttesterSlashing { _: &Operations, ) -> Result<(), BlockProcessingError> { let mut ctxt = ConsensusContext::new(state.slot()); - initialize_progressive_balances_cache(state, None, spec)?; + initialize_progressive_balances_cache(state, spec)?; process_attester_slashings( state, &[self.clone()], @@ -185,7 +185,7 @@ impl Operation for ProposerSlashing { _: &Operations, ) -> Result<(), BlockProcessingError> { let mut ctxt = ConsensusContext::new(state.slot()); - initialize_progressive_balances_cache(state, None, spec)?; + initialize_progressive_balances_cache(state, spec)?; process_proposer_slashings( state, &[self.clone()], From c66b08c379b683b549c22ff7f2db9994b682783b Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 22 Feb 2024 10:30:11 +0800 Subject: [PATCH 10/21] update help --- book/src/help_bn.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 3d2124964b2..021c5432d38 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -385,10 +385,8 @@ OPTIONS: useful for execution nodes which don't improve their payload after the first call, and high values are useful for ensuring the EL is given ample notice. Default: 1/3 of a slot. --progressive-balances - Control the progressive balances cache mode. The default `fast` mode uses the cache to speed up fork choice. - A more conservative `checked` mode compares the cache's results against results without the cache. If there - is a mismatch, it falls back to the cache-free result. Using the default `fast` mode is recommended unless - advised otherwise by the Lighthouse team. [possible values: disabled, checked, strict, fast] + Deprecated. This optimisation is now the default and cannot be disabled. [possible values: fast, disabled, + checked, strict] --proposer-reorg-cutoff Maximum delay after the start of the slot at which to propose a reorging block. Lower values can prevent failed reorgs by ensuring the block has ample time to propagate and be processed by the network. The default From 11a39bdb7215437a832a90c3147893a2dd9f20f6 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 22 Feb 2024 12:18:49 +0800 Subject: [PATCH 11/21] Fix op_pool tests --- beacon_node/operation_pool/src/lib.rs | 37 ++++++++++++++++--- consensus/state_processing/src/epoch_cache.rs | 26 +++++++++---- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 7e1ddb1fd2f..4651ea92988 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -18,6 +18,8 @@ pub use persistence::{ PersistedOperationPoolV15, PersistedOperationPoolV5, }; pub use reward_cache::RewardCache; +use state_processing::epoch_cache::is_epoch_cache_initialized; +use types::EpochCacheError; use crate::attestation_storage::{AttestationMap, CheckpointKey}; use crate::bls_to_execution_changes::BlsToExecutionChanges; @@ -75,6 +77,8 @@ pub enum OpPoolError { RewardCacheValidatorUnknown(BeaconStateError), RewardCacheOutOfBounds, IncorrectOpPoolVariant, + EpochCacheNotInitialized, + EpochCacheError(EpochCacheError), } #[derive(Default)] @@ -252,6 +256,17 @@ impl OperationPool { curr_epoch_validity_filter: impl for<'a> FnMut(&AttestationRef<'a, T>) -> bool + Send, spec: &ChainSpec, ) -> Result>, OpPoolError> { + if let BeaconState::Base(_) = state { + // Ok + } else { + // epoch cache must be initialized to fetch base_reward values in the max_cover score + // function. Currently max_cover ignores items on errors. If epoch cache is not + // initialized, this function returns Ok([]). + if !is_epoch_cache_initialized(state).map_err(OpPoolError::EpochCacheError)? { + return Err(OpPoolError::EpochCacheNotInitialized); + } + }; + // Attestations for the current fork, which may be from the current or previous epoch. let (prev_epoch_key, curr_epoch_key) = CheckpointKey::keys_for_state(state); let all_attestations = self.attestations.read(); @@ -773,6 +788,7 @@ mod release_tests { }; use lazy_static::lazy_static; use maplit::hashset; + use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::{common::get_attesting_indices_from_state, VerifyOperation}; use std::collections::BTreeSet; use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; @@ -814,6 +830,15 @@ mod release_tests { (harness, spec) } + fn get_current_state_initialize_epoch_cache( + harness: &BeaconChainHarness>, + spec: &ChainSpec, + ) -> BeaconState { + let mut state = harness.get_current_state(); + initialize_epoch_cache(&mut state, spec).unwrap(); + state + } + /// Test state for sync contribution-related tests. async fn sync_contribution_test_state( num_committees: usize, @@ -847,7 +872,7 @@ mod release_tests { return; } - let mut state = harness.get_current_state(); + let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); let slot = state.slot(); let committees = state .get_beacon_committees_at_slot(slot) @@ -929,7 +954,7 @@ mod release_tests { let (harness, ref spec) = attestation_test_state::(1); let op_pool = OperationPool::::new(); - let mut state = harness.get_current_state(); + let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); let slot = state.slot(); let committees = state @@ -1004,7 +1029,7 @@ mod release_tests { fn attestation_duplicate() { let (harness, ref spec) = attestation_test_state::(1); - let state = harness.get_current_state(); + let state = get_current_state_initialize_epoch_cache(&harness, &spec); let op_pool = OperationPool::::new(); @@ -1044,7 +1069,7 @@ mod release_tests { fn attestation_pairwise_overlapping() { let (harness, ref spec) = attestation_test_state::(1); - let state = harness.get_current_state(); + let state = get_current_state_initialize_epoch_cache(&harness, &spec); let op_pool = OperationPool::::new(); @@ -1142,7 +1167,7 @@ mod release_tests { let (harness, ref spec) = attestation_test_state::(num_committees); - let mut state = harness.get_current_state(); + let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); let op_pool = OperationPool::::new(); @@ -1232,7 +1257,7 @@ mod release_tests { let (harness, ref spec) = attestation_test_state::(num_committees); - let mut state = harness.get_current_state(); + let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); let op_pool = OperationPool::::new(); let slot = state.slot(); diff --git a/consensus/state_processing/src/epoch_cache.rs b/consensus/state_processing/src/epoch_cache.rs index de0782d762e..954837d1977 100644 --- a/consensus/state_processing/src/epoch_cache.rs +++ b/consensus/state_processing/src/epoch_cache.rs @@ -72,25 +72,35 @@ impl PreEpochCache { } } -pub fn initialize_epoch_cache( - state: &mut BeaconState, - spec: &ChainSpec, -) -> Result<(), EpochCacheError> { +pub fn is_epoch_cache_initialized( + state: &BeaconState, +) -> Result { 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 + Ok(epoch_cache .check_validity::(current_epoch, decision_block_root) - .is_ok() - { + .is_ok()) +} + +pub fn initialize_epoch_cache( + state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), EpochCacheError> { + if is_epoch_cache_initialized(state)? { // `EpochCache` has already been initialized and is valid, no need to initialize. return Ok(()); } + let current_epoch = state.current_epoch(); + let next_epoch = state.next_epoch().map_err(EpochCacheError::BeaconState)?; + let decision_block_root = state + .proposer_shuffling_decision_root(Hash256::zero()) + .map_err(EpochCacheError::BeaconState)?; + state.build_total_active_balance_cache_at(current_epoch, spec)?; let total_active_balance = state.get_total_active_balance_at_epoch(current_epoch)?; From e5341d591d369ccfd10599ebe720e7169b77ef2c Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 22 Feb 2024 16:13:57 +1100 Subject: [PATCH 12/21] Fix fork choice tests --- consensus/fork_choice/tests/tests.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 29f296b1b41..aa66fb6250d 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -47,15 +47,7 @@ impl fmt::Debug for ForkChoiceTest { impl ForkChoiceTest { /// Creates a new tester. pub fn new() -> Self { - // Run fork choice tests against the latest fork. - let spec = ForkName::latest().make_genesis_spec(ChainSpec::default()); - let harness = BeaconChainHarness::builder(MainnetEthSpec) - .spec(spec) - .deterministic_keypairs(VALIDATOR_COUNT) - .fresh_ephemeral_store() - .build(); - - Self { harness } + Self::new_with_chain_config(ChainConfig::default()) } /// Creates a new tester with a custom chain config. @@ -67,6 +59,7 @@ impl ForkChoiceTest { .chain_config(chain_config) .deterministic_keypairs(VALIDATOR_COUNT) .fresh_ephemeral_store() + .mock_execution_layer() .build(); Self { harness } From 42a3867d4974fdbbbdeb5eb226d02ea7b3050480 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 23 Feb 2024 16:15:31 +1100 Subject: [PATCH 13/21] Simplify exit cache (#5280) --- consensus/types/src/beacon_state.rs | 4 ++ .../types/src/beacon_state/exit_cache.rs | 49 +++++++++++++------ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 77f15c0ed1f..36d26aa3f2b 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -105,6 +105,10 @@ pub enum Error { }, RelativeEpochError(RelativeEpochError), ExitCacheUninitialized, + ExitCacheInvalidEpoch { + max_exit_epoch: Epoch, + request_epoch: Epoch, + }, SlashingsCacheUninitialized { initialized_slot: Option, latest_block_slot: Slot, diff --git a/consensus/types/src/beacon_state/exit_cache.rs b/consensus/types/src/beacon_state/exit_cache.rs index cb96fba6917..1ab7a8e3119 100644 --- a/consensus/types/src/beacon_state/exit_cache.rs +++ b/consensus/types/src/beacon_state/exit_cache.rs @@ -1,13 +1,16 @@ use super::{BeaconStateError, ChainSpec, Epoch, Validator}; -use safe_arith::SafeArith; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::cmp::Ordering; /// Map from exit epoch to the number of validators with that exit epoch. #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub struct ExitCache { + /// True if the cache has been initialized. initialized: bool, - exit_epoch_counts: HashMap, + /// Maximum `exit_epoch` of any validator. + max_exit_epoch: Epoch, + /// Number of validators known to be exiting at `max_exit_epoch`. + max_exit_epoch_churn: u64, } impl ExitCache { @@ -15,7 +18,8 @@ impl ExitCache { pub fn new(validators: &[Validator], spec: &ChainSpec) -> Result { let mut exit_cache = ExitCache { initialized: true, - ..ExitCache::default() + max_exit_epoch: Epoch::new(0), + max_exit_epoch_churn: 0, }; // Add all validators with a non-default exit epoch to the cache. validators @@ -37,27 +41,44 @@ impl ExitCache { /// Record the exit epoch of a validator. Must be called only once per exiting validator. pub fn record_validator_exit(&mut self, exit_epoch: Epoch) -> Result<(), BeaconStateError> { self.check_initialized()?; - self.exit_epoch_counts - .entry(exit_epoch) - .or_insert(0) - .safe_add_assign(1)?; + match exit_epoch.cmp(&self.max_exit_epoch) { + // Update churn for the current maximum epoch. + Ordering::Equal => { + self.max_exit_epoch_churn += 1; + } + // Increase the max exit epoch, reset the churn to 1. + Ordering::Greater => { + self.max_exit_epoch = exit_epoch; + self.max_exit_epoch_churn = 1; + } + // Older exit epochs are not relevant. + Ordering::Less => (), + } Ok(()) } /// Get the largest exit epoch with a non-zero exit epoch count. pub fn max_epoch(&self) -> Result, BeaconStateError> { self.check_initialized()?; - Ok(self.exit_epoch_counts.keys().max().cloned()) + Ok((self.max_exit_epoch_churn > 0).then_some(self.max_exit_epoch)) } /// Get number of validators with the given exit epoch. (Return 0 for the default exit epoch.) pub fn get_churn_at(&self, exit_epoch: Epoch) -> Result { self.check_initialized()?; - Ok(self - .exit_epoch_counts - .get(&exit_epoch) - .cloned() - .unwrap_or(0)) + match exit_epoch.cmp(&self.max_exit_epoch) { + // Epochs are equal, we know the churn exactly. + Ordering::Equal => Ok(self.max_exit_epoch_churn), + // If exiting at an epoch later than the cached epoch then the churn is 0. This is a + // common case which happens when there are no exits for an epoch. + Ordering::Greater => Ok(0), + // Consensus code should never require the churn at an epoch prior to the cached epoch. + // That's a bug. + Ordering::Less => Err(BeaconStateError::ExitCacheInvalidEpoch { + max_exit_epoch: self.max_exit_epoch, + request_epoch: exit_epoch, + }), + } } } From ea287976e623a0945374b2d31a0a97ba825c14b8 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 23 Feb 2024 16:18:13 +1100 Subject: [PATCH 14/21] Fix clippy on exit cache --- consensus/types/src/beacon_state/exit_cache.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/consensus/types/src/beacon_state/exit_cache.rs b/consensus/types/src/beacon_state/exit_cache.rs index 1ab7a8e3119..bda788e63b9 100644 --- a/consensus/types/src/beacon_state/exit_cache.rs +++ b/consensus/types/src/beacon_state/exit_cache.rs @@ -1,4 +1,5 @@ use super::{BeaconStateError, ChainSpec, Epoch, Validator}; +use safe_arith::SafeArith; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; @@ -44,7 +45,7 @@ impl ExitCache { match exit_epoch.cmp(&self.max_exit_epoch) { // Update churn for the current maximum epoch. Ordering::Equal => { - self.max_exit_epoch_churn += 1; + self.max_exit_epoch_churn.safe_add_assign(1)?; } // Increase the max exit epoch, reset the churn to 1. Ordering::Greater => { From efb457bfb4aa96e16682ffbb98ec2c87d6e9b2c2 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 23 Feb 2024 19:00:53 +1100 Subject: [PATCH 15/21] Clean up single-pass a bit (#5282) --- .../beacon_chain/src/attestation_rewards.rs | 3 +-- beacon_node/genesis/src/interop.rs | 10 +++---- book/src/validator-inclusion.md | 4 +-- consensus/state_processing/src/all_caches.rs | 6 ++--- consensus/state_processing/src/common/base.rs | 2 +- .../process_operations.rs | 1 - .../effective_balance_updates.rs | 2 ++ consensus/types/src/beacon_state.rs | 26 +++++++------------ 8 files changed, 20 insertions(+), 34 deletions(-) diff --git a/beacon_node/beacon_chain/src/attestation_rewards.rs b/beacon_node/beacon_chain/src/attestation_rewards.rs index 5c3ddb9b3b9..48b623138d9 100644 --- a/beacon_node/beacon_chain/src/attestation_rewards.rs +++ b/beacon_node/beacon_chain/src/attestation_rewards.rs @@ -193,8 +193,7 @@ impl BeaconChain { }; 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. + // Return 0s for unknown/inactive validator indices. let Ok(validator) = state.get_validator(validator_index) else { debug!( self.log, diff --git a/beacon_node/genesis/src/interop.rs b/beacon_node/genesis/src/interop.rs index 5c67e045f8a..d0129834300 100644 --- a/beacon_node/genesis/src/interop.rs +++ b/beacon_node/genesis/src/interop.rs @@ -178,14 +178,13 @@ mod test { } for v in state.validators() { - let creds = v.withdrawal_credentials; + let creds = v.withdrawal_credentials.as_bytes(); assert_eq!( - creds.as_bytes()[0], - spec.bls_withdrawal_prefix_byte, + creds[0], spec.bls_withdrawal_prefix_byte, "first byte of withdrawal creds should be bls prefix" ); assert_eq!( - &creds.as_bytes()[1..], + &creds[1..], &hash(&v.pubkey.as_ssz_bytes())[1..], "rest of withdrawal creds should be pubkey hash" ) @@ -241,8 +240,7 @@ mod test { } for (index, v) in state.validators().iter().enumerate() { - let withdrawal_credientials = v.withdrawal_credentials; - let creds = withdrawal_credientials.as_bytes(); + let creds = v.withdrawal_credentials.as_bytes(); if index % 2 == 0 { assert_eq!( creds[0], spec.bls_withdrawal_prefix_byte, diff --git a/book/src/validator-inclusion.md b/book/src/validator-inclusion.md index cd31d78d62d..f31d7294499 100644 --- a/book/src/validator-inclusion.md +++ b/book/src/validator-inclusion.md @@ -56,7 +56,6 @@ The following fields are returned: able to vote) during the current epoch. - `current_epoch_target_attesting_gwei`: the total staked gwei that attested to the majority-elected Casper FFG target epoch during the current epoch. -- `previous_epoch_active_gwei`: as per `current_epoch_active_gwei`, but during the previous epoch. - `previous_epoch_target_attesting_gwei`: see `current_epoch_target_attesting_gwei`. - `previous_epoch_head_attesting_gwei`: the total staked gwei that attested to a head beacon block that is in the canonical chain. @@ -65,7 +64,7 @@ From this data you can calculate: #### Justification/Finalization Rate -`previous_epoch_target_attesting_gwei / previous_epoch_active_gwei` +`previous_epoch_target_attesting_gwei / current_epoch_active_gwei` When this value is greater than or equal to `2/3` it is possible that the beacon chain may justify and/or finalize the epoch. @@ -80,7 +79,6 @@ curl -X GET "http://localhost:5052/lighthouse/validator_inclusion/0/global" -H { "data": { "current_epoch_active_gwei": 642688000000000, - "previous_epoch_active_gwei": 642688000000000, "current_epoch_target_attesting_gwei": 366208000000000, "previous_epoch_target_attesting_gwei": 1000000000, "previous_epoch_head_attesting_gwei": 1000000000 diff --git a/consensus/state_processing/src/all_caches.rs b/consensus/state_processing/src/all_caches.rs index af0e394221c..41382813f1c 100644 --- a/consensus/state_processing/src/all_caches.rs +++ b/consensus/state_processing/src/all_caches.rs @@ -9,14 +9,12 @@ use types::{BeaconState, ChainSpec, EpochCacheError, EthSpec, Hash256, RelativeE pub trait AllCaches { /// Build all caches. /// - /// Note that this excludes milhouse's intrinsic tree-hash cache. That needs to be managed - /// separately. + /// Note that this excludes the 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. + /// Note that this excludes the tree-hash cache. That needs to be managed separately. fn all_caches_built(&self) -> bool; } diff --git a/consensus/state_processing/src/common/base.rs b/consensus/state_processing/src/common/base.rs index 3b08b783228..cb6ea6c04cd 100644 --- a/consensus/state_processing/src/common/base.rs +++ b/consensus/state_processing/src/common/base.rs @@ -3,7 +3,7 @@ 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`). +/// since they are used in close proximity and have the same type (`u64`). #[derive(Copy, Clone)] pub struct SqrtTotalActiveBalance(u64); 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 d83b3434cde..20ada1a3213 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -113,7 +113,6 @@ pub mod altair_deneb { }) } - #[allow(clippy::too_many_arguments)] pub fn process_attestation( state: &mut BeaconState, attestation: &Attestation, 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 920199908d5..d56f8367209 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 @@ -5,6 +5,7 @@ use types::beacon_state::BeaconState; use types::chain_spec::ChainSpec; use types::{BeaconStateError, EthSpec}; +/// This implementation is now only used in phase0. Later hard forks use single-pass. pub fn process_effective_balance_updates( state: &mut BeaconState, spec: &ChainSpec, @@ -52,6 +53,7 @@ pub fn process_effective_balance_updates( Ok(()) } +/// Only used to test the effective balance part of single-pass in isolation. pub fn process_effective_balance_updates_slow( state: &mut BeaconState, spec: &ChainSpec, diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 36d26aa3f2b..d2a8ee75cff 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -1480,24 +1480,10 @@ impl BeaconState { Ok(cache.get_attestation_duties(validator_index)) } - /// Implementation of `get_total_balance`, matching the spec. + /// Build the total active balance cache from scratch. /// - /// Returns minimum `EFFECTIVE_BALANCE_INCREMENT`, to avoid div by 0. - pub fn get_total_balance<'a, I: IntoIterator>( - &'a self, - validator_indices: I, - spec: &ChainSpec, - ) -> Result { - let total_balance = validator_indices.into_iter().try_fold(0_u64, |acc, i| { - self.get_effective_balance(*i) - .and_then(|bal| Ok(acc.safe_add(bal)?)) - })?; - Ok(std::cmp::max( - total_balance, - spec.effective_balance_increment, - )) - } - + /// This method should rarely be invoked because single-pass epoch processing keeps the total + /// active balance cache up to date. pub fn compute_total_active_balance_slow( &self, epoch: Epoch, @@ -1530,6 +1516,7 @@ impl BeaconState { self.get_total_active_balance_at_epoch(self.current_epoch()) } + /// Get the cached total active balance while checking that it is for the correct `epoch`. pub fn get_total_active_balance_at_epoch(&self, epoch: Epoch) -> Result { let (initialized_epoch, balance) = self .total_active_balance() @@ -1545,6 +1532,10 @@ impl BeaconState { } } + /// Manually set the total active balance. + /// + /// This should only be called when the total active balance has been computed as part of + /// single-pass epoch processing (or `process_rewards_and_penalties` for phase0). pub fn set_total_active_balance(&mut self, epoch: Epoch, balance: u64) { *self.total_active_balance_mut() = Some((epoch, balance)); } @@ -1561,6 +1552,7 @@ impl BeaconState { Ok(()) } + /// Build the total active balance cache, even if it is already built. pub fn force_build_total_active_balance_cache_at( &mut self, epoch: Epoch, From 4d3d507e1415e54ed9c87a330bbf47289c0a76c2 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 12 Mar 2024 10:06:36 +1100 Subject: [PATCH 16/21] Address Mark's review of single-pass (#5386) --- .../beacon_chain/src/attestation_rewards.rs | 35 +++++++++++-------- .../src/common/initiate_validator_exit.rs | 4 ++- .../src/per_block_processing.rs | 2 +- .../process_operations.rs | 9 ++--- .../altair/inactivity_updates.rs | 4 +-- .../src/per_epoch_processing/single_pass.rs | 4 ++- consensus/types/src/beacon_state.rs | 17 +++++---- consensus/types/src/chain_spec.rs | 3 ++ 8 files changed, 46 insertions(+), 32 deletions(-) diff --git a/beacon_node/beacon_chain/src/attestation_rewards.rs b/beacon_node/beacon_chain/src/attestation_rewards.rs index 48b623138d9..1fb90d934cf 100644 --- a/beacon_node/beacon_chain/src/attestation_rewards.rs +++ b/beacon_node/beacon_chain/src/attestation_rewards.rs @@ -1,14 +1,27 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2::lighthouse::attestation_rewards::{IdealAttestationRewards, TotalAttestationRewards}; use eth2::lighthouse::StandardAttestationRewards; +use eth2::types::ValidatorId; use safe_arith::SafeArith; use serde_utils::quoted_u64::Quoted; use slog::debug; +use state_processing::common::base::get_base_reward_from_effective_balance; use state_processing::per_epoch_processing::altair::{ process_inactivity_updates_slow, process_justification_and_finalization, }; +use state_processing::per_epoch_processing::base::rewards_and_penalties::{ + get_attestation_component_delta, get_attestation_deltas_all, get_attestation_deltas_subset, + get_inactivity_penalty_delta, get_inclusion_delay_delta, +}; +use state_processing::per_epoch_processing::base::validator_statuses::InclusionInfo; +use state_processing::per_epoch_processing::base::{ + process_justification_and_finalization as process_justification_and_finalization_base, + TotalBalances, ValidatorStatus, ValidatorStatuses, +}; use state_processing::{ common::altair::BaseRewardPerIncrement, + common::update_progressive_balances_cache::initialize_progressive_balances_cache, + epoch_cache::initialize_epoch_cache, per_epoch_processing::altair::rewards_and_penalties::get_flag_weight, }; use std::collections::HashMap; @@ -17,20 +30,7 @@ use store::consts::altair::{ TIMELY_TARGET_FLAG_INDEX, }; use types::consts::altair::WEIGHT_DENOMINATOR; - -use types::{BeaconState, Epoch, EthSpec}; - -use eth2::types::ValidatorId; -use state_processing::common::base::get_base_reward_from_effective_balance; -use state_processing::per_epoch_processing::base::rewards_and_penalties::{ - get_attestation_component_delta, get_attestation_deltas_all, get_attestation_deltas_subset, - get_inactivity_penalty_delta, get_inclusion_delay_delta, -}; -use state_processing::per_epoch_processing::base::validator_statuses::InclusionInfo; -use state_processing::per_epoch_processing::base::{ - process_justification_and_finalization as process_justification_and_finalization_base, - TotalBalances, ValidatorStatus, ValidatorStatuses, -}; +use types::{BeaconState, Epoch, EthSpec, RelativeEpoch}; impl BeaconChain { pub fn compute_attestation_rewards( @@ -132,6 +132,13 @@ impl BeaconChain { ) -> Result { let spec = &self.spec; + // Build required caches. + initialize_epoch_cache(&mut state, spec)?; + initialize_progressive_balances_cache(&mut state, spec)?; + state.build_exit_cache(spec)?; + state.build_committee_cache(RelativeEpoch::Previous, spec)?; + state.build_committee_cache(RelativeEpoch::Current, spec)?; + // Calculate ideal_rewards process_justification_and_finalization(&state)?.apply_changes_to_state(&mut state); process_inactivity_updates_slow(&mut state, spec)?; diff --git a/consensus/state_processing/src/common/initiate_validator_exit.rs b/consensus/state_processing/src/common/initiate_validator_exit.rs index f8c159b2b64..f77c1f5c402 100644 --- a/consensus/state_processing/src/common/initiate_validator_exit.rs +++ b/consensus/state_processing/src/common/initiate_validator_exit.rs @@ -11,7 +11,9 @@ pub fn initiate_validator_exit( // 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. + // validator tree (expensive), make the check and mutate as appropriate. Compared to the spec + // ordering, this saves us from looking up the validator in the validator registry multiple + // times. // Ensure the exit cache is built. state.build_exit_cache(spec)?; diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index b358a16447f..eeaf3a77160 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -163,7 +163,7 @@ pub fn per_block_processing>( } else { verify_signatures }; - // Ensure the current and previous epoch caches are built. + // Ensure the current and previous epoch committee caches are built. state.build_committee_cache(RelativeEpoch::Previous, spec)?; state.build_committee_cache(RelativeEpoch::Current, spec)?; 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 20ada1a3213..78ed53ee407 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -54,8 +54,12 @@ pub mod base { ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - // Ensure the previous epoch cache exists. + // Ensure required caches are all built. These should be no-ops during regular operation. + state.build_committee_cache(RelativeEpoch::Current, spec)?; state.build_committee_cache(RelativeEpoch::Previous, spec)?; + initialize_epoch_cache(state, spec)?; + initialize_progressive_balances_cache(state, spec)?; + state.build_slashings_cache()?; let proposer_index = ctxt.get_proposer_index(state, spec)?; @@ -121,9 +125,6 @@ pub mod altair_deneb { verify_signatures: VerifySignatures, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - state.build_committee_cache(RelativeEpoch::Previous, spec)?; - state.build_committee_cache(RelativeEpoch::Current, spec)?; - let proposer_index = ctxt.get_proposer_index(state, spec)?; let previous_epoch = ctxt.previous_epoch; let current_epoch = ctxt.current_epoch; 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 592ffc09616..b136f7a7f90 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 @@ -4,9 +4,9 @@ use types::beacon_state::BeaconState; use types::chain_spec::ChainSpec; use types::eth_spec::EthSpec; -/// Slow version of `process_inactivity_updates`. +/// Slow version of `process_inactivity_updates` that runs a subset of single-pass processing. /// -/// Should only be used for testing. +/// Should not be used for block processing, but is useful for testing & analytics. pub fn process_inactivity_updates_slow( state: &mut BeaconState, spec: &ChainSpec, diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index 5f23ce0081c..b3ae0e5a9a5 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -13,7 +13,7 @@ use types::{ TIMELY_TARGET_FLAG_INDEX, WEIGHT_DENOMINATOR, }, ActivationQueue, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ExitCache, ForkName, - ParticipationFlags, ProgressiveBalancesCache, Unsigned, Validator, + ParticipationFlags, ProgressiveBalancesCache, RelativeEpoch, Unsigned, Validator, }; pub struct SinglePassConfig { @@ -112,6 +112,8 @@ pub fn process_epoch_single_pass( initialize_epoch_cache(state, spec)?; initialize_progressive_balances_cache(state, spec)?; state.build_exit_cache(spec)?; + state.build_committee_cache(RelativeEpoch::Previous, spec)?; + state.build_committee_cache(RelativeEpoch::Current, spec)?; let previous_epoch = state.previous_epoch(); let current_epoch = state.current_epoch(); diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index d2a8ee75cff..7a5337b7b6f 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -1433,9 +1433,7 @@ impl BeaconState { /// Return the churn limit for the current epoch (number of validators who can leave per epoch). /// - /// Uses the epoch cache, and will error if it isn't initialized. - /// - /// Spec v0.12.1 + /// Uses the current epoch committee cache, and will error if it isn't initialized. pub fn get_churn_limit(&self, spec: &ChainSpec) -> Result { Ok(std::cmp::max( spec.min_per_epoch_churn_limit, @@ -1448,9 +1446,7 @@ impl BeaconState { /// Return the activation churn limit for the current epoch (number of validators who can enter per epoch). /// - /// Uses the epoch cache, and will error if it isn't initialized. - /// - /// Spec v1.4.0 + /// Uses the current epoch committee cache, and will error if it isn't initialized. pub fn get_activation_churn_limit(&self, spec: &ChainSpec) -> Result { Ok(match self { BeaconState::Base(_) @@ -1673,7 +1669,7 @@ impl BeaconState { }) } - /// Build an epoch cache, unless it is has already been built. + /// Build a committee cache, unless it is has already been built. pub fn build_committee_cache( &mut self, relative_epoch: RelativeEpoch, @@ -1694,7 +1690,7 @@ impl BeaconState { Ok(()) } - /// Always builds the previous epoch cache, even if it is already initialized. + /// Always builds the requested committee cache, even if it is already initialized. pub fn force_build_committee_cache( &mut self, relative_epoch: RelativeEpoch, @@ -1724,7 +1720,7 @@ impl BeaconState { /// This should be used if the `slot` of this state is advanced beyond an epoch boundary. /// /// 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. + /// balance cache if the (new) current committee cache is initialized. pub fn advance_caches(&mut self, _spec: &ChainSpec) -> Result<(), Error> { self.committee_caches_mut().rotate_left(1); @@ -1970,6 +1966,9 @@ impl BeaconState { Ok(sync_committee) } + /// Get the base reward for `validator_index` from the epoch cache. + /// + /// This function will error if the epoch cache is not initialized. pub fn get_base_reward(&self, validator_index: usize) -> Result { self.epoch_cache().get_base_reward(validator_index) } diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index e5ba7ab7c97..3f2efc7d5d5 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -495,6 +495,9 @@ impl ChainSpec { Hash256::from(domain) } + /// Compute the epoch used for activations prior to Deneb, and for exits under all forks. + /// + /// Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_activation_exit_epoch pub fn compute_activation_exit_epoch(&self, epoch: Epoch) -> Result { epoch.safe_add(1)?.safe_add(self.max_seed_lookahead) } From 05bc1eeb2bacbc526813a705c9565b475b1d7cc1 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 19 Mar 2024 16:50:20 +1100 Subject: [PATCH 17/21] Address Sean's review comments (#5414) * Address most of Sean's review comments * Simplify total balance cache building --- .../beacon_chain/src/attestation_rewards.rs | 11 ++--- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- beacon_node/operation_pool/src/lib.rs | 12 +++--- consensus/state_processing/src/all_caches.rs | 2 +- consensus/state_processing/src/common/base.rs | 12 ------ consensus/state_processing/src/epoch_cache.rs | 7 +++- consensus/state_processing/src/metrics.rs | 4 -- .../src/per_epoch_processing/altair.rs | 4 +- .../src/per_epoch_processing/base.rs | 4 +- consensus/types/src/beacon_state.rs | 42 ++++++++----------- consensus/types/src/epoch_cache.rs | 5 ++- 11 files changed, 40 insertions(+), 65 deletions(-) diff --git a/beacon_node/beacon_chain/src/attestation_rewards.rs b/beacon_node/beacon_chain/src/attestation_rewards.rs index 1fb90d934cf..299f516fdf8 100644 --- a/beacon_node/beacon_chain/src/attestation_rewards.rs +++ b/beacon_node/beacon_chain/src/attestation_rewards.rs @@ -5,7 +5,7 @@ use eth2::types::ValidatorId; use safe_arith::SafeArith; use serde_utils::quoted_u64::Quoted; use slog::debug; -use state_processing::common::base::get_base_reward_from_effective_balance; +use state_processing::common::base::{self, SqrtTotalActiveBalance}; use state_processing::per_epoch_processing::altair::{ process_inactivity_updates_slow, process_justification_and_finalization, }; @@ -376,15 +376,12 @@ impl BeaconChain { }; let mut ideal_attestation_rewards_list = Vec::new(); - + let sqrt_total_active_balance = SqrtTotalActiveBalance::new(total_balances.current_epoch()); for effective_balance_step in 1..=self.max_effective_balance_increment_steps()? { let effective_balance = effective_balance_step.safe_mul(spec.effective_balance_increment)?; - let base_reward = get_base_reward_from_effective_balance::( - effective_balance, - total_balances.current_epoch(), - spec, - )?; + let base_reward = + base::get_base_reward(effective_balance, sqrt_total_active_balance, spec)?; // compute ideal head rewards let head = get_attestation_component_delta( diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index f3ff3787d80..7535016b46b 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4972,7 +4972,7 @@ impl BeaconChain { metrics::start_timer(&metrics::BLOCK_PRODUCTION_ATTESTATION_TIMES); // Epoch cache and total balance cache are required for op pool packing. - state.build_total_active_balance_cache_at(state.current_epoch(), &self.spec)?; + state.build_total_active_balance_cache(&self.spec)?; initialize_epoch_cache(&mut state, &self.spec)?; let mut prev_filter_cache = HashMap::new(); diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 4651ea92988..9de7b8d77ed 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -256,16 +256,14 @@ impl OperationPool { curr_epoch_validity_filter: impl for<'a> FnMut(&AttestationRef<'a, T>) -> bool + Send, spec: &ChainSpec, ) -> Result>, OpPoolError> { - if let BeaconState::Base(_) = state { - // Ok - } else { - // epoch cache must be initialized to fetch base_reward values in the max_cover score - // function. Currently max_cover ignores items on errors. If epoch cache is not - // initialized, this function returns Ok([]). + if !matches!(state, BeaconState::Base(_)) { + // Epoch cache must be initialized to fetch base reward values in the max cover `score` + // function. Currently max cover ignores items on errors. If epoch cache is not + // initialized, this function returns an error. if !is_epoch_cache_initialized(state).map_err(OpPoolError::EpochCacheError)? { return Err(OpPoolError::EpochCacheNotInitialized); } - }; + } // Attestations for the current fork, which may be from the current or previous epoch. let (prev_epoch_key, curr_epoch_key) = CheckpointKey::keys_for_state(state); diff --git a/consensus/state_processing/src/all_caches.rs b/consensus/state_processing/src/all_caches.rs index 41382813f1c..106692c63aa 100644 --- a/consensus/state_processing/src/all_caches.rs +++ b/consensus/state_processing/src/all_caches.rs @@ -46,7 +46,7 @@ impl AllCaches for BeaconState { && self.slashings_cache_is_initialized() && self .epoch_cache() - .check_validity::(current_epoch, epoch_cache_decision_block_root) + .check_validity(current_epoch, epoch_cache_decision_block_root) .is_ok() } } diff --git a/consensus/state_processing/src/common/base.rs b/consensus/state_processing/src/common/base.rs index cb6ea6c04cd..d582e0bea2d 100644 --- a/consensus/state_processing/src/common/base.rs +++ b/consensus/state_processing/src/common/base.rs @@ -28,15 +28,3 @@ pub fn get_base_reward( .safe_div(sqrt_total_active_balance.as_u64())? .safe_div(spec.base_rewards_per_epoch) } - -pub fn get_base_reward_from_effective_balance( - effective_balance: u64, - total_active_balance: u64, - spec: &ChainSpec, -) -> Result { - effective_balance - .safe_mul(spec.base_reward_factor)? - .safe_div(total_active_balance.integer_sqrt())? - .safe_div(spec.base_rewards_per_epoch) - .map_err(Into::into) -} diff --git a/consensus/state_processing/src/epoch_cache.rs b/consensus/state_processing/src/epoch_cache.rs index 954837d1977..be641d2398f 100644 --- a/consensus/state_processing/src/epoch_cache.rs +++ b/consensus/state_processing/src/epoch_cache.rs @@ -19,6 +19,9 @@ impl PreEpochCache { // 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(); + if decision_block_root.is_zero() { + return Err(EpochCacheError::ZeroDecisionBlock); + } let epoch_key = EpochCacheKey { epoch: state.next_epoch()?, @@ -82,7 +85,7 @@ pub fn is_epoch_cache_initialized( .map_err(EpochCacheError::BeaconState)?; Ok(epoch_cache - .check_validity::(current_epoch, decision_block_root) + .check_validity(current_epoch, decision_block_root) .is_ok()) } @@ -101,7 +104,7 @@ pub fn initialize_epoch_cache( .proposer_shuffling_decision_root(Hash256::zero()) .map_err(EpochCacheError::BeaconState)?; - state.build_total_active_balance_cache_at(current_epoch, spec)?; + state.build_total_active_balance_cache(spec)?; let total_active_balance = state.get_total_active_balance_at_epoch(current_epoch)?; // Collect effective balances and compute activation queue. diff --git a/consensus/state_processing/src/metrics.rs b/consensus/state_processing/src/metrics.rs index 891689921ad..e163f3b76b8 100644 --- a/consensus/state_processing/src/metrics.rs +++ b/consensus/state_processing/src/metrics.rs @@ -17,10 +17,6 @@ lazy_static! { "beacon_participation_prev_epoch_source_attesting_gwei_total", "Total effective balance (gwei) of validators who attested to the source in the previous epoch" ); - pub static ref PARTICIPATION_PREV_EPOCH_ACTIVE_GWEI_TOTAL: Result = try_create_int_gauge( - "beacon_participation_prev_epoch_active_gwei_total", - "Total effective balance (gwei) of validators active in the previous epoch" - ); /* * Processing metrics */ diff --git a/consensus/state_processing/src/per_epoch_processing/altair.rs b/consensus/state_processing/src/per_epoch_processing/altair.rs index be2bba405bf..144483649f1 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair.rs @@ -30,7 +30,7 @@ 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)?; + state.build_total_active_balance_cache(spec)?; initialize_epoch_cache(state, spec)?; initialize_progressive_balances_cache::(state, spec)?; @@ -78,7 +78,7 @@ pub fn process_epoch( process_sync_committee_updates(state, spec)?; // Rotate the epoch caches to suit the epoch transition. - state.advance_caches(spec)?; + state.advance_caches()?; update_progressive_balances_on_epoch_transition(state, spec)?; Ok(EpochProcessingSummary::Altair { diff --git a/consensus/state_processing/src/per_epoch_processing/base.rs b/consensus/state_processing/src/per_epoch_processing/base.rs index 796379ae756..361bb75f416 100644 --- a/consensus/state_processing/src/per_epoch_processing/base.rs +++ b/consensus/state_processing/src/per_epoch_processing/base.rs @@ -24,7 +24,7 @@ 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)?; + state.build_total_active_balance_cache(spec)?; initialize_epoch_cache(state, spec)?; // Load the struct we use to assign validators into sets based on their participation. @@ -70,7 +70,7 @@ pub fn process_epoch( process_participation_record_updates(state)?; // Rotate the epoch caches to suit the epoch transition. - state.advance_caches(spec)?; + state.advance_caches()?; Ok(EpochProcessingSummary::Base { total_balances: validator_statuses.total_balances, diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 7a5337b7b6f..231a8820323 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -1480,19 +1480,13 @@ impl BeaconState { /// /// This method should rarely be invoked because single-pass epoch processing keeps the total /// active balance cache up to date. - pub fn compute_total_active_balance_slow( - &self, - epoch: Epoch, - spec: &ChainSpec, - ) -> Result { - if epoch != self.current_epoch() && epoch != self.next_epoch()? { - return Err(Error::EpochOutOfBounds); - } + pub fn compute_total_active_balance_slow(&self, spec: &ChainSpec) -> Result { + let current_epoch = self.current_epoch(); let mut total_active_balance = 0; for validator in self.validators() { - if validator.is_active_at(epoch) { + if validator.is_active_at(current_epoch) { total_active_balance.safe_add_assign(validator.effective_balance)?; } } @@ -1536,26 +1530,24 @@ impl BeaconState { *self.total_active_balance_mut() = Some((epoch, balance)); } - /// Build the total active balance cache. - 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)?; + /// Build the total active balance cache for the current epoch if it is not already built. + pub fn build_total_active_balance_cache(&mut self, spec: &ChainSpec) -> Result<(), Error> { + if self + .get_total_active_balance_at_epoch(self.current_epoch()) + .is_err() + { + self.force_build_total_active_balance_cache(spec)?; } Ok(()) } /// Build the total active balance cache, even if it is already built. - pub fn force_build_total_active_balance_cache_at( + pub fn force_build_total_active_balance_cache( &mut self, - epoch: Epoch, spec: &ChainSpec, ) -> Result<(), Error> { - let total_active_balance = self.compute_total_active_balance_slow(epoch, spec)?; - *self.total_active_balance_mut() = Some((epoch, total_active_balance)); + let total_active_balance = self.compute_total_active_balance_slow(spec)?; + *self.total_active_balance_mut() = Some((self.current_epoch(), total_active_balance)); Ok(()) } @@ -1685,7 +1677,7 @@ impl BeaconState { } if self.total_active_balance().is_none() && relative_epoch == RelativeEpoch::Current { - self.build_total_active_balance_cache_at(self.current_epoch(), spec)?; + self.build_total_active_balance_cache(spec)?; } Ok(()) } @@ -1719,9 +1711,9 @@ impl BeaconState { /// /// This should be used if the `slot` of this state is advanced beyond an epoch boundary. /// - /// Note: this function will not build any new committee caches, but will build the total - /// balance cache if the (new) current committee cache is initialized. - pub fn advance_caches(&mut self, _spec: &ChainSpec) -> Result<(), Error> { + /// Note: this function will not build any new committee caches, nor will it update the total + /// active balance cache. The total active balance cache must be updated separately. + pub fn advance_caches(&mut self) -> Result<(), Error> { self.committee_caches_mut().rotate_left(1); let next = Self::committee_cache_index(RelativeEpoch::Next); diff --git a/consensus/types/src/epoch_cache.rs b/consensus/types/src/epoch_cache.rs index 1f717f2fcaf..60266fe2fb3 100644 --- a/consensus/types/src/epoch_cache.rs +++ b/consensus/types/src/epoch_cache.rs @@ -1,4 +1,4 @@ -use crate::{ActivationQueue, BeaconStateError, ChainSpec, Epoch, EthSpec, Hash256, Slot}; +use crate::{ActivationQueue, BeaconStateError, ChainSpec, Epoch, Hash256, Slot}; use safe_arith::{ArithError, SafeArith}; use std::sync::Arc; @@ -40,6 +40,7 @@ pub struct EpochCacheKey { pub enum EpochCacheError { IncorrectEpoch { cache: Epoch, state: Epoch }, IncorrectDecisionBlock { cache: Hash256, state: Hash256 }, + ZeroDecisionBlock, ValidatorIndexOutOfBounds { validator_index: usize }, EffectiveBalanceOutOfBounds { effective_balance_eth: usize }, InvalidSlot { slot: Slot }, @@ -79,7 +80,7 @@ impl EpochCache { } } - pub fn check_validity( + pub fn check_validity( &self, current_epoch: Epoch, state_decision_root: Hash256, From b3091cf929da53450ba0c8ea3b7fa4abfc568cea Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 25 Mar 2024 15:13:03 +1100 Subject: [PATCH 18/21] Clean up unused junk --- consensus/types/src/beacon_state.rs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 231a8820323..91e64d496e7 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -147,19 +147,6 @@ 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), @@ -1476,7 +1463,7 @@ impl BeaconState { Ok(cache.get_attestation_duties(validator_index)) } - /// Build the total active balance cache from scratch. + /// Compute the total active balance cache from scratch. /// /// This method should rarely be invoked because single-pass epoch processing keeps the total /// active balance cache up to date. From 1f90350b5fecc57f4862cf8548d400a5e43a41c1 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 26 Mar 2024 10:48:59 +1100 Subject: [PATCH 19/21] More self-review --- consensus/state_processing/src/epoch_cache.rs | 6 +++--- .../per_epoch_processing/effective_balance_updates.rs | 2 +- .../src/per_epoch_processing/registry_updates.rs | 2 +- .../src/per_epoch_processing/single_pass.rs | 2 +- consensus/types/src/activation_queue.rs | 6 ++++++ consensus/types/src/beacon_state.rs | 11 +++++------ consensus/types/src/beacon_state/clone_config.rs | 2 -- consensus/types/src/beacon_state/committee_cache.rs | 2 +- consensus/types/src/beacon_state/tests.rs | 7 +++---- consensus/types/src/chain_spec.rs | 6 ------ consensus/types/src/epoch_cache.rs | 2 +- 11 files changed, 22 insertions(+), 26 deletions(-) diff --git a/consensus/state_processing/src/epoch_cache.rs b/consensus/state_processing/src/epoch_cache.rs index be641d2398f..8f8006e2bfa 100644 --- a/consensus/state_processing/src/epoch_cache.rs +++ b/consensus/state_processing/src/epoch_cache.rs @@ -18,10 +18,10 @@ impl PreEpochCache { // 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(); - if decision_block_root.is_zero() { - return Err(EpochCacheError::ZeroDecisionBlock); + if latest_block_header.state_root.is_zero() { + return Err(EpochCacheError::ZeroStateRoot); } + let decision_block_root = latest_block_header.canonical_root(); let epoch_key = EpochCacheKey { epoch: state.next_epoch()?, 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 d56f8367209..d894e456385 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 @@ -48,7 +48,7 @@ pub fn process_effective_balance_updates( } } - state.set_total_active_balance(next_epoch, new_total_active_balance); + state.set_total_active_balance(next_epoch, new_total_active_balance, spec); Ok(()) } 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 a7eaace29c6..0d2105bec2c 100644 --- a/consensus/state_processing/src/per_epoch_processing/registry_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/registry_updates.rs @@ -43,7 +43,7 @@ pub fn process_registry_updates( // Dequeue validators for activation up to churn limit let churn_limit = state.get_activation_churn_limit(spec)? as usize; - let epoch_cache = state.epoch_cache().clone(); + let epoch_cache = state.epoch_cache(); let activation_queue = epoch_cache .activation_queue()? .get_validators_eligible_for_activation(state.finalized_checkpoint().epoch, churn_limit); diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index b3ae0e5a9a5..9319d2941b5 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -278,7 +278,7 @@ pub fn process_epoch_single_pass( } if conf.effective_balance_updates { - state.set_total_active_balance(next_epoch, next_epoch_total_active_balance); + state.set_total_active_balance(next_epoch, next_epoch_total_active_balance, spec); *state.epoch_cache_mut() = next_epoch_cache.into_epoch_cache( next_epoch_total_active_balance, next_epoch_activation_queue, diff --git a/consensus/types/src/activation_queue.rs b/consensus/types/src/activation_queue.rs index 5ec4ef66895..09ffa5b85e7 100644 --- a/consensus/types/src/activation_queue.rs +++ b/consensus/types/src/activation_queue.rs @@ -5,10 +5,15 @@ use std::collections::BTreeSet; #[derive(Debug, PartialEq, Eq, Default, Clone, arbitrary::Arbitrary)] pub struct ActivationQueue { /// Validators represented by `(activation_eligibility_epoch, index)` in sorted order. + /// + /// These validators are not *necessarily* going to be activated. Their activation depends + /// on how finalization is updated, and the `churn_limit`. queue: BTreeSet<(Epoch, usize)>, } impl ActivationQueue { + /// Check if `validator` could be eligible for activation in the next epoch and add them to + /// the tentative activation queue if this is the case. pub fn add_if_could_be_eligible_for_activation( &mut self, index: usize, @@ -22,6 +27,7 @@ impl ActivationQueue { } } + /// Determine the final activation queue after accounting for finalization & the churn limit. pub fn get_validators_eligible_for_activation( &self, finalized_epoch: Epoch, diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 601eeb077c2..e520a164181 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -373,7 +373,6 @@ where #[ssz(skip_serializing, skip_deserializing)] #[tree_hash(skip_hashing)] #[test_random(default)] - #[derivative(Clone(clone_with = "clone_default"))] pub epoch_cache: EpochCache, #[serde(skip_serializing, skip_deserializing)] #[ssz(skip_serializing, skip_deserializing)] @@ -1513,8 +1512,11 @@ impl BeaconState { /// /// This should only be called when the total active balance has been computed as part of /// single-pass epoch processing (or `process_rewards_and_penalties` for phase0). - pub fn set_total_active_balance(&mut self, epoch: Epoch, balance: u64) { - *self.total_active_balance_mut() = Some((epoch, balance)); + /// + /// This function will ensure the balance is never set to 0, thus conforming to the spec. + pub fn set_total_active_balance(&mut self, epoch: Epoch, balance: u64, spec: &ChainSpec) { + let safe_balance = std::cmp::max(balance, spec.effective_balance_increment); + *self.total_active_balance_mut() = Some((epoch, safe_balance)); } /// Build the total active balance cache for the current epoch if it is not already built. @@ -1884,9 +1886,6 @@ impl BeaconState { if config.slashings_cache { *res.slashings_cache_mut() = self.slashings_cache().clone(); } - if config.epoch_cache { - *res.epoch_cache_mut() = self.epoch_cache().clone(); - } if config.tree_hash_cache { *res.tree_hash_cache_mut() = self.tree_hash_cache().clone(); } diff --git a/consensus/types/src/beacon_state/clone_config.rs b/consensus/types/src/beacon_state/clone_config.rs index a43070a194b..27e066d5db6 100644 --- a/consensus/types/src/beacon_state/clone_config.rs +++ b/consensus/types/src/beacon_state/clone_config.rs @@ -5,7 +5,6 @@ pub struct CloneConfig { pub pubkey_cache: bool, pub exit_cache: bool, pub slashings_cache: bool, - pub epoch_cache: bool, pub tree_hash_cache: bool, pub progressive_balances_cache: bool, } @@ -17,7 +16,6 @@ impl CloneConfig { pubkey_cache: true, exit_cache: true, slashings_cache: true, - epoch_cache: true, tree_hash_cache: true, progressive_balances_cache: true, } diff --git a/consensus/types/src/beacon_state/committee_cache.rs b/consensus/types/src/beacon_state/committee_cache.rs index d8eb3e753e2..7e73fc46475 100644 --- a/consensus/types/src/beacon_state/committee_cache.rs +++ b/consensus/types/src/beacon_state/committee_cache.rs @@ -35,7 +35,7 @@ pub struct CommitteeCache { /// /// 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 +/// cannot be active however and will always be omitted 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`. /// diff --git a/consensus/types/src/beacon_state/tests.rs b/consensus/types/src/beacon_state/tests.rs index d559b77516b..59295d1b785 100644 --- a/consensus/types/src/beacon_state/tests.rs +++ b/consensus/types/src/beacon_state/tests.rs @@ -223,15 +223,14 @@ async fn clone_config() { .update_tree_hash_cache() .expect("should update tree hash cache"); - let num_caches = 7; + let num_caches = 6; let all_configs = (0..2u8.pow(num_caches)).map(|i| CloneConfig { committee_caches: (i & 1) != 0, pubkey_cache: ((i >> 1) & 1) != 0, exit_cache: ((i >> 2) & 1) != 0, slashings_cache: ((i >> 3) & 1) != 0, - epoch_cache: ((i >> 4) & 1) != 0, - tree_hash_cache: ((i >> 5) & 1) != 0, - progressive_balances_cache: ((i >> 6) & 1) != 0, + tree_hash_cache: ((i >> 4) & 1) != 0, + progressive_balances_cache: ((i >> 5) & 1) != 0, }); for config in all_configs { diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 3f2efc7d5d5..ff3ebe58a24 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -502,12 +502,6 @@ impl ChainSpec { 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 index 60266fe2fb3..86ac7b45cca 100644 --- a/consensus/types/src/epoch_cache.rs +++ b/consensus/types/src/epoch_cache.rs @@ -40,7 +40,7 @@ pub struct EpochCacheKey { pub enum EpochCacheError { IncorrectEpoch { cache: Epoch, state: Epoch }, IncorrectDecisionBlock { cache: Hash256, state: Hash256 }, - ZeroDecisionBlock, + ZeroStateRoot, ValidatorIndexOutOfBounds { validator_index: usize }, EffectiveBalanceOutOfBounds { effective_balance_eth: usize }, InvalidSlot { slot: Slot }, From 5cc51f3ae654c3cf4b25d990029b14b02ff2308b Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 4 Apr 2024 15:26:07 +1100 Subject: [PATCH 20/21] Fix imports for beta compiler --- .../state_processing/src/per_block_processing/errors.rs | 2 +- consensus/types/src/beacon_state.rs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index 15c2b800912..28d36dbc518 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -1,5 +1,5 @@ use super::signature_sets::Error as SignatureSetError; -use crate::{ContextError, EpochCacheError}; +use crate::ContextError; use merkle_proof::MerkleTreeError; use safe_arith::ArithError; use ssz::DecodeError; diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 7cb34a4d152..ba11c9c4cce 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -1,4 +1,5 @@ use self::committee_cache::get_active_validator_indices; +use crate::historical_summary::HistoricalSummary; use crate::test_utils::TestRandom; use crate::*; use compare_fields::CompareFields; @@ -11,8 +12,6 @@ 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}; -use std::convert::TryInto; use std::hash::Hash; use std::{fmt, mem, sync::Arc}; use superstruct::superstruct; @@ -29,8 +28,6 @@ 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::*; pub use iter::BlockRootsIter; From 8824c0b71f7e01c604d7db35c9e7309aca4497de Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 4 Apr 2024 19:09:50 +1100 Subject: [PATCH 21/21] Fix tests, probably --- beacon_node/beacon_chain/tests/store_tests.rs | 13 ++++++------- consensus/state_processing/src/epoch_cache.rs | 19 ++++++++++++------- consensus/types/src/epoch_cache.rs | 1 - 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index ff201729821..66f4138afb4 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3535,13 +3535,12 @@ fn assert_chains_pretty_much_the_same(a: &BeaconChain, b a_head.beacon_block, b_head.beacon_block, "head blocks should be equal" ); - // Clone with committee caches only to prevent other caches from messing with the equality - // check. - assert_eq!( - a_head.beacon_state.clone_with_only_committee_caches(), - b_head.beacon_state.clone_with_only_committee_caches(), - "head states should be equal" - ); + // Drop all caches to prevent them messing with the equality check. + let mut a_head_state = a_head.beacon_state.clone(); + a_head_state.drop_all_caches().unwrap(); + let mut b_head_state = b_head.beacon_state.clone(); + b_head_state.drop_all_caches().unwrap(); + assert_eq!(a_head_state, b_head_state, "head states should be equal"); assert_eq!(a.heads(), b.heads(), "heads() should be equal"); assert_eq!( a.genesis_block_root, b.genesis_block_root, diff --git a/consensus/state_processing/src/epoch_cache.rs b/consensus/state_processing/src/epoch_cache.rs index 8f8006e2bfa..b2f2d85407e 100644 --- a/consensus/state_processing/src/epoch_cache.rs +++ b/consensus/state_processing/src/epoch_cache.rs @@ -12,16 +12,21 @@ pub struct PreEpochCache { } impl PreEpochCache { - pub fn new_for_next_epoch(state: &BeaconState) -> Result { + pub fn new_for_next_epoch( + state: &mut 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`. - if latest_block_header.state_root.is_zero() { - return Err(EpochCacheError::ZeroStateRoot); - } - let decision_block_root = latest_block_header.canonical_root(); + let decision_block_root = if !latest_block_header.state_root.is_zero() { + latest_block_header.canonical_root() + } else { + // State root should already have been filled in by `process_slot`, except in the case + // of a `partial_state_advance`. Once we have tree-states this can be an error, and + // `self` can be immutable. + let state_root = state.update_tree_hash_cache()?; + state.get_latest_block_root(state_root) + }; let epoch_key = EpochCacheKey { epoch: state.next_epoch()?, diff --git a/consensus/types/src/epoch_cache.rs b/consensus/types/src/epoch_cache.rs index 86ac7b45cca..b447e9b71e0 100644 --- a/consensus/types/src/epoch_cache.rs +++ b/consensus/types/src/epoch_cache.rs @@ -40,7 +40,6 @@ pub struct EpochCacheKey { pub enum EpochCacheError { IncorrectEpoch { cache: Epoch, state: Epoch }, IncorrectDecisionBlock { cache: Hash256, state: Hash256 }, - ZeroStateRoot, ValidatorIndexOutOfBounds { validator_index: usize }, EffectiveBalanceOutOfBounds { effective_balance_eth: usize }, InvalidSlot { slot: Slot },