From 2806326155a3590d03fe18c49112cd1c342d4d89 Mon Sep 17 00:00:00 2001 From: Dhruv Bodani Date: Wed, 11 Oct 2023 18:53:02 +0530 Subject: [PATCH] integrate validator count endpoint in validator client (#12912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: RadosÅ‚aw Kapka --- .../rpc/prysm/validator/validator_count.go | 2 +- hack/update-mockgen.sh | 1 + testing/validator-mock/BUILD.bazel | 3 + .../prysm_beacon_chain_client_mock.go | 52 +++ validator/client/BUILD.bazel | 1 + validator/client/beacon-api/BUILD.bazel | 6 + .../beacon-api/beacon_api_validator_client.go | 5 + validator/client/beacon-api/duties_test.go | 25 ++ .../client/beacon-api/mock/duties_mock.go | 4 +- .../client/beacon-api/mock/genesis_mock.go | 2 +- .../beacon-api/prysm_beacon_chain_client.go | 81 ++++ validator/client/beacon-api/status.go | 58 ++- validator/client/beacon-api/status_test.go | 390 ++++++++++-------- .../client/beacon-api/validator_count_test.go | 163 ++++++++ .../beacon-chain-client-factory/BUILD.bazel | 1 + .../beacon_chain_client_factory.go | 21 +- validator/client/grpc-api/BUILD.bazel | 18 +- .../grpc_prysm_beacon_chain_client.go | 97 +++++ .../grpc_prysm_beacon_chain_client_test.go | 326 +++++++++++++++ validator/client/iface/BUILD.bazel | 5 +- .../client/iface/prysm_beacon_chain_client.go | 20 + validator/client/key_reload.go | 17 +- validator/client/key_reload_test.go | 35 +- validator/client/service.go | 2 + validator/client/validator.go | 18 +- validator/client/validator_test.go | 28 +- validator/client/wait_for_activation.go | 15 +- validator/client/wait_for_activation_test.go | 105 +++-- 28 files changed, 1225 insertions(+), 276 deletions(-) create mode 100644 testing/validator-mock/prysm_beacon_chain_client_mock.go create mode 100644 validator/client/beacon-api/prysm_beacon_chain_client.go create mode 100644 validator/client/beacon-api/validator_count_test.go create mode 100644 validator/client/grpc-api/grpc_prysm_beacon_chain_client.go create mode 100644 validator/client/grpc-api/grpc_prysm_beacon_chain_client_test.go create mode 100644 validator/client/iface/prysm_beacon_chain_client.go diff --git a/beacon-chain/rpc/prysm/validator/validator_count.go b/beacon-chain/rpc/prysm/validator/validator_count.go index 6f698d258..4a829dd6b 100644 --- a/beacon-chain/rpc/prysm/validator/validator_count.go +++ b/beacon-chain/rpc/prysm/validator/validator_count.go @@ -162,7 +162,7 @@ func validatorCountByStatus(validators []*eth.Validator, statuses []validator.Va var resp []*ValidatorCount for status, count := range countByStatus { resp = append(resp, &ValidatorCount{ - Status: strings.ToLower(ethpb.ValidatorStatus_name[int32(status)]), + Status: status.String(), Count: strconv.FormatUint(count, 10), }) } diff --git a/hack/update-mockgen.sh b/hack/update-mockgen.sh index a717fbb46..8c84dad74 100755 --- a/hack/update-mockgen.sh +++ b/hack/update-mockgen.sh @@ -57,6 +57,7 @@ done # -------------------------------------------------------- iface_mocks=( "$iface_mock_path/beacon_chain_client_mock.go BeaconChainClient" + "$iface_mock_path/prysm_beacon_chain_client_mock.go PrysmBeaconChainClient" "$iface_mock_path/node_client_mock.go NodeClient" "$iface_mock_path/slasher_client_mock.go SlasherClient" "$iface_mock_path/validator_client_mock.go ValidatorClient" diff --git a/testing/validator-mock/BUILD.bazel b/testing/validator-mock/BUILD.bazel index b6aa73a3e..28be37b91 100644 --- a/testing/validator-mock/BUILD.bazel +++ b/testing/validator-mock/BUILD.bazel @@ -7,13 +7,16 @@ go_library( srcs = [ "beacon_chain_client_mock.go", "node_client_mock.go", + "prysm_beacon_chain_client_mock.go", "validator_client_mock.go", ], importpath = "github.com/prysmaticlabs/prysm/v4/testing/validator-mock", visibility = ["//visibility:public"], deps = [ "//consensus-types/primitives:go_default_library", + "//consensus-types/validator:go_default_library", "//proto/prysm/v1alpha1:go_default_library", + "//validator/client/iface:go_default_library", "@com_github_golang_mock//gomock:go_default_library", "@org_golang_google_protobuf//types/known/emptypb:go_default_library", ], diff --git a/testing/validator-mock/prysm_beacon_chain_client_mock.go b/testing/validator-mock/prysm_beacon_chain_client_mock.go new file mode 100644 index 000000000..dec4aeaa8 --- /dev/null +++ b/testing/validator-mock/prysm_beacon_chain_client_mock.go @@ -0,0 +1,52 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/prysmaticlabs/prysm/v4/validator/client/iface (interfaces: PrysmBeaconChainClient) + +// Package validator_mock is a generated GoMock package. +package validator_mock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + validator "github.com/prysmaticlabs/prysm/v4/consensus-types/validator" + iface "github.com/prysmaticlabs/prysm/v4/validator/client/iface" +) + +// MockPrysmBeaconChainClient is a mock of PrysmBeaconChainClient interface. +type MockPrysmBeaconChainClient struct { + ctrl *gomock.Controller + recorder *MockPrysmBeaconChainClientMockRecorder +} + +// MockPrysmBeaconChainClientMockRecorder is the mock recorder for MockPrysmBeaconChainClient. +type MockPrysmBeaconChainClientMockRecorder struct { + mock *MockPrysmBeaconChainClient +} + +// NewMockPrysmBeaconChainClient creates a new mock instance. +func NewMockPrysmBeaconChainClient(ctrl *gomock.Controller) *MockPrysmBeaconChainClient { + mock := &MockPrysmBeaconChainClient{ctrl: ctrl} + mock.recorder = &MockPrysmBeaconChainClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPrysmBeaconChainClient) EXPECT() *MockPrysmBeaconChainClientMockRecorder { + return m.recorder +} + +// GetValidatorCount mocks base method. +func (m *MockPrysmBeaconChainClient) GetValidatorCount(arg0 context.Context, arg1 string, arg2 []validator.ValidatorStatus) ([]iface.ValidatorCount, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetValidatorCount", arg0, arg1, arg2) + ret0, _ := ret[0].([]iface.ValidatorCount) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetValidatorCount indicates an expected call of GetValidatorCount. +func (mr *MockPrysmBeaconChainClientMockRecorder) GetValidatorCount(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatorCount", reflect.TypeOf((*MockPrysmBeaconChainClient)(nil).GetValidatorCount), arg0, arg1, arg2) +} diff --git a/validator/client/BUILD.bazel b/validator/client/BUILD.bazel index 0117fd19f..829398d92 100644 --- a/validator/client/BUILD.bazel +++ b/validator/client/BUILD.bazel @@ -41,6 +41,7 @@ go_library( "//consensus-types/blocks:go_default_library", "//consensus-types/interfaces:go_default_library", "//consensus-types/primitives:go_default_library", + "//consensus-types/validator:go_default_library", "//crypto/bls:go_default_library", "//crypto/hash:go_default_library", "//crypto/rand:go_default_library", diff --git a/validator/client/beacon-api/BUILD.bazel b/validator/client/beacon-api/BUILD.bazel index 24c2d9083..5b2843c44 100644 --- a/validator/client/beacon-api/BUILD.bazel +++ b/validator/client/beacon-api/BUILD.bazel @@ -24,6 +24,7 @@ go_library( "propose_attestation.go", "propose_beacon_block.go", "propose_exit.go", + "prysm_beacon_chain_client.go", "registration.go", "state_validators.go", "status.go", @@ -48,6 +49,7 @@ go_library( "//beacon-chain/rpc/prysm/validator:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", + "//consensus-types/validator:go_default_library", "//encoding/bytesutil:go_default_library", "//network/forks:go_default_library", "//proto/engine/v1:go_default_library", @@ -105,6 +107,7 @@ go_test( "submit_signed_contribution_and_proof_test.go", "subscribe_committee_subnets_test.go", "sync_committee_test.go", + "validator_count_test.go", "wait_for_chain_start_test.go", ], embed = [":go_default_library"], @@ -118,14 +121,17 @@ go_test( "//beacon-chain/rpc/prysm/validator:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", + "//consensus-types/validator:go_default_library", "//encoding/bytesutil:go_default_library", "//proto/engine/v1:go_default_library", "//proto/prysm/v1alpha1:go_default_library", "//testing/assert:go_default_library", "//testing/require:go_default_library", + "//testing/validator-mock:go_default_library", "//time/slots:go_default_library", "//validator/client/beacon-api/mock:go_default_library", "//validator/client/beacon-api/test-helpers:go_default_library", + "//validator/client/iface:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_golang_mock//gomock:go_default_library", "@com_github_golang_protobuf//ptypes/empty", diff --git a/validator/client/beacon-api/beacon_api_validator_client.go b/validator/client/beacon-api/beacon_api_validator_client.go index 361782154..585693fe0 100644 --- a/validator/client/beacon-api/beacon_api_validator_client.go +++ b/validator/client/beacon-api/beacon_api_validator_client.go @@ -20,6 +20,7 @@ type beaconApiValidatorClient struct { stateValidatorsProvider stateValidatorsProvider jsonRestHandler jsonRestHandler beaconBlockConverter beaconBlockConverter + prysmBeaconChainCLient iface.PrysmBeaconChainClient } func NewBeaconApiValidatorClient(host string, timeout time.Duration) iface.ValidatorClient { @@ -34,6 +35,10 @@ func NewBeaconApiValidatorClient(host string, timeout time.Duration) iface.Valid stateValidatorsProvider: beaconApiStateValidatorsProvider{jsonRestHandler: jsonRestHandler}, jsonRestHandler: jsonRestHandler, beaconBlockConverter: beaconApiBeaconBlockConverter{}, + prysmBeaconChainCLient: prysmBeaconChainClient{ + nodeClient: &beaconApiNodeClient{jsonRestHandler: jsonRestHandler}, + jsonRestHandler: jsonRestHandler, + }, } } diff --git a/validator/client/beacon-api/duties_test.go b/validator/client/beacon-api/duties_test.go index a737c6854..0c168a5af 100644 --- a/validator/client/beacon-api/duties_test.go +++ b/validator/client/beacon-api/duties_test.go @@ -9,6 +9,9 @@ import ( "strconv" "testing" + validatormock "github.com/prysmaticlabs/prysm/v4/testing/validator-mock" + "github.com/prysmaticlabs/prysm/v4/validator/client/iface" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/golang/mock/gomock" "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/beacon" @@ -1259,10 +1262,21 @@ func TestGetDuties_Valid(t *testing.T) { nil, ).MinTimes(1) + prysmBeaconChainClient := validatormock.NewMockPrysmBeaconChainClient(ctrl) + prysmBeaconChainClient.EXPECT().GetValidatorCount( + ctx, + gomock.Any(), + gomock.Any(), + ).Return( + nil, + iface.ErrNotSupported, + ).MinTimes(1) + // Make sure that our values are equal to what would be returned by calling getDutiesForEpoch individually validatorClient := &beaconApiValidatorClient{ dutiesProvider: dutiesProvider, stateValidatorsProvider: stateValidatorsProvider, + prysmBeaconChainCLient: prysmBeaconChainClient, } expectedCurrentEpochDuties, err := validatorClient.getDutiesForEpoch( @@ -1356,9 +1370,20 @@ func TestGetDuties_GetDutiesForEpochFailed(t *testing.T) { errors.New("foo error"), ).Times(1) + prysmBeaconChainClient := validatormock.NewMockPrysmBeaconChainClient(ctrl) + prysmBeaconChainClient.EXPECT().GetValidatorCount( + ctx, + gomock.Any(), + gomock.Any(), + ).Return( + nil, + iface.ErrNotSupported, + ).MinTimes(1) + validatorClient := &beaconApiValidatorClient{ stateValidatorsProvider: stateValidatorsProvider, dutiesProvider: dutiesProvider, + prysmBeaconChainCLient: prysmBeaconChainClient, } _, err := validatorClient.getDuties(ctx, ðpb.DutiesRequest{ diff --git a/validator/client/beacon-api/mock/duties_mock.go b/validator/client/beacon-api/mock/duties_mock.go index 178be9988..3d8fc86dc 100644 --- a/validator/client/beacon-api/mock/duties_mock.go +++ b/validator/client/beacon-api/mock/duties_mock.go @@ -9,8 +9,8 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/shared" - "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/validator" + shared "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/shared" + validator "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/validator" primitives "github.com/prysmaticlabs/prysm/v4/consensus-types/primitives" ) diff --git a/validator/client/beacon-api/mock/genesis_mock.go b/validator/client/beacon-api/mock/genesis_mock.go index c2b3c4ccd..a8ee97fc9 100644 --- a/validator/client/beacon-api/mock/genesis_mock.go +++ b/validator/client/beacon-api/mock/genesis_mock.go @@ -10,7 +10,7 @@ import ( gomock "github.com/golang/mock/gomock" apimiddleware "github.com/prysmaticlabs/prysm/v4/api/gateway/apimiddleware" - "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/beacon" + beacon "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/beacon" ) // MockgenesisProvider is a mock of genesisProvider interface. diff --git a/validator/client/beacon-api/prysm_beacon_chain_client.go b/validator/client/beacon-api/prysm_beacon_chain_client.go new file mode 100644 index 000000000..01076bf4d --- /dev/null +++ b/validator/client/beacon-api/prysm_beacon_chain_client.go @@ -0,0 +1,81 @@ +package beacon_api + +import ( + "context" + "fmt" + "net/http" + neturl "net/url" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/prysm/validator" + validator2 "github.com/prysmaticlabs/prysm/v4/consensus-types/validator" + "github.com/prysmaticlabs/prysm/v4/validator/client/iface" +) + +// NewPrysmBeaconChainClient returns implementation of iface.PrysmBeaconChainClient. +func NewPrysmBeaconChainClient(host string, timeout time.Duration, nodeClient iface.NodeClient) iface.PrysmBeaconChainClient { + jsonRestHandler := beaconApiJsonRestHandler{ + httpClient: http.Client{Timeout: timeout}, + host: host, + } + + return prysmBeaconChainClient{ + jsonRestHandler: jsonRestHandler, + nodeClient: nodeClient, + } +} + +type prysmBeaconChainClient struct { + jsonRestHandler jsonRestHandler + nodeClient iface.NodeClient +} + +func (c prysmBeaconChainClient) GetValidatorCount(ctx context.Context, stateID string, statuses []validator2.ValidatorStatus) ([]iface.ValidatorCount, error) { + // Check node version for prysm beacon node as it is a custom endpoint for prysm beacon node. + nodeVersion, err := c.nodeClient.GetVersion(ctx, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to get node version") + } + + if !strings.Contains(strings.ToLower(nodeVersion.Version), "prysm") { + return nil, iface.ErrNotSupported + } + + queryParams := neturl.Values{} + for _, status := range statuses { + queryParams.Add("status", status.String()) + } + + queryUrl := buildURL(fmt.Sprintf("/eth/v1/beacon/states/%s/validator_count", stateID), queryParams) + + var validatorCountResponse validator.ValidatorCountResponse + if _, err := c.jsonRestHandler.GetRestJsonResponse(ctx, queryUrl, &validatorCountResponse); err != nil { + return nil, errors.Wrap(err, "failed to query GET REST endpoint") + } + + if validatorCountResponse.Data == nil { + return nil, errors.New("validator count data is nil") + } + + if len(statuses) != 0 && len(statuses) != len(validatorCountResponse.Data) { + return nil, errors.New("mismatch between validator count data and the number of statuses provided") + } + + var resp []iface.ValidatorCount + for _, vc := range validatorCountResponse.Data { + count, err := strconv.ParseUint(vc.Count, 10, 64) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse validator count %s", vc.Count) + } + + resp = append(resp, iface.ValidatorCount{ + Status: vc.Status, + Count: count, + }) + } + + return resp, nil +} diff --git a/validator/client/beacon-api/status.go b/validator/client/beacon-api/status.go index 7253b4ce1..aee3c479c 100644 --- a/validator/client/beacon-api/status.go +++ b/validator/client/beacon-api/status.go @@ -4,6 +4,8 @@ import ( "context" "strconv" + "github.com/prysmaticlabs/prysm/v4/validator/client/iface" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v4/config/params" @@ -73,12 +75,32 @@ func (c *beaconApiValidatorClient) getValidatorsStatusResponse(ctx context.Conte return nil, nil, nil, errors.Wrap(err, "failed to get state validators") } - isLastActivatedValidatorIndexRetrieved := false - var lastActivatedValidatorIndex uint64 = 0 + validatorsCountResponse, err := c.prysmBeaconChainCLient.GetValidatorCount(ctx, "head", nil) + if err != nil && !errors.Is(err, iface.ErrNotSupported) { + return nil, nil, nil, errors.Wrap(err, "failed to get total validator count") + } + + var total, pending uint64 + for _, valCount := range validatorsCountResponse { + if valCount.Status == "pending" { + pending = valCount.Count + } else { + total += valCount.Count + } + } + + // Calculate last activated validator's index, it will be -1 whenever all validators are pending. + lastActivatedValidatorIndex := int(total - pending - 1) for i, validatorContainer := range stateValidatorsResponse.Data { - stringPubKey := validatorContainer.Validator.Pubkey + validatorIndex, err := strconv.ParseUint(validatorContainer.Index, 10, 64) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "failed to parse validator index %s", validatorContainer.Index) + } + outIndexes[i] = primitives.ValidatorIndex(validatorIndex) + + stringPubKey := validatorContainer.Validator.Pubkey stringRetrievedPubKeys[stringPubKey] = struct{}{} pubKey, ok := stringTargetPubKeysToPubKeys[stringPubKey] @@ -90,14 +112,7 @@ func (c *beaconApiValidatorClient) getValidatorsStatusResponse(ctx context.Conte } } - validatorIndex, err := strconv.ParseUint(validatorContainer.Index, 10, 64) - if err != nil { - return nil, nil, nil, errors.Wrapf(err, "failed to parse validator index %s", validatorContainer.Index) - } - outPubKeys[i] = pubKey - outIndexes[i] = primitives.ValidatorIndex(validatorIndex) - validatorStatus := ðpb.ValidatorStatusResponse{} // Set Status @@ -119,28 +134,9 @@ func (c *beaconApiValidatorClient) getValidatorsStatusResponse(ctx context.Conte // Set PositionInActivationQueue switch status { case ethpb.ValidatorStatus_PENDING, ethpb.ValidatorStatus_PARTIALLY_DEPOSITED, ethpb.ValidatorStatus_DEPOSITED: - if !isLastActivatedValidatorIndexRetrieved { - isLastActivatedValidatorIndexRetrieved = true - // TODO: double check this due to potential of PENDING STATE being active.. - // edge case https://github.com/prysmaticlabs/prysm/blob/0669050ffabe925c3d6e5e5d535a86361ae8522b/validator/client/validator.go#L1068 - activeStateValidators, err := c.stateValidatorsProvider.GetStateValidators(ctx, nil, nil, []string{"active"}) - if err != nil { - return nil, nil, nil, errors.Wrap(err, "failed to get state validators") - } - - data := activeStateValidators.Data - - if nbActiveValidators := len(data); nbActiveValidators != 0 { - lastValidator := data[nbActiveValidators-1] - - lastActivatedValidatorIndex, err = strconv.ParseUint(lastValidator.Index, 10, 64) - if err != nil { - return nil, nil, nil, errors.Wrapf(err, "failed to parse last validator index %s", lastValidator.Index) - } - } + if lastActivatedValidatorIndex >= 0 { + validatorStatus.PositionInActivationQueue = validatorIndex - uint64(lastActivatedValidatorIndex) } - - validatorStatus.PositionInActivationQueue = validatorIndex - lastActivatedValidatorIndex } outValidatorsStatuses[i] = validatorStatus diff --git a/validator/client/beacon-api/status_test.go b/validator/client/beacon-api/status_test.go index 029b61638..0be9efb56 100644 --- a/validator/client/beacon-api/status_test.go +++ b/validator/client/beacon-api/status_test.go @@ -6,6 +6,10 @@ import ( "fmt" "testing" + "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/apimiddleware" + validator2 "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/prysm/validator" + "github.com/prysmaticlabs/prysm/v4/validator/client/iface" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/golang/mock/gomock" "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/beacon" @@ -50,7 +54,26 @@ func TestValidatorStatus_Nominal(t *testing.T) { nil, ).Times(1) - validatorClient := beaconApiValidatorClient{stateValidatorsProvider: stateValidatorsProvider} + jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) + validatorClient := beaconApiValidatorClient{ + stateValidatorsProvider: stateValidatorsProvider, + prysmBeaconChainCLient: prysmBeaconChainClient{ + nodeClient: &beaconApiNodeClient{ + jsonRestHandler: jsonRestHandler, + }, + }, + } + + // Expect node version endpoint call. + var nodeVersionResponse apimiddleware.VersionResponseJson + jsonRestHandler.EXPECT().GetRestJsonResponse( + ctx, + "/eth/v1/node/version", + &nodeVersionResponse, + ).Return( + nil, + iface.ErrNotSupported, + ).Times(1) actualValidatorStatusResponse, err := validatorClient.ValidatorStatus( ctx, @@ -147,7 +170,27 @@ func TestMultipleValidatorStatus_Nominal(t *testing.T) { nil, ).Times(1) - validatorClient := beaconApiValidatorClient{stateValidatorsProvider: stateValidatorsProvider} + jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) + + // Expect node version endpoint call. + var nodeVersionResponse apimiddleware.VersionResponseJson + jsonRestHandler.EXPECT().GetRestJsonResponse( + ctx, + "/eth/v1/node/version", + &nodeVersionResponse, + ).Return( + nil, + iface.ErrNotSupported, + ).Times(1) + + validatorClient := beaconApiValidatorClient{ + stateValidatorsProvider: stateValidatorsProvider, + prysmBeaconChainCLient: prysmBeaconChainClient{ + nodeClient: &beaconApiNodeClient{ + jsonRestHandler: jsonRestHandler, + }, + }, + } expectedValidatorStatusResponse := ethpb.MultipleValidatorStatusResponse{ PublicKeys: validatorsPubKey, @@ -289,33 +332,44 @@ func TestGetValidatorsStatusResponse_Nominal_SomeActiveValidators(t *testing.T) nil, ).Times(1) - stateValidatorsProvider.EXPECT().GetStateValidators( + jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) + + // Expect node version endpoint call. + var nodeVersionResponse apimiddleware.VersionResponseJson + jsonRestHandler.EXPECT().GetRestJsonResponse( ctx, - nil, - nil, - []string{"active"}, + "/eth/v1/node/version", + &nodeVersionResponse, ).Return( - &beacon.GetValidatorsResponse{ - Data: []*beacon.ValidatorContainer{ + nil, + nil, + ).SetArg( + 2, + apimiddleware.VersionResponseJson{Data: &apimiddleware.VersionJson{Version: "prysm/v0.0.1"}}, + ).Times(1) + + var validatorCountResponse validator2.ValidatorCountResponse + jsonRestHandler.EXPECT().GetRestJsonResponse( + ctx, + "/eth/v1/beacon/states/head/validator_count?", + &validatorCountResponse, + ).Return( + nil, + nil, + ).SetArg( + 2, + validator2.ValidatorCountResponse{ + Data: []*validator2.ValidatorCount{ { - Index: "35000", - Status: "active_ongoing", - Validator: &beacon.Validator{ - Pubkey: "0x8000ab56b051f9d8f31c687528c6e91c9b98e4c3a241e752f9ccfbea7c5a7fbbd272bdf2c0a7e52ce7e0b57693df364d", - ActivationEpoch: "56", - }, + Status: "active", + Count: "50001", }, { - Index: "39000", - Status: "active_ongoing", - Validator: &beacon.Validator{ - Pubkey: "0x8000ab56b051f9d8f31c687528c6e91c9b98e4c3a241e752f9ccfbea7c5a7fbbd272bdf2c0a7e52ce7e0b57693df364e", - ActivationEpoch: "56", - }, + Status: "pending", + Count: "11000", }, }, }, - nil, ).Times(1) wantedStringValidatorsPubkey := []string{ @@ -379,7 +433,15 @@ func TestGetValidatorsStatusResponse_Nominal_SomeActiveValidators(t *testing.T) }, } - validatorClient := beaconApiValidatorClient{stateValidatorsProvider: stateValidatorsProvider} + validatorClient := beaconApiValidatorClient{ + stateValidatorsProvider: stateValidatorsProvider, + prysmBeaconChainCLient: prysmBeaconChainClient{ + nodeClient: &beaconApiNodeClient{ + jsonRestHandler: jsonRestHandler, + }, + jsonRestHandler: jsonRestHandler, + }, + } actualValidatorsPubKey, actualValidatorsIndex, actualValidatorsStatusResponse, err := validatorClient.getValidatorsStatusResponse(ctx, validatorsPubKey, validatorsIndex) require.NoError(t, err) @@ -420,29 +482,37 @@ func TestGetValidatorsStatusResponse_Nominal_NoActiveValidators(t *testing.T) { nil, ).Times(1) - stateValidatorsProvider.EXPECT().GetStateValidators( + jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) + + // Expect node version endpoint call. + var nodeVersionResponse apimiddleware.VersionResponseJson + jsonRestHandler.EXPECT().GetRestJsonResponse( ctx, - nil, - nil, - []string{"active"}, + "/eth/v1/node/version", + &nodeVersionResponse, ).Return( - &beacon.GetValidatorsResponse{ - Data: []*beacon.ValidatorContainer{}, - }, nil, + iface.ErrNotSupported, ).Times(1) wantedValidatorsPubKey := [][]byte{validatorPubKey} wantedValidatorsIndex := []primitives.ValidatorIndex{40000} wantedValidatorsStatusResponse := []*ethpb.ValidatorStatusResponse{ { - Status: ethpb.ValidatorStatus_PENDING, - ActivationEpoch: params.BeaconConfig().FarFutureEpoch, - PositionInActivationQueue: 40000, + Status: ethpb.ValidatorStatus_PENDING, + ActivationEpoch: params.BeaconConfig().FarFutureEpoch, }, } - validatorClient := beaconApiValidatorClient{stateValidatorsProvider: stateValidatorsProvider} + validatorClient := beaconApiValidatorClient{ + stateValidatorsProvider: stateValidatorsProvider, + prysmBeaconChainCLient: prysmBeaconChainClient{ + nodeClient: &beaconApiNodeClient{ + jsonRestHandler: jsonRestHandler, + }, + jsonRestHandler: jsonRestHandler, + }, + } actualValidatorsPubKey, actualValidatorsIndex, actualValidatorsStatusResponse, err := validatorClient.getValidatorsStatusResponse(ctx, wantedValidatorsPubKey, nil) require.NoError(t, err) @@ -472,27 +542,22 @@ func TestValidatorStatusResponse_InvalidData(t *testing.T) { name string // Inputs - inputPubKeys [][]byte - inputIndexes []int64 - inputGetStateValidatorsInterfaces []getStateValidatorsInterface + inputPubKeys [][]byte + inputIndexes []int64 + inputGetStateValidatorsInterface getStateValidatorsInterface + validatorCountCalled int // Outputs outputErrMessage string }{ { - name: "failed getStateValidators", - + name: "failed getStateValidators", inputPubKeys: [][]byte{pubKey}, inputIndexes: nil, - inputGetStateValidatorsInterfaces: []getStateValidatorsInterface{ - { - inputStringPubKeys: []string{stringPubKey}, - inputIndexes: nil, - inputStatuses: nil, - - outputStateValidatorsResponseJson: &beacon.GetValidatorsResponse{}, - outputErr: errors.New("a specific error"), - }, + inputGetStateValidatorsInterface: getStateValidatorsInterface{ + inputStringPubKeys: []string{stringPubKey}, + outputStateValidatorsResponseJson: &beacon.GetValidatorsResponse{}, + outputErr: errors.New("a specific error"), }, outputErrMessage: "failed to get state validators", }, @@ -501,140 +566,117 @@ func TestValidatorStatusResponse_InvalidData(t *testing.T) { inputPubKeys: [][]byte{pubKey}, inputIndexes: nil, - inputGetStateValidatorsInterfaces: []getStateValidatorsInterface{ - { - inputStringPubKeys: []string{stringPubKey}, - inputIndexes: nil, - inputStatuses: nil, + inputGetStateValidatorsInterface: getStateValidatorsInterface{ + inputStringPubKeys: []string{stringPubKey}, + inputIndexes: nil, + inputStatuses: nil, - outputStateValidatorsResponseJson: &beacon.GetValidatorsResponse{ - Data: []*beacon.ValidatorContainer{ - { - Validator: &beacon.Validator{ - Pubkey: "NotAPublicKey", - }, + outputStateValidatorsResponseJson: &beacon.GetValidatorsResponse{ + Data: []*beacon.ValidatorContainer{ + { + Index: "0", + Validator: &beacon.Validator{ + Pubkey: "NotAPublicKey", }, }, }, - outputErr: nil, }, + outputErr: nil, }, - outputErrMessage: "failed to parse validator public key", + validatorCountCalled: 1, + outputErrMessage: "failed to parse validator public key", }, { name: "failed to parse validator index NotAnIndex", inputPubKeys: [][]byte{pubKey}, inputIndexes: nil, - inputGetStateValidatorsInterfaces: []getStateValidatorsInterface{ - { - inputStringPubKeys: []string{stringPubKey}, - inputIndexes: nil, - inputStatuses: nil, + inputGetStateValidatorsInterface: getStateValidatorsInterface{ - outputStateValidatorsResponseJson: &beacon.GetValidatorsResponse{ - Data: []*beacon.ValidatorContainer{ - { - Index: "NotAnIndex", - Validator: &beacon.Validator{ - Pubkey: stringPubKey, - }, + inputStringPubKeys: []string{stringPubKey}, + inputIndexes: nil, + inputStatuses: nil, + + outputStateValidatorsResponseJson: &beacon.GetValidatorsResponse{ + Data: []*beacon.ValidatorContainer{ + { + Index: "NotAnIndex", + Validator: &beacon.Validator{ + Pubkey: stringPubKey, }, }, }, - outputErr: nil, }, + outputErr: nil, }, - outputErrMessage: "failed to parse validator index", + validatorCountCalled: 1, + outputErrMessage: "failed to parse validator index", }, { name: "invalid validator status", inputPubKeys: [][]byte{pubKey}, inputIndexes: nil, - inputGetStateValidatorsInterfaces: []getStateValidatorsInterface{ - { - inputStringPubKeys: []string{stringPubKey}, - inputIndexes: nil, - inputStatuses: nil, + inputGetStateValidatorsInterface: getStateValidatorsInterface{ + inputStringPubKeys: []string{stringPubKey}, + inputIndexes: nil, + inputStatuses: nil, - outputStateValidatorsResponseJson: &beacon.GetValidatorsResponse{ - Data: []*beacon.ValidatorContainer{ - { - Index: "12345", - Status: "NotAStatus", - Validator: &beacon.Validator{ - Pubkey: stringPubKey, - }, + outputStateValidatorsResponseJson: &beacon.GetValidatorsResponse{ + Data: []*beacon.ValidatorContainer{ + { + Index: "12345", + Status: "NotAStatus", + Validator: &beacon.Validator{ + Pubkey: stringPubKey, }, }, }, - outputErr: nil, }, + outputErr: nil, }, - outputErrMessage: "invalid validator status NotAStatus", + validatorCountCalled: 1, + outputErrMessage: "invalid validator status NotAStatus", }, { name: "failed to parse activation epoch", inputPubKeys: [][]byte{pubKey}, inputIndexes: nil, - inputGetStateValidatorsInterfaces: []getStateValidatorsInterface{ - { - inputStringPubKeys: []string{stringPubKey}, - inputIndexes: nil, - inputStatuses: nil, + inputGetStateValidatorsInterface: getStateValidatorsInterface{ + inputStringPubKeys: []string{stringPubKey}, + inputIndexes: nil, + inputStatuses: nil, - outputStateValidatorsResponseJson: &beacon.GetValidatorsResponse{ - Data: []*beacon.ValidatorContainer{ - { - Index: "12345", - Status: "active_ongoing", - Validator: &beacon.Validator{ - Pubkey: stringPubKey, - ActivationEpoch: "NotAnEpoch", - }, + outputStateValidatorsResponseJson: &beacon.GetValidatorsResponse{ + Data: []*beacon.ValidatorContainer{ + { + Index: "12345", + Status: "active_ongoing", + Validator: &beacon.Validator{ + Pubkey: stringPubKey, + ActivationEpoch: "NotAnEpoch", }, }, }, - outputErr: nil, }, + outputErr: nil, }, - outputErrMessage: "failed to parse activation epoch NotAnEpoch", + validatorCountCalled: 1, + outputErrMessage: "failed to parse activation epoch NotAnEpoch", }, { name: "failed to get state validators", inputPubKeys: [][]byte{pubKey}, inputIndexes: nil, - inputGetStateValidatorsInterfaces: []getStateValidatorsInterface{ - { - inputStringPubKeys: []string{stringPubKey}, - inputIndexes: nil, - inputStatuses: nil, + inputGetStateValidatorsInterface: getStateValidatorsInterface{ + inputStringPubKeys: []string{stringPubKey}, + inputIndexes: nil, + inputStatuses: nil, - outputStateValidatorsResponseJson: &beacon.GetValidatorsResponse{ - Data: []*beacon.ValidatorContainer{ - { - Index: "12345", - Status: "pending_queued", - Validator: &beacon.Validator{ - Pubkey: stringPubKey, - ActivationEpoch: "10", - }, - }, - }, - }, - outputErr: nil, - }, - { - inputStringPubKeys: nil, - inputIndexes: nil, - inputStatuses: []string{"active"}, - - outputStateValidatorsResponseJson: &beacon.GetValidatorsResponse{}, - outputErr: errors.New("a specific error"), - }, + outputStateValidatorsResponseJson: nil, + outputErr: errors.New("a specific error"), }, outputErrMessage: "failed to get state validators", }, @@ -643,42 +685,22 @@ func TestValidatorStatusResponse_InvalidData(t *testing.T) { inputPubKeys: [][]byte{pubKey}, inputIndexes: nil, - inputGetStateValidatorsInterfaces: []getStateValidatorsInterface{ - { - inputStringPubKeys: []string{stringPubKey}, - inputIndexes: nil, - inputStatuses: nil, + inputGetStateValidatorsInterface: getStateValidatorsInterface{ + inputStringPubKeys: []string{stringPubKey}, + inputIndexes: nil, + inputStatuses: nil, - outputStateValidatorsResponseJson: &beacon.GetValidatorsResponse{ - Data: []*beacon.ValidatorContainer{ - { - Index: "12345", - Status: "pending_queued", - Validator: &beacon.Validator{ - Pubkey: stringPubKey, - ActivationEpoch: "10", - }, - }, + outputStateValidatorsResponseJson: &beacon.GetValidatorsResponse{ + Data: []*beacon.ValidatorContainer{ + { + Index: "NotAnIndex", }, }, - outputErr: nil, - }, - { - inputStringPubKeys: nil, - inputIndexes: nil, - inputStatuses: []string{"active"}, - - outputStateValidatorsResponseJson: &beacon.GetValidatorsResponse{ - Data: []*beacon.ValidatorContainer{ - { - Index: "NotAnIndex", - }, - }, - }, - outputErr: nil, }, + outputErr: nil, }, - outputErrMessage: "failed to parse last validator index NotAnIndex", + validatorCountCalled: 1, + outputErrMessage: "failed to parse validator index NotAnIndex", }, } @@ -690,21 +712,39 @@ func TestValidatorStatusResponse_InvalidData(t *testing.T) { ctx := context.Background() stateValidatorsProvider := mock.NewMockstateValidatorsProvider(ctrl) + stateValidatorsProvider.EXPECT().GetStateValidators( + ctx, + testCase.inputGetStateValidatorsInterface.inputStringPubKeys, + testCase.inputGetStateValidatorsInterface.inputIndexes, + testCase.inputGetStateValidatorsInterface.inputStatuses, + ).Return( + testCase.inputGetStateValidatorsInterface.outputStateValidatorsResponseJson, + testCase.inputGetStateValidatorsInterface.outputErr, + ).Times(1) - for _, aa := range testCase.inputGetStateValidatorsInterfaces { - stateValidatorsProvider.EXPECT().GetStateValidators( - ctx, - aa.inputStringPubKeys, - aa.inputIndexes, - aa.inputStatuses, - ).Return( - aa.outputStateValidatorsResponseJson, - aa.outputErr, - ).Times(1) + jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) + + // Expect node version endpoint call. + var nodeVersionResponse apimiddleware.VersionResponseJson + jsonRestHandler.EXPECT().GetRestJsonResponse( + ctx, + "/eth/v1/node/version", + &nodeVersionResponse, + ).Return( + nil, + iface.ErrNotSupported, + ).Times(testCase.validatorCountCalled) + + validatorClient := beaconApiValidatorClient{ + stateValidatorsProvider: stateValidatorsProvider, + prysmBeaconChainCLient: prysmBeaconChainClient{ + nodeClient: &beaconApiNodeClient{ + jsonRestHandler: jsonRestHandler, + }, + jsonRestHandler: jsonRestHandler, + }, } - validatorClient := beaconApiValidatorClient{stateValidatorsProvider: stateValidatorsProvider} - _, _, _, err := validatorClient.getValidatorsStatusResponse( ctx, testCase.inputPubKeys, diff --git a/validator/client/beacon-api/validator_count_test.go b/validator/client/beacon-api/validator_count_test.go new file mode 100644 index 000000000..338084e99 --- /dev/null +++ b/validator/client/beacon-api/validator_count_test.go @@ -0,0 +1,163 @@ +package beacon_api + +import ( + "context" + "errors" + "testing" + + "github.com/golang/mock/gomock" + "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/apimiddleware" + validator2 "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/prysm/validator" + "github.com/prysmaticlabs/prysm/v4/consensus-types/validator" + "github.com/prysmaticlabs/prysm/v4/testing/require" + "github.com/prysmaticlabs/prysm/v4/validator/client/beacon-api/mock" + "github.com/prysmaticlabs/prysm/v4/validator/client/iface" +) + +func TestGetValidatorCount(t *testing.T) { + const nodeVersion = "prysm/v0.0.1" + + testCases := []struct { + name string + versionEndpointError error + validatorCountEndpointError error + versionResponse apimiddleware.VersionResponseJson + validatorCountResponse validator2.ValidatorCountResponse + validatorCountCalled int + expectedResponse []iface.ValidatorCount + expectedError string + }{ + { + name: "success", + versionResponse: apimiddleware.VersionResponseJson{ + Data: &apimiddleware.VersionJson{Version: nodeVersion}, + }, + validatorCountResponse: validator2.ValidatorCountResponse{ + ExecutionOptimistic: "false", + Finalized: "true", + Data: []*validator2.ValidatorCount{ + { + Status: "active", + Count: "10", + }, + }, + }, + validatorCountCalled: 1, + expectedResponse: []iface.ValidatorCount{ + { + Status: "active", + Count: 10, + }, + }, + }, + { + name: "not supported beacon node", + versionResponse: apimiddleware.VersionResponseJson{ + Data: &apimiddleware.VersionJson{Version: "lighthouse/v0.0.1"}, + }, + expectedError: "endpoint not supported", + }, + { + name: "fails to get version", + versionEndpointError: errors.New("foo error"), + expectedError: "failed to get node version", + }, + { + name: "fails to get validator count", + versionResponse: apimiddleware.VersionResponseJson{ + Data: &apimiddleware.VersionJson{Version: nodeVersion}, + }, + validatorCountEndpointError: errors.New("foo error"), + validatorCountCalled: 1, + expectedError: "foo error", + }, + { + name: "nil validator count data", + versionResponse: apimiddleware.VersionResponseJson{ + Data: &apimiddleware.VersionJson{Version: nodeVersion}, + }, + validatorCountResponse: validator2.ValidatorCountResponse{ + ExecutionOptimistic: "false", + Finalized: "true", + Data: nil, + }, + validatorCountCalled: 1, + expectedError: "validator count data is nil", + }, + { + name: "invalid validator count", + versionResponse: apimiddleware.VersionResponseJson{ + Data: &apimiddleware.VersionJson{Version: nodeVersion}, + }, + validatorCountResponse: validator2.ValidatorCountResponse{ + ExecutionOptimistic: "false", + Finalized: "true", + Data: []*validator2.ValidatorCount{ + { + Status: "active", + Count: "10", + }, + { + Status: "exited", + Count: "10", + }, + }, + }, + validatorCountCalled: 1, + expectedError: "mismatch between validator count data and the number of statuses provided", + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) + + // Expect node version endpoint call. + var nodeVersionResponse apimiddleware.VersionResponseJson + jsonRestHandler.EXPECT().GetRestJsonResponse( + ctx, + "/eth/v1/node/version", + &nodeVersionResponse, + ).Return( + nil, + test.versionEndpointError, + ).SetArg( + 2, + test.versionResponse, + ) + + var validatorCountResponse validator2.ValidatorCountResponse + jsonRestHandler.EXPECT().GetRestJsonResponse( + ctx, + "/eth/v1/beacon/states/head/validator_count?status=active", + &validatorCountResponse, + ).Return( + nil, + test.validatorCountEndpointError, + ).SetArg( + 2, + test.validatorCountResponse, + ).Times(test.validatorCountCalled) + + // Type assertion. + var client iface.PrysmBeaconChainClient = &prysmBeaconChainClient{ + nodeClient: &beaconApiNodeClient{jsonRestHandler: jsonRestHandler}, + jsonRestHandler: jsonRestHandler, + } + + countResponse, err := client.GetValidatorCount(ctx, "head", []validator.ValidatorStatus{validator.Active}) + + if len(test.expectedResponse) == 0 { + require.ErrorContains(t, test.expectedError, err) + } else { + require.NoError(t, err) + require.DeepEqual(t, test.expectedResponse, countResponse) + } + }) + } + +} diff --git a/validator/client/beacon-chain-client-factory/BUILD.bazel b/validator/client/beacon-chain-client-factory/BUILD.bazel index 369c12271..0f3e1ba31 100644 --- a/validator/client/beacon-chain-client-factory/BUILD.bazel +++ b/validator/client/beacon-chain-client-factory/BUILD.bazel @@ -10,6 +10,7 @@ go_library( "//validator/client/beacon-api:go_default_library", "//validator/client/grpc-api:go_default_library", "//validator/client/iface:go_default_library", + "//validator/client/node-client-factory:go_default_library", "//validator/helpers:go_default_library", ], ) diff --git a/validator/client/beacon-chain-client-factory/beacon_chain_client_factory.go b/validator/client/beacon-chain-client-factory/beacon_chain_client_factory.go index ddddeabcd..0cc5d8885 100644 --- a/validator/client/beacon-chain-client-factory/beacon_chain_client_factory.go +++ b/validator/client/beacon-chain-client-factory/beacon_chain_client_factory.go @@ -5,6 +5,7 @@ import ( beaconApi "github.com/prysmaticlabs/prysm/v4/validator/client/beacon-api" grpcApi "github.com/prysmaticlabs/prysm/v4/validator/client/grpc-api" "github.com/prysmaticlabs/prysm/v4/validator/client/iface" + nodeClientFactory "github.com/prysmaticlabs/prysm/v4/validator/client/node-client-factory" validatorHelpers "github.com/prysmaticlabs/prysm/v4/validator/helpers" ) @@ -13,8 +14,26 @@ func NewBeaconChainClient(validatorConn validatorHelpers.NodeConnection) iface.B featureFlags := features.Get() if featureFlags.EnableBeaconRESTApi { - return beaconApi.NewBeaconApiBeaconChainClientWithFallback(validatorConn.GetBeaconApiUrl(), validatorConn.GetBeaconApiTimeout(), grpcClient) + return beaconApi.NewBeaconApiBeaconChainClientWithFallback( + validatorConn.GetBeaconApiUrl(), + validatorConn.GetBeaconApiTimeout(), + grpcClient, + ) } else { return grpcClient } } + +func NewPrysmBeaconClient(validatorConn validatorHelpers.NodeConnection) iface.PrysmBeaconChainClient { + featureFlags := features.Get() + + if featureFlags.EnableBeaconRESTApi { + return beaconApi.NewPrysmBeaconChainClient( + validatorConn.GetBeaconApiUrl(), + validatorConn.GetBeaconApiTimeout(), + nodeClientFactory.NewNodeClient(validatorConn), + ) + } else { + return grpcApi.NewGrpcPrysmBeaconChainClient(validatorConn.GetGrpcClientConn()) + } +} diff --git a/validator/client/grpc-api/BUILD.bazel b/validator/client/grpc-api/BUILD.bazel index 048ac8211..8558091f7 100644 --- a/validator/client/grpc-api/BUILD.bazel +++ b/validator/client/grpc-api/BUILD.bazel @@ -5,12 +5,17 @@ go_library( srcs = [ "grpc_beacon_chain_client.go", "grpc_node_client.go", + "grpc_prysm_beacon_chain_client.go", "grpc_validator_client.go", ], importpath = "github.com/prysmaticlabs/prysm/v4/validator/client/grpc-api", visibility = ["//validator:__subpackages__"], deps = [ + "//beacon-chain/rpc/eth/helpers:go_default_library", + "//beacon-chain/state/state-native:go_default_library", "//consensus-types/primitives:go_default_library", + "//consensus-types/validator:go_default_library", + "//proto/eth/v1:go_default_library", "//proto/prysm/v1alpha1:go_default_library", "//validator/client/iface:go_default_library", "@com_github_golang_protobuf//ptypes/empty", @@ -22,11 +27,22 @@ go_library( go_test( name = "go_default_test", size = "small", - srcs = ["grpc_validator_client_test.go"], + srcs = [ + "grpc_prysm_beacon_chain_client_test.go", + "grpc_validator_client_test.go", + ], embed = [":go_default_library"], deps = [ + "//config/params:go_default_library", + "//consensus-types/primitives:go_default_library", + "//consensus-types/validator:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", "//testing/assert:go_default_library", "//testing/mock:go_default_library", + "//testing/require:go_default_library", + "//testing/util:go_default_library", + "//testing/validator-mock:go_default_library", + "//validator/client/iface:go_default_library", "@com_github_golang_mock//gomock:go_default_library", "@org_golang_google_protobuf//types/known/emptypb:go_default_library", ], diff --git a/validator/client/grpc-api/grpc_prysm_beacon_chain_client.go b/validator/client/grpc-api/grpc_prysm_beacon_chain_client.go new file mode 100644 index 000000000..1cf00bc22 --- /dev/null +++ b/validator/client/grpc-api/grpc_prysm_beacon_chain_client.go @@ -0,0 +1,97 @@ +package grpc_api + +import ( + "context" + "fmt" + "sort" + + "github.com/golang/protobuf/ptypes/empty" + + "github.com/pkg/errors" + "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" + eth "github.com/prysmaticlabs/prysm/v4/proto/eth/v1" + ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v4/validator/client/iface" + "google.golang.org/grpc" +) + +type grpcPrysmBeaconChainClient struct { + beaconChainClient iface.BeaconChainClient +} + +func (g grpcPrysmBeaconChainClient) GetValidatorCount(ctx context.Context, _ string, statuses []validator.ValidatorStatus) ([]iface.ValidatorCount, error) { + resp, err := g.beaconChainClient.ListValidators(ctx, ðpb.ListValidatorsRequest{PageSize: 0}) + if err != nil { + return nil, errors.Wrap(err, "list validators failed") + } + + var vals []*ethpb.Validator + for _, val := range resp.ValidatorList { + vals = append(vals, val.Validator) + } + + head, err := g.beaconChainClient.GetChainHead(ctx, &empty.Empty{}) + if err != nil { + return nil, errors.Wrap(err, "get chain head") + } + + if len(statuses) == 0 { + for _, val := range eth.ValidatorStatus_value { + statuses = append(statuses, validator.ValidatorStatus(val)) + } + } + + valCount, err := validatorCountByStatus(vals, statuses, head.HeadEpoch) + if err != nil { + return nil, errors.Wrap(err, "validator count by status") + } + + return valCount, nil +} + +// validatorCountByStatus returns a slice of validator count for each status in the given epoch. +func validatorCountByStatus(validators []*ethpb.Validator, statuses []validator.ValidatorStatus, epoch primitives.Epoch) ([]iface.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 []iface.ValidatorCount + for status, count := range countByStatus { + resp = append(resp, iface.ValidatorCount{ + Status: status.String(), + Count: count, + }) + } + + // 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 +} + +func NewGrpcPrysmBeaconChainClient(cc grpc.ClientConnInterface) iface.PrysmBeaconChainClient { + return &grpcPrysmBeaconChainClient{beaconChainClient: &grpcBeaconChainClient{ethpb.NewBeaconChainClient(cc)}} +} diff --git a/validator/client/grpc-api/grpc_prysm_beacon_chain_client_test.go b/validator/client/grpc-api/grpc_prysm_beacon_chain_client_test.go new file mode 100644 index 000000000..1cae805e3 --- /dev/null +++ b/validator/client/grpc-api/grpc_prysm_beacon_chain_client_test.go @@ -0,0 +1,326 @@ +package grpc_api + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/prysmaticlabs/prysm/v4/config/params" + "github.com/prysmaticlabs/prysm/v4/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v4/consensus-types/validator" + ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v4/testing/require" + "github.com/prysmaticlabs/prysm/v4/testing/util" + mock "github.com/prysmaticlabs/prysm/v4/testing/validator-mock" + "github.com/prysmaticlabs/prysm/v4/validator/client/iface" +) + +func TestGetValidatorCount(t *testing.T) { + st, _ := util.DeterministicGenesisState(t, 10) + farFutureEpoch := params.BeaconConfig().FarFutureEpoch + validators := []*ethpb.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 + statuses []string + currentEpoch int + expectedResponse []iface.ValidatorCount + }{ + { + name: "Head count active validators", + statuses: []string{"active"}, + expectedResponse: []iface.ValidatorCount{ + { + Status: "active", + Count: 13, + }, + }, + }, + { + name: "Head count active ongoing validators", + statuses: []string{"active_ongoing"}, + expectedResponse: []iface.ValidatorCount{ + { + Status: "active_ongoing", + Count: 11, + }, + }, + }, + { + name: "Head count active exiting validators", + statuses: []string{"active_exiting"}, + expectedResponse: []iface.ValidatorCount{ + { + Status: "active_exiting", + Count: 1, + }, + }, + }, + { + name: "Head count active slashed validators", + statuses: []string{"active_slashed"}, + expectedResponse: []iface.ValidatorCount{ + { + Status: "active_slashed", + Count: 1, + }, + }, + }, + { + name: "Head count pending validators", + statuses: []string{"pending"}, + expectedResponse: []iface.ValidatorCount{ + { + Status: "pending", + Count: 6, + }, + }, + }, + { + name: "Head count pending initialized validators", + statuses: []string{"pending_initialized"}, + expectedResponse: []iface.ValidatorCount{ + { + Status: "pending_initialized", + Count: 1, + }, + }, + }, + { + name: "Head count pending queued validators", + statuses: []string{"pending_queued"}, + expectedResponse: []iface.ValidatorCount{ + { + Status: "pending_queued", + Count: 5, + }, + }, + }, + { + name: "Head count exited validators", + statuses: []string{"exited"}, + currentEpoch: 35, + expectedResponse: []iface.ValidatorCount{ + { + Status: "exited", + Count: 6, + }, + }, + }, + { + name: "Head count exited slashed validators", + statuses: []string{"exited_slashed"}, + currentEpoch: 35, + expectedResponse: []iface.ValidatorCount{ + { + Status: "exited_slashed", + Count: 2, + }, + }, + }, + { + name: "Head count exited unslashed validators", + statuses: []string{"exited_unslashed"}, + currentEpoch: 35, + expectedResponse: []iface.ValidatorCount{ + { + Status: "exited_unslashed", + Count: 4, + }, + }, + }, + { + name: "Head count withdrawal validators", + statuses: []string{"withdrawal"}, + currentEpoch: 45, + expectedResponse: []iface.ValidatorCount{ + { + Status: "withdrawal", + Count: 2, + }, + }, + }, + { + name: "Head count withdrawal possible validators", + statuses: []string{"withdrawal_possible"}, + currentEpoch: 45, + expectedResponse: []iface.ValidatorCount{ + { + Status: "withdrawal_possible", + Count: 1, + }, + }, + }, + { + name: "Head count withdrawal done validators", + statuses: []string{"withdrawal_done"}, + currentEpoch: 45, + expectedResponse: []iface.ValidatorCount{ + { + Status: "withdrawal_done", + Count: 1, + }, + }, + }, + { + name: "Head count active and pending validators", + statuses: []string{"active", "pending"}, + expectedResponse: []iface.ValidatorCount{ + { + Status: "active", + Count: 13, + }, + { + Status: "pending", + Count: 6, + }, + }, + }, + { + name: "Head count of ALL validators", + expectedResponse: []iface.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) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + listValidatorResp := ðpb.Validators{} + for _, val := range st.Validators() { + listValidatorResp.ValidatorList = append(listValidatorResp.ValidatorList, ðpb.Validators_ValidatorContainer{ + Validator: val, + }) + } + + beaconChainClient := mock.NewMockBeaconChainClient(ctrl) + beaconChainClient.EXPECT().ListValidators( + gomock.Any(), + gomock.Any(), + ).Return( + listValidatorResp, + nil, + ) + + beaconChainClient.EXPECT().GetChainHead( + gomock.Any(), + gomock.Any(), + ).Return( + ðpb.ChainHead{HeadEpoch: primitives.Epoch(test.currentEpoch)}, + nil, + ) + + prysmBeaconChainClient := &grpcPrysmBeaconChainClient{ + beaconChainClient: beaconChainClient, + } + + var statuses []validator.ValidatorStatus + for _, status := range test.statuses { + ok, valStatus := validator.ValidatorStatusFromString(status) + require.Equal(t, true, ok) + statuses = append(statuses, valStatus) + } + vcCountResp, err := prysmBeaconChainClient.GetValidatorCount(context.Background(), "", statuses) + require.NoError(t, err) + require.DeepEqual(t, test.expectedResponse, vcCountResp) + }) + } +} diff --git a/validator/client/iface/BUILD.bazel b/validator/client/iface/BUILD.bazel index 486feb163..4acfedda9 100644 --- a/validator/client/iface/BUILD.bazel +++ b/validator/client/iface/BUILD.bazel @@ -5,19 +5,22 @@ go_library( srcs = [ "beacon_chain_client.go", "node_client.go", + "prysm_beacon_chain_client.go", "validator.go", "validator_client.go", ], importpath = "github.com/prysmaticlabs/prysm/v4/validator/client/iface", - visibility = ["//validator:__subpackages__"], + visibility = ["//visibility:public"], deps = [ "//config/fieldparams:go_default_library", "//config/validator/service:go_default_library", "//consensus-types/primitives:go_default_library", + "//consensus-types/validator:go_default_library", "//crypto/bls:go_default_library", "//proto/prysm/v1alpha1:go_default_library", "//proto/prysm/v1alpha1/validator-client:go_default_library", "//validator/keymanager:go_default_library", "@com_github_golang_protobuf//ptypes/empty", + "@com_github_pkg_errors//:go_default_library", ], ) diff --git a/validator/client/iface/prysm_beacon_chain_client.go b/validator/client/iface/prysm_beacon_chain_client.go new file mode 100644 index 000000000..dbd1037dc --- /dev/null +++ b/validator/client/iface/prysm_beacon_chain_client.go @@ -0,0 +1,20 @@ +package iface + +import ( + "context" + + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v4/consensus-types/validator" +) + +var ErrNotSupported = errors.New("endpoint not supported") + +type ValidatorCount struct { + Status string + Count uint64 +} + +// PrysmBeaconChainClient defines an interface required to implement all the prysm specific custom endpoints. +type PrysmBeaconChainClient interface { + GetValidatorCount(context.Context, string, []validator.ValidatorStatus) ([]ValidatorCount, error) +} diff --git a/validator/client/key_reload.go b/validator/client/key_reload.go index 628bf5033..19f65997e 100644 --- a/validator/client/key_reload.go +++ b/validator/client/key_reload.go @@ -4,7 +4,10 @@ import ( "context" "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v4/validator/client/iface" + fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams" + validator2 "github.com/prysmaticlabs/prysm/v4/consensus-types/validator" eth "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1" "go.opencensus.io/trace" ) @@ -33,11 +36,19 @@ func (v *validator) HandleKeyReload(ctx context.Context, currentKeys [][fieldpar index: resp.Indices[i], } } - vals, err := v.beaconClient.ListValidators(ctx, ð.ListValidatorsRequest{Active: true, PageSize: 0}) - if err != nil { + + // "-1" indicates that validator count endpoint is not supported by the beacon node. + var valCount int64 = -1 + valCounts, err := v.prysmBeaconClient.GetValidatorCount(ctx, "head", []validator2.ValidatorStatus{validator2.Active}) + if err != nil && !errors.Is(err, iface.ErrNotSupported) { return false, errors.Wrap(err, "could not get active validator count") } - anyActive = v.checkAndLogValidatorStatus(statuses, uint64(vals.TotalSize)) + + if len(valCounts) > 0 { + valCount = int64(valCounts[0].Count) + } + + anyActive = v.checkAndLogValidatorStatus(statuses, valCount) if anyActive { logActiveValidatorStatus(statuses) } diff --git a/validator/client/key_reload_test.go b/validator/client/key_reload_test.go index 0aba32ab4..dd9b9272f 100644 --- a/validator/client/key_reload_test.go +++ b/validator/client/key_reload_test.go @@ -4,6 +4,9 @@ import ( "context" "testing" + validator2 "github.com/prysmaticlabs/prysm/v4/consensus-types/validator" + "github.com/prysmaticlabs/prysm/v4/validator/client/iface" + "github.com/golang/mock/gomock" "github.com/pkg/errors" fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams" @@ -27,11 +30,13 @@ func TestValidator_HandleKeyReload(t *testing.T) { client := validatormock.NewMockValidatorClient(ctrl) beaconClient := validatormock.NewMockBeaconChainClient(ctrl) + prysmBeaconClient := validatormock.NewMockPrysmBeaconChainClient(ctrl) v := validator{ - validatorClient: client, - keyManager: newMockKeymanager(t, inactive), - genesisTime: 1, - beaconClient: beaconClient, + validatorClient: client, + keyManager: newMockKeymanager(t, inactive), + genesisTime: 1, + beaconClient: beaconClient, + prysmBeaconClient: prysmBeaconClient, } resp := testutil.GenerateMultipleValidatorStatusResponse([][]byte{inactive.pub[:], active.pub[:]}) @@ -43,7 +48,11 @@ func TestValidator_HandleKeyReload(t *testing.T) { PublicKeys: [][]byte{inactive.pub[:], active.pub[:]}, }, ).Return(resp, nil) - beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(ðpb.Validators{}, nil) + prysmBeaconClient.EXPECT().GetValidatorCount( + gomock.Any(), + "head", + []validator2.ValidatorStatus{validator2.Active}, + ).Return([]iface.ValidatorCount{}, nil) anyActive, err := v.HandleKeyReload(context.Background(), [][fieldparams.BLSPubkeyLength]byte{inactive.pub, active.pub}) require.NoError(t, err) @@ -57,12 +66,14 @@ func TestValidator_HandleKeyReload(t *testing.T) { client := validatormock.NewMockValidatorClient(ctrl) beaconClient := validatormock.NewMockBeaconChainClient(ctrl) + prysmBeaconClient := validatormock.NewMockPrysmBeaconChainClient(ctrl) kp := randKeypair(t) v := validator{ - validatorClient: client, - keyManager: newMockKeymanager(t, kp), - genesisTime: 1, - beaconClient: beaconClient, + validatorClient: client, + keyManager: newMockKeymanager(t, kp), + genesisTime: 1, + beaconClient: beaconClient, + prysmBeaconClient: prysmBeaconClient, } resp := testutil.GenerateMultipleValidatorStatusResponse([][]byte{kp.pub[:]}) @@ -73,7 +84,11 @@ func TestValidator_HandleKeyReload(t *testing.T) { PublicKeys: [][]byte{kp.pub[:]}, }, ).Return(resp, nil) - beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(ðpb.Validators{}, nil) + prysmBeaconClient.EXPECT().GetValidatorCount( + gomock.Any(), + "head", + []validator2.ValidatorStatus{validator2.Active}, + ).Return([]iface.ValidatorCount{}, nil) anyActive, err := v.HandleKeyReload(context.Background(), [][fieldparams.BLSPubkeyLength]byte{kp.pub}) require.NoError(t, err) diff --git a/validator/client/service.go b/validator/client/service.go index 39d58672c..c0ec403a7 100644 --- a/validator/client/service.go +++ b/validator/client/service.go @@ -189,6 +189,7 @@ func (v *ValidatorService) Start() { validatorClient := validatorClientFactory.NewValidatorClient(v.conn) beaconClient := beaconChainClientFactory.NewBeaconChainClient(v.conn) + prysmBeaconClient := beaconChainClientFactory.NewPrysmBeaconClient(v.conn) valStruct := &validator{ db: v.db, @@ -218,6 +219,7 @@ func (v *ValidatorService) Start() { Web3SignerConfig: v.Web3SignerConfig, proposerSettings: v.proposerSettings, walletInitializedChannel: make(chan *wallet.Wallet, 1), + prysmBeaconClient: prysmBeaconClient, } // To resolve a race condition at startup due to the interface diff --git a/validator/client/validator.go b/validator/client/validator.go index 10f580ac0..35bb68f55 100644 --- a/validator/client/validator.go +++ b/validator/client/validator.go @@ -104,6 +104,7 @@ type validator struct { Web3SignerConfig *remoteweb3signer.SetupConfig proposerSettings *validatorserviceconfig.ProposerSettings walletInitializedChannel chan *wallet.Wallet + prysmBeaconClient iface.PrysmBeaconChainClient } type validatorStatus struct { @@ -369,10 +370,7 @@ func (v *validator) ReceiveBlocks(ctx context.Context, connectionErrorChannel ch } } -func (v *validator) checkAndLogValidatorStatus(statuses []*validatorStatus, activeValCount uint64) bool { - activationsPerEpoch := - uint64(math.Max(float64(params.BeaconConfig().MinPerEpochChurnLimit), float64(activeValCount/params.BeaconConfig().ChurnLimitQuotient))) - +func (v *validator) checkAndLogValidatorStatus(statuses []*validatorStatus, activeValCount int64) bool { nonexistentIndex := primitives.ValidatorIndex(^uint64(0)) var validatorActivated bool for _, status := range statuses { @@ -398,15 +396,17 @@ func (v *validator) checkAndLogValidatorStatus(statuses []*validatorStatus, acti ).Info("Deposit processed, entering activation queue after finalization") } case ethpb.ValidatorStatus_PENDING: - secondsPerEpoch := uint64(params.BeaconConfig().SlotsPerEpoch.Mul(params.BeaconConfig().SecondsPerSlot)) - expectedWaitingTime := - time.Duration((status.status.PositionInActivationQueue+activationsPerEpoch)/activationsPerEpoch*secondsPerEpoch) * time.Second - if status.status.ActivationEpoch == params.BeaconConfig().FarFutureEpoch { + if activeValCount >= 0 && status.status.ActivationEpoch == params.BeaconConfig().FarFutureEpoch { + activationsPerEpoch := + uint64(math.Max(float64(params.BeaconConfig().MinPerEpochChurnLimit), float64(uint64(activeValCount)/params.BeaconConfig().ChurnLimitQuotient))) + secondsPerEpoch := uint64(params.BeaconConfig().SlotsPerEpoch.Mul(params.BeaconConfig().SecondsPerSlot)) + expectedWaitingTime := + time.Duration((status.status.PositionInActivationQueue+activationsPerEpoch)/activationsPerEpoch*secondsPerEpoch) * time.Second log.WithFields(logrus.Fields{ "positionInActivationQueue": status.status.PositionInActivationQueue, "expectedWaitingTime": expectedWaitingTime.String(), }).Info("Waiting to be assigned activation epoch") - } else { + } else if status.status.ActivationEpoch != params.BeaconConfig().FarFutureEpoch { log.WithFields(logrus.Fields{ "activationEpoch": status.status.ActivationEpoch, }).Info("Waiting for activation") diff --git a/validator/client/validator_test.go b/validator/client/validator_test.go index 3ac31f5a4..a9920f233 100644 --- a/validator/client/validator_test.go +++ b/validator/client/validator_test.go @@ -358,12 +358,14 @@ func TestWaitMultipleActivation_LogsActivationEpochOK(t *testing.T) { defer ctrl.Finish() validatorClient := validatormock.NewMockValidatorClient(ctrl) beaconClient := validatormock.NewMockBeaconChainClient(ctrl) + prysmBeaconClient := validatormock.NewMockPrysmBeaconChainClient(ctrl) kp := randKeypair(t) v := validator{ - validatorClient: validatorClient, - keyManager: newMockKeymanager(t, kp), - beaconClient: beaconClient, + validatorClient: validatorClient, + keyManager: newMockKeymanager(t, kp), + beaconClient: beaconClient, + prysmBeaconClient: prysmBeaconClient, } resp := generateMockStatusResponse([][]byte{kp.pub[:]}) @@ -379,7 +381,11 @@ func TestWaitMultipleActivation_LogsActivationEpochOK(t *testing.T) { resp, nil, ) - beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(ðpb.Validators{}, nil) + prysmBeaconClient.EXPECT().GetValidatorCount( + gomock.Any(), + "head", + []validatorType.ValidatorStatus{validatorType.Active}, + ).Return([]iface.ValidatorCount{}, nil) require.NoError(t, v.WaitForActivation(ctx, nil), "Could not wait for activation") require.LogsContain(t, hook, "Validator activated") } @@ -389,12 +395,14 @@ func TestWaitActivation_NotAllValidatorsActivatedOK(t *testing.T) { defer ctrl.Finish() validatorClient := validatormock.NewMockValidatorClient(ctrl) beaconClient := validatormock.NewMockBeaconChainClient(ctrl) + prysmBeaconClient := validatormock.NewMockPrysmBeaconChainClient(ctrl) kp := randKeypair(t) v := validator{ - validatorClient: validatorClient, - keyManager: newMockKeymanager(t, kp), - beaconClient: beaconClient, + validatorClient: validatorClient, + keyManager: newMockKeymanager(t, kp), + beaconClient: beaconClient, + prysmBeaconClient: prysmBeaconClient, } resp := generateMockStatusResponse([][]byte{kp.pub[:]}) resp.Statuses[0].Status.Status = ethpb.ValidatorStatus_ACTIVE @@ -403,7 +411,11 @@ func TestWaitActivation_NotAllValidatorsActivatedOK(t *testing.T) { gomock.Any(), gomock.Any(), ).Return(clientStream, nil) - beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(ðpb.Validators{}, nil).Times(2) + prysmBeaconClient.EXPECT().GetValidatorCount( + gomock.Any(), + "head", + []validatorType.ValidatorStatus{validatorType.Active}, + ).Return([]iface.ValidatorCount{}, nil).Times(2) clientStream.EXPECT().Recv().Return( ðpb.ValidatorActivationResponse{}, nil, diff --git a/validator/client/wait_for_activation.go b/validator/client/wait_for_activation.go index d1f4a96d5..017fa6024 100644 --- a/validator/client/wait_for_activation.go +++ b/validator/client/wait_for_activation.go @@ -5,6 +5,9 @@ import ( "io" "time" + validator2 "github.com/prysmaticlabs/prysm/v4/consensus-types/validator" + "github.com/prysmaticlabs/prysm/v4/validator/client/iface" + "github.com/pkg/errors" fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams" "github.com/prysmaticlabs/prysm/v4/config/params" @@ -136,12 +139,18 @@ func (v *validator) handleAccountsChanged(ctx context.Context, accountsChangedCh } } - vals, err := v.beaconClient.ListValidators(ctx, ðpb.ListValidatorsRequest{Active: true, PageSize: 0}) - if err != nil { + // "-1" indicates that validator count endpoint is not supported by the beacon node. + var valCount int64 = -1 + valCounts, err := v.prysmBeaconClient.GetValidatorCount(ctx, "head", []validator2.ValidatorStatus{validator2.Active}) + if err != nil && !errors.Is(err, iface.ErrNotSupported) { return errors.Wrap(err, "could not get active validator count") } - valActivated := v.checkAndLogValidatorStatus(statuses, uint64(vals.TotalSize)) + if len(valCounts) > 0 { + valCount = int64(valCounts[0].Count) + } + + valActivated := v.checkAndLogValidatorStatus(statuses, valCount) if valActivated { logActiveValidatorStatus(statuses) } else { diff --git a/validator/client/wait_for_activation_test.go b/validator/client/wait_for_activation_test.go index 18b64def0..da2dccdee 100644 --- a/validator/client/wait_for_activation_test.go +++ b/validator/client/wait_for_activation_test.go @@ -7,6 +7,9 @@ import ( "testing" "time" + validatorType "github.com/prysmaticlabs/prysm/v4/consensus-types/validator" + "github.com/prysmaticlabs/prysm/v4/validator/client/iface" + "github.com/golang/mock/gomock" "github.com/pkg/errors" fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams" @@ -57,11 +60,13 @@ func TestWaitActivation_StreamSetupFails_AttemptsToReconnect(t *testing.T) { defer ctrl.Finish() validatorClient := validatormock.NewMockValidatorClient(ctrl) beaconClient := validatormock.NewMockBeaconChainClient(ctrl) + prysmBeaconClient := validatormock.NewMockPrysmBeaconChainClient(ctrl) kp := randKeypair(t) v := validator{ - validatorClient: validatorClient, - keyManager: newMockKeymanager(t, kp), - beaconClient: beaconClient, + validatorClient: validatorClient, + keyManager: newMockKeymanager(t, kp), + beaconClient: beaconClient, + prysmBeaconClient: prysmBeaconClient, } clientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl) validatorClient.EXPECT().WaitForActivation( @@ -70,7 +75,11 @@ func TestWaitActivation_StreamSetupFails_AttemptsToReconnect(t *testing.T) { PublicKeys: [][]byte{kp.pub[:]}, }, ).Return(clientStream, errors.New("failed stream")).Return(clientStream, nil) - beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(ðpb.Validators{}, nil) + prysmBeaconClient.EXPECT().GetValidatorCount( + gomock.Any(), + "head", + []validatorType.ValidatorStatus{validatorType.Active}, + ).Return([]iface.ValidatorCount{}, nil) resp := generateMockStatusResponse([][]byte{kp.pub[:]}) resp.Statuses[0].Status.Status = ethpb.ValidatorStatus_ACTIVE clientStream.EXPECT().Recv().Return(resp, nil) @@ -82,11 +91,13 @@ func TestWaitForActivation_ReceiveErrorFromStream_AttemptsReconnection(t *testin defer ctrl.Finish() validatorClient := validatormock.NewMockValidatorClient(ctrl) beaconClient := validatormock.NewMockBeaconChainClient(ctrl) + prysmBeaconClient := validatormock.NewMockPrysmBeaconChainClient(ctrl) kp := randKeypair(t) v := validator{ - validatorClient: validatorClient, - keyManager: newMockKeymanager(t, kp), - beaconClient: beaconClient, + validatorClient: validatorClient, + keyManager: newMockKeymanager(t, kp), + beaconClient: beaconClient, + prysmBeaconClient: prysmBeaconClient, } clientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl) validatorClient.EXPECT().WaitForActivation( @@ -95,7 +106,11 @@ func TestWaitForActivation_ReceiveErrorFromStream_AttemptsReconnection(t *testin PublicKeys: [][]byte{kp.pub[:]}, }, ).Return(clientStream, nil) - beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(ðpb.Validators{}, nil) + prysmBeaconClient.EXPECT().GetValidatorCount( + gomock.Any(), + "head", + []validatorType.ValidatorStatus{validatorType.Active}, + ).Return([]iface.ValidatorCount{}, nil) // A stream fails the first time, but succeeds the second time. resp := generateMockStatusResponse([][]byte{kp.pub[:]}) resp.Statuses[0].Status.Status = ethpb.ValidatorStatus_ACTIVE @@ -112,12 +127,14 @@ func TestWaitActivation_LogsActivationEpochOK(t *testing.T) { defer ctrl.Finish() validatorClient := validatormock.NewMockValidatorClient(ctrl) beaconClient := validatormock.NewMockBeaconChainClient(ctrl) + prysmBeaconClient := validatormock.NewMockPrysmBeaconChainClient(ctrl) kp := randKeypair(t) v := validator{ - validatorClient: validatorClient, - keyManager: newMockKeymanager(t, kp), - genesisTime: 1, - beaconClient: beaconClient, + validatorClient: validatorClient, + keyManager: newMockKeymanager(t, kp), + genesisTime: 1, + beaconClient: beaconClient, + prysmBeaconClient: prysmBeaconClient, } resp := generateMockStatusResponse([][]byte{kp.pub[:]}) resp.Statuses[0].Status.Status = ethpb.ValidatorStatus_ACTIVE @@ -128,7 +145,11 @@ func TestWaitActivation_LogsActivationEpochOK(t *testing.T) { PublicKeys: [][]byte{kp.pub[:]}, }, ).Return(clientStream, nil) - beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(ðpb.Validators{}, nil) + prysmBeaconClient.EXPECT().GetValidatorCount( + gomock.Any(), + "head", + []validatorType.ValidatorStatus{validatorType.Active}, + ).Return([]iface.ValidatorCount{}, nil) clientStream.EXPECT().Recv().Return( resp, nil, @@ -142,11 +163,13 @@ func TestWaitForActivation_Exiting(t *testing.T) { defer ctrl.Finish() validatorClient := validatormock.NewMockValidatorClient(ctrl) beaconClient := validatormock.NewMockBeaconChainClient(ctrl) + prysmBeaconClient := validatormock.NewMockPrysmBeaconChainClient(ctrl) kp := randKeypair(t) v := validator{ - validatorClient: validatorClient, - keyManager: newMockKeymanager(t, kp), - beaconClient: beaconClient, + validatorClient: validatorClient, + keyManager: newMockKeymanager(t, kp), + beaconClient: beaconClient, + prysmBeaconClient: prysmBeaconClient, } resp := generateMockStatusResponse([][]byte{kp.pub[:]}) resp.Statuses[0].Status.Status = ethpb.ValidatorStatus_EXITING @@ -157,7 +180,11 @@ func TestWaitForActivation_Exiting(t *testing.T) { PublicKeys: [][]byte{kp.pub[:]}, }, ).Return(clientStream, nil) - beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(ðpb.Validators{}, nil) + prysmBeaconClient.EXPECT().GetValidatorCount( + gomock.Any(), + "head", + []validatorType.ValidatorStatus{validatorType.Active}, + ).Return([]iface.ValidatorCount{}, nil) clientStream.EXPECT().Recv().Return( resp, nil, @@ -177,15 +204,17 @@ func TestWaitForActivation_RefetchKeys(t *testing.T) { defer ctrl.Finish() validatorClient := validatormock.NewMockValidatorClient(ctrl) beaconClient := validatormock.NewMockBeaconChainClient(ctrl) + prysmBeaconClient := validatormock.NewMockPrysmBeaconChainClient(ctrl) kp := randKeypair(t) km := newMockKeymanager(t, kp) km.fetchNoKeys = true v := validator{ - validatorClient: validatorClient, - keyManager: km, - beaconClient: beaconClient, + validatorClient: validatorClient, + keyManager: km, + beaconClient: beaconClient, + prysmBeaconClient: prysmBeaconClient, } resp := generateMockStatusResponse([][]byte{kp.pub[:]}) resp.Statuses[0].Status.Status = ethpb.ValidatorStatus_ACTIVE @@ -196,7 +225,11 @@ func TestWaitForActivation_RefetchKeys(t *testing.T) { PublicKeys: [][]byte{kp.pub[:]}, }, ).Return(clientStream, nil) - beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(ðpb.Validators{}, nil) + prysmBeaconClient.EXPECT().GetValidatorCount( + gomock.Any(), + "head", + []validatorType.ValidatorStatus{validatorType.Active}, + ).Return([]iface.ValidatorCount{}, nil) clientStream.EXPECT().Recv().Return( resp, nil) @@ -217,10 +250,12 @@ func TestWaitForActivation_AccountsChanged(t *testing.T) { km := newMockKeymanager(t, inactive) validatorClient := validatormock.NewMockValidatorClient(ctrl) beaconClient := validatormock.NewMockBeaconChainClient(ctrl) + prysmBeaconClient := validatormock.NewMockPrysmBeaconChainClient(ctrl) v := validator{ - validatorClient: validatorClient, - keyManager: km, - beaconClient: beaconClient, + validatorClient: validatorClient, + keyManager: km, + beaconClient: beaconClient, + prysmBeaconClient: prysmBeaconClient, } inactiveResp := generateMockStatusResponse([][]byte{inactive.pub[:]}) inactiveResp.Statuses[0].Status.Status = ethpb.ValidatorStatus_UNKNOWN_STATUS @@ -231,7 +266,11 @@ func TestWaitForActivation_AccountsChanged(t *testing.T) { PublicKeys: [][]byte{inactive.pub[:]}, }, ).Return(inactiveClientStream, nil) - beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(ðpb.Validators{}, nil).AnyTimes() + prysmBeaconClient.EXPECT().GetValidatorCount( + gomock.Any(), + "head", + []validatorType.ValidatorStatus{validatorType.Active}, + ).Return([]iface.ValidatorCount{}, nil).AnyTimes() inactiveClientStream.EXPECT().Recv().Return( inactiveResp, nil, @@ -297,11 +336,13 @@ func TestWaitForActivation_AccountsChanged(t *testing.T) { require.NoError(t, err) validatorClient := validatormock.NewMockValidatorClient(ctrl) beaconClient := validatormock.NewMockBeaconChainClient(ctrl) + prysmBeaconClient := validatormock.NewMockPrysmBeaconChainClient(ctrl) v := validator{ - validatorClient: validatorClient, - keyManager: km, - genesisTime: 1, - beaconClient: beaconClient, + validatorClient: validatorClient, + keyManager: km, + genesisTime: 1, + beaconClient: beaconClient, + prysmBeaconClient: prysmBeaconClient, } inactiveResp := generateMockStatusResponse([][]byte{inactivePubKey[:]}) @@ -313,7 +354,11 @@ func TestWaitForActivation_AccountsChanged(t *testing.T) { PublicKeys: [][]byte{inactivePubKey[:]}, }, ).Return(inactiveClientStream, nil) - beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(ðpb.Validators{}, nil).AnyTimes() + prysmBeaconClient.EXPECT().GetValidatorCount( + gomock.Any(), + "head", + []validatorType.ValidatorStatus{validatorType.Active}, + ).Return([]iface.ValidatorCount{}, nil).AnyTimes() inactiveClientStream.EXPECT().Recv().Return( inactiveResp, nil,