Add REST implementation for CheckDoppelGanger (#11835)

* Add REST implementation for `MultipleValidatorStatus`

* Fix PR comments

* Address PR comments

* Add REST implementation for `CheckDoppelGanger`

* Use context

* Fix comments

* Fix PR comments

* Fix PR comments

* remove blank lines

* Fix comments

Co-authored-by: Radosław Kapka <rkapka@wp.pl>
Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com>
This commit is contained in:
Manu NALEPA 2023-01-18 17:13:45 +01:00 committed by GitHub
parent 30974039f3
commit 0f90bacac9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1495 additions and 6 deletions

View File

@ -10,10 +10,12 @@ go_library(
"beacon_block_json_helpers.go",
"beacon_block_proto_helpers.go",
"domain_data.go",
"doppelganger.go",
"genesis.go",
"get_beacon_block.go",
"index.go",
"json_rest_handler.go",
"log.go",
"prepare_beacon_proposer.go",
"propose_attestation.go",
"propose_beacon_block.go",
@ -37,9 +39,12 @@ go_library(
"//network/forks:go_default_library",
"//proto/engine/v1:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//runtime/version:go_default_library",
"//time/slots:go_default_library",
"//validator/client/iface:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@io_bazel_rules_go//proto/wkt:empty_go_proto",
"@org_golang_google_grpc//:go_default_library",
],
@ -56,6 +61,7 @@ go_test(
"beacon_block_json_helpers_test.go",
"beacon_block_proto_helpers_test.go",
"domain_data_test.go",
"doppelganger_test.go",
"genesis_test.go",
"get_beacon_block_altair_test.go",
"get_beacon_block_bellatrix_test.go",
@ -86,6 +92,7 @@ go_test(
deps = [
"//api/gateway/apimiddleware:go_default_library",
"//beacon-chain/rpc/apimiddleware:go_default_library",
"//beacon-chain/rpc/eth/helpers:go_default_library",
"//config/params:go_default_library",
"//consensus-types/primitives:go_default_library",
"//encoding/bytesutil:go_default_library",

View File

@ -1,11 +1,16 @@
package beacon_api
import (
"bytes"
"context"
"encoding/json"
"fmt"
neturl "net/url"
"regexp"
"strconv"
"github.com/pkg/errors"
"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"
@ -42,3 +47,81 @@ func buildURL(path string, queryParams ...neturl.Values) string {
return fmt.Sprintf("%s?%s", path, queryParams[0].Encode())
}
func (c *beaconApiValidatorClient) getFork(ctx context.Context) (*apimiddleware.StateForkResponseJson, error) {
const endpoint = "/eth/v1/beacon/states/head/fork"
stateForkResponseJson := &apimiddleware.StateForkResponseJson{}
_, err := c.jsonRestHandler.GetRestJsonResponse(
ctx,
endpoint,
stateForkResponseJson,
)
if err != nil {
return nil, errors.Wrapf(err, "failed to get json response from `%s` REST endpoint", endpoint)
}
return stateForkResponseJson, nil
}
func (c *beaconApiValidatorClient) getHeaders(ctx context.Context) (*apimiddleware.BlockHeadersResponseJson, error) {
const endpoint = "/eth/v1/beacon/headers"
blockHeadersResponseJson := &apimiddleware.BlockHeadersResponseJson{}
_, err := c.jsonRestHandler.GetRestJsonResponse(
ctx,
endpoint,
blockHeadersResponseJson,
)
if err != nil {
return nil, errors.Wrapf(err, "failed to get json response from `%s` REST endpoint", endpoint)
}
return blockHeadersResponseJson, nil
}
func (c *beaconApiValidatorClient) getLiveness(ctx context.Context, epoch types.Epoch, validatorIndexes []string) (*apimiddleware.LivenessResponseJson, error) {
const endpoint = "/eth/v1/validator/liveness/"
url := endpoint + strconv.FormatUint(uint64(epoch), 10)
livenessResponseJson := &apimiddleware.LivenessResponseJson{}
marshalledJsonValidatorIndexes, err := json.Marshal(validatorIndexes)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal validator indexes")
}
if _, err := c.jsonRestHandler.PostRestJson(ctx, url, nil, bytes.NewBuffer(marshalledJsonValidatorIndexes), livenessResponseJson); err != nil {
return nil, errors.Wrapf(err, "failed to send POST data to `%s` REST URL", url)
}
return livenessResponseJson, nil
}
func (c *beaconApiValidatorClient) getSyncing(ctx context.Context) (*apimiddleware.SyncingResponseJson, error) {
const endpoint = "/eth/v1/node/syncing"
syncingResponseJson := &apimiddleware.SyncingResponseJson{}
_, err := c.jsonRestHandler.GetRestJsonResponse(
ctx,
endpoint,
syncingResponseJson,
)
if err != nil {
return nil, errors.Wrapf(err, "failed to get json response from `%s` REST endpoint", endpoint)
}
return syncingResponseJson, nil
}
func (c *beaconApiValidatorClient) isSyncing(ctx context.Context) (bool, error) {
response, err := c.getSyncing(ctx)
if err != nil || response == nil || response.Data == nil {
return true, errors.Wrapf(err, "failed to get syncing status")
}
return response.Data.IsSyncing, err
}

View File

@ -1,11 +1,20 @@
package beacon_api
import (
"bytes"
"context"
"encoding/json"
"errors"
"net/url"
"testing"
"github.com/golang/mock/gomock"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/eth/helpers"
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"
)
func TestBeaconApiHelpers(t *testing.T) {
@ -80,3 +89,296 @@ func TestBuildURL_WithParams(t *testing.T) {
actual := buildURL("/aaa/bbb/ccc", params)
assert.Equal(t, wanted, actual)
}
const forkEndpoint = "/eth/v1/beacon/states/head/fork"
func TestGetFork_Nominal(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
stateForkResponseJson := apimiddleware.StateForkResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
expected := apimiddleware.StateForkResponseJson{
Data: &apimiddleware.ForkJson{
PreviousVersion: "0x1",
CurrentVersion: "0x2",
Epoch: "3",
},
}
ctx := context.Background()
jsonRestHandler.EXPECT().GetRestJsonResponse(
ctx,
forkEndpoint,
&stateForkResponseJson,
).Return(
nil,
nil,
).SetArg(
2,
expected,
).Times(1)
validatorClient := beaconApiValidatorClient{
jsonRestHandler: jsonRestHandler,
}
fork, err := validatorClient.getFork(ctx)
require.NoError(t, err)
assert.DeepEqual(t, &expected, fork)
}
func TestGetFork_Invalid(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
ctx := context.Background()
jsonRestHandler.EXPECT().GetRestJsonResponse(
ctx,
forkEndpoint,
gomock.Any(),
).Return(
nil,
errors.New("custom error"),
).Times(1)
validatorClient := beaconApiValidatorClient{
jsonRestHandler: jsonRestHandler,
}
_, err := validatorClient.getFork(ctx)
require.ErrorContains(t, "failed to get json response from `/eth/v1/beacon/states/head/fork` REST endpoint", err)
}
const headersEndpoint = "/eth/v1/beacon/headers"
func TestGetHeaders_Nominal(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
blockHeadersResponseJson := apimiddleware.BlockHeadersResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
expected := apimiddleware.BlockHeadersResponseJson{
Data: []*apimiddleware.BlockHeaderContainerJson{
{
Header: &apimiddleware.BeaconBlockHeaderContainerJson{
Message: &apimiddleware.BeaconBlockHeaderJson{
Slot: "42",
},
},
},
},
}
ctx := context.Background()
jsonRestHandler.EXPECT().GetRestJsonResponse(
ctx,
headersEndpoint,
&blockHeadersResponseJson,
).Return(
nil,
nil,
).SetArg(
2,
expected,
).Times(1)
validatorClient := beaconApiValidatorClient{
jsonRestHandler: jsonRestHandler,
}
headers, err := validatorClient.getHeaders(ctx)
require.NoError(t, err)
assert.DeepEqual(t, &expected, headers)
}
func TestGetHeaders_Invalid(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
ctx := context.Background()
jsonRestHandler.EXPECT().GetRestJsonResponse(
ctx,
headersEndpoint,
gomock.Any(),
).Return(
nil,
errors.New("custom error"),
).Times(1)
validatorClient := beaconApiValidatorClient{
jsonRestHandler: jsonRestHandler,
}
_, err := validatorClient.getHeaders(ctx)
require.ErrorContains(t, "failed to get json response from `/eth/v1/beacon/headers` REST endpoint", err)
}
const livenessEndpoint = "/eth/v1/validator/liveness/42"
func TestGetLiveness_Nominal(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
livenessResponseJson := apimiddleware.LivenessResponseJson{}
indexes := []string{"1", "2"}
marshalledIndexes, err := json.Marshal(indexes)
require.NoError(t, err)
expected := apimiddleware.LivenessResponseJson{
Data: []*struct {
Index string `json:"index"`
IsLive bool `json:"is_live"`
}{
{
Index: "1",
IsLive: true,
},
{
Index: "2",
IsLive: false,
},
},
}
ctx := context.Background()
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().PostRestJson(
ctx,
livenessEndpoint,
nil,
bytes.NewBuffer(marshalledIndexes),
&livenessResponseJson,
).SetArg(
4,
expected,
).Return(
nil,
nil,
).Times(1)
validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
liveness, err := validatorClient.getLiveness(ctx, 42, indexes)
require.NoError(t, err)
assert.DeepEqual(t, &expected, liveness)
}
func TestGetLiveness_Invalid(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.Background()
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().PostRestJson(
ctx,
livenessEndpoint,
nil,
gomock.Any(),
gomock.Any(),
).Return(
nil,
errors.New("custom error"),
).Times(1)
validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
_, err := validatorClient.getLiveness(ctx, 42, nil)
require.ErrorContains(t, "failed to send POST data to `/eth/v1/validator/liveness/42` REST URL", err)
}
const syncingEnpoint = "/eth/v1/node/syncing"
func TestGetIsSyncing_Nominal(t *testing.T) {
testCases := []struct {
name string
isSyncing bool
}{
{
name: "Syncing",
isSyncing: true,
},
{
name: "Not syncing",
isSyncing: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
syncingResponseJson := apimiddleware.SyncingResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
expected := apimiddleware.SyncingResponseJson{
Data: &helpers.SyncDetailsJson{
IsSyncing: testCase.isSyncing,
},
}
ctx := context.Background()
jsonRestHandler.EXPECT().GetRestJsonResponse(
ctx,
syncingEnpoint,
&syncingResponseJson,
).Return(
nil,
nil,
).SetArg(
2,
expected,
).Times(1)
validatorClient := beaconApiValidatorClient{
jsonRestHandler: jsonRestHandler,
}
isSyncing, err := validatorClient.isSyncing(ctx)
require.NoError(t, err)
assert.Equal(t, testCase.isSyncing, isSyncing)
})
}
}
func TestGetIsSyncing_Invalid(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
syncingResponseJson := apimiddleware.SyncingResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
ctx := context.Background()
jsonRestHandler.EXPECT().GetRestJsonResponse(
ctx,
syncingEnpoint,
&syncingResponseJson,
).Return(
nil,
errors.New("custom error"),
).Times(1)
validatorClient := beaconApiValidatorClient{
jsonRestHandler: jsonRestHandler,
}
isSyncing, err := validatorClient.isSyncing(ctx)
assert.Equal(t, true, isSyncing)
assert.ErrorContains(t, "failed to get syncing status", err)
}

View File

@ -48,12 +48,7 @@ func (c *beaconApiValidatorClient) GetDuties(ctx context.Context, in *ethpb.Duti
}
func (c *beaconApiValidatorClient) CheckDoppelGanger(ctx context.Context, in *ethpb.DoppelGangerRequest) (*ethpb.DoppelGangerResponse, error) {
if c.fallbackClient != nil {
return c.fallbackClient.CheckDoppelGanger(ctx, in)
}
// TODO: Implement me
panic("beaconApiValidatorClient.CheckDoppelGanger is not implemented. To use a fallback client, create this validator with NewBeaconApiValidatorClientWithFallback instead.")
return c.checkDoppelGanger(ctx, in)
}
func (c *beaconApiValidatorClient) DomainData(ctx context.Context, in *ethpb.DomainRequest) (*ethpb.DomainResponse, error) {

View File

@ -0,0 +1,238 @@
package beacon_api
import (
"context"
"encoding/binary"
"fmt"
"strconv"
"github.com/pkg/errors"
"github.com/ethereum/go-ethereum/common/hexutil"
types "github.com/prysmaticlabs/prysm/v3/consensus-types/primitives"
ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v3/runtime/version"
"github.com/prysmaticlabs/prysm/v3/time/slots"
)
type DoppelGangerInfo struct {
validatorEpoch types.Epoch
response *ethpb.DoppelGangerResponse_ValidatorResponse
}
func (c *beaconApiValidatorClient) checkDoppelGanger(ctx context.Context, in *ethpb.DoppelGangerRequest) (*ethpb.DoppelGangerResponse, error) {
// Check if there is any doppelganger validator for the last 2 epochs.
// - Check if the beacon node is synced
// - If we are in Phase0, we consider there is no doppelganger.
// - If all validators we want to check doppelganger existence were live in local antislashing
// database for the last 2 epochs, we consider there is no doppelganger.
// This is typically the case when we reboot the validator client.
// - If some validators we want to check doppelganger existence were NOT live
// in local antislashing for the last two epochs, then we check onchain if there is
// some liveness for these validators. If yes, we consider there is a doppelganger.
// Check inputs are correct.
if in == nil || in.ValidatorRequests == nil || len(in.ValidatorRequests) == 0 {
return &ethpb.DoppelGangerResponse{
Responses: []*ethpb.DoppelGangerResponse_ValidatorResponse{},
}, nil
}
validatorRequests := in.ValidatorRequests
// Prepare response.
stringPubKeys := make([]string, len(validatorRequests))
stringPubKeyToDoppelGangerInfo := make(map[string]DoppelGangerInfo, len(validatorRequests))
for i, vr := range validatorRequests {
if vr == nil {
return nil, errors.New("validator request is nil")
}
pubKey := vr.PublicKey
stringPubKey := hexutil.Encode(pubKey)
stringPubKeys[i] = stringPubKey
stringPubKeyToDoppelGangerInfo[stringPubKey] = DoppelGangerInfo{
validatorEpoch: vr.Epoch,
response: &ethpb.DoppelGangerResponse_ValidatorResponse{
PublicKey: pubKey,
DuplicateExists: false,
},
}
}
// Check if the beacon node if synced.
isSyncing, err := c.isSyncing(ctx)
if err != nil {
return nil, errors.Wrapf(err, "failed to get beacon node sync status")
}
if isSyncing {
return nil, errors.New("beacon node not synced")
}
// Retrieve fork version -- Return early if we are in phase0.
forkResponse, err := c.getFork(ctx)
if err != nil || forkResponse == nil || forkResponse.Data == nil {
return nil, errors.Wrapf(err, "failed to get fork")
}
forkVersionBytes, err := hexutil.Decode(forkResponse.Data.CurrentVersion)
if err != nil {
return nil, errors.Wrapf(err, "failed to decode fork version")
}
forkVersion := binary.LittleEndian.Uint32(forkVersionBytes)
if forkVersion == version.Phase0 {
log.Info("Skipping doppelganger check for Phase 0")
return buildResponse(stringPubKeys, stringPubKeyToDoppelGangerInfo), nil
}
// Retrieve current epoch.
headers, err := c.getHeaders(ctx)
if err != nil || headers == nil || headers.Data == nil || len(headers.Data) == 0 ||
headers.Data[0].Header == nil || headers.Data[0].Header.Message == nil {
return nil, errors.Wrapf(err, "failed to get headers")
}
headSlotUint64, err := strconv.ParseUint(headers.Data[0].Header.Message.Slot, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse head slot")
}
headSlot := types.Slot(headSlotUint64)
currentEpoch := slots.ToEpoch(headSlot)
// Extract input pubkeys we did not validate for the 2 last epochs.
// If we detect onchain liveness for these keys during the 2 last epochs, a doppelganger may exist somewhere.
var notRecentStringPubKeys []string
for _, spk := range stringPubKeys {
dph, ok := stringPubKeyToDoppelGangerInfo[spk]
if !ok {
return nil, errors.New("failed to retrieve doppelganger info from string public key")
}
if dph.validatorEpoch+2 < currentEpoch {
notRecentStringPubKeys = append(notRecentStringPubKeys, spk)
}
}
// If all provided keys are recent (aka `notRecentPubKeys` is empty) we return early
// as we are unable to effectively determine if a doppelganger is active.
if len(notRecentStringPubKeys) == 0 {
return buildResponse(stringPubKeys, stringPubKeyToDoppelGangerInfo), nil
}
// Retrieve correspondence between validator pubkey and index.
stateValidators, err := c.stateValidatorsProvider.GetStateValidators(ctx, notRecentStringPubKeys, nil, nil)
if err != nil || stateValidators == nil || stateValidators.Data == nil {
return nil, errors.Wrapf(err, "failed to get state validators")
}
validators := stateValidators.Data
stringPubKeyToIndex := make(map[string]string, len(validators))
indexes := make([]string, len(validators))
for i, v := range validators {
if v == nil {
return nil, errors.New("validator container is nil")
}
index := v.Index
if v.Validator == nil {
return nil, errors.New("validator is nil")
}
stringPubKeyToIndex[v.Validator.PublicKey] = index
indexes[i] = index
}
// Get validators liveness for the the last epoch.
// We request a state 1 epoch ago. We are guaranteed to have currentEpoch > 2
// since we assume that we are not in phase0.
previousEpoch := currentEpoch - 1
indexToPreviousLiveness, err := c.getIndexToLiveness(ctx, previousEpoch, indexes)
if err != nil {
return nil, errors.Wrapf(err, "failed to get map from validator index to liveness for previous epoch %d", previousEpoch)
}
// Get validators liveness for the current epoch.
indexToCurrentLiveness, err := c.getIndexToLiveness(ctx, currentEpoch, indexes)
if err != nil {
return nil, errors.Wrapf(err, "failed to get map from validator index to liveness for current epoch %d", currentEpoch)
}
// Set `DuplicateExists` to `true` if needed.
for _, spk := range notRecentStringPubKeys {
index, ok := stringPubKeyToIndex[spk]
if !ok {
// if !ok, the validator corresponding to `stringPubKey` does not exist onchain.
continue
}
previousLiveness, ok := indexToPreviousLiveness[index]
if !ok {
return nil, fmt.Errorf("failed to retrieve liveness for previous epoch `%d` for validator index `%s`", previousEpoch, index)
}
if previousLiveness {
log.WithField("pubkey", spk).WithField("epoch", previousEpoch).Warn("Doppelganger found")
}
currentLiveness, ok := indexToCurrentLiveness[index]
if !ok {
return nil, fmt.Errorf("failed to retrieve liveness for current epoch `%d` for validator index `%s`", currentEpoch, index)
}
if currentLiveness {
log.WithField("pubkey", spk).WithField("epoch", currentEpoch).Warn("Doppelganger found")
}
globalLiveness := previousLiveness || currentLiveness
if globalLiveness {
stringPubKeyToDoppelGangerInfo[spk].response.DuplicateExists = true
}
}
return buildResponse(stringPubKeys, stringPubKeyToDoppelGangerInfo), nil
}
func buildResponse(
stringPubKeys []string,
stringPubKeyToDoppelGangerHelper map[string]DoppelGangerInfo,
) *ethpb.DoppelGangerResponse {
responses := make([]*ethpb.DoppelGangerResponse_ValidatorResponse, len(stringPubKeys))
for i, spk := range stringPubKeys {
responses[i] = stringPubKeyToDoppelGangerHelper[spk].response
}
return &ethpb.DoppelGangerResponse{
Responses: responses,
}
}
func (c *beaconApiValidatorClient) getIndexToLiveness(ctx context.Context, epoch types.Epoch, indexes []string) (map[string]bool, error) {
livenessResponse, err := c.getLiveness(ctx, epoch, indexes)
if err != nil || livenessResponse.Data == nil {
return nil, errors.Wrapf(err, fmt.Sprintf("failed to get liveness for epoch %d", epoch))
}
indexToLiveness := make(map[string]bool, len(livenessResponse.Data))
for _, liveness := range livenessResponse.Data {
if liveness == nil {
return nil, errors.New("liveness is nil")
}
indexToLiveness[liveness.Index] = liveness.IsLive
}
return indexToLiveness, nil
}

View File

@ -0,0 +1,859 @@
package beacon_api
import (
"bytes"
"context"
"encoding/json"
"testing"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/golang/mock/gomock"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/eth/helpers"
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"
)
func TestCheckDoppelGanger_Nominal(t *testing.T) {
const stringPubKey1 = "0x80000e851c0f53c3246ff726d7ff7766661ca5e12a07c45c114d208d54f0f8233d4380b2e9aff759d69795d1df905526"
const stringPubKey2 = "0x80002662ecb857da7a37ed468291cb248979eca5131db56c20843262f7909220c296e18f59af1726ef86ec15c08b8317"
const stringPubKey3 = "0x80003a1c67216514e4ab257738e59ef38063edf43bc4a2ef9d38633bdde117384401684c6cf81aa04cf18890e75ab52c"
const stringPubKey4 = "0x80007e05ba643a3e5be65d1595154023dc2cf009626f32ab1054c5225a6beb28b8be3d52a463ab45f698df884614c87d"
const stringPubKey5 = "0x80006ab8cd402459b445b2f5f955c9bae550bc269717837a8cd68176ce42a21fd372b844d508711d6e0bb0efe65abfe5"
const stringPubKey6 = "0x800077c436fc0c57bec2b91509519deadeed235f35f6377e7865e17ee86271120381a49c643829be12d232a4ba8360d2"
pubKey1, err := hexutil.Decode(stringPubKey1)
require.NoError(t, err)
pubKey2, err := hexutil.Decode(stringPubKey2)
require.NoError(t, err)
pubKey3, err := hexutil.Decode(stringPubKey3)
require.NoError(t, err)
pubKey4, err := hexutil.Decode(stringPubKey4)
require.NoError(t, err)
pubKey5, err := hexutil.Decode(stringPubKey5)
require.NoError(t, err)
pubKey6, err := hexutil.Decode(stringPubKey6)
require.NoError(t, err)
testCases := []struct {
name string
doppelGangerInput *ethpb.DoppelGangerRequest
doppelGangerExpectedOutput *ethpb.DoppelGangerResponse
getSyncingOutput *apimiddleware.SyncingResponseJson
getForkOutput *apimiddleware.StateForkResponseJson
getHeadersOutput *apimiddleware.BlockHeadersResponseJson
getStateValidatorsInterface *struct {
input []string
output *apimiddleware.StateValidatorsResponseJson
}
getLivelinessInterfaces []struct {
inputUrl string
inputStringIndexes []string
output *apimiddleware.LivenessResponseJson
}
}{
{
name: "nil input",
doppelGangerInput: nil,
doppelGangerExpectedOutput: &ethpb.DoppelGangerResponse{
Responses: []*ethpb.DoppelGangerResponse_ValidatorResponse{},
},
},
{
name: "nil validator requests",
doppelGangerInput: &ethpb.DoppelGangerRequest{
ValidatorRequests: nil,
},
doppelGangerExpectedOutput: &ethpb.DoppelGangerResponse{
Responses: []*ethpb.DoppelGangerResponse_ValidatorResponse{},
},
},
{
name: "empty validator requests",
doppelGangerInput: &ethpb.DoppelGangerRequest{
ValidatorRequests: []*ethpb.DoppelGangerRequest_ValidatorRequest{},
},
doppelGangerExpectedOutput: &ethpb.DoppelGangerResponse{
Responses: []*ethpb.DoppelGangerResponse_ValidatorResponse{},
},
},
{
name: "phase0",
doppelGangerInput: &ethpb.DoppelGangerRequest{
ValidatorRequests: []*ethpb.DoppelGangerRequest_ValidatorRequest{
{PublicKey: pubKey1},
{PublicKey: pubKey2},
{PublicKey: pubKey3},
{PublicKey: pubKey4},
{PublicKey: pubKey5},
{PublicKey: pubKey6},
},
},
doppelGangerExpectedOutput: &ethpb.DoppelGangerResponse{
Responses: []*ethpb.DoppelGangerResponse_ValidatorResponse{
{PublicKey: pubKey1, DuplicateExists: false},
{PublicKey: pubKey2, DuplicateExists: false},
{PublicKey: pubKey3, DuplicateExists: false},
{PublicKey: pubKey4, DuplicateExists: false},
{PublicKey: pubKey5, DuplicateExists: false},
{PublicKey: pubKey6, DuplicateExists: false},
},
},
getSyncingOutput: &apimiddleware.SyncingResponseJson{
Data: &helpers.SyncDetailsJson{
IsSyncing: false,
},
},
getForkOutput: &apimiddleware.StateForkResponseJson{
Data: &apimiddleware.ForkJson{
PreviousVersion: "0x00000000",
CurrentVersion: "0x00000000",
Epoch: "42",
},
},
},
{
name: "all validators are recent",
doppelGangerInput: &ethpb.DoppelGangerRequest{
ValidatorRequests: []*ethpb.DoppelGangerRequest_ValidatorRequest{
{PublicKey: pubKey1, Epoch: 2},
{PublicKey: pubKey2, Epoch: 2},
{PublicKey: pubKey3, Epoch: 2},
{PublicKey: pubKey4, Epoch: 2},
{PublicKey: pubKey5, Epoch: 2},
{PublicKey: pubKey6, Epoch: 2},
},
},
doppelGangerExpectedOutput: &ethpb.DoppelGangerResponse{
Responses: []*ethpb.DoppelGangerResponse_ValidatorResponse{
{PublicKey: pubKey1, DuplicateExists: false},
{PublicKey: pubKey2, DuplicateExists: false},
{PublicKey: pubKey3, DuplicateExists: false},
{PublicKey: pubKey4, DuplicateExists: false},
{PublicKey: pubKey5, DuplicateExists: false},
{PublicKey: pubKey6, DuplicateExists: false},
},
},
getSyncingOutput: &apimiddleware.SyncingResponseJson{
Data: &helpers.SyncDetailsJson{
IsSyncing: false,
},
},
getForkOutput: &apimiddleware.StateForkResponseJson{
Data: &apimiddleware.ForkJson{
PreviousVersion: "0x01000000",
CurrentVersion: "0x02000000",
Epoch: "2",
},
},
getHeadersOutput: &apimiddleware.BlockHeadersResponseJson{
Data: []*apimiddleware.BlockHeaderContainerJson{
{
Header: &apimiddleware.BeaconBlockHeaderContainerJson{
Message: &apimiddleware.BeaconBlockHeaderJson{
Slot: "99",
},
},
},
},
},
},
{
name: "some validators are recent, some not, some duplicates",
doppelGangerInput: &ethpb.DoppelGangerRequest{
ValidatorRequests: []*ethpb.DoppelGangerRequest_ValidatorRequest{
{PublicKey: pubKey1, Epoch: 99}, // recent
{PublicKey: pubKey2, Epoch: 80}, // not recent - duplicate on previous epoch
{PublicKey: pubKey3, Epoch: 80}, // not recent - duplicate on current epoch
{PublicKey: pubKey4, Epoch: 80}, // not recent - duplicate on both previous and current epoch
{PublicKey: pubKey5, Epoch: 80}, // non existing validator
{PublicKey: pubKey6, Epoch: 80}, // not recent - not duplicate
},
},
doppelGangerExpectedOutput: &ethpb.DoppelGangerResponse{
Responses: []*ethpb.DoppelGangerResponse_ValidatorResponse{
{PublicKey: pubKey1, DuplicateExists: false}, // recent
{PublicKey: pubKey2, DuplicateExists: true}, // not recent - duplicate on previous epoch
{PublicKey: pubKey3, DuplicateExists: true}, // not recent - duplicate on current epoch
{PublicKey: pubKey4, DuplicateExists: true}, // not recent - duplicate on both previous and current epoch
{PublicKey: pubKey5, DuplicateExists: false}, // non existing validator
{PublicKey: pubKey6, DuplicateExists: false}, // not recent - not duplicate
},
},
getSyncingOutput: &apimiddleware.SyncingResponseJson{
Data: &helpers.SyncDetailsJson{
IsSyncing: false,
},
},
getForkOutput: &apimiddleware.StateForkResponseJson{
Data: &apimiddleware.ForkJson{
PreviousVersion: "0x01000000",
CurrentVersion: "0x02000000",
Epoch: "2",
},
},
getHeadersOutput: &apimiddleware.BlockHeadersResponseJson{
Data: []*apimiddleware.BlockHeaderContainerJson{
{
Header: &apimiddleware.BeaconBlockHeaderContainerJson{
Message: &apimiddleware.BeaconBlockHeaderJson{
Slot: "3201",
},
},
},
},
},
getStateValidatorsInterface: &struct {
input []string
output *apimiddleware.StateValidatorsResponseJson
}{
input: []string{
// no stringPubKey1 since recent
stringPubKey2, // not recent - duplicate on previous epoch
stringPubKey3, // not recent - duplicate on current epoch
stringPubKey4, // not recent - duplicate on both previous and current epoch
stringPubKey5, // non existing validator
stringPubKey6, // not recent - not duplicate
},
output: &apimiddleware.StateValidatorsResponseJson{
Data: []*apimiddleware.ValidatorContainerJson{
// No "11111" since corresponding validator is recent
{Index: "22222", Validator: &apimiddleware.ValidatorJson{PublicKey: stringPubKey2}}, // not recent - duplicate on previous epoch
{Index: "33333", Validator: &apimiddleware.ValidatorJson{PublicKey: stringPubKey3}}, // not recent - duplicate on current epoch
{Index: "44444", Validator: &apimiddleware.ValidatorJson{PublicKey: stringPubKey4}}, // not recent - duplicate on both previous and current epoch
// No "55555" sicee corresponding validator does not exist
{Index: "66666", Validator: &apimiddleware.ValidatorJson{PublicKey: stringPubKey6}}, // not recent - not duplicate
},
},
},
getLivelinessInterfaces: []struct {
inputUrl string
inputStringIndexes []string
output *apimiddleware.LivenessResponseJson
}{
{
inputUrl: "/eth/v1/validator/liveness/99", // previous epoch
inputStringIndexes: []string{
// No "11111" since corresponding validator is recent
"22222", // not recent - duplicate on previous epoch
"33333", // not recent - duplicate on current epoch
"44444", // not recent - duplicate on both previous and current epoch
// No "55555" since corresponding validator it does not exist
"66666", // not recent - not duplicate
},
output: &apimiddleware.LivenessResponseJson{
Data: []*struct {
Index string `json:"index"`
IsLive bool `json:"is_live"`
}{
// No "11111" since corresponding validator is recent
{Index: "22222", IsLive: true}, // not recent - duplicate on previous epoch
{Index: "33333", IsLive: false}, // not recent - duplicate on current epoch
{Index: "44444", IsLive: true}, // not recent - duplicate on both previous and current epoch
// No "55555" since corresponding validator it does not exist
{Index: "66666", IsLive: false}, // not recent - not duplicate
},
},
},
{
inputUrl: "/eth/v1/validator/liveness/100", // current epoch
inputStringIndexes: []string{
// No "11111" since corresponding validator is recent
"22222", // not recent - duplicate on previous epoch
"33333", // not recent - duplicate on current epoch
"44444", // not recent - duplicate on both previous and current epoch
// No "55555" since corresponding validator it does not exist
"66666", // not recent - not duplicate
},
output: &apimiddleware.LivenessResponseJson{
Data: []*struct {
Index string `json:"index"`
IsLive bool `json:"is_live"`
}{
// No "11111" since corresponding validator is recent
{Index: "22222", IsLive: false}, // not recent - duplicate on previous epoch
{Index: "33333", IsLive: true}, // not recent - duplicate on current epoch
{Index: "44444", IsLive: true}, // not recent - duplicate on both previous and current epoch
// No "55555" since corresponding validator it does not exist
{Index: "66666", IsLive: false}, // not recent - not duplicate
},
},
},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
ctx := context.Background()
if testCase.getSyncingOutput != nil {
syncingResponseJson := apimiddleware.SyncingResponseJson{}
jsonRestHandler.EXPECT().GetRestJsonResponse(
ctx,
syncingEnpoint,
&syncingResponseJson,
).Return(
nil,
nil,
).SetArg(
2,
*testCase.getSyncingOutput,
).Times(1)
}
if testCase.getForkOutput != nil {
stateForkResponseJson := apimiddleware.StateForkResponseJson{}
jsonRestHandler.EXPECT().GetRestJsonResponse(
ctx,
forkEndpoint,
&stateForkResponseJson,
).Return(
nil,
nil,
).SetArg(
2,
*testCase.getForkOutput,
).Times(1)
}
if testCase.getHeadersOutput != nil {
blockHeadersResponseJson := apimiddleware.BlockHeadersResponseJson{}
jsonRestHandler.EXPECT().GetRestJsonResponse(
ctx,
headersEndpoint,
&blockHeadersResponseJson,
).Return(
nil,
nil,
).SetArg(
2,
*testCase.getHeadersOutput,
).Times(1)
}
if testCase.getLivelinessInterfaces != nil {
for _, iface := range testCase.getLivelinessInterfaces {
livenessResponseJson := apimiddleware.LivenessResponseJson{}
marshalledIndexes, err := json.Marshal(iface.inputStringIndexes)
require.NoError(t, err)
jsonRestHandler.EXPECT().PostRestJson(
ctx,
iface.inputUrl,
nil,
bytes.NewBuffer(marshalledIndexes),
&livenessResponseJson,
).SetArg(
4,
*iface.output,
).Return(
nil,
nil,
).Times(1)
}
}
stateValidatorsProvider := mock.NewMockstateValidatorsProvider(ctrl)
if testCase.getStateValidatorsInterface != nil {
stateValidatorsProvider.EXPECT().GetStateValidators(
ctx,
testCase.getStateValidatorsInterface.input,
nil,
nil,
).Return(
testCase.getStateValidatorsInterface.output,
nil,
).Times(1)
}
validatorClient := beaconApiValidatorClient{
jsonRestHandler: jsonRestHandler,
stateValidatorsProvider: stateValidatorsProvider,
}
doppelGangerActualOutput, err := validatorClient.CheckDoppelGanger(
context.Background(),
testCase.doppelGangerInput,
)
require.DeepEqual(t, testCase.doppelGangerExpectedOutput, doppelGangerActualOutput)
assert.NoError(t, err)
})
}
}
func TestCheckDoppelGanger_Errors(t *testing.T) {
const stringPubKey = "0x80000e851c0f53c3246ff726d7ff7766661ca5e12a07c45c114d208d54f0f8233d4380b2e9aff759d69795d1df905526"
pubKey, err := hexutil.Decode(stringPubKey)
require.NoError(t, err)
standardInputValidatorRequests := []*ethpb.DoppelGangerRequest_ValidatorRequest{
{
PublicKey: pubKey,
Epoch: 1,
},
}
standardGetSyncingOutput := &apimiddleware.SyncingResponseJson{
Data: &helpers.SyncDetailsJson{
IsSyncing: false,
},
}
standardGetForkOutput := &apimiddleware.StateForkResponseJson{
Data: &apimiddleware.ForkJson{
CurrentVersion: "0x02000000",
},
}
standardGetHeadersOutput := &apimiddleware.BlockHeadersResponseJson{
Data: []*apimiddleware.BlockHeaderContainerJson{
{
Header: &apimiddleware.BeaconBlockHeaderContainerJson{
Message: &apimiddleware.BeaconBlockHeaderJson{
Slot: "1000",
},
},
},
},
}
standardGetStateValidatorsInterface := &struct {
input []string
output *apimiddleware.StateValidatorsResponseJson
err error
}{
input: []string{stringPubKey},
output: &apimiddleware.StateValidatorsResponseJson{
Data: []*apimiddleware.ValidatorContainerJson{
{
Index: "42",
Validator: &apimiddleware.ValidatorJson{
PublicKey: stringPubKey,
},
},
},
},
}
testCases := []struct {
name string
expectedErrorMessage string
inputValidatorRequests []*ethpb.DoppelGangerRequest_ValidatorRequest
getSyncingOutput *apimiddleware.SyncingResponseJson
getSyncingError error
getForkOutput *apimiddleware.StateForkResponseJson
getForkError error
getHeadersOutput *apimiddleware.BlockHeadersResponseJson
getHeadersError error
getStateValidatorsInterface *struct {
input []string
output *apimiddleware.StateValidatorsResponseJson
err error
}
getLivenessInterfaces []struct {
inputUrl string
inputStringIndexes []string
output *apimiddleware.LivenessResponseJson
err error
}
}{
{
name: "nil validatorRequest",
expectedErrorMessage: "validator request is nil",
inputValidatorRequests: []*ethpb.DoppelGangerRequest_ValidatorRequest{nil},
},
{
name: "isSyncing on error",
expectedErrorMessage: "failed to get beacon node sync status",
inputValidatorRequests: standardInputValidatorRequests,
getSyncingOutput: standardGetSyncingOutput,
getSyncingError: errors.New("custom error"),
},
{
name: "beacon node not synced",
expectedErrorMessage: "beacon node not synced",
inputValidatorRequests: standardInputValidatorRequests,
getSyncingOutput: &apimiddleware.SyncingResponseJson{
Data: &helpers.SyncDetailsJson{
IsSyncing: true,
},
},
},
{
name: "getFork on error",
expectedErrorMessage: "failed to get fork",
inputValidatorRequests: standardInputValidatorRequests,
getSyncingOutput: standardGetSyncingOutput,
getForkOutput: &apimiddleware.StateForkResponseJson{},
getForkError: errors.New("custom error"),
},
{
name: "cannot decode fork version",
expectedErrorMessage: "failed to decode fork version",
inputValidatorRequests: standardInputValidatorRequests,
getSyncingOutput: standardGetSyncingOutput,
getForkOutput: &apimiddleware.StateForkResponseJson{Data: &apimiddleware.ForkJson{CurrentVersion: "not a version"}},
},
{
name: "get headers on error",
expectedErrorMessage: "failed to get headers",
inputValidatorRequests: standardInputValidatorRequests,
getSyncingOutput: standardGetSyncingOutput,
getForkOutput: standardGetForkOutput,
getHeadersOutput: &apimiddleware.BlockHeadersResponseJson{},
getHeadersError: errors.New("custom error"),
},
{
name: "cannot parse head slot",
expectedErrorMessage: "failed to parse head slot",
inputValidatorRequests: standardInputValidatorRequests,
getSyncingOutput: standardGetSyncingOutput,
getForkOutput: standardGetForkOutput,
getHeadersOutput: &apimiddleware.BlockHeadersResponseJson{
Data: []*apimiddleware.BlockHeaderContainerJson{
{
Header: &apimiddleware.BeaconBlockHeaderContainerJson{
Message: &apimiddleware.BeaconBlockHeaderJson{
Slot: "not a slot",
},
},
},
},
},
},
{
name: "state validators error",
expectedErrorMessage: "failed to get state validators",
inputValidatorRequests: standardInputValidatorRequests,
getSyncingOutput: standardGetSyncingOutput,
getForkOutput: standardGetForkOutput,
getHeadersOutput: standardGetHeadersOutput,
getStateValidatorsInterface: &struct {
input []string
output *apimiddleware.StateValidatorsResponseJson
err error
}{
input: []string{stringPubKey},
err: errors.New("custom error"),
},
},
{
name: "validator container is nil",
expectedErrorMessage: "validator container is nil",
inputValidatorRequests: standardInputValidatorRequests,
getSyncingOutput: standardGetSyncingOutput,
getForkOutput: standardGetForkOutput,
getHeadersOutput: standardGetHeadersOutput,
getStateValidatorsInterface: &struct {
input []string
output *apimiddleware.StateValidatorsResponseJson
err error
}{
input: []string{stringPubKey},
output: &apimiddleware.StateValidatorsResponseJson{Data: []*apimiddleware.ValidatorContainerJson{nil}},
},
},
{
name: "validator is nil",
expectedErrorMessage: "validator is nil",
inputValidatorRequests: standardInputValidatorRequests,
getSyncingOutput: standardGetSyncingOutput,
getForkOutput: standardGetForkOutput,
getHeadersOutput: standardGetHeadersOutput,
getStateValidatorsInterface: &struct {
input []string
output *apimiddleware.StateValidatorsResponseJson
err error
}{
input: []string{stringPubKey},
output: &apimiddleware.StateValidatorsResponseJson{Data: []*apimiddleware.ValidatorContainerJson{{Validator: nil}}},
},
},
{
name: "previous epoch liveness error",
expectedErrorMessage: "failed to get map from validator index to liveness for previous epoch 30",
inputValidatorRequests: standardInputValidatorRequests,
getSyncingOutput: standardGetSyncingOutput,
getForkOutput: standardGetForkOutput,
getHeadersOutput: standardGetHeadersOutput,
getStateValidatorsInterface: standardGetStateValidatorsInterface,
getLivenessInterfaces: []struct {
inputUrl string
inputStringIndexes []string
output *apimiddleware.LivenessResponseJson
err error
}{
{
inputUrl: "/eth/v1/validator/liveness/30",
inputStringIndexes: []string{"42"},
output: &apimiddleware.LivenessResponseJson{},
err: errors.New("custom error"),
},
},
},
{
name: "liveness is nil",
expectedErrorMessage: "liveness is nil",
inputValidatorRequests: standardInputValidatorRequests,
getSyncingOutput: standardGetSyncingOutput,
getForkOutput: standardGetForkOutput,
getHeadersOutput: standardGetHeadersOutput,
getStateValidatorsInterface: standardGetStateValidatorsInterface,
getLivenessInterfaces: []struct {
inputUrl string
inputStringIndexes []string
output *apimiddleware.LivenessResponseJson
err error
}{
{
inputUrl: "/eth/v1/validator/liveness/30",
inputStringIndexes: []string{"42"},
output: &apimiddleware.LivenessResponseJson{
Data: []*struct {
Index string `json:"index"`
IsLive bool `json:"is_live"`
}{nil},
},
},
},
},
{
name: "current epoch liveness error",
expectedErrorMessage: "failed to get map from validator index to liveness for current epoch 31",
inputValidatorRequests: standardInputValidatorRequests,
getSyncingOutput: standardGetSyncingOutput,
getForkOutput: standardGetForkOutput,
getHeadersOutput: standardGetHeadersOutput,
getStateValidatorsInterface: standardGetStateValidatorsInterface,
getLivenessInterfaces: []struct {
inputUrl string
inputStringIndexes []string
output *apimiddleware.LivenessResponseJson
err error
}{
{
inputUrl: "/eth/v1/validator/liveness/30",
inputStringIndexes: []string{"42"},
output: &apimiddleware.LivenessResponseJson{
Data: []*struct {
Index string `json:"index"`
IsLive bool `json:"is_live"`
}{},
},
},
{
inputUrl: "/eth/v1/validator/liveness/31",
inputStringIndexes: []string{"42"},
output: &apimiddleware.LivenessResponseJson{},
err: errors.New("custom error"),
},
},
},
{
name: "wrong validator index for previous epoch",
expectedErrorMessage: "failed to retrieve liveness for previous epoch `30` for validator index `42`",
inputValidatorRequests: standardInputValidatorRequests,
getSyncingOutput: standardGetSyncingOutput,
getForkOutput: standardGetForkOutput,
getHeadersOutput: standardGetHeadersOutput,
getStateValidatorsInterface: standardGetStateValidatorsInterface,
getLivenessInterfaces: []struct {
inputUrl string
inputStringIndexes []string
output *apimiddleware.LivenessResponseJson
err error
}{
{
inputUrl: "/eth/v1/validator/liveness/30",
inputStringIndexes: []string{"42"},
output: &apimiddleware.LivenessResponseJson{
Data: []*struct {
Index string `json:"index"`
IsLive bool `json:"is_live"`
}{},
},
},
{
inputUrl: "/eth/v1/validator/liveness/31",
inputStringIndexes: []string{"42"},
output: &apimiddleware.LivenessResponseJson{
Data: []*struct {
Index string `json:"index"`
IsLive bool `json:"is_live"`
}{},
},
},
},
},
{
name: "wrong validator index for current epoch",
expectedErrorMessage: "failed to retrieve liveness for current epoch `31` for validator index `42`",
inputValidatorRequests: standardInputValidatorRequests,
getSyncingOutput: standardGetSyncingOutput,
getForkOutput: standardGetForkOutput,
getHeadersOutput: standardGetHeadersOutput,
getStateValidatorsInterface: standardGetStateValidatorsInterface,
getLivenessInterfaces: []struct {
inputUrl string
inputStringIndexes []string
output *apimiddleware.LivenessResponseJson
err error
}{
{
inputUrl: "/eth/v1/validator/liveness/30",
inputStringIndexes: []string{"42"},
output: &apimiddleware.LivenessResponseJson{
Data: []*struct {
Index string `json:"index"`
IsLive bool `json:"is_live"`
}{
{
Index: "42",
},
},
},
},
{
inputUrl: "/eth/v1/validator/liveness/31",
inputStringIndexes: []string{"42"},
output: &apimiddleware.LivenessResponseJson{
Data: []*struct {
Index string `json:"index"`
IsLive bool `json:"is_live"`
}{},
},
},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
ctx := context.Background()
if testCase.getSyncingOutput != nil {
syncingResponseJson := apimiddleware.SyncingResponseJson{}
jsonRestHandler.EXPECT().GetRestJsonResponse(
ctx,
syncingEnpoint,
&syncingResponseJson,
).Return(
nil,
testCase.getSyncingError,
).SetArg(
2,
*testCase.getSyncingOutput,
).Times(1)
}
if testCase.getForkOutput != nil {
stateForkResponseJson := apimiddleware.StateForkResponseJson{}
jsonRestHandler.EXPECT().GetRestJsonResponse(
ctx,
forkEndpoint,
&stateForkResponseJson,
).Return(
nil,
testCase.getForkError,
).SetArg(
2,
*testCase.getForkOutput,
).Times(1)
}
if testCase.getHeadersOutput != nil {
blockHeadersResponseJson := apimiddleware.BlockHeadersResponseJson{}
jsonRestHandler.EXPECT().GetRestJsonResponse(
ctx,
headersEndpoint,
&blockHeadersResponseJson,
).Return(
nil,
testCase.getHeadersError,
).SetArg(
2,
*testCase.getHeadersOutput,
).Times(1)
}
stateValidatorsProvider := mock.NewMockstateValidatorsProvider(ctrl)
if testCase.getStateValidatorsInterface != nil {
stateValidatorsProvider.EXPECT().GetStateValidators(
ctx,
testCase.getStateValidatorsInterface.input,
nil,
nil,
).Return(
testCase.getStateValidatorsInterface.output,
testCase.getStateValidatorsInterface.err,
).Times(1)
}
if testCase.getLivenessInterfaces != nil {
for _, iface := range testCase.getLivenessInterfaces {
livenessResponseJson := apimiddleware.LivenessResponseJson{}
marshalledIndexes, err := json.Marshal(iface.inputStringIndexes)
require.NoError(t, err)
jsonRestHandler.EXPECT().PostRestJson(
ctx,
iface.inputUrl,
nil,
bytes.NewBuffer(marshalledIndexes),
&livenessResponseJson,
).SetArg(
4,
*iface.output,
).Return(
nil,
iface.err,
).Times(1)
}
}
validatorClient := beaconApiValidatorClient{
jsonRestHandler: jsonRestHandler,
stateValidatorsProvider: stateValidatorsProvider,
}
_, err := validatorClient.CheckDoppelGanger(
context.Background(),
&ethpb.DoppelGangerRequest{
ValidatorRequests: testCase.inputValidatorRequests,
},
)
require.ErrorContains(t, testCase.expectedErrorMessage, err)
})
}
}

View File

@ -0,0 +1,5 @@
package beacon_api
import "github.com/sirupsen/logrus"
var log = logrus.WithField("prefix", "beacon-api")