diff --git a/beacon-chain/core/validators/BUILD.bazel b/beacon-chain/core/validators/BUILD.bazel index 11668f0cf999..cc009285ffb7 100644 --- a/beacon-chain/core/validators/BUILD.bazel +++ b/beacon-chain/core/validators/BUILD.bazel @@ -15,7 +15,9 @@ go_library( "//beacon-chain/state:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", + "//math:go_default_library", "//proto/prysm/v1alpha1:go_default_library", + "//runtime/version:go_default_library", "//time/slots:go_default_library", "@com_github_pkg_errors//:go_default_library", ], @@ -32,9 +34,11 @@ go_test( "//beacon-chain/state/state-native:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", + "//math:go_default_library", "//proto/prysm/v1alpha1:go_default_library", "//runtime/version:go_default_library", "//testing/assert:go_default_library", "//testing/require:go_default_library", + "//time/slots:go_default_library", ], ) diff --git a/beacon-chain/core/validators/validator.go b/beacon-chain/core/validators/validator.go index e35a684236a0..ed015a77b052 100644 --- a/beacon-chain/core/validators/validator.go +++ b/beacon-chain/core/validators/validator.go @@ -13,7 +13,9 @@ import ( "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/math" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/runtime/version" "github.com/prysmaticlabs/prysm/v5/time/slots" ) @@ -43,34 +45,26 @@ func MaxExitEpochAndChurn(s state.BeaconState) (maxExitEpoch primitives.Epoch, c // InitiateValidatorExit takes in validator index and updates // validator with correct voluntary exit parameters. +// Note: As of Electra, the exitQueueEpoch and churn parameters are unused. // // Spec pseudocode definition: // // def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None: -// """ -// Initiate the exit of the validator with index ``index``. -// """ -// # Return if validator already initiated exit -// validator = state.validators[index] -// if validator.exit_epoch != FAR_FUTURE_EPOCH: -// return +// """ +// Initiate the exit of the validator with index ``index``. +// """ +// # Return if validator already initiated exit +// validator = state.validators[index] +// if validator.exit_epoch != FAR_FUTURE_EPOCH: +// return // -// # Compute exit queue epoch -// exit_epochs = [v.exit_epoch for v in state.validators if v.exit_epoch != FAR_FUTURE_EPOCH] -// exit_queue_epoch = max(exit_epochs + [compute_activation_exit_epoch(get_current_epoch(state))]) -// exit_queue_churn = len([v for v in state.validators if v.exit_epoch == exit_queue_epoch]) -// if exit_queue_churn >= get_validator_churn_limit(state): -// exit_queue_epoch += Epoch(1) +// # Compute exit queue epoch [Modified in Electra:EIP7251] +// exit_queue_epoch = compute_exit_epoch_and_update_churn(state, validator.effective_balance) // -// # Set validator exit epoch and withdrawable epoch -// validator.exit_epoch = exit_queue_epoch -// validator.withdrawable_epoch = Epoch(validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) +// # Set validator exit epoch and withdrawable epoch +// validator.exit_epoch = exit_queue_epoch +// validator.withdrawable_epoch = Epoch(validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) func InitiateValidatorExit(ctx context.Context, s state.BeaconState, idx primitives.ValidatorIndex, exitQueueEpoch primitives.Epoch, churn uint64) (state.BeaconState, primitives.Epoch, error) { - exitableEpoch := helpers.ActivationExitEpoch(time.CurrentEpoch(s)) - if exitableEpoch > exitQueueEpoch { - exitQueueEpoch = exitableEpoch - churn = 0 - } validator, err := s.ValidatorAtIndex(idx) if err != nil { return nil, 0, err @@ -78,14 +72,38 @@ func InitiateValidatorExit(ctx context.Context, s state.BeaconState, idx primiti if validator.ExitEpoch != params.BeaconConfig().FarFutureEpoch { return s, validator.ExitEpoch, ErrValidatorAlreadyExited } - activeValidatorCount, err := helpers.ActiveValidatorCount(ctx, s, time.CurrentEpoch(s)) - if err != nil { - return nil, 0, errors.Wrap(err, "could not get active validator count") - } - currentChurn := helpers.ValidatorExitChurnLimit(activeValidatorCount) - if churn >= currentChurn { - exitQueueEpoch, err = exitQueueEpoch.SafeAdd(1) + // Compute exit queue epoch. + if s.Version() < version.Electra { + // Relevant spec code from deneb: + // + // exit_epochs = [v.exit_epoch for v in state.validators if v.exit_epoch != FAR_FUTURE_EPOCH] + // exit_queue_epoch = max(exit_epochs + [compute_activation_exit_epoch(get_current_epoch(state))]) + // exit_queue_churn = len([v for v in state.validators if v.exit_epoch == exit_queue_epoch]) + // if exit_queue_churn >= get_validator_churn_limit(state): + // exit_queue_epoch += Epoch(1) + exitableEpoch := helpers.ActivationExitEpoch(time.CurrentEpoch(s)) + if exitableEpoch > exitQueueEpoch { + exitQueueEpoch = exitableEpoch + churn = 0 + } + activeValidatorCount, err := helpers.ActiveValidatorCount(ctx, s, time.CurrentEpoch(s)) + if err != nil { + return nil, 0, errors.Wrap(err, "could not get active validator count") + } + currentChurn := helpers.ValidatorExitChurnLimit(activeValidatorCount) + + if churn >= currentChurn { + exitQueueEpoch, err = exitQueueEpoch.SafeAdd(1) + if err != nil { + return nil, 0, err + } + } + } else { + // [Modified in Electra:EIP7251] + // exit_queue_epoch = compute_exit_epoch_and_update_churn(state, validator.effective_balance) + var err error + exitQueueEpoch, err = s.ExitEpochAndUpdateChurn(math.Gwei(validator.EffectiveBalance)) if err != nil { return nil, 0, err } diff --git a/beacon-chain/core/validators/validator_test.go b/beacon-chain/core/validators/validator_test.go index 9eb62f5bdec0..09ad957dfd26 100644 --- a/beacon-chain/core/validators/validator_test.go +++ b/beacon-chain/core/validators/validator_test.go @@ -9,10 +9,12 @@ import ( state_native "github.com/prysmaticlabs/prysm/v5/beacon-chain/state/state-native" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/math" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/runtime/version" "github.com/prysmaticlabs/prysm/v5/testing/assert" "github.com/prysmaticlabs/prysm/v5/testing/require" + "github.com/prysmaticlabs/prysm/v5/time/slots" ) func TestHasVoted_OK(t *testing.T) { @@ -113,6 +115,54 @@ func TestInitiateValidatorExit_WithdrawalOverflows(t *testing.T) { require.ErrorContains(t, "addition overflows", err) } +func TestInitiateValidatorExit_ProperExit_Electra(t *testing.T) { + exitedEpoch := primitives.Epoch(100) + idx := primitives.ValidatorIndex(3) + base := ðpb.BeaconStateElectra{ + Slot: slots.UnsafeEpochStart(exitedEpoch + 1), + Validators: []*ethpb.Validator{ + { + ExitEpoch: exitedEpoch, + EffectiveBalance: params.BeaconConfig().MinActivationBalance, + }, + { + ExitEpoch: exitedEpoch + 1, + EffectiveBalance: params.BeaconConfig().MinActivationBalance, + }, + { + ExitEpoch: exitedEpoch + 2, + EffectiveBalance: params.BeaconConfig().MinActivationBalance, + }, + { + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + EffectiveBalance: params.BeaconConfig().MinActivationBalance, + }, + }, + } + state, err := state_native.InitializeFromProtoElectra(base) + require.NoError(t, err) + + // Pre-check: Exit balance to consume should be zero. + ebtc, err := state.ExitBalanceToConsume() + require.NoError(t, err) + require.Equal(t, math.Gwei(0), ebtc) + + newState, epoch, err := InitiateValidatorExit(context.Background(), state, idx, 0, 0) // exitQueueEpoch and churn are not used in electra + require.NoError(t, err) + + // Expect that the exit epoch is the next available epoch with max seed lookahead. + want := helpers.ActivationExitEpoch(exitedEpoch + 1) + require.Equal(t, want, epoch) + v, err := newState.ValidatorAtIndex(idx) + require.NoError(t, err) + assert.Equal(t, want, v.ExitEpoch, "Exit epoch was not the highest") + + // Check that the exit balance to consume has been updated on the state. + ebtc, err = state.ExitBalanceToConsume() + require.NoError(t, err) + require.NotEqual(t, math.Gwei(0), ebtc, "Exit balance to consume was not updated") +} + func TestSlashValidator_OK(t *testing.T) { validatorCount := 100 registry := make([]*ethpb.Validator, 0, validatorCount)