Add REST implementation for Validator's ValidatorIndex (#11712)

* Add GetAttestationData

* Add tests

* Add many more tests and refactor

* Fix logic

* Address PR comments

* Address PR comments

* Add jsonRestHandler and decouple http logic from rest of the code

* Add buildURL tests

* Remove handlers_test.go

* Improve tests

* Implement `ValidatorIndex` of `beaconApiValidatorClient` using Beacon API

* Implement getStateValidators

* `validatorIndex`: Use `getStateValidators`

Co-authored-by: Patrice Vignola <vignola.patrice@gmail.com>
This commit is contained in:
Manu NALEPA 2022-12-06 13:27:26 +01:00 committed by GitHub
parent 7dc966bb3b
commit 0a5c65e29c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1380 additions and 344 deletions

View File

@ -73,6 +73,7 @@ gofmt -s -w "$mock_path/."
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"
)
for ((i = 0; i < ${#beacon_api_mocks[@]}; i++)); do

View File

@ -4,10 +4,14 @@ load("@prysm//tools/go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"attestation_data.go",
"beacon_api_helpers.go",
"beacon_api_validator_client.go",
"domain_data.go",
"genesis.go",
"index.go",
"json_rest_handler.go",
"state_validators.go",
],
importpath = "github.com/prysmaticlabs/prysm/v3/validator/client/beacon-api",
visibility = ["//validator:__subpackages__"],
@ -31,11 +35,14 @@ go_test(
name = "go_default_test",
size = "small",
srcs = [
"attestation_data_test.go",
"beacon_api_helpers_test.go",
"beacon_api_validator_client_test.go",
"domain_data_test.go",
"genesis_test.go",
"handlers_test.go",
"index_test.go",
"json_rest_handler_test.go",
"state_validators_test.go",
"wait_for_chain_start_test.go",
],
embed = [":go_default_library"],
@ -44,6 +51,7 @@ go_test(
"//api/gateway/apimiddleware:go_default_library",
"//beacon-chain/rpc/apimiddleware:go_default_library",
"//config/params:go_default_library",
"//consensus-types/primitives:go_default_library",
"//encoding/bytesutil:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//testing/assert:go_default_library",
@ -51,6 +59,7 @@ go_test(
"//validator/client/beacon-api/mock:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@com_github_golang_mock//gomock:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@org_golang_google_protobuf//types/known/emptypb:go_default_library",
],
)

View File

@ -0,0 +1,107 @@
//go:build use_beacon_api
// +build use_beacon_api
package beacon_api
import (
"net/url"
"strconv"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/pkg/errors"
rpcmiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
types "github.com/prysmaticlabs/prysm/v3/consensus-types/primitives"
ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1"
)
func (c beaconApiValidatorClient) getAttestationData(
reqSlot types.Slot,
reqCommitteeIndex types.CommitteeIndex,
) (*ethpb.AttestationData, error) {
params := url.Values{}
params.Add("slot", strconv.FormatUint(uint64(reqSlot), 10))
params.Add("committee_index", strconv.FormatUint(uint64(reqCommitteeIndex), 10))
query := buildURL("/eth/v1/validator/attestation_data", params)
produceAttestationDataResponseJson := rpcmiddleware.ProduceAttestationDataResponseJson{}
_, err := c.jsonRestHandler.GetRestJsonResponse(query, &produceAttestationDataResponseJson)
if err != nil {
return nil, errors.Wrap(err, "failed to get json response")
}
if produceAttestationDataResponseJson.Data == nil {
return nil, errors.New("attestation data is nil")
}
attestationData := produceAttestationDataResponseJson.Data
committeeIndex, err := strconv.ParseUint(attestationData.CommitteeIndex, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse attestation committee index: %s", attestationData.CommitteeIndex)
}
if !validRoot(attestationData.BeaconBlockRoot) {
return nil, errors.Errorf("invalid beacon block root: %s", attestationData.BeaconBlockRoot)
}
beaconBlockRoot, err := hexutil.Decode(attestationData.BeaconBlockRoot)
if err != nil {
return nil, errors.Wrapf(err, "failed to decode beacon block root: %s", attestationData.BeaconBlockRoot)
}
slot, err := strconv.ParseUint(attestationData.Slot, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse attestation slot: %s", attestationData.Slot)
}
if attestationData.Source == nil {
return nil, errors.New("attestation source is nil")
}
sourceEpoch, err := strconv.ParseUint(attestationData.Source.Epoch, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse attestation source epoch: %s", attestationData.Source.Epoch)
}
if !validRoot(attestationData.Source.Root) {
return nil, errors.Errorf("invalid attestation source root: %s", attestationData.Source.Root)
}
sourceRoot, err := hexutil.Decode(attestationData.Source.Root)
if err != nil {
return nil, errors.Wrapf(err, "failed to decode attestation source root: %s", attestationData.Source.Root)
}
if attestationData.Target == nil {
return nil, errors.New("attestation target is nil")
}
targetEpoch, err := strconv.ParseUint(attestationData.Target.Epoch, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse attestation target epoch: %s", attestationData.Target.Epoch)
}
if !validRoot(attestationData.Target.Root) {
return nil, errors.Errorf("invalid attestation target root: %s", attestationData.Target.Root)
}
targetRoot, err := hexutil.Decode(attestationData.Target.Root)
if err != nil {
return nil, errors.Wrapf(err, "failed to decode attestation target root: %s", attestationData.Target.Root)
}
response := &ethpb.AttestationData{
BeaconBlockRoot: beaconBlockRoot,
CommitteeIndex: types.CommitteeIndex(committeeIndex),
Slot: types.Slot(slot),
Source: &ethpb.Checkpoint{
Epoch: types.Epoch(sourceEpoch),
Root: sourceRoot,
},
Target: &ethpb.Checkpoint{
Epoch: types.Epoch(targetEpoch),
Root: targetRoot,
},
}
return response, nil
}

View File

@ -0,0 +1,243 @@
//go:build use_beacon_api
// +build use_beacon_api
package beacon_api
import (
"errors"
"fmt"
"strconv"
"testing"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/golang/mock/gomock"
rpcmiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
types "github.com/prysmaticlabs/prysm/v3/consensus-types/primitives"
"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 attestationDataEndpoint = "/eth/v1/validator/attestation_data"
func TestGetAttestationData_ValidAttestation(t *testing.T) {
expectedSlot := uint64(5)
expectedCommitteeIndex := uint64(6)
expectedBeaconBlockRoot := "0x0636045df9bdda3ab96592cf5389032c8ec3977f911e2b53509b348dfe164d4d"
expectedSourceEpoch := uint64(7)
expectedSourceRoot := "0xd4bcbdefc8156e85247681086e8050e5d2d5d1bf076a25f6decd99250f3a378d"
expectedTargetEpoch := uint64(8)
expectedTargetRoot := "0x246590e8e4c2a9bd13cc776ecc7025bc432219f076e80b27267b8fa0456dc821"
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
produceAttestationDataResponseJson := rpcmiddleware.ProduceAttestationDataResponseJson{}
jsonRestHandler.EXPECT().GetRestJsonResponse(
fmt.Sprintf("/eth/v1/validator/attestation_data?committee_index=%d&slot=%d", expectedCommitteeIndex, expectedSlot),
&produceAttestationDataResponseJson,
).Return(
nil,
nil,
).SetArg(
1,
rpcmiddleware.ProduceAttestationDataResponseJson{
Data: &rpcmiddleware.AttestationDataJson{
Slot: strconv.FormatUint(expectedSlot, 10),
CommitteeIndex: strconv.FormatUint(expectedCommitteeIndex, 10),
BeaconBlockRoot: expectedBeaconBlockRoot,
Source: &rpcmiddleware.CheckpointJson{
Epoch: strconv.FormatUint(expectedSourceEpoch, 10),
Root: expectedSourceRoot,
},
Target: &rpcmiddleware.CheckpointJson{
Epoch: strconv.FormatUint(expectedTargetEpoch, 10),
Root: expectedTargetRoot,
},
},
},
).Times(1)
validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
resp, err := validatorClient.getAttestationData(types.Slot(expectedSlot), types.CommitteeIndex(expectedCommitteeIndex))
assert.NoError(t, err)
require.NotNil(t, resp)
assert.Equal(t, expectedBeaconBlockRoot, hexutil.Encode(resp.BeaconBlockRoot))
assert.Equal(t, expectedCommitteeIndex, uint64(resp.CommitteeIndex))
assert.Equal(t, expectedSlot, uint64(resp.Slot))
require.NotNil(t, resp.Source)
assert.Equal(t, expectedSourceEpoch, uint64(resp.Source.Epoch))
assert.Equal(t, expectedSourceRoot, hexutil.Encode(resp.Source.Root))
require.NotNil(t, resp.Target)
assert.Equal(t, expectedTargetEpoch, uint64(resp.Target.Epoch))
assert.Equal(t, expectedTargetRoot, hexutil.Encode(resp.Target.Root))
}
func TestGetAttestationData_InvalidData(t *testing.T) {
testCases := []struct {
name string
generateData func() rpcmiddleware.ProduceAttestationDataResponseJson
expectedErrorMessage string
}{
{
name: "nil attestation data",
generateData: func() rpcmiddleware.ProduceAttestationDataResponseJson {
return rpcmiddleware.ProduceAttestationDataResponseJson{
Data: nil,
}
},
expectedErrorMessage: "attestation data is nil",
},
{
name: "invalid committee index",
generateData: func() rpcmiddleware.ProduceAttestationDataResponseJson {
attestation := generateValidAttestation(1, 2)
attestation.Data.CommitteeIndex = "foo"
return attestation
},
expectedErrorMessage: "failed to parse attestation committee index: foo",
},
{
name: "invalid block root",
generateData: func() rpcmiddleware.ProduceAttestationDataResponseJson {
attestation := generateValidAttestation(1, 2)
attestation.Data.BeaconBlockRoot = "foo"
return attestation
},
expectedErrorMessage: "invalid beacon block root: foo",
},
{
name: "invalid slot",
generateData: func() rpcmiddleware.ProduceAttestationDataResponseJson {
attestation := generateValidAttestation(1, 2)
attestation.Data.Slot = "foo"
return attestation
},
expectedErrorMessage: "failed to parse attestation slot: foo",
},
{
name: "nil source",
generateData: func() rpcmiddleware.ProduceAttestationDataResponseJson {
attestation := generateValidAttestation(1, 2)
attestation.Data.Source = nil
return attestation
},
expectedErrorMessage: "attestation source is nil",
},
{
name: "invalid source epoch",
generateData: func() rpcmiddleware.ProduceAttestationDataResponseJson {
attestation := generateValidAttestation(1, 2)
attestation.Data.Source.Epoch = "foo"
return attestation
},
expectedErrorMessage: "failed to parse attestation source epoch: foo",
},
{
name: "invalid source root",
generateData: func() rpcmiddleware.ProduceAttestationDataResponseJson {
attestation := generateValidAttestation(1, 2)
attestation.Data.Source.Root = "foo"
return attestation
},
expectedErrorMessage: "invalid attestation source root: foo",
},
{
name: "nil target",
generateData: func() rpcmiddleware.ProduceAttestationDataResponseJson {
attestation := generateValidAttestation(1, 2)
attestation.Data.Target = nil
return attestation
},
expectedErrorMessage: "attestation target is nil",
},
{
name: "invalid target epoch",
generateData: func() rpcmiddleware.ProduceAttestationDataResponseJson {
attestation := generateValidAttestation(1, 2)
attestation.Data.Target.Epoch = "foo"
return attestation
},
expectedErrorMessage: "failed to parse attestation target epoch: foo",
},
{
name: "invalid target root",
generateData: func() rpcmiddleware.ProduceAttestationDataResponseJson {
attestation := generateValidAttestation(1, 2)
attestation.Data.Target.Root = "foo"
return attestation
},
expectedErrorMessage: "invalid attestation target root: foo",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
produceAttestationDataResponseJson := rpcmiddleware.ProduceAttestationDataResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().GetRestJsonResponse(
"/eth/v1/validator/attestation_data?committee_index=2&slot=1",
&produceAttestationDataResponseJson,
).Return(
nil,
nil,
).SetArg(
1,
testCase.generateData(),
).Times(1)
validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
_, err := validatorClient.getAttestationData(1, 2)
assert.ErrorContains(t, testCase.expectedErrorMessage, err)
})
}
}
func TestGetAttestationData_JsonResponseError(t *testing.T) {
const slot = types.Slot(1)
const committeeIndex = types.CommitteeIndex(2)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
produceAttestationDataResponseJson := rpcmiddleware.ProduceAttestationDataResponseJson{}
jsonRestHandler.EXPECT().GetRestJsonResponse(
fmt.Sprintf("/eth/v1/validator/attestation_data?committee_index=%d&slot=%d", committeeIndex, slot),
&produceAttestationDataResponseJson,
).Return(
nil,
errors.New("some specific json response error"),
).Times(1)
validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
_, err := validatorClient.getAttestationData(slot, committeeIndex)
assert.ErrorContains(t, "failed to get json response", err)
assert.ErrorContains(t, "some specific json response error", err)
}
func generateValidAttestation(slot uint64, committeeIndex uint64) rpcmiddleware.ProduceAttestationDataResponseJson {
return rpcmiddleware.ProduceAttestationDataResponseJson{
Data: &rpcmiddleware.AttestationDataJson{
Slot: strconv.FormatUint(slot, 10),
CommitteeIndex: strconv.FormatUint(committeeIndex, 10),
BeaconBlockRoot: "0x5ecf3bff35e39d5f75476d42950d549f81fa93038c46b6652ae89ae1f7ad834f",
Source: &rpcmiddleware.CheckpointJson{
Epoch: "3",
Root: "0x9023c9e64f23c1d451d5073c641f5f69597c2ad7d82f6f16e67d703e0ce5db8b",
},
Target: &rpcmiddleware.CheckpointJson{
Epoch: "4",
Root: "0xb154d46803b15b458ca822466547b054bc124338c6ee1d9c433dcde8c4457cca",
},
},
}
}

View File

@ -4,6 +4,8 @@
package beacon_api
import (
"fmt"
neturl "net/url"
"regexp"
)
@ -14,3 +16,11 @@ func validRoot(root string) bool {
}
return matchesRegex
}
func buildURL(path string, queryParams ...neturl.Values) string {
if len(queryParams) == 0 {
return path
}
return fmt.Sprintf("%s?%s", path, queryParams[0].Encode())
}

View File

@ -4,6 +4,7 @@
package beacon_api
import (
"net/url"
"testing"
"github.com/prysmaticlabs/prysm/v3/testing/assert"
@ -53,3 +54,20 @@ func TestBeaconApiHelpers(t *testing.T) {
})
}
}
func TestBuildURL_NoParams(t *testing.T) {
wanted := "/aaa/bbb/ccc"
actual := buildURL("/aaa/bbb/ccc")
assert.Equal(t, wanted, actual)
}
func TestBuildURL_WithParams(t *testing.T) {
params := url.Values{}
params.Add("xxxx", "1")
params.Add("yyyy", "2")
params.Add("zzzz", "3")
wanted := "/aaa/bbb/ccc?xxxx=1&yyyy=2&zzzz=3"
actual := buildURL("/aaa/bbb/ccc", params)
assert.Equal(t, wanted, actual)
}

View File

@ -17,19 +17,20 @@ import (
)
type beaconApiValidatorClient struct {
url string
httpClient http.Client
fallbackClient iface.ValidatorClient
genesisProvider genesisProvider
jsonRestHandler jsonRestHandler
fallbackClient iface.ValidatorClient
}
func NewBeaconApiValidatorClient(url string, timeout time.Duration) *beaconApiValidatorClient {
httpClient := http.Client{Timeout: timeout}
func NewBeaconApiValidatorClient(host string, timeout time.Duration) *beaconApiValidatorClient {
jsonRestHandler := beaconApiJsonRestHandler{
httpClient: http.Client{Timeout: timeout},
host: host,
}
return &beaconApiValidatorClient{
url: url,
httpClient: httpClient,
genesisProvider: beaconApiGenesisProvider{httpClient: httpClient, url: url},
genesisProvider: beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler},
jsonRestHandler: jsonRestHandler,
}
}
@ -66,13 +67,12 @@ func (c *beaconApiValidatorClient) DomainData(_ context.Context, in *ethpb.Domai
return c.getDomainData(in.Epoch, domainType)
}
func (c *beaconApiValidatorClient) GetAttestationData(ctx context.Context, in *ethpb.AttestationDataRequest) (*ethpb.AttestationData, error) {
if c.fallbackClient != nil {
return c.fallbackClient.GetAttestationData(ctx, in)
func (c *beaconApiValidatorClient) GetAttestationData(_ context.Context, in *ethpb.AttestationDataRequest) (*ethpb.AttestationData, error) {
if in == nil {
return nil, errors.New("GetAttestationData received nil argument `in`")
}
// TODO: Implement me
panic("beaconApiValidatorClient.GetAttestationData is not implemented. To use a fallback client, create this validator with NewBeaconApiValidatorClientWithFallback instead.")
return c.getAttestationData(in.Slot, in.CommitteeIndex)
}
func (c *beaconApiValidatorClient) GetBeaconBlock(ctx context.Context, in *ethpb.BlockRequest) (*ethpb.GenericBeaconBlock, error) {
@ -237,13 +237,8 @@ func (c *beaconApiValidatorClient) SubscribeCommitteeSubnets(ctx context.Context
panic("beaconApiValidatorClient.SubscribeCommitteeSubnets is not implemented. To use a fallback client, create this validator with NewBeaconApiValidatorClientWithFallback instead.")
}
func (c *beaconApiValidatorClient) ValidatorIndex(ctx context.Context, in *ethpb.ValidatorIndexRequest) (*ethpb.ValidatorIndexResponse, error) {
if c.fallbackClient != nil {
return c.fallbackClient.ValidatorIndex(ctx, in)
}
// TODO: Implement me
panic("beaconApiValidatorClient.ValidatorIndex is not implemented. To use a fallback client, create this validator with NewBeaconApiValidatorClientWithFallback instead.")
func (c *beaconApiValidatorClient) ValidatorIndex(_ context.Context, in *ethpb.ValidatorIndexRequest) (*ethpb.ValidatorIndexResponse, error) {
return c.validatorIndex(in)
}
func (c *beaconApiValidatorClient) ValidatorStatus(ctx context.Context, in *ethpb.ValidatorStatusRequest) (*ethpb.ValidatorStatusResponse, error) {

View File

@ -5,12 +5,15 @@ package beacon_api
import (
"context"
"errors"
"fmt"
"testing"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/golang/mock/gomock"
rpcmiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
types "github.com/prysmaticlabs/prysm/v3/consensus-types/primitives"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/prysmaticlabs/prysm/v3/config/params"
"github.com/prysmaticlabs/prysm/v3/encoding/bytesutil"
ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1"
@ -18,7 +21,45 @@ import (
"github.com/prysmaticlabs/prysm/v3/validator/client/beacon-api/mock"
)
// Check that the DomainData() returns whatever the internal getDomainData() returns
func TestBeaconApiValidatorClient_GetAttestationDataNilInput(t *testing.T) {
validatorClient := beaconApiValidatorClient{}
_, err := validatorClient.GetAttestationData(context.Background(), nil)
assert.ErrorContains(t, "GetAttestationData received nil argument `in`", err)
}
// Make sure that GetAttestationData() returns the same thing as the internal getAttestationData()
func TestBeaconApiValidatorClient_GetAttestationDataValid(t *testing.T) {
const slot = types.Slot(1)
const committeeIndex = types.CommitteeIndex(2)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
produceAttestationDataResponseJson := rpcmiddleware.ProduceAttestationDataResponseJson{}
jsonRestHandler.EXPECT().GetRestJsonResponse(
fmt.Sprintf("/eth/v1/validator/attestation_data?committee_index=%d&slot=%d", committeeIndex, slot),
&produceAttestationDataResponseJson,
).Return(
nil,
nil,
).SetArg(
1,
generateValidAttestation(uint64(slot), uint64(committeeIndex)),
).Times(2)
validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
expectedResp, expectedErr := validatorClient.getAttestationData(slot, committeeIndex)
resp, err := validatorClient.GetAttestationData(
context.Background(),
&ethpb.AttestationDataRequest{Slot: slot, CommitteeIndex: committeeIndex},
)
assert.DeepEqual(t, expectedErr, err)
assert.DeepEqual(t, expectedResp, resp)
}
func TestBeaconApiValidatorClient_DomainDataValid(t *testing.T) {
const genesisValidatorRoot = "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"
epoch := params.BeaconConfig().AltairForkEpoch
@ -50,3 +91,35 @@ func TestBeaconApiValidatorClient_DomainDataError(t *testing.T) {
_, err := validatorClient.DomainData(context.Background(), &ethpb.DomainRequest{Epoch: epoch, Domain: domainType})
assert.ErrorContains(t, fmt.Sprintf("invalid domain type: %s", hexutil.Encode(domainType)), err)
}
func TestBeaconApiValidatorClient_GetAttestationDataError(t *testing.T) {
const slot = types.Slot(1)
const committeeIndex = types.CommitteeIndex(2)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
produceAttestationDataResponseJson := rpcmiddleware.ProduceAttestationDataResponseJson{}
jsonRestHandler.EXPECT().GetRestJsonResponse(
fmt.Sprintf("/eth/v1/validator/attestation_data?committee_index=%d&slot=%d", committeeIndex, slot),
&produceAttestationDataResponseJson,
).Return(
nil,
errors.New("some specific json error"),
).SetArg(
1,
generateValidAttestation(uint64(slot), uint64(committeeIndex)),
).Times(2)
validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
expectedResp, expectedErr := validatorClient.getAttestationData(slot, committeeIndex)
resp, err := validatorClient.GetAttestationData(
context.Background(),
&ethpb.AttestationDataRequest{Slot: slot, CommitteeIndex: committeeIndex},
)
assert.ErrorContains(t, expectedErr.Error(), err)
assert.DeepEqual(t, expectedResp, resp)
}

View File

@ -5,7 +5,6 @@ package beacon_api
import (
"context"
"encoding/json"
"net/http"
"strconv"
"time"
@ -22,8 +21,7 @@ type genesisProvider interface {
}
type beaconApiGenesisProvider struct {
httpClient http.Client
url string
jsonRestHandler jsonRestHandler
}
func (c beaconApiValidatorClient) waitForChainStart(ctx context.Context) (*ethpb.ChainStartResponse, error) {
@ -65,29 +63,12 @@ func (c beaconApiValidatorClient) waitForChainStart(ctx context.Context) (*ethpb
return chainStartResponse, nil
}
// GetGenesis gets the genesis information from the beacon node via the /eth/v1/beacon/genesis endpoint
func (c beaconApiGenesisProvider) GetGenesis() (*rpcmiddleware.GenesisResponse_GenesisJson, *apimiddleware.DefaultErrorJson, error) {
resp, err := c.httpClient.Get(c.url + "/eth/v1/beacon/genesis")
if err != nil {
return nil, nil, errors.Wrap(err, "failed to query REST API genesis endpoint")
}
defer func() {
if err = resp.Body.Close(); err != nil {
return
}
}()
if resp.StatusCode != http.StatusOK {
errorJson := &apimiddleware.DefaultErrorJson{}
if err := json.NewDecoder(resp.Body).Decode(&errorJson); err != nil {
return nil, nil, errors.Wrap(err, "failed to decode response body genesis error json")
}
return nil, errorJson, errors.Errorf("error %d: %s", errorJson.Code, errorJson.Message)
}
genesisJson := &rpcmiddleware.GenesisResponseJson{}
if err := json.NewDecoder(resp.Body).Decode(&genesisJson); err != nil {
return nil, nil, errors.Wrap(err, "failed to decode response body genesis json")
errorJson, err := c.jsonRestHandler.GetRestJsonResponse("/eth/v1/beacon/genesis", genesisJson)
if err != nil {
return nil, errorJson, errors.Wrap(err, "failed to get json response")
}
if genesisJson.Data == nil {

View File

@ -4,25 +4,40 @@
package beacon_api
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v3/api/gateway/apimiddleware"
rpcmiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
"github.com/prysmaticlabs/prysm/v3/testing/assert"
"github.com/prysmaticlabs/prysm/v3/testing/require"
"github.com/prysmaticlabs/prysm/v3/validator/client/beacon-api/mock"
)
func TestGetGenesis_ValidGenesis(t *testing.T) {
server := httptest.NewServer(createGenesisHandler(&rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "1234",
GenesisValidatorsRoot: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
}))
defer server.Close()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
genesisProvider := &beaconApiGenesisProvider{url: server.URL, httpClient: http.Client{Timeout: time.Second * 5}}
genesisResponseJson := rpcmiddleware.GenesisResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().GetRestJsonResponse(
"/eth/v1/beacon/genesis",
&genesisResponseJson,
).Return(
nil,
nil,
).SetArg(
1,
rpcmiddleware.GenesisResponseJson{
Data: &rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "1234",
GenesisValidatorsRoot: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
},
},
).Times(1)
genesisProvider := &beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler}
resp, httpError, err := genesisProvider.GetGenesis()
assert.NoError(t, err)
assert.Equal(t, (*apimiddleware.DefaultErrorJson)(nil), httpError)
@ -32,71 +47,50 @@ func TestGetGenesis_ValidGenesis(t *testing.T) {
}
func TestGetGenesis_NilData(t *testing.T) {
server := httptest.NewServer(createGenesisHandler(nil))
defer server.Close()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
genesisProvider := &beaconApiGenesisProvider{url: server.URL, httpClient: http.Client{Timeout: time.Second * 5}}
genesisResponseJson := rpcmiddleware.GenesisResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().GetRestJsonResponse(
"/eth/v1/beacon/genesis",
&genesisResponseJson,
).Return(
nil,
nil,
).SetArg(
1,
rpcmiddleware.GenesisResponseJson{Data: nil},
).Times(1)
genesisProvider := &beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler}
_, httpError, err := genesisProvider.GetGenesis()
assert.Equal(t, (*apimiddleware.DefaultErrorJson)(nil), httpError)
assert.ErrorContains(t, "genesis data is nil", err)
}
func TestGetGenesis_InvalidJsonGenesis(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("foo"))
require.NoError(t, err)
}))
defer server.Close()
func TestGetGenesis_JsonResponseError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
genesisProvider := &beaconApiGenesisProvider{url: server.URL, httpClient: http.Client{Timeout: time.Second * 5}}
expectedHttpErrorJson := &apimiddleware.DefaultErrorJson{
Message: "http error message",
Code: 999,
}
genesisResponseJson := rpcmiddleware.GenesisResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().GetRestJsonResponse(
"/eth/v1/beacon/genesis",
&genesisResponseJson,
).Return(
expectedHttpErrorJson,
errors.New("some specific json response error"),
).Times(1)
genesisProvider := &beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler}
_, httpError, err := genesisProvider.GetGenesis()
assert.Equal(t, (*apimiddleware.DefaultErrorJson)(nil), httpError)
assert.ErrorContains(t, "failed to decode response body genesis json", err)
}
func TestGetGenesis_InvalidJsonError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(invalidJsonErrHandler))
defer server.Close()
genesisProvider := &beaconApiGenesisProvider{url: server.URL, httpClient: http.Client{Timeout: time.Second * 5}}
_, httpError, err := genesisProvider.GetGenesis()
assert.Equal(t, (*apimiddleware.DefaultErrorJson)(nil), httpError)
assert.ErrorContains(t, "failed to decode response body genesis error json", err)
}
func TestGetGenesis_404Error(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(notFoundErrHandler))
defer server.Close()
validatorClient := &beaconApiGenesisProvider{url: server.URL, httpClient: http.Client{Timeout: time.Second * 5}}
_, httpError, err := validatorClient.GetGenesis()
require.NotNil(t, httpError)
assert.Equal(t, http.StatusNotFound, httpError.Code)
assert.Equal(t, "Not found", httpError.Message)
assert.ErrorContains(t, "error 404: Not found", err)
}
func TestGetGenesis_500Error(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(internalServerErrHandler))
defer server.Close()
validatorClient := &beaconApiGenesisProvider{url: server.URL, httpClient: http.Client{Timeout: time.Second * 5}}
_, httpError, err := validatorClient.GetGenesis()
require.NotNil(t, httpError)
assert.Equal(t, http.StatusInternalServerError, httpError.Code)
assert.Equal(t, "Internal server error", httpError.Message)
assert.ErrorContains(t, "error 500: Internal server error", err)
}
func TestGetGenesis_Timeout(t *testing.T) {
server := httptest.NewServer(createGenesisHandler(&rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "1234",
GenesisValidatorsRoot: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
}))
defer server.Close()
genesisProvider := &beaconApiGenesisProvider{url: server.URL, httpClient: http.Client{Timeout: 1}}
_, httpError, err := genesisProvider.GetGenesis()
assert.Equal(t, (*apimiddleware.DefaultErrorJson)(nil), httpError)
assert.ErrorContains(t, "failed to query REST API genesis endpoint", err)
assert.ErrorContains(t, "failed to get json response", err)
assert.ErrorContains(t, "some specific json response error", err)
assert.DeepEqual(t, expectedHttpErrorJson, httpError)
}

