diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index b9d9d76186..9f1f244ef9 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -98,7 +98,10 @@ pub async fn handle_rpc( .unwrap()) } } - ENGINE_NEW_PAYLOAD_V1 | ENGINE_NEW_PAYLOAD_V2 | ENGINE_NEW_PAYLOAD_V3 => { + ENGINE_NEW_PAYLOAD_V1 + | ENGINE_NEW_PAYLOAD_V2 + | ENGINE_NEW_PAYLOAD_V3 + | ENGINE_NEW_PAYLOAD_V4 => { let request = match method { ENGINE_NEW_PAYLOAD_V1 => JsonExecutionPayload::V1( get_param::>(params, 0) diff --git a/consensus/state_processing/src/common/initiate_validator_exit.rs b/consensus/state_processing/src/common/initiate_validator_exit.rs index 8874e9ed4b..49e3a7390d 100644 --- a/consensus/state_processing/src/common/initiate_validator_exit.rs +++ b/consensus/state_processing/src/common/initiate_validator_exit.rs @@ -8,12 +8,12 @@ pub fn initiate_validator_exit( index: usize, spec: &ChainSpec, ) -> Result<(), Error> { - // 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. Compared to the spec - // ordering, this saves us from looking up the validator in the validator registry multiple - // times. + let validator = state.get_validator_cow(index)?; + + // Return if the validator already initiated exit + if validator.exit_epoch != spec.far_future_epoch { + return Ok(()); + } // Ensure the exit cache is built. state.build_exit_cache(spec)?; @@ -36,14 +36,7 @@ pub fn initiate_validator_exit( exit_queue_epoch }; - let validator = state.get_validator_cow(index)?; - - // Return if the validator already initiated exit - if validator.exit_epoch != spec.far_future_epoch { - return Ok(()); - } - - let validator = validator.into_mut()?; + let validator = state.get_validator_mut(index)?; validator.exit_epoch = exit_queue_epoch; validator.withdrawable_epoch = exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?; diff --git a/consensus/state_processing/src/epoch_cache.rs b/consensus/state_processing/src/epoch_cache.rs index 3741f49c57..0e940fabe4 100644 --- a/consensus/state_processing/src/epoch_cache.rs +++ b/consensus/state_processing/src/epoch_cache.rs @@ -86,9 +86,10 @@ impl PreEpochCache { 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 max_effective_balance = + spec.max_effective_balance_for_fork(spec.fork_name_at_epoch(epoch)); + let max_effective_balance_eth = + max_effective_balance.safe_div(effective_balance_increment)?; let mut base_rewards = Vec::with_capacity(max_effective_balance_eth.safe_add(1)? as usize); diff --git a/consensus/state_processing/src/per_epoch_processing/errors.rs b/consensus/state_processing/src/per_epoch_processing/errors.rs index 7f60f4a6e0..b6c9dbea52 100644 --- a/consensus/state_processing/src/per_epoch_processing/errors.rs +++ b/consensus/state_processing/src/per_epoch_processing/errors.rs @@ -26,6 +26,8 @@ pub enum EpochProcessingError { MilhouseError(milhouse::Error), EpochCache(EpochCacheError), SinglePassMissingActivationQueue, + MissingEarliestExitEpoch, + MissingExitBalanceToConsume, } impl From for EpochProcessingError { 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 3d72f0cdd8..f5d3f974a6 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -167,6 +167,9 @@ pub fn process_epoch_single_pass( None }; + let mut earliest_exit_epoch = state.earliest_exit_epoch().ok(); + let mut exit_balance_to_consume = state.exit_balance_to_consume().ok(); + // Split the state into several disjoint mutable borrows. let ( validators, @@ -286,6 +289,8 @@ pub fn process_epoch_single_pass( exit_cache, activation_queue_refs, state_ctxt, + earliest_exit_epoch.as_mut(), + exit_balance_to_consume.as_mut(), spec, )?; } @@ -320,6 +325,17 @@ pub fn process_epoch_single_pass( } } + if conf.registry_updates && fork_name >= ForkName::Electra { + if let Ok(earliest_exit_epoch_state) = state.earliest_exit_epoch_mut() { + *earliest_exit_epoch_state = + earliest_exit_epoch.ok_or(Error::MissingEarliestExitEpoch)?; + } + if let Ok(exit_balance_to_consume_state) = state.exit_balance_to_consume_mut() { + *exit_balance_to_consume_state = + exit_balance_to_consume.ok_or(Error::MissingExitBalanceToConsume)?; + } + } + // Finish processing pending balance deposits if relevant. // // This *could* be reordered after `process_pending_consolidations` which pushes only to the end @@ -527,12 +543,15 @@ impl RewardsAndPenaltiesContext { } } +#[allow(clippy::too_many_arguments)] fn process_single_registry_update( validator: &mut Cow, validator_info: &ValidatorInfo, exit_cache: &mut ExitCache, activation_queues: Option<(&BTreeSet, &mut ActivationQueue)>, state_ctxt: &StateContext, + earliest_exit_epoch: Option<&mut Epoch>, + exit_balance_to_consume: Option<&mut u64>, spec: &ChainSpec, ) -> Result<(), Error> { if state_ctxt.fork_name < ForkName::Electra { @@ -548,7 +567,14 @@ fn process_single_registry_update( spec, ) } else { - process_single_registry_update_post_electra(validator, exit_cache, state_ctxt, spec) + process_single_registry_update_post_electra( + validator, + exit_cache, + state_ctxt, + earliest_exit_epoch.ok_or(Error::MissingEarliestExitEpoch)?, + exit_balance_to_consume.ok_or(Error::MissingExitBalanceToConsume)?, + spec, + ) } } @@ -569,7 +595,7 @@ fn process_single_registry_update_pre_electra( if validator.is_active_at(current_epoch) && validator.effective_balance <= spec.ejection_balance { - initiate_validator_exit(validator, exit_cache, state_ctxt, spec)?; + initiate_validator_exit(validator, exit_cache, state_ctxt, None, None, spec)?; } if activation_queue.contains(&validator_info.index) { @@ -592,6 +618,8 @@ fn process_single_registry_update_post_electra( validator: &mut Cow, exit_cache: &mut ExitCache, state_ctxt: &StateContext, + earliest_exit_epoch: &mut Epoch, + exit_balance_to_consume: &mut u64, spec: &ChainSpec, ) -> Result<(), Error> { let current_epoch = state_ctxt.current_epoch; @@ -602,8 +630,14 @@ fn process_single_registry_update_post_electra( if validator.is_active_at(current_epoch) && validator.effective_balance <= spec.ejection_balance { - // TODO(electra): make sure initiate_validator_exit is updated - initiate_validator_exit(validator, exit_cache, state_ctxt, spec)?; + initiate_validator_exit( + validator, + exit_cache, + state_ctxt, + Some(earliest_exit_epoch), + Some(exit_balance_to_consume), + spec, + )?; } if validator.is_eligible_for_activation_with_finalized_checkpoint( @@ -621,6 +655,8 @@ fn initiate_validator_exit( validator: &mut Cow, exit_cache: &mut ExitCache, state_ctxt: &StateContext, + earliest_exit_epoch: Option<&mut Epoch>, + exit_balance_to_consume: Option<&mut u64>, spec: &ChainSpec, ) -> Result<(), Error> { // Return if the validator already initiated exit @@ -628,16 +664,27 @@ fn initiate_validator_exit( 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)?; + let exit_queue_epoch = if state_ctxt.fork_name >= ForkName::Electra { + compute_exit_epoch_and_update_churn( + validator, + state_ctxt, + earliest_exit_epoch.ok_or(Error::MissingEarliestExitEpoch)?, + exit_balance_to_consume.ok_or(Error::MissingExitBalanceToConsume)?, + spec, + )? + } else { + // 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)?; - } + if exit_queue_churn >= state_ctxt.churn_limit { + exit_queue_epoch.safe_add_assign(1)?; + } + exit_queue_epoch + }; let validator = validator.make_mut()?; validator.exit_epoch = exit_queue_epoch; @@ -648,6 +695,64 @@ fn initiate_validator_exit( Ok(()) } +fn compute_exit_epoch_and_update_churn( + validator: &mut Cow, + state_ctxt: &StateContext, + earliest_exit_epoch_state: &mut Epoch, + exit_balance_to_consume_state: &mut u64, + spec: &ChainSpec, +) -> Result { + let exit_balance = validator.effective_balance; + let mut earliest_exit_epoch = std::cmp::max( + *earliest_exit_epoch_state, + spec.compute_activation_exit_epoch(state_ctxt.current_epoch)?, + ); + + let per_epoch_churn = get_activation_exit_churn_limit(state_ctxt, spec)?; + // New epoch for exits + let mut exit_balance_to_consume = if *earliest_exit_epoch_state < earliest_exit_epoch { + per_epoch_churn + } else { + *exit_balance_to_consume_state + }; + + // Exit doesn't fit in the current earliest epoch + if exit_balance > exit_balance_to_consume { + let balance_to_process = exit_balance.safe_sub(exit_balance_to_consume)?; + let additional_epochs = balance_to_process + .safe_sub(1)? + .safe_div(per_epoch_churn)? + .safe_add(1)?; + earliest_exit_epoch.safe_add_assign(additional_epochs)?; + exit_balance_to_consume.safe_add_assign(additional_epochs.safe_mul(per_epoch_churn)?)?; + } + // Consume the balance and update state variables + *exit_balance_to_consume_state = exit_balance_to_consume.safe_sub(exit_balance)?; + *earliest_exit_epoch_state = earliest_exit_epoch; + + Ok(earliest_exit_epoch) +} + +fn get_activation_exit_churn_limit( + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result { + Ok(std::cmp::min( + spec.max_per_epoch_activation_exit_churn_limit, + get_balance_churn_limit(state_ctxt, spec)?, + )) +} + +fn get_balance_churn_limit(state_ctxt: &StateContext, spec: &ChainSpec) -> Result { + let total_active_balance = state_ctxt.total_active_balance; + let churn = std::cmp::max( + spec.min_per_epoch_churn_limit_electra, + total_active_balance.safe_div(spec.churn_limit_quotient)?, + ); + + Ok(churn.safe_sub(churn.safe_rem(spec.effective_balance_increment)?)?) +} + impl SlashingsContext { fn new( state: &BeaconState, diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 8ffafe6266..df25713f02 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -400,6 +400,14 @@ impl ChainSpec { } } + pub fn max_effective_balance_for_fork(&self, fork_name: ForkName) -> u64 { + if fork_name >= ForkName::Electra { + self.max_effective_balance_electra + } else { + self.max_effective_balance + } + } + /// Returns a full `Fork` struct for a given epoch. pub fn fork_at_epoch(&self, epoch: Epoch) -> Fork { let current_fork_name = self.fork_name_at_epoch(epoch);