diff --git a/validator/client/beacon-api/BUILD.bazel b/validator/client/beacon-api/BUILD.bazel index 67eb30283..278d89d30 100644 --- a/validator/client/beacon-api/BUILD.bazel +++ b/validator/client/beacon-api/BUILD.bazel @@ -21,6 +21,7 @@ go_library( "state_validators.go", "status.go", "sync_committee.go", + "sync_message_block_root.go", ], importpath = "github.com/prysmaticlabs/prysm/v3/validator/client/beacon-api", visibility = ["//validator:__subpackages__"], @@ -72,6 +73,7 @@ go_test( "state_validators_test.go", "status_test.go", "sync_committee_test.go", + "sync_message_block_root_test.go", "wait_for_chain_start_test.go", ], embed = [":go_default_library"], diff --git a/validator/client/beacon-api/beacon_api_validator_client.go b/validator/client/beacon-api/beacon_api_validator_client.go index fd4dd525c..4bd3995fe 100644 --- a/validator/client/beacon-api/beacon_api_validator_client.go +++ b/validator/client/beacon-api/beacon_api_validator_client.go @@ -95,13 +95,8 @@ func (c *beaconApiValidatorClient) GetSyncCommitteeContribution(ctx context.Cont panic("beaconApiValidatorClient.GetSyncCommitteeContribution is not implemented. To use a fallback client, create this validator with NewBeaconApiValidatorClientWithFallback instead.") } -func (c *beaconApiValidatorClient) GetSyncMessageBlockRoot(ctx context.Context, in *empty.Empty) (*ethpb.SyncMessageBlockRootResponse, error) { - if c.fallbackClient != nil { - return c.fallbackClient.GetSyncMessageBlockRoot(ctx, in) - } - - // TODO: Implement me - panic("beaconApiValidatorClient.GetSyncMessageBlockRoot is not implemented. To use a fallback client, create this validator with NewBeaconApiValidatorClientWithFallback instead.") +func (c *beaconApiValidatorClient) GetSyncMessageBlockRoot(ctx context.Context, _ *empty.Empty) (*ethpb.SyncMessageBlockRootResponse, error) { + return c.getSyncMessageBlockRoot(ctx) } func (c *beaconApiValidatorClient) GetSyncSubcommitteeIndex(ctx context.Context, in *ethpb.SyncSubcommitteeIndexRequest) (*ethpb.SyncSubcommitteeIndexResponse, error) { diff --git a/validator/client/beacon-api/sync_message_block_root.go b/validator/client/beacon-api/sync_message_block_root.go new file mode 100644 index 000000000..2b16e8eb8 --- /dev/null +++ b/validator/client/beacon-api/sync_message_block_root.go @@ -0,0 +1,41 @@ +package beacon_api + +import ( + "context" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware" + ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1" +) + +func (c *beaconApiValidatorClient) getSyncMessageBlockRoot(ctx context.Context) (*ethpb.SyncMessageBlockRootResponse, error) { + // Get head beacon block root. + var resp apimiddleware.BlockRootResponseJson + if _, err := c.jsonRestHandler.GetRestJsonResponse(ctx, "/eth/v1/beacon/blocks/head/root", &resp); err != nil { + return nil, errors.Wrap(err, "failed to query GET REST endpoint") + } + + // An optimistic validator MUST NOT participate in sync committees + // (i.e., sign across the DOMAIN_SYNC_COMMITTEE, DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF or DOMAIN_CONTRIBUTION_AND_PROOF domains). + if resp.ExecutionOptimistic { + return nil, errors.New("the node is currently optimistic and cannot serve validators") + } + + if resp.Data == nil { + return nil, errors.New("no data returned") + } + + if resp.Data.Root == "" { + return nil, errors.New("no root returned") + } + + blockRoot, err := hexutil.Decode(resp.Data.Root) + if err != nil { + return nil, errors.Wrap(err, "failed to decode beacon block root") + } + + return ðpb.SyncMessageBlockRootResponse{ + Root: blockRoot, + }, nil +} diff --git a/validator/client/beacon-api/sync_message_block_root_test.go b/validator/client/beacon-api/sync_message_block_root_test.go new file mode 100644 index 000000000..feb717b52 --- /dev/null +++ b/validator/client/beacon-api/sync_message_block_root_test.go @@ -0,0 +1,92 @@ +package beacon_api + +import ( + "context" + "errors" + "testing" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/golang/mock/gomock" + "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware" + "github.com/prysmaticlabs/prysm/v3/testing/require" + "github.com/prysmaticlabs/prysm/v3/validator/client/beacon-api/mock" +) + +func TestGetSyncMessageBlockRoot(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + const blockRoot = "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + tests := []struct { + name string + endpointError error + expectedErrorMessage string + expectedResponse apimiddleware.BlockRootResponseJson + }{ + { + name: "valid request", + expectedResponse: apimiddleware.BlockRootResponseJson{ + Data: &apimiddleware.BlockRootContainerJson{ + Root: blockRoot, + }, + }, + }, + { + name: "internal server error", + expectedErrorMessage: "internal server error", + endpointError: errors.New("internal server error"), + }, + { + name: "execution optimistic", + expectedResponse: apimiddleware.BlockRootResponseJson{ + ExecutionOptimistic: true, + }, + expectedErrorMessage: "the node is currently optimistic and cannot serve validators", + }, + { + name: "no data", + expectedResponse: apimiddleware.BlockRootResponseJson{}, + expectedErrorMessage: "no data returned", + }, + { + name: "no root", + expectedResponse: apimiddleware.BlockRootResponseJson{ + Data: new(apimiddleware.BlockRootContainerJson), + }, + expectedErrorMessage: "no root returned", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) + jsonRestHandler.EXPECT().GetRestJsonResponse( + ctx, + "/eth/v1/beacon/blocks/head/root", + &apimiddleware.BlockRootResponseJson{}, + ).SetArg( + 2, + test.expectedResponse, + ).Return( + nil, + test.endpointError, + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + actualResponse, err := validatorClient.getSyncMessageBlockRoot(ctx) + if test.expectedErrorMessage != "" { + require.ErrorContains(t, test.expectedErrorMessage, err) + return + } + + require.NoError(t, err) + + expectedRootBytes, err := hexutil.Decode(test.expectedResponse.Data.Root) + require.NoError(t, err) + + require.NoError(t, err) + require.DeepEqual(t, expectedRootBytes, actualResponse.Root) + }) + } +}