View File

@ -1,96 +0,0 @@
//go:build use_beacon_api
// +build use_beacon_api
package beacon_api
import (
"encoding/json"
"net/http"
"github.com/prysmaticlabs/prysm/v3/api/gateway/apimiddleware"
rpcmiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
)
func internalServerErrHandler(w http.ResponseWriter, r *http.Request) {
internalErrorJson := &apimiddleware.DefaultErrorJson{
Code: http.StatusInternalServerError,
Message: "Internal server error",
}
marshalledError, err := json.Marshal(internalErrorJson)
if err != nil {
panic(err)
}
w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write(marshalledError)
if err != nil {
panic(err)
}
}
func notFoundErrHandler(w http.ResponseWriter, r *http.Request) {
internalErrorJson := &apimiddleware.DefaultErrorJson{
Code: http.StatusNotFound,
Message: "Not found",
}
marshalledError, err := json.Marshal(internalErrorJson)
if err != nil {
panic(err)
}
w.WriteHeader(http.StatusNotFound)
_, err = w.Write(marshalledError)
if err != nil {
panic(err)
}
}
func invalidErr999Handler(w http.ResponseWriter, r *http.Request) {
internalErrorJson := &apimiddleware.DefaultErrorJson{
Code: 999,
Message: "Invalid error",
}
marshalledError, err := json.Marshal(internalErrorJson)
if err != nil {
panic(err)
}
w.WriteHeader(999)
_, err = w.Write(marshalledError)
if err != nil {
panic(err)
}
}
func invalidJsonErrHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, err := w.Write([]byte("foo"))
if err != nil {
panic(err)
}
}
func invalidJsonResultHandler(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("foo"))
if err != nil {
panic(err)
}
}
func createGenesisHandler(data *rpcmiddleware.GenesisResponse_GenesisJson) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
genesisResponseJson := &rpcmiddleware.GenesisResponseJson{Data: data}
marshalledResponse, err := json.Marshal(genesisResponseJson)
if err != nil {
panic(err)
}
_, err = w.Write(marshalledResponse)
if err != nil {
panic(err)
}
})
}

