diff --git a/beacon-chain/rpc/beacon/validators.go b/beacon-chain/rpc/beacon/validators.go index 480c64a72..ef42f9ac1 100644 --- a/beacon-chain/rpc/beacon/validators.go +++ b/beacon-chain/rpc/beacon/validators.go @@ -540,7 +540,9 @@ func (bs *Server) GetValidatorQueue( minEpoch := exitQueueEpoch + params.BeaconConfig().MinValidatorWithdrawabilityDelay exitQueueIndices := make([]uint64, 0) for _, valIdx := range awaitingExit { - if headState.Validators[valIdx].WithdrawableEpoch < minEpoch { + val := headState.Validators[valIdx] + // Ensure the validator has not yet exited before adding its index to the exit queue. + if val.WithdrawableEpoch < minEpoch && !validatorHasExited(val, helpers.CurrentEpoch(headState)) { exitQueueIndices = append(exitQueueIndices, valIdx) } } @@ -611,3 +613,24 @@ func (bs *Server) GetValidatorPerformance( TotalActiveValidators: activeCount, }, nil } + +// Determines whether a validator has already exited. +func validatorHasExited(validator *ethpb.Validator, currentEpoch uint64) bool { + farFutureEpoch := params.BeaconConfig().FarFutureEpoch + if currentEpoch < validator.ActivationEligibilityEpoch { + return false + } + if currentEpoch < validator.ActivationEpoch { + return false + } + if validator.ExitEpoch == farFutureEpoch { + return false + } + if currentEpoch < validator.ExitEpoch { + if validator.Slashed { + return false + } + return false + } + return true +} diff --git a/beacon-chain/rpc/beacon/validators_test.go b/beacon-chain/rpc/beacon/validators_test.go index 0afd2d1ad..a9bb0ac95 100644 --- a/beacon-chain/rpc/beacon/validators_test.go +++ b/beacon-chain/rpc/beacon/validators_test.go @@ -1222,6 +1222,67 @@ func TestServer_GetValidatorQueue_PendingActivation(t *testing.T) { } } +func TestServer_GetValidatorQueue_ExitedValidatorLeavesQueue(t *testing.T) { + headState := &pbp2p.BeaconState{ + Validators: []*ethpb.Validator{ + { + ActivationEpoch: 0, + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + WithdrawableEpoch: params.BeaconConfig().FarFutureEpoch, + PublicKey: []byte("1"), + }, + { + ActivationEpoch: 0, + ExitEpoch: 4, + WithdrawableEpoch: 6, + PublicKey: []byte("2"), + }, + }, + FinalizedCheckpoint: ðpb.Checkpoint{ + Epoch: 0, + }, + } + bs := &Server{ + HeadFetcher: &mock.ChainService{ + State: headState, + }, + } + + // First we check if validator with index 1 is in the exit queue. + res, err := bs.GetValidatorQueue(context.Background(), &ptypes.Empty{}) + if err != nil { + t.Fatal(err) + } + wanted := [][]byte{ + []byte("2"), + } + activeValidatorCount, err := helpers.ActiveValidatorCount(headState, helpers.CurrentEpoch(headState)) + if err != nil { + t.Fatal(err) + } + wantChurn, err := helpers.ValidatorChurnLimit(activeValidatorCount) + if err != nil { + t.Fatal(err) + } + if res.ChurnLimit != wantChurn { + t.Errorf("Wanted churn %d, received %d", wantChurn, res.ChurnLimit) + } + if !reflect.DeepEqual(res.ExitPublicKeys, wanted) { + t.Errorf("Wanted %v, received %v", wanted, res.ExitPublicKeys) + } + + // Now, we move the state.slot past the exit epoch of the validator, and now + // the validator should no longer exist in the queue. + headState.Slot = helpers.StartSlot(headState.Validators[1].ExitEpoch + 1) + res, err = bs.GetValidatorQueue(context.Background(), &ptypes.Empty{}) + if err != nil { + t.Fatal(err) + } + if len(res.ExitPublicKeys) != 0 { + t.Errorf("Wanted empty exit queue, received %v", res.ExitPublicKeys) + } +} + func TestServer_GetValidatorQueue_PendingExit(t *testing.T) { headState := &pbp2p.BeaconState{ Validators: []*ethpb.Validator{