mirror of
https://gitlab.com/pulsechaincom/prysm-pulse.git
synced 2024-12-22 03:30:35 +00:00
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:
parent
30974039f3
commit
0f90bacac9
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
238
validator/client/beacon-api/doppelganger.go
Normal file
238
validator/client/beacon-api/doppelganger.go
Normal 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 ðpb.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: ðpb.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 ðpb.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
|
||||
}
|
859
validator/client/beacon-api/doppelganger_test.go
Normal file
859
validator/client/beacon-api/doppelganger_test.go
Normal 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: ðpb.DoppelGangerResponse{
|
||||
Responses: []*ethpb.DoppelGangerResponse_ValidatorResponse{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil validator requests",
|
||||
doppelGangerInput: ðpb.DoppelGangerRequest{
|
||||
ValidatorRequests: nil,
|
||||
},
|
||||
doppelGangerExpectedOutput: ðpb.DoppelGangerResponse{
|
||||
Responses: []*ethpb.DoppelGangerResponse_ValidatorResponse{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty validator requests",
|
||||
doppelGangerInput: ðpb.DoppelGangerRequest{
|
||||
ValidatorRequests: []*ethpb.DoppelGangerRequest_ValidatorRequest{},
|
||||
},
|
||||
doppelGangerExpectedOutput: ðpb.DoppelGangerResponse{
|
||||
Responses: []*ethpb.DoppelGangerResponse_ValidatorResponse{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "phase0",
|
||||
doppelGangerInput: ðpb.DoppelGangerRequest{
|
||||
ValidatorRequests: []*ethpb.DoppelGangerRequest_ValidatorRequest{
|
||||
{PublicKey: pubKey1},
|
||||
{PublicKey: pubKey2},
|
||||
{PublicKey: pubKey3},
|
||||
{PublicKey: pubKey4},
|
||||
{PublicKey: pubKey5},
|
||||
{PublicKey: pubKey6},
|
||||
},
|
||||
},
|
||||
doppelGangerExpectedOutput: ðpb.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: ðpb.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: ðpb.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: ðpb.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: ðpb.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(),
|
||||
ðpb.DoppelGangerRequest{
|
||||
ValidatorRequests: testCase.inputValidatorRequests,
|
||||
},
|
||||
)
|
||||
|
||||
require.ErrorContains(t, testCase.expectedErrorMessage, err)
|
||||
})
|
||||
}
|
||||
}
|
5
validator/client/beacon-api/log.go
Normal file
5
validator/client/beacon-api/log.go
Normal file
@ -0,0 +1,5 @@
|
||||
package beacon_api
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
var log = logrus.WithField("prefix", "beacon-api")
|
Loading…
Reference in New Issue
Block a user