View File

@ -0,0 +1,35 @@
//go:build use_beacon_api
// +build use_beacon_api
package beacon_api
import (
"strconv"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/pkg/errors"
types "github.com/prysmaticlabs/prysm/v3/consensus-types/primitives"
ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1"
)
func (c beaconApiValidatorClient) validatorIndex(in *ethpb.ValidatorIndexRequest) (*ethpb.ValidatorIndexResponse, error) {
stringPubKey := hexutil.Encode(in.PublicKey)
stateValidator, err := c.getStateValidators([]string{stringPubKey})
if err != nil {
return nil, errors.Wrap(err, "failed to get validator state")
}
if len(stateValidator.Data) == 0 {
return nil, errors.Errorf("could not find validator index for public key `%s`", stringPubKey)
}
stringValidatorIndex := stateValidator.Data[0].Index
index, err := strconv.ParseUint(stringValidatorIndex, 10, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse validator index")
}
return &ethpb.ValidatorIndexResponse{Index: types.ValidatorIndex(index)}, nil
}

View File

@ -0,0 +1,181 @@
//go:build use_beacon_api
// +build use_beacon_api
package beacon_api
import (
"context"
"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"
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 stringPubKey = "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13"
func getPubKeyAndURL(t *testing.T, stringPubkey string) ([]byte, string) {
baseUrl := "/eth/v1/beacon/states/head/validators"
url := fmt.Sprintf("%s?id=%s", baseUrl, stringPubKey)
pubKey, err := hexutil.Decode(stringPubKey)
require.NoError(t, err)
return pubKey, url
}
func TestIndex_Nominal(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
pubKey, url := getPubKeyAndURL(t, stringPubKey)
stateValidatorsResponseJson := rpcmiddleware.StateValidatorsResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().GetRestJsonResponse(
url,
&stateValidatorsResponseJson,
).Return(
nil,
nil,
).SetArg(
1,
rpcmiddleware.StateValidatorsResponseJson{
Data: []*rpcmiddleware.ValidatorContainerJson{
{
Index: "55293",
Status: "active_ongoing",
Validator: &rpcmiddleware.ValidatorJson{
PublicKey: stringPubKey,
},
},
},
},
).Times(1)
validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
validatorIndex, err := validatorClient.ValidatorIndex(
context.Background(),
&ethpb.ValidatorIndexRequest{
PublicKey: pubKey,
},
)
require.NoError(t, err)
assert.Equal(t, types.ValidatorIndex(55293), validatorIndex.Index)
}
func TestIndex_UnexistingValidator(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
pubKey, url := getPubKeyAndURL(t, stringPubKey)
stateValidatorsResponseJson := rpcmiddleware.StateValidatorsResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().GetRestJsonResponse(
url,
&stateValidatorsResponseJson,
).Return(
nil,
nil,
).SetArg(
1,
rpcmiddleware.StateValidatorsResponseJson{
Data: []*rpcmiddleware.ValidatorContainerJson{},
},
).Times(1)
validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
_, err := validatorClient.ValidatorIndex(
context.Background(),
&ethpb.ValidatorIndexRequest{
PublicKey: pubKey,
},
)
wanted := "could not find validator index for public key `0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13`"
assert.ErrorContains(t, wanted, err)
}
func TestIndex_BadIndexError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
pubKey, url := getPubKeyAndURL(t, stringPubKey)
stateValidatorsResponseJson := rpcmiddleware.StateValidatorsResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().GetRestJsonResponse(
url,
&stateValidatorsResponseJson,
).Return(
nil,
nil,
).SetArg(
1,
rpcmiddleware.StateValidatorsResponseJson{
Data: []*rpcmiddleware.ValidatorContainerJson{
{
Index: "This is not an index",
Status: "active_ongoing",
Validator: &rpcmiddleware.ValidatorJson{
PublicKey: stringPubKey,
},
},
},
},
).Times(1)
validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
_, err := validatorClient.ValidatorIndex(
context.Background(),
&ethpb.ValidatorIndexRequest{
PublicKey: pubKey,
},
)
assert.ErrorContains(t, "failed to parse validator index", err)
}
func TestIndex_JsonResponseError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
pubKey, url := getPubKeyAndURL(t, stringPubKey)
stateValidatorsResponseJson := rpcmiddleware.StateValidatorsResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().GetRestJsonResponse(
url,
&stateValidatorsResponseJson,
).Return(
nil,
errors.New("some specific json error"),
).Times(1)
validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
_, err := validatorClient.ValidatorIndex(
context.Background(),
&ethpb.ValidatorIndexRequest{
PublicKey: pubKey,
},
)
assert.ErrorContains(t, "failed to get validator state", err)
}

