From e1408deb40287f937bda07e8f70195b8e9b798d8 Mon Sep 17 00:00:00 2001 From: Manu NALEPA Date: Thu, 5 Jan 2023 00:15:23 +0100 Subject: [PATCH] Add REST implementation for `MultipleValidatorStatus` (#11786) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add REST implementation for `MultipleValidatorStatus` * Fix PR comments * Address PR comments Co-authored-by: RadosÅ‚aw Kapka Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com> --- hack/update-mockgen.sh | 3 +- validator/client/beacon-api/activation.go | 2 +- .../client/beacon-api/activation_test.go | 18 +- .../beacon-api/beacon_api_validator_client.go | 23 +- validator/client/beacon-api/index.go | 2 +- validator/client/beacon-api/index_test.go | 24 +- validator/client/beacon-api/mock/BUILD.bazel | 1 + .../beacon-api/mock/state_validators_mock.go | 50 + .../client/beacon-api/state_validators.go | 31 +- .../beacon-api/state_validators_test.go | 27 +- validator/client/beacon-api/status.go | 175 +++- validator/client/beacon-api/status_test.go | 908 ++++++++++++------ 12 files changed, 910 insertions(+), 354 deletions(-) create mode 100644 validator/client/beacon-api/mock/state_validators_mock.go diff --git a/hack/update-mockgen.sh b/hack/update-mockgen.sh index 43224b0ee..e7664acdf 100755 --- a/hack/update-mockgen.sh +++ b/hack/update-mockgen.sh @@ -69,11 +69,12 @@ goimports -w "$mock_path/." gofmt -s -w "$mock_path/." # github.com/prysmaticlabs/prysm/v3/validator/client/beacon-api -# -------------------------------------------------------- +# ------------------------------------------------------------- beacon_api_mock_path="validator/client/beacon-api/mock" beacon_api_mocks=( "$beacon_api_mock_path/genesis_mock.go genesis.go" "$beacon_api_mock_path/json_rest_handler_mock.go json_rest_handler.go" + "$beacon_api_mock_path/state_validators_mock.go state_validators.go" ) for ((i = 0; i < ${#beacon_api_mocks[@]}; i++)); do diff --git a/validator/client/beacon-api/activation.go b/validator/client/beacon-api/activation.go index ed008b115..37854a691 100644 --- a/validator/client/beacon-api/activation.go +++ b/validator/client/beacon-api/activation.go @@ -69,7 +69,7 @@ func (c *waitForActivationClient) Recv() (*ethpb.ValidatorActivationResponse, er stringTargetPubKeys[index] = stringPubKey } - stateValidators, err := c.beaconApiValidatorClient.getStateValidators(stringTargetPubKeys, nil) + stateValidators, err := c.stateValidatorsProvider.GetStateValidators(stringTargetPubKeys, nil, nil) if err != nil { return nil, errors.Wrap(err, "failed to get state validators") } diff --git a/validator/client/beacon-api/activation_test.go b/validator/client/beacon-api/activation_test.go index daeb70118..726fbb309 100644 --- a/validator/client/beacon-api/activation_test.go +++ b/validator/client/beacon-api/activation_test.go @@ -147,7 +147,11 @@ func TestActivation_Nominal(t *testing.T) { }, ).Times(1) - validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + validatorClient := beaconApiValidatorClient{ + stateValidatorsProvider: beaconApiStateValidatorsProvider{ + jsonRestHandler: jsonRestHandler, + }, + } ctx := context.Background() ctx, cancel := context.WithCancel(ctx) @@ -245,7 +249,11 @@ func TestActivation_InvalidData(t *testing.T) { }, ).Times(1) - validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + validatorClient := beaconApiValidatorClient{ + stateValidatorsProvider: beaconApiStateValidatorsProvider{ + jsonRestHandler: jsonRestHandler, + }, + } waitForActivation, err := validatorClient.WaitForActivation( context.Background(), @@ -274,7 +282,11 @@ func TestActivation_JsonResponseError(t *testing.T) { errors.New("some specific json error"), ).Times(1) - validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + validatorClient := beaconApiValidatorClient{ + stateValidatorsProvider: beaconApiStateValidatorsProvider{ + jsonRestHandler: jsonRestHandler, + }, + } waitForActivation, err := validatorClient.WaitForActivation( context.Background(), diff --git a/validator/client/beacon-api/beacon_api_validator_client.go b/validator/client/beacon-api/beacon_api_validator_client.go index 33a46a985..8008df0ee 100644 --- a/validator/client/beacon-api/beacon_api_validator_client.go +++ b/validator/client/beacon-api/beacon_api_validator_client.go @@ -14,9 +14,10 @@ import ( ) type beaconApiValidatorClient struct { - genesisProvider genesisProvider - jsonRestHandler jsonRestHandler - fallbackClient iface.ValidatorClient + genesisProvider genesisProvider + stateValidatorsProvider stateValidatorsProvider + jsonRestHandler jsonRestHandler + fallbackClient iface.ValidatorClient } func NewBeaconApiValidatorClient(host string, timeout time.Duration) iface.ValidatorClient { @@ -30,9 +31,10 @@ func NewBeaconApiValidatorClientWithFallback(host string, timeout time.Duration, } return &beaconApiValidatorClient{ - genesisProvider: beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler}, - jsonRestHandler: jsonRestHandler, - fallbackClient: fallbackClient, + genesisProvider: beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler}, + stateValidatorsProvider: beaconApiStateValidatorsProvider{jsonRestHandler: jsonRestHandler}, + jsonRestHandler: jsonRestHandler, + fallbackClient: fallbackClient, } } @@ -111,13 +113,8 @@ func (c *beaconApiValidatorClient) GetSyncSubcommitteeIndex(ctx context.Context, panic("beaconApiValidatorClient.GetSyncSubcommitteeIndex is not implemented. To use a fallback client, create this validator with NewBeaconApiValidatorClientWithFallback instead.") } -func (c *beaconApiValidatorClient) MultipleValidatorStatus(ctx context.Context, in *ethpb.MultipleValidatorStatusRequest) (*ethpb.MultipleValidatorStatusResponse, error) { - if c.fallbackClient != nil { - return c.fallbackClient.MultipleValidatorStatus(ctx, in) - } - - // TODO: Implement me - panic("beaconApiValidatorClient.MultipleValidatorStatus is not implemented. To use a fallback client, create this validator with NewBeaconApiValidatorClientWithFallback instead.") +func (c *beaconApiValidatorClient) MultipleValidatorStatus(_ context.Context, in *ethpb.MultipleValidatorStatusRequest) (*ethpb.MultipleValidatorStatusResponse, error) { + return c.multipleValidatorStatus(in) } func (c *beaconApiValidatorClient) PrepareBeaconProposer(_ context.Context, in *ethpb.PrepareBeaconProposerRequest) (*empty.Empty, error) { diff --git a/validator/client/beacon-api/index.go b/validator/client/beacon-api/index.go index dc6f23e67..a75f13fc8 100644 --- a/validator/client/beacon-api/index.go +++ b/validator/client/beacon-api/index.go @@ -12,7 +12,7 @@ import ( func (c beaconApiValidatorClient) validatorIndex(in *ethpb.ValidatorIndexRequest) (*ethpb.ValidatorIndexResponse, error) { stringPubKey := hexutil.Encode(in.PublicKey) - stateValidator, err := c.getStateValidators([]string{stringPubKey}, nil) + stateValidator, err := c.stateValidatorsProvider.GetStateValidators([]string{stringPubKey}, nil, nil) if err != nil { return nil, errors.Wrap(err, "failed to get state validator") } diff --git a/validator/client/beacon-api/index_test.go b/validator/client/beacon-api/index_test.go index cbe84b686..ceca46ae3 100644 --- a/validator/client/beacon-api/index_test.go +++ b/validator/client/beacon-api/index_test.go @@ -58,7 +58,11 @@ func TestIndex_Nominal(t *testing.T) { }, ).Times(1) - validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + validatorClient := beaconApiValidatorClient{ + stateValidatorsProvider: beaconApiStateValidatorsProvider{ + jsonRestHandler: jsonRestHandler, + }, + } validatorIndex, err := validatorClient.ValidatorIndex( context.Background(), @@ -93,7 +97,11 @@ func TestIndex_UnexistingValidator(t *testing.T) { }, ).Times(1) - validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + validatorClient := beaconApiValidatorClient{ + stateValidatorsProvider: beaconApiStateValidatorsProvider{ + jsonRestHandler: jsonRestHandler, + }, + } _, err := validatorClient.ValidatorIndex( context.Background(), @@ -136,7 +144,11 @@ func TestIndex_BadIndexError(t *testing.T) { }, ).Times(1) - validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + validatorClient := beaconApiValidatorClient{ + stateValidatorsProvider: beaconApiStateValidatorsProvider{ + jsonRestHandler: jsonRestHandler, + }, + } _, err := validatorClient.ValidatorIndex( context.Background(), @@ -165,7 +177,11 @@ func TestIndex_JsonResponseError(t *testing.T) { errors.New("some specific json error"), ).Times(1) - validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + validatorClient := beaconApiValidatorClient{ + stateValidatorsProvider: beaconApiStateValidatorsProvider{ + jsonRestHandler: jsonRestHandler, + }, + } _, err := validatorClient.ValidatorIndex( context.Background(), diff --git a/validator/client/beacon-api/mock/BUILD.bazel b/validator/client/beacon-api/mock/BUILD.bazel index 6a3db0d56..aafe083dc 100644 --- a/validator/client/beacon-api/mock/BUILD.bazel +++ b/validator/client/beacon-api/mock/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "genesis_mock.go", "json_rest_handler_mock.go", + "state_validators_mock.go", ], importpath = "github.com/prysmaticlabs/prysm/v3/validator/client/beacon-api/mock", visibility = ["//visibility:public"], diff --git a/validator/client/beacon-api/mock/state_validators_mock.go b/validator/client/beacon-api/mock/state_validators_mock.go new file mode 100644 index 000000000..3f067b65e --- /dev/null +++ b/validator/client/beacon-api/mock/state_validators_mock.go @@ -0,0 +1,50 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: validator/client/beacon-api/state_validators.go + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + apimiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware" +) + +// MockstateValidatorsProvider is a mock of stateValidatorsProvider interface. +type MockstateValidatorsProvider struct { + ctrl *gomock.Controller + recorder *MockstateValidatorsProviderMockRecorder +} + +// MockstateValidatorsProviderMockRecorder is the mock recorder for MockstateValidatorsProvider. +type MockstateValidatorsProviderMockRecorder struct { + mock *MockstateValidatorsProvider +} + +// NewMockstateValidatorsProvider creates a new mock instance. +func NewMockstateValidatorsProvider(ctrl *gomock.Controller) *MockstateValidatorsProvider { + mock := &MockstateValidatorsProvider{ctrl: ctrl} + mock.recorder = &MockstateValidatorsProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockstateValidatorsProvider) EXPECT() *MockstateValidatorsProviderMockRecorder { + return m.recorder +} + +// GetStateValidators mocks base method. +func (m *MockstateValidatorsProvider) GetStateValidators(arg0 []string, arg1 []int64, arg2 []string) (*apimiddleware.StateValidatorsResponseJson, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetStateValidators", arg0, arg1, arg2) + ret0, _ := ret[0].(*apimiddleware.StateValidatorsResponseJson) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetStateValidators indicates an expected call of GetStateValidators. +func (mr *MockstateValidatorsProviderMockRecorder) GetStateValidators(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStateValidators", reflect.TypeOf((*MockstateValidatorsProvider)(nil).GetStateValidators), arg0, arg1, arg2) +} diff --git a/validator/client/beacon-api/state_validators.go b/validator/client/beacon-api/state_validators.go index 63f936723..93760eead 100644 --- a/validator/client/beacon-api/state_validators.go +++ b/validator/client/beacon-api/state_validators.go @@ -2,19 +2,42 @@ package beacon_api import ( neturl "net/url" + "strconv" "github.com/pkg/errors" rpcmiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware" ) -func (c *beaconApiValidatorClient) getStateValidators( +type stateValidatorsProvider interface { + GetStateValidators([]string, []int64, []string) (*rpcmiddleware.StateValidatorsResponseJson, error) +} + +type beaconApiStateValidatorsProvider struct { + jsonRestHandler jsonRestHandler +} + +func (c beaconApiStateValidatorsProvider) GetStateValidators( stringPubkeys []string, + indexes []int64, statuses []string, ) (*rpcmiddleware.StateValidatorsResponseJson, error) { params := neturl.Values{} + stringPubKeysSet := make(map[string]struct{}, len(stringPubkeys)) + indexesSet := make(map[int64]struct{}, len(indexes)) + for _, stringPubkey := range stringPubkeys { - params.Add("id", stringPubkey) + if _, ok := stringPubKeysSet[stringPubkey]; !ok { + stringPubKeysSet[stringPubkey] = struct{}{} + params.Add("id", stringPubkey) + } + } + + for _, index := range indexes { + if _, ok := indexesSet[index]; !ok { + indexesSet[index] = struct{}{} + params.Add("id", strconv.FormatInt(index, 10)) + } } for _, status := range statuses { @@ -30,11 +53,11 @@ func (c *beaconApiValidatorClient) getStateValidators( _, err := c.jsonRestHandler.GetRestJsonResponse(url, stateValidatorsJson) if err != nil { - return nil, errors.Wrap(err, "failed to get json response") + return &rpcmiddleware.StateValidatorsResponseJson{}, errors.Wrap(err, "failed to get json response") } if stateValidatorsJson.Data == nil { - return nil, errors.New("stateValidatorsJson.Data is nil") + return &rpcmiddleware.StateValidatorsResponseJson{}, errors.New("stateValidatorsJson.Data is nil") } return stateValidatorsJson, nil diff --git a/validator/client/beacon-api/state_validators_test.go b/validator/client/beacon-api/state_validators_test.go index 823db4d75..494e15a22 100644 --- a/validator/client/beacon-api/state_validators_test.go +++ b/validator/client/beacon-api/state_validators_test.go @@ -22,6 +22,7 @@ func TestGetStateValidators_Nominal(t *testing.T) { "id=0x80000e851c0f53c3246ff726d7ff7766661ca5e12a07c45c114d208d54f0f8233d4380b2e9aff759d69795d1df905526&", // active_exiting "id=0x424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242&", // does not exist "id=0x800015473bdc3a7f45ef8eb8abc598bc20021e55ad6e6ad1d745aaef9730dd2c28ec08bf42df18451de94dd4a6d24ec5&", // exited_slashed + "id=12345&", // active_ongoing "status=active_ongoing&status=active_exiting&status=exited_slashed&status=exited_unslashed", }, "") @@ -29,6 +30,13 @@ func TestGetStateValidators_Nominal(t *testing.T) { jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) wanted := []*rpcmiddleware.ValidatorContainerJson{ + { + Index: "12345", + Status: "active_ongoing", + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be19", + }, + }, { Index: "55293", Status: "active_ongoing", @@ -65,13 +73,18 @@ func TestGetStateValidators_Nominal(t *testing.T) { }, ).Times(1) - validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} - actual, err := validatorClient.getStateValidators([]string{ + stateValidatorsProvider := beaconApiStateValidatorsProvider{jsonRestHandler: jsonRestHandler} + actual, err := stateValidatorsProvider.GetStateValidators([]string{ "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13", // active_ongoing "0x80000e851c0f53c3246ff726d7ff7766661ca5e12a07c45c114d208d54f0f8233d4380b2e9aff759d69795d1df905526", // active_exiting "0x424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242", // does not exist + "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13", // active_ongoing - duplicate "0x800015473bdc3a7f45ef8eb8abc598bc20021e55ad6e6ad1d745aaef9730dd2c28ec08bf42df18451de94dd4a6d24ec5", // exited_slashed }, + []int64{ + 12345, // active_ongoing + 12345, // active_ongoing - duplicate + }, []string{"active_ongoing", "active_exiting", "exited_slashed", "exited_unslashed"}, ) require.NoError(t, err) @@ -95,11 +108,12 @@ func TestGetStateValidators_GetRestJsonResponseOnError(t *testing.T) { errors.New("an error"), ).Times(1) - validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} - _, err := validatorClient.getStateValidators([]string{ + stateValidatorsProvider := beaconApiStateValidatorsProvider{jsonRestHandler: jsonRestHandler} + _, err := stateValidatorsProvider.GetStateValidators([]string{ "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13", // active_ongoing }, nil, + nil, ) assert.ErrorContains(t, "an error", err) assert.ErrorContains(t, "failed to get json response", err) @@ -127,11 +141,12 @@ func TestGetStateValidators_DataIsNil(t *testing.T) { }, ).Times(1) - validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} - _, err := validatorClient.getStateValidators([]string{ + stateValidatorsProvider := beaconApiStateValidatorsProvider{jsonRestHandler: jsonRestHandler} + _, err := stateValidatorsProvider.GetStateValidators([]string{ "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13", // active_ongoing }, nil, + nil, ) assert.ErrorContains(t, "stateValidatorsJson.Data is nil", err) } diff --git a/validator/client/beacon-api/status.go b/validator/client/beacon-api/status.go index ca6355f54..00bd995c9 100644 --- a/validator/client/beacon-api/status.go +++ b/validator/client/beacon-api/status.go @@ -11,69 +11,158 @@ import ( ) func (c *beaconApiValidatorClient) validatorStatus(in *ethpb.ValidatorStatusRequest) (*ethpb.ValidatorStatusResponse, error) { - stringPubKey := hexutil.Encode(in.PublicKey) - - resp := ðpb.ValidatorStatusResponse{ - Status: ethpb.ValidatorStatus_UNKNOWN_STATUS, - ActivationEpoch: params.BeaconConfig().FarFutureEpoch, - } - - stateValidator, err := c.getStateValidators([]string{stringPubKey}, nil) + _, _, validatorsStatusResponse, err := c.getValidatorsStatusResponse([][]byte{in.PublicKey}, nil) if err != nil { - return nil, errors.Wrap(err, "failed to get state validator") + return nil, errors.Wrap(err, "failed to get validator status response") } - // If no data, the validator is in unknown status - if len(stateValidator.Data) == 0 { - return resp, nil + if len(validatorsStatusResponse) != 1 { + return nil, errors.New("number of validator status response not expected") } - validatorContainer := stateValidator.Data[0] + validatorStatusResponse := validatorsStatusResponse[0] - // Set Status - status, ok := beaconAPITogRPCValidatorStatus[validatorContainer.Status] - if !ok { - return nil, errors.New("invalid validator status: " + validatorContainer.Status) - } + return validatorStatusResponse, nil +} - resp.Status = status - - // Set activation epoch - activationEpoch, err := strconv.ParseInt(validatorContainer.Validator.ActivationEpoch, 10, 64) +func (c *beaconApiValidatorClient) multipleValidatorStatus(in *ethpb.MultipleValidatorStatusRequest) (*ethpb.MultipleValidatorStatusResponse, error) { + publicKeys, indices, statuses, err := c.getValidatorsStatusResponse(in.PublicKeys, in.Indices) if err != nil { - return nil, errors.Wrap(err, "failed to parse activation epoch") + return nil, errors.Wrap(err, "failed to get validators status response") } - resp.ActivationEpoch = types.Epoch(activationEpoch) + return ðpb.MultipleValidatorStatusResponse{ + PublicKeys: publicKeys, + Indices: indices, + Statuses: statuses, + }, nil +} - // Set PositionInActivationQueue - switch status { - case ethpb.ValidatorStatus_DEPOSITED, ethpb.ValidatorStatus_PENDING, ethpb.ValidatorStatus_PARTIALLY_DEPOSITED: - validatorIndex, err := strconv.ParseUint(validatorContainer.Index, 10, 64) - if err != nil { - return nil, errors.Wrap(err, "failed to parse validator index") - } +func (c *beaconApiValidatorClient) getValidatorsStatusResponse(inPubKeys [][]byte, inIndexes []int64) ( + [][]byte, + []types.ValidatorIndex, + []*ethpb.ValidatorStatusResponse, + error, +) { + // Represents the target set of keys + stringTargetPubKeysToPubKeys := make(map[string][]byte, len(inPubKeys)) + stringTargetPubKeys := make([]string, len(inPubKeys)) - activeStateValidators, err := c.getStateValidators(nil, []string{"active"}) - if err != nil { - return nil, errors.Wrap(err, "failed to get state validators") - } + // Represents the set of keys actually returned by the beacon node + stringRetrievedPubKeys := make(map[string]struct{}) - data := activeStateValidators.Data + // Contains all keys in targetPubKeys but not in retrievedPubKeys + missingPubKeys := [][]byte{} - var lastActivatedValidatorIndex uint64 = 0 + totalLen := len(inPubKeys) + len(inIndexes) - if nbActiveValidators := len(data); nbActiveValidators != 0 { - lastValidator := data[nbActiveValidators-1] + outPubKeys := make([][]byte, totalLen) + outIndexes := make([]types.ValidatorIndex, totalLen) + outValidatorsStatuses := make([]*ethpb.ValidatorStatusResponse, totalLen) - lastActivatedValidatorIndex, err = strconv.ParseUint(lastValidator.Index, 10, 64) + for index, publicKey := range inPubKeys { + stringPubKey := hexutil.Encode(publicKey) + stringTargetPubKeysToPubKeys[stringPubKey] = publicKey + stringTargetPubKeys[index] = stringPubKey + } + + // Get state for the current validator + stateValidatorsResponse, err := c.stateValidatorsProvider.GetStateValidators(stringTargetPubKeys, inIndexes, nil) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "failed to get state validators") + } + + isLastActivatedValidatorIndexRetrieved := false + var lastActivatedValidatorIndex uint64 = 0 + + for i, validatorContainer := range stateValidatorsResponse.Data { + stringPubKey := validatorContainer.Validator.PublicKey + + stringRetrievedPubKeys[stringPubKey] = struct{}{} + + pubKey, ok := stringTargetPubKeysToPubKeys[stringPubKey] + if !ok { + // string pub key is not already known because the index was used for this validator + pubKey, err = hexutil.Decode(stringPubKey) if err != nil { - return nil, errors.Wrap(err, "failed to parse last validator index") + return nil, nil, nil, errors.Wrapf(err, "failed to parse validator public key %s", stringPubKey) } } - resp.PositionInActivationQueue = validatorIndex - lastActivatedValidatorIndex + 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] = types.ValidatorIndex(validatorIndex) + + validatorStatus := ðpb.ValidatorStatusResponse{} + + // Set Status + status, ok := beaconAPITogRPCValidatorStatus[validatorContainer.Status] + if !ok { + return nil, nil, nil, errors.New("invalid validator status " + validatorContainer.Status) + } + + validatorStatus.Status = status + + // Set activation epoch + activationEpoch, err := strconv.ParseUint(validatorContainer.Validator.ActivationEpoch, 10, 64) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "failed to parse activation epoch %s", validatorContainer.Validator.ActivationEpoch) + } + + validatorStatus.ActivationEpoch = types.Epoch(activationEpoch) + + // Set PositionInActivationQueue + switch status { + case ethpb.ValidatorStatus_PENDING, ethpb.ValidatorStatus_PARTIALLY_DEPOSITED, ethpb.ValidatorStatus_DEPOSITED: + if !isLastActivatedValidatorIndexRetrieved { + isLastActivatedValidatorIndexRetrieved = true + + activeStateValidators, err := c.stateValidatorsProvider.GetStateValidators(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) + } + } + } + + validatorStatus.PositionInActivationQueue = validatorIndex - lastActivatedValidatorIndex + } + + outValidatorsStatuses[i] = validatorStatus } - return resp, nil + for _, stringTargetPubKey := range stringTargetPubKeys { + if _, ok := stringRetrievedPubKeys[stringTargetPubKey]; !ok { + targetPubKey := stringTargetPubKeysToPubKeys[stringTargetPubKey] + missingPubKeys = append(missingPubKeys, targetPubKey) + } + } + + nbStringRetrievedPubKeys := len(stringRetrievedPubKeys) + + for i, missingPubKey := range missingPubKeys { + outPubKeys[nbStringRetrievedPubKeys+i] = missingPubKey + outIndexes[nbStringRetrievedPubKeys+i] = types.ValidatorIndex(^uint64(0)) + + outValidatorsStatuses[nbStringRetrievedPubKeys+i] = ðpb.ValidatorStatusResponse{ + Status: ethpb.ValidatorStatus_UNKNOWN_STATUS, + ActivationEpoch: params.BeaconConfig().FarFutureEpoch, + } + } + + outLen := len(stateValidatorsResponse.Data) + len(missingPubKeys) + return outPubKeys[:outLen], outIndexes[:outLen], outValidatorsStatuses[:outLen], nil } diff --git a/validator/client/beacon-api/status_test.go b/validator/client/beacon-api/status_test.go index 8d009e64a..7ee4ae57e 100644 --- a/validator/client/beacon-api/status_test.go +++ b/validator/client/beacon-api/status_test.go @@ -2,346 +2,698 @@ package beacon_api import ( "context" + "errors" "fmt" "testing" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/golang/mock/gomock" - "github.com/pkg/errors" rpcmiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware" "github.com/prysmaticlabs/prysm/v3/config/params" + types "github.com/prysmaticlabs/prysm/v3/consensus-types/primitives" ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v3/testing/assert" "github.com/prysmaticlabs/prysm/v3/testing/require" "github.com/prysmaticlabs/prysm/v3/validator/client/beacon-api/mock" ) -const defaultStringPubKey = "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13" - -type urlAndData struct { - Url string - Data []*rpcmiddleware.ValidatorContainerJson -} - -func TestValidatorsStatus_Nominal(t *testing.T) { - testCases := []struct { - name string - urlsAndData []urlAndData - wanted *ethpb.ValidatorStatusResponse - }{ - { - name: "Some active validators", - urlsAndData: []urlAndData{ - { - Url: fmt.Sprintf("/eth/v1/beacon/states/head/validators?id=%s", stringPubKey), - Data: []*rpcmiddleware.ValidatorContainerJson{ - { - Index: "55300", - Status: "pending_queued", - Validator: &rpcmiddleware.ValidatorJson{ - ActivationEpoch: "100", - }, - }, - }, - }, - { - Url: "/eth/v1/beacon/states/head/validators?status=active", - Data: []*rpcmiddleware.ValidatorContainerJson{ - { - Index: "55293", - Status: "active_ongoing", - Validator: &rpcmiddleware.ValidatorJson{}, - }, - { - Index: "55294", - Status: "active_ongoing", - Validator: &rpcmiddleware.ValidatorJson{}, - }, - }, - }, - }, - wanted: ðpb.ValidatorStatusResponse{ - Status: ethpb.ValidatorStatus_PENDING, - ActivationEpoch: 100, - PositionInActivationQueue: 6, - }, - }, - { - name: "No active validators", - urlsAndData: []urlAndData{ - { - Url: fmt.Sprintf("/eth/v1/beacon/states/head/validators?id=%s", stringPubKey), - Data: []*rpcmiddleware.ValidatorContainerJson{ - { - Index: "55300", - Status: "pending_queued", - Validator: &rpcmiddleware.ValidatorJson{ - ActivationEpoch: "100", - }, - }, - }, - }, - { - Url: "/eth/v1/beacon/states/head/validators?status=active", - Data: []*rpcmiddleware.ValidatorContainerJson{}, - }, - }, - wanted: ðpb.ValidatorStatusResponse{ - Status: ethpb.ValidatorStatus_PENDING, - ActivationEpoch: 100, - PositionInActivationQueue: 55300, - }, - }, - { - name: "Unknown status", - urlsAndData: []urlAndData{ - { - Url: fmt.Sprintf("/eth/v1/beacon/states/head/validators?id=%s", stringPubKey), - Data: []*rpcmiddleware.ValidatorContainerJson{}, - }, - }, - wanted: ðpb.ValidatorStatusResponse{ - Status: ethpb.ValidatorStatus_UNKNOWN_STATUS, - ActivationEpoch: params.BeaconConfig().FarFutureEpoch, - }, - }, - } - - pubKey, err := hexutil.Decode(defaultStringPubKey) - require.NoError(t, err) - - for _, testCase := range testCases { - t.Run(testCase.name, - func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) - - for _, urlAndData := range testCase.urlsAndData { - jsonRestHandler.EXPECT().GetRestJsonResponse( - urlAndData.Url, - gomock.Any(), - ).Return( - nil, - nil, - ).SetArg( - 1, - rpcmiddleware.StateValidatorsResponseJson{ - Data: urlAndData.Data, - }, - ).Times(1) - } - - validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} - - actual, err := validatorClient.validatorStatus( - ðpb.ValidatorStatusRequest{ - PublicKey: pubKey, - }, - ) - require.NoError(t, err) - - assert.DeepEqual(t, testCase.wanted, actual) - }, - ) - } -} - -func TestValidatorStatus_InvalidData(t *testing.T) { - testCases := []struct { - name string - data []*rpcmiddleware.ValidatorContainerJson - expectedErrorMessage string - err error - }{ - { - name: "bad validator status", - data: []*rpcmiddleware.ValidatorContainerJson{}, - expectedErrorMessage: "failed to get state validator", - err: errors.New("some specific json error"), - }, - { - name: "bad validator status", - data: []*rpcmiddleware.ValidatorContainerJson{ - { - Index: "12345", - Status: "NotAStatus", - Validator: &rpcmiddleware.ValidatorJson{ - PublicKey: stringPubKey, - }, - }, - }, - expectedErrorMessage: "invalid validator status: NotAStatus", - err: nil, - }, - { - name: "bad activation epoch", - data: []*rpcmiddleware.ValidatorContainerJson{ - { - Status: "pending_queued", - Validator: &rpcmiddleware.ValidatorJson{ - ActivationEpoch: "NotAnEpoch", - }, - }, - }, - expectedErrorMessage: "failed to parse activation epoch", - err: nil, - }, - { - name: "bad validator index", - data: []*rpcmiddleware.ValidatorContainerJson{ - { - Status: "pending_queued", - Validator: &rpcmiddleware.ValidatorJson{ - ActivationEpoch: "12345", - }, - Index: "NotAnIndex", - }, - }, - expectedErrorMessage: "failed to parse validator index", - err: nil, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, - func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) - - jsonRestHandler.EXPECT().GetRestJsonResponse( - gomock.Any(), - gomock.Any(), - ).Return( - nil, - testCase.err, - ).SetArg( - 1, - rpcmiddleware.StateValidatorsResponseJson{ - Data: testCase.data, - }, - ).Times(1) - - validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} - - _, err := validatorClient.ValidatorStatus( - context.Background(), - ðpb.ValidatorStatusRequest{}, - ) - assert.ErrorContains(t, testCase.expectedErrorMessage, err) - }, - ) - } -} - -func TestValidatorStatus_InvalidValidatorsState(t *testing.T) { - pubKey, err := hexutil.Decode(defaultStringPubKey) +func TestValidatorStatus_Nominal(t *testing.T) { + const stringValidatorPubKey = "0x8000a6c975761b488bdb0dfba4ed37c0d97d6e6b968562ef5c84aa9a5dfb92d8e309195004e97709077723739bf04463" + validatorPubKey, err := hexutil.Decode(stringValidatorPubKey) require.NoError(t, err) ctrl := gomock.NewController(t) defer ctrl.Finish() - stateValidatorsResponseJson := rpcmiddleware.StateValidatorsResponseJson{} - jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) + stateValidatorsProvider := mock.NewMockstateValidatorsProvider(ctrl) - jsonRestHandler.EXPECT().GetRestJsonResponse( - gomock.Any(), - &stateValidatorsResponseJson, + stateValidatorsProvider.EXPECT().GetStateValidators( + []string{stringValidatorPubKey}, + nil, + nil, ).Return( - nil, - nil, - ).SetArg( - 1, - rpcmiddleware.StateValidatorsResponseJson{ + &rpcmiddleware.StateValidatorsResponseJson{ Data: []*rpcmiddleware.ValidatorContainerJson{ { - Index: "55300", - Status: "pending_queued", + Index: "35000", + Status: "active_ongoing", Validator: &rpcmiddleware.ValidatorJson{ - ActivationEpoch: "100", + PublicKey: stringValidatorPubKey, + ActivationEpoch: "56", }, }, }, }, + nil, ).Times(1) - jsonRestHandler.EXPECT().GetRestJsonResponse( - "/eth/v1/beacon/states/head/validators?status=active", - &stateValidatorsResponseJson, - ).Return( + validatorClient := beaconApiValidatorClient{stateValidatorsProvider: stateValidatorsProvider} + + actualValidatorStatusResponse, err := validatorClient.ValidatorStatus( + context.Background(), + ðpb.ValidatorStatusRequest{ + PublicKey: validatorPubKey, + }, + ) + + expectedValidatorStatusResponse := ethpb.ValidatorStatusResponse{ + Status: ethpb.ValidatorStatus_ACTIVE, + ActivationEpoch: 56, + } + + require.NoError(t, err) + assert.DeepEqual(t, &expectedValidatorStatusResponse, actualValidatorStatusResponse) +} + +func TestValidatorStatus_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + stateValidatorsProvider := mock.NewMockstateValidatorsProvider(ctrl) + + stateValidatorsProvider.EXPECT().GetStateValidators( + gomock.Any(), nil, + nil, + ).Return( + &rpcmiddleware.StateValidatorsResponseJson{}, errors.New("a specific error"), ).Times(1) - validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + validatorClient := beaconApiValidatorClient{stateValidatorsProvider: stateValidatorsProvider} - _, err = validatorClient.validatorStatus( + _, err := validatorClient.ValidatorStatus( + context.Background(), ðpb.ValidatorStatusRequest{ - PublicKey: pubKey, + PublicKey: []byte{}, }, ) - require.ErrorContains(t, "failed to get state validators", err) + + require.ErrorContains(t, "failed to get validator status response", err) } -func TestValidatorStatus_InvalidLastValidatorIndex(t *testing.T) { - pubKey, err := hexutil.Decode(defaultStringPubKey) - require.NoError(t, err) +func TestMultipleValidatorStatus_Nominal(t *testing.T) { + stringValidatorsPubKey := []string{ + "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13", // existing + "0x8000a6c975761b488bdb0dfba4ed37c0d97d6e6b968562ef5c84aa9a5dfb92d8e309195004e97709077723739bf04463", // existing + } + + validatorsPubKey := make([][]byte, len(stringValidatorsPubKey)) + + for i, stringValidatorPubKey := range stringValidatorsPubKey { + validatorPubKey, err := hexutil.Decode(stringValidatorPubKey) + require.NoError(t, err) + validatorsPubKey[i] = validatorPubKey + } ctrl := gomock.NewController(t) defer ctrl.Finish() - stateValidatorsResponseJson := rpcmiddleware.StateValidatorsResponseJson{} - jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) + stateValidatorsProvider := mock.NewMockstateValidatorsProvider(ctrl) - jsonRestHandler.EXPECT().GetRestJsonResponse( - gomock.Any(), - &stateValidatorsResponseJson, + stateValidatorsProvider.EXPECT().GetStateValidators( + stringValidatorsPubKey, + nil, + nil, ).Return( - nil, - nil, - ).SetArg( - 1, - rpcmiddleware.StateValidatorsResponseJson{ + &rpcmiddleware.StateValidatorsResponseJson{ Data: []*rpcmiddleware.ValidatorContainerJson{ { - Index: "55300", - Status: "pending_queued", + Index: "11111", + Status: "active_ongoing", Validator: &rpcmiddleware.ValidatorJson{ - ActivationEpoch: "100", + PublicKey: "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13", + ActivationEpoch: "12", + }, + }, + { + Index: "22222", + Status: "active_ongoing", + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: "0x8000a6c975761b488bdb0dfba4ed37c0d97d6e6b968562ef5c84aa9a5dfb92d8e309195004e97709077723739bf04463", + ActivationEpoch: "34", }, }, }, }, + nil, ).Times(1) - jsonRestHandler.EXPECT().GetRestJsonResponse( - "/eth/v1/beacon/states/head/validators?status=active", - &stateValidatorsResponseJson, + validatorClient := beaconApiValidatorClient{stateValidatorsProvider: stateValidatorsProvider} + + expectedValidatorStatusResponse := ethpb.MultipleValidatorStatusResponse{ + PublicKeys: validatorsPubKey, + Indices: []types.ValidatorIndex{ + 11111, + 22222, + }, + Statuses: []*ethpb.ValidatorStatusResponse{ + { + Status: ethpb.ValidatorStatus_ACTIVE, + ActivationEpoch: 12, + }, + { + Status: ethpb.ValidatorStatus_ACTIVE, + ActivationEpoch: 34, + }, + }, + } + + actualValidatorStatusResponse, err := validatorClient.MultipleValidatorStatus( + context.Background(), + ðpb.MultipleValidatorStatusRequest{ + PublicKeys: validatorsPubKey, + }, + ) + require.NoError(t, err) + assert.DeepEqual(t, &expectedValidatorStatusResponse, actualValidatorStatusResponse) +} + +func TestMultipleValidatorStatus_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + stateValidatorsProvider := mock.NewMockstateValidatorsProvider(ctrl) + + stateValidatorsProvider.EXPECT().GetStateValidators( + gomock.Any(), + nil, + nil, ).Return( + &rpcmiddleware.StateValidatorsResponseJson{}, + errors.New("a specific error"), + ).Times(1) + + validatorClient := beaconApiValidatorClient{stateValidatorsProvider: stateValidatorsProvider} + + _, err := validatorClient.MultipleValidatorStatus( + context.Background(), + ðpb.MultipleValidatorStatusRequest{ + PublicKeys: [][]byte{}, + }, + ) + require.ErrorContains(t, "failed to get validators status response", err) +} + +func TestGetValidatorsStatusResponse_Nominal_SomeActiveValidators(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + stringValidatorsPubKey := []string{ + "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13", // existing + "0x8000a6c975761b488bdb0dfba4ed37c0d97d6e6b968562ef5c84aa9a5dfb92d8e309195004e97709077723739bf04463", // existing + "0x80000e851c0f53c3246ff726d7ff7766661ca5e12a07c45c114d208d54f0f8233d4380b2e9aff759d69795d1df905526", // NOT existing + "0x8000ab56b051f9d8f31c687528c6e91c9b98e4c3a241e752f9ccfbea7c5a7fbbd272bdf2c0a7e52ce7e0b57693df364c", // existing + "0x8000b3e51de7e2e319b23a42d468dc8e63cd61daa5c4609cf2d800026d92706d1240414e155057bdc35e0574bba3ad80", // NOT existing + "0x800010c20716ef4264a6d93b3873a008ece58fb9312ac2cc3b0ccc40aedb050f2038281e6a92242a35476af9903c7919", // existing + } + + validatorsPubKey := make([][]byte, len(stringValidatorsPubKey)) + + for i, stringValidatorPubKey := range stringValidatorsPubKey { + validatorPubKey, err := hexutil.Decode(stringValidatorPubKey) + require.NoError(t, err) + validatorsPubKey[i] = validatorPubKey + } + + validatorsIndex := []int64{ + 12345, // NOT existing + 33333, // existing + } + + extraStringValidatorKey := "0x80003eb1e78ffdea6c878026b7074f84aaa16536c8e1960a652e817c848e7ccb051087f837b7d2bb6773cd9705601ede" + + stateValidatorsProvider := mock.NewMockstateValidatorsProvider(ctrl) + + stateValidatorsProvider.EXPECT().GetStateValidators( + stringValidatorsPubKey, + validatorsIndex, nil, - nil, - ).SetArg( - 1, - rpcmiddleware.StateValidatorsResponseJson{ + ).Return( + &rpcmiddleware.StateValidatorsResponseJson{ Data: []*rpcmiddleware.ValidatorContainerJson{ { - Index: "NotAnIndex", - Status: "active_ongoing", - Validator: &rpcmiddleware.ValidatorJson{}, + Index: "11111", + Status: "active_ongoing", + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13", + ActivationEpoch: "12", + }, + }, + { + Index: "22222", + Status: "active_exiting", + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: "0x800010c20716ef4264a6d93b3873a008ece58fb9312ac2cc3b0ccc40aedb050f2038281e6a92242a35476af9903c7919", + ActivationEpoch: "34", + }, + }, + { + Index: "33333", + Status: "active_ongoing", + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: extraStringValidatorKey, + ActivationEpoch: "56", + }, + }, + { + Index: "40000", + Status: "pending_queued", + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: "0x8000a6c975761b488bdb0dfba4ed37c0d97d6e6b968562ef5c84aa9a5dfb92d8e309195004e97709077723739bf04463", + ActivationEpoch: fmt.Sprintf("%d", params.BeaconConfig().FarFutureEpoch), + }, + }, + { + Index: "50000", + Status: "pending_queued", + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: "0x8000ab56b051f9d8f31c687528c6e91c9b98e4c3a241e752f9ccfbea7c5a7fbbd272bdf2c0a7e52ce7e0b57693df364c", + ActivationEpoch: fmt.Sprintf("%d", params.BeaconConfig().FarFutureEpoch), + }, }, }, }, + nil, ).Times(1) - validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} - - _, err = validatorClient.validatorStatus( - ðpb.ValidatorStatusRequest{ - PublicKey: pubKey, + stateValidatorsProvider.EXPECT().GetStateValidators( + nil, + nil, + []string{"active"}, + ).Return( + &rpcmiddleware.StateValidatorsResponseJson{ + Data: []*rpcmiddleware.ValidatorContainerJson{ + { + Index: "35000", + Status: "active_ongoing", + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: "0x8000ab56b051f9d8f31c687528c6e91c9b98e4c3a241e752f9ccfbea7c5a7fbbd272bdf2c0a7e52ce7e0b57693df364d", + ActivationEpoch: "56", + }, + }, + { + Index: "39000", + Status: "active_ongoing", + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: "0x8000ab56b051f9d8f31c687528c6e91c9b98e4c3a241e752f9ccfbea7c5a7fbbd272bdf2c0a7e52ce7e0b57693df364e", + ActivationEpoch: "56", + }, + }, + }, }, - ) - require.ErrorContains(t, "failed to parse last validator index", err) + nil, + ).Times(1) + + wantedStringValidatorsPubkey := []string{ + "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13", // existing + "0x800010c20716ef4264a6d93b3873a008ece58fb9312ac2cc3b0ccc40aedb050f2038281e6a92242a35476af9903c7919", // existing, + extraStringValidatorKey, // existing, + "0x8000a6c975761b488bdb0dfba4ed37c0d97d6e6b968562ef5c84aa9a5dfb92d8e309195004e97709077723739bf04463", // existing, + "0x8000ab56b051f9d8f31c687528c6e91c9b98e4c3a241e752f9ccfbea7c5a7fbbd272bdf2c0a7e52ce7e0b57693df364c", // existing + "0x80000e851c0f53c3246ff726d7ff7766661ca5e12a07c45c114d208d54f0f8233d4380b2e9aff759d69795d1df905526", // NOT existing + "0x8000b3e51de7e2e319b23a42d468dc8e63cd61daa5c4609cf2d800026d92706d1240414e155057bdc35e0574bba3ad80", // NOT existing + } + + wantedValidatorsPubKey := make([][]byte, len(wantedStringValidatorsPubkey)) + for i, stringValidatorPubKey := range wantedStringValidatorsPubkey { + validatorPubKey, err := hexutil.Decode(stringValidatorPubKey) + require.NoError(t, err) + + wantedValidatorsPubKey[i] = validatorPubKey + } + + wantedValidatorsIndex := []types.ValidatorIndex{ + 11111, + 22222, + 33333, + 40000, + 50000, + types.ValidatorIndex(^uint64(0)), + types.ValidatorIndex(^uint64(0)), + } + + wantedValidatorsStatusResponse := []*ethpb.ValidatorStatusResponse{ + { + Status: ethpb.ValidatorStatus_ACTIVE, + ActivationEpoch: 12, + }, + { + Status: ethpb.ValidatorStatus_EXITING, + ActivationEpoch: 34, + }, + { + Status: ethpb.ValidatorStatus_ACTIVE, + ActivationEpoch: 56, + }, + { + Status: ethpb.ValidatorStatus_PENDING, + ActivationEpoch: params.BeaconConfig().FarFutureEpoch, + PositionInActivationQueue: 1000, + }, + { + Status: ethpb.ValidatorStatus_PENDING, + ActivationEpoch: params.BeaconConfig().FarFutureEpoch, + PositionInActivationQueue: 11000, + }, + { + Status: ethpb.ValidatorStatus_UNKNOWN_STATUS, + ActivationEpoch: params.BeaconConfig().FarFutureEpoch, + }, + { + Status: ethpb.ValidatorStatus_UNKNOWN_STATUS, + ActivationEpoch: params.BeaconConfig().FarFutureEpoch, + }, + } + + validatorClient := beaconApiValidatorClient{stateValidatorsProvider: stateValidatorsProvider} + actualValidatorsPubKey, actualValidatorsIndex, actualValidatorsStatusResponse, err := validatorClient.getValidatorsStatusResponse(validatorsPubKey, validatorsIndex) + + require.NoError(t, err) + assert.DeepEqual(t, wantedValidatorsPubKey, actualValidatorsPubKey) + assert.DeepEqual(t, wantedValidatorsIndex, actualValidatorsIndex) + assert.DeepEqual(t, wantedValidatorsStatusResponse, actualValidatorsStatusResponse) +} + +func TestGetValidatorsStatusResponse_Nominal_NoActiveValidators(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + const stringValidatorPubKey = "0x8000a6c975761b488bdb0dfba4ed37c0d97d6e6b968562ef5c84aa9a5dfb92d8e309195004e97709077723739bf04463" + validatorPubKey, err := hexutil.Decode(stringValidatorPubKey) + require.NoError(t, err) + + stateValidatorsProvider := mock.NewMockstateValidatorsProvider(ctrl) + + stateValidatorsProvider.EXPECT().GetStateValidators( + []string{stringValidatorPubKey}, + nil, + nil, + ).Return( + &rpcmiddleware.StateValidatorsResponseJson{ + Data: []*rpcmiddleware.ValidatorContainerJson{ + { + Index: "40000", + Status: "pending_queued", + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: "0x8000a6c975761b488bdb0dfba4ed37c0d97d6e6b968562ef5c84aa9a5dfb92d8e309195004e97709077723739bf04463", + ActivationEpoch: fmt.Sprintf("%d", params.BeaconConfig().FarFutureEpoch), + }, + }, + }, + }, + nil, + ).Times(1) + + stateValidatorsProvider.EXPECT().GetStateValidators( + nil, + nil, + []string{"active"}, + ).Return( + &rpcmiddleware.StateValidatorsResponseJson{ + Data: []*rpcmiddleware.ValidatorContainerJson{}, + }, + nil, + ).Times(1) + + wantedValidatorsPubKey := [][]byte{validatorPubKey} + wantedValidatorsIndex := []types.ValidatorIndex{40000} + wantedValidatorsStatusResponse := []*ethpb.ValidatorStatusResponse{ + { + Status: ethpb.ValidatorStatus_PENDING, + ActivationEpoch: params.BeaconConfig().FarFutureEpoch, + PositionInActivationQueue: 40000, + }, + } + + validatorClient := beaconApiValidatorClient{stateValidatorsProvider: stateValidatorsProvider} + actualValidatorsPubKey, actualValidatorsIndex, actualValidatorsStatusResponse, err := validatorClient.getValidatorsStatusResponse(wantedValidatorsPubKey, nil) + + require.NoError(t, err) + require.NoError(t, err) + assert.DeepEqual(t, wantedValidatorsPubKey, actualValidatorsPubKey) + assert.DeepEqual(t, wantedValidatorsIndex, actualValidatorsIndex) + assert.DeepEqual(t, wantedValidatorsStatusResponse, actualValidatorsStatusResponse) +} + +type getStateValidatorsInterface struct { + // Inputs + inputStringPubKeys []string + inputIndexes []int64 + inputStatuses []string + + // Outputs + outputStateValidatorsResponseJson *rpcmiddleware.StateValidatorsResponseJson + outputErr error +} + +func TestValidatorStatusResponse_InvalidData(t *testing.T) { + stringPubKey := "0x8000a6c975761b488bdb0dfba4ed37c0d97d6e6b968562ef5c84aa9a5dfb92d8e309195004e97709077723739bf04463" + pubKey, err := hexutil.Decode(stringPubKey) + require.NoError(t, err) + + testCases := []struct { + name string + + // Inputs + inputPubKeys [][]byte + inputIndexes []int64 + inputGetStateValidatorsInterfaces []getStateValidatorsInterface + + // Outputs + outputErrMessage string + }{ + { + name: "failed getStateValidators", + + inputPubKeys: [][]byte{pubKey}, + inputIndexes: nil, + inputGetStateValidatorsInterfaces: []getStateValidatorsInterface{ + { + inputStringPubKeys: []string{stringPubKey}, + inputIndexes: nil, + inputStatuses: nil, + + outputStateValidatorsResponseJson: &rpcmiddleware.StateValidatorsResponseJson{}, + outputErr: errors.New("a specific error"), + }, + }, + outputErrMessage: "failed to get state validators", + }, + { + name: "failed to parse validator public key NotAPublicKey", + + inputPubKeys: [][]byte{pubKey}, + inputIndexes: nil, + inputGetStateValidatorsInterfaces: []getStateValidatorsInterface{ + { + inputStringPubKeys: []string{stringPubKey}, + inputIndexes: nil, + inputStatuses: nil, + + outputStateValidatorsResponseJson: &rpcmiddleware.StateValidatorsResponseJson{ + Data: []*rpcmiddleware.ValidatorContainerJson{ + { + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: "NotAPublicKey", + }, + }, + }, + }, + outputErr: nil, + }, + }, + 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, + + outputStateValidatorsResponseJson: &rpcmiddleware.StateValidatorsResponseJson{ + Data: []*rpcmiddleware.ValidatorContainerJson{ + { + Index: "NotAnIndex", + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: stringPubKey, + }, + }, + }, + }, + outputErr: nil, + }, + }, + outputErrMessage: "failed to parse validator index", + }, + { + name: "invalid validator status", + + inputPubKeys: [][]byte{pubKey}, + inputIndexes: nil, + inputGetStateValidatorsInterfaces: []getStateValidatorsInterface{ + { + inputStringPubKeys: []string{stringPubKey}, + inputIndexes: nil, + inputStatuses: nil, + + outputStateValidatorsResponseJson: &rpcmiddleware.StateValidatorsResponseJson{ + Data: []*rpcmiddleware.ValidatorContainerJson{ + { + Index: "12345", + Status: "NotAStatus", + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: stringPubKey, + }, + }, + }, + }, + outputErr: nil, + }, + }, + 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, + + outputStateValidatorsResponseJson: &rpcmiddleware.StateValidatorsResponseJson{ + Data: []*rpcmiddleware.ValidatorContainerJson{ + { + Index: "12345", + Status: "active_ongoing", + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: stringPubKey, + ActivationEpoch: "NotAnEpoch", + }, + }, + }, + }, + outputErr: nil, + }, + }, + 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, + + outputStateValidatorsResponseJson: &rpcmiddleware.StateValidatorsResponseJson{ + Data: []*rpcmiddleware.ValidatorContainerJson{ + { + Index: "12345", + Status: "pending_queued", + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: stringPubKey, + ActivationEpoch: "10", + }, + }, + }, + }, + outputErr: nil, + }, + { + inputStringPubKeys: nil, + inputIndexes: nil, + inputStatuses: []string{"active"}, + + outputStateValidatorsResponseJson: &rpcmiddleware.StateValidatorsResponseJson{}, + outputErr: errors.New("a specific error"), + }, + }, + outputErrMessage: "failed to get state validators", + }, + { + name: "failed to parse last validator index", + + inputPubKeys: [][]byte{pubKey}, + inputIndexes: nil, + inputGetStateValidatorsInterfaces: []getStateValidatorsInterface{ + { + inputStringPubKeys: []string{stringPubKey}, + inputIndexes: nil, + inputStatuses: nil, + + outputStateValidatorsResponseJson: &rpcmiddleware.StateValidatorsResponseJson{ + Data: []*rpcmiddleware.ValidatorContainerJson{ + { + Index: "12345", + Status: "pending_queued", + Validator: &rpcmiddleware.ValidatorJson{ + PublicKey: stringPubKey, + ActivationEpoch: "10", + }, + }, + }, + }, + outputErr: nil, + }, + { + inputStringPubKeys: nil, + inputIndexes: nil, + inputStatuses: []string{"active"}, + + outputStateValidatorsResponseJson: &rpcmiddleware.StateValidatorsResponseJson{ + Data: []*rpcmiddleware.ValidatorContainerJson{ + { + Index: "NotAnIndex", + }, + }, + }, + outputErr: nil, + }, + }, + outputErrMessage: "failed to parse last validator index NotAnIndex", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, + func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + stateValidatorsProvider := mock.NewMockstateValidatorsProvider(ctrl) + + for _, aa := range testCase.inputGetStateValidatorsInterfaces { + stateValidatorsProvider.EXPECT().GetStateValidators( + aa.inputStringPubKeys, + aa.inputIndexes, + aa.inputStatuses, + ).Return( + aa.outputStateValidatorsResponseJson, + aa.outputErr, + ).Times(1) + } + + validatorClient := beaconApiValidatorClient{stateValidatorsProvider: stateValidatorsProvider} + + _, _, _, err := validatorClient.getValidatorsStatusResponse( + testCase.inputPubKeys, + testCase.inputIndexes, + ) + + assert.ErrorContains(t, testCase.outputErrMessage, err) + }, + ) + } }