Skip to content

Commit

Permalink
validator REST API: block v2 and Electra support (#14623)
Browse files Browse the repository at this point in the history
* adding electra to validator client rest for get and post, also migrates to use the v2 endpoints

* changelog

* fixing test

* fixing linting
  • Loading branch information
james-prysm authored Nov 8, 2024
1 parent c0f9689 commit 5179af1
Show file tree
Hide file tree
Showing 19 changed files with 1,437 additions and 24 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve
- Added ListAttestationsV2 endpoint.
- Add ability to rollback node's internal state during processing.
- Change how unsafe protobuf state is created to prevent unnecessary copies.
- Added benchmarks for process slots for Capella, Deneb, Electra.
- Added benchmarks for process slots for Capella, Deneb, Electra
- Add helper to cast bytes to string without allocating memory.
- Added GetAggregatedAttestationV2 endpoint.
- Added SubmitAttestationsV2 endpoint.
- Validator REST mode Electra block support

### Changed

Expand Down Expand Up @@ -51,6 +52,7 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve
- Capella blocks are execution.
- Fixed panic when http request to subscribe to event stream fails.
- Return early for blob reconstructor during capella fork
- Updated block endpoint from V1 to V2

### Deprecated

Expand Down
2 changes: 2 additions & 0 deletions validator/client/beacon-api/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,10 @@ go_test(
"propose_beacon_block_blinded_bellatrix_test.go",
"propose_beacon_block_blinded_capella_test.go",
"propose_beacon_block_blinded_deneb_test.go",
"propose_beacon_block_blinded_electra_test.go",
"propose_beacon_block_capella_test.go",
"propose_beacon_block_deneb_test.go",
"propose_beacon_block_electra_test.go",
"propose_beacon_block_phase0_test.go",
"propose_beacon_block_test.go",
"propose_exit_test.go",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func TestBeaconApiValidatorClient_ProposeBeaconBlockValid(t *testing.T) {
jsonRestHandler := mock.NewMockJsonRestHandler(ctrl)
jsonRestHandler.EXPECT().Post(
gomock.Any(),
"/eth/v1/beacon/blocks",
"/eth/v2/beacon/blocks",
map[string]string{"Eth-Consensus-Version": "phase0"},
gomock.Any(),
nil,
Expand Down Expand Up @@ -175,7 +175,7 @@ func TestBeaconApiValidatorClient_ProposeBeaconBlockError(t *testing.T) {
jsonRestHandler := mock.NewMockJsonRestHandler(ctrl)
jsonRestHandler.EXPECT().Post(
gomock.Any(),
"/eth/v1/beacon/blocks",
"/eth/v2/beacon/blocks",
map[string]string{"Eth-Consensus-Version": "phase0"},
gomock.Any(),
nil,
Expand Down
34 changes: 31 additions & 3 deletions validator/client/beacon-api/get_beacon_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,14 @@ func (c *beaconApiValidatorClient) beaconBlock(ctx context.Context, slot primiti
blinded = produceBlockV3ResponseJson.ExecutionPayloadBlinded
decoder = json.NewDecoder(bytes.NewReader(produceBlockV3ResponseJson.Data))
}
return processBlockResponse(ver, blinded, decoder)
}

func processBlockResponse(ver string, isBlinded bool, decoder *json.Decoder) (*ethpb.GenericBeaconBlock, error) {
var response *ethpb.GenericBeaconBlock
if decoder == nil {
return nil, errors.New("no produce block json decoder found")
}
switch ver {
case version.String(version.Phase0):
jsonPhase0Block := structs.BeaconBlock{}
Expand All @@ -93,7 +99,7 @@ func (c *beaconApiValidatorClient) beaconBlock(ctx context.Context, slot primiti
}
response = genericBlock
case version.String(version.Bellatrix):
if blinded {
if isBlinded {
jsonBellatrixBlock := structs.BlindedBeaconBlockBellatrix{}
if err := decoder.Decode(&jsonBellatrixBlock); err != nil {
return nil, errors.Wrap(err, "failed to decode blinded bellatrix block response json")
Expand All @@ -115,7 +121,7 @@ func (c *beaconApiValidatorClient) beaconBlock(ctx context.Context, slot primiti
response = genericBlock
}
case version.String(version.Capella):
if blinded {
if isBlinded {
jsonCapellaBlock := structs.BlindedBeaconBlockCapella{}
if err := decoder.Decode(&jsonCapellaBlock); err != nil {
return nil, errors.Wrap(err, "failed to decode blinded capella block response json")
Expand All @@ -137,7 +143,7 @@ func (c *beaconApiValidatorClient) beaconBlock(ctx context.Context, slot primiti
response = genericBlock
}
case version.String(version.Deneb):
if blinded {
if isBlinded {
jsonDenebBlock := structs.BlindedBeaconBlockDeneb{}
if err := decoder.Decode(&jsonDenebBlock); err != nil {
return nil, errors.Wrap(err, "failed to decode blinded deneb block response json")
Expand All @@ -158,6 +164,28 @@ func (c *beaconApiValidatorClient) beaconBlock(ctx context.Context, slot primiti
}
response = genericBlock
}
case version.String(version.Electra):
if isBlinded {
jsonElectraBlock := structs.BlindedBeaconBlockElectra{}
if err := decoder.Decode(&jsonElectraBlock); err != nil {
return nil, errors.Wrap(err, "failed to decode blinded electra block response json")
}
genericBlock, err := jsonElectraBlock.ToGeneric()
if err != nil {
return nil, errors.Wrap(err, "failed to get blinded electra block")
}
response = genericBlock
} else {
jsonElectraBlockContents := structs.BeaconBlockContentsElectra{}
if err := decoder.Decode(&jsonElectraBlockContents); err != nil {
return nil, errors.Wrap(err, "failed to decode electra block response json")
}
genericBlock, err := jsonElectraBlockContents.ToGeneric()
if err != nil {
return nil, errors.Wrap(err, "failed to get electra block")
}
response = genericBlock
}
default:
return nil, errors.Errorf("unsupported consensus version `%s`", ver)
}
Expand Down
90 changes: 90 additions & 0 deletions validator/client/beacon-api/get_beacon_block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,96 @@ func TestGetBeaconBlock_BlindedDenebValid(t *testing.T) {
assert.DeepEqual(t, expectedBeaconBlock, beaconBlock)
}

func TestGetBeaconBlock_ElectraValid(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

proto := testhelpers.GenerateProtoElectraBeaconBlockContents()
block := testhelpers.GenerateJsonElectraBeaconBlockContents()
bytes, err := json.Marshal(block)
require.NoError(t, err)

const slot = primitives.Slot(1)
randaoReveal := []byte{2}
graffiti := []byte{3}

ctx := context.Background()

jsonRestHandler := mock.NewMockJsonRestHandler(ctrl)
jsonRestHandler.EXPECT().Get(
gomock.Any(),
fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)),
&structs.ProduceBlockV3Response{},
).SetArg(
2,
structs.ProduceBlockV3Response{
Version: "electra",
ExecutionPayloadBlinded: false,
Data: bytes,
},
).Return(
nil,
).Times(1)

validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
beaconBlock, err := validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti)
require.NoError(t, err)

expectedBeaconBlock := &ethpb.GenericBeaconBlock{
Block: &ethpb.GenericBeaconBlock_Electra{
Electra: proto,
},
IsBlinded: false,
}

assert.DeepEqual(t, expectedBeaconBlock, beaconBlock)
}

func TestGetBeaconBlock_BlindedElectraValid(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

proto := testhelpers.GenerateProtoBlindedElectraBeaconBlock()
block := testhelpers.GenerateJsonBlindedElectraBeaconBlock()
bytes, err := json.Marshal(block)
require.NoError(t, err)

const slot = primitives.Slot(1)
randaoReveal := []byte{2}
graffiti := []byte{3}

ctx := context.Background()

jsonRestHandler := mock.NewMockJsonRestHandler(ctrl)
jsonRestHandler.EXPECT().Get(
gomock.Any(),
fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)),
&structs.ProduceBlockV3Response{},
).SetArg(
2,
structs.ProduceBlockV3Response{
Version: "electra",
ExecutionPayloadBlinded: true,
Data: bytes,
},
).Return(
nil,
).Times(1)

validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
beaconBlock, err := validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti)
require.NoError(t, err)

expectedBeaconBlock := &ethpb.GenericBeaconBlock{
Block: &ethpb.GenericBeaconBlock_BlindedElectra{
BlindedElectra: proto,
},
IsBlinded: true,
}

assert.DeepEqual(t, expectedBeaconBlock, beaconBlock)
}

func TestGetBeaconBlock_FallbackToBlindedBlock(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
Expand Down
33 changes: 31 additions & 2 deletions validator/client/beacon-api/propose_beacon_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,45 @@ func (c *beaconApiValidatorClient) proposeBeaconBlock(ctx context.Context, in *e
if err != nil {
return nil, errors.Wrap(err, "failed to marshal blinded deneb beacon block contents")
}
case *ethpb.GenericSignedBeaconBlock_Electra:
consensusVersion = "electra"
beaconBlockRoot, err = blockType.Electra.Block.HashTreeRoot()
if err != nil {
return nil, errors.Wrap(err, "failed to compute block root for electra beacon block")
}
signedBlock, err := structs.SignedBeaconBlockContentsElectraFromConsensus(blockType.Electra)
if err != nil {
return nil, errors.Wrap(err, "failed to convert electra beacon block contents")
}
marshalledSignedBeaconBlockJson, err = json.Marshal(signedBlock)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal electra beacon block contents")
}
case *ethpb.GenericSignedBeaconBlock_BlindedElectra:
blinded = true
consensusVersion = "electra"
beaconBlockRoot, err = blockType.BlindedElectra.HashTreeRoot()
if err != nil {
return nil, errors.Wrap(err, "failed to compute block root for blinded electra beacon block")
}
signedBlock, err := structs.SignedBlindedBeaconBlockElectraFromConsensus(blockType.BlindedElectra)
if err != nil {
return nil, errors.Wrap(err, "failed to convert blinded electra beacon block contents")
}
marshalledSignedBeaconBlockJson, err = json.Marshal(signedBlock)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal blinded electra beacon block contents")
}
default:
return nil, errors.Errorf("unsupported block type %T", in.Block)
}

var endpoint string

if blinded {
endpoint = "/eth/v1/beacon/blinded_blocks"
endpoint = "/eth/v2/beacon/blinded_blocks"
} else {
endpoint = "/eth/v1/beacon/blocks"
endpoint = "/eth/v2/beacon/blocks"
}

headers := map[string]string{"Eth-Consensus-Version": consensusVersion}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestProposeBeaconBlock_Altair(t *testing.T) {
headers := map[string]string{"Eth-Consensus-Version": "altair"}
jsonRestHandler.EXPECT().Post(
gomock.Any(),
"/eth/v1/beacon/blocks",
"/eth/v2/beacon/blocks",
headers,
bytes.NewBuffer(marshalledBlock),
nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestProposeBeaconBlock_Bellatrix(t *testing.T) {
headers := map[string]string{"Eth-Consensus-Version": "bellatrix"}
jsonRestHandler.EXPECT().Post(
gomock.Any(),
"/eth/v1/beacon/blocks",
"/eth/v2/beacon/blocks",
headers,
bytes.NewBuffer(marshalledBlock),
nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func TestProposeBeaconBlock_BlindedBellatrix(t *testing.T) {
headers := map[string]string{"Eth-Consensus-Version": "bellatrix"}
jsonRestHandler.EXPECT().Post(
gomock.Any(),
"/eth/v1/beacon/blinded_blocks",
"/eth/v2/beacon/blinded_blocks",
headers,
bytes.NewBuffer(marshalledBlock),
nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func TestProposeBeaconBlock_BlindedCapella(t *testing.T) {
headers := map[string]string{"Eth-Consensus-Version": "capella"}
jsonRestHandler.EXPECT().Post(
gomock.Any(),
"/eth/v1/beacon/blinded_blocks",
"/eth/v2/beacon/blinded_blocks",
headers,
bytes.NewBuffer(marshalledBlock),
nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
)

func TestProposeBeaconBlock_BlindedDeneb(t *testing.T) {
t.Skip("TODO: Fix this in the beacon-API PR")
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jsonRestHandler := mock.NewMockJsonRestHandler(ctrl)
Expand All @@ -32,7 +31,7 @@ func TestProposeBeaconBlock_BlindedDeneb(t *testing.T) {
headers := map[string]string{"Eth-Consensus-Version": "deneb"}
jsonRestHandler.EXPECT().Post(
gomock.Any(),
"/eth/v1/beacon/blinded_blocks",
"/eth/v2/beacon/blinded_blocks",
headers,
bytes.NewBuffer(denebBytes),
nil,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package beacon_api

import (
"bytes"
"context"
"encoding/json"
"testing"

"github.com/prysmaticlabs/prysm/v5/api/server/structs"
rpctesting "github.com/prysmaticlabs/prysm/v5/beacon-chain/rpc/eth/shared/testing"
"github.com/prysmaticlabs/prysm/v5/testing/assert"
"github.com/prysmaticlabs/prysm/v5/testing/require"
"github.com/prysmaticlabs/prysm/v5/validator/client/beacon-api/mock"
"go.uber.org/mock/gomock"
)

func TestProposeBeaconBlock_BlindedElectra(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jsonRestHandler := mock.NewMockJsonRestHandler(ctrl)

var block structs.SignedBlindedBeaconBlockElectra
err := json.Unmarshal([]byte(rpctesting.BlindedElectraBlock), &block)
require.NoError(t, err)
genericSignedBlock, err := block.ToGeneric()
require.NoError(t, err)

electraBytes, err := json.Marshal(block)
require.NoError(t, err)
// Make sure that what we send in the POST body is the marshalled version of the protobuf block
headers := map[string]string{"Eth-Consensus-Version": "electra"}
jsonRestHandler.EXPECT().Post(
gomock.Any(),
"/eth/v2/beacon/blinded_blocks",
headers,
bytes.NewBuffer(electraBytes),
nil,
)

validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler}
proposeResponse, err := validatorClient.proposeBeaconBlock(context.Background(), genericSignedBlock)
assert.NoError(t, err)
require.NotNil(t, proposeResponse)

expectedBlockRoot, err := genericSignedBlock.GetBlindedElectra().HashTreeRoot()
require.NoError(t, err)

// Make sure that the block root is set
assert.DeepEqual(t, expectedBlockRoot[:], proposeResponse.BlockRoot)
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestProposeBeaconBlock_Capella(t *testing.T) {
headers := map[string]string{"Eth-Consensus-Version": "capella"}
jsonRestHandler.EXPECT().Post(
gomock.Any(),
"/eth/v1/beacon/blocks",
"/eth/v2/beacon/blocks",
headers,
bytes.NewBuffer(marshalledBlock),
nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import (
)

func TestProposeBeaconBlock_Deneb(t *testing.T) {
t.Skip("TODO: Fix this in the beacon-API PR")

ctrl := gomock.NewController(t)
defer ctrl.Finish()
jsonRestHandler := mock.NewMockJsonRestHandler(ctrl)
Expand All @@ -33,7 +31,7 @@ func TestProposeBeaconBlock_Deneb(t *testing.T) {
headers := map[string]string{"Eth-Consensus-Version": "deneb"}
jsonRestHandler.EXPECT().Post(
gomock.Any(),
"/eth/v1/beacon/blocks",
"/eth/v2/beacon/blocks",
headers,
bytes.NewBuffer(denebBytes),
nil,
Expand Down
Loading

0 comments on commit 5179af1

Please sign in to comment.