View File

@ -0,0 +1,55 @@
//go:build use_beacon_api
// +build use_beacon_api
package beacon_api
import (
"encoding/json"
"net/http"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v3/api/gateway/apimiddleware"
)
type jsonRestHandler interface {
GetRestJsonResponse(query string, responseJson interface{}) (*apimiddleware.DefaultErrorJson, error)
}
type beaconApiJsonRestHandler struct {
httpClient http.Client
host string
}
// GetRestJsonResponse sends a GET requests to apiEndpoint and decodes the response body as a JSON object into responseJson.
// If an HTTP error is returned, the body is decoded as a DefaultErrorJson JSON object instead and returned as the first return value.
func (c beaconApiJsonRestHandler) GetRestJsonResponse(apiEndpoint string, responseJson interface{}) (*apimiddleware.DefaultErrorJson, error) {
if responseJson == nil {
return nil, errors.New("responseJson is nil")
}
url := c.host + apiEndpoint
resp, err := c.httpClient.Get(url)
if err != nil {
return nil, errors.Wrapf(err, "failed to query REST API %s", url)
}
defer func() {
if err := resp.Body.Close(); err != nil {
return
}
}()
if resp.StatusCode != http.StatusOK {
errorJson := &apimiddleware.DefaultErrorJson{}
if err := json.NewDecoder(resp.Body).Decode(errorJson); err != nil {
return nil, errors.Wrapf(err, "failed to decode error json for %s", url)
}
return errorJson, errors.Errorf("error %d: %s", errorJson.Code, errorJson.Message)
}
if err := json.NewDecoder(resp.Body).Decode(responseJson); err != nil {
return nil, errors.Wrapf(err, "failed to decode response json for %s", url)
}
return nil, nil
}

