validator REST API: block v2 and Electra support (#14623)

* adding electra to validator client rest for get and post, also migrates to use the v2 endpoints

* changelog

* fixing test

* fixing linting
This commit is contained in:
james-prysm
2024-11-08 12:24:51 -06:00
committed by GitHub
parent c0f9689e30
commit 5179af1438
19 changed files with 1437 additions and 24 deletions

View File

@@ -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
@@ -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

View File

@@ -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",

View File

@@ -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,
@@ -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,

View File

@@ -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{}
@@ -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")
@@ -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")
@@ -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")
@@ -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)
}

View File

@@ -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()

View File

@@ -120,6 +120,35 @@ 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)
}
@@ -127,9 +156,9 @@ func (c *beaconApiValidatorClient) proposeBeaconBlock(ctx context.Context, in *e
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}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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)
@@ -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,

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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)
@@ -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,

View File

@@ -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_Electra(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jsonRestHandler := mock.NewMockJsonRestHandler(ctrl)
var blockContents structs.SignedBeaconBlockContentsElectra
err := json.Unmarshal([]byte(rpctesting.ElectraBlockContents), &blockContents)
require.NoError(t, err)
genericSignedBlock, err := blockContents.ToGeneric()
require.NoError(t, err)
electraBytes, err := json.Marshal(blockContents)
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/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.GetElectra().Block.HashTreeRoot()
require.NoError(t, err)
// Make sure that the block root is set
assert.DeepEqual(t, expectedBlockRoot[:], proposeResponse.BlockRoot)
}

View File

@@ -55,7 +55,7 @@ func TestProposeBeaconBlock_Phase0(t *testing.T) {
headers := map[string]string{"Eth-Consensus-Version": "phase0"}
jsonRestHandler.EXPECT().Post(
gomock.Any(),
"/eth/v1/beacon/blocks",
"/eth/v2/beacon/blocks",
headers,
bytes.NewBuffer(marshalledBlock),
nil,

View File

@@ -51,7 +51,7 @@ func TestProposeBeaconBlock_Error(t *testing.T) {
{
name: "phase0",
consensusVersion: "phase0",
endpoint: "/eth/v1/beacon/blocks",
endpoint: "/eth/v2/beacon/blocks",
block: &ethpb.GenericSignedBeaconBlock{
Block: generateSignedPhase0Block(),
},
@@ -59,7 +59,7 @@ func TestProposeBeaconBlock_Error(t *testing.T) {
{
name: "altair",
consensusVersion: "altair",
endpoint: "/eth/v1/beacon/blocks",
endpoint: "/eth/v2/beacon/blocks",
block: &ethpb.GenericSignedBeaconBlock{
Block: generateSignedAltairBlock(),
},
@@ -67,7 +67,7 @@ func TestProposeBeaconBlock_Error(t *testing.T) {
{
name: "bellatrix",
consensusVersion: "bellatrix",
endpoint: "/eth/v1/beacon/blocks",
endpoint: "/eth/v2/beacon/blocks",
block: &ethpb.GenericSignedBeaconBlock{
Block: generateSignedBellatrixBlock(),
},
@@ -75,15 +75,23 @@ func TestProposeBeaconBlock_Error(t *testing.T) {
{
name: "blinded bellatrix",
consensusVersion: "bellatrix",
endpoint: "/eth/v1/beacon/blinded_blocks",
endpoint: "/eth/v2/beacon/blinded_blocks",
block: &ethpb.GenericSignedBeaconBlock{
Block: generateSignedBlindedBellatrixBlock(),
},
},
{
name: "capella",
consensusVersion: "capella",
endpoint: "/eth/v2/beacon/blocks",
block: &ethpb.GenericSignedBeaconBlock{
Block: generateSignedCapellaBlock(),
},
},
{
name: "blinded capella",
consensusVersion: "capella",
endpoint: "/eth/v1/beacon/blinded_blocks",
endpoint: "/eth/v2/beacon/blinded_blocks",
block: &ethpb.GenericSignedBeaconBlock{
Block: generateSignedBlindedCapellaBlock(),
},

View File

@@ -8,6 +8,7 @@ go_library(
"bellatrix_beacon_block_test_helpers.go",
"capella_beacon_block_test_helpers.go",
"deneb_beacon_block_test_helpers.go",
"electra_beacon_block_test_helpers.go",
"phase0_beacon_block_test_helpers.go",
"test_helpers.go",
],

File diff suppressed because it is too large Load Diff