diff --git a/api/client/builder/client.go b/api/client/builder/client.go index 2e0857ba7d..de291ceb2b 100644 --- a/api/client/builder/client.go +++ b/api/client/builder/client.go @@ -102,6 +102,7 @@ type BuilderClient interface { GetHeader(ctx context.Context, slot primitives.Slot, parentHash [32]byte, pubkey [48]byte) (SignedBid, error) RegisterValidator(ctx context.Context, svr []*ethpb.SignedValidatorRegistrationV1) error SubmitBlindedBlock(ctx context.Context, sb interfaces.ReadOnlySignedBeaconBlock) (interfaces.ExecutionData, v1.BlobsBundler, error) + SubmitBlindedBlockPostFulu(ctx context.Context, sb interfaces.ReadOnlySignedBeaconBlock) error Status(ctx context.Context) error } @@ -152,7 +153,8 @@ func (c *Client) NodeURL() string { type reqOption func(*http.Request) // do is a generic, opinionated request function to reduce boilerplate amongst the methods in this package api/client/builder. -func (c *Client) do(ctx context.Context, method string, path string, body io.Reader, opts ...reqOption) (res []byte, header http.Header, err error) { +// It validates that the HTTP response status matches the expectedStatus parameter. +func (c *Client) do(ctx context.Context, method string, path string, body io.Reader, expectedStatus int, opts ...reqOption) (res []byte, header http.Header, err error) { ctx, span := trace.StartSpan(ctx, "builder.client.do") defer func() { tracing.AnnotateError(span, err) @@ -187,8 +189,8 @@ func (c *Client) do(ctx context.Context, method string, path string, body io.Rea log.WithError(closeErr).Error("Failed to close response body") } }() - if r.StatusCode != http.StatusOK { - err = non200Err(r) + if r.StatusCode != expectedStatus { + err = unexpectedStatusErr(r, expectedStatus) return } res, err = io.ReadAll(io.LimitReader(r.Body, client.MaxBodySize)) @@ -236,7 +238,7 @@ func (c *Client) GetHeader(ctx context.Context, slot primitives.Slot, parentHash r.Header.Set("Accept", api.JsonMediaType) } } - data, header, err := c.do(ctx, http.MethodGet, path, nil, getOpts) + data, header, err := c.do(ctx, http.MethodGet, path, nil, http.StatusOK, getOpts) if err != nil { return nil, errors.Wrap(err, "error getting header from builder server") } @@ -409,7 +411,7 @@ func (c *Client) RegisterValidator(ctx context.Context, svr []*ethpb.SignedValid } } - if _, _, err = c.do(ctx, http.MethodPost, postRegisterValidatorPath, bytes.NewBuffer(body), postOpts); err != nil { + if _, _, err = c.do(ctx, http.MethodPost, postRegisterValidatorPath, bytes.NewBuffer(body), http.StatusOK, postOpts); err != nil { return errors.Wrap(err, "do") } log.WithField("registrationCount", len(svr)).Debug("Successfully registered validator(s) on builder") @@ -471,7 +473,7 @@ func (c *Client) SubmitBlindedBlock(ctx context.Context, sb interfaces.ReadOnlyS // post the blinded block - the execution payload response should contain the unblinded payload, along with the // blobs bundle if it is post deneb. - data, header, err := c.do(ctx, http.MethodPost, postBlindedBeaconBlockPath, bytes.NewBuffer(body), postOpts) + data, header, err := c.do(ctx, http.MethodPost, postBlindedBeaconBlockPath, bytes.NewBuffer(body), http.StatusOK, postOpts) if err != nil { return nil, nil, errors.Wrap(err, "error posting the blinded block to the builder api") } @@ -501,6 +503,24 @@ func (c *Client) SubmitBlindedBlock(ctx context.Context, sb interfaces.ReadOnlyS return ed, blobs, nil } +// SubmitBlindedBlockPostFulu calls the builder API endpoint post-Fulu where relays only return status codes. +// This method is used after the Fulu fork when MEV-boost relays no longer return execution payloads. +func (c *Client) SubmitBlindedBlockPostFulu(ctx context.Context, sb interfaces.ReadOnlySignedBeaconBlock) error { + body, postOpts, err := c.buildBlindedBlockRequest(sb) + if err != nil { + return err + } + + // Post the blinded block - the response should only contain a status code (no payload) + _, _, err = c.do(ctx, http.MethodPost, postBlindedBeaconBlockPath, bytes.NewBuffer(body), http.StatusAccepted, postOpts) + if err != nil { + return errors.Wrap(err, "error posting the blinded block to the builder api post-Fulu") + } + + // Success is indicated by no error (status 202) + return nil +} + func (c *Client) checkBlockVersion(respBytes []byte, header http.Header) (int, error) { var versionHeader string if c.sszEnabled { @@ -657,11 +677,11 @@ func (c *Client) Status(ctx context.Context) error { getOpts := func(r *http.Request) { r.Header.Set("Accept", api.JsonMediaType) } - _, _, err := c.do(ctx, http.MethodGet, getStatus, nil, getOpts) + _, _, err := c.do(ctx, http.MethodGet, getStatus, nil, http.StatusOK, getOpts) return err } -func non200Err(response *http.Response) error { +func unexpectedStatusErr(response *http.Response, expected int) error { bodyBytes, err := io.ReadAll(io.LimitReader(response.Body, client.MaxErrBodySize)) var errMessage ErrorMessage var body string @@ -670,7 +690,7 @@ func non200Err(response *http.Response) error { } else { body = "response body:\n" + string(bodyBytes) } - msg := fmt.Sprintf("code=%d, url=%s, body=%s", response.StatusCode, response.Request.URL, body) + msg := fmt.Sprintf("expected=%d, got=%d, url=%s, body=%s", expected, response.StatusCode, response.Request.URL, body) switch response.StatusCode { case http.StatusUnsupportedMediaType: log.WithError(ErrUnsupportedMediaType).Debug(msg) diff --git a/api/client/builder/client_test.go b/api/client/builder/client_test.go index 14d3d6e83d..e9c3cab7be 100644 --- a/api/client/builder/client_test.go +++ b/api/client/builder/client_test.go @@ -1555,6 +1555,89 @@ func testSignedBlindedBeaconBlockElectra(t *testing.T) *eth.SignedBlindedBeaconB } } +func TestSubmitBlindedBlockPostFulu(t *testing.T) { + ctx := t.Context() + + t.Run("success", func(t *testing.T) { + hc := &http.Client{ + Transport: roundtrip(func(r *http.Request) (*http.Response, error) { + require.Equal(t, postBlindedBeaconBlockPath, r.URL.Path) + require.Equal(t, "bellatrix", r.Header.Get("Eth-Consensus-Version")) + require.Equal(t, api.JsonMediaType, r.Header.Get("Content-Type")) + require.Equal(t, api.JsonMediaType, r.Header.Get("Accept")) + // Post-Fulu: only return status code, no payload + return &http.Response{ + StatusCode: http.StatusAccepted, + Body: io.NopCloser(bytes.NewBufferString("")), + Request: r.Clone(ctx), + }, nil + }), + } + c := &Client{ + hc: hc, + baseURL: &url.URL{Host: "localhost:3500", Scheme: "http"}, + } + sbbb, err := blocks.NewSignedBeaconBlock(testSignedBlindedBeaconBlockBellatrix(t)) + require.NoError(t, err) + err = c.SubmitBlindedBlockPostFulu(ctx, sbbb) + require.NoError(t, err) + }) + + t.Run("success_ssz", func(t *testing.T) { + hc := &http.Client{ + Transport: roundtrip(func(r *http.Request) (*http.Response, error) { + require.Equal(t, postBlindedBeaconBlockPath, r.URL.Path) + require.Equal(t, "bellatrix", r.Header.Get(api.VersionHeader)) + require.Equal(t, api.OctetStreamMediaType, r.Header.Get("Content-Type")) + require.Equal(t, api.OctetStreamMediaType, r.Header.Get("Accept")) + // Post-Fulu: only return status code, no payload + return &http.Response{ + StatusCode: http.StatusAccepted, + Body: io.NopCloser(bytes.NewBufferString("")), + Request: r.Clone(ctx), + }, nil + }), + } + c := &Client{ + hc: hc, + baseURL: &url.URL{Host: "localhost:3500", Scheme: "http"}, + sszEnabled: true, + } + sbbb, err := blocks.NewSignedBeaconBlock(testSignedBlindedBeaconBlockBellatrix(t)) + require.NoError(t, err) + err = c.SubmitBlindedBlockPostFulu(ctx, sbbb) + require.NoError(t, err) + }) + + t.Run("error_response", func(t *testing.T) { + hc := &http.Client{ + Transport: roundtrip(func(r *http.Request) (*http.Response, error) { + require.Equal(t, postBlindedBeaconBlockPath, r.URL.Path) + require.Equal(t, "bellatrix", r.Header.Get("Eth-Consensus-Version")) + message := ErrorMessage{ + Code: 400, + Message: "Bad Request", + } + resp, err := json.Marshal(message) + require.NoError(t, err) + return &http.Response{ + StatusCode: http.StatusBadRequest, + Body: io.NopCloser(bytes.NewBuffer(resp)), + Request: r.Clone(ctx), + }, nil + }), + } + c := &Client{ + hc: hc, + baseURL: &url.URL{Host: "localhost:3500", Scheme: "http"}, + } + sbbb, err := blocks.NewSignedBeaconBlock(testSignedBlindedBeaconBlockBellatrix(t)) + require.NoError(t, err) + err = c.SubmitBlindedBlockPostFulu(ctx, sbbb) + require.ErrorIs(t, err, ErrNotOK) + }) +} + func TestRequestLogger(t *testing.T) { wo := WithObserver(&requestLogger{}) c, err := NewClient("localhost:3500", wo) diff --git a/api/client/builder/testing/mock.go b/api/client/builder/testing/mock.go index 93854f2d23..fd2d8336a1 100644 --- a/api/client/builder/testing/mock.go +++ b/api/client/builder/testing/mock.go @@ -45,6 +45,11 @@ func (MockClient) SubmitBlindedBlock(_ context.Context, _ interfaces.ReadOnlySig return nil, nil, nil } +// SubmitBlindedBlockPostFulu -- +func (MockClient) SubmitBlindedBlockPostFulu(_ context.Context, _ interfaces.ReadOnlySignedBeaconBlock) error { + return nil +} + // Status -- func (MockClient) Status(_ context.Context) error { return nil diff --git a/api/client/builder/types_test.go b/api/client/builder/types_test.go index ace6ee0999..9dff0c049e 100644 --- a/api/client/builder/types_test.go +++ b/api/client/builder/types_test.go @@ -1699,7 +1699,7 @@ func TestExecutionPayloadHeaderCapellaRoundtrip(t *testing.T) { require.DeepEqual(t, string(expected[0:len(expected)-1]), string(m)) } -func TestErrorMessage_non200Err(t *testing.T) { +func TestErrorMessage_unexpectedStatusErr(t *testing.T) { mockRequest := &http.Request{ URL: &url.URL{Path: "example.com"}, } @@ -1779,7 +1779,7 @@ func TestErrorMessage_non200Err(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := non200Err(tt.args) + err := unexpectedStatusErr(tt.args, http.StatusOK) if err != nil && tt.wantMessage != "" { require.ErrorContains(t, tt.wantMessage, err) } diff --git a/beacon-chain/builder/service.go b/beacon-chain/builder/service.go index 7cb86d60c6..26d6f70f5c 100644 --- a/beacon-chain/builder/service.go +++ b/beacon-chain/builder/service.go @@ -25,6 +25,7 @@ var ErrNoBuilder = errors.New("builder endpoint not configured") // BlockBuilder defines the interface for interacting with the block builder type BlockBuilder interface { SubmitBlindedBlock(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock) (interfaces.ExecutionData, v1.BlobsBundler, error) + SubmitBlindedBlockPostFulu(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock) error GetHeader(ctx context.Context, slot primitives.Slot, parentHash [32]byte, pubKey [48]byte) (builder.SignedBid, error) RegisterValidator(ctx context.Context, reg []*ethpb.SignedValidatorRegistrationV1) error RegistrationByValidatorID(ctx context.Context, id primitives.ValidatorIndex) (*ethpb.ValidatorRegistrationV1, error) @@ -101,6 +102,22 @@ func (s *Service) SubmitBlindedBlock(ctx context.Context, b interfaces.ReadOnlyS return s.c.SubmitBlindedBlock(ctx, b) } +// SubmitBlindedBlockPostFulu submits a blinded block to the builder relay network post-Fulu. +// After Fulu, relays only return status codes (no payload). +func (s *Service) SubmitBlindedBlockPostFulu(ctx context.Context, b interfaces.ReadOnlySignedBeaconBlock) error { + ctx, span := trace.StartSpan(ctx, "builder.SubmitBlindedBlockPostFulu") + defer span.End() + start := time.Now() + defer func() { + submitBlindedBlockLatency.Observe(float64(time.Since(start).Milliseconds())) + }() + if s.c == nil { + return ErrNoBuilder + } + + return s.c.SubmitBlindedBlockPostFulu(ctx, b) +} + // GetHeader retrieves the header for a given slot and parent hash from the builder relay network. func (s *Service) GetHeader(ctx context.Context, slot primitives.Slot, parentHash [32]byte, pubKey [48]byte) (builder.SignedBid, error) { ctx, span := trace.StartSpan(ctx, "builder.GetHeader") diff --git a/beacon-chain/builder/testing/mock.go b/beacon-chain/builder/testing/mock.go index 8b1eb12fc2..16282f823c 100644 --- a/beacon-chain/builder/testing/mock.go +++ b/beacon-chain/builder/testing/mock.go @@ -24,21 +24,22 @@ type Config struct { // MockBuilderService to mock builder. type MockBuilderService struct { - HasConfigured bool - Payload *v1.ExecutionPayload - PayloadCapella *v1.ExecutionPayloadCapella - PayloadDeneb *v1.ExecutionPayloadDeneb - BlobBundle *v1.BlobsBundle - BlobBundleV2 *v1.BlobsBundleV2 - ErrSubmitBlindedBlock error - Bid *ethpb.SignedBuilderBid - BidCapella *ethpb.SignedBuilderBidCapella - BidDeneb *ethpb.SignedBuilderBidDeneb - BidElectra *ethpb.SignedBuilderBidElectra - RegistrationCache *cache.RegistrationCache - ErrGetHeader error - ErrRegisterValidator error - Cfg *Config + HasConfigured bool + Payload *v1.ExecutionPayload + PayloadCapella *v1.ExecutionPayloadCapella + PayloadDeneb *v1.ExecutionPayloadDeneb + BlobBundle *v1.BlobsBundle + BlobBundleV2 *v1.BlobsBundleV2 + ErrSubmitBlindedBlock error + ErrSubmitBlindedBlockPostFulu error + Bid *ethpb.SignedBuilderBid + BidCapella *ethpb.SignedBuilderBidCapella + BidDeneb *ethpb.SignedBuilderBidDeneb + BidElectra *ethpb.SignedBuilderBidElectra + RegistrationCache *cache.RegistrationCache + ErrGetHeader error + ErrRegisterValidator error + Cfg *Config } // Configured for mocking. @@ -115,3 +116,8 @@ func (s *MockBuilderService) RegistrationByValidatorID(ctx context.Context, id p func (s *MockBuilderService) RegisterValidator(context.Context, []*ethpb.SignedValidatorRegistrationV1) error { return s.ErrRegisterValidator } + +// SubmitBlindedBlockPostFulu for mocking. +func (s *MockBuilderService) SubmitBlindedBlockPostFulu(_ context.Context, _ interfaces.ReadOnlySignedBeaconBlock) error { + return s.ErrSubmitBlindedBlockPostFulu +} diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer.go b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer.go index b0b7f1fc5a..4db451ab9b 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer.go @@ -286,6 +286,19 @@ func (vs *Server) ProposeBeaconBlock(ctx context.Context, req *ethpb.GenericSign if err != nil { return nil, status.Errorf(codes.InvalidArgument, "%s: %v", "decode block failed", err) } + root, err := block.Block().HashTreeRoot() + if err != nil { + return nil, status.Errorf(codes.Internal, "Could not hash tree root: %v", err) + } + + // For post-Fulu blinded blocks, submit to relay and return early + if block.IsBlinded() && slots.ToEpoch(block.Block().Slot()) >= params.BeaconConfig().FuluForkEpoch { + err := vs.BlockBuilder.SubmitBlindedBlockPostFulu(ctx, block) + if err != nil { + return nil, status.Errorf(codes.Internal, "Could not submit blinded block post-Fulu: %v", err) + } + return ðpb.ProposeResponse{BlockRoot: root[:]}, nil + } var sidecars []*ethpb.BlobSidecar if block.IsBlinded() { @@ -297,11 +310,6 @@ func (vs *Server) ProposeBeaconBlock(ctx context.Context, req *ethpb.GenericSign return nil, status.Errorf(codes.Internal, "%s: %v", "handle block failed", err) } - root, err := block.Block().HashTreeRoot() - if err != nil { - return nil, status.Errorf(codes.Internal, "Could not hash tree root: %v", err) - } - var wg sync.WaitGroup errChan := make(chan error, 1) @@ -327,7 +335,8 @@ func (vs *Server) ProposeBeaconBlock(ctx context.Context, req *ethpb.GenericSign return ðpb.ProposeResponse{BlockRoot: root[:]}, nil } -// handleBlindedBlock processes blinded beacon blocks. +// handleBlindedBlock processes blinded beacon blocks (pre-Fulu only). +// Post-Fulu blinded blocks are handled directly in ProposeBeaconBlock. func (vs *Server) handleBlindedBlock(ctx context.Context, block interfaces.SignedBeaconBlock) (interfaces.SignedBeaconBlock, []*ethpb.BlobSidecar, error) { if block.Version() < version.Bellatrix { return nil, nil, errors.New("pre-Bellatrix blinded block") diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_test.go b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_test.go index be005bb6b0..940a3fad28 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_test.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_test.go @@ -3286,3 +3286,252 @@ func TestProposer_ElectraBlobsAndProofs(t *testing.T) { require.Equal(t, 10, len(blobs)) require.Equal(t, 10, len(proofs)) } + +func TestServer_ProposeBeaconBlock_PostFuluBlindedBlock(t *testing.T) { + db := dbutil.SetupDB(t) + ctx := t.Context() + + beaconState, parentRoot, _ := util.DeterministicGenesisStateWithGenesisBlock(t, ctx, db, 100) + require.NoError(t, beaconState.SetSlot(1)) + + t.Run("post-Fulu blinded block - early return success", func(t *testing.T) { + // Set up config with Fulu fork at epoch 5 + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.FuluForkEpoch = 5 + params.OverrideBeaconConfig(cfg) + + mockBuilder := &builderTest.MockBuilderService{ + HasConfigured: true, + Cfg: &builderTest.Config{BeaconDB: db}, + ErrSubmitBlindedBlockPostFulu: nil, // Success case + } + + c := &mock.ChainService{State: beaconState, Root: parentRoot[:]} + proposerServer := &Server{ + ChainStartFetcher: &mockExecution.Chain{}, + Eth1InfoFetcher: &mockExecution.Chain{}, + Eth1BlockFetcher: &mockExecution.Chain{}, + BlockReceiver: c, + BlobReceiver: c, + HeadFetcher: c, + BlockNotifier: c.BlockNotifier(), + OperationNotifier: c.OperationNotifier(), + StateGen: stategen.New(db, doublylinkedtree.New()), + TimeFetcher: c, + SyncChecker: &mockSync.Sync{IsSyncing: false}, + BeaconDB: db, + BlockBuilder: mockBuilder, + P2P: &mockp2p.MockBroadcaster{}, + } + + // Create a blinded block at slot 160 (epoch 5, which is >= FuluForkEpoch) + blindedBlock := util.NewBlindedBeaconBlockDeneb() + blindedBlock.Message.Slot = 160 // This puts us at epoch 5 (160/32 = 5) + blindedBlock.Message.ProposerIndex = 0 + blindedBlock.Message.ParentRoot = parentRoot[:] + blindedBlock.Message.StateRoot = make([]byte, 32) + + req := ðpb.GenericSignedBeaconBlock{ + Block: ðpb.GenericSignedBeaconBlock_BlindedDeneb{BlindedDeneb: blindedBlock}, + } + + // This should trigger the post-Fulu early return path + res, err := proposerServer.ProposeBeaconBlock(ctx, req) + require.NoError(t, err) + require.NotNil(t, res) + require.NotEmpty(t, res.BlockRoot) + }) + + t.Run("post-Fulu blinded block - builder submission error", func(t *testing.T) { + // Set up config with Fulu fork at epoch 5 + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.FuluForkEpoch = 5 + params.OverrideBeaconConfig(cfg) + + mockBuilder := &builderTest.MockBuilderService{ + HasConfigured: true, + Cfg: &builderTest.Config{BeaconDB: db}, + ErrSubmitBlindedBlockPostFulu: errors.New("post-Fulu builder submission failed"), + } + + c := &mock.ChainService{State: beaconState, Root: parentRoot[:]} + proposerServer := &Server{ + ChainStartFetcher: &mockExecution.Chain{}, + Eth1InfoFetcher: &mockExecution.Chain{}, + Eth1BlockFetcher: &mockExecution.Chain{}, + BlockReceiver: c, + BlobReceiver: c, + HeadFetcher: c, + BlockNotifier: c.BlockNotifier(), + OperationNotifier: c.OperationNotifier(), + StateGen: stategen.New(db, doublylinkedtree.New()), + TimeFetcher: c, + SyncChecker: &mockSync.Sync{IsSyncing: false}, + BeaconDB: db, + BlockBuilder: mockBuilder, + P2P: &mockp2p.MockBroadcaster{}, + } + + // Create a blinded block at slot 160 (epoch 5) + blindedBlock := util.NewBlindedBeaconBlockDeneb() + blindedBlock.Message.Slot = 160 + blindedBlock.Message.ProposerIndex = 0 + blindedBlock.Message.ParentRoot = parentRoot[:] + blindedBlock.Message.StateRoot = make([]byte, 32) + + req := ðpb.GenericSignedBeaconBlock{ + Block: ðpb.GenericSignedBeaconBlock_BlindedDeneb{BlindedDeneb: blindedBlock}, + } + + _, err := proposerServer.ProposeBeaconBlock(ctx, req) + require.ErrorContains(t, "Could not submit blinded block post-Fulu", err) + require.ErrorContains(t, "post-Fulu builder submission failed", err) + }) + + t.Run("pre-Fulu blinded block - uses regular handleBlindedBlock path", func(t *testing.T) { + // Set up config with Fulu fork at epoch 10 (future) + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.FuluForkEpoch = 10 + params.OverrideBeaconConfig(cfg) + + mockBuilder := &builderTest.MockBuilderService{ + HasConfigured: true, + Cfg: &builderTest.Config{BeaconDB: db}, + PayloadDeneb: &enginev1.ExecutionPayloadDeneb{}, + BlobBundle: &enginev1.BlobsBundle{}, + } + + c := &mock.ChainService{State: beaconState, Root: parentRoot[:]} + proposerServer := &Server{ + ChainStartFetcher: &mockExecution.Chain{}, + Eth1InfoFetcher: &mockExecution.Chain{}, + Eth1BlockFetcher: &mockExecution.Chain{}, + BlockReceiver: c, + BlobReceiver: c, + HeadFetcher: c, + BlockNotifier: c.BlockNotifier(), + OperationNotifier: c.OperationNotifier(), + StateGen: stategen.New(db, doublylinkedtree.New()), + TimeFetcher: c, + SyncChecker: &mockSync.Sync{IsSyncing: false}, + BeaconDB: db, + BlockBuilder: mockBuilder, + P2P: &mockp2p.MockBroadcaster{}, + } + + // Create a blinded block at slot 160 (epoch 5, which is < FuluForkEpoch=10) + blindedBlock := util.NewBlindedBeaconBlockDeneb() + blindedBlock.Message.Slot = 160 + blindedBlock.Message.ProposerIndex = 0 + blindedBlock.Message.ParentRoot = parentRoot[:] + blindedBlock.Message.StateRoot = make([]byte, 32) + + req := ðpb.GenericSignedBeaconBlock{ + Block: ðpb.GenericSignedBeaconBlock_BlindedDeneb{BlindedDeneb: blindedBlock}, + } + + // This should NOT trigger the post-Fulu early return path, but use handleBlindedBlock instead + res, err := proposerServer.ProposeBeaconBlock(ctx, req) + require.NoError(t, err) + require.NotNil(t, res) + require.NotEmpty(t, res.BlockRoot) + }) + + t.Run("boundary test - exactly at Fulu fork epoch", func(t *testing.T) { + // Set up config with Fulu fork at epoch 5 + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.FuluForkEpoch = 5 + params.OverrideBeaconConfig(cfg) + + mockBuilder := &builderTest.MockBuilderService{ + HasConfigured: true, + Cfg: &builderTest.Config{BeaconDB: db}, + ErrSubmitBlindedBlockPostFulu: nil, + } + + c := &mock.ChainService{State: beaconState, Root: parentRoot[:]} + proposerServer := &Server{ + ChainStartFetcher: &mockExecution.Chain{}, + Eth1InfoFetcher: &mockExecution.Chain{}, + Eth1BlockFetcher: &mockExecution.Chain{}, + BlockReceiver: c, + BlobReceiver: c, + HeadFetcher: c, + BlockNotifier: c.BlockNotifier(), + OperationNotifier: c.OperationNotifier(), + StateGen: stategen.New(db, doublylinkedtree.New()), + TimeFetcher: c, + SyncChecker: &mockSync.Sync{IsSyncing: false}, + BeaconDB: db, + BlockBuilder: mockBuilder, + P2P: &mockp2p.MockBroadcaster{}, + } + + // Create a blinded block at slot 160 (exactly epoch 5) + blindedBlock := util.NewBlindedBeaconBlockDeneb() + blindedBlock.Message.Slot = 160 // 160/32 = 5 (exactly at FuluForkEpoch) + blindedBlock.Message.ProposerIndex = 0 + blindedBlock.Message.ParentRoot = parentRoot[:] + blindedBlock.Message.StateRoot = make([]byte, 32) + + req := ðpb.GenericSignedBeaconBlock{ + Block: ðpb.GenericSignedBeaconBlock_BlindedDeneb{BlindedDeneb: blindedBlock}, + } + + // Should trigger post-Fulu path since epoch 5 >= FuluForkEpoch (5) + res, err := proposerServer.ProposeBeaconBlock(ctx, req) + require.NoError(t, err) + require.NotNil(t, res) + require.NotEmpty(t, res.BlockRoot) + }) + + t.Run("unblinded block - not affected by post-Fulu condition", func(t *testing.T) { + // Set up config with Fulu fork at epoch 5 + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.FuluForkEpoch = 5 + params.OverrideBeaconConfig(cfg) + + c := &mock.ChainService{State: beaconState, Root: parentRoot[:]} + proposerServer := &Server{ + ChainStartFetcher: &mockExecution.Chain{}, + Eth1InfoFetcher: &mockExecution.Chain{}, + Eth1BlockFetcher: &mockExecution.Chain{}, + BlockReceiver: c, + BlobReceiver: c, + HeadFetcher: c, + BlockNotifier: c.BlockNotifier(), + OperationNotifier: c.OperationNotifier(), + StateGen: stategen.New(db, doublylinkedtree.New()), + TimeFetcher: c, + SyncChecker: &mockSync.Sync{IsSyncing: false}, + BeaconDB: db, + P2P: &mockp2p.MockBroadcaster{}, + } + + // Create an unblinded block at slot 160 (epoch 5) + unblindeBlock := util.NewBeaconBlockDeneb() + unblindeBlock.Block.Slot = 160 + unblindeBlock.Block.ProposerIndex = 0 + unblindeBlock.Block.ParentRoot = parentRoot[:] + unblindeBlock.Block.StateRoot = make([]byte, 32) + + req := ðpb.GenericSignedBeaconBlock{ + Block: ðpb.GenericSignedBeaconBlock_Deneb{ + Deneb: ðpb.SignedBeaconBlockContentsDeneb{ + Block: unblindeBlock, + }, + }, + } + + // Unblinded blocks should not trigger post-Fulu condition, even at epoch >= FuluForkEpoch + res, err := proposerServer.ProposeBeaconBlock(ctx, req) + require.NoError(t, err) + require.NotNil(t, res) + require.NotEmpty(t, res.BlockRoot) + }) +} diff --git a/changelog/tt_post-fulu-mev-boost-protocol.md b/changelog/tt_post-fulu-mev-boost-protocol.md new file mode 100644 index 0000000000..363daa931b --- /dev/null +++ b/changelog/tt_post-fulu-mev-boost-protocol.md @@ -0,0 +1,3 @@ +### Added + +- Implement post-Fulu MEV-boost protocol changes where relays only return status codes for blinded block submissions. \ No newline at end of file