View File

@ -0,0 +1,192 @@
//go:build use_beacon_api
// +build use_beacon_api
package beacon_api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/prysmaticlabs/prysm/v3/api/gateway/apimiddleware"
rpcmiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
"github.com/prysmaticlabs/prysm/v3/testing/assert"
"github.com/prysmaticlabs/prysm/v3/testing/require"
)
func TestGetRestJsonResponse_Valid(t *testing.T) {
const endpoint = "/example/rest/api/endpoint"
genesisJson := &rpcmiddleware.GenesisResponseJson{
Data: &rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "123",
GenesisValidatorsRoot: "0x456",
GenesisForkVersion: "0x789",
},
}
mux := http.NewServeMux()
mux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
// Make sure the url parameters match
assert.Equal(t, "abc", r.URL.Query().Get("arg1"))
assert.Equal(t, "def", r.URL.Query().Get("arg2"))
marshalledJson, err := json.Marshal(genesisJson)
require.NoError(t, err)
_, err = w.Write(marshalledJson)
require.NoError(t, err)
})
server := httptest.NewServer(mux)
defer server.Close()
jsonRestHandler := beaconApiJsonRestHandler{
httpClient: http.Client{Timeout: time.Second * 5},
host: server.URL,
}
responseJson := &rpcmiddleware.GenesisResponseJson{}
_, err := jsonRestHandler.GetRestJsonResponse(endpoint+"?arg1=abc&arg2=def", responseJson)
assert.NoError(t, err)
assert.DeepEqual(t, genesisJson, responseJson)
}
func TestGetRestJsonResponse_Error(t *testing.T) {
const endpoint = "/example/rest/api/endpoint"
testCases := []struct {
name string
funcHandler func(w http.ResponseWriter, r *http.Request)
expectedErrorJson *apimiddleware.DefaultErrorJson
expectedErrorMessage string
timeout time.Duration
responseJson interface{}
}{
{
name: "nil response json",
funcHandler: invalidJsonResponseHandler,
expectedErrorMessage: "responseJson is nil",
timeout: time.Second * 5,
responseJson: nil,
},
{
name: "400 error",
funcHandler: httpErrorJsonHandler(http.StatusBadRequest, "Bad request"),
expectedErrorMessage: "error 400: Bad request",
expectedErrorJson: &apimiddleware.DefaultErrorJson{
Code: http.StatusBadRequest,
Message: "Bad request",
},
timeout: time.Second * 5,
responseJson: &rpcmiddleware.GenesisResponseJson{},
},
{
name: "404 error",
funcHandler: httpErrorJsonHandler(http.StatusNotFound, "Not found"),
expectedErrorMessage: "error 404: Not found",
expectedErrorJson: &apimiddleware.DefaultErrorJson{
Code: http.StatusNotFound,
Message: "Not found",
},
timeout: time.Second * 5,
responseJson: &rpcmiddleware.GenesisResponseJson{},
},
{
name: "500 error",
funcHandler: httpErrorJsonHandler(http.StatusInternalServerError, "Internal server error"),
expectedErrorMessage: "error 500: Internal server error",
expectedErrorJson: &apimiddleware.DefaultErrorJson{
Code: http.StatusInternalServerError,
Message: "Internal server error",
},
timeout: time.Second * 5,
responseJson: &rpcmiddleware.GenesisResponseJson{},
},
{
name: "999 error",
funcHandler: httpErrorJsonHandler(999, "Invalid error"),
expectedErrorMessage: "error 999: Invalid error",
expectedErrorJson: &apimiddleware.DefaultErrorJson{
Code: 999,
Message: "Invalid error",
},
timeout: time.Second * 5,
responseJson: &rpcmiddleware.GenesisResponseJson{},
},
{
name: "bad error json formatting",
funcHandler: invalidJsonErrHandler,
expectedErrorMessage: "failed to decode error json",
timeout: time.Second * 5,
responseJson: &rpcmiddleware.GenesisResponseJson{},
},
{
name: "bad response json formatting",
funcHandler: invalidJsonResponseHandler,
expectedErrorMessage: "failed to decode response json",
timeout: time.Second * 5,
responseJson: &rpcmiddleware.GenesisResponseJson{},
},
{
name: "timeout",
funcHandler: httpErrorJsonHandler(http.StatusNotFound, "Not found"),
expectedErrorMessage: "failed to query REST API",
timeout: 1,
responseJson: &rpcmiddleware.GenesisResponseJson{},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc(endpoint, testCase.funcHandler)
server := httptest.NewServer(mux)
defer server.Close()
jsonRestHandler := beaconApiJsonRestHandler{
httpClient: http.Client{Timeout: testCase.timeout},
host: server.URL,
}
errorJson, err := jsonRestHandler.GetRestJsonResponse(endpoint, testCase.responseJson)
assert.ErrorContains(t, testCase.expectedErrorMessage, err)
assert.DeepEqual(t, testCase.expectedErrorJson, errorJson)
})
}
}
func httpErrorJsonHandler(statusCode int, errorMessage string) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
errorJson := &apimiddleware.DefaultErrorJson{
Code: statusCode,
Message: errorMessage,
}
marshalledError, err := json.Marshal(errorJson)
if err != nil {
panic(err)
}
w.WriteHeader(statusCode)
_, err = w.Write(marshalledError)
if err != nil {
panic(err)
}
}
}
func invalidJsonErrHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, err := w.Write([]byte("foo"))
if err != nil {
panic(err)
}
}
func invalidJsonResponseHandler(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("foo"))
if err != nil {
panic(err)
}
}

