From d63ae69920038ad1796968949480e5cc59f843bc Mon Sep 17 00:00:00 2001 From: james-prysm <90280386+james-prysm@users.noreply.github.com> Date: Fri, 20 Jun 2025 09:27:09 -0500 Subject: [PATCH] adding ssz for get block endpoint (#15390) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * adding get ssz * adding some tests * gaz * adding ssz to e2e * wip ssz * adding in additional check on header type * remove unused * renaming json rest handler, and adding in usage of use ssz debug flag * fixing unit tests * fixing tests * gaz * radek feedback * Update config/features/config.go Co-authored-by: Radosław Kapka * Update config/features/flags.go Co-authored-by: Radosław Kapka * Update config/features/flags.go Co-authored-by: Radosław Kapka * Update validator/client/beacon-api/get_beacon_block.go Co-authored-by: Radosław Kapka * Update validator/client/beacon-api/get_beacon_block.go Co-authored-by: Radosław Kapka * Update validator/client/beacon-api/get_beacon_block.go Co-authored-by: Radosław Kapka * addressing feedback * missing import * another missing import * fixing tests * gaz * removing unused * gaz * more radek feedback * fixing context * adding in check for non accepted conent type * reverting to not create more edgecases --------- Co-authored-by: Radosław Kapka --- changelog/james-prysm_ssz-validator-block.md | 7 + config/features/config.go | 6 + config/features/flags.go | 7 + proto/prysm/v1alpha1/beacon_block.go | 6 + testing/endtoend/components/validator.go | 3 + testing/endtoend/minimal_scenario_e2e_test.go | 4 + testing/endtoend/types/types.go | 7 + validator/accounts/cli_manager.go | 2 +- validator/client/beacon-api/BUILD.bazel | 8 +- .../beacon_api_beacon_chain_client.go | 4 +- .../beacon-api/beacon_api_node_client.go | 4 +- .../beacon-api/beacon_api_validator_client.go | 4 +- validator/client/beacon-api/duties.go | 2 +- validator/client/beacon-api/genesis.go | 2 +- .../client/beacon-api/get_beacon_block.go | 339 ++++--- .../beacon-api/get_beacon_block_test.go | 869 +++++++++++++++--- .../beacon-api/mock/json_rest_handler_mock.go | 17 + .../beacon-api/prysm_beacon_chain_client.go | 4 +- ...rest_handler.go => rest_handler_client.go} | 76 +- ...er_test.go => rest_handler_client_test.go} | 99 +- .../client/beacon-api/state_validators.go | 2 +- .../beacon_chain_client_factory.go | 4 +- .../node_client_factory.go | 2 +- validator/client/service.go | 2 +- .../validator_client_factory.go | 2 +- validator/rpc/beacon.go | 2 +- 26 files changed, 1200 insertions(+), 284 deletions(-) create mode 100644 changelog/james-prysm_ssz-validator-block.md rename validator/client/beacon-api/{json_rest_handler.go => rest_handler_client.go} (56%) rename validator/client/beacon-api/{json_rest_handler_test.go => rest_handler_client_test.go} (67%) diff --git a/changelog/james-prysm_ssz-validator-block.md b/changelog/james-prysm_ssz-validator-block.md new file mode 100644 index 0000000000..6965120693 --- /dev/null +++ b/changelog/james-prysm_ssz-validator-block.md @@ -0,0 +1,7 @@ +### Added + +- New ssz-only flag for validator client to enable calling rest apis in SSZ, starting with get block endpoint. + +### Changed + +- when REST api is enabled the get Block api defaults to requesting and receiving SSZ instead of JSON, JSON is the fallback. \ No newline at end of file diff --git a/config/features/config.go b/config/features/config.go index 4d5a64cc8a..9fa377e071 100644 --- a/config/features/config.go +++ b/config/features/config.go @@ -52,6 +52,7 @@ type Flags struct { EnableExperimentalAttestationPool bool // EnableExperimentalAttestationPool enables an experimental attestation pool design. EnableDutiesV2 bool // EnableDutiesV2 sets validator client to use the get Duties V2 endpoint EnableWeb bool // EnableWeb enables the webui on the validator client + SSZOnly bool // SSZOnly forces the validator client to use SSZ for communication with the beacon node when REST mode is enabled (useful for debugging) // Logging related toggles. DisableGRPCConnectionLogs bool // Disables logging when a new grpc client has connected. EnableFullSSZDataLogging bool // Enables logging for full ssz data on rejected gossip messages @@ -344,6 +345,11 @@ func ConfigureValidator(ctx *cli.Context) error { logEnabled(EnableWebFlag) cfg.EnableWeb = true } + if ctx.Bool(SSZOnly.Name) { + logEnabled(SSZOnly) + cfg.SSZOnly = true + } + cfg.KeystoreImportDebounceInterval = ctx.Duration(dynamicKeyReloadDebounceInterval.Name) Init(cfg) return nil diff --git a/config/features/flags.go b/config/features/flags.go index 243de982d0..a5d640549b 100644 --- a/config/features/flags.go +++ b/config/features/flags.go @@ -201,6 +201,12 @@ var ( Usage: "(Work in progress): Enables the web portal for the validator client.", Value: false, } + + // SSZOnly forces the validator client to use SSZ for communication with the beacon node when REST mode is enabled + SSZOnly = &cli.BoolFlag{ + Name: "ssz-only", + Usage: "(debug): Forces the validator client to use SSZ for communication with the beacon node when REST mode is enabled", + } ) // devModeFlags holds list of flags that are set when development mode is on. @@ -223,6 +229,7 @@ var ValidatorFlags = append(deprecatedFlags, []cli.Flag{ EnableBeaconRESTApi, EnableDutiesV2, EnableWebFlag, + SSZOnly, }...) // E2EValidatorFlags contains a list of the validator feature flags to be tested in E2E. diff --git a/proto/prysm/v1alpha1/beacon_block.go b/proto/prysm/v1alpha1/beacon_block.go index e439a0a2f8..9363392a33 100644 --- a/proto/prysm/v1alpha1/beacon_block.go +++ b/proto/prysm/v1alpha1/beacon_block.go @@ -5,6 +5,12 @@ import ( enginev1 "github.com/OffchainLabs/prysm/v6/proto/engine/v1" ) +// GenericConverter defines any struct that can be converted to a generic beacon block. +// We assume all your versioned block structs implement this method. +type GenericConverter interface { + ToGeneric() (*GenericBeaconBlock, error) +} + // ---------------------------------------------------------------------------- // Phase 0 // ---------------------------------------------------------------------------- diff --git a/testing/endtoend/components/validator.go b/testing/endtoend/components/validator.go index f2b7bf2374..f10e6aaa54 100644 --- a/testing/endtoend/components/validator.go +++ b/testing/endtoend/components/validator.go @@ -248,6 +248,9 @@ func (v *ValidatorNode) Start(ctx context.Context) error { args = append(args, fmt.Sprintf("--%s=http://localhost:%d", flags.BeaconRESTApiProviderFlag.Name, beaconRestApiPort), fmt.Sprintf("--%s", features.EnableBeaconRESTApi.Name)) + if v.config.UseSSZOnly { + args = append(args, fmt.Sprintf("--%s", features.SSZOnly.Name)) + } } // Only apply e2e flags to the current branch. New flags may not exist in previous release. diff --git a/testing/endtoend/minimal_scenario_e2e_test.go b/testing/endtoend/minimal_scenario_e2e_test.go index 978aae4714..96ba723d1b 100644 --- a/testing/endtoend/minimal_scenario_e2e_test.go +++ b/testing/endtoend/minimal_scenario_e2e_test.go @@ -29,6 +29,10 @@ func TestEndToEnd_MinimalConfig_ValidatorRESTApi(t *testing.T) { e2eMinimal(t, types.InitForkCfg(version.Bellatrix, version.Electra, params.E2ETestConfig()), types.WithCheckpointSync(), types.WithValidatorRESTApi()).run() } +func TestEndToEnd_MinimalConfig_ValidatorRESTApi_SSZ(t *testing.T) { + e2eMinimal(t, types.InitForkCfg(version.Bellatrix, version.Electra, params.E2ETestConfig()), types.WithCheckpointSync(), types.WithValidatorRESTApi(), types.WithSSZOnly()).run() +} + func TestEndToEnd_ScenarioRun_EEOffline(t *testing.T) { t.Skip("TODO(#10242) Prysm is current unable to handle an offline e2e") cfg := types.InitForkCfg(version.Bellatrix, version.Deneb, params.E2ETestConfig()) diff --git a/testing/endtoend/types/types.go b/testing/endtoend/types/types.go index d0c1eca9aa..0b4040247f 100644 --- a/testing/endtoend/types/types.go +++ b/testing/endtoend/types/types.go @@ -51,6 +51,12 @@ func WithValidatorRESTApi() E2EConfigOpt { } } +func WithSSZOnly() E2EConfigOpt { + return func(cfg *E2EConfig) { + cfg.UseSSZOnly = true + } +} + func WithBuilder() E2EConfigOpt { return func(cfg *E2EConfig) { cfg.UseBuilder = true @@ -70,6 +76,7 @@ type E2EConfig struct { UseFixedPeerIDs bool UseValidatorCrossClient bool UseBeaconRestApi bool + UseSSZOnly bool UseBuilder bool EpochsToRun uint64 Seed int64 diff --git a/validator/accounts/cli_manager.go b/validator/accounts/cli_manager.go index e9add42ed6..ed4851d303 100644 --- a/validator/accounts/cli_manager.go +++ b/validator/accounts/cli_manager.go @@ -87,7 +87,7 @@ func (acm *CLIManager) prepareBeaconClients(ctx context.Context) (*iface.Validat acm.beaconApiTimeout, ) - restHandler := beaconApi.NewBeaconApiJsonRestHandler( + restHandler := beaconApi.NewBeaconApiRestHandler( http.Client{Timeout: acm.beaconApiTimeout}, acm.beaconApiEndpoint, ) diff --git a/validator/client/beacon-api/BUILD.bazel b/validator/client/beacon-api/BUILD.bazel index 36f625e9bf..b91480254e 100644 --- a/validator/client/beacon-api/BUILD.bazel +++ b/validator/client/beacon-api/BUILD.bazel @@ -18,7 +18,6 @@ go_library( "genesis.go", "get_beacon_block.go", "index.go", - "json_rest_handler.go", "log.go", "metrics.go", "prepare_beacon_proposer.go", @@ -27,6 +26,7 @@ go_library( "propose_exit.go", "prysm_beacon_chain_client.go", "registration.go", + "rest_handler_client.go", "state_validators.go", "status.go", "stream_blocks.go", @@ -47,6 +47,7 @@ go_library( "//api/server/structs:go_default_library", "//beacon-chain/core/helpers:go_default_library", "//beacon-chain/core/signing:go_default_library", + "//config/features:go_default_library", "//config/fieldparams:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", @@ -91,7 +92,6 @@ go_test( "genesis_test.go", "get_beacon_block_test.go", "index_test.go", - "json_rest_handler_test.go", "prepare_beacon_proposer_test.go", "propose_attestation_test.go", "propose_beacon_block_altair_test.go", @@ -110,6 +110,7 @@ go_test( "propose_exit_test.go", "prysm_beacon_chain_client_test.go", "registration_test.go", + "rest_handler_client_test.go", "state_validators_test.go", "status_test.go", "stream_blocks_test.go", @@ -128,6 +129,7 @@ go_test( "//api/server/structs:go_default_library", "//beacon-chain/core/helpers:go_default_library", "//beacon-chain/rpc/eth/shared/testing:go_default_library", + "//config/features:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", "//consensus-types/validator:go_default_library", @@ -145,6 +147,8 @@ go_test( "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_golang_protobuf//ptypes/empty", "@com_github_pkg_errors//:go_default_library", + "@com_github_sirupsen_logrus//:go_default_library", + "@com_github_sirupsen_logrus//hooks/test:go_default_library", "@org_golang_google_protobuf//types/known/emptypb:go_default_library", "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", "@org_uber_go_mock//gomock:go_default_library", diff --git a/validator/client/beacon-api/beacon_api_beacon_chain_client.go b/validator/client/beacon-api/beacon_api_beacon_chain_client.go index afa72ea5df..29ea1c4e37 100644 --- a/validator/client/beacon-api/beacon_api_beacon_chain_client.go +++ b/validator/client/beacon-api/beacon_api_beacon_chain_client.go @@ -17,7 +17,7 @@ import ( type beaconApiChainClient struct { fallbackClient iface.ChainClient - jsonRestHandler JsonRestHandler + jsonRestHandler RestHandler stateValidatorsProvider StateValidatorsProvider } @@ -333,7 +333,7 @@ func (c beaconApiChainClient) ValidatorParticipation(ctx context.Context, in *et return nil, errors.New("beaconApiChainClient.ValidatorParticipation is not implemented. To use a fallback client, pass a fallback client as the last argument of NewBeaconApiChainClientWithFallback.") } -func NewBeaconApiChainClientWithFallback(jsonRestHandler JsonRestHandler, fallbackClient iface.ChainClient) iface.ChainClient { +func NewBeaconApiChainClientWithFallback(jsonRestHandler RestHandler, fallbackClient iface.ChainClient) iface.ChainClient { return &beaconApiChainClient{ jsonRestHandler: jsonRestHandler, fallbackClient: fallbackClient, diff --git a/validator/client/beacon-api/beacon_api_node_client.go b/validator/client/beacon-api/beacon_api_node_client.go index b975f95a34..086d757415 100644 --- a/validator/client/beacon-api/beacon_api_node_client.go +++ b/validator/client/beacon-api/beacon_api_node_client.go @@ -20,7 +20,7 @@ var ( type beaconApiNodeClient struct { fallbackClient iface.NodeClient - jsonRestHandler JsonRestHandler + jsonRestHandler RestHandler genesisProvider GenesisProvider healthTracker health.Tracker } @@ -111,7 +111,7 @@ func (c *beaconApiNodeClient) HealthTracker() health.Tracker { return c.healthTracker } -func NewNodeClientWithFallback(jsonRestHandler JsonRestHandler, fallbackClient iface.NodeClient) iface.NodeClient { +func NewNodeClientWithFallback(jsonRestHandler RestHandler, fallbackClient iface.NodeClient) iface.NodeClient { b := &beaconApiNodeClient{ jsonRestHandler: jsonRestHandler, fallbackClient: fallbackClient, diff --git a/validator/client/beacon-api/beacon_api_validator_client.go b/validator/client/beacon-api/beacon_api_validator_client.go index 58143b8361..784a7cae99 100644 --- a/validator/client/beacon-api/beacon_api_validator_client.go +++ b/validator/client/beacon-api/beacon_api_validator_client.go @@ -22,13 +22,13 @@ type beaconApiValidatorClient struct { genesisProvider GenesisProvider dutiesProvider dutiesProvider stateValidatorsProvider StateValidatorsProvider - jsonRestHandler JsonRestHandler + jsonRestHandler RestHandler beaconBlockConverter BeaconBlockConverter prysmChainClient iface.PrysmChainClient isEventStreamRunning bool } -func NewBeaconApiValidatorClient(jsonRestHandler JsonRestHandler, opts ...ValidatorClientOpt) iface.ValidatorClient { +func NewBeaconApiValidatorClient(jsonRestHandler RestHandler, opts ...ValidatorClientOpt) iface.ValidatorClient { c := &beaconApiValidatorClient{ genesisProvider: &beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler}, dutiesProvider: beaconApiDutiesProvider{jsonRestHandler: jsonRestHandler}, diff --git a/validator/client/beacon-api/duties.go b/validator/client/beacon-api/duties.go index 1b9536ec60..e99f019dfd 100644 --- a/validator/client/beacon-api/duties.go +++ b/validator/client/beacon-api/duties.go @@ -27,7 +27,7 @@ type dutiesProvider interface { } type beaconApiDutiesProvider struct { - jsonRestHandler JsonRestHandler + jsonRestHandler RestHandler } type attesterDuty struct { diff --git a/validator/client/beacon-api/genesis.go b/validator/client/beacon-api/genesis.go index fb5b8dd136..ddbf543d92 100644 --- a/validator/client/beacon-api/genesis.go +++ b/validator/client/beacon-api/genesis.go @@ -20,7 +20,7 @@ type GenesisProvider interface { } type beaconApiGenesisProvider struct { - jsonRestHandler JsonRestHandler + jsonRestHandler RestHandler genesis *structs.Genesis once sync.Once } diff --git a/validator/client/beacon-api/get_beacon_block.go b/validator/client/beacon-api/get_beacon_block.go index 26e444e2b4..ef17b363d8 100644 --- a/validator/client/beacon-api/get_beacon_block.go +++ b/validator/client/beacon-api/get_beacon_block.go @@ -6,7 +6,10 @@ import ( "encoding/json" "fmt" neturl "net/url" + "strconv" + "strings" + "github.com/OffchainLabs/prysm/v6/api" "github.com/OffchainLabs/prysm/v6/api/apiutil" "github.com/OffchainLabs/prysm/v6/api/server/structs" "github.com/OffchainLabs/prysm/v6/consensus-types/primitives" @@ -22,160 +25,224 @@ func (c *beaconApiValidatorClient) beaconBlock(ctx context.Context, slot primiti if len(graffiti) > 0 { queryParams.Add("graffiti", hexutil.Encode(graffiti)) } - queryUrl := apiutil.BuildURL(fmt.Sprintf("/eth/v3/validator/blocks/%d", slot), queryParams) - produceBlockV3ResponseJson := structs.ProduceBlockV3Response{} - err := c.jsonRestHandler.Get(ctx, queryUrl, &produceBlockV3ResponseJson) + data, header, err := c.jsonRestHandler.GetSSZ(ctx, queryUrl) if err != nil { return nil, err } - - return processBlockResponse( - produceBlockV3ResponseJson.Version, - produceBlockV3ResponseJson.ExecutionPayloadBlinded, - json.NewDecoder(bytes.NewReader(produceBlockV3ResponseJson.Data)), - ) + if strings.Contains(header.Get("Content-Type"), api.OctetStreamMediaType) { + ver, err := version.FromString(header.Get(api.VersionHeader)) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("unsupported header version %s", header.Get(api.VersionHeader))) + } + isBlindedRaw := header.Get(api.ExecutionPayloadBlindedHeader) + isBlinded, err := strconv.ParseBool(isBlindedRaw) + if err != nil { + return nil, err + } + return processBlockSSZResponse(ver, data, isBlinded) + } else { + decoder := json.NewDecoder(bytes.NewBuffer(data)) + produceBlockV3ResponseJson := structs.ProduceBlockV3Response{} + if err = decoder.Decode(&produceBlockV3ResponseJson); err != nil { + return nil, errors.Wrapf(err, "failed to decode response body into json for %s", queryUrl) + } + return processBlockJSONResponse( + produceBlockV3ResponseJson.Version, + produceBlockV3ResponseJson.ExecutionPayloadBlinded, + json.NewDecoder(bytes.NewReader(produceBlockV3ResponseJson.Data)), + ) + } } -// nolint: gocognit -func processBlockResponse(ver string, isBlinded bool, decoder *json.Decoder) (*ethpb.GenericBeaconBlock, error) { - var response *ethpb.GenericBeaconBlock +func processBlockSSZResponse(ver int, data []byte, isBlinded bool) (*ethpb.GenericBeaconBlock, error) { + if ver >= version.Fulu { + return processBlockSSZResponseFulu(data, isBlinded) + } + if ver >= version.Electra { + return processBlockSSZResponseElectra(data, isBlinded) + } + if ver >= version.Deneb { + return processBlockSSZResponseDeneb(data, isBlinded) + } + if ver >= version.Capella { + return processBlockSSZResponseCapella(data, isBlinded) + } + if ver >= version.Bellatrix { + return processBlockSSZResponseBellatrix(data, isBlinded) + } + if ver >= version.Altair { + block := ðpb.BeaconBlockAltair{} + if err := block.UnmarshalSSZ(data); err != nil { + return nil, err + } + return ðpb.GenericBeaconBlock{Block: ðpb.GenericBeaconBlock_Altair{Altair: block}}, nil + } + if ver >= version.Phase0 { + block := ðpb.BeaconBlock{} + if err := block.UnmarshalSSZ(data); err != nil { + return nil, err + } + return ðpb.GenericBeaconBlock{Block: ðpb.GenericBeaconBlock_Phase0{Phase0: block}}, nil + } + return nil, fmt.Errorf("unsupported block version %s", version.String(ver)) +} + +func processBlockSSZResponseFulu(data []byte, isBlinded bool) (*ethpb.GenericBeaconBlock, error) { + if isBlinded { + blindedBlock := ðpb.BlindedBeaconBlockFulu{} + if err := blindedBlock.UnmarshalSSZ(data); err != nil { + return nil, err + } + return ðpb.GenericBeaconBlock{Block: ðpb.GenericBeaconBlock_BlindedFulu{BlindedFulu: blindedBlock}, IsBlinded: true}, nil + } + block := ðpb.BeaconBlockContentsFulu{} + if err := block.UnmarshalSSZ(data); err != nil { + return nil, err + } + return ðpb.GenericBeaconBlock{Block: ðpb.GenericBeaconBlock_Fulu{Fulu: block}}, nil +} + +func processBlockSSZResponseElectra(data []byte, isBlinded bool) (*ethpb.GenericBeaconBlock, error) { + if isBlinded { + blindedBlock := ðpb.BlindedBeaconBlockElectra{} + if err := blindedBlock.UnmarshalSSZ(data); err != nil { + return nil, err + } + return ðpb.GenericBeaconBlock{Block: ðpb.GenericBeaconBlock_BlindedElectra{BlindedElectra: blindedBlock}, IsBlinded: true}, nil + } + block := ðpb.BeaconBlockContentsElectra{} + if err := block.UnmarshalSSZ(data); err != nil { + return nil, err + } + return ðpb.GenericBeaconBlock{Block: ðpb.GenericBeaconBlock_Electra{Electra: block}}, nil +} + +func processBlockSSZResponseDeneb(data []byte, isBlinded bool) (*ethpb.GenericBeaconBlock, error) { + if isBlinded { + blindedBlock := ðpb.BlindedBeaconBlockDeneb{} + if err := blindedBlock.UnmarshalSSZ(data); err != nil { + return nil, err + } + return ðpb.GenericBeaconBlock{Block: ðpb.GenericBeaconBlock_BlindedDeneb{BlindedDeneb: blindedBlock}, IsBlinded: true}, nil + } + block := ðpb.BeaconBlockContentsDeneb{} + if err := block.UnmarshalSSZ(data); err != nil { + return nil, err + } + return ðpb.GenericBeaconBlock{Block: ðpb.GenericBeaconBlock_Deneb{Deneb: block}}, nil +} + +func processBlockSSZResponseCapella(data []byte, isBlinded bool) (*ethpb.GenericBeaconBlock, error) { + if isBlinded { + blindedBlock := ðpb.BlindedBeaconBlockCapella{} + if err := blindedBlock.UnmarshalSSZ(data); err != nil { + return nil, err + } + return ðpb.GenericBeaconBlock{Block: ðpb.GenericBeaconBlock_BlindedCapella{BlindedCapella: blindedBlock}, IsBlinded: true}, nil + } + block := ðpb.BeaconBlockCapella{} + if err := block.UnmarshalSSZ(data); err != nil { + return nil, err + } + return ðpb.GenericBeaconBlock{Block: ðpb.GenericBeaconBlock_Capella{Capella: block}}, nil +} + +func processBlockSSZResponseBellatrix(data []byte, isBlinded bool) (*ethpb.GenericBeaconBlock, error) { + if isBlinded { + blindedBlock := ðpb.BlindedBeaconBlockBellatrix{} + if err := blindedBlock.UnmarshalSSZ(data); err != nil { + return nil, err + } + return ðpb.GenericBeaconBlock{Block: ðpb.GenericBeaconBlock_BlindedBellatrix{BlindedBellatrix: blindedBlock}, IsBlinded: true}, nil + } + block := ðpb.BeaconBlockBellatrix{} + if err := block.UnmarshalSSZ(data); err != nil { + return nil, err + } + return ðpb.GenericBeaconBlock{Block: ðpb.GenericBeaconBlock_Bellatrix{Bellatrix: block}}, nil +} + +func convertBlockToGeneric(decoder *json.Decoder, dest ethpb.GenericConverter, version string, isBlinded bool) (*ethpb.GenericBeaconBlock, error) { + typeName := version + if isBlinded { + typeName = "blinded " + typeName + } + + if err := decoder.Decode(dest); err != nil { + return nil, errors.Wrapf(err, "failed to decode %s block response json", typeName) + } + + genericBlock, err := dest.ToGeneric() + if err != nil { + return nil, errors.Wrapf(err, "failed to convert %s block", typeName) + } + return genericBlock, nil +} + +func processBlockJSONResponse(ver string, isBlinded bool, decoder *json.Decoder) (*ethpb.GenericBeaconBlock, error) { if decoder == nil { return nil, errors.New("no produce block json decoder found") } + switch ver { case version.String(version.Phase0): - jsonPhase0Block := structs.BeaconBlock{} - if err := decoder.Decode(&jsonPhase0Block); err != nil { - return nil, errors.Wrap(err, "failed to decode phase0 block response json") - } - genericBlock, err := jsonPhase0Block.ToGeneric() - if err != nil { - return nil, errors.Wrap(err, "failed to get phase0 block") - } - response = genericBlock + return convertBlockToGeneric(decoder, &structs.BeaconBlock{}, version.String(version.Phase0), false) + case version.String(version.Altair): - jsonAltairBlock := structs.BeaconBlockAltair{} - if err := decoder.Decode(&jsonAltairBlock); err != nil { - return nil, errors.Wrap(err, "failed to decode altair block response json") - } - genericBlock, err := jsonAltairBlock.ToGeneric() - if err != nil { - return nil, errors.Wrap(err, "failed to get altair block") - } - response = genericBlock + return convertBlockToGeneric(decoder, &structs.BeaconBlockAltair{}, "altair", false) + case version.String(version.Bellatrix): - 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") - } - genericBlock, err := jsonBellatrixBlock.ToGeneric() - if err != nil { - return nil, errors.Wrap(err, "failed to get blinded bellatrix block") - } - response = genericBlock - } else { - jsonBellatrixBlock := structs.BeaconBlockBellatrix{} - if err := decoder.Decode(&jsonBellatrixBlock); err != nil { - return nil, errors.Wrap(err, "failed to decode bellatrix block response json") - } - genericBlock, err := jsonBellatrixBlock.ToGeneric() - if err != nil { - return nil, errors.Wrap(err, "failed to get bellatrix block") - } - response = genericBlock - } + return processBellatrixBlock(decoder, isBlinded) + case version.String(version.Capella): - 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") - } - genericBlock, err := jsonCapellaBlock.ToGeneric() - if err != nil { - return nil, errors.Wrap(err, "failed to get blinded capella block") - } - response = genericBlock - } else { - jsonCapellaBlock := structs.BeaconBlockCapella{} - if err := decoder.Decode(&jsonCapellaBlock); err != nil { - return nil, errors.Wrap(err, "failed to decode capella block response json") - } - genericBlock, err := jsonCapellaBlock.ToGeneric() - if err != nil { - return nil, errors.Wrap(err, "failed to get capella block") - } - response = genericBlock - } + return processCapellaBlock(decoder, isBlinded) + case version.String(version.Deneb): - 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") - } - genericBlock, err := jsonDenebBlock.ToGeneric() - if err != nil { - return nil, errors.Wrap(err, "failed to get blinded deneb block") - } - response = genericBlock - } else { - jsonDenebBlockContents := structs.BeaconBlockContentsDeneb{} - if err := decoder.Decode(&jsonDenebBlockContents); err != nil { - return nil, errors.Wrap(err, "failed to decode deneb block response json") - } - genericBlock, err := jsonDenebBlockContents.ToGeneric() - if err != nil { - return nil, errors.Wrap(err, "failed to get deneb block") - } - response = genericBlock - } + return processDenebBlock(decoder, isBlinded) + 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 - } + return processElectraBlock(decoder, isBlinded) + case version.String(version.Fulu): - if isBlinded { - jsonFuluBlock := structs.BlindedBeaconBlockFulu{} - if err := decoder.Decode(&jsonFuluBlock); err != nil { - return nil, errors.Wrap(err, "failed to decode blinded fulu block response json") - } - genericBlock, err := jsonFuluBlock.ToGeneric() - if err != nil { - return nil, errors.Wrap(err, "failed to get blinded fulu block") - } - response = genericBlock - } else { - jsonFuluBlockContents := structs.BeaconBlockContentsFulu{} - if err := decoder.Decode(&jsonFuluBlockContents); err != nil { - return nil, errors.Wrap(err, "failed to decode fulu block response json") - } - genericBlock, err := jsonFuluBlockContents.ToGeneric() - if err != nil { - return nil, errors.Wrap(err, "failed to get fulu block") - } - response = genericBlock - } + return processFuluBlock(decoder, isBlinded) + default: return nil, errors.Errorf("unsupported consensus version `%s`", ver) } - return response, nil +} + +func processBellatrixBlock(decoder *json.Decoder, isBlinded bool) (*ethpb.GenericBeaconBlock, error) { + if isBlinded { + return convertBlockToGeneric(decoder, &structs.BlindedBeaconBlockBellatrix{}, "bellatrix", true) + } + return convertBlockToGeneric(decoder, &structs.BeaconBlockBellatrix{}, "bellatrix", false) +} + +func processCapellaBlock(decoder *json.Decoder, isBlinded bool) (*ethpb.GenericBeaconBlock, error) { + if isBlinded { + return convertBlockToGeneric(decoder, &structs.BlindedBeaconBlockCapella{}, "capella", true) + } + return convertBlockToGeneric(decoder, &structs.BeaconBlockCapella{}, "capella", false) +} + +func processDenebBlock(decoder *json.Decoder, isBlinded bool) (*ethpb.GenericBeaconBlock, error) { + if isBlinded { + return convertBlockToGeneric(decoder, &structs.BlindedBeaconBlockDeneb{}, "deneb", true) + } + return convertBlockToGeneric(decoder, &structs.BeaconBlockContentsDeneb{}, "deneb", false) +} + +func processElectraBlock(decoder *json.Decoder, isBlinded bool) (*ethpb.GenericBeaconBlock, error) { + if isBlinded { + return convertBlockToGeneric(decoder, &structs.BlindedBeaconBlockElectra{}, "electra", true) + } + return convertBlockToGeneric(decoder, &structs.BeaconBlockContentsElectra{}, "electra", false) +} + +func processFuluBlock(decoder *json.Decoder, isBlinded bool) (*ethpb.GenericBeaconBlock, error) { + if isBlinded { + return convertBlockToGeneric(decoder, &structs.BlindedBeaconBlockFulu{}, "fulu", true) + } + return convertBlockToGeneric(decoder, &structs.BeaconBlockContentsFulu{}, "fulu", false) } diff --git a/validator/client/beacon-api/get_beacon_block_test.go b/validator/client/beacon-api/get_beacon_block_test.go index e038235dbb..ed691e33e7 100644 --- a/validator/client/beacon-api/get_beacon_block_test.go +++ b/validator/client/beacon-api/get_beacon_block_test.go @@ -4,9 +4,12 @@ import ( "encoding/json" "errors" "fmt" + "net/http" "testing" + "github.com/OffchainLabs/prysm/v6/api" "github.com/OffchainLabs/prysm/v6/api/server/structs" + "github.com/OffchainLabs/prysm/v6/config/features" "github.com/OffchainLabs/prysm/v6/consensus-types/primitives" ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v6/testing/assert" @@ -24,11 +27,12 @@ func TestGetBeaconBlock_RequestFailed(t *testing.T) { ctx := t.Context() jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) - jsonRestHandler.EXPECT().Get( - gomock.Any(), + jsonRestHandler.EXPECT().GetSSZ( gomock.Any(), gomock.Any(), ).Return( + nil, + nil, errors.New("foo error"), ).Times(1) @@ -44,67 +48,86 @@ func TestGetBeaconBlock_Error(t *testing.T) { expectedErrorMessage string consensusVersion string blinded bool - data json.RawMessage }{ { name: "phase0 block decoding failed", - expectedErrorMessage: "failed to decode phase0 block response json", + expectedErrorMessage: "failed to convert phase0 block: could not decode ", consensusVersion: "phase0", - data: []byte{}, }, { name: "altair block decoding failed", - expectedErrorMessage: "failed to decode altair block response json", + expectedErrorMessage: "failed to convert altair block: could not decode ", consensusVersion: "altair", - data: []byte{}, }, { name: "bellatrix block decoding failed", - expectedErrorMessage: "failed to decode bellatrix block response json", + expectedErrorMessage: "failed to convert bellatrix block: could not decode ", beaconBlock: "foo", consensusVersion: "bellatrix", blinded: false, - data: []byte{}, }, { name: "blinded bellatrix block decoding failed", - expectedErrorMessage: "failed to decode bellatrix block response json", + expectedErrorMessage: "failed to convert bellatrix block: could not decode ", beaconBlock: "foo", consensusVersion: "bellatrix", blinded: true, - data: []byte{}, }, { name: "capella block decoding failed", - expectedErrorMessage: "failed to decode capella block response json", + expectedErrorMessage: "failed to convert capella block: could not decode ", beaconBlock: "foo", consensusVersion: "capella", blinded: false, - data: []byte{}, }, { name: "blinded capella block decoding failed", - expectedErrorMessage: "failed to decode capella block response json", + expectedErrorMessage: "failed to convert capella block: could not decode ", beaconBlock: "foo", consensusVersion: "capella", blinded: true, - data: []byte{}, }, { name: "deneb block decoding failed", - expectedErrorMessage: "failed to decode deneb block response json", + expectedErrorMessage: "failed to convert deneb block: could not decode ", beaconBlock: "foo", consensusVersion: "deneb", blinded: false, - data: []byte{}, }, { name: "blinded deneb block decoding failed", - expectedErrorMessage: "failed to decode deneb block response json", + expectedErrorMessage: "failed to convert deneb block: could not decode ", beaconBlock: "foo", consensusVersion: "deneb", blinded: true, - data: []byte{}, + }, + { + name: "electra block decoding failed", + expectedErrorMessage: "failed to convert electra block: could not decode ", + beaconBlock: "foo", + consensusVersion: "electra", + blinded: false, + }, + { + name: "blinded electra block decoding failed", + expectedErrorMessage: "failed to convert electra block: could not decode ", + beaconBlock: "foo", + consensusVersion: "electra", + blinded: true, + }, + { + name: "fulu block decoding failed", + expectedErrorMessage: "failed to convert fulu block: could not decode ", + beaconBlock: "foo", + consensusVersion: "fulu", + blinded: false, + }, + { + name: "blinded fulu block decoding failed", + expectedErrorMessage: "failed to convert fulu block: could not decode ", + beaconBlock: "foo", + consensusVersion: "fulu", + blinded: true, }, { name: "unsupported consensus version", @@ -120,23 +143,25 @@ func TestGetBeaconBlock_Error(t *testing.T) { ctx := t.Context() + resp := structs.ProduceBlockV3Response{ + Version: testCase.consensusVersion, + Data: json.RawMessage(`{}`), // ← valid JSON object + } + + b, err := json.Marshal(resp) + require.NoError(t, err) jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) - jsonRestHandler.EXPECT().Get( + jsonRestHandler.EXPECT().GetSSZ( gomock.Any(), gomock.Any(), - &structs.ProduceBlockV3Response{}, - ).SetArg( - 2, - structs.ProduceBlockV3Response{ - Version: testCase.consensusVersion, - Data: testCase.data, - }, ).Return( + b, + http.Header{"Content-Type": []string{"application/json"}}, nil, ).Times(1) validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} - _, err := validatorClient.beaconBlock(ctx, 1, []byte{1}, []byte{2}) + _, err = validatorClient.beaconBlock(ctx, 1, []byte{1}, []byte{2}) assert.ErrorContains(t, testCase.expectedErrorMessage, err) }) } @@ -156,18 +181,18 @@ func TestGetBeaconBlock_Phase0Valid(t *testing.T) { graffiti := []byte{3} ctx := t.Context() + b, err := json.Marshal(structs.ProduceBlockV3Response{ + Version: "phase0", + Data: bytes, + }) + require.NoError(t, err) jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) - jsonRestHandler.EXPECT().Get( + jsonRestHandler.EXPECT().GetSSZ( 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: "phase0", - Data: bytes, - }, ).Return( + b, + http.Header{"Content-Type": []string{"application/json"}}, nil, ).Times(1) @@ -184,6 +209,618 @@ func TestGetBeaconBlock_Phase0Valid(t *testing.T) { assert.DeepEqual(t, expectedBeaconBlock, beaconBlock) } +// Add SSZ test cases below this line + +func TestGetBeaconBlock_SSZ_BellatrixValid(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resetFn := features.InitWithReset(&features.Flags{ + SSZOnly: true, + }) + defer resetFn() + + proto := testhelpers.GenerateProtoBellatrixBeaconBlock() + bytes, err := proto.MarshalSSZ() + require.NoError(t, err) + + const slot = primitives.Slot(1) + randaoReveal := []byte{2} + graffiti := []byte{3} + + ctx := t.Context() + + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().GetSSZ( + gomock.Any(), + fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)), + ).Return( + bytes, + http.Header{ + "Content-Type": []string{api.OctetStreamMediaType}, + api.VersionHeader: []string{"bellatrix"}, + api.ExecutionPayloadBlindedHeader: []string{"false"}, + }, + nil, + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + beaconBlock, err := validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti) + require.NoError(t, err) + + expectedBeaconBlock := ðpb.GenericBeaconBlock{ + Block: ðpb.GenericBeaconBlock_Bellatrix{ + Bellatrix: proto, + }, + IsBlinded: false, + } + + assert.DeepEqual(t, expectedBeaconBlock, beaconBlock) +} + +func TestGetBeaconBlock_SSZ_BlindedBellatrixValid(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resetFn := features.InitWithReset(&features.Flags{ + SSZOnly: true, + }) + defer resetFn() + + proto := testhelpers.GenerateProtoBlindedBellatrixBeaconBlock() + bytes, err := proto.MarshalSSZ() + require.NoError(t, err) + + const slot = primitives.Slot(1) + randaoReveal := []byte{2} + graffiti := []byte{3} + + ctx := t.Context() + + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().GetSSZ( + gomock.Any(), + fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)), + ).Return( + bytes, + http.Header{ + "Content-Type": []string{api.OctetStreamMediaType}, + api.VersionHeader: []string{"bellatrix"}, + api.ExecutionPayloadBlindedHeader: []string{"true"}, + }, + nil, + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + beaconBlock, err := validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti) + require.NoError(t, err) + + expectedBeaconBlock := ðpb.GenericBeaconBlock{ + Block: ðpb.GenericBeaconBlock_BlindedBellatrix{ + BlindedBellatrix: proto, + }, + IsBlinded: true, + } + + assert.DeepEqual(t, expectedBeaconBlock, beaconBlock) +} + +func TestGetBeaconBlock_SSZ_CapellaValid(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resetFn := features.InitWithReset(&features.Flags{ + SSZOnly: true, + }) + defer resetFn() + + proto := testhelpers.GenerateProtoCapellaBeaconBlock() + bytes, err := proto.MarshalSSZ() + require.NoError(t, err) + + const slot = primitives.Slot(1) + randaoReveal := []byte{2} + graffiti := []byte{3} + + ctx := t.Context() + + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().GetSSZ( + gomock.Any(), + fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)), + ).Return( + bytes, + http.Header{ + "Content-Type": []string{api.OctetStreamMediaType}, + api.VersionHeader: []string{"capella"}, + api.ExecutionPayloadBlindedHeader: []string{"false"}, + }, + nil, + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + beaconBlock, err := validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti) + require.NoError(t, err) + + expectedBeaconBlock := ðpb.GenericBeaconBlock{ + Block: ðpb.GenericBeaconBlock_Capella{ + Capella: proto, + }, + IsBlinded: false, + } + + assert.DeepEqual(t, expectedBeaconBlock, beaconBlock) +} + +func TestGetBeaconBlock_SSZ_BlindedCapellaValid(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resetFn := features.InitWithReset(&features.Flags{ + SSZOnly: true, + }) + defer resetFn() + + proto := testhelpers.GenerateProtoBlindedCapellaBeaconBlock() + bytes, err := proto.MarshalSSZ() + require.NoError(t, err) + + const slot = primitives.Slot(1) + randaoReveal := []byte{2} + graffiti := []byte{3} + + ctx := t.Context() + + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().GetSSZ( + gomock.Any(), + fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)), + ).Return( + bytes, + http.Header{ + "Content-Type": []string{api.OctetStreamMediaType}, + api.VersionHeader: []string{"capella"}, + api.ExecutionPayloadBlindedHeader: []string{"true"}, + }, + nil, + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + beaconBlock, err := validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti) + require.NoError(t, err) + + expectedBeaconBlock := ðpb.GenericBeaconBlock{ + Block: ðpb.GenericBeaconBlock_BlindedCapella{ + BlindedCapella: proto, + }, + IsBlinded: true, + } + + assert.DeepEqual(t, expectedBeaconBlock, beaconBlock) +} + +func TestGetBeaconBlock_SSZ_DenebValid(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resetFn := features.InitWithReset(&features.Flags{ + SSZOnly: true, + }) + defer resetFn() + + proto := testhelpers.GenerateProtoDenebBeaconBlockContents() + bytes, err := proto.MarshalSSZ() + require.NoError(t, err) + + const slot = primitives.Slot(1) + randaoReveal := []byte{2} + graffiti := []byte{3} + + ctx := t.Context() + + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().GetSSZ( + gomock.Any(), + fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)), + ).Return( + bytes, + http.Header{ + "Content-Type": []string{api.OctetStreamMediaType}, + api.VersionHeader: []string{"deneb"}, + api.ExecutionPayloadBlindedHeader: []string{"false"}, + }, + nil, + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + beaconBlock, err := validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti) + require.NoError(t, err) + + expectedBeaconBlock := ðpb.GenericBeaconBlock{ + Block: ðpb.GenericBeaconBlock_Deneb{ + Deneb: proto, + }, + IsBlinded: false, + } + + assert.DeepEqual(t, expectedBeaconBlock, beaconBlock) +} + +func TestGetBeaconBlock_SSZ_BlindedDenebValid(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resetFn := features.InitWithReset(&features.Flags{ + SSZOnly: true, + }) + defer resetFn() + + proto := testhelpers.GenerateProtoBlindedDenebBeaconBlock() + bytes, err := proto.MarshalSSZ() + require.NoError(t, err) + + const slot = primitives.Slot(1) + randaoReveal := []byte{2} + graffiti := []byte{3} + + ctx := t.Context() + + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().GetSSZ( + gomock.Any(), + fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)), + ).Return( + bytes, + http.Header{ + "Content-Type": []string{api.OctetStreamMediaType}, + api.VersionHeader: []string{"deneb"}, + api.ExecutionPayloadBlindedHeader: []string{"true"}, + }, + nil, + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + beaconBlock, err := validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti) + require.NoError(t, err) + + expectedBeaconBlock := ðpb.GenericBeaconBlock{ + Block: ðpb.GenericBeaconBlock_BlindedDeneb{ + BlindedDeneb: proto, + }, + IsBlinded: true, + } + + assert.DeepEqual(t, expectedBeaconBlock, beaconBlock) +} + +func TestGetBeaconBlock_SSZ_ElectraValid(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resetFn := features.InitWithReset(&features.Flags{ + SSZOnly: true, + }) + defer resetFn() + + proto := testhelpers.GenerateProtoElectraBeaconBlockContents() + bytes, err := proto.MarshalSSZ() + require.NoError(t, err) + + const slot = primitives.Slot(1) + randaoReveal := []byte{2} + graffiti := []byte{3} + + ctx := t.Context() + + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().GetSSZ( + gomock.Any(), + fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)), + ).Return( + bytes, + http.Header{ + "Content-Type": []string{api.OctetStreamMediaType}, + api.VersionHeader: []string{"electra"}, + api.ExecutionPayloadBlindedHeader: []string{"false"}, + }, + nil, + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + beaconBlock, err := validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti) + require.NoError(t, err) + + expectedBeaconBlock := ðpb.GenericBeaconBlock{ + Block: ðpb.GenericBeaconBlock_Electra{ + Electra: proto, + }, + IsBlinded: false, + } + + assert.DeepEqual(t, expectedBeaconBlock, beaconBlock) +} + +func TestGetBeaconBlock_SSZ_BlindedElectraValid(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resetFn := features.InitWithReset(&features.Flags{ + SSZOnly: true, + }) + defer resetFn() + + proto := testhelpers.GenerateProtoBlindedElectraBeaconBlock() + bytes, err := proto.MarshalSSZ() + require.NoError(t, err) + + const slot = primitives.Slot(1) + randaoReveal := []byte{2} + graffiti := []byte{3} + + ctx := t.Context() + + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().GetSSZ( + gomock.Any(), + fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)), + ).Return( + bytes, + http.Header{ + "Content-Type": []string{api.OctetStreamMediaType}, + api.VersionHeader: []string{"electra"}, + api.ExecutionPayloadBlindedHeader: []string{"true"}, + }, + nil, + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + beaconBlock, err := validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti) + require.NoError(t, err) + + expectedBeaconBlock := ðpb.GenericBeaconBlock{ + Block: ðpb.GenericBeaconBlock_BlindedElectra{ + BlindedElectra: proto, + }, + IsBlinded: true, + } + + assert.DeepEqual(t, expectedBeaconBlock, beaconBlock) +} + +func TestGetBeaconBlock_SSZ_UnsupportedVersion(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resetFn := features.InitWithReset(&features.Flags{ + SSZOnly: true, + }) + defer resetFn() + + const slot = primitives.Slot(1) + randaoReveal := []byte{2} + graffiti := []byte{3} + + ctx := t.Context() + + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().GetSSZ( + gomock.Any(), + fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)), + ).Return( + []byte{}, + http.Header{ + "Content-Type": []string{api.OctetStreamMediaType}, + api.VersionHeader: []string{"unsupported"}, + api.ExecutionPayloadBlindedHeader: []string{"false"}, + }, + nil, + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + _, err := validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti) + assert.ErrorContains(t, "version name doesn't map to a known value in the enum", err) +} + +func TestGetBeaconBlock_SSZ_InvalidBlindedHeader(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resetFn := features.InitWithReset(&features.Flags{ + SSZOnly: true, + }) + defer resetFn() + + proto := testhelpers.GenerateProtoBellatrixBeaconBlock() + bytes, err := proto.MarshalSSZ() + require.NoError(t, err) + + const slot = primitives.Slot(1) + randaoReveal := []byte{2} + graffiti := []byte{3} + + ctx := t.Context() + + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().GetSSZ( + gomock.Any(), + fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)), + ).Return( + bytes, + http.Header{ + "Content-Type": []string{api.OctetStreamMediaType}, + api.VersionHeader: []string{"bellatrix"}, + api.ExecutionPayloadBlindedHeader: []string{"invalid"}, + }, + nil, + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + _, err = validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti) + assert.ErrorContains(t, "strconv.ParseBool: parsing \"invalid\": invalid syntax", err) +} + +func TestGetBeaconBlock_SSZ_InvalidVersionHeader(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resetFn := features.InitWithReset(&features.Flags{ + SSZOnly: true, + }) + defer resetFn() + + proto := testhelpers.GenerateProtoBellatrixBeaconBlock() + bytes, err := proto.MarshalSSZ() + require.NoError(t, err) + + const slot = primitives.Slot(1) + randaoReveal := []byte{2} + graffiti := []byte{3} + + ctx := t.Context() + + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().GetSSZ( + gomock.Any(), + fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)), + ).Return( + bytes, + http.Header{ + "Content-Type": []string{api.OctetStreamMediaType}, + api.VersionHeader: []string{"invalid"}, + api.ExecutionPayloadBlindedHeader: []string{"false"}, + }, + nil, + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + _, err = validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti) + assert.ErrorContains(t, "unsupported header version invalid", err) +} + +func TestGetBeaconBlock_SSZ_GetSSZError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resetFn := features.InitWithReset(&features.Flags{ + SSZOnly: true, + }) + defer resetFn() + + const slot = primitives.Slot(1) + randaoReveal := []byte{2} + graffiti := []byte{3} + + ctx := t.Context() + + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().GetSSZ( + gomock.Any(), + fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)), + ).Return( + nil, + nil, + errors.New("get ssz error"), + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + _, err := validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti) + assert.ErrorContains(t, "get ssz error", err) +} + +func TestGetBeaconBlock_SSZ_Phase0Valid(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resetFn := features.InitWithReset(&features.Flags{ + SSZOnly: true, + }) + defer resetFn() + + proto := testhelpers.GenerateProtoPhase0BeaconBlock() + bytes, err := proto.MarshalSSZ() + require.NoError(t, err) + + const slot = primitives.Slot(1) + randaoReveal := []byte{2} + graffiti := []byte{3} + + ctx := t.Context() + + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().GetSSZ( + gomock.Any(), + fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)), + ).Return( + bytes, + http.Header{ + "Content-Type": []string{api.OctetStreamMediaType}, + api.VersionHeader: []string{"phase0"}, + api.ExecutionPayloadBlindedHeader: []string{"false"}, + }, + nil, + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + beaconBlock, err := validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti) + require.NoError(t, err) + + expectedBeaconBlock := ðpb.GenericBeaconBlock{ + Block: ðpb.GenericBeaconBlock_Phase0{ + Phase0: proto, + }, + IsBlinded: false, + } + + assert.DeepEqual(t, expectedBeaconBlock, beaconBlock) +} + +func TestGetBeaconBlock_SSZ_AltairValid(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resetFn := features.InitWithReset(&features.Flags{ + SSZOnly: true, + }) + defer resetFn() + + proto := testhelpers.GenerateProtoAltairBeaconBlock() + bytes, err := proto.MarshalSSZ() + require.NoError(t, err) + + const slot = primitives.Slot(1) + randaoReveal := []byte{2} + graffiti := []byte{3} + + ctx := t.Context() + + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().GetSSZ( + gomock.Any(), + fmt.Sprintf("/eth/v3/validator/blocks/%d?graffiti=%s&randao_reveal=%s", slot, hexutil.Encode(graffiti), hexutil.Encode(randaoReveal)), + ).Return( + bytes, + http.Header{ + "Content-Type": []string{api.OctetStreamMediaType}, + api.VersionHeader: []string{"altair"}, + api.ExecutionPayloadBlindedHeader: []string{"false"}, + }, + nil, + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + beaconBlock, err := validatorClient.beaconBlock(ctx, slot, randaoReveal, graffiti) + require.NoError(t, err) + + expectedBeaconBlock := ðpb.GenericBeaconBlock{ + Block: ðpb.GenericBeaconBlock_Altair{ + Altair: proto, + }, + IsBlinded: false, + } + + assert.DeepEqual(t, expectedBeaconBlock, beaconBlock) +} + func TestGetBeaconBlock_AltairValid(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -199,18 +836,18 @@ func TestGetBeaconBlock_AltairValid(t *testing.T) { ctx := t.Context() + b, err := json.Marshal(structs.ProduceBlockV3Response{ + Version: "altair", + Data: bytes, + }) + require.NoError(t, err) jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) - jsonRestHandler.EXPECT().Get( + jsonRestHandler.EXPECT().GetSSZ( 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: "altair", - Data: bytes, - }, ).Return( + b, + http.Header{"Content-Type": []string{"application/json"}}, nil, ).Times(1) @@ -242,19 +879,19 @@ func TestGetBeaconBlock_BellatrixValid(t *testing.T) { ctx := t.Context() + b, err := json.Marshal(structs.ProduceBlockV3Response{ + Version: "bellatrix", + ExecutionPayloadBlinded: false, + Data: bytes, + }) + require.NoError(t, err) jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) - jsonRestHandler.EXPECT().Get( + jsonRestHandler.EXPECT().GetSSZ( 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: "bellatrix", - ExecutionPayloadBlinded: false, - Data: bytes, - }, ).Return( + b, + http.Header{"Content-Type": []string{"application/json"}}, nil, ).Times(1) @@ -287,19 +924,19 @@ func TestGetBeaconBlock_BlindedBellatrixValid(t *testing.T) { ctx := t.Context() + b, err := json.Marshal(structs.ProduceBlockV3Response{ + Version: "bellatrix", + ExecutionPayloadBlinded: true, + Data: bytes, + }) + require.NoError(t, err) jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) - jsonRestHandler.EXPECT().Get( + jsonRestHandler.EXPECT().GetSSZ( 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: "bellatrix", - ExecutionPayloadBlinded: true, - Data: bytes, - }, ).Return( + b, + http.Header{"Content-Type": []string{"application/json"}}, nil, ).Times(1) @@ -332,19 +969,19 @@ func TestGetBeaconBlock_CapellaValid(t *testing.T) { ctx := t.Context() + b, err := json.Marshal(structs.ProduceBlockV3Response{ + Version: "capella", + ExecutionPayloadBlinded: false, + Data: bytes, + }) + require.NoError(t, err) jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) - jsonRestHandler.EXPECT().Get( + jsonRestHandler.EXPECT().GetSSZ( 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: "capella", - ExecutionPayloadBlinded: false, - Data: bytes, - }, ).Return( + b, + http.Header{"Content-Type": []string{"application/json"}}, nil, ).Times(1) @@ -377,19 +1014,19 @@ func TestGetBeaconBlock_BlindedCapellaValid(t *testing.T) { ctx := t.Context() + b, err := json.Marshal(structs.ProduceBlockV3Response{ + Version: "capella", + ExecutionPayloadBlinded: true, + Data: bytes, + }) + require.NoError(t, err) jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) - jsonRestHandler.EXPECT().Get( + jsonRestHandler.EXPECT().GetSSZ( 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: "capella", - ExecutionPayloadBlinded: true, - Data: bytes, - }, ).Return( + b, + http.Header{"Content-Type": []string{"application/json"}}, nil, ).Times(1) @@ -422,19 +1059,19 @@ func TestGetBeaconBlock_DenebValid(t *testing.T) { ctx := t.Context() + b, err := json.Marshal(structs.ProduceBlockV3Response{ + Version: "deneb", + ExecutionPayloadBlinded: false, + Data: bytes, + }) + require.NoError(t, err) jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) - jsonRestHandler.EXPECT().Get( + jsonRestHandler.EXPECT().GetSSZ( 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: "deneb", - ExecutionPayloadBlinded: false, - Data: bytes, - }, ).Return( + b, + http.Header{"Content-Type": []string{"application/json"}}, nil, ).Times(1) @@ -467,19 +1104,19 @@ func TestGetBeaconBlock_BlindedDenebValid(t *testing.T) { ctx := t.Context() + b, err := json.Marshal(structs.ProduceBlockV3Response{ + Version: "deneb", + ExecutionPayloadBlinded: true, + Data: bytes, + }) + require.NoError(t, err) jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) - jsonRestHandler.EXPECT().Get( + jsonRestHandler.EXPECT().GetSSZ( 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: "deneb", - ExecutionPayloadBlinded: true, - Data: bytes, - }, ).Return( + b, + http.Header{"Content-Type": []string{"application/json"}}, nil, ).Times(1) @@ -512,19 +1149,19 @@ func TestGetBeaconBlock_ElectraValid(t *testing.T) { ctx := t.Context() + b, err := json.Marshal(structs.ProduceBlockV3Response{ + Version: "electra", + ExecutionPayloadBlinded: false, + Data: bytes, + }) + require.NoError(t, err) jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) - jsonRestHandler.EXPECT().Get( + jsonRestHandler.EXPECT().GetSSZ( 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( + b, + http.Header{"Content-Type": []string{"application/json"}}, nil, ).Times(1) @@ -557,19 +1194,19 @@ func TestGetBeaconBlock_BlindedElectraValid(t *testing.T) { ctx := t.Context() + b, err := json.Marshal(structs.ProduceBlockV3Response{ + Version: "electra", + ExecutionPayloadBlinded: true, + Data: bytes, + }) + require.NoError(t, err) jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) - jsonRestHandler.EXPECT().Get( + jsonRestHandler.EXPECT().GetSSZ( 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( + b, + http.Header{"Content-Type": []string{"application/json"}}, nil, ).Times(1) diff --git a/validator/client/beacon-api/mock/json_rest_handler_mock.go b/validator/client/beacon-api/mock/json_rest_handler_mock.go index c74708dedd..c8b09e8f0c 100644 --- a/validator/client/beacon-api/mock/json_rest_handler_mock.go +++ b/validator/client/beacon-api/mock/json_rest_handler_mock.go @@ -56,6 +56,23 @@ func (mr *MockJsonRestHandlerMockRecorder) Get(ctx, endpoint, resp any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockJsonRestHandler)(nil).Get), ctx, endpoint, resp) } + +// GetSSZ mocks base method. +func (m *MockJsonRestHandler) GetSSZ(ctx context.Context, endpoint string) ([]byte, http.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSSZ", ctx, endpoint) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(http.Header) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetSSZ indicates an expected call of GetSSZ. +func (mr *MockJsonRestHandlerMockRecorder) GetSSZ(ctx, endpoint any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSSZ", reflect.TypeOf((*MockJsonRestHandler)(nil).GetSSZ), ctx, endpoint) +} + // Host mocks base method. func (m *MockJsonRestHandler) Host() string { m.ctrl.T.Helper() diff --git a/validator/client/beacon-api/prysm_beacon_chain_client.go b/validator/client/beacon-api/prysm_beacon_chain_client.go index 761283fda5..5dc9558b1c 100644 --- a/validator/client/beacon-api/prysm_beacon_chain_client.go +++ b/validator/client/beacon-api/prysm_beacon_chain_client.go @@ -18,7 +18,7 @@ import ( ) // NewPrysmChainClient returns implementation of iface.PrysmChainClient. -func NewPrysmChainClient(jsonRestHandler JsonRestHandler, nodeClient iface.NodeClient) iface.PrysmChainClient { +func NewPrysmChainClient(jsonRestHandler RestHandler, nodeClient iface.NodeClient) iface.PrysmChainClient { return prysmChainClient{ jsonRestHandler: jsonRestHandler, nodeClient: nodeClient, @@ -26,7 +26,7 @@ func NewPrysmChainClient(jsonRestHandler JsonRestHandler, nodeClient iface.NodeC } type prysmChainClient struct { - jsonRestHandler JsonRestHandler + jsonRestHandler RestHandler nodeClient iface.NodeClient } diff --git a/validator/client/beacon-api/json_rest_handler.go b/validator/client/beacon-api/rest_handler_client.go similarity index 56% rename from validator/client/beacon-api/json_rest_handler.go rename to validator/client/beacon-api/rest_handler_client.go index e3928268f7..e2dae19ab3 100644 --- a/validator/client/beacon-api/json_rest_handler.go +++ b/validator/client/beacon-api/rest_handler_client.go @@ -4,49 +4,53 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io" "net/http" "strings" "github.com/OffchainLabs/prysm/v6/api" + "github.com/OffchainLabs/prysm/v6/config/features" "github.com/OffchainLabs/prysm/v6/network/httputil" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) -type JsonRestHandler interface { +type RestHandler interface { Get(ctx context.Context, endpoint string, resp interface{}) error + GetSSZ(ctx context.Context, endpoint string) ([]byte, http.Header, error) Post(ctx context.Context, endpoint string, headers map[string]string, data *bytes.Buffer, resp interface{}) error HttpClient() *http.Client Host() string SetHost(host string) } -type BeaconApiJsonRestHandler struct { +type BeaconApiRestHandler struct { client http.Client host string } -// NewBeaconApiJsonRestHandler returns a JsonRestHandler -func NewBeaconApiJsonRestHandler(client http.Client, host string) JsonRestHandler { - return &BeaconApiJsonRestHandler{ +// NewBeaconApiRestHandler returns a RestHandler +func NewBeaconApiRestHandler(client http.Client, host string) RestHandler { + return &BeaconApiRestHandler{ client: client, host: host, } } // HttpClient returns the underlying HTTP client of the handler -func (c *BeaconApiJsonRestHandler) HttpClient() *http.Client { +func (c *BeaconApiRestHandler) HttpClient() *http.Client { return &c.client } // Host returns the underlying HTTP host -func (c *BeaconApiJsonRestHandler) Host() string { +func (c *BeaconApiRestHandler) Host() string { return c.host } // Get sends a GET request and decodes the response body as a JSON object into the passed in object. // If an HTTP error is returned, the body is decoded as a DefaultJsonError JSON object and returned as the first return value. -func (c *BeaconApiJsonRestHandler) Get(ctx context.Context, endpoint string, resp interface{}) error { +func (c *BeaconApiRestHandler) Get(ctx context.Context, endpoint string, resp interface{}) error { url := c.host + endpoint req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { @@ -66,9 +70,61 @@ func (c *BeaconApiJsonRestHandler) Get(ctx context.Context, endpoint string, res return decodeResp(httpResp, resp) } +func (c *BeaconApiRestHandler) GetSSZ(ctx context.Context, endpoint string) ([]byte, http.Header, error) { + url := c.host + endpoint + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to create request for endpoint %s", url) + } + primaryAcceptType := fmt.Sprintf("%s;q=%s", api.OctetStreamMediaType, "0.95") + secondaryAcceptType := fmt.Sprintf("%s;q=%s", api.JsonMediaType, "0.9") + acceptHeaderString := fmt.Sprintf("%s,%s", primaryAcceptType, secondaryAcceptType) + if features.Get().SSZOnly { + acceptHeaderString = api.OctetStreamMediaType + } + req.Header.Set("Accept", acceptHeaderString) + httpResp, err := c.client.Do(req) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to perform request for endpoint %s", url) + } + defer func() { + if err := httpResp.Body.Close(); err != nil { + return + } + }() + contentType := httpResp.Header.Get("Content-Type") + body, err := io.ReadAll(httpResp.Body) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to read response body for %s", httpResp.Request.URL) + } + if !strings.Contains(primaryAcceptType, contentType) { + log.WithFields(logrus.Fields{ + "primaryAcceptType": primaryAcceptType, + "secondaryAcceptType": secondaryAcceptType, + "receivedAcceptType": contentType, + }).Debug("Server responded with non primary accept type") + } + + // non-2XX codes are a failure + if !strings.HasPrefix(httpResp.Status, "2") { + decoder := json.NewDecoder(bytes.NewBuffer(body)) + errorJson := &httputil.DefaultJsonError{} + if err = decoder.Decode(errorJson); err != nil { + return nil, nil, errors.Wrapf(err, "failed to decode response body into error json for %s", httpResp.Request.URL) + } + return nil, nil, errorJson + } + + if features.Get().SSZOnly && contentType != api.OctetStreamMediaType { + return nil, nil, errors.Errorf("server responded with non primary accept type %s", contentType) + } + + return body, httpResp.Header, nil +} + // Post sends a POST request and decodes the response body as a JSON object into the passed in object. // If an HTTP error is returned, the body is decoded as a DefaultJsonError JSON object and returned as the first return value. -func (c *BeaconApiJsonRestHandler) Post( +func (c *BeaconApiRestHandler) Post( ctx context.Context, apiEndpoint string, headers map[string]string, @@ -136,6 +192,6 @@ func decodeResp(httpResp *http.Response, resp interface{}) error { return nil } -func (c *BeaconApiJsonRestHandler) SetHost(host string) { +func (c *BeaconApiRestHandler) SetHost(host string) { c.host = host } diff --git a/validator/client/beacon-api/json_rest_handler_test.go b/validator/client/beacon-api/rest_handler_client_test.go similarity index 67% rename from validator/client/beacon-api/json_rest_handler_test.go rename to validator/client/beacon-api/rest_handler_client_test.go index 1a8666d028..6928e1f1e7 100644 --- a/validator/client/beacon-api/json_rest_handler_test.go +++ b/validator/client/beacon-api/rest_handler_client_test.go @@ -2,6 +2,7 @@ package beacon_api import ( "bytes" + "context" "encoding/json" "io" "net/http" @@ -15,6 +16,8 @@ import ( "github.com/OffchainLabs/prysm/v6/testing/assert" "github.com/OffchainLabs/prysm/v6/testing/require" "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" ) func TestGet(t *testing.T) { @@ -39,7 +42,7 @@ func TestGet(t *testing.T) { server := httptest.NewServer(mux) defer server.Close() - jsonRestHandler := BeaconApiJsonRestHandler{ + jsonRestHandler := BeaconApiRestHandler{ client: http.Client{Timeout: time.Second * 5}, host: server.URL, } @@ -48,6 +51,98 @@ func TestGet(t *testing.T) { assert.DeepEqual(t, genesisJson, resp) } +func TestGetSSZ(t *testing.T) { + ctx := context.Background() + const endpoint = "/example/rest/api/ssz" + genesisJson := &structs.GetGenesisResponse{ + Data: &structs.Genesis{ + GenesisTime: "123", + GenesisValidatorsRoot: "0x456", + GenesisForkVersion: "0x789", + }, + } + + t.Run("Successful SSZ response", func(t *testing.T) { + expectedBody := []byte{10, 20, 30, 40} + + mux := http.NewServeMux() + mux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) { + assert.StringContains(t, api.OctetStreamMediaType, r.Header.Get("Accept")) + w.Header().Set("Content-Type", api.OctetStreamMediaType) + _, err := w.Write(expectedBody) + require.NoError(t, err) + }) + server := httptest.NewServer(mux) + defer server.Close() + + jsonRestHandler := BeaconApiRestHandler{ + client: http.Client{Timeout: time.Second * 5}, + host: server.URL, + } + + body, header, err := jsonRestHandler.GetSSZ(ctx, endpoint) + require.NoError(t, err) + assert.DeepEqual(t, expectedBody, body) + require.StringContains(t, api.OctetStreamMediaType, header.Get("Content-Type")) + }) + + t.Run("Json Content-Type response", func(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + defer logrus.SetLevel(logrus.InfoLevel) // reset it afterwards + logHook := test.NewGlobal() + mux := http.NewServeMux() + mux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) { + assert.StringContains(t, api.OctetStreamMediaType, r.Header.Get("Accept")) + w.Header().Set("Content-Type", api.JsonMediaType) + + marshalledJson, err := json.Marshal(genesisJson) + require.NoError(t, err) + + _, err = w.Write(marshalledJson) + require.NoError(t, err) + }) + server := httptest.NewServer(mux) + defer server.Close() + + jsonRestHandler := BeaconApiRestHandler{ + client: http.Client{Timeout: time.Second * 5}, + host: server.URL, + } + + body, header, err := jsonRestHandler.GetSSZ(ctx, endpoint) + require.NoError(t, err) + assert.LogsContain(t, logHook, "Server responded with non primary accept type") + require.Equal(t, api.JsonMediaType, header.Get("Content-Type")) + resp := &structs.GetGenesisResponse{} + require.NoError(t, json.Unmarshal(body, resp)) + require.Equal(t, "123", resp.Data.GenesisTime) + }) + + t.Run("Wrong Content-Type response, doesn't error out and instead handled downstream", func(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + defer logrus.SetLevel(logrus.InfoLevel) // reset it afterwards + logHook := test.NewGlobal() + mux := http.NewServeMux() + mux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) { + assert.StringContains(t, api.OctetStreamMediaType, r.Header.Get("Accept")) + w.Header().Set("Content-Type", "text/plain") // Invalid content type + _, err := w.Write([]byte("some text")) + require.NoError(t, err) + }) + server := httptest.NewServer(mux) + defer server.Close() + + jsonRestHandler := BeaconApiRestHandler{ + client: http.Client{Timeout: time.Second * 5}, + host: server.URL, + } + + _, _, err := jsonRestHandler.GetSSZ(ctx, endpoint) + require.NoError(t, err) + assert.LogsContain(t, logHook, "Server responded with non primary accept type") + }) +} + func TestPost(t *testing.T) { ctx := t.Context() const endpoint = "/example/rest/api/endpoint" @@ -85,7 +180,7 @@ func TestPost(t *testing.T) { server := httptest.NewServer(mux) defer server.Close() - jsonRestHandler := BeaconApiJsonRestHandler{ + jsonRestHandler := BeaconApiRestHandler{ client: http.Client{Timeout: time.Second * 5}, host: server.URL, } diff --git a/validator/client/beacon-api/state_validators.go b/validator/client/beacon-api/state_validators.go index 2c8c2fb0de..a7f3b07c88 100644 --- a/validator/client/beacon-api/state_validators.go +++ b/validator/client/beacon-api/state_validators.go @@ -21,7 +21,7 @@ type StateValidatorsProvider interface { } type beaconApiStateValidatorsProvider struct { - jsonRestHandler JsonRestHandler + jsonRestHandler RestHandler } func (c beaconApiStateValidatorsProvider) StateValidators( diff --git a/validator/client/beacon-chain-client-factory/beacon_chain_client_factory.go b/validator/client/beacon-chain-client-factory/beacon_chain_client_factory.go index 61569ee004..cac3c2c9c8 100644 --- a/validator/client/beacon-chain-client-factory/beacon_chain_client_factory.go +++ b/validator/client/beacon-chain-client-factory/beacon_chain_client_factory.go @@ -9,7 +9,7 @@ import ( validatorHelpers "github.com/OffchainLabs/prysm/v6/validator/helpers" ) -func NewChainClient(validatorConn validatorHelpers.NodeConnection, jsonRestHandler beaconApi.JsonRestHandler) iface.ChainClient { +func NewChainClient(validatorConn validatorHelpers.NodeConnection, jsonRestHandler beaconApi.RestHandler) iface.ChainClient { grpcClient := grpcApi.NewGrpcChainClient(validatorConn.GetGrpcClientConn()) if features.Get().EnableBeaconRESTApi { return beaconApi.NewBeaconApiChainClientWithFallback(jsonRestHandler, grpcClient) @@ -18,7 +18,7 @@ func NewChainClient(validatorConn validatorHelpers.NodeConnection, jsonRestHandl } } -func NewPrysmChainClient(validatorConn validatorHelpers.NodeConnection, jsonRestHandler beaconApi.JsonRestHandler) iface.PrysmChainClient { +func NewPrysmChainClient(validatorConn validatorHelpers.NodeConnection, jsonRestHandler beaconApi.RestHandler) iface.PrysmChainClient { if features.Get().EnableBeaconRESTApi { return beaconApi.NewPrysmChainClient(jsonRestHandler, nodeClientFactory.NewNodeClient(validatorConn, jsonRestHandler)) } else { diff --git a/validator/client/node-client-factory/node_client_factory.go b/validator/client/node-client-factory/node_client_factory.go index 2b849c8575..b933cd9116 100644 --- a/validator/client/node-client-factory/node_client_factory.go +++ b/validator/client/node-client-factory/node_client_factory.go @@ -8,7 +8,7 @@ import ( validatorHelpers "github.com/OffchainLabs/prysm/v6/validator/helpers" ) -func NewNodeClient(validatorConn validatorHelpers.NodeConnection, jsonRestHandler beaconApi.JsonRestHandler) iface.NodeClient { +func NewNodeClient(validatorConn validatorHelpers.NodeConnection, jsonRestHandler beaconApi.RestHandler) iface.NodeClient { grpcClient := grpcApi.NewNodeClient(validatorConn.GetGrpcClientConn()) if features.Get().EnableBeaconRESTApi { return beaconApi.NewNodeClientWithFallback(jsonRestHandler, grpcClient) diff --git a/validator/client/service.go b/validator/client/service.go index 40ead364fa..ef2ad1acf3 100644 --- a/validator/client/service.go +++ b/validator/client/service.go @@ -179,7 +179,7 @@ func (v *ValidatorService) Start() { return } - restHandler := beaconApi.NewBeaconApiJsonRestHandler( + restHandler := beaconApi.NewBeaconApiRestHandler( http.Client{Timeout: v.conn.GetBeaconApiTimeout(), Transport: otelhttp.NewTransport(http.DefaultTransport)}, hosts[0], ) diff --git a/validator/client/validator-client-factory/validator_client_factory.go b/validator/client/validator-client-factory/validator_client_factory.go index fe560c3695..57a7174eb8 100644 --- a/validator/client/validator-client-factory/validator_client_factory.go +++ b/validator/client/validator-client-factory/validator_client_factory.go @@ -10,7 +10,7 @@ import ( func NewValidatorClient( validatorConn validatorHelpers.NodeConnection, - jsonRestHandler beaconApi.JsonRestHandler, + jsonRestHandler beaconApi.RestHandler, opt ...beaconApi.ValidatorClientOpt, ) iface.ValidatorClient { if features.Get().EnableBeaconRESTApi { diff --git a/validator/rpc/beacon.go b/validator/rpc/beacon.go index ca8d5008ad..596eef0ae9 100644 --- a/validator/rpc/beacon.go +++ b/validator/rpc/beacon.go @@ -55,7 +55,7 @@ func (s *Server) registerBeaconClient() error { s.beaconApiTimeout, ) - restHandler := beaconApi.NewBeaconApiJsonRestHandler( + restHandler := beaconApi.NewBeaconApiRestHandler( http.Client{Timeout: s.beaconApiTimeout, Transport: otelhttp.NewTransport(http.DefaultTransport)}, s.beaconApiEndpoint, )