mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 15:37:56 -05:00
Add support for validator count endpoint (#12752)
This commit is contained in:
@@ -2,6 +2,7 @@ package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/lookup"
|
||||
"github.com/prysmaticlabs/prysm/v4/beacon-chain/state/stategen"
|
||||
@@ -34,3 +35,15 @@ type SingleIndexedVerificationFailure struct {
|
||||
Index int `json:"index"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// PrepareStateFetchError returns an appropriate error based on the supplied argument.
|
||||
// The argument error should be a result of fetching state.
|
||||
func PrepareStateFetchError(err error) error {
|
||||
if errors.Is(err, stategen.ErrNoDataForSlot) {
|
||||
return errors.New("lacking historical data needed to fulfill request")
|
||||
}
|
||||
if stateNotFoundErr, ok := err.(*lookup.StateNotFoundError); ok {
|
||||
return fmt.Errorf("state not found: %v", stateNotFoundErr)
|
||||
}
|
||||
return fmt.Errorf("could not fetch state: %v", err)
|
||||
}
|
||||
|
||||
@@ -4,38 +4,55 @@ go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"server.go",
|
||||
"validator_count.go",
|
||||
"validator_performance.go",
|
||||
],
|
||||
importpath = "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/prysm/validator",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//beacon-chain/blockchain:go_default_library",
|
||||
"//beacon-chain/db:go_default_library",
|
||||
"//beacon-chain/rpc/core:go_default_library",
|
||||
"//beacon-chain/rpc/eth/helpers:go_default_library",
|
||||
"//beacon-chain/rpc/lookup:go_default_library",
|
||||
"//beacon-chain/state/state-native:go_default_library",
|
||||
"//beacon-chain/sync:go_default_library",
|
||||
"//consensus-types/primitives:go_default_library",
|
||||
"//consensus-types/validator:go_default_library",
|
||||
"//network/http:go_default_library",
|
||||
"//proto/eth/v1:go_default_library",
|
||||
"//proto/prysm/v1alpha1:go_default_library",
|
||||
"//time/slots:go_default_library",
|
||||
"@com_github_gorilla_mux//:go_default_library",
|
||||
"@io_opencensus_go//trace:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["validator_performance_test.go"],
|
||||
srcs = [
|
||||
"validator_count_test.go",
|
||||
"validator_performance_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//beacon-chain/blockchain/testing:go_default_library",
|
||||
"//beacon-chain/core/epoch/precompute:go_default_library",
|
||||
"//beacon-chain/core/helpers:go_default_library",
|
||||
"//beacon-chain/rpc/core:go_default_library",
|
||||
"//beacon-chain/rpc/lookup:go_default_library",
|
||||
"//beacon-chain/rpc/testutil:go_default_library",
|
||||
"//beacon-chain/state:go_default_library",
|
||||
"//beacon-chain/sync/initial-sync/testing:go_default_library",
|
||||
"//config/params:go_default_library",
|
||||
"//consensus-types/primitives:go_default_library",
|
||||
"//encoding/bytesutil:go_default_library",
|
||||
"//network/http:go_default_library",
|
||||
"//proto/prysm/v1alpha1:go_default_library",
|
||||
"//runtime/version:go_default_library",
|
||||
"//testing/require:go_default_library",
|
||||
"//testing/util:go_default_library",
|
||||
"@com_github_gorilla_mux//:go_default_library",
|
||||
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -2,15 +2,22 @@ package validator
|
||||
|
||||
import (
|
||||
"github.com/prysmaticlabs/prysm/v4/beacon-chain/blockchain"
|
||||
"github.com/prysmaticlabs/prysm/v4/beacon-chain/db"
|
||||
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/core"
|
||||
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/lookup"
|
||||
"github.com/prysmaticlabs/prysm/v4/beacon-chain/sync"
|
||||
)
|
||||
|
||||
// Server defines a server implementation for HTTP endpoints, providing
|
||||
// access data relevant to the Ethereum Beacon Chain.
|
||||
type Server struct {
|
||||
GenesisTimeFetcher blockchain.TimeFetcher
|
||||
SyncChecker sync.Checker
|
||||
HeadFetcher blockchain.HeadFetcher
|
||||
CoreService *core.Service
|
||||
GenesisTimeFetcher blockchain.TimeFetcher
|
||||
SyncChecker sync.Checker
|
||||
HeadFetcher blockchain.HeadFetcher
|
||||
CoreService *core.Service
|
||||
OptimisticModeFetcher blockchain.OptimisticModeFetcher
|
||||
Stater lookup.Stater
|
||||
ChainInfoFetcher blockchain.ChainInfoFetcher
|
||||
BeaconDB db.ReadOnlyDatabase
|
||||
FinalizationFetcher blockchain.FinalizationFetcher
|
||||
}
|
||||
|
||||
189
beacon-chain/rpc/prysm/validator/validator_count.go
Normal file
189
beacon-chain/rpc/prysm/validator/validator_count.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/lookup"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/helpers"
|
||||
statenative "github.com/prysmaticlabs/prysm/v4/beacon-chain/state/state-native"
|
||||
"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
|
||||
"github.com/prysmaticlabs/prysm/v4/consensus-types/validator"
|
||||
http2 "github.com/prysmaticlabs/prysm/v4/network/http"
|
||||
ethpb "github.com/prysmaticlabs/prysm/v4/proto/eth/v1"
|
||||
eth "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
|
||||
"github.com/prysmaticlabs/prysm/v4/time/slots"
|
||||
"go.opencensus.io/trace"
|
||||
)
|
||||
|
||||
type ValidatorCountResponse struct {
|
||||
ExecutionOptimistic string `json:"execution_optimistic"`
|
||||
Finalized string `json:"finalized"`
|
||||
Data []*ValidatorCount `json:"data"`
|
||||
}
|
||||
|
||||
type ValidatorCount struct {
|
||||
Status string `json:"status"`
|
||||
Count string `json:"count"`
|
||||
}
|
||||
|
||||
// GetValidatorCount is a HTTP handler that serves the GET /eth/v1/beacon/states/{state_id}/validator_count endpoint.
|
||||
// It returns the total validator count according to the given statuses provided as a query parameter.
|
||||
//
|
||||
// The state ID is expected to be a valid Beacon Chain state identifier.
|
||||
// The status query parameter can be an array of strings with the following values: pending_initialized, pending_queued, active_ongoing,
|
||||
// active_exiting, active_slashed, exited_unslashed, exited_slashed, withdrawal_possible, withdrawal_done, active, pending, exited, withdrawal.
|
||||
// The response is a JSON object containing the total validator count for the specified statuses.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// GET /eth/v1/beacon/states/12345/validator_count?status=active&status=pending
|
||||
//
|
||||
// The above request will return a JSON response like:
|
||||
//
|
||||
// {
|
||||
// "execution_optimistic": "false",
|
||||
// "finalized": "true",
|
||||
// "data": [
|
||||
// {
|
||||
// "status": "active",
|
||||
// "count": "13"
|
||||
// },
|
||||
// {
|
||||
// "status": "pending",
|
||||
// "count": "6"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
func (vs *Server) GetValidatorCount(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := trace.StartSpan(r.Context(), "beacon.GetValidatorCount")
|
||||
defer span.End()
|
||||
|
||||
stateID := mux.Vars(r)["state_id"]
|
||||
|
||||
isOptimistic, err := helpers.IsOptimistic(ctx, []byte(stateID), vs.OptimisticModeFetcher, vs.Stater, vs.ChainInfoFetcher, vs.BeaconDB)
|
||||
if err != nil {
|
||||
errJson := &http2.DefaultErrorJson{
|
||||
Message: fmt.Sprintf("could not check if slot's block is optimistic: %v", err),
|
||||
Code: http.StatusInternalServerError,
|
||||
}
|
||||
http2.WriteError(w, errJson)
|
||||
return
|
||||
}
|
||||
|
||||
st, err := vs.Stater.State(ctx, []byte(stateID))
|
||||
if err != nil {
|
||||
var errJson *http2.DefaultErrorJson
|
||||
if _, ok := err.(*lookup.StateIdParseError); ok {
|
||||
errJson = &http2.DefaultErrorJson{
|
||||
Message: "invalid state ID",
|
||||
Code: http.StatusBadRequest,
|
||||
}
|
||||
} else {
|
||||
errJson = &http2.DefaultErrorJson{
|
||||
Message: helpers.PrepareStateFetchError(err).Error(),
|
||||
Code: http.StatusInternalServerError,
|
||||
}
|
||||
}
|
||||
http2.WriteError(w, errJson)
|
||||
return
|
||||
}
|
||||
|
||||
blockRoot, err := st.LatestBlockHeader().HashTreeRoot()
|
||||
if err != nil {
|
||||
errJson := &http2.DefaultErrorJson{
|
||||
Message: fmt.Sprintf("could not calculate root of latest block header: %v", err),
|
||||
Code: http.StatusInternalServerError,
|
||||
}
|
||||
http2.WriteError(w, errJson)
|
||||
return
|
||||
}
|
||||
|
||||
isFinalized := vs.FinalizationFetcher.IsFinalized(ctx, blockRoot)
|
||||
|
||||
var statusVals []validator.ValidatorStatus
|
||||
for _, status := range r.URL.Query()["status"] {
|
||||
statusVal, ok := ethpb.ValidatorStatus_value[strings.ToUpper(status)]
|
||||
if !ok {
|
||||
errJson := &http2.DefaultErrorJson{
|
||||
Message: fmt.Sprintf("invalid status query parameter: %v", status),
|
||||
Code: http.StatusBadRequest,
|
||||
}
|
||||
http2.WriteError(w, errJson)
|
||||
return
|
||||
}
|
||||
|
||||
statusVals = append(statusVals, validator.ValidatorStatus(statusVal))
|
||||
}
|
||||
|
||||
// If no status was provided then consider all the statuses to return validator count for each status.
|
||||
if len(statusVals) == 0 {
|
||||
for _, val := range ethpb.ValidatorStatus_value {
|
||||
statusVals = append(statusVals, validator.ValidatorStatus(val))
|
||||
}
|
||||
}
|
||||
|
||||
epoch := slots.ToEpoch(st.Slot())
|
||||
valCount, err := validatorCountByStatus(st.Validators(), statusVals, epoch)
|
||||
if err != nil {
|
||||
errJson := &http2.DefaultErrorJson{
|
||||
Message: fmt.Sprintf("could not get validator count: %v", err),
|
||||
Code: http.StatusInternalServerError,
|
||||
}
|
||||
http2.WriteError(w, errJson)
|
||||
return
|
||||
}
|
||||
|
||||
valCountResponse := &ValidatorCountResponse{
|
||||
ExecutionOptimistic: strconv.FormatBool(isOptimistic),
|
||||
Finalized: strconv.FormatBool(isFinalized),
|
||||
Data: valCount,
|
||||
}
|
||||
|
||||
http2.WriteJson(w, valCountResponse)
|
||||
}
|
||||
|
||||
// validatorCountByStatus returns a slice of validator count for each status in the given epoch.
|
||||
func validatorCountByStatus(validators []*eth.Validator, statuses []validator.ValidatorStatus, epoch primitives.Epoch) ([]*ValidatorCount, error) {
|
||||
countByStatus := make(map[validator.ValidatorStatus]uint64)
|
||||
for _, val := range validators {
|
||||
readOnlyVal, err := statenative.NewValidator(val)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not convert validator: %v", err)
|
||||
}
|
||||
valStatus, err := helpers.ValidatorStatus(readOnlyVal, epoch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get validator status: %v", err)
|
||||
}
|
||||
valSubStatus, err := helpers.ValidatorSubStatus(readOnlyVal, epoch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get validator sub status: %v", err)
|
||||
}
|
||||
|
||||
for _, status := range statuses {
|
||||
if valStatus == status || valSubStatus == status {
|
||||
countByStatus[status]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var resp []*ValidatorCount
|
||||
for status, count := range countByStatus {
|
||||
resp = append(resp, &ValidatorCount{
|
||||
Status: strings.ToLower(ethpb.ValidatorStatus_name[int32(status)]),
|
||||
Count: strconv.FormatUint(count, 10),
|
||||
})
|
||||
}
|
||||
|
||||
// Sort the response slice according to status strings for deterministic ordering of validator count response.
|
||||
sort.Slice(resp, func(i, j int) bool {
|
||||
return resp[i].Status < resp[j].Status
|
||||
})
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
491
beacon-chain/rpc/prysm/validator/validator_count_test.go
Normal file
491
beacon-chain/rpc/prysm/validator/validator_count_test.go
Normal file
@@ -0,0 +1,491 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
neturl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/prysmaticlabs/prysm/v4/beacon-chain/state"
|
||||
|
||||
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/lookup"
|
||||
http2 "github.com/prysmaticlabs/prysm/v4/network/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
chainMock "github.com/prysmaticlabs/prysm/v4/beacon-chain/blockchain/testing"
|
||||
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/testutil"
|
||||
"github.com/prysmaticlabs/prysm/v4/config/params"
|
||||
"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
|
||||
eth "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
|
||||
"github.com/prysmaticlabs/prysm/v4/testing/require"
|
||||
"github.com/prysmaticlabs/prysm/v4/testing/util"
|
||||
)
|
||||
|
||||
func TestGetValidatorCountInvalidRequest(t *testing.T) {
|
||||
st, _ := util.DeterministicGenesisState(t, 10)
|
||||
stateIdCheckerStateFunc := func(_ context.Context, stateId []byte) (state.BeaconState, error) {
|
||||
stateIdString := strings.ToLower(string(stateId))
|
||||
switch stateIdString {
|
||||
case "head", "genesis", "finalized", "justified":
|
||||
return st, nil
|
||||
default:
|
||||
if len(stateId) == 32 {
|
||||
return nil, nil
|
||||
} else {
|
||||
_, parseErr := strconv.ParseUint(stateIdString, 10, 64)
|
||||
if parseErr != nil {
|
||||
// ID format does not match any valid options.
|
||||
e := lookup.NewStateIdParseError(parseErr)
|
||||
return nil, &e
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
stater lookup.Stater
|
||||
status string
|
||||
stateID string
|
||||
expectedErrorMessage string
|
||||
statusCode int
|
||||
}{
|
||||
{
|
||||
name: "invalid status",
|
||||
stater: &testutil.MockStater{
|
||||
BeaconState: st,
|
||||
},
|
||||
status: "helloworld",
|
||||
stateID: "head",
|
||||
expectedErrorMessage: "invalid status query parameter",
|
||||
statusCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "invalid state ID",
|
||||
stater: &testutil.MockStater{StateProviderFunc: stateIdCheckerStateFunc},
|
||||
stateID: "helloworld",
|
||||
expectedErrorMessage: "invalid state ID",
|
||||
statusCode: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
chainService := &chainMock.ChainService{Optimistic: false, FinalizedRoots: make(map[[32]byte]bool)}
|
||||
|
||||
server := &Server{
|
||||
OptimisticModeFetcher: chainService,
|
||||
FinalizationFetcher: chainService,
|
||||
Stater: test.stater,
|
||||
}
|
||||
|
||||
testRouter := mux.NewRouter()
|
||||
testRouter.HandleFunc("/eth/v1/beacon/states/{state_id}/validator_count", server.GetValidatorCount)
|
||||
s := httptest.NewServer(testRouter)
|
||||
defer s.Close()
|
||||
|
||||
queryParams := neturl.Values{}
|
||||
queryParams.Add("status", test.status)
|
||||
resp, err := http.Get(s.URL + fmt.Sprintf("/eth/v1/beacon/states/%s/validator_count?%s",
|
||||
test.stateID, queryParams.Encode()))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var errJson http2.DefaultErrorJson
|
||||
err = json.Unmarshal(body, &errJson)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.statusCode, errJson.Code)
|
||||
require.StringContains(t, test.expectedErrorMessage, errJson.Message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetValidatorCount(t *testing.T) {
|
||||
st, _ := util.DeterministicGenesisState(t, 10)
|
||||
farFutureEpoch := params.BeaconConfig().FarFutureEpoch
|
||||
validators := []*eth.Validator{
|
||||
// Pending initialized.
|
||||
{
|
||||
ActivationEpoch: farFutureEpoch,
|
||||
ActivationEligibilityEpoch: farFutureEpoch,
|
||||
ExitEpoch: farFutureEpoch,
|
||||
WithdrawableEpoch: farFutureEpoch,
|
||||
},
|
||||
// Pending queued.
|
||||
{
|
||||
ActivationEpoch: 10,
|
||||
ActivationEligibilityEpoch: 4,
|
||||
ExitEpoch: farFutureEpoch,
|
||||
WithdrawableEpoch: farFutureEpoch,
|
||||
},
|
||||
// Active ongoing.
|
||||
{
|
||||
ActivationEpoch: 0,
|
||||
ExitEpoch: farFutureEpoch,
|
||||
},
|
||||
// Active slashed.
|
||||
{
|
||||
ActivationEpoch: 0,
|
||||
ExitEpoch: 30,
|
||||
Slashed: true,
|
||||
WithdrawableEpoch: 50,
|
||||
},
|
||||
// Active exiting.
|
||||
{
|
||||
ActivationEpoch: 0,
|
||||
ExitEpoch: 30,
|
||||
Slashed: false,
|
||||
WithdrawableEpoch: 50,
|
||||
},
|
||||
// Exit slashed (at epoch 35).
|
||||
{
|
||||
ActivationEpoch: 3,
|
||||
ExitEpoch: 30,
|
||||
WithdrawableEpoch: 50,
|
||||
Slashed: true,
|
||||
},
|
||||
// Exit unslashed (at epoch 35).
|
||||
{
|
||||
ActivationEpoch: 3,
|
||||
ExitEpoch: 30,
|
||||
WithdrawableEpoch: 50,
|
||||
Slashed: false,
|
||||
},
|
||||
// Withdrawable (at epoch 45).
|
||||
{
|
||||
ActivationEpoch: 3,
|
||||
ExitEpoch: 30,
|
||||
WithdrawableEpoch: 40,
|
||||
EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance,
|
||||
Slashed: false,
|
||||
},
|
||||
// Withdrawal done (at epoch 45).
|
||||
{
|
||||
ActivationEpoch: 3,
|
||||
ExitEpoch: 30,
|
||||
WithdrawableEpoch: 40,
|
||||
EffectiveBalance: 0,
|
||||
Slashed: false,
|
||||
},
|
||||
}
|
||||
for _, validator := range validators {
|
||||
require.NoError(t, st.AppendValidator(validator))
|
||||
require.NoError(t, st.AppendBalance(params.BeaconConfig().MaxEffectiveBalance))
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
stateID string
|
||||
statuses []string
|
||||
currentEpoch int
|
||||
expectedResponse ValidatorCountResponse
|
||||
}{
|
||||
{
|
||||
name: "Head count active validators",
|
||||
stateID: "head",
|
||||
statuses: []string{"active"},
|
||||
expectedResponse: ValidatorCountResponse{
|
||||
ExecutionOptimistic: "false",
|
||||
Finalized: "true",
|
||||
Data: []*ValidatorCount{
|
||||
{
|
||||
Status: "active",
|
||||
Count: "13",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Head count active ongoing validators",
|
||||
stateID: "head",
|
||||
statuses: []string{"active_ongoing"},
|
||||
expectedResponse: ValidatorCountResponse{
|
||||
ExecutionOptimistic: "false",
|
||||
Finalized: "true",
|
||||
Data: []*ValidatorCount{
|
||||
{
|
||||
Status: "active_ongoing",
|
||||
Count: "11",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Head count active exiting validators",
|
||||
stateID: "head",
|
||||
statuses: []string{"active_exiting"},
|
||||
expectedResponse: ValidatorCountResponse{
|
||||
ExecutionOptimistic: "false",
|
||||
Finalized: "true",
|
||||
Data: []*ValidatorCount{
|
||||
{
|
||||
Status: "active_exiting",
|
||||
Count: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Head count active slashed validators",
|
||||
stateID: "head",
|
||||
statuses: []string{"active_slashed"},
|
||||
expectedResponse: ValidatorCountResponse{
|
||||
ExecutionOptimistic: "false",
|
||||
Finalized: "true",
|
||||
Data: []*ValidatorCount{
|
||||
{
|
||||
Status: "active_slashed",
|
||||
Count: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Head count pending validators",
|
||||
stateID: "head",
|
||||
statuses: []string{"pending"},
|
||||
expectedResponse: ValidatorCountResponse{
|
||||
ExecutionOptimistic: "false",
|
||||
Finalized: "true",
|
||||
Data: []*ValidatorCount{
|
||||
{
|
||||
Status: "pending",
|
||||
Count: "6",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Head count pending initialized validators",
|
||||
stateID: "head",
|
||||
statuses: []string{"pending_initialized"},
|
||||
expectedResponse: ValidatorCountResponse{
|
||||
ExecutionOptimistic: "false",
|
||||
Finalized: "true",
|
||||
Data: []*ValidatorCount{
|
||||
{
|
||||
Status: "pending_initialized",
|
||||
Count: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Head count pending queued validators",
|
||||
stateID: "head",
|
||||
statuses: []string{"pending_queued"},
|
||||
expectedResponse: ValidatorCountResponse{
|
||||
ExecutionOptimistic: "false",
|
||||
Finalized: "true",
|
||||
Data: []*ValidatorCount{
|
||||
{
|
||||
Status: "pending_queued",
|
||||
Count: "5",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Head count exited validators",
|
||||
stateID: "head",
|
||||
statuses: []string{"exited"},
|
||||
currentEpoch: 35,
|
||||
expectedResponse: ValidatorCountResponse{
|
||||
ExecutionOptimistic: "false",
|
||||
Finalized: "true",
|
||||
Data: []*ValidatorCount{
|
||||
{
|
||||
Status: "exited",
|
||||
Count: "6",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Head count exited slashed validators",
|
||||
stateID: "head",
|
||||
statuses: []string{"exited_slashed"},
|
||||
currentEpoch: 35,
|
||||
expectedResponse: ValidatorCountResponse{
|
||||
ExecutionOptimistic: "false",
|
||||
Finalized: "true",
|
||||
Data: []*ValidatorCount{
|
||||
{
|
||||
Status: "exited_slashed",
|
||||
Count: "2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Head count exited unslashed validators",
|
||||
stateID: "head",
|
||||
statuses: []string{"exited_unslashed"},
|
||||
currentEpoch: 35,
|
||||
expectedResponse: ValidatorCountResponse{
|
||||
ExecutionOptimistic: "false",
|
||||
Finalized: "true",
|
||||
Data: []*ValidatorCount{
|
||||
{
|
||||
Status: "exited_unslashed",
|
||||
Count: "4",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Head count withdrawal validators",
|
||||
stateID: "head",
|
||||
statuses: []string{"withdrawal"},
|
||||
currentEpoch: 45,
|
||||
expectedResponse: ValidatorCountResponse{
|
||||
ExecutionOptimistic: "false",
|
||||
Finalized: "true",
|
||||
Data: []*ValidatorCount{
|
||||
{
|
||||
Status: "withdrawal",
|
||||
Count: "2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Head count withdrawal possible validators",
|
||||
stateID: "head",
|
||||
statuses: []string{"withdrawal_possible"},
|
||||
currentEpoch: 45,
|
||||
expectedResponse: ValidatorCountResponse{
|
||||
ExecutionOptimistic: "false",
|
||||
Finalized: "true",
|
||||
Data: []*ValidatorCount{
|
||||
{
|
||||
Status: "withdrawal_possible",
|
||||
Count: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Head count withdrawal done validators",
|
||||
stateID: "head",
|
||||
statuses: []string{"withdrawal_done"},
|
||||
currentEpoch: 45,
|
||||
expectedResponse: ValidatorCountResponse{
|
||||
ExecutionOptimistic: "false",
|
||||
Finalized: "true",
|
||||
Data: []*ValidatorCount{
|
||||
{
|
||||
Status: "withdrawal_done",
|
||||
Count: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Head count active and pending validators",
|
||||
stateID: "head",
|
||||
statuses: []string{"active", "pending"},
|
||||
expectedResponse: ValidatorCountResponse{
|
||||
ExecutionOptimistic: "false",
|
||||
Finalized: "true",
|
||||
Data: []*ValidatorCount{
|
||||
{
|
||||
Status: "active",
|
||||
Count: "13",
|
||||
},
|
||||
{
|
||||
Status: "pending",
|
||||
Count: "6",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Head count of ALL validators",
|
||||
stateID: "head",
|
||||
expectedResponse: ValidatorCountResponse{
|
||||
ExecutionOptimistic: "false",
|
||||
Finalized: "true",
|
||||
Data: []*ValidatorCount{
|
||||
{
|
||||
Status: "active",
|
||||
Count: "13",
|
||||
},
|
||||
{
|
||||
Status: "active_exiting",
|
||||
Count: "1",
|
||||
},
|
||||
{
|
||||
Status: "active_ongoing",
|
||||
Count: "11",
|
||||
},
|
||||
{
|
||||
Status: "active_slashed",
|
||||
Count: "1",
|
||||
},
|
||||
{
|
||||
Status: "pending",
|
||||
Count: "6",
|
||||
},
|
||||
{
|
||||
Status: "pending_initialized",
|
||||
Count: "1",
|
||||
},
|
||||
{
|
||||
Status: "pending_queued",
|
||||
Count: "5",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
chainService := &chainMock.ChainService{Optimistic: false, FinalizedRoots: make(map[[32]byte]bool)}
|
||||
blockRoot, err := st.LatestBlockHeader().HashTreeRoot()
|
||||
require.NoError(t, err)
|
||||
chainService.FinalizedRoots[blockRoot] = true
|
||||
require.NoError(t, st.SetSlot(params.BeaconConfig().SlotsPerEpoch*primitives.Slot(test.currentEpoch)))
|
||||
|
||||
server := &Server{
|
||||
OptimisticModeFetcher: chainService,
|
||||
FinalizationFetcher: chainService,
|
||||
Stater: &testutil.MockStater{
|
||||
BeaconState: st,
|
||||
},
|
||||
}
|
||||
|
||||
testRouter := mux.NewRouter()
|
||||
testRouter.HandleFunc("/eth/v1/beacon/states/{state_id}/validator_count", server.GetValidatorCount)
|
||||
s := httptest.NewServer(testRouter)
|
||||
defer s.Close()
|
||||
|
||||
queryParams := neturl.Values{}
|
||||
for _, status := range test.statuses {
|
||||
queryParams.Add("status", status)
|
||||
}
|
||||
resp, err := http.Get(s.URL + fmt.Sprintf("/eth/v1/beacon/states/%s/validator_count?%s",
|
||||
test.stateID, queryParams.Encode()))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var count ValidatorCountResponse
|
||||
err = json.Unmarshal(body, &count)
|
||||
require.NoError(t, err)
|
||||
require.DeepEqual(t, test.expectedResponse, count)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -402,12 +402,18 @@ func (s *Service) Start() {
|
||||
ForkchoiceFetcher: s.cfg.ForkchoiceFetcher,
|
||||
}
|
||||
httpServer := &httpserver.Server{
|
||||
GenesisTimeFetcher: s.cfg.GenesisTimeFetcher,
|
||||
HeadFetcher: s.cfg.HeadFetcher,
|
||||
SyncChecker: s.cfg.SyncService,
|
||||
CoreService: coreService,
|
||||
GenesisTimeFetcher: s.cfg.GenesisTimeFetcher,
|
||||
HeadFetcher: s.cfg.HeadFetcher,
|
||||
SyncChecker: s.cfg.SyncService,
|
||||
CoreService: coreService,
|
||||
OptimisticModeFetcher: s.cfg.OptimisticModeFetcher,
|
||||
Stater: stater,
|
||||
ChainInfoFetcher: s.cfg.ChainInfoFetcher,
|
||||
BeaconDB: s.cfg.BeaconDB,
|
||||
FinalizationFetcher: s.cfg.FinalizationFetcher,
|
||||
}
|
||||
s.cfg.Router.HandleFunc("/prysm/validators/performance", httpServer.GetValidatorPerformance).Methods(http.MethodPost)
|
||||
s.cfg.Router.HandleFunc("/eth/v1/beacon/states/{state_id}/validator_count", httpServer.GetValidatorCount).Methods(http.MethodGet)
|
||||
s.cfg.Router.HandleFunc("/eth/v2/beacon/blocks", beaconChainServerV1.PublishBlockV2).Methods(http.MethodPost)
|
||||
s.cfg.Router.HandleFunc("/eth/v2/beacon/blinded_blocks", beaconChainServerV1.PublishBlindedBlockV2).Methods(http.MethodPost)
|
||||
s.cfg.Router.HandleFunc("/eth/v1/beacon/blocks/{block_id}/root", beaconChainServerV1.GetBlockRoot).Methods(http.MethodGet)
|
||||
|
||||
@@ -3,24 +3,31 @@ package testutil
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/prysmaticlabs/prysm/v4/encoding/bytesutil"
|
||||
|
||||
"github.com/prysmaticlabs/prysm/v4/beacon-chain/state"
|
||||
"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
|
||||
"github.com/prysmaticlabs/prysm/v4/encoding/bytesutil"
|
||||
)
|
||||
|
||||
// MockStater is a fake implementation of lookup.Stater.
|
||||
type MockStater struct {
|
||||
BeaconState state.BeaconState
|
||||
BeaconStateRoot []byte
|
||||
StatesBySlot map[primitives.Slot]state.BeaconState
|
||||
StatesByRoot map[[32]byte]state.BeaconState
|
||||
BeaconState state.BeaconState
|
||||
StateProviderFunc func(ctx context.Context, stateId []byte) (state.BeaconState, error)
|
||||
BeaconStateRoot []byte
|
||||
StatesBySlot map[primitives.Slot]state.BeaconState
|
||||
StatesByRoot map[[32]byte]state.BeaconState
|
||||
}
|
||||
|
||||
// State --
|
||||
func (m *MockStater) State(_ context.Context, id []byte) (state.BeaconState, error) {
|
||||
func (m *MockStater) State(ctx context.Context, id []byte) (state.BeaconState, error) {
|
||||
if m.StateProviderFunc != nil {
|
||||
return m.StateProviderFunc(ctx, id)
|
||||
}
|
||||
|
||||
if m.BeaconState != nil {
|
||||
return m.BeaconState, nil
|
||||
}
|
||||
|
||||
return m.StatesByRoot[bytesutil.ToBytes32(id)], nil
|
||||
}
|
||||
|
||||
|
||||
@@ -77,3 +77,8 @@ func NotEmpty(tb assertions.AssertionTestingTB, obj interface{}, msg ...interfac
|
||||
func ErrorIs(tb assertions.AssertionTestingTB, err, target error, msg ...interface{}) {
|
||||
assertions.ErrorIs(tb.Fatalf, err, target, msg)
|
||||
}
|
||||
|
||||
// StringContains asserts that actual string contains expected message.
|
||||
func StringContains(tb assertions.AssertionTestingTB, expected, actual string, msg ...interface{}) {
|
||||
assertions.StringContains(tb.Fatalf, expected, actual, true, msg)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user