View File

@ -1,11 +1,13 @@
load("@prysm//tools/go:def.bzl", "go_library")
# gazelle:build_tags use_beacon_api
go_library(
name = "go_default_library",
srcs = ["genesis_mock.go"],
srcs = [
"genesis_mock.go",
"json_rest_handler_mock.go",
],
importpath = "github.com/prysmaticlabs/prysm/v3/validator/client/beacon-api/mock",
visibility = ["//validator:__subpackages__"],
visibility = ["//visibility:public"],
deps = [
"//api/gateway/apimiddleware:go_default_library",
"//beacon-chain/rpc/apimiddleware:go_default_library",

View File

@ -0,0 +1,50 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: validator/client/beacon-api/json_rest_handler.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/api/gateway/apimiddleware"
)
// MockjsonRestHandler is a mock of jsonRestHandler interface.
type MockjsonRestHandler struct {
ctrl *gomock.Controller
recorder *MockjsonRestHandlerMockRecorder
}
// MockjsonRestHandlerMockRecorder is the mock recorder for MockjsonRestHandler.
type MockjsonRestHandlerMockRecorder struct {
mock *MockjsonRestHandler
}
// NewMockjsonRestHandler creates a new mock instance.
func NewMockjsonRestHandler(ctrl *gomock.Controller) *MockjsonRestHandler {
mock := &MockjsonRestHandler{ctrl: ctrl}
mock.recorder = &MockjsonRestHandlerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockjsonRestHandler) EXPECT() *MockjsonRestHandlerMockRecorder {
return m.recorder
}
// GetRestJsonResponse mocks base method.
func (m *MockjsonRestHandler) GetRestJsonResponse(query string, responseJson interface{}) (*apimiddleware.DefaultErrorJson, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetRestJsonResponse", query, responseJson)
ret0, _ := ret[0].(*apimiddleware.DefaultErrorJson)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetRestJsonResponse indicates an expected call of GetRestJsonResponse.
func (mr *MockjsonRestHandlerMockRecorder) GetRestJsonResponse(query, responseJson interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRestJsonResponse", reflect.TypeOf((*MockjsonRestHandler)(nil).GetRestJsonResponse), query, responseJson)
}

View File

@ -0,0 +1,37 @@
//go:build use_beacon_api
// +build use_beacon_api
package beacon_api
import (
neturl "net/url"
"github.com/pkg/errors"
rpcmiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
)
func (c *beaconApiValidatorClient) getStateValidators(stringPubkeys []string) (*rpcmiddleware.StateValidatorsResponseJson, error) {
params := neturl.Values{}
for _, stringPubkey := range stringPubkeys {
params.Add("id", stringPubkey)
}
url := buildURL(
"/eth/v1/beacon/states/head/validators",
params,
)
stateValidatorsJson := &rpcmiddleware.StateValidatorsResponseJson{}
_, err := c.jsonRestHandler.GetRestJsonResponse(url, stateValidatorsJson)
if err != nil {
return nil, errors.Wrap(err, "failed to get json response")
}
if stateValidatorsJson.Data == nil {
return nil, errors.New("stateValidatorsJson.Data is nil")
}
return stateValidatorsJson, nil
}

View File

@ -0,0 +1,133 @@
//go:build use_beacon_api
// +build use_beacon_api
package beacon_api
import (
"strings"
"testing"
"github.com/golang/mock/gomock"
"github.com/pkg/errors"
rpcmiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
"github.com/prysmaticlabs/prysm/v3/testing/assert"
"github.com/prysmaticlabs/prysm/v3/testing/require"
"github.com/prysmaticlabs/prysm/v3/validator/client/beacon-api/mock"
)
func TestGetStateValidators_Nominal(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
url := strings.Join([]string{
"/eth/v1/beacon/states/head/validators?",
"id=0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13&", // active_ongoing
"id=0x80000e851c0f53c3246ff726d7ff7766661ca5e12a07c45c114d208d54f0f8233d4380b2e9aff759d69795d1df905526&", // active_exiting
"id=0x424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242&", // does not exist
"id=0x800015473bdc3a7f45ef8eb8abc598bc20021e55ad6e6ad1d745aaef9730dd2c28ec08bf42df18451de94dd4a6d24ec5", // exited_slashed
}, "")
stateValidatorsResponseJson := rpcmiddleware.StateValidatorsResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
wanted := []*rpcmiddleware.ValidatorContainerJson{
{
Index: "55293",
Status: "active_ongoing",
Validator: &rpcmiddleware.ValidatorJson{
PublicKey: "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13",
},
},
{
Index: "55294",
Status: "active_exiting",
Validator: &rpcmiddleware.ValidatorJson{
PublicKey: "0x80000e851c0f53c3246ff726d7ff7766661ca5e12a07c45c114d208d54f0f8233d4380b2e9aff759d69795d1df905526",
},
},
{
Index: "55295",
Status: "exited_slashed",
Validator: &rpcmiddleware.ValidatorJson{
PublicKey: "0x800015473bdc3a7f45ef8eb8abc598bc20021e55ad6e6ad1d745aaef9730dd2c28ec08bf42df18451de94dd4a6d24ec5",
},
},
}
jsonRestHandler.EXPECT().GetRestJsonResponse(
url,
&stateValidatorsResponseJson,
).Return(
nil,
nil,
).SetArg(
1,
rpcmiddleware.StateValidatorsResponseJson{
Data: wanted,
},
).Times(1)
validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
actual, err := validatorClient.getStateValidators([]string{
"0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13", // active_ongoing
"0x80000e851c0f53c3246ff726d7ff7766661ca5e12a07c45c114d208d54f0f8233d4380b2e9aff759d69795d1df905526", // active_exiting
"0x424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242", // does not exist
"0x800015473bdc3a7f45ef8eb8abc598bc20021e55ad6e6ad1d745aaef9730dd2c28ec08bf42df18451de94dd4a6d24ec5", // exited_slashed
})
require.NoError(t, err)
assert.DeepEqual(t, wanted, actual.Data)
}
func TestGetStateValidators_GetRestJsonResponseOnError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
url := "/eth/v1/beacon/states/head/validators?id=0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13"
stateValidatorsResponseJson := rpcmiddleware.StateValidatorsResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().GetRestJsonResponse(
url,
&stateValidatorsResponseJson,
).Return(
nil,
errors.New("an error"),
).Times(1)
validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
_, err := validatorClient.getStateValidators([]string{
"0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13", // active_ongoing
})
assert.ErrorContains(t, "an error", err)
assert.ErrorContains(t, "failed to get json response", err)
}
func TestGetStateValidators_DataIsNil(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
url := "/eth/v1/beacon/states/head/validators?id=0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13"
stateValidatorsResponseJson := rpcmiddleware.StateValidatorsResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().GetRestJsonResponse(
url,
&stateValidatorsResponseJson,
).Return(
nil,
nil,
).SetArg(
1,
rpcmiddleware.StateValidatorsResponseJson{
Data: nil,
},
).Times(1)
validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
_, err := validatorClient.getStateValidators([]string{
"0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13", // active_ongoing
})
assert.ErrorContains(t, "stateValidatorsJson.Data is nil", err)
}

View File

@ -5,27 +5,44 @@ package beacon_api
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/golang/mock/gomock"
"github.com/prysmaticlabs/prysm/v3/api/gateway/apimiddleware"
rpcmiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
"github.com/prysmaticlabs/prysm/v3/testing/assert"
"github.com/prysmaticlabs/prysm/v3/testing/require"
"github.com/prysmaticlabs/prysm/v3/validator/client/beacon-api/mock"
"google.golang.org/protobuf/types/known/emptypb"
)
func TestWaitForChainStart_ValidGenesis(t *testing.T) {
server := httptest.NewServer(createGenesisHandler(&rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "1234",
GenesisValidatorsRoot: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
}))
defer server.Close()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
validatorClient := NewBeaconApiValidatorClient(server.URL, time.Second*5)
genesisResponseJson := rpcmiddleware.GenesisResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().GetRestJsonResponse(
"/eth/v1/beacon/genesis",
&genesisResponseJson,
).Return(
nil,
nil,
).SetArg(
1,
rpcmiddleware.GenesisResponseJson{
Data: &rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "1234",
GenesisValidatorsRoot: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
},
},
).Times(1)
genesisProvider := beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler}
validatorClient := beaconApiValidatorClient{genesisProvider: genesisProvider}
resp, err := validatorClient.WaitForChainStart(context.Background(), &emptypb.Empty{})
assert.NoError(t, err)
@ -38,132 +55,131 @@ func TestWaitForChainStart_ValidGenesis(t *testing.T) {
assert.DeepEqual(t, expectedRoot, resp.GenesisValidatorsRoot)
}
func TestWaitForChainStart_NilData(t *testing.T) {
server := httptest.NewServer(createGenesisHandler(nil))
defer server.Close()
func TestWaitForChainStart_BadGenesis(t *testing.T) {
testCases := []struct {
name string
data *rpcmiddleware.GenesisResponse_GenesisJson
errorMessage string
}{
{
name: "nil data",
data: nil,
errorMessage: "failed to get genesis data",
},
{
name: "invalid time",
data: &rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "foo",
GenesisValidatorsRoot: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
},
errorMessage: "failed to parse genesis time: foo",
},
{
name: "invalid root",
data: &rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "1234",
GenesisValidatorsRoot: "0xzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
},
errorMessage: "invalid genesis validators root: ",
},
}
validatorClient := NewBeaconApiValidatorClient(server.URL, time.Second*5)
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
genesisResponseJson := rpcmiddleware.GenesisResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().GetRestJsonResponse(
"/eth/v1/beacon/genesis",
&genesisResponseJson,
).Return(
nil,
nil,
).SetArg(
1,
rpcmiddleware.GenesisResponseJson{
Data: testCase.data,
},
).Times(1)
genesisProvider := beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler}
validatorClient := beaconApiValidatorClient{genesisProvider: genesisProvider}
_, err := validatorClient.WaitForChainStart(context.Background(), &emptypb.Empty{})
assert.ErrorContains(t, testCase.errorMessage, err)
})
}
}
func TestWaitForChainStart_JsonResponseError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
genesisResponseJson := rpcmiddleware.GenesisResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().GetRestJsonResponse(
"/eth/v1/beacon/genesis",
&genesisResponseJson,
).Return(
nil,
errors.New("some specific json error"),
).Times(1)
genesisProvider := beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler}
validatorClient := beaconApiValidatorClient{genesisProvider: genesisProvider}
_, err := validatorClient.WaitForChainStart(context.Background(), &emptypb.Empty{})
assert.ErrorContains(t, "failed to get genesis data", err)
assert.ErrorContains(t, "some specific json error", err)
}
func TestWaitForChainStart_InvalidTime(t *testing.T) {
server := httptest.NewServer(createGenesisHandler(&rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "foo",
GenesisValidatorsRoot: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
}))
defer server.Close()
// For WaitForChainStart, error 404 just means that we keep retrying until the information becomes available
func TestWaitForChainStart_JsonResponseError404(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
validatorClient := NewBeaconApiValidatorClient(server.URL, time.Second*5)
_, err := validatorClient.WaitForChainStart(context.Background(), &emptypb.Empty{})
assert.ErrorContains(t, "failed to parse genesis time", err)
}
func TestWaitForChainStart_EmptyTime(t *testing.T) {
server := httptest.NewServer(createGenesisHandler(&rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "",
GenesisValidatorsRoot: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
}))
defer server.Close()
validatorClient := NewBeaconApiValidatorClient(server.URL, time.Second*5)
_, err := validatorClient.WaitForChainStart(context.Background(), &emptypb.Empty{})
assert.ErrorContains(t, "failed to parse genesis time", err)
}
func TestWaitForChainStart_InvalidRoot(t *testing.T) {
server := httptest.NewServer(createGenesisHandler(&rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "1234",
GenesisValidatorsRoot: "0xzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
}))
defer server.Close()
validatorClient := NewBeaconApiValidatorClient(server.URL, time.Second*5)
_, err := validatorClient.WaitForChainStart(context.Background(), &emptypb.Empty{})
assert.ErrorContains(t, "invalid genesis validators root", err)
}
func TestWaitForChainStart_EmptyRoot(t *testing.T) {
server := httptest.NewServer(createGenesisHandler(&rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "1234",
GenesisValidatorsRoot: "",
}))
defer server.Close()
validatorClient := NewBeaconApiValidatorClient(server.URL, time.Second*5)
_, err := validatorClient.WaitForChainStart(context.Background(), &emptypb.Empty{})
assert.ErrorContains(t, "invalid genesis validators root", err)
}
func TestWaitForChainStart_InvalidJsonGenesis(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("foo"))
require.NoError(t, err)
}))
defer server.Close()
validatorClient := NewBeaconApiValidatorClient(server.URL, time.Second*5)
_, err := validatorClient.WaitForChainStart(context.Background(), &emptypb.Empty{})
assert.ErrorContains(t, "failed to get genesis data", err)
}
func TestWaitForChainStart_InternalServerError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(internalServerErrHandler))
defer server.Close()
validatorClient := NewBeaconApiValidatorClient(server.URL, time.Second*5)
_, err := validatorClient.WaitForChainStart(context.Background(), &emptypb.Empty{})
assert.ErrorContains(t, "500: Internal server error", err)
}
func TestWaitForChainStart_NotFoundErrorContextCancelled(t *testing.T) {
// WaitForChainStart blocks until the error is not 404, but it needs to listen to context cancellations
server := httptest.NewServer(http.HandlerFunc(notFoundErrHandler))
defer server.Close()
validatorClient := NewBeaconApiValidatorClient(server.URL, time.Second*5)
// Create a context that can be canceled
ctx, cancel := context.WithCancel(context.Background())
// Cancel the context after 1 second
go func(ctx context.Context) {
time.Sleep(time.Second)
cancel()
}(ctx)
_, err := validatorClient.WaitForChainStart(ctx, &emptypb.Empty{})
assert.ErrorContains(t, "context canceled", err)
}
// This test makes sure that we handle even errors not specified in the Beacon API spec
func TestWaitForChainStart_UnknownError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(invalidErr999Handler))
defer server.Close()
validatorClient := NewBeaconApiValidatorClient(server.URL, time.Second*5)
_, err := validatorClient.WaitForChainStart(context.Background(), &emptypb.Empty{})
assert.ErrorContains(t, "999: Invalid error", err)
}
// Make sure that we fail gracefully if the error json is not valid json
func TestWaitForChainStart_InvalidJsonError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(invalidJsonErrHandler))
defer server.Close()
validatorClient := NewBeaconApiValidatorClient(server.URL, time.Second*5)
_, err := validatorClient.WaitForChainStart(context.Background(), &emptypb.Empty{})
assert.ErrorContains(t, "failed to get genesis data", err)
}
func TestWaitForChainStart_Timeout(t *testing.T) {
server := httptest.NewServer(createGenesisHandler(&rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "1234",
GenesisValidatorsRoot: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
}))
defer server.Close()
validatorClient := NewBeaconApiValidatorClient(server.URL, 1)
_, err := validatorClient.WaitForChainStart(context.Background(), &emptypb.Empty{})
assert.ErrorContains(t, "failed to get genesis data", err)
genesisResponseJson := rpcmiddleware.GenesisResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
// First, mock a request that receives a 404 error (which means that the genesis data is not available yet)
jsonRestHandler.EXPECT().GetRestJsonResponse(
"/eth/v1/beacon/genesis",
&genesisResponseJson,
).Return(
&apimiddleware.DefaultErrorJson{
Code: http.StatusNotFound,
Message: "404 error",
},
errors.New("404 error"),
).Times(1)
// After receiving a 404 error, mock a request that actually has genesis data available
jsonRestHandler.EXPECT().GetRestJsonResponse(
"/eth/v1/beacon/genesis",
&genesisResponseJson,
).Return(
nil,
nil,
).SetArg(
1,
rpcmiddleware.GenesisResponseJson{
Data: &rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "1234",
GenesisValidatorsRoot: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
},
},
).Times(1)
genesisProvider := beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler}
validatorClient := beaconApiValidatorClient{genesisProvider: genesisProvider}
resp, err := validatorClient.WaitForChainStart(context.Background(), &emptypb.Empty{})
assert.NoError(t, err)
require.NotNil(t, resp)
assert.Equal(t, true, resp.Started)
assert.Equal(t, uint64(1234), resp.GenesisTime)
expectedRoot, err := hexutil.Decode("0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2")
require.NoError(t, err)
assert.DeepEqual(t, expectedRoot, resp.GenesisValidatorsRoot)
}