From aa3847f3ec535bb2fe1f0c1d595f1afa92667d1f Mon Sep 17 00:00:00 2001 From: james-prysm Date: Fri, 22 May 2026 14:42:40 -0500 Subject: [PATCH 01/15] wip --- .../structs/conversions_block_gloas_test.go | 90 ++++++++++++++ api/server/structs/conversions_gloas.go | 57 +++++++++ api/server/structs/other.go | 13 ++ beacon-chain/rpc/endpoints.go | 11 ++ beacon-chain/rpc/endpoints_test.go | 1 + beacon-chain/rpc/eth/validator/BUILD.bazel | 4 + beacon-chain/rpc/eth/validator/handlers.go | 52 ++++++++ .../handlers_proposer_preferences_test.go | 116 ++++++++++++++++++ validator/client/beacon-api/BUILD.bazel | 2 + .../beacon-api/beacon_api_validator_client.go | 12 +- .../client/beacon-api/proposer_preferences.go | 39 ++++++ .../beacon-api/proposer_preferences_test.go | 89 ++++++++++++++ 12 files changed, 481 insertions(+), 5 deletions(-) create mode 100644 beacon-chain/rpc/eth/validator/handlers_proposer_preferences_test.go create mode 100644 validator/client/beacon-api/proposer_preferences.go create mode 100644 validator/client/beacon-api/proposer_preferences_test.go diff --git a/api/server/structs/conversions_block_gloas_test.go b/api/server/structs/conversions_block_gloas_test.go index d3c6d024dee2..1788c674aaa1 100644 --- a/api/server/structs/conversions_block_gloas_test.go +++ b/api/server/structs/conversions_block_gloas_test.go @@ -71,3 +71,93 @@ func TestBlockContentsGloasFromConsensus(t *testing.T) { require.Equal(t, 1, len(result.Blobs)) require.Equal(t, hexutil.Encode(blobs[0]), result.Blobs[0]) } + +func validProposerPreferences() *ProposerPreferences { + return &ProposerPreferences{ + DependentRoot: hexutil.Encode(bytes.Repeat([]byte{0xcc}, fieldparams.RootLength)), + ProposalSlot: "32", + ValidatorIndex: "2", + FeeRecipient: hexutil.Encode(bytes.Repeat([]byte{0xab}, 20)), + TargetGasLimit: "30000000", + } +} + +func TestSignedProposerPreferences_ToConsensus_NilMessage(t *testing.T) { + s := &SignedProposerPreferences{Message: nil, Signature: ""} + _, err := s.ToConsensus() + require.ErrorContains(t, errNilValue.Error(), err) +} + +func TestSignedProposerPreferences_ToConsensus_NilReceiver(t *testing.T) { + var s *SignedProposerPreferences + _, err := s.ToConsensus() + require.ErrorContains(t, errNilValue.Error(), err) +} + +func TestSignedProposerPreferences_ToConsensus_BadSignature(t *testing.T) { + s := &SignedProposerPreferences{Message: validProposerPreferences(), Signature: "0xnothex"} + _, err := s.ToConsensus() + require.ErrorContains(t, "Signature", err) +} + +func TestSignedProposerPreferences_ToConsensus_OK(t *testing.T) { + sig := hexutil.Encode(bytes.Repeat([]byte{0x01}, fieldparams.BLSSignatureLength)) + s := &SignedProposerPreferences{Message: validProposerPreferences(), Signature: sig} + out, err := s.ToConsensus() + require.NoError(t, err) + require.Equal(t, uint64(30_000_000), out.Message.TargetGasLimit) + require.Equal(t, uint64(32), uint64(out.Message.ProposalSlot)) + require.Equal(t, uint64(2), uint64(out.Message.ValidatorIndex)) + require.Equal(t, fieldparams.BLSSignatureLength, len(out.Signature)) + require.Equal(t, 20, len(out.Message.FeeRecipient)) + require.Equal(t, fieldparams.RootLength, len(out.Message.DependentRoot)) +} + +func TestProposerPreferences_ToConsensus_BadDependentRootHex(t *testing.T) { + p := validProposerPreferences() + p.DependentRoot = "0xnothex" + _, err := p.ToConsensus() + require.ErrorContains(t, "DependentRoot", err) +} + +func TestProposerPreferences_ToConsensus_ShortDependentRoot(t *testing.T) { + p := validProposerPreferences() + p.DependentRoot = "0xcc" + _, err := p.ToConsensus() + require.ErrorContains(t, "DependentRoot", err) +} + +func TestProposerPreferences_ToConsensus_BadProposalSlot(t *testing.T) { + p := validProposerPreferences() + p.ProposalSlot = "nope" + _, err := p.ToConsensus() + require.ErrorContains(t, "ProposalSlot", err) +} + +func TestProposerPreferences_ToConsensus_BadValidatorIndex(t *testing.T) { + p := validProposerPreferences() + p.ValidatorIndex = "nope" + _, err := p.ToConsensus() + require.ErrorContains(t, "ValidatorIndex", err) +} + +func TestProposerPreferences_ToConsensus_BadFeeRecipientHex(t *testing.T) { + p := validProposerPreferences() + p.FeeRecipient = "0xnothex" + _, err := p.ToConsensus() + require.ErrorContains(t, "FeeRecipient", err) +} + +func TestProposerPreferences_ToConsensus_ShortFeeRecipient(t *testing.T) { + p := validProposerPreferences() + p.FeeRecipient = "0xab" + _, err := p.ToConsensus() + require.ErrorContains(t, "FeeRecipient", err) +} + +func TestProposerPreferences_ToConsensus_BadTargetGasLimit(t *testing.T) { + p := validProposerPreferences() + p.TargetGasLimit = "nope" + _, err := p.ToConsensus() + require.ErrorContains(t, "TargetGasLimit", err) +} diff --git a/api/server/structs/conversions_gloas.go b/api/server/structs/conversions_gloas.go index 43d506b66963..2a24343c9340 100644 --- a/api/server/structs/conversions_gloas.go +++ b/api/server/structs/conversions_gloas.go @@ -2,9 +2,15 @@ package structs import ( "fmt" + "strconv" + "github.com/OffchainLabs/prysm/v7/api/server" + fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/consensus-types/interfaces" + "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + "github.com/OffchainLabs/prysm/v7/encoding/bytesutil" ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ) @@ -108,3 +114,54 @@ func PTCsFromConsensus(p *ethpb.PTCs) *PTCs { } return &PTCs{ValidatorIndices: indices} } + +func (s *SignedProposerPreferences) ToConsensus() (*ethpb.SignedProposerPreferences, error) { + if s == nil { + return nil, server.NewDecodeError(errNilValue, "SignedProposerPreferences") + } + if s.Message == nil { + return nil, server.NewDecodeError(errNilValue, "Message") + } + msg, err := s.Message.ToConsensus() + if err != nil { + return nil, server.NewDecodeError(err, "Message") + } + sig, err := bytesutil.DecodeHexWithLength(s.Signature, fieldparams.BLSSignatureLength) + if err != nil { + return nil, server.NewDecodeError(err, "Signature") + } + return ðpb.SignedProposerPreferences{ + Message: msg, + Signature: sig, + }, nil +} + +func (p *ProposerPreferences) ToConsensus() (*ethpb.ProposerPreferences, error) { + dependentRoot, err := bytesutil.DecodeHexWithLength(p.DependentRoot, fieldparams.RootLength) + if err != nil { + return nil, server.NewDecodeError(err, "DependentRoot") + } + slot, err := strconv.ParseUint(p.ProposalSlot, 10, 64) + if err != nil { + return nil, server.NewDecodeError(err, "ProposalSlot") + } + valIdx, err := strconv.ParseUint(p.ValidatorIndex, 10, 64) + if err != nil { + return nil, server.NewDecodeError(err, "ValidatorIndex") + } + feeRecipient, err := bytesutil.DecodeHexWithLength(p.FeeRecipient, common.AddressLength) + if err != nil { + return nil, server.NewDecodeError(err, "FeeRecipient") + } + gasLimit, err := strconv.ParseUint(p.TargetGasLimit, 10, 64) + if err != nil { + return nil, server.NewDecodeError(err, "TargetGasLimit") + } + return ðpb.ProposerPreferences{ + DependentRoot: dependentRoot, + ProposalSlot: primitives.Slot(slot), + ValidatorIndex: primitives.ValidatorIndex(valIdx), + FeeRecipient: feeRecipient, + TargetGasLimit: gasLimit, + }, nil +} diff --git a/api/server/structs/other.go b/api/server/structs/other.go index b56456331ef1..1e047d457022 100644 --- a/api/server/structs/other.go +++ b/api/server/structs/other.go @@ -286,3 +286,16 @@ type BuilderPendingWithdrawal struct { type PTCs struct { ValidatorIndices []string `json:"validator_indices"` } + +type ProposerPreferences struct { + DependentRoot string `json:"dependent_root"` + ProposalSlot string `json:"proposal_slot"` + ValidatorIndex string `json:"validator_index"` + FeeRecipient string `json:"fee_recipient"` + TargetGasLimit string `json:"target_gas_limit"` +} + +type SignedProposerPreferences struct { + Message *ProposerPreferences `json:"message"` + Signature string `json:"signature"` +} diff --git a/beacon-chain/rpc/endpoints.go b/beacon-chain/rpc/endpoints.go index 3f26d0a7f5da..4e106a5a47f7 100644 --- a/beacon-chain/rpc/endpoints.go +++ b/beacon-chain/rpc/endpoints.go @@ -373,6 +373,17 @@ func (s *Service) validatorEndpoints( handler: server.PrepareBeaconProposer, methods: []string{http.MethodPost}, }, + { + template: "/eth/v1/validator/proposer_preferences", + name: namespace + ".SubmitSignedProposerPreferences", + middleware: []middleware.Middleware{ + middleware.ContentTypeHandler([]string{api.JsonMediaType}), + middleware.AcceptHeaderHandler([]string{api.JsonMediaType}), + middleware.AcceptEncodingHeaderHandler(), + }, + handler: server.SubmitSignedProposerPreferences, + methods: []string{http.MethodPost}, + }, { template: "/eth/v1/validator/liveness/{epoch}", name: namespace + ".GetLiveness", diff --git a/beacon-chain/rpc/endpoints_test.go b/beacon-chain/rpc/endpoints_test.go index fa73868db5c9..d5a297deaecb 100644 --- a/beacon-chain/rpc/endpoints_test.go +++ b/beacon-chain/rpc/endpoints_test.go @@ -113,6 +113,7 @@ func Test_endpoints(t *testing.T) { "/eth/v1/validator/sync_committee_contribution": {http.MethodGet}, "/eth/v1/validator/contribution_and_proofs": {http.MethodPost}, "/eth/v1/validator/prepare_beacon_proposer": {http.MethodPost}, + "/eth/v1/validator/proposer_preferences": {http.MethodPost}, "/eth/v1/validator/register_validator": {http.MethodPost}, "/eth/v1/validator/liveness/{epoch}": {http.MethodPost}, } diff --git a/beacon-chain/rpc/eth/validator/BUILD.bazel b/beacon-chain/rpc/eth/validator/BUILD.bazel index 21cbab394571..450062555a29 100644 --- a/beacon-chain/rpc/eth/validator/BUILD.bazel +++ b/beacon-chain/rpc/eth/validator/BUILD.bazel @@ -61,6 +61,7 @@ go_test( srcs = [ "handlers_block_gloas_test.go", "handlers_block_test.go", + "handlers_proposer_preferences_test.go", "handlers_test.go", ], embed = [":go_default_library"], @@ -107,6 +108,9 @@ go_test( "@com_github_prysmaticlabs_go_bitfield//:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@com_github_sirupsen_logrus//hooks/test:go_default_library", + "@org_golang_google_grpc//codes:go_default_library", + "@org_golang_google_grpc//status:go_default_library", + "@org_golang_google_protobuf//types/known/emptypb:go_default_library", "@org_uber_go_mock//gomock:go_default_library", ], ) diff --git a/beacon-chain/rpc/eth/validator/handlers.go b/beacon-chain/rpc/eth/validator/handlers.go index 3b0efd52eafb..47785c8ba0cc 100644 --- a/beacon-chain/rpc/eth/validator/handlers.go +++ b/beacon-chain/rpc/eth/validator/handlers.go @@ -37,6 +37,8 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) // GetAggregateAttestationV2 aggregates all attestations matching the given attestation data root and slot, returning the aggregated result. @@ -213,6 +215,56 @@ func matchingAtts(atts []ethpbalpha.Att, slot primitives.Slot, attDataRoot []byt return result, nil } +// SubmitSignedProposerPreferences broadcasts signed proposer preferences and +// caches them for subsequent bid validation. Delegates to the gRPC server so +// validation and broadcast logic remain in one place. +func (s *Server) SubmitSignedProposerPreferences(w http.ResponseWriter, r *http.Request) { + ctx, span := trace.StartSpan(r.Context(), "validator.SubmitSignedProposerPreferences") + defer span.End() + + var data []*structs.SignedProposerPreferences + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + if errors.Is(err, io.EOF) { + httputil.HandleError(w, "No data submitted", http.StatusBadRequest) + } else { + httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest) + } + return + } + if len(data) == 0 { + httputil.HandleError(w, "No data submitted", http.StatusBadRequest) + return + } + + req := ðpbalpha.SubmitSignedProposerPreferencesRequest{ + SignedProposerPreferences: make([]*ethpbalpha.SignedProposerPreferences, len(data)), + } + for i, item := range data { + consensusItem, err := item.ToConsensus() + if err != nil { + httputil.HandleError(w, fmt.Sprintf("Could not convert signed proposer preferences at index %d: %s", i, err.Error()), http.StatusBadRequest) + return + } + req.SignedProposerPreferences[i] = consensusItem + } + + if _, err := s.V1Alpha1Server.SubmitSignedProposerPreferences(ctx, req); err != nil { + if st, ok := status.FromError(err); ok { + switch st.Code() { + case codes.InvalidArgument: + httputil.HandleError(w, st.Message(), http.StatusBadRequest) + case codes.Unavailable: + httputil.HandleError(w, st.Message(), http.StatusServiceUnavailable) + default: + httputil.HandleError(w, st.Message(), http.StatusInternalServerError) + } + return + } + httputil.HandleError(w, err.Error(), http.StatusInternalServerError) + return + } +} + // SubmitContributionAndProofs publishes multiple signed sync committee contribution and proofs. func (s *Server) SubmitContributionAndProofs(w http.ResponseWriter, r *http.Request) { ctx, span := trace.StartSpan(r.Context(), "validator.SubmitContributionAndProofs") diff --git a/beacon-chain/rpc/eth/validator/handlers_proposer_preferences_test.go b/beacon-chain/rpc/eth/validator/handlers_proposer_preferences_test.go new file mode 100644 index 000000000000..d312b4de94bb --- /dev/null +++ b/beacon-chain/rpc/eth/validator/handlers_proposer_preferences_test.go @@ -0,0 +1,116 @@ +package validator + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/OffchainLabs/prysm/v7/network/httputil" + eth "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v7/testing/assert" + mock2 "github.com/OffchainLabs/prysm/v7/testing/mock" + "github.com/OffchainLabs/prysm/v7/testing/require" + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" +) + +func validProposerPreferencesBody() string { + return `[{ + "message": { + "dependent_root": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "proposal_slot": "32", + "validator_index": "2", + "fee_recipient": "0x0000000000000000000000000000000000000000", + "target_gas_limit": "30000000" + }, + "signature": "0x` + strings.Repeat("00", 96) + `" + }]` +} + +func TestSubmitSignedProposerPreferences_OK(t *testing.T) { + ctrl := gomock.NewController(t) + v1alpha1Server := mock2.NewMockBeaconNodeValidatorServer(ctrl) + v1alpha1Server.EXPECT(). + SubmitSignedProposerPreferences(gomock.Any(), gomock.AssignableToTypeOf(ð.SubmitSignedProposerPreferencesRequest{})). + DoAndReturn(func(_ context.Context, req *eth.SubmitSignedProposerPreferencesRequest) (*emptypb.Empty, error) { + require.Equal(t, 1, len(req.SignedProposerPreferences)) + require.Equal(t, uint64(30_000_000), req.SignedProposerPreferences[0].Message.TargetGasLimit) + return &emptypb.Empty{}, nil + }) + + s := &Server{V1Alpha1Server: v1alpha1Server} + req := httptest.NewRequest(http.MethodPost, "http://example.com/eth/v1/validator/proposer_preferences", bytes.NewBufferString(validProposerPreferencesBody())) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.SubmitSignedProposerPreferences(w, req) + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestSubmitSignedProposerPreferences_NoBody(t *testing.T) { + s := &Server{} + req := httptest.NewRequest(http.MethodPost, "http://example.com", nil) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.SubmitSignedProposerPreferences(w, req) + assert.Equal(t, http.StatusBadRequest, w.Code) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), e)) + assert.Equal(t, true, strings.Contains(e.Message, "No data submitted")) +} + +func TestSubmitSignedProposerPreferences_Empty(t *testing.T) { + s := &Server{} + req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString("[]")) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.SubmitSignedProposerPreferences(w, req) + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestSubmitSignedProposerPreferences_InvalidJSON(t *testing.T) { + s := &Server{} + req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString(`[{"message": null, "signature": "0x00"}]`)) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.SubmitSignedProposerPreferences(w, req) + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func runWithGRPCError(t *testing.T, code codes.Code) int { + t.Helper() + ctrl := gomock.NewController(t) + v1alpha1Server := mock2.NewMockBeaconNodeValidatorServer(ctrl) + v1alpha1Server.EXPECT(). + SubmitSignedProposerPreferences(gomock.Any(), gomock.Any()). + Return(nil, status.Error(code, "boom")) + + s := &Server{V1Alpha1Server: v1alpha1Server} + req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString(validProposerPreferencesBody())) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.SubmitSignedProposerPreferences(w, req) + return w.Code +} + +func TestSubmitSignedProposerPreferences_InvalidArgumentMapsTo400(t *testing.T) { + assert.Equal(t, http.StatusBadRequest, runWithGRPCError(t, codes.InvalidArgument)) +} + +func TestSubmitSignedProposerPreferences_UnavailableMapsTo503(t *testing.T) { + assert.Equal(t, http.StatusServiceUnavailable, runWithGRPCError(t, codes.Unavailable)) +} + +func TestSubmitSignedProposerPreferences_InternalMapsTo500(t *testing.T) { + assert.Equal(t, http.StatusInternalServerError, runWithGRPCError(t, codes.Internal)) +} diff --git a/validator/client/beacon-api/BUILD.bazel b/validator/client/beacon-api/BUILD.bazel index c887e5138773..36b133535f50 100644 --- a/validator/client/beacon-api/BUILD.bazel +++ b/validator/client/beacon-api/BUILD.bazel @@ -26,6 +26,7 @@ go_library( "propose_attestation.go", "propose_beacon_block.go", "propose_exit.go", + "proposer_preferences.go", "prysm_beacon_chain_client.go", "registration.go", "state_validators.go", @@ -97,6 +98,7 @@ go_test( "index_test.go", "prepare_beacon_proposer_test.go", "propose_attestation_test.go", + "proposer_preferences_test.go", "propose_beacon_block_test.go", "propose_exit_test.go", "prysm_beacon_chain_client_test.go", diff --git a/validator/client/beacon-api/beacon_api_validator_client.go b/validator/client/beacon-api/beacon_api_validator_client.go index da702ad543c0..80f7224e61e6 100644 --- a/validator/client/beacon-api/beacon_api_validator_client.go +++ b/validator/client/beacon-api/beacon_api_validator_client.go @@ -270,11 +270,13 @@ func (c *beaconApiValidatorClient) SubmitValidatorRegistrations(ctx context.Cont }) } -// TODO(gloas): Wire up actual REST call to POST /eth/v1/validator/proposer_preferences -// once the beacon API endpoint is available (lodekeeper/beacon-APIs#1). -func (c *beaconApiValidatorClient) SubmitSignedProposerPreferences(_ context.Context, in *ethpb.SubmitSignedProposerPreferencesRequest) (*empty.Empty, error) { - log.WithField("count", len(in.GetSignedProposerPreferences())).Debug("SubmitSignedProposerPreferences not yet implemented, skipping") - return new(empty.Empty), nil +func (c *beaconApiValidatorClient) SubmitSignedProposerPreferences(ctx context.Context, in *ethpb.SubmitSignedProposerPreferencesRequest) (*empty.Empty, error) { + ctx, span := trace.StartSpan(ctx, "beacon-api.SubmitSignedProposerPreferences") + defer span.End() + + return wrapInMetrics[*empty.Empty]("SubmitSignedProposerPreferences", func() (*empty.Empty, error) { + return new(empty.Empty), c.submitSignedProposerPreferences(ctx, in.GetSignedProposerPreferences()) + }) } // TODO(gloas): Wire up actual REST call to POST /eth/v2/beacon/execution_payload/bid diff --git a/validator/client/beacon-api/proposer_preferences.go b/validator/client/beacon-api/proposer_preferences.go new file mode 100644 index 000000000000..9080315293b9 --- /dev/null +++ b/validator/client/beacon-api/proposer_preferences.go @@ -0,0 +1,39 @@ +package beacon_api + +import ( + "bytes" + "context" + "encoding/json" + "strconv" + + "github.com/OffchainLabs/prysm/v7/api/server/structs" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/pkg/errors" +) + +func (c *beaconApiValidatorClient) submitSignedProposerPreferences(ctx context.Context, prefs []*ethpb.SignedProposerPreferences) error { + jsonPrefs := make([]*structs.SignedProposerPreferences, len(prefs)) + for i, p := range prefs { + if p == nil || p.Message == nil { + return errors.Errorf("signed proposer preferences at index %d is nil", i) + } + jsonPrefs[i] = &structs.SignedProposerPreferences{ + Message: &structs.ProposerPreferences{ + DependentRoot: hexutil.Encode(p.Message.DependentRoot), + ProposalSlot: strconv.FormatUint(uint64(p.Message.ProposalSlot), 10), + ValidatorIndex: strconv.FormatUint(uint64(p.Message.ValidatorIndex), 10), + FeeRecipient: hexutil.Encode(p.Message.FeeRecipient), + TargetGasLimit: strconv.FormatUint(p.Message.TargetGasLimit, 10), + }, + Signature: hexutil.Encode(p.Signature), + } + } + + body, err := json.Marshal(jsonPrefs) + if err != nil { + return errors.Wrap(err, "failed to marshal signed proposer preferences") + } + + return c.handler.Post(ctx, "/eth/v1/validator/proposer_preferences", nil, bytes.NewBuffer(body), nil) +} diff --git a/validator/client/beacon-api/proposer_preferences_test.go b/validator/client/beacon-api/proposer_preferences_test.go new file mode 100644 index 000000000000..5ffdc2f7e642 --- /dev/null +++ b/validator/client/beacon-api/proposer_preferences_test.go @@ -0,0 +1,89 @@ +package beacon_api + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/OffchainLabs/prysm/v7/api/server/structs" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v7/testing/assert" + "github.com/OffchainLabs/prysm/v7/testing/require" + "github.com/OffchainLabs/prysm/v7/validator/client/beacon-api/mock" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/pkg/errors" + "go.uber.org/mock/gomock" +) + +const proposerPreferencesEndpoint = "/eth/v1/validator/proposer_preferences" + +func TestSubmitSignedProposerPreferences_Valid(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + dependentRoot := bytes.Repeat([]byte{0xcc}, 32) + feeRecipient := bytes.Repeat([]byte{0xab}, 20) + signature := bytes.Repeat([]byte{0x01}, 96) + + expected := []*structs.SignedProposerPreferences{{ + Message: &structs.ProposerPreferences{ + DependentRoot: hexutil.Encode(dependentRoot), + ProposalSlot: "32", + ValidatorIndex: "2", + FeeRecipient: hexutil.Encode(feeRecipient), + TargetGasLimit: "30000000", + }, + Signature: hexutil.Encode(signature), + }} + body, err := json.Marshal(expected) + require.NoError(t, err) + + handler := mock.NewMockJsonRestHandler(ctrl) + handler.EXPECT().Post( + gomock.Any(), + proposerPreferencesEndpoint, + nil, + bytes.NewBuffer(body), + nil, + ).Return(nil).Times(1) + + client := &beaconApiValidatorClient{handler: handler} + err = client.submitSignedProposerPreferences(t.Context(), []*ethpb.SignedProposerPreferences{{ + Message: ðpb.ProposerPreferences{ + DependentRoot: dependentRoot, + ProposalSlot: 32, + ValidatorIndex: 2, + FeeRecipient: feeRecipient, + TargetGasLimit: 30_000_000, + }, + Signature: signature, + }}) + require.NoError(t, err) +} + +func TestSubmitSignedProposerPreferences_HandlerError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + handler := mock.NewMockJsonRestHandler(ctrl) + handler.EXPECT().Post( + gomock.Any(), + proposerPreferencesEndpoint, + nil, + gomock.Any(), + nil, + ).Return(errors.New("foo error")).Times(1) + + client := &beaconApiValidatorClient{handler: handler} + err := client.submitSignedProposerPreferences(t.Context(), []*ethpb.SignedProposerPreferences{{ + Message: ðpb.ProposerPreferences{DependentRoot: bytes.Repeat([]byte{0xcc}, 32), FeeRecipient: bytes.Repeat([]byte{0xab}, 20)}, + Signature: bytes.Repeat([]byte{0x01}, 96), + }}) + assert.ErrorContains(t, "foo error", err) +} + +func TestSubmitSignedProposerPreferences_NilEntry(t *testing.T) { + client := &beaconApiValidatorClient{} + err := client.submitSignedProposerPreferences(t.Context(), []*ethpb.SignedProposerPreferences{nil}) + assert.ErrorContains(t, "is nil", err) +} From 62663737e5ea88337bdcff30f76d1ac7746df47e Mon Sep 17 00:00:00 2001 From: james-prysm Date: Fri, 22 May 2026 16:08:42 -0500 Subject: [PATCH 02/15] gaz --- validator/client/beacon-api/BUILD.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator/client/beacon-api/BUILD.bazel b/validator/client/beacon-api/BUILD.bazel index 36b133535f50..a16841acf510 100644 --- a/validator/client/beacon-api/BUILD.bazel +++ b/validator/client/beacon-api/BUILD.bazel @@ -98,9 +98,9 @@ go_test( "index_test.go", "prepare_beacon_proposer_test.go", "propose_attestation_test.go", - "proposer_preferences_test.go", "propose_beacon_block_test.go", "propose_exit_test.go", + "proposer_preferences_test.go", "prysm_beacon_chain_client_test.go", "registration_test.go", "rest_handler_client_test.go", From 20e9d3791954f9fbee507730dc738792f42431ff Mon Sep 17 00:00:00 2001 From: james-prysm Date: Tue, 26 May 2026 09:02:26 -0500 Subject: [PATCH 03/15] changelog --- api/server/structs/conversions_gloas.go | 23 ++++++ api/server/structs/endpoints_beacon.go | 4 + beacon-chain/cache/proposer_preferences.go | 15 ++++ beacon-chain/core/feed/operation/events.go | 8 ++ beacon-chain/rpc/endpoints.go | 65 ++++++++------- beacon-chain/rpc/endpoints_test.go | 1 + beacon-chain/rpc/eth/beacon/BUILD.bazel | 2 + beacon-chain/rpc/eth/beacon/handlers_pool.go | 27 +++++++ .../handlers_proposer_preferences_test.go | 81 +++++++++++++++++++ beacon-chain/rpc/eth/beacon/server.go | 55 ++++++------- beacon-chain/rpc/eth/events/events.go | 9 +++ beacon-chain/rpc/eth/events/events_test.go | 18 ++++- .../validator/proposer_preferences.go | 2 + .../validate_signed_proposer_preferences.go | 12 ++- ...lidate_signed_proposer_preferences_test.go | 4 +- .../james-prysm_proposer-preference-rest.md | 3 + 16 files changed, 272 insertions(+), 57 deletions(-) create mode 100644 beacon-chain/rpc/eth/beacon/handlers_proposer_preferences_test.go create mode 100644 changelog/james-prysm_proposer-preference-rest.md diff --git a/api/server/structs/conversions_gloas.go b/api/server/structs/conversions_gloas.go index 2a24343c9340..6c5b7f564789 100644 --- a/api/server/structs/conversions_gloas.go +++ b/api/server/structs/conversions_gloas.go @@ -136,6 +136,29 @@ func (s *SignedProposerPreferences) ToConsensus() (*ethpb.SignedProposerPreferen }, nil } +func SignedProposerPreferencesFromConsensus(s *ethpb.SignedProposerPreferences) *SignedProposerPreferences { + if s == nil { + return nil + } + return &SignedProposerPreferences{ + Message: ProposerPreferencesFromConsensus(s.Message), + Signature: hexutil.Encode(s.Signature), + } +} + +func ProposerPreferencesFromConsensus(p *ethpb.ProposerPreferences) *ProposerPreferences { + if p == nil { + return nil + } + return &ProposerPreferences{ + DependentRoot: hexutil.Encode(p.DependentRoot), + ProposalSlot: fmt.Sprintf("%d", p.ProposalSlot), + ValidatorIndex: fmt.Sprintf("%d", p.ValidatorIndex), + FeeRecipient: hexutil.Encode(p.FeeRecipient), + TargetGasLimit: fmt.Sprintf("%d", p.TargetGasLimit), + } +} + func (p *ProposerPreferences) ToConsensus() (*ethpb.ProposerPreferences, error) { dependentRoot, err := bytesutil.DecodeHexWithLength(p.DependentRoot, fieldparams.RootLength) if err != nil { diff --git a/api/server/structs/endpoints_beacon.go b/api/server/structs/endpoints_beacon.go index de12948bc95d..d14563dd7292 100644 --- a/api/server/structs/endpoints_beacon.go +++ b/api/server/structs/endpoints_beacon.go @@ -197,6 +197,10 @@ type GetProposerSlashingsResponse struct { Data []*ProposerSlashing `json:"data"` } +type GetProposerPreferencesResponse struct { + Data []*SignedProposerPreferences `json:"data"` +} + type GetWeakSubjectivityResponse struct { Data *WeakSubjectivityData `json:"data"` } diff --git a/beacon-chain/cache/proposer_preferences.go b/beacon-chain/cache/proposer_preferences.go index a0757e05362c..4a6543d0d64e 100644 --- a/beacon-chain/cache/proposer_preferences.go +++ b/beacon-chain/cache/proposer_preferences.go @@ -3,6 +3,7 @@ package cache import ( "sync" + fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" ) @@ -10,9 +11,11 @@ import ( // via DependentRoot (Gloas spec). type ProposerPreference struct { DependentRoot [32]byte + ProposalSlot primitives.Slot ValidatorIndex primitives.ValidatorIndex FeeRecipient primitives.ExecutionAddress TargetGasLimit uint64 + Signature [fieldparams.BLSSignatureLength]byte } // ProposerPreferencesCache stores broadcast proposer preferences indexed by @@ -83,6 +86,18 @@ func (c *ProposerPreferencesCache) PruneBefore(slot primitives.Slot) { } } +// List returns a flat slice of every cached proposer preference. +func (c *ProposerPreferencesCache) List() []ProposerPreference { + c.lock.RLock() + defer c.lock.RUnlock() + + var out []ProposerPreference + for _, prefs := range c.preferences { + out = append(out, prefs...) + } + return out +} + // Clear removes all cached proposer preferences. func (c *ProposerPreferencesCache) Clear() { c.lock.Lock() diff --git a/beacon-chain/core/feed/operation/events.go b/beacon-chain/core/feed/operation/events.go index 8de9cf0f5029..7cb9850b6387 100644 --- a/beacon-chain/core/feed/operation/events.go +++ b/beacon-chain/core/feed/operation/events.go @@ -49,6 +49,9 @@ const ( // PayloadAttestationMessageReceived is sent after a payload attestation message is received from gossip or rpc. PayloadAttestationMessageReceived = 13 + + // ProposerPreferencesReceived is sent after signed proposer preferences are received from gossip or rpc. + ProposerPreferencesReceived = 14 ) // UnAggregatedAttReceivedData is the data sent with UnaggregatedAttReceived events. @@ -122,3 +125,8 @@ type DataColumnReceivedData struct { type PayloadAttestationMessageReceivedData struct { Message *ethpb.PayloadAttestationMessage } + +// ProposerPreferencesReceivedData is the data sent with ProposerPreferencesReceived events. +type ProposerPreferencesReceivedData struct { + SignedProposerPreferences *ethpb.SignedProposerPreferences +} diff --git a/beacon-chain/rpc/endpoints.go b/beacon-chain/rpc/endpoints.go index 4e106a5a47f7..407b2c28fc8d 100644 --- a/beacon-chain/rpc/endpoints.go +++ b/beacon-chain/rpc/endpoints.go @@ -553,33 +553,34 @@ func (s *Service) beaconEndpoints( coreService *core.Service, ) []endpoint { server := &beacon.Server{ - CanonicalHistory: ch, - BeaconDB: s.cfg.BeaconDB, - AttestationCache: s.cfg.AttestationCache, - AttestationsPool: s.cfg.AttestationsPool, - SlashingsPool: s.cfg.SlashingsPool, - ChainInfoFetcher: s.cfg.ChainInfoFetcher, - GenesisTimeFetcher: s.cfg.GenesisTimeFetcher, - BlockNotifier: s.cfg.BlockNotifier, - OperationNotifier: s.cfg.OperationNotifier, - Broadcaster: s.cfg.Broadcaster, - BlockReceiver: s.cfg.BlockReceiver, - StateGenService: s.cfg.StateGen, - Stater: stater, - Blocker: blocker, - OptimisticModeFetcher: s.cfg.OptimisticModeFetcher, - HeadFetcher: s.cfg.HeadFetcher, - TimeFetcher: s.cfg.GenesisTimeFetcher, - VoluntaryExitsPool: s.cfg.ExitPool, - V1Alpha1ValidatorServer: validatorServer, - DataColumnReceiver: s.cfg.DataColumnReceiver, - SyncChecker: s.cfg.SyncService, - ExecutionReconstructor: s.cfg.ExecutionReconstructor, - BLSChangesPool: s.cfg.BLSChangesPool, - FinalizationFetcher: s.cfg.FinalizationFetcher, - ForkchoiceFetcher: s.cfg.ForkchoiceFetcher, - CoreService: coreService, - AttestationStateFetcher: s.cfg.AttestationReceiver, + CanonicalHistory: ch, + BeaconDB: s.cfg.BeaconDB, + AttestationCache: s.cfg.AttestationCache, + AttestationsPool: s.cfg.AttestationsPool, + SlashingsPool: s.cfg.SlashingsPool, + ChainInfoFetcher: s.cfg.ChainInfoFetcher, + GenesisTimeFetcher: s.cfg.GenesisTimeFetcher, + BlockNotifier: s.cfg.BlockNotifier, + OperationNotifier: s.cfg.OperationNotifier, + Broadcaster: s.cfg.Broadcaster, + BlockReceiver: s.cfg.BlockReceiver, + StateGenService: s.cfg.StateGen, + Stater: stater, + Blocker: blocker, + OptimisticModeFetcher: s.cfg.OptimisticModeFetcher, + HeadFetcher: s.cfg.HeadFetcher, + TimeFetcher: s.cfg.GenesisTimeFetcher, + VoluntaryExitsPool: s.cfg.ExitPool, + V1Alpha1ValidatorServer: validatorServer, + DataColumnReceiver: s.cfg.DataColumnReceiver, + SyncChecker: s.cfg.SyncService, + ExecutionReconstructor: s.cfg.ExecutionReconstructor, + BLSChangesPool: s.cfg.BLSChangesPool, + FinalizationFetcher: s.cfg.FinalizationFetcher, + ForkchoiceFetcher: s.cfg.ForkchoiceFetcher, + CoreService: coreService, + AttestationStateFetcher: s.cfg.AttestationReceiver, + ProposerPreferencesCache: s.cfg.ProposerPreferencesCache, } const namespace = "beacon" @@ -801,6 +802,16 @@ func (s *Service) beaconEndpoints( handler: server.GetProposerSlashings, methods: []string{http.MethodGet}, }, + { + template: "/eth/v1/beacon/pool/proposer_preferences", + name: namespace + ".GetProposerPreferences", + middleware: []middleware.Middleware{ + middleware.AcceptHeaderHandler([]string{api.JsonMediaType}), + middleware.AcceptEncodingHeaderHandler(), + }, + handler: server.GetProposerPreferences, + methods: []string{http.MethodGet}, + }, { template: "/eth/v1/beacon/pool/proposer_slashings", name: namespace + ".SubmitProposerSlashing", diff --git a/beacon-chain/rpc/endpoints_test.go b/beacon-chain/rpc/endpoints_test.go index d5a297deaecb..9818ce0f8492 100644 --- a/beacon-chain/rpc/endpoints_test.go +++ b/beacon-chain/rpc/endpoints_test.go @@ -112,6 +112,7 @@ func Test_endpoints(t *testing.T) { "/eth/v1/validator/execution_payload_envelope/{slot}": {http.MethodGet}, "/eth/v1/validator/sync_committee_contribution": {http.MethodGet}, "/eth/v1/validator/contribution_and_proofs": {http.MethodPost}, + "/eth/v1/beacon/pool/proposer_preferences": {http.MethodGet}, "/eth/v1/validator/prepare_beacon_proposer": {http.MethodPost}, "/eth/v1/validator/proposer_preferences": {http.MethodPost}, "/eth/v1/validator/register_validator": {http.MethodPost}, diff --git a/beacon-chain/rpc/eth/beacon/BUILD.bazel b/beacon-chain/rpc/eth/beacon/BUILD.bazel index f726b30daa83..27c2b136fa6a 100644 --- a/beacon-chain/rpc/eth/beacon/BUILD.bazel +++ b/beacon-chain/rpc/eth/beacon/BUILD.bazel @@ -79,6 +79,7 @@ go_test( "handlers_gloas_bid_test.go", "handlers_gloas_test.go", "handlers_pool_test.go", + "handlers_proposer_preferences_test.go", "handlers_state_test.go", "handlers_test.go", "handlers_validators_test.go", @@ -91,6 +92,7 @@ go_test( "//api/server/structs:go_default_library", "//beacon-chain/blockchain/kzg:go_default_library", "//beacon-chain/blockchain/testing:go_default_library", + "//beacon-chain/cache:go_default_library", "//beacon-chain/core/signing:go_default_library", "//beacon-chain/core/time:go_default_library", "//beacon-chain/core/transition:go_default_library", diff --git a/beacon-chain/rpc/eth/beacon/handlers_pool.go b/beacon-chain/rpc/eth/beacon/handlers_pool.go index 1f95d722037c..53ee0d4ea76c 100644 --- a/beacon-chain/rpc/eth/beacon/handlers_pool.go +++ b/beacon-chain/rpc/eth/beacon/handlers_pool.go @@ -30,6 +30,7 @@ import ( eth "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v7/runtime/version" "github.com/OffchainLabs/prysm/v7/time/slots" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -865,6 +866,32 @@ func (s *Server) submitAttesterSlashing( } } +// GetProposerPreferences retrieves signed proposer preferences known by the +// node but not yet acted on. +func (s *Server) GetProposerPreferences(w http.ResponseWriter, r *http.Request) { + _, span := trace.StartSpan(r.Context(), "beacon.GetProposerPreferences") + defer span.End() + + var data []*structs.SignedProposerPreferences + if s.ProposerPreferencesCache != nil { + entries := s.ProposerPreferencesCache.List() + data = make([]*structs.SignedProposerPreferences, 0, len(entries)) + for _, e := range entries { + data = append(data, &structs.SignedProposerPreferences{ + Message: &structs.ProposerPreferences{ + DependentRoot: hexutil.Encode(e.DependentRoot[:]), + ProposalSlot: fmt.Sprintf("%d", e.ProposalSlot), + ValidatorIndex: fmt.Sprintf("%d", e.ValidatorIndex), + FeeRecipient: hexutil.Encode(e.FeeRecipient[:]), + TargetGasLimit: fmt.Sprintf("%d", e.TargetGasLimit), + }, + Signature: hexutil.Encode(e.Signature[:]), + }) + } + } + httputil.WriteJson(w, &structs.GetProposerPreferencesResponse{Data: data}) +} + // GetProposerSlashings retrieves proposer slashings known by the node // but not necessarily incorporated into any block. func (s *Server) GetProposerSlashings(w http.ResponseWriter, r *http.Request) { diff --git a/beacon-chain/rpc/eth/beacon/handlers_proposer_preferences_test.go b/beacon-chain/rpc/eth/beacon/handlers_proposer_preferences_test.go new file mode 100644 index 000000000000..39ab75420f98 --- /dev/null +++ b/beacon-chain/rpc/eth/beacon/handlers_proposer_preferences_test.go @@ -0,0 +1,81 @@ +package beacon + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/OffchainLabs/prysm/v7/api/server/structs" + "github.com/OffchainLabs/prysm/v7/beacon-chain/cache" + "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + "github.com/OffchainLabs/prysm/v7/testing/assert" + "github.com/OffchainLabs/prysm/v7/testing/require" +) + +func TestGetProposerPreferences_Empty(t *testing.T) { + s := &Server{ProposerPreferencesCache: cache.NewProposerPreferencesCache()} + req := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.GetProposerPreferences(w, req) + require.Equal(t, http.StatusOK, w.Code) + resp := &structs.GetProposerPreferencesResponse{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), resp)) + assert.Equal(t, 0, len(resp.Data)) +} + +func TestGetProposerPreferences_NilCache(t *testing.T) { + s := &Server{} + req := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.GetProposerPreferences(w, req) + require.Equal(t, http.StatusOK, w.Code) + resp := &structs.GetProposerPreferencesResponse{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), resp)) + assert.Equal(t, 0, len(resp.Data)) +} + +func TestGetProposerPreferences_Populated(t *testing.T) { + c := cache.NewProposerPreferencesCache() + c.Add(cache.ProposerPreference{ + DependentRoot: [32]byte{0xaa}, + ProposalSlot: primitives.Slot(32), + ValidatorIndex: 1, + FeeRecipient: primitives.ExecutionAddress{0x01}, + TargetGasLimit: 30_000_000, + Signature: [96]byte{0xff}, + }, primitives.Slot(32)) + c.Add(cache.ProposerPreference{ + DependentRoot: [32]byte{0xbb}, + ProposalSlot: primitives.Slot(33), + ValidatorIndex: 2, + FeeRecipient: primitives.ExecutionAddress{0x02}, + TargetGasLimit: 25_000_000, + Signature: [96]byte{0xee}, + }, primitives.Slot(33)) + + s := &Server{ProposerPreferencesCache: c} + req := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.GetProposerPreferences(w, req) + require.Equal(t, http.StatusOK, w.Code) + resp := &structs.GetProposerPreferencesResponse{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), resp)) + require.Equal(t, 2, len(resp.Data)) + + bySlot := map[string]*structs.SignedProposerPreferences{} + for _, p := range resp.Data { + bySlot[p.Message.ProposalSlot] = p + } + assert.Equal(t, "1", bySlot["32"].Message.ValidatorIndex) + assert.Equal(t, "30000000", bySlot["32"].Message.TargetGasLimit) + assert.Equal(t, "2", bySlot["33"].Message.ValidatorIndex) + assert.Equal(t, "25000000", bySlot["33"].Message.TargetGasLimit) +} diff --git a/beacon-chain/rpc/eth/beacon/server.go b/beacon-chain/rpc/eth/beacon/server.go index 58e006c6f407..539fdba2c64b 100644 --- a/beacon-chain/rpc/eth/beacon/server.go +++ b/beacon-chain/rpc/eth/beacon/server.go @@ -25,31 +25,32 @@ import ( // Server defines a server implementation of the gRPC Beacon Chain service, // providing RPC endpoints to access data relevant to the Ethereum Beacon Chain. type Server struct { - BeaconDB db.ReadOnlyDatabase - ChainInfoFetcher blockchain.ChainInfoFetcher - GenesisTimeFetcher blockchain.TimeFetcher - BlockReceiver blockchain.BlockReceiver - BlockNotifier blockfeed.Notifier - OperationNotifier operation.Notifier - Broadcaster p2p.Broadcaster - DataColumnReceiver blockchain.DataColumnReceiver - AttestationCache *cache.AttestationCache - AttestationsPool attestations.Pool - SlashingsPool slashings.PoolManager - VoluntaryExitsPool voluntaryexits.PoolManager - StateGenService stategen.StateManager - Stater lookup.Stater - Blocker lookup.Blocker - HeadFetcher blockchain.HeadFetcher - TimeFetcher blockchain.TimeFetcher - OptimisticModeFetcher blockchain.OptimisticModeFetcher - V1Alpha1ValidatorServer eth.BeaconNodeValidatorServer - SyncChecker sync.Checker - CanonicalHistory *stategen.CanonicalHistory - ExecutionReconstructor execution.Reconstructor - FinalizationFetcher blockchain.FinalizationFetcher - BLSChangesPool blstoexec.PoolManager - ForkchoiceFetcher blockchain.ForkchoiceFetcher - CoreService *core.Service - AttestationStateFetcher blockchain.AttestationStateFetcher + BeaconDB db.ReadOnlyDatabase + ChainInfoFetcher blockchain.ChainInfoFetcher + GenesisTimeFetcher blockchain.TimeFetcher + BlockReceiver blockchain.BlockReceiver + BlockNotifier blockfeed.Notifier + OperationNotifier operation.Notifier + Broadcaster p2p.Broadcaster + DataColumnReceiver blockchain.DataColumnReceiver + AttestationCache *cache.AttestationCache + AttestationsPool attestations.Pool + SlashingsPool slashings.PoolManager + VoluntaryExitsPool voluntaryexits.PoolManager + StateGenService stategen.StateManager + Stater lookup.Stater + Blocker lookup.Blocker + HeadFetcher blockchain.HeadFetcher + TimeFetcher blockchain.TimeFetcher + OptimisticModeFetcher blockchain.OptimisticModeFetcher + V1Alpha1ValidatorServer eth.BeaconNodeValidatorServer + SyncChecker sync.Checker + CanonicalHistory *stategen.CanonicalHistory + ExecutionReconstructor execution.Reconstructor + FinalizationFetcher blockchain.FinalizationFetcher + BLSChangesPool blstoexec.PoolManager + ForkchoiceFetcher blockchain.ForkchoiceFetcher + CoreService *core.Service + AttestationStateFetcher blockchain.AttestationStateFetcher + ProposerPreferencesCache *cache.ProposerPreferencesCache } diff --git a/beacon-chain/rpc/eth/events/events.go b/beacon-chain/rpc/eth/events/events.go index ca889aa496ee..2783101fcdae 100644 --- a/beacon-chain/rpc/eth/events/events.go +++ b/beacon-chain/rpc/eth/events/events.go @@ -81,6 +81,8 @@ const ( ExecutionPayloadBidTopic = "execution_payload_bid" // PayloadAttestationMessageTopic represents a new payload attestation message event topic. PayloadAttestationMessageTopic = "payload_attestation_message" + // ProposerPreferencesTopic represents a new signed proposer preferences event topic. + ProposerPreferencesTopic = "proposer_preferences" ) var ( @@ -116,6 +118,7 @@ var opsFeedEventTopics = map[feed.EventType]string{ operation.BlockGossipReceived: BlockGossipTopic, operation.DataColumnReceived: DataColumnTopic, operation.PayloadAttestationMessageReceived: PayloadAttestationMessageTopic, + operation.ProposerPreferencesReceived: ProposerPreferencesTopic, } var stateFeedEventTopics = map[feed.EventType]string{ @@ -481,6 +484,8 @@ func topicForEvent(event *feed.Event) string { return DataColumnTopic case *operation.PayloadAttestationMessageReceivedData: return PayloadAttestationMessageTopic + case *operation.ProposerPreferencesReceivedData: + return ProposerPreferencesTopic case *statefeed.PayloadProcessedData: return ExecutionPayloadTopic default: @@ -659,6 +664,10 @@ func (s *Server) lazyReaderForEvent(ctx context.Context, event *feed.Event, topi return func() io.Reader { return jsonMarshalReader(eventName, structs.PayloadAttestationMessageFromConsensus(v.Message)) }, nil + case *operation.ProposerPreferencesReceivedData: + return func() io.Reader { + return jsonMarshalReader(eventName, structs.SignedProposerPreferencesFromConsensus(v.SignedProposerPreferences)) + }, nil case *statefeed.PayloadProcessedData: return func() io.Reader { return jsonMarshalReader(eventName, &structs.PayloadEvent{ diff --git a/beacon-chain/rpc/eth/events/events_test.go b/beacon-chain/rpc/eth/events/events_test.go index debf466df7e1..6b5d1e35d19c 100644 --- a/beacon-chain/rpc/eth/events/events_test.go +++ b/beacon-chain/rpc/eth/events/events_test.go @@ -124,6 +124,7 @@ func operationEventsFixtures(t *testing.T) (*topicRequest, []*feed.Event) { BlockGossipTopic, DataColumnTopic, PayloadAttestationMessageTopic, + ProposerPreferencesTopic, }) require.NoError(t, err) ro, err := blocks.NewROBlob(util.HydrateBlobSidecar(ð.BlobSidecar{})) @@ -328,6 +329,21 @@ func operationEventsFixtures(t *testing.T) (*topicRequest, []*feed.Event) { }, }, }, + { + Type: operation.ProposerPreferencesReceived, + Data: &operation.ProposerPreferencesReceivedData{ + SignedProposerPreferences: ð.SignedProposerPreferences{ + Message: ð.ProposerPreferences{ + DependentRoot: make([]byte, fieldparams.RootLength), + ProposalSlot: 32, + ValidatorIndex: 7, + FeeRecipient: make([]byte, 20), + TargetGasLimit: 30_000_000, + }, + Signature: make([]byte, fieldparams.BLSSignatureLength), + }, + }, + }, } } @@ -745,7 +761,7 @@ func TestStuckReaderScenarios(t *testing.T) { func wedgedWriterTestCase(t *testing.T, queueDepth func([]*feed.Event) int) { topics, events := operationEventsFixtures(t) - require.Equal(t, 13, len(events)) + require.Equal(t, 14, len(events)) // set eventFeedDepth to a number lower than the events we intend to send to force the server to drop the reader. stn := mockChain.NewEventFeedWrapper() diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_preferences.go b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_preferences.go index 147fc9648454..f2eb7c6aa76e 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_preferences.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_preferences.go @@ -93,9 +93,11 @@ func (vs *Server) SubmitSignedProposerPreferences( vs.ProposerPreferencesCache.Add(cache.ProposerPreference{ DependentRoot: dependentRoot, + ProposalSlot: proposalSlot, ValidatorIndex: msg.Message.ValidatorIndex, FeeRecipient: bytesutil.ToBytes20(msg.Message.FeeRecipient), TargetGasLimit: msg.Message.TargetGasLimit, + Signature: bytesutil.ToBytes96(msg.Signature), }, proposalSlot) broadcast++ } diff --git a/beacon-chain/sync/validate_signed_proposer_preferences.go b/beacon-chain/sync/validate_signed_proposer_preferences.go index fd3d27e9eb7d..6954f858046d 100644 --- a/beacon-chain/sync/validate_signed_proposer_preferences.go +++ b/beacon-chain/sync/validate_signed_proposer_preferences.go @@ -4,6 +4,8 @@ import ( "context" "github.com/OffchainLabs/prysm/v7/beacon-chain/cache" + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed" + opfeed "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed/operation" "github.com/OffchainLabs/prysm/v7/beacon-chain/core/transition" "github.com/OffchainLabs/prysm/v7/beacon-chain/p2p" "github.com/OffchainLabs/prysm/v7/beacon-chain/state" @@ -117,18 +119,26 @@ func (s *Service) validateSignedProposerPreferencesGossip(ctx context.Context, p s.proposerPreferencesCache.Add(cache.ProposerPreference{ DependentRoot: dependentRoot, + ProposalSlot: slot, ValidatorIndex: signedPreferences.Message.ValidatorIndex, FeeRecipient: bytesutil.ToBytes20(signedPreferences.Message.FeeRecipient), TargetGasLimit: signedPreferences.Message.TargetGasLimit, + Signature: bytesutil.ToBytes96(signedPreferences.Signature), }, slot) msg.ValidatorData = signedPreferences return pubsub.ValidationAccept, nil } func (s *Service) signedProposerPreferencesSubscriber(_ context.Context, msg proto.Message) error { - _, ok := msg.(*ethpb.SignedProposerPreferences) + signedPreferences, ok := msg.(*ethpb.SignedProposerPreferences) if !ok { return errWrongMessage } + s.cfg.operationNotifier.OperationFeed().Send(&feed.Event{ + Type: opfeed.ProposerPreferencesReceived, + Data: &opfeed.ProposerPreferencesReceivedData{ + SignedProposerPreferences: signedPreferences, + }, + }) return nil } diff --git a/beacon-chain/sync/validate_signed_proposer_preferences_test.go b/beacon-chain/sync/validate_signed_proposer_preferences_test.go index c06f0863c9c3..4abcf1abbfdb 100644 --- a/beacon-chain/sync/validate_signed_proposer_preferences_test.go +++ b/beacon-chain/sync/validate_signed_proposer_preferences_test.go @@ -177,7 +177,9 @@ func TestSignedProposerPreferencesSubscriber_WrongMessage(t *testing.T) { } func TestSignedProposerPreferencesSubscriber_HappyPath(t *testing.T) { - s := &Service{} + s := &Service{ + cfg: &config{operationNotifier: &mock.MockOperationNotifier{}}, + } err := s.signedProposerPreferencesSubscriber(context.Background(), ðpb.SignedProposerPreferences{}) require.NoError(t, err) } diff --git a/changelog/james-prysm_proposer-preference-rest.md b/changelog/james-prysm_proposer-preference-rest.md new file mode 100644 index 000000000000..22f877cf82c7 --- /dev/null +++ b/changelog/james-prysm_proposer-preference-rest.md @@ -0,0 +1,3 @@ +### Added + +- adding /eth/v1/validator/proposer_preferences POST endpoint \ No newline at end of file From baee1846b02e9c5369fb72834a294a165e68a6f8 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Tue, 26 May 2026 17:10:25 -0500 Subject: [PATCH 04/15] fixing issue from bad merge --- beacon-chain/rpc/endpoints.go | 32 +------------------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/beacon-chain/rpc/endpoints.go b/beacon-chain/rpc/endpoints.go index 6fa3c46ad9f8..20779a4f2073 100644 --- a/beacon-chain/rpc/endpoints.go +++ b/beacon-chain/rpc/endpoints.go @@ -564,7 +564,6 @@ func (s *Service) beaconEndpoints( coreService *core.Service, ) []endpoint { server := &beacon.Server{ -<<<<<<< proposer-preference-rest CanonicalHistory: ch, BeaconDB: s.cfg.BeaconDB, AttestationCache: s.cfg.AttestationCache, @@ -588,41 +587,12 @@ func (s *Service) beaconEndpoints( SyncChecker: s.cfg.SyncService, ExecutionReconstructor: s.cfg.ExecutionReconstructor, BLSChangesPool: s.cfg.BLSChangesPool, + PayloadAttestationPool: s.cfg.PayloadAttestationPool, FinalizationFetcher: s.cfg.FinalizationFetcher, ForkchoiceFetcher: s.cfg.ForkchoiceFetcher, CoreService: coreService, AttestationStateFetcher: s.cfg.AttestationReceiver, ProposerPreferencesCache: s.cfg.ProposerPreferencesCache, -======= - CanonicalHistory: ch, - BeaconDB: s.cfg.BeaconDB, - AttestationCache: s.cfg.AttestationCache, - AttestationsPool: s.cfg.AttestationsPool, - SlashingsPool: s.cfg.SlashingsPool, - ChainInfoFetcher: s.cfg.ChainInfoFetcher, - GenesisTimeFetcher: s.cfg.GenesisTimeFetcher, - BlockNotifier: s.cfg.BlockNotifier, - OperationNotifier: s.cfg.OperationNotifier, - Broadcaster: s.cfg.Broadcaster, - BlockReceiver: s.cfg.BlockReceiver, - StateGenService: s.cfg.StateGen, - Stater: stater, - Blocker: blocker, - OptimisticModeFetcher: s.cfg.OptimisticModeFetcher, - HeadFetcher: s.cfg.HeadFetcher, - TimeFetcher: s.cfg.GenesisTimeFetcher, - VoluntaryExitsPool: s.cfg.ExitPool, - V1Alpha1ValidatorServer: validatorServer, - DataColumnReceiver: s.cfg.DataColumnReceiver, - SyncChecker: s.cfg.SyncService, - ExecutionReconstructor: s.cfg.ExecutionReconstructor, - BLSChangesPool: s.cfg.BLSChangesPool, - PayloadAttestationPool: s.cfg.PayloadAttestationPool, - FinalizationFetcher: s.cfg.FinalizationFetcher, - ForkchoiceFetcher: s.cfg.ForkchoiceFetcher, - CoreService: coreService, - AttestationStateFetcher: s.cfg.AttestationReceiver, ->>>>>>> develop } const namespace = "beacon" From 8d33265d3345ac0206122b56d962e3f725640d8d Mon Sep 17 00:00:00 2001 From: james-prysm Date: Wed, 27 May 2026 10:49:25 -0500 Subject: [PATCH 05/15] linting --- beacon-chain/rpc/eth/beacon/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon-chain/rpc/eth/beacon/server.go b/beacon-chain/rpc/eth/beacon/server.go index f4fbaede235d..611306ee95b2 100644 --- a/beacon-chain/rpc/eth/beacon/server.go +++ b/beacon-chain/rpc/eth/beacon/server.go @@ -50,7 +50,7 @@ type Server struct { ExecutionReconstructor execution.Reconstructor FinalizationFetcher blockchain.FinalizationFetcher BLSChangesPool blstoexec.PoolManager - PayloadAttestationPool payloadattestation.PoolManager + PayloadAttestationPool payloadattestation.PoolManager ForkchoiceFetcher blockchain.ForkchoiceFetcher CoreService *core.Service AttestationStateFetcher blockchain.AttestationStateFetcher From 176b66be12a77af92fceac7578d97a2cf33641e5 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Wed, 27 May 2026 11:26:35 -0500 Subject: [PATCH 06/15] removing get request for now --- api/server/structs/endpoints_beacon.go | 4 - beacon-chain/rpc/endpoints.go | 67 +++++++-------- beacon-chain/rpc/endpoints_test.go | 3 +- beacon-chain/rpc/eth/beacon/BUILD.bazel | 1 - beacon-chain/rpc/eth/beacon/handlers_pool.go | 27 ------- .../handlers_proposer_preferences_test.go | 81 ------------------- beacon-chain/rpc/eth/beacon/server.go | 57 +++++++------ 7 files changed, 57 insertions(+), 183 deletions(-) delete mode 100644 beacon-chain/rpc/eth/beacon/handlers_proposer_preferences_test.go diff --git a/api/server/structs/endpoints_beacon.go b/api/server/structs/endpoints_beacon.go index 7d6e7f252435..e26caf3aea64 100644 --- a/api/server/structs/endpoints_beacon.go +++ b/api/server/structs/endpoints_beacon.go @@ -202,10 +202,6 @@ type GetProposerSlashingsResponse struct { Data []*ProposerSlashing `json:"data"` } -type GetProposerPreferencesResponse struct { - Data []*SignedProposerPreferences `json:"data"` -} - type GetWeakSubjectivityResponse struct { Data *WeakSubjectivityData `json:"data"` } diff --git a/beacon-chain/rpc/endpoints.go b/beacon-chain/rpc/endpoints.go index 20779a4f2073..ac88fd765549 100644 --- a/beacon-chain/rpc/endpoints.go +++ b/beacon-chain/rpc/endpoints.go @@ -564,35 +564,34 @@ func (s *Service) beaconEndpoints( coreService *core.Service, ) []endpoint { server := &beacon.Server{ - CanonicalHistory: ch, - BeaconDB: s.cfg.BeaconDB, - AttestationCache: s.cfg.AttestationCache, - AttestationsPool: s.cfg.AttestationsPool, - SlashingsPool: s.cfg.SlashingsPool, - ChainInfoFetcher: s.cfg.ChainInfoFetcher, - GenesisTimeFetcher: s.cfg.GenesisTimeFetcher, - BlockNotifier: s.cfg.BlockNotifier, - OperationNotifier: s.cfg.OperationNotifier, - Broadcaster: s.cfg.Broadcaster, - BlockReceiver: s.cfg.BlockReceiver, - StateGenService: s.cfg.StateGen, - Stater: stater, - Blocker: blocker, - OptimisticModeFetcher: s.cfg.OptimisticModeFetcher, - HeadFetcher: s.cfg.HeadFetcher, - TimeFetcher: s.cfg.GenesisTimeFetcher, - VoluntaryExitsPool: s.cfg.ExitPool, - V1Alpha1ValidatorServer: validatorServer, - DataColumnReceiver: s.cfg.DataColumnReceiver, - SyncChecker: s.cfg.SyncService, - ExecutionReconstructor: s.cfg.ExecutionReconstructor, - BLSChangesPool: s.cfg.BLSChangesPool, - PayloadAttestationPool: s.cfg.PayloadAttestationPool, - FinalizationFetcher: s.cfg.FinalizationFetcher, - ForkchoiceFetcher: s.cfg.ForkchoiceFetcher, - CoreService: coreService, - AttestationStateFetcher: s.cfg.AttestationReceiver, - ProposerPreferencesCache: s.cfg.ProposerPreferencesCache, + CanonicalHistory: ch, + BeaconDB: s.cfg.BeaconDB, + AttestationCache: s.cfg.AttestationCache, + AttestationsPool: s.cfg.AttestationsPool, + SlashingsPool: s.cfg.SlashingsPool, + ChainInfoFetcher: s.cfg.ChainInfoFetcher, + GenesisTimeFetcher: s.cfg.GenesisTimeFetcher, + BlockNotifier: s.cfg.BlockNotifier, + OperationNotifier: s.cfg.OperationNotifier, + Broadcaster: s.cfg.Broadcaster, + BlockReceiver: s.cfg.BlockReceiver, + StateGenService: s.cfg.StateGen, + Stater: stater, + Blocker: blocker, + OptimisticModeFetcher: s.cfg.OptimisticModeFetcher, + HeadFetcher: s.cfg.HeadFetcher, + TimeFetcher: s.cfg.GenesisTimeFetcher, + VoluntaryExitsPool: s.cfg.ExitPool, + V1Alpha1ValidatorServer: validatorServer, + DataColumnReceiver: s.cfg.DataColumnReceiver, + SyncChecker: s.cfg.SyncService, + ExecutionReconstructor: s.cfg.ExecutionReconstructor, + BLSChangesPool: s.cfg.BLSChangesPool, + PayloadAttestationPool: s.cfg.PayloadAttestationPool, + FinalizationFetcher: s.cfg.FinalizationFetcher, + ForkchoiceFetcher: s.cfg.ForkchoiceFetcher, + CoreService: coreService, + AttestationStateFetcher: s.cfg.AttestationReceiver, } const namespace = "beacon" @@ -814,16 +813,6 @@ func (s *Service) beaconEndpoints( handler: server.GetProposerSlashings, methods: []string{http.MethodGet}, }, - { - template: "/eth/v1/beacon/pool/proposer_preferences", - name: namespace + ".GetProposerPreferences", - middleware: []middleware.Middleware{ - middleware.AcceptHeaderHandler([]string{api.JsonMediaType}), - middleware.AcceptEncodingHeaderHandler(), - }, - handler: server.GetProposerPreferences, - methods: []string{http.MethodGet}, - }, { template: "/eth/v1/beacon/pool/proposer_slashings", name: namespace + ".SubmitProposerSlashing", diff --git a/beacon-chain/rpc/endpoints_test.go b/beacon-chain/rpc/endpoints_test.go index d5dd34809723..7a35703598cd 100644 --- a/beacon-chain/rpc/endpoints_test.go +++ b/beacon-chain/rpc/endpoints_test.go @@ -113,12 +113,11 @@ func Test_endpoints(t *testing.T) { "/eth/v1/validator/execution_payload_envelope/{slot}": {http.MethodGet}, "/eth/v1/validator/sync_committee_contribution": {http.MethodGet}, "/eth/v1/validator/contribution_and_proofs": {http.MethodPost}, - "/eth/v1/beacon/pool/proposer_preferences": {http.MethodGet}, "/eth/v1/validator/prepare_beacon_proposer": {http.MethodPost}, "/eth/v1/validator/proposer_preferences": {http.MethodPost}, "/eth/v1/validator/register_validator": {http.MethodPost}, "/eth/v1/validator/liveness/{epoch}": {http.MethodPost}, - "/eth/v1/validator/payload_attestation_data/{slot}": {http.MethodGet}, + "/eth/v1/validator/payload_attestation_data/{slot}": {http.MethodGet}, } prysmBeaconRoutes := map[string][]string{ diff --git a/beacon-chain/rpc/eth/beacon/BUILD.bazel b/beacon-chain/rpc/eth/beacon/BUILD.bazel index c4bbc88b3183..5d6d45476332 100644 --- a/beacon-chain/rpc/eth/beacon/BUILD.bazel +++ b/beacon-chain/rpc/eth/beacon/BUILD.bazel @@ -80,7 +80,6 @@ go_test( "handlers_gloas_bid_test.go", "handlers_gloas_test.go", "handlers_pool_test.go", - "handlers_proposer_preferences_test.go", "handlers_state_test.go", "handlers_test.go", "handlers_validators_test.go", diff --git a/beacon-chain/rpc/eth/beacon/handlers_pool.go b/beacon-chain/rpc/eth/beacon/handlers_pool.go index 59c9975edde2..a2f71fcb4c06 100644 --- a/beacon-chain/rpc/eth/beacon/handlers_pool.go +++ b/beacon-chain/rpc/eth/beacon/handlers_pool.go @@ -31,7 +31,6 @@ import ( eth "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v7/runtime/version" "github.com/OffchainLabs/prysm/v7/time/slots" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -867,32 +866,6 @@ func (s *Server) submitAttesterSlashing( } } -// GetProposerPreferences retrieves signed proposer preferences known by the -// node but not yet acted on. -func (s *Server) GetProposerPreferences(w http.ResponseWriter, r *http.Request) { - _, span := trace.StartSpan(r.Context(), "beacon.GetProposerPreferences") - defer span.End() - - var data []*structs.SignedProposerPreferences - if s.ProposerPreferencesCache != nil { - entries := s.ProposerPreferencesCache.List() - data = make([]*structs.SignedProposerPreferences, 0, len(entries)) - for _, e := range entries { - data = append(data, &structs.SignedProposerPreferences{ - Message: &structs.ProposerPreferences{ - DependentRoot: hexutil.Encode(e.DependentRoot[:]), - ProposalSlot: fmt.Sprintf("%d", e.ProposalSlot), - ValidatorIndex: fmt.Sprintf("%d", e.ValidatorIndex), - FeeRecipient: hexutil.Encode(e.FeeRecipient[:]), - TargetGasLimit: fmt.Sprintf("%d", e.TargetGasLimit), - }, - Signature: hexutil.Encode(e.Signature[:]), - }) - } - } - httputil.WriteJson(w, &structs.GetProposerPreferencesResponse{Data: data}) -} - // GetProposerSlashings retrieves proposer slashings known by the node // but not necessarily incorporated into any block. func (s *Server) GetProposerSlashings(w http.ResponseWriter, r *http.Request) { diff --git a/beacon-chain/rpc/eth/beacon/handlers_proposer_preferences_test.go b/beacon-chain/rpc/eth/beacon/handlers_proposer_preferences_test.go deleted file mode 100644 index 39ab75420f98..000000000000 --- a/beacon-chain/rpc/eth/beacon/handlers_proposer_preferences_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package beacon - -import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/OffchainLabs/prysm/v7/api/server/structs" - "github.com/OffchainLabs/prysm/v7/beacon-chain/cache" - "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" - "github.com/OffchainLabs/prysm/v7/testing/assert" - "github.com/OffchainLabs/prysm/v7/testing/require" -) - -func TestGetProposerPreferences_Empty(t *testing.T) { - s := &Server{ProposerPreferencesCache: cache.NewProposerPreferencesCache()} - req := httptest.NewRequest(http.MethodGet, "http://example.com", nil) - w := httptest.NewRecorder() - w.Body = &bytes.Buffer{} - - s.GetProposerPreferences(w, req) - require.Equal(t, http.StatusOK, w.Code) - resp := &structs.GetProposerPreferencesResponse{} - require.NoError(t, json.Unmarshal(w.Body.Bytes(), resp)) - assert.Equal(t, 0, len(resp.Data)) -} - -func TestGetProposerPreferences_NilCache(t *testing.T) { - s := &Server{} - req := httptest.NewRequest(http.MethodGet, "http://example.com", nil) - w := httptest.NewRecorder() - w.Body = &bytes.Buffer{} - - s.GetProposerPreferences(w, req) - require.Equal(t, http.StatusOK, w.Code) - resp := &structs.GetProposerPreferencesResponse{} - require.NoError(t, json.Unmarshal(w.Body.Bytes(), resp)) - assert.Equal(t, 0, len(resp.Data)) -} - -func TestGetProposerPreferences_Populated(t *testing.T) { - c := cache.NewProposerPreferencesCache() - c.Add(cache.ProposerPreference{ - DependentRoot: [32]byte{0xaa}, - ProposalSlot: primitives.Slot(32), - ValidatorIndex: 1, - FeeRecipient: primitives.ExecutionAddress{0x01}, - TargetGasLimit: 30_000_000, - Signature: [96]byte{0xff}, - }, primitives.Slot(32)) - c.Add(cache.ProposerPreference{ - DependentRoot: [32]byte{0xbb}, - ProposalSlot: primitives.Slot(33), - ValidatorIndex: 2, - FeeRecipient: primitives.ExecutionAddress{0x02}, - TargetGasLimit: 25_000_000, - Signature: [96]byte{0xee}, - }, primitives.Slot(33)) - - s := &Server{ProposerPreferencesCache: c} - req := httptest.NewRequest(http.MethodGet, "http://example.com", nil) - w := httptest.NewRecorder() - w.Body = &bytes.Buffer{} - - s.GetProposerPreferences(w, req) - require.Equal(t, http.StatusOK, w.Code) - resp := &structs.GetProposerPreferencesResponse{} - require.NoError(t, json.Unmarshal(w.Body.Bytes(), resp)) - require.Equal(t, 2, len(resp.Data)) - - bySlot := map[string]*structs.SignedProposerPreferences{} - for _, p := range resp.Data { - bySlot[p.Message.ProposalSlot] = p - } - assert.Equal(t, "1", bySlot["32"].Message.ValidatorIndex) - assert.Equal(t, "30000000", bySlot["32"].Message.TargetGasLimit) - assert.Equal(t, "2", bySlot["33"].Message.ValidatorIndex) - assert.Equal(t, "25000000", bySlot["33"].Message.TargetGasLimit) -} diff --git a/beacon-chain/rpc/eth/beacon/server.go b/beacon-chain/rpc/eth/beacon/server.go index 611306ee95b2..b28e871391f1 100644 --- a/beacon-chain/rpc/eth/beacon/server.go +++ b/beacon-chain/rpc/eth/beacon/server.go @@ -26,33 +26,32 @@ import ( // Server defines a server implementation of the gRPC Beacon Chain service, // providing RPC endpoints to access data relevant to the Ethereum Beacon Chain. type Server struct { - BeaconDB db.ReadOnlyDatabase - ChainInfoFetcher blockchain.ChainInfoFetcher - GenesisTimeFetcher blockchain.TimeFetcher - BlockReceiver blockchain.BlockReceiver - BlockNotifier blockfeed.Notifier - OperationNotifier operation.Notifier - Broadcaster p2p.Broadcaster - DataColumnReceiver blockchain.DataColumnReceiver - AttestationCache *cache.AttestationCache - AttestationsPool attestations.Pool - SlashingsPool slashings.PoolManager - VoluntaryExitsPool voluntaryexits.PoolManager - StateGenService stategen.StateManager - Stater lookup.Stater - Blocker lookup.Blocker - HeadFetcher blockchain.HeadFetcher - TimeFetcher blockchain.TimeFetcher - OptimisticModeFetcher blockchain.OptimisticModeFetcher - V1Alpha1ValidatorServer eth.BeaconNodeValidatorServer - SyncChecker sync.Checker - CanonicalHistory *stategen.CanonicalHistory - ExecutionReconstructor execution.Reconstructor - FinalizationFetcher blockchain.FinalizationFetcher - BLSChangesPool blstoexec.PoolManager - PayloadAttestationPool payloadattestation.PoolManager - ForkchoiceFetcher blockchain.ForkchoiceFetcher - CoreService *core.Service - AttestationStateFetcher blockchain.AttestationStateFetcher - ProposerPreferencesCache *cache.ProposerPreferencesCache + BeaconDB db.ReadOnlyDatabase + ChainInfoFetcher blockchain.ChainInfoFetcher + GenesisTimeFetcher blockchain.TimeFetcher + BlockReceiver blockchain.BlockReceiver + BlockNotifier blockfeed.Notifier + OperationNotifier operation.Notifier + Broadcaster p2p.Broadcaster + DataColumnReceiver blockchain.DataColumnReceiver + AttestationCache *cache.AttestationCache + AttestationsPool attestations.Pool + SlashingsPool slashings.PoolManager + VoluntaryExitsPool voluntaryexits.PoolManager + StateGenService stategen.StateManager + Stater lookup.Stater + Blocker lookup.Blocker + HeadFetcher blockchain.HeadFetcher + TimeFetcher blockchain.TimeFetcher + OptimisticModeFetcher blockchain.OptimisticModeFetcher + V1Alpha1ValidatorServer eth.BeaconNodeValidatorServer + SyncChecker sync.Checker + CanonicalHistory *stategen.CanonicalHistory + ExecutionReconstructor execution.Reconstructor + FinalizationFetcher blockchain.FinalizationFetcher + BLSChangesPool blstoexec.PoolManager + PayloadAttestationPool payloadattestation.PoolManager + ForkchoiceFetcher blockchain.ForkchoiceFetcher + CoreService *core.Service + AttestationStateFetcher blockchain.AttestationStateFetcher } From def4f5d76f33dfd93adcf19ca5722603d8cc52f1 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Wed, 27 May 2026 12:09:33 -0500 Subject: [PATCH 07/15] updating changelog --- changelog/james-prysm_proposer-preference-rest.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog/james-prysm_proposer-preference-rest.md b/changelog/james-prysm_proposer-preference-rest.md index 22f877cf82c7..7740aa295348 100644 --- a/changelog/james-prysm_proposer-preference-rest.md +++ b/changelog/james-prysm_proposer-preference-rest.md @@ -1,3 +1,4 @@ ### Added -- adding /eth/v1/validator/proposer_preferences POST endpoint \ No newline at end of file +- adding /eth/v1/validator/proposer_preferences POST endpoint +- adding `proposer_preferences` SSE event topic on /eth/v1/events \ No newline at end of file From 563360f990c432dfe989fb956702ba99924e4d97 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Wed, 27 May 2026 13:09:10 -0500 Subject: [PATCH 08/15] gaz --- beacon-chain/rpc/eth/beacon/BUILD.bazel | 1 - 1 file changed, 1 deletion(-) diff --git a/beacon-chain/rpc/eth/beacon/BUILD.bazel b/beacon-chain/rpc/eth/beacon/BUILD.bazel index 5d6d45476332..112cf0d76aec 100644 --- a/beacon-chain/rpc/eth/beacon/BUILD.bazel +++ b/beacon-chain/rpc/eth/beacon/BUILD.bazel @@ -92,7 +92,6 @@ go_test( "//api/server/structs:go_default_library", "//beacon-chain/blockchain/kzg:go_default_library", "//beacon-chain/blockchain/testing:go_default_library", - "//beacon-chain/cache:go_default_library", "//beacon-chain/core/signing:go_default_library", "//beacon-chain/core/time:go_default_library", "//beacon-chain/core/transition:go_default_library", From 97a5e0b141bc5e75e7ea356e2263cad51d842e80 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Wed, 27 May 2026 14:32:16 -0500 Subject: [PATCH 09/15] removing unneeded fields with get api removed --- beacon-chain/cache/proposer_preferences.go | 15 --------------- .../v1alpha1/validator/proposer_preferences.go | 2 -- .../sync/validate_signed_proposer_preferences.go | 2 -- 3 files changed, 19 deletions(-) diff --git a/beacon-chain/cache/proposer_preferences.go b/beacon-chain/cache/proposer_preferences.go index 4a6543d0d64e..a0757e05362c 100644 --- a/beacon-chain/cache/proposer_preferences.go +++ b/beacon-chain/cache/proposer_preferences.go @@ -3,7 +3,6 @@ package cache import ( "sync" - fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" ) @@ -11,11 +10,9 @@ import ( // via DependentRoot (Gloas spec). type ProposerPreference struct { DependentRoot [32]byte - ProposalSlot primitives.Slot ValidatorIndex primitives.ValidatorIndex FeeRecipient primitives.ExecutionAddress TargetGasLimit uint64 - Signature [fieldparams.BLSSignatureLength]byte } // ProposerPreferencesCache stores broadcast proposer preferences indexed by @@ -86,18 +83,6 @@ func (c *ProposerPreferencesCache) PruneBefore(slot primitives.Slot) { } } -// List returns a flat slice of every cached proposer preference. -func (c *ProposerPreferencesCache) List() []ProposerPreference { - c.lock.RLock() - defer c.lock.RUnlock() - - var out []ProposerPreference - for _, prefs := range c.preferences { - out = append(out, prefs...) - } - return out -} - // Clear removes all cached proposer preferences. func (c *ProposerPreferencesCache) Clear() { c.lock.Lock() diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_preferences.go b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_preferences.go index f2eb7c6aa76e..147fc9648454 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_preferences.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_preferences.go @@ -93,11 +93,9 @@ func (vs *Server) SubmitSignedProposerPreferences( vs.ProposerPreferencesCache.Add(cache.ProposerPreference{ DependentRoot: dependentRoot, - ProposalSlot: proposalSlot, ValidatorIndex: msg.Message.ValidatorIndex, FeeRecipient: bytesutil.ToBytes20(msg.Message.FeeRecipient), TargetGasLimit: msg.Message.TargetGasLimit, - Signature: bytesutil.ToBytes96(msg.Signature), }, proposalSlot) broadcast++ } diff --git a/beacon-chain/sync/validate_signed_proposer_preferences.go b/beacon-chain/sync/validate_signed_proposer_preferences.go index fed53977ee3d..3ed5de3080d5 100644 --- a/beacon-chain/sync/validate_signed_proposer_preferences.go +++ b/beacon-chain/sync/validate_signed_proposer_preferences.go @@ -127,11 +127,9 @@ func (s *Service) validateSignedProposerPreferencesGossip(ctx context.Context, p s.proposerPreferencesCache.Add(cache.ProposerPreference{ DependentRoot: dependentRoot, - ProposalSlot: slot, ValidatorIndex: signedPreferences.Message.ValidatorIndex, FeeRecipient: bytesutil.ToBytes20(signedPreferences.Message.FeeRecipient), TargetGasLimit: signedPreferences.Message.TargetGasLimit, - Signature: bytesutil.ToBytes96(signedPreferences.Signature), }, slot) msg.ValidatorData = signedPreferences return pubsub.ValidationAccept, nil From 0b7189cdefee35916bafb08a664cffb866fa31f8 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Tue, 2 Jun 2026 12:59:22 -0500 Subject: [PATCH 10/15] applying jun's suggestions --- api/server/structs/endpoints_events.go | 5 + beacon-chain/rpc/eth/events/BUILD.bazel | 1 + beacon-chain/rpc/eth/events/events.go | 5 +- beacon-chain/rpc/eth/events/events_test.go | 35 ++++++ beacon-chain/rpc/eth/validator/BUILD.bazel | 3 +- .../handlers_proposer_preferences_test.go | 116 ------------------ .../rpc/eth/validator/handlers_test.go | 110 +++++++++++++++++ 7 files changed, 156 insertions(+), 119 deletions(-) delete mode 100644 beacon-chain/rpc/eth/validator/handlers_proposer_preferences_test.go diff --git a/api/server/structs/endpoints_events.go b/api/server/structs/endpoints_events.go index bbf040e29d46..7399d93366e7 100644 --- a/api/server/structs/endpoints_events.go +++ b/api/server/structs/endpoints_events.go @@ -113,6 +113,11 @@ type LightClientOptimisticUpdateEvent struct { Data *LightClientOptimisticUpdate `json:"data"` } +type ProposerPreferencesEvent struct { + Version string `json:"version"` + Data *SignedProposerPreferences `json:"data"` +} + type PayloadEvent struct { Slot string `json:"slot"` BlockRoot string `json:"block_root"` diff --git a/beacon-chain/rpc/eth/events/BUILD.bazel b/beacon-chain/rpc/eth/events/BUILD.bazel index 485880a814f6..00a2a2d06757 100644 --- a/beacon-chain/rpc/eth/events/BUILD.bazel +++ b/beacon-chain/rpc/eth/events/BUILD.bazel @@ -48,6 +48,7 @@ go_test( ], embed = [":go_default_library"], deps = [ + "//api/server/structs:go_default_library", "//beacon-chain/blockchain/testing:go_default_library", "//beacon-chain/cache:go_default_library", "//beacon-chain/core/feed:go_default_library", diff --git a/beacon-chain/rpc/eth/events/events.go b/beacon-chain/rpc/eth/events/events.go index 2783101fcdae..1ce97fa16ed0 100644 --- a/beacon-chain/rpc/eth/events/events.go +++ b/beacon-chain/rpc/eth/events/events.go @@ -666,7 +666,10 @@ func (s *Server) lazyReaderForEvent(ctx context.Context, event *feed.Event, topi }, nil case *operation.ProposerPreferencesReceivedData: return func() io.Reader { - return jsonMarshalReader(eventName, structs.SignedProposerPreferencesFromConsensus(v.SignedProposerPreferences)) + return jsonMarshalReader(eventName, &structs.ProposerPreferencesEvent{ + Version: version.String(version.Gloas), + Data: structs.SignedProposerPreferencesFromConsensus(v.SignedProposerPreferences), + }) }, nil case *statefeed.PayloadProcessedData: return func() io.Reader { diff --git a/beacon-chain/rpc/eth/events/events_test.go b/beacon-chain/rpc/eth/events/events_test.go index 6b5d1e35d19c..79529fe67466 100644 --- a/beacon-chain/rpc/eth/events/events_test.go +++ b/beacon-chain/rpc/eth/events/events_test.go @@ -3,6 +3,7 @@ package events import ( "context" "encoding/binary" + "encoding/json" "fmt" "io" "math" @@ -12,6 +13,7 @@ import ( "testing" "time" + "github.com/OffchainLabs/prysm/v7/api/server/structs" mockChain "github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/testing" "github.com/OffchainLabs/prysm/v7/beacon-chain/cache" "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed" @@ -385,6 +387,39 @@ func newStreamTestSync(t *testing.T) *streamTestSync { } } +func TestStreamEvents_ProposerPreferencesWrappedWithVersion(t *testing.T) { + s := &Server{} + topics, err := newTopicRequest([]string{ProposerPreferencesTopic}) + require.NoError(t, err) + ev := &feed.Event{ + Type: operation.ProposerPreferencesReceived, + Data: &operation.ProposerPreferencesReceivedData{ + SignedProposerPreferences: ð.SignedProposerPreferences{ + Message: ð.ProposerPreferences{ + DependentRoot: make([]byte, fieldparams.RootLength), + ProposalSlot: 32, + ValidatorIndex: 7, + FeeRecipient: make([]byte, 20), + TargetGasLimit: 30_000_000, + }, + Signature: make([]byte, fieldparams.BLSSignatureLength), + }, + }, + } + lr, err := s.lazyReaderForEvent(t.Context(), ev, topics) + require.NoError(t, err) + out, err := io.ReadAll(lr()) + require.NoError(t, err) + + _, payload, found := strings.Cut(string(out), "data: ") + require.Equal(t, true, found) + var got structs.ProposerPreferencesEvent + require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(payload)), &got)) + require.Equal(t, "gloas", got.Version) + require.NotNil(t, got.Data) + require.Equal(t, "7", got.Data.Message.ValidatorIndex) +} + func TestStreamEvents_OperationsEvents(t *testing.T) { t.Run("operations", func(t *testing.T) { testSync := newStreamTestSync(t) diff --git a/beacon-chain/rpc/eth/validator/BUILD.bazel b/beacon-chain/rpc/eth/validator/BUILD.bazel index 057a0bc62397..25dfdecafc6f 100644 --- a/beacon-chain/rpc/eth/validator/BUILD.bazel +++ b/beacon-chain/rpc/eth/validator/BUILD.bazel @@ -62,7 +62,6 @@ go_test( srcs = [ "handlers_block_gloas_test.go", "handlers_block_test.go", - "handlers_proposer_preferences_test.go", "handlers_test.go", ], embed = [":go_default_library"], @@ -85,6 +84,7 @@ go_test( "//beacon-chain/rpc/eth/rewards/testing:go_default_library", "//beacon-chain/rpc/eth/shared/testing:go_default_library", "//beacon-chain/rpc/lookup:go_default_library", + "//beacon-chain/rpc/prysm/v1alpha1/validator:go_default_library", "//beacon-chain/rpc/testutil:go_default_library", "//beacon-chain/state:go_default_library", "//beacon-chain/state/stategen:go_default_library", @@ -111,7 +111,6 @@ go_test( "@com_github_sirupsen_logrus//hooks/test:go_default_library", "@org_golang_google_grpc//codes:go_default_library", "@org_golang_google_grpc//status:go_default_library", - "@org_golang_google_protobuf//types/known/emptypb:go_default_library", "@org_uber_go_mock//gomock:go_default_library", ], ) diff --git a/beacon-chain/rpc/eth/validator/handlers_proposer_preferences_test.go b/beacon-chain/rpc/eth/validator/handlers_proposer_preferences_test.go deleted file mode 100644 index d312b4de94bb..000000000000 --- a/beacon-chain/rpc/eth/validator/handlers_proposer_preferences_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package validator - -import ( - "bytes" - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/OffchainLabs/prysm/v7/network/httputil" - eth "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" - "github.com/OffchainLabs/prysm/v7/testing/assert" - mock2 "github.com/OffchainLabs/prysm/v7/testing/mock" - "github.com/OffchainLabs/prysm/v7/testing/require" - "go.uber.org/mock/gomock" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/emptypb" -) - -func validProposerPreferencesBody() string { - return `[{ - "message": { - "dependent_root": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "proposal_slot": "32", - "validator_index": "2", - "fee_recipient": "0x0000000000000000000000000000000000000000", - "target_gas_limit": "30000000" - }, - "signature": "0x` + strings.Repeat("00", 96) + `" - }]` -} - -func TestSubmitSignedProposerPreferences_OK(t *testing.T) { - ctrl := gomock.NewController(t) - v1alpha1Server := mock2.NewMockBeaconNodeValidatorServer(ctrl) - v1alpha1Server.EXPECT(). - SubmitSignedProposerPreferences(gomock.Any(), gomock.AssignableToTypeOf(ð.SubmitSignedProposerPreferencesRequest{})). - DoAndReturn(func(_ context.Context, req *eth.SubmitSignedProposerPreferencesRequest) (*emptypb.Empty, error) { - require.Equal(t, 1, len(req.SignedProposerPreferences)) - require.Equal(t, uint64(30_000_000), req.SignedProposerPreferences[0].Message.TargetGasLimit) - return &emptypb.Empty{}, nil - }) - - s := &Server{V1Alpha1Server: v1alpha1Server} - req := httptest.NewRequest(http.MethodPost, "http://example.com/eth/v1/validator/proposer_preferences", bytes.NewBufferString(validProposerPreferencesBody())) - w := httptest.NewRecorder() - w.Body = &bytes.Buffer{} - - s.SubmitSignedProposerPreferences(w, req) - assert.Equal(t, http.StatusOK, w.Code) -} - -func TestSubmitSignedProposerPreferences_NoBody(t *testing.T) { - s := &Server{} - req := httptest.NewRequest(http.MethodPost, "http://example.com", nil) - w := httptest.NewRecorder() - w.Body = &bytes.Buffer{} - - s.SubmitSignedProposerPreferences(w, req) - assert.Equal(t, http.StatusBadRequest, w.Code) - e := &httputil.DefaultJsonError{} - require.NoError(t, json.Unmarshal(w.Body.Bytes(), e)) - assert.Equal(t, true, strings.Contains(e.Message, "No data submitted")) -} - -func TestSubmitSignedProposerPreferences_Empty(t *testing.T) { - s := &Server{} - req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString("[]")) - w := httptest.NewRecorder() - w.Body = &bytes.Buffer{} - - s.SubmitSignedProposerPreferences(w, req) - assert.Equal(t, http.StatusBadRequest, w.Code) -} - -func TestSubmitSignedProposerPreferences_InvalidJSON(t *testing.T) { - s := &Server{} - req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString(`[{"message": null, "signature": "0x00"}]`)) - w := httptest.NewRecorder() - w.Body = &bytes.Buffer{} - - s.SubmitSignedProposerPreferences(w, req) - assert.Equal(t, http.StatusBadRequest, w.Code) -} - -func runWithGRPCError(t *testing.T, code codes.Code) int { - t.Helper() - ctrl := gomock.NewController(t) - v1alpha1Server := mock2.NewMockBeaconNodeValidatorServer(ctrl) - v1alpha1Server.EXPECT(). - SubmitSignedProposerPreferences(gomock.Any(), gomock.Any()). - Return(nil, status.Error(code, "boom")) - - s := &Server{V1Alpha1Server: v1alpha1Server} - req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString(validProposerPreferencesBody())) - w := httptest.NewRecorder() - w.Body = &bytes.Buffer{} - - s.SubmitSignedProposerPreferences(w, req) - return w.Code -} - -func TestSubmitSignedProposerPreferences_InvalidArgumentMapsTo400(t *testing.T) { - assert.Equal(t, http.StatusBadRequest, runWithGRPCError(t, codes.InvalidArgument)) -} - -func TestSubmitSignedProposerPreferences_UnavailableMapsTo503(t *testing.T) { - assert.Equal(t, http.StatusServiceUnavailable, runWithGRPCError(t, codes.Unavailable)) -} - -func TestSubmitSignedProposerPreferences_InternalMapsTo500(t *testing.T) { - assert.Equal(t, http.StatusInternalServerError, runWithGRPCError(t, codes.Internal)) -} diff --git a/beacon-chain/rpc/eth/validator/handlers_test.go b/beacon-chain/rpc/eth/validator/handlers_test.go index 713e4ae652db..b051fa996dca 100644 --- a/beacon-chain/rpc/eth/validator/handlers_test.go +++ b/beacon-chain/rpc/eth/validator/handlers_test.go @@ -26,6 +26,7 @@ import ( p2pmock "github.com/OffchainLabs/prysm/v7/beacon-chain/p2p/testing" "github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/core" "github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/lookup" + validatorv1alpha1 "github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/prysm/v1alpha1/validator" "github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/testutil" "github.com/OffchainLabs/prysm/v7/beacon-chain/state" "github.com/OffchainLabs/prysm/v7/beacon-chain/state/stategen" @@ -40,6 +41,7 @@ import ( ethpbalpha "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v7/runtime/version" "github.com/OffchainLabs/prysm/v7/testing/assert" + mock2 "github.com/OffchainLabs/prysm/v7/testing/mock" "github.com/OffchainLabs/prysm/v7/testing/require" "github.com/OffchainLabs/prysm/v7/testing/util" "github.com/OffchainLabs/prysm/v7/time/slots" @@ -47,6 +49,9 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" logTest "github.com/sirupsen/logrus/hooks/test" + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) func TestGetAggregateAttestationV2(t *testing.T) { @@ -4535,3 +4540,108 @@ var ( "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" }]` ) + +func validProposerPreferencesBody() string { + return `[{ + "message": { + "dependent_root": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "proposal_slot": "32", + "validator_index": "2", + "fee_recipient": "0x0000000000000000000000000000000000000000", + "target_gas_limit": "30000000" + }, + "signature": "0x` + strings.Repeat("00", 96) + `" + }]` +} + +func TestSubmitSignedProposerPreferences_OK(t *testing.T) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.GloasForkEpoch = 1 + params.OverrideBeaconConfig(cfg) + + currentSlot := primitives.Slot(31) + proposalSlot := primitives.Slot(32) + c := cache.NewProposerPreferencesCache() + v1alpha1Server := &validatorv1alpha1.Server{ + SyncChecker: &mockSync.Sync{IsSyncing: false}, + TimeFetcher: &mockChain.ChainService{Slot: ¤tSlot}, + P2P: &p2pmock.MockBroadcaster{}, + ProposerPreferencesCache: c, + } + + s := &Server{V1Alpha1Server: v1alpha1Server} + req := httptest.NewRequest(http.MethodPost, "http://example.com/eth/v1/validator/proposer_preferences", bytes.NewBufferString(validProposerPreferencesBody())) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.SubmitSignedProposerPreferences(w, req) + assert.Equal(t, http.StatusOK, w.Code) + + pref, ok := c.Get(bytesutil.ToBytes32(bytes.Repeat([]byte{0xcc}, 32)), proposalSlot) + require.Equal(t, true, ok) + require.Equal(t, primitives.ValidatorIndex(2), pref.ValidatorIndex) + require.Equal(t, uint64(30_000_000), pref.TargetGasLimit) +} + +func TestSubmitSignedProposerPreferences_NoBody(t *testing.T) { + s := &Server{} + req := httptest.NewRequest(http.MethodPost, "http://example.com", nil) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.SubmitSignedProposerPreferences(w, req) + assert.Equal(t, http.StatusBadRequest, w.Code) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), e)) + assert.Equal(t, true, strings.Contains(e.Message, "No data submitted")) +} + +func TestSubmitSignedProposerPreferences_Empty(t *testing.T) { + s := &Server{} + req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString("[]")) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.SubmitSignedProposerPreferences(w, req) + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestSubmitSignedProposerPreferences_InvalidJSON(t *testing.T) { + s := &Server{} + req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString(`[{"message": null, "signature": "0x00"}]`)) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.SubmitSignedProposerPreferences(w, req) + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func runWithGRPCError(t *testing.T, code codes.Code) int { + t.Helper() + ctrl := gomock.NewController(t) + v1alpha1Server := mock2.NewMockBeaconNodeValidatorServer(ctrl) + v1alpha1Server.EXPECT(). + SubmitSignedProposerPreferences(gomock.Any(), gomock.Any()). + Return(nil, status.Error(code, "boom")) + + s := &Server{V1Alpha1Server: v1alpha1Server} + req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString(validProposerPreferencesBody())) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.SubmitSignedProposerPreferences(w, req) + return w.Code +} + +func TestSubmitSignedProposerPreferences_InvalidArgumentMapsTo400(t *testing.T) { + assert.Equal(t, http.StatusBadRequest, runWithGRPCError(t, codes.InvalidArgument)) +} + +func TestSubmitSignedProposerPreferences_UnavailableMapsTo503(t *testing.T) { + assert.Equal(t, http.StatusServiceUnavailable, runWithGRPCError(t, codes.Unavailable)) +} + +func TestSubmitSignedProposerPreferences_InternalMapsTo500(t *testing.T) { + assert.Equal(t, http.StatusInternalServerError, runWithGRPCError(t, codes.Internal)) +} From 777c71f21e43d1ffb3a1743e4f49332c62c14aaf Mon Sep 17 00:00:00 2001 From: james-prysm Date: Tue, 2 Jun 2026 22:17:36 -0500 Subject: [PATCH 11/15] missed error handling --- beacon-chain/rpc/eth/validator/BUILD.bazel | 1 + beacon-chain/rpc/eth/validator/handlers.go | 28 +++++++++- .../rpc/eth/validator/handlers_test.go | 51 +++++++++++++++++++ .../client/beacon-api/proposer_preferences.go | 4 +- .../beacon-api/proposer_preferences_test.go | 4 +- 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/beacon-chain/rpc/eth/validator/BUILD.bazel b/beacon-chain/rpc/eth/validator/BUILD.bazel index 25dfdecafc6f..9389eb46b860 100644 --- a/beacon-chain/rpc/eth/validator/BUILD.bazel +++ b/beacon-chain/rpc/eth/validator/BUILD.bazel @@ -67,6 +67,7 @@ go_test( embed = [":go_default_library"], deps = [ "//api:go_default_library", + "//api/server:go_default_library", "//api/server/structs:go_default_library", "//beacon-chain/blockchain/kzg:go_default_library", "//beacon-chain/blockchain/testing:go_default_library", diff --git a/beacon-chain/rpc/eth/validator/handlers.go b/beacon-chain/rpc/eth/validator/handlers.go index 38ca65ce3433..bdf25d2a0723 100644 --- a/beacon-chain/rpc/eth/validator/handlers.go +++ b/beacon-chain/rpc/eth/validator/handlers.go @@ -222,6 +222,21 @@ func (s *Server) SubmitSignedProposerPreferences(w http.ResponseWriter, r *http. ctx, span := trace.StartSpan(r.Context(), "validator.SubmitSignedProposerPreferences") defer span.End() + versionHeader := r.Header.Get(api.VersionHeader) + if versionHeader == "" { + httputil.HandleError(w, api.VersionHeader+" header is required", http.StatusBadRequest) + return + } + v, err := version.FromString(versionHeader) + if err != nil { + httputil.HandleError(w, "Invalid version: "+err.Error(), http.StatusBadRequest) + return + } + if v < version.Gloas { + httputil.HandleError(w, "Signed proposer preferences are only supported from the gloas fork", http.StatusBadRequest) + return + } + var data []*structs.SignedProposerPreferences if err := json.NewDecoder(r.Body).Decode(&data); err != nil { if errors.Is(err, io.EOF) { @@ -239,14 +254,23 @@ func (s *Server) SubmitSignedProposerPreferences(w http.ResponseWriter, r *http. req := ðpbalpha.SubmitSignedProposerPreferencesRequest{ SignedProposerPreferences: make([]*ethpbalpha.SignedProposerPreferences, len(data)), } + var failures []*server.IndexedError for i, item := range data { consensusItem, err := item.ToConsensus() if err != nil { - httputil.HandleError(w, fmt.Sprintf("Could not convert signed proposer preferences at index %d: %s", i, err.Error()), http.StatusBadRequest) - return + failures = append(failures, &server.IndexedError{Index: i, Message: err.Error()}) + continue } req.SignedProposerPreferences[i] = consensusItem } + if len(failures) > 0 { + httputil.WriteError(w, &server.IndexedErrorContainer{ + Code: http.StatusBadRequest, + Message: server.ErrIndexedValidationFail, + Failures: failures, + }) + return + } if _, err := s.V1Alpha1Server.SubmitSignedProposerPreferences(ctx, req); err != nil { if st, ok := status.FromError(err); ok { diff --git a/beacon-chain/rpc/eth/validator/handlers_test.go b/beacon-chain/rpc/eth/validator/handlers_test.go index b051fa996dca..22ce0fcc8aec 100644 --- a/beacon-chain/rpc/eth/validator/handlers_test.go +++ b/beacon-chain/rpc/eth/validator/handlers_test.go @@ -13,6 +13,7 @@ import ( "github.com/OffchainLabs/go-bitfield" "github.com/OffchainLabs/prysm/v7/api" + "github.com/OffchainLabs/prysm/v7/api/server" "github.com/OffchainLabs/prysm/v7/api/server/structs" mockChain "github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/testing" builderTest "github.com/OffchainLabs/prysm/v7/beacon-chain/builder/testing" @@ -4572,6 +4573,7 @@ func TestSubmitSignedProposerPreferences_OK(t *testing.T) { s := &Server{V1Alpha1Server: v1alpha1Server} req := httptest.NewRequest(http.MethodPost, "http://example.com/eth/v1/validator/proposer_preferences", bytes.NewBufferString(validProposerPreferencesBody())) + req.Header.Set(api.VersionHeader, version.String(version.Gloas)) w := httptest.NewRecorder() w.Body = &bytes.Buffer{} @@ -4587,6 +4589,7 @@ func TestSubmitSignedProposerPreferences_OK(t *testing.T) { func TestSubmitSignedProposerPreferences_NoBody(t *testing.T) { s := &Server{} req := httptest.NewRequest(http.MethodPost, "http://example.com", nil) + req.Header.Set(api.VersionHeader, version.String(version.Gloas)) w := httptest.NewRecorder() w.Body = &bytes.Buffer{} @@ -4600,6 +4603,7 @@ func TestSubmitSignedProposerPreferences_NoBody(t *testing.T) { func TestSubmitSignedProposerPreferences_Empty(t *testing.T) { s := &Server{} req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString("[]")) + req.Header.Set(api.VersionHeader, version.String(version.Gloas)) w := httptest.NewRecorder() w.Body = &bytes.Buffer{} @@ -4610,11 +4614,16 @@ func TestSubmitSignedProposerPreferences_Empty(t *testing.T) { func TestSubmitSignedProposerPreferences_InvalidJSON(t *testing.T) { s := &Server{} req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString(`[{"message": null, "signature": "0x00"}]`)) + req.Header.Set(api.VersionHeader, version.String(version.Gloas)) w := httptest.NewRecorder() w.Body = &bytes.Buffer{} s.SubmitSignedProposerPreferences(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) + e := &server.IndexedErrorContainer{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), e)) + require.Equal(t, 1, len(e.Failures)) + assert.Equal(t, 0, e.Failures[0].Index) } func runWithGRPCError(t *testing.T, code codes.Code) int { @@ -4627,6 +4636,7 @@ func runWithGRPCError(t *testing.T, code codes.Code) int { s := &Server{V1Alpha1Server: v1alpha1Server} req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString(validProposerPreferencesBody())) + req.Header.Set(api.VersionHeader, version.String(version.Gloas)) w := httptest.NewRecorder() w.Body = &bytes.Buffer{} @@ -4645,3 +4655,44 @@ func TestSubmitSignedProposerPreferences_UnavailableMapsTo503(t *testing.T) { func TestSubmitSignedProposerPreferences_InternalMapsTo500(t *testing.T) { assert.Equal(t, http.StatusInternalServerError, runWithGRPCError(t, codes.Internal)) } + +func TestSubmitSignedProposerPreferences_MissingVersionHeader(t *testing.T) { + s := &Server{} + req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString(validProposerPreferencesBody())) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.SubmitSignedProposerPreferences(w, req) + assert.Equal(t, http.StatusBadRequest, w.Code) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), e)) + assert.Equal(t, true, strings.Contains(e.Message, api.VersionHeader+" header is required")) +} + +func TestSubmitSignedProposerPreferences_InvalidVersionHeader(t *testing.T) { + s := &Server{} + req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString(validProposerPreferencesBody())) + req.Header.Set(api.VersionHeader, "notaversion") + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.SubmitSignedProposerPreferences(w, req) + assert.Equal(t, http.StatusBadRequest, w.Code) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), e)) + assert.Equal(t, true, strings.Contains(e.Message, "Invalid version")) +} + +func TestSubmitSignedProposerPreferences_PreGloasVersion(t *testing.T) { + s := &Server{} + req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString(validProposerPreferencesBody())) + req.Header.Set(api.VersionHeader, version.String(version.Fulu)) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + + s.SubmitSignedProposerPreferences(w, req) + assert.Equal(t, http.StatusBadRequest, w.Code) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), e)) + assert.Equal(t, true, strings.Contains(e.Message, "only supported from the gloas fork")) +} diff --git a/validator/client/beacon-api/proposer_preferences.go b/validator/client/beacon-api/proposer_preferences.go index 9080315293b9..ded2c8bdd45d 100644 --- a/validator/client/beacon-api/proposer_preferences.go +++ b/validator/client/beacon-api/proposer_preferences.go @@ -8,6 +8,7 @@ import ( "github.com/OffchainLabs/prysm/v7/api/server/structs" ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v7/runtime/version" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" ) @@ -35,5 +36,6 @@ func (c *beaconApiValidatorClient) submitSignedProposerPreferences(ctx context.C return errors.Wrap(err, "failed to marshal signed proposer preferences") } - return c.handler.Post(ctx, "/eth/v1/validator/proposer_preferences", nil, bytes.NewBuffer(body), nil) + headers := map[string]string{"Eth-Consensus-Version": version.String(version.Gloas)} + return c.handler.Post(ctx, "/eth/v1/validator/proposer_preferences", headers, bytes.NewBuffer(body), nil) } diff --git a/validator/client/beacon-api/proposer_preferences_test.go b/validator/client/beacon-api/proposer_preferences_test.go index 5ffdc2f7e642..c412b8f65250 100644 --- a/validator/client/beacon-api/proposer_preferences_test.go +++ b/validator/client/beacon-api/proposer_preferences_test.go @@ -42,7 +42,7 @@ func TestSubmitSignedProposerPreferences_Valid(t *testing.T) { handler.EXPECT().Post( gomock.Any(), proposerPreferencesEndpoint, - nil, + map[string]string{"Eth-Consensus-Version": "gloas"}, bytes.NewBuffer(body), nil, ).Return(nil).Times(1) @@ -69,7 +69,7 @@ func TestSubmitSignedProposerPreferences_HandlerError(t *testing.T) { handler.EXPECT().Post( gomock.Any(), proposerPreferencesEndpoint, - nil, + map[string]string{"Eth-Consensus-Version": "gloas"}, gomock.Any(), nil, ).Return(errors.New("foo error")).Times(1) From a0155dab3b16a0c0024b80af767d99b10f97346f Mon Sep 17 00:00:00 2001 From: james-prysm <90280386+james-prysm@users.noreply.github.com> Date: Mon, 8 Jun 2026 09:16:54 -0500 Subject: [PATCH 12/15] Update validator/client/beacon-api/proposer_preferences.go Co-authored-by: Jun Song <87601811+syjn99@users.noreply.github.com> --- validator/client/beacon-api/proposer_preferences.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/validator/client/beacon-api/proposer_preferences.go b/validator/client/beacon-api/proposer_preferences.go index ded2c8bdd45d..467f63dfab5a 100644 --- a/validator/client/beacon-api/proposer_preferences.go +++ b/validator/client/beacon-api/proposer_preferences.go @@ -19,16 +19,7 @@ func (c *beaconApiValidatorClient) submitSignedProposerPreferences(ctx context.C if p == nil || p.Message == nil { return errors.Errorf("signed proposer preferences at index %d is nil", i) } - jsonPrefs[i] = &structs.SignedProposerPreferences{ - Message: &structs.ProposerPreferences{ - DependentRoot: hexutil.Encode(p.Message.DependentRoot), - ProposalSlot: strconv.FormatUint(uint64(p.Message.ProposalSlot), 10), - ValidatorIndex: strconv.FormatUint(uint64(p.Message.ValidatorIndex), 10), - FeeRecipient: hexutil.Encode(p.Message.FeeRecipient), - TargetGasLimit: strconv.FormatUint(p.Message.TargetGasLimit, 10), - }, - Signature: hexutil.Encode(p.Signature), - } + jsonPrefs[i] = structs.SignedProposerPreferencesFromConsensus(p) } body, err := json.Marshal(jsonPrefs) From 17a67945aa1b29d5f7388971bc046a0601f6ca23 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Mon, 8 Jun 2026 09:51:41 -0500 Subject: [PATCH 13/15] Remove unused imports in proposer_preferences.go --- validator/client/beacon-api/proposer_preferences.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/validator/client/beacon-api/proposer_preferences.go b/validator/client/beacon-api/proposer_preferences.go index 467f63dfab5a..57f9da731e29 100644 --- a/validator/client/beacon-api/proposer_preferences.go +++ b/validator/client/beacon-api/proposer_preferences.go @@ -4,12 +4,10 @@ import ( "bytes" "context" "encoding/json" - "strconv" "github.com/OffchainLabs/prysm/v7/api/server/structs" ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v7/runtime/version" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" ) From e6997b2b0ebea61b782163d37b863002b1d55d08 Mon Sep 17 00:00:00 2001 From: james-prysm <90280386+james-prysm@users.noreply.github.com> Date: Mon, 8 Jun 2026 09:53:19 -0500 Subject: [PATCH 14/15] Update beacon-chain/core/feed/operation/events.go Co-authored-by: Jun Song <87601811+syjn99@users.noreply.github.com> --- beacon-chain/core/feed/operation/events.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon-chain/core/feed/operation/events.go b/beacon-chain/core/feed/operation/events.go index 2ccbfe259520..e654780e9163 100644 --- a/beacon-chain/core/feed/operation/events.go +++ b/beacon-chain/core/feed/operation/events.go @@ -140,5 +140,5 @@ type ExecutionPayloadGossipReceivedData struct { // ProposerPreferencesReceivedData is the data sent with ProposerPreferencesReceived events. type ProposerPreferencesReceivedData struct { - SignedProposerPreferences *ethpb.SignedProposerPreferences + Data *ethpb.SignedProposerPreferences } From 634ab62a9b80ef5d4d0d68d990e1ee43f52a039e Mon Sep 17 00:00:00 2001 From: james-prysm Date: Mon, 8 Jun 2026 10:12:14 -0500 Subject: [PATCH 15/15] fixing jun's suggestions --- beacon-chain/rpc/eth/events/events.go | 5 ++- beacon-chain/rpc/eth/events/events_test.go | 11 ++++-- beacon-chain/sync/BUILD.bazel | 2 + .../subscriber_signed_proposer_preferences.go | 27 ++++++++++++++ ...criber_signed_proposer_preferences_test.go | 37 +++++++++++++++++++ .../validate_signed_proposer_preferences.go | 17 --------- ...lidate_signed_proposer_preferences_test.go | 14 ------- 7 files changed, 77 insertions(+), 36 deletions(-) create mode 100644 beacon-chain/sync/subscriber_signed_proposer_preferences.go create mode 100644 beacon-chain/sync/subscriber_signed_proposer_preferences_test.go diff --git a/beacon-chain/rpc/eth/events/events.go b/beacon-chain/rpc/eth/events/events.go index 70810aa56a28..1367b4acf2be 100644 --- a/beacon-chain/rpc/eth/events/events.go +++ b/beacon-chain/rpc/eth/events/events.go @@ -680,9 +680,10 @@ func (s *Server) lazyReaderForEvent(ctx context.Context, event *feed.Event, topi }, nil case *operation.ProposerPreferencesReceivedData: return func() io.Reader { + epoch := slots.ToEpoch(v.Data.Message.ProposalSlot) return jsonMarshalReader(eventName, &structs.ProposerPreferencesEvent{ - Version: version.String(version.Gloas), - Data: structs.SignedProposerPreferencesFromConsensus(v.SignedProposerPreferences), + Version: version.String(params.GetNetworkScheduleEntry(epoch).VersionEnum), + Data: structs.SignedProposerPreferencesFromConsensus(v.Data), }) }, nil case *statefeed.ExecutionPayloadAvailableData: diff --git a/beacon-chain/rpc/eth/events/events_test.go b/beacon-chain/rpc/eth/events/events_test.go index 807f8b180b1f..180bb5d4529d 100644 --- a/beacon-chain/rpc/eth/events/events_test.go +++ b/beacon-chain/rpc/eth/events/events_test.go @@ -335,7 +335,7 @@ func operationEventsFixtures(t *testing.T) (*topicRequest, []*feed.Event) { { Type: operation.ProposerPreferencesReceived, Data: &operation.ProposerPreferencesReceivedData{ - SignedProposerPreferences: ð.SignedProposerPreferences{ + Data: ð.SignedProposerPreferences{ Message: ð.ProposerPreferences{ DependentRoot: make([]byte, fieldparams.RootLength), ProposalSlot: 32, @@ -398,13 +398,18 @@ func newStreamTestSync(t *testing.T) *streamTestSync { } func TestStreamEvents_ProposerPreferencesWrappedWithVersion(t *testing.T) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.GloasForkEpoch = 0 + params.OverrideBeaconConfig(cfg) + s := &Server{} topics, err := newTopicRequest([]string{ProposerPreferencesTopic}) require.NoError(t, err) ev := &feed.Event{ Type: operation.ProposerPreferencesReceived, Data: &operation.ProposerPreferencesReceivedData{ - SignedProposerPreferences: ð.SignedProposerPreferences{ + Data: ð.SignedProposerPreferences{ Message: ð.ProposerPreferences{ DependentRoot: make([]byte, fieldparams.RootLength), ProposalSlot: 32, @@ -817,7 +822,7 @@ func TestStuckReaderScenarios(t *testing.T) { func wedgedWriterTestCase(t *testing.T, queueDepth func([]*feed.Event) int) { topics, events := operationEventsFixtures(t) - require.Equal(t, 14, len(events)) + require.Equal(t, 15, len(events)) // set eventFeedDepth to a number lower than the events we intend to send to force the server to drop the reader. stn := mockChain.NewEventFeedWrapper() diff --git a/beacon-chain/sync/BUILD.bazel b/beacon-chain/sync/BUILD.bazel index 16c2e21af7d6..8e6c04804de2 100644 --- a/beacon-chain/sync/BUILD.bazel +++ b/beacon-chain/sync/BUILD.bazel @@ -52,6 +52,7 @@ go_library( "subscriber_data_column_sidecar.go", "subscriber_handlers.go", "subscriber_payload_attestation.go", + "subscriber_signed_proposer_preferences.go", "subscriber_sync_committee_message.go", "subscriber_sync_contribution_proof.go", "subscription_topic_handler.go", @@ -218,6 +219,7 @@ go_test( "subscriber_beacon_blocks_test.go", "subscriber_data_column_sidecar_test.go", "subscriber_payload_attestation_test.go", + "subscriber_signed_proposer_preferences_test.go", "subscriber_test.go", "subscription_topic_handler_test.go", "sync_fuzz_test.go", diff --git a/beacon-chain/sync/subscriber_signed_proposer_preferences.go b/beacon-chain/sync/subscriber_signed_proposer_preferences.go new file mode 100644 index 000000000000..f1f6c7c53d04 --- /dev/null +++ b/beacon-chain/sync/subscriber_signed_proposer_preferences.go @@ -0,0 +1,27 @@ +package sync + +import ( + "context" + + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed" + opfeed "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed/operation" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "google.golang.org/protobuf/proto" +) + +func (s *Service) signedProposerPreferencesSubscriber(_ context.Context, msg proto.Message) error { + signedPreferences, ok := msg.(*ethpb.SignedProposerPreferences) + if !ok { + return errWrongMessage + } + if signedPreferences == nil || signedPreferences.Message == nil { + return errNilMessage + } + s.cfg.operationNotifier.OperationFeed().Send(&feed.Event{ + Type: opfeed.ProposerPreferencesReceived, + Data: &opfeed.ProposerPreferencesReceivedData{ + Data: signedPreferences, + }, + }) + return nil +} diff --git a/beacon-chain/sync/subscriber_signed_proposer_preferences_test.go b/beacon-chain/sync/subscriber_signed_proposer_preferences_test.go new file mode 100644 index 000000000000..f859917c95c6 --- /dev/null +++ b/beacon-chain/sync/subscriber_signed_proposer_preferences_test.go @@ -0,0 +1,37 @@ +package sync + +import ( + "testing" + + mock "github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/testing" + fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v7/testing/require" +) + +func TestSignedProposerPreferencesSubscriber_WrongMessage(t *testing.T) { + s := &Service{cfg: &config{}} + err := s.signedProposerPreferencesSubscriber(t.Context(), ðpb.SignedVoluntaryExit{}) + require.ErrorIs(t, err, errWrongMessage) +} + +func TestSignedProposerPreferencesSubscriber_NilMessage(t *testing.T) { + s := &Service{cfg: &config{}} + err := s.signedProposerPreferencesSubscriber(t.Context(), ðpb.SignedProposerPreferences{}) + require.ErrorIs(t, err, errNilMessage) +} + +func TestSignedProposerPreferencesSubscriber_Send(t *testing.T) { + s := &Service{cfg: &config{operationNotifier: &mock.MockOperationNotifier{}}} + msg := ðpb.SignedProposerPreferences{ + Message: ðpb.ProposerPreferences{ + DependentRoot: make([]byte, fieldparams.RootLength), + ProposalSlot: 32, + ValidatorIndex: 7, + FeeRecipient: make([]byte, 20), + TargetGasLimit: 30_000_000, + }, + Signature: make([]byte, fieldparams.BLSSignatureLength), + } + require.NoError(t, s.signedProposerPreferencesSubscriber(t.Context(), msg)) +} diff --git a/beacon-chain/sync/validate_signed_proposer_preferences.go b/beacon-chain/sync/validate_signed_proposer_preferences.go index 3ed5de3080d5..f38cd06e8faa 100644 --- a/beacon-chain/sync/validate_signed_proposer_preferences.go +++ b/beacon-chain/sync/validate_signed_proposer_preferences.go @@ -4,8 +4,6 @@ import ( "context" "github.com/OffchainLabs/prysm/v7/beacon-chain/cache" - "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed" - opfeed "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed/operation" "github.com/OffchainLabs/prysm/v7/beacon-chain/core/transition" "github.com/OffchainLabs/prysm/v7/beacon-chain/p2p" "github.com/OffchainLabs/prysm/v7/beacon-chain/verification" @@ -17,7 +15,6 @@ import ( pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/peer" "github.com/pkg/errors" - "google.golang.org/protobuf/proto" ) func (s *Service) validateSignedProposerPreferencesGossip(ctx context.Context, pid peer.ID, msg *pubsub.Message) (pubsub.ValidationResult, error) { @@ -134,17 +131,3 @@ func (s *Service) validateSignedProposerPreferencesGossip(ctx context.Context, p msg.ValidatorData = signedPreferences return pubsub.ValidationAccept, nil } - -func (s *Service) signedProposerPreferencesSubscriber(_ context.Context, msg proto.Message) error { - signedPreferences, ok := msg.(*ethpb.SignedProposerPreferences) - if !ok { - return errWrongMessage - } - s.cfg.operationNotifier.OperationFeed().Send(&feed.Event{ - Type: opfeed.ProposerPreferencesReceived, - Data: &opfeed.ProposerPreferencesReceivedData{ - SignedProposerPreferences: signedPreferences, - }, - }) - return nil -} diff --git a/beacon-chain/sync/validate_signed_proposer_preferences_test.go b/beacon-chain/sync/validate_signed_proposer_preferences_test.go index 44b005d0ee62..7ea56ddd8eee 100644 --- a/beacon-chain/sync/validate_signed_proposer_preferences_test.go +++ b/beacon-chain/sync/validate_signed_proposer_preferences_test.go @@ -206,20 +206,6 @@ func TestValidateSignedProposerPreferencesGossip_HappyPath(t *testing.T) { require.DeepEqual(t, signedPreferences, validatorData) } -func TestSignedProposerPreferencesSubscriber_WrongMessage(t *testing.T) { - s := &Service{} - err := s.signedProposerPreferencesSubscriber(context.Background(), ðpb.BeaconBlock{}) - require.ErrorIs(t, errWrongMessage, err) -} - -func TestSignedProposerPreferencesSubscriber_HappyPath(t *testing.T) { - s := &Service{ - cfg: &config{operationNotifier: &mock.MockOperationNotifier{}}, - } - err := s.signedProposerPreferencesSubscriber(context.Background(), ðpb.SignedProposerPreferences{}) - require.NoError(t, err) -} - type mockSignedProposerPreferencesVerifier struct { errCurrentOrNextEpoch error errDependentRootSeen error