Add REST API endpoint for beacon chain client's GetChainHead (#12245)

* Add REST API endpoint for beacon chain client's GetChainHead

* Remove unused parameters

---------

Co-authored-by: Radosław Kapka <rkapka@wp.pl>
This commit is contained in:
Patrice Vignola
2023-04-13 04:42:59 -07:00
committed by GitHub
parent 75338dd83d
commit 80e26143eb
2 changed files with 467 additions and 16 deletions

View File

@@ -23,13 +23,124 @@ type beaconApiBeaconChainClient struct {
stateValidatorsProvider stateValidatorsProvider
}
func (c beaconApiBeaconChainClient) GetChainHead(ctx context.Context, in *empty.Empty) (*ethpb.ChainHead, error) {
if c.fallbackClient != nil {
return c.fallbackClient.GetChainHead(ctx, in)
func (c beaconApiBeaconChainClient) getHeadBlockHeaders(ctx context.Context) (*apimiddleware.BlockHeaderResponseJson, error) {
blockHeader := apimiddleware.BlockHeaderResponseJson{}
if _, err := c.jsonRestHandler.GetRestJsonResponse(ctx, "/eth/v1/beacon/headers/head", &blockHeader); err != nil {
return nil, errors.Wrap(err, "failed to get head block header")
}
// TODO: Implement me
panic("beaconApiBeaconChainClient.GetChainHead is not implemented. To use a fallback client, pass a fallback client as the last argument of NewBeaconApiBeaconChainClientWithFallback.")
if blockHeader.Data == nil || blockHeader.Data.Header == nil {
return nil, errors.New("block header data is nil")
}
if blockHeader.Data.Header.Message == nil {
return nil, errors.New("block header message is nil")
}
return &blockHeader, nil
}
func (c beaconApiBeaconChainClient) GetChainHead(ctx context.Context, _ *empty.Empty) (*ethpb.ChainHead, error) {
const endpoint = "/eth/v1/beacon/states/head/finality_checkpoints"
finalityCheckpoints := apimiddleware.StateFinalityCheckpointResponseJson{}
if _, err := c.jsonRestHandler.GetRestJsonResponse(ctx, endpoint, &finalityCheckpoints); err != nil {
return nil, errors.Wrapf(err, "failed to query %s", endpoint)
}
if finalityCheckpoints.Data == nil {
return nil, errors.New("finality checkpoints data is nil")
}
if finalityCheckpoints.Data.Finalized == nil {
return nil, errors.New("finalized checkpoint is nil")
}
finalizedEpoch, err := strconv.ParseUint(finalityCheckpoints.Data.Finalized.Epoch, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse finalized epoch `%s`", finalityCheckpoints.Data.Finalized.Epoch)
}
finalizedSlot, err := slots.EpochStart(primitives.Epoch(finalizedEpoch))
if err != nil {
return nil, errors.Wrapf(err, "failed to get first slot for epoch `%d`", finalizedEpoch)
}
finalizedRoot, err := hexutil.Decode(finalityCheckpoints.Data.Finalized.Root)
if err != nil {
return nil, errors.Wrapf(err, "failed to decode finalized checkpoint root `%s`", finalityCheckpoints.Data.Finalized.Root)
}
if finalityCheckpoints.Data.CurrentJustified == nil {
return nil, errors.New("current justified checkpoint is nil")
}
justifiedEpoch, err := strconv.ParseUint(finalityCheckpoints.Data.CurrentJustified.Epoch, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse current justified checkpoint epoch `%s`", finalityCheckpoints.Data.CurrentJustified.Epoch)
}
justifiedSlot, err := slots.EpochStart(primitives.Epoch(justifiedEpoch))
if err != nil {
return nil, errors.Wrapf(err, "failed to get first slot for epoch `%d`", justifiedEpoch)
}
justifiedRoot, err := hexutil.Decode(finalityCheckpoints.Data.CurrentJustified.Root)
if err != nil {
return nil, errors.Wrapf(err, "failed to decode current justified checkpoint root `%s`", finalityCheckpoints.Data.CurrentJustified.Root)
}
if finalityCheckpoints.Data.PreviousJustified == nil {
return nil, errors.New("previous justified checkpoint is nil")
}
previousJustifiedEpoch, err := strconv.ParseUint(finalityCheckpoints.Data.PreviousJustified.Epoch, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse previous justified checkpoint epoch `%s`", finalityCheckpoints.Data.PreviousJustified.Epoch)
}
previousJustifiedSlot, err := slots.EpochStart(primitives.Epoch(previousJustifiedEpoch))
if err != nil {
return nil, errors.Wrapf(err, "failed to get first slot for epoch `%d`", previousJustifiedEpoch)
}
previousJustifiedRoot, err := hexutil.Decode(finalityCheckpoints.Data.PreviousJustified.Root)
if err != nil {
return nil, errors.Wrapf(err, "failed to decode previous justified checkpoint root `%s`", finalityCheckpoints.Data.PreviousJustified.Root)
}
blockHeader, err := c.getHeadBlockHeaders(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get head block headers")
}
headSlot, err := strconv.ParseUint(blockHeader.Data.Header.Message.Slot, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse head block slot `%s`", blockHeader.Data.Header.Message.Slot)
}
headEpoch := slots.ToEpoch(primitives.Slot(headSlot))
headBlockRoot, err := hexutil.Decode(blockHeader.Data.Root)
if err != nil {
return nil, errors.Wrapf(err, "failed to decode head block root `%s`", blockHeader.Data.Root)
}
return &ethpb.ChainHead{
HeadSlot: primitives.Slot(headSlot),
HeadEpoch: headEpoch,
HeadBlockRoot: headBlockRoot,
FinalizedSlot: finalizedSlot,
FinalizedEpoch: primitives.Epoch(finalizedEpoch),
FinalizedBlockRoot: finalizedRoot,
JustifiedSlot: justifiedSlot,
JustifiedEpoch: primitives.Epoch(justifiedEpoch),
JustifiedBlockRoot: justifiedRoot,
PreviousJustifiedSlot: previousJustifiedSlot,
PreviousJustifiedEpoch: primitives.Epoch(previousJustifiedEpoch),
PreviousJustifiedBlockRoot: previousJustifiedRoot,
OptimisticStatus: blockHeader.ExecutionOptimistic,
}, nil
}
func (c beaconApiBeaconChainClient) ListValidatorBalances(ctx context.Context, in *ethpb.ListValidatorBalancesRequest) (*ethpb.ValidatorBalances, error) {
@@ -91,17 +202,9 @@ func (c beaconApiBeaconChainClient) ListValidators(ctx context.Context, in *ethp
return nil, errors.Wrap(err, "failed to get head state validators")
}
blockHeader := apimiddleware.BlockHeaderResponseJson{}
if _, err := c.jsonRestHandler.GetRestJsonResponse(ctx, "/eth/v1/beacon/headers/head", &blockHeader); err != nil {
return nil, errors.Wrap(err, "failed to get head block header")
}
if blockHeader.Data == nil || blockHeader.Data.Header == nil {
return nil, errors.New("block header data is nil")
}
if blockHeader.Data.Header.Message == nil {
return nil, errors.New("block header message is nil")
blockHeader, err := c.getHeadBlockHeaders(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get head block headers")
}
slot, err := strconv.ParseUint(blockHeader.Data.Header.Message.Slot, 10, 64)

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"math"
"strconv"
"testing"
"github.com/ethereum/go-ethereum/common/hexutil"
@@ -14,7 +15,9 @@ import (
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v4/testing/assert"
"github.com/prysmaticlabs/prysm/v4/testing/require"
"github.com/prysmaticlabs/prysm/v4/time/slots"
"github.com/prysmaticlabs/prysm/v4/validator/client/beacon-api/mock"
"google.golang.org/protobuf/types/known/emptypb"
)
func TestListValidators(t *testing.T) {
@@ -577,3 +580,348 @@ func TestListValidators(t *testing.T) {
}
})
}
func TestGetChainHead(t *testing.T) {
const finalityCheckpointsEndpoint = "/eth/v1/beacon/states/head/finality_checkpoints"
const headBlockHeadersEndpoint = "/eth/v1/beacon/headers/head"
generateValidFinalityCheckpointsResponse := func() apimiddleware.StateFinalityCheckpointResponseJson {
return apimiddleware.StateFinalityCheckpointResponseJson{
Data: &apimiddleware.StateFinalityCheckpointResponse_StateFinalityCheckpointJson{
PreviousJustified: &apimiddleware.CheckpointJson{
Epoch: "1",
Root: hexutil.Encode([]byte{2}),
},
CurrentJustified: &apimiddleware.CheckpointJson{
Epoch: "3",
Root: hexutil.Encode([]byte{4}),
},
Finalized: &apimiddleware.CheckpointJson{
Epoch: "5",
Root: hexutil.Encode([]byte{6}),
},
},
}
}
t.Run("fails to get finality checkpoints", func(t *testing.T) {
testCases := []struct {
name string
generateFinalityCheckpointsResponse func() apimiddleware.StateFinalityCheckpointResponseJson
finalityCheckpointsError error
expectedError string
}{
{
name: "query failed",
finalityCheckpointsError: errors.New("foo error"),
expectedError: fmt.Sprintf("failed to query %s: foo error", finalityCheckpointsEndpoint),
generateFinalityCheckpointsResponse: func() apimiddleware.StateFinalityCheckpointResponseJson {
return apimiddleware.StateFinalityCheckpointResponseJson{}
},
},
{
name: "nil finality checkpoints data",
expectedError: "finality checkpoints data is nil",
generateFinalityCheckpointsResponse: func() apimiddleware.StateFinalityCheckpointResponseJson {
validResponse := generateValidFinalityCheckpointsResponse()
validResponse.Data = nil
return validResponse
},
},
{
name: "nil finalized checkpoint",
expectedError: "finalized checkpoint is nil",
generateFinalityCheckpointsResponse: func() apimiddleware.StateFinalityCheckpointResponseJson {
validResponse := generateValidFinalityCheckpointsResponse()
validResponse.Data.Finalized = nil
return validResponse
},
},
{
name: "invalid finalized epoch",
expectedError: "failed to parse finalized epoch `foo`",
generateFinalityCheckpointsResponse: func() apimiddleware.StateFinalityCheckpointResponseJson {
validResponse := generateValidFinalityCheckpointsResponse()
validResponse.Data.Finalized.Epoch = "foo"
return validResponse
},
},
{
name: "failed to get first slot of finalized epoch",
expectedError: fmt.Sprintf("failed to get first slot for epoch `%d`", uint64(math.MaxUint64)),
generateFinalityCheckpointsResponse: func() apimiddleware.StateFinalityCheckpointResponseJson {
validResponse := generateValidFinalityCheckpointsResponse()
validResponse.Data.Finalized.Epoch = strconv.FormatUint(uint64(math.MaxUint64), 10)
return validResponse
},
},
{
name: "invalid finalized root",
expectedError: "failed to decode finalized checkpoint root `bar`",
generateFinalityCheckpointsResponse: func() apimiddleware.StateFinalityCheckpointResponseJson {
validResponse := generateValidFinalityCheckpointsResponse()
validResponse.Data.Finalized.Root = "bar"
return validResponse
},
},
{
name: "nil current justified checkpoint",
expectedError: "current justified checkpoint is nil",
generateFinalityCheckpointsResponse: func() apimiddleware.StateFinalityCheckpointResponseJson {
validResponse := generateValidFinalityCheckpointsResponse()
validResponse.Data.CurrentJustified = nil
return validResponse
},
},
{
name: "nil current justified epoch",
expectedError: "failed to parse current justified checkpoint epoch `foo`",
generateFinalityCheckpointsResponse: func() apimiddleware.StateFinalityCheckpointResponseJson {
validResponse := generateValidFinalityCheckpointsResponse()
validResponse.Data.CurrentJustified.Epoch = "foo"
return validResponse
},
},
{
name: "failed to get first slot of current justified epoch",
expectedError: fmt.Sprintf("failed to get first slot for epoch `%d`", uint64(math.MaxUint64)),
generateFinalityCheckpointsResponse: func() apimiddleware.StateFinalityCheckpointResponseJson {
validResponse := generateValidFinalityCheckpointsResponse()
validResponse.Data.CurrentJustified.Epoch = strconv.FormatUint(uint64(math.MaxUint64), 10)
return validResponse
},
},
{
name: "invalid current justified root",
expectedError: "failed to decode current justified checkpoint root `bar`",
generateFinalityCheckpointsResponse: func() apimiddleware.StateFinalityCheckpointResponseJson {
validResponse := generateValidFinalityCheckpointsResponse()
validResponse.Data.CurrentJustified.Root = "bar"
return validResponse
},
},
{
name: "nil previous justified checkpoint",
expectedError: "previous justified checkpoint is nil",
generateFinalityCheckpointsResponse: func() apimiddleware.StateFinalityCheckpointResponseJson {
validResponse := generateValidFinalityCheckpointsResponse()
validResponse.Data.PreviousJustified = nil
return validResponse
},
},
{
name: "nil previous justified epoch",
expectedError: "failed to parse previous justified checkpoint epoch `foo`",
generateFinalityCheckpointsResponse: func() apimiddleware.StateFinalityCheckpointResponseJson {
validResponse := generateValidFinalityCheckpointsResponse()
validResponse.Data.PreviousJustified.Epoch = "foo"
return validResponse
},
},
{
name: "failed to get first slot of previous justified epoch",
expectedError: fmt.Sprintf("failed to get first slot for epoch `%d`", uint64(math.MaxUint64)),
generateFinalityCheckpointsResponse: func() apimiddleware.StateFinalityCheckpointResponseJson {
validResponse := generateValidFinalityCheckpointsResponse()
validResponse.Data.PreviousJustified.Epoch = strconv.FormatUint(uint64(math.MaxUint64), 10)
return validResponse
},
},
{
name: "invalid previous justified root",
expectedError: "failed to decode previous justified checkpoint root `bar`",
generateFinalityCheckpointsResponse: func() apimiddleware.StateFinalityCheckpointResponseJson {
validResponse := generateValidFinalityCheckpointsResponse()
validResponse.Data.PreviousJustified.Root = "bar"
return validResponse
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.Background()
finalityCheckpointsResponse := apimiddleware.StateFinalityCheckpointResponseJson{}
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
jsonRestHandler.EXPECT().GetRestJsonResponse(ctx, finalityCheckpointsEndpoint, &finalityCheckpointsResponse).Return(
nil,
testCase.finalityCheckpointsError,
).SetArg(
2,
testCase.generateFinalityCheckpointsResponse(),
)
beaconChainClient := beaconApiBeaconChainClient{jsonRestHandler: jsonRestHandler}
_, err := beaconChainClient.GetChainHead(ctx, &emptypb.Empty{})
assert.ErrorContains(t, testCase.expectedError, err)
})
}
})
generateValidBlockHeadersResponse := func() apimiddleware.BlockHeaderResponseJson {
return apimiddleware.BlockHeaderResponseJson{
Data: &apimiddleware.BlockHeaderContainerJson{
Root: hexutil.Encode([]byte{7}),
Header: &apimiddleware.BeaconBlockHeaderContainerJson{
Message: &apimiddleware.BeaconBlockHeaderJson{
Slot: "8",
},
},
},
}
}
t.Run("fails to get head block headers", func(t *testing.T) {
testCases := []struct {
name string
generateHeadBlockHeadersResponse func() apimiddleware.BlockHeaderResponseJson
headBlockHeadersError error
expectedError string
}{
{
name: "query failed",
headBlockHeadersError: errors.New("foo error"),
expectedError: "failed to get head block header",
generateHeadBlockHeadersResponse: func() apimiddleware.BlockHeaderResponseJson {
return apimiddleware.BlockHeaderResponseJson{}
},
},
{
name: "nil block header data",
expectedError: "block header data is nil",
generateHeadBlockHeadersResponse: func() apimiddleware.BlockHeaderResponseJson {
validResponse := generateValidBlockHeadersResponse()
validResponse.Data = nil
return validResponse
},
},
{
name: "nil block header data header",
expectedError: "block header data is nil",
generateHeadBlockHeadersResponse: func() apimiddleware.BlockHeaderResponseJson {
validResponse := generateValidBlockHeadersResponse()
validResponse.Data.Header = nil
return validResponse
},
},
{
name: "nil block header message",
expectedError: "block header message is nil",
generateHeadBlockHeadersResponse: func() apimiddleware.BlockHeaderResponseJson {
validResponse := generateValidBlockHeadersResponse()
validResponse.Data.Header.Message = nil
return validResponse
},
},
{
name: "invalid message slot",
expectedError: "failed to parse head block slot `foo`",
generateHeadBlockHeadersResponse: func() apimiddleware.BlockHeaderResponseJson {
validResponse := generateValidBlockHeadersResponse()
validResponse.Data.Header.Message.Slot = "foo"
return validResponse
},
},
{
name: "invalid root",
expectedError: "failed to decode head block root `bar`",
generateHeadBlockHeadersResponse: func() apimiddleware.BlockHeaderResponseJson {
validResponse := generateValidBlockHeadersResponse()
validResponse.Data.Root = "bar"
return validResponse
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.Background()
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
finalityCheckpointsResponse := apimiddleware.StateFinalityCheckpointResponseJson{}
jsonRestHandler.EXPECT().GetRestJsonResponse(ctx, finalityCheckpointsEndpoint, &finalityCheckpointsResponse).Return(
nil,
nil,
).SetArg(
2,
generateValidFinalityCheckpointsResponse(),
)
headBlockHeadersResponse := apimiddleware.BlockHeaderResponseJson{}
jsonRestHandler.EXPECT().GetRestJsonResponse(ctx, headBlockHeadersEndpoint, &headBlockHeadersResponse).Return(
nil,
testCase.headBlockHeadersError,
).SetArg(
2,
testCase.generateHeadBlockHeadersResponse(),
)
beaconChainClient := beaconApiBeaconChainClient{jsonRestHandler: jsonRestHandler}
_, err := beaconChainClient.GetChainHead(ctx, &emptypb.Empty{})
assert.ErrorContains(t, testCase.expectedError, err)
})
}
})
t.Run("returns a valid chain head", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.Background()
jsonRestHandler := mock.NewMockjsonRestHandler(ctrl)
finalityCheckpointsResponse := apimiddleware.StateFinalityCheckpointResponseJson{}
jsonRestHandler.EXPECT().GetRestJsonResponse(ctx, finalityCheckpointsEndpoint, &finalityCheckpointsResponse).Return(
nil,
nil,
).SetArg(
2,
generateValidFinalityCheckpointsResponse(),
)
headBlockHeadersResponse := apimiddleware.BlockHeaderResponseJson{}
jsonRestHandler.EXPECT().GetRestJsonResponse(ctx, headBlockHeadersEndpoint, &headBlockHeadersResponse).Return(
nil,
nil,
).SetArg(
2,
generateValidBlockHeadersResponse(),
)
expectedPreviousJustifiedSlot, err := slots.EpochStart(1)
require.NoError(t, err)
expectedCurrentJustifiedSlot, err := slots.EpochStart(3)
require.NoError(t, err)
expectedFinalizedSlot, err := slots.EpochStart(5)
require.NoError(t, err)
expectedChainHead := &ethpb.ChainHead{
PreviousJustifiedEpoch: 1,
PreviousJustifiedBlockRoot: []byte{2},
PreviousJustifiedSlot: expectedPreviousJustifiedSlot,
JustifiedEpoch: 3,
JustifiedBlockRoot: []byte{4},
JustifiedSlot: expectedCurrentJustifiedSlot,
FinalizedEpoch: 5,
FinalizedBlockRoot: []byte{6},
FinalizedSlot: expectedFinalizedSlot,
HeadBlockRoot: []byte{7},
HeadSlot: 8,
HeadEpoch: slots.ToEpoch(8),
}
beaconChainClient := beaconApiBeaconChainClient{jsonRestHandler: jsonRestHandler}
chainHead, err := beaconChainClient.GetChainHead(ctx, &emptypb.Empty{})
require.NoError(t, err)
assert.DeepEqual(t, expectedChainHead, chainHead)
})
}