diff --git a/beacon-chain/rpc/eth/beacon/handlers.go b/beacon-chain/rpc/eth/beacon/handlers.go index 02d88a1be1..db60578341 100644 --- a/beacon-chain/rpc/eth/beacon/handlers.go +++ b/beacon-chain/rpc/eth/beacon/handlers.go @@ -15,6 +15,9 @@ import ( "github.com/prysmaticlabs/prysm/v4/consensus-types/blocks" "github.com/prysmaticlabs/prysm/v4/consensus-types/interfaces" "github.com/prysmaticlabs/prysm/v4/network" + ethpbv1 "github.com/prysmaticlabs/prysm/v4/proto/eth/v1" + ethpbv2 "github.com/prysmaticlabs/prysm/v4/proto/eth/v2" + "github.com/prysmaticlabs/prysm/v4/proto/migration" eth "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1" ) @@ -38,7 +41,142 @@ func (bs *Server) PublishBlindedBlockV2(w http.ResponseWriter, r *http.Request) if ok := bs.checkSync(r.Context(), w); !ok { return } + isSSZ, err := network.SszRequested(r) + if isSSZ && err == nil { + publishBlindedBlockV2SSZ(bs, w, r) + } else { + publishBlindedBlockV2(bs, w, r) + } +} +func publishBlindedBlockV2SSZ(bs *Server, w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + errJson := &network.DefaultErrorJson{ + Message: "Could not read request body: " + err.Error(), + Code: http.StatusInternalServerError, + } + network.WriteError(w, errJson) + return + } + capellaBlock := ðpbv2.SignedBlindedBeaconBlockCapella{} + if err := capellaBlock.UnmarshalSSZ(body); err == nil { + v1block, err := migration.BlindedCapellaToV1Alpha1SignedBlock(capellaBlock) + if err != nil { + errJson := &network.DefaultErrorJson{ + Message: "Could not decode request body into consensus block: " + err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + genericBlock := ð.GenericSignedBeaconBlock{ + Block: ð.GenericSignedBeaconBlock_BlindedCapella{ + BlindedCapella: v1block, + }, + } + if err = bs.validateBroadcast(r, genericBlock); err != nil { + errJson := &network.DefaultErrorJson{ + Message: err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + bs.proposeBlock(r.Context(), w, genericBlock) + return + } + bellatrixBlock := ðpbv2.SignedBlindedBeaconBlockBellatrix{} + if err := bellatrixBlock.UnmarshalSSZ(body); err == nil { + v1block, err := migration.BlindedBellatrixToV1Alpha1SignedBlock(bellatrixBlock) + if err != nil { + errJson := &network.DefaultErrorJson{ + Message: "Could not decode request body into consensus block: " + err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + genericBlock := ð.GenericSignedBeaconBlock{ + Block: ð.GenericSignedBeaconBlock_BlindedBellatrix{ + BlindedBellatrix: v1block, + }, + } + if err = bs.validateBroadcast(r, genericBlock); err != nil { + errJson := &network.DefaultErrorJson{ + Message: err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + bs.proposeBlock(r.Context(), w, genericBlock) + return + } + + // blinded is not supported before bellatrix hardfork + altairBlock := ðpbv2.SignedBeaconBlockAltair{} + if err := altairBlock.UnmarshalSSZ(body); err == nil { + v1block, err := migration.AltairToV1Alpha1SignedBlock(altairBlock) + if err != nil { + errJson := &network.DefaultErrorJson{ + Message: "Could not decode request body into consensus block: " + err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + genericBlock := ð.GenericSignedBeaconBlock{ + Block: ð.GenericSignedBeaconBlock_Altair{ + Altair: v1block, + }, + } + if err = bs.validateBroadcast(r, genericBlock); err != nil { + errJson := &network.DefaultErrorJson{ + Message: err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + bs.proposeBlock(r.Context(), w, genericBlock) + return + } + phase0Block := ðpbv1.SignedBeaconBlock{} + if err := phase0Block.UnmarshalSSZ(body); err == nil { + v1block, err := migration.V1ToV1Alpha1SignedBlock(phase0Block) + if err != nil { + errJson := &network.DefaultErrorJson{ + Message: "Could not decode request body into consensus block: " + err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + genericBlock := ð.GenericSignedBeaconBlock{ + Block: ð.GenericSignedBeaconBlock_Phase0{ + Phase0: v1block, + }, + } + if err = bs.validateBroadcast(r, genericBlock); err != nil { + errJson := &network.DefaultErrorJson{ + Message: err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + bs.proposeBlock(r.Context(), w, genericBlock) + return + } + errJson := &network.DefaultErrorJson{ + Message: "Body does not represent a valid block type", + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) +} + +func publishBlindedBlockV2(bs *Server, w http.ResponseWriter, r *http.Request) { validate := validator.New() body, err := io.ReadAll(r.Body) if err != nil { @@ -167,7 +305,15 @@ func (bs *Server) PublishBlockV2(w http.ResponseWriter, r *http.Request) { if ok := bs.checkSync(r.Context(), w); !ok { return } + isSSZ, err := network.SszRequested(r) + if isSSZ && err == nil { + publishBlockV2SSZ(bs, w, r) + } else { + publishBlockV2(bs, w, r) + } +} +func publishBlockV2SSZ(bs *Server, w http.ResponseWriter, r *http.Request) { validate := validator.New() body, err := io.ReadAll(r.Body) if err != nil { @@ -178,7 +324,140 @@ func (bs *Server) PublishBlockV2(w http.ResponseWriter, r *http.Request) { network.WriteError(w, errJson) return } + capellaBlock := ðpbv2.SignedBeaconBlockCapella{} + if err := capellaBlock.UnmarshalSSZ(body); err == nil { + if err = validate.Struct(capellaBlock); err == nil { + v1block, err := migration.CapellaToV1Alpha1SignedBlock(capellaBlock) + if err != nil { + errJson := &network.DefaultErrorJson{ + Message: "Could not decode request body into consensus block: " + err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + genericBlock := ð.GenericSignedBeaconBlock{ + Block: ð.GenericSignedBeaconBlock_Capella{ + Capella: v1block, + }, + } + if err = bs.validateBroadcast(r, genericBlock); err != nil { + errJson := &network.DefaultErrorJson{ + Message: err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + bs.proposeBlock(r.Context(), w, genericBlock) + return + } + } + bellatrixBlock := ðpbv2.SignedBeaconBlockBellatrix{} + if err := bellatrixBlock.UnmarshalSSZ(body); err == nil { + if err = validate.Struct(bellatrixBlock); err == nil { + v1block, err := migration.BellatrixToV1Alpha1SignedBlock(bellatrixBlock) + if err != nil { + errJson := &network.DefaultErrorJson{ + Message: "Could not decode request body into consensus block: " + err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + genericBlock := ð.GenericSignedBeaconBlock{ + Block: ð.GenericSignedBeaconBlock_Bellatrix{ + Bellatrix: v1block, + }, + } + if err = bs.validateBroadcast(r, genericBlock); err != nil { + errJson := &network.DefaultErrorJson{ + Message: err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + bs.proposeBlock(r.Context(), w, genericBlock) + return + } + } + altairBlock := ðpbv2.SignedBeaconBlockAltair{} + if err := altairBlock.UnmarshalSSZ(body); err == nil { + if err = validate.Struct(altairBlock); err == nil { + v1block, err := migration.AltairToV1Alpha1SignedBlock(altairBlock) + if err != nil { + errJson := &network.DefaultErrorJson{ + Message: "Could not decode request body into consensus block: " + err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + genericBlock := ð.GenericSignedBeaconBlock{ + Block: ð.GenericSignedBeaconBlock_Altair{ + Altair: v1block, + }, + } + if err = bs.validateBroadcast(r, genericBlock); err != nil { + errJson := &network.DefaultErrorJson{ + Message: err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + bs.proposeBlock(r.Context(), w, genericBlock) + return + } + } + phase0Block := ðpbv1.SignedBeaconBlock{} + if err := phase0Block.UnmarshalSSZ(body); err == nil { + if err = validate.Struct(phase0Block); err == nil { + v1block, err := migration.V1ToV1Alpha1SignedBlock(phase0Block) + if err != nil { + errJson := &network.DefaultErrorJson{ + Message: "Could not decode request body into consensus block: " + err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + genericBlock := ð.GenericSignedBeaconBlock{ + Block: ð.GenericSignedBeaconBlock_Phase0{ + Phase0: v1block, + }, + } + if err = bs.validateBroadcast(r, genericBlock); err != nil { + errJson := &network.DefaultErrorJson{ + Message: err.Error(), + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) + return + } + bs.proposeBlock(r.Context(), w, genericBlock) + return + } + } + errJson := &network.DefaultErrorJson{ + Message: "Body does not represent a valid block type", + Code: http.StatusBadRequest, + } + network.WriteError(w, errJson) +} +func publishBlockV2(bs *Server, w http.ResponseWriter, r *http.Request) { + validate := validator.New() + body, err := io.ReadAll(r.Body) + if err != nil { + errJson := &network.DefaultErrorJson{ + Message: "Could not read request body", + Code: http.StatusInternalServerError, + } + network.WriteError(w, errJson) + return + } var capellaBlock *SignedBeaconBlockCapella if err = unmarshalStrict(body, &capellaBlock); err == nil { if err = validate.Struct(capellaBlock); err == nil { diff --git a/beacon-chain/rpc/eth/beacon/handlers_test.go b/beacon-chain/rpc/eth/beacon/handlers_test.go index 6f7b00efb7..58d4ed8cf0 100644 --- a/beacon-chain/rpc/eth/beacon/handlers_test.go +++ b/beacon-chain/rpc/eth/beacon/handlers_test.go @@ -2,6 +2,7 @@ package beacon import ( "bytes" + "encoding/json" "net/http" "net/http/httptest" "strings" @@ -13,6 +14,7 @@ import ( eth "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v4/testing/assert" mock2 "github.com/prysmaticlabs/prysm/v4/testing/mock" + "github.com/prysmaticlabs/prysm/v4/testing/require" "github.com/stretchr/testify/mock" ) @@ -30,7 +32,7 @@ func TestPublishBlockV2(t *testing.T) { SyncChecker: &mockSync.Sync{IsSyncing: false}, } - request := httptest.NewRequest("GET", "http://foo.example", bytes.NewReader([]byte(phase0Block))) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader([]byte(phase0Block))) writer := httptest.NewRecorder() writer.Body = &bytes.Buffer{} server.PublishBlockV2(writer, request) @@ -47,7 +49,7 @@ func TestPublishBlockV2(t *testing.T) { SyncChecker: &mockSync.Sync{IsSyncing: false}, } - request := httptest.NewRequest("GET", "http://foo.example", bytes.NewReader([]byte(altairBlock))) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader([]byte(altairBlock))) writer := httptest.NewRecorder() writer.Body = &bytes.Buffer{} server.PublishBlockV2(writer, request) @@ -64,7 +66,7 @@ func TestPublishBlockV2(t *testing.T) { SyncChecker: &mockSync.Sync{IsSyncing: false}, } - request := httptest.NewRequest("GET", "http://foo.example", bytes.NewReader([]byte(bellatrixBlock))) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader([]byte(bellatrixBlock))) writer := httptest.NewRecorder() writer.Body = &bytes.Buffer{} server.PublishBlockV2(writer, request) @@ -81,7 +83,7 @@ func TestPublishBlockV2(t *testing.T) { SyncChecker: &mockSync.Sync{IsSyncing: false}, } - request := httptest.NewRequest("GET", "http://foo.example", bytes.NewReader([]byte(capellaBlock))) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader([]byte(capellaBlock))) writer := httptest.NewRecorder() writer.Body = &bytes.Buffer{} server.PublishBlockV2(writer, request) @@ -92,7 +94,7 @@ func TestPublishBlockV2(t *testing.T) { SyncChecker: &mockSync.Sync{IsSyncing: false}, } - request := httptest.NewRequest("GET", "http://foo.example", bytes.NewReader([]byte(blindedBellatrixBlock))) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader([]byte(blindedBellatrixBlock))) writer := httptest.NewRecorder() writer.Body = &bytes.Buffer{} server.PublishBlockV2(writer, request) @@ -108,7 +110,7 @@ func TestPublishBlockV2(t *testing.T) { OptimisticModeFetcher: chainService, } - request := httptest.NewRequest("GET", "http://foo.example", bytes.NewReader([]byte("foo"))) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader([]byte("foo"))) writer := httptest.NewRecorder() writer.Body = &bytes.Buffer{} server.PublishBlockV2(writer, request) @@ -117,9 +119,74 @@ func TestPublishBlockV2(t *testing.T) { }) } -func TestPublishBlindedBlockV2(t *testing.T) { +func TestPublishBlockV2SSZ(t *testing.T) { ctrl := gomock.NewController(t) + t.Run("Bellatrix", func(t *testing.T) { + v1alpha1Server := mock2.NewMockBeaconNodeValidatorServer(ctrl) + v1alpha1Server.EXPECT().ProposeBeaconBlock(gomock.Any(), mock.MatchedBy(func(req *eth.GenericSignedBeaconBlock) bool { + _, ok := req.Block.(*eth.GenericSignedBeaconBlock_Bellatrix) + return ok + })) + server := &Server{ + V1Alpha1ValidatorServer: v1alpha1Server, + SyncChecker: &mockSync.Sync{IsSyncing: false}, + } + var bellablock SignedBeaconBlockBellatrix + err := json.Unmarshal([]byte(bellatrixBlock), &bellablock) + require.NoError(t, err) + genericBlock, err := bellablock.ToGeneric() + require.NoError(t, err) + sszvalue, err := genericBlock.GetBellatrix().MarshalSSZ() + require.NoError(t, err) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader(sszvalue)) + request.Header.Set("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + server.PublishBlockV2(writer, request) + assert.Equal(t, http.StatusOK, writer.Code) + }) + t.Run("Capella", func(t *testing.T) { + v1alpha1Server := mock2.NewMockBeaconNodeValidatorServer(ctrl) + v1alpha1Server.EXPECT().ProposeBeaconBlock(gomock.Any(), mock.MatchedBy(func(req *eth.GenericSignedBeaconBlock) bool { + _, ok := req.Block.(*eth.GenericSignedBeaconBlock_Capella) + return ok + })) + server := &Server{ + V1Alpha1ValidatorServer: v1alpha1Server, + SyncChecker: &mockSync.Sync{IsSyncing: false}, + } + + var cblock SignedBeaconBlockCapella + err := json.Unmarshal([]byte(capellaBlock), &cblock) + require.NoError(t, err) + genericBlock, err := cblock.ToGeneric() + require.NoError(t, err) + sszvalue, err := genericBlock.GetCapella().MarshalSSZ() + require.NoError(t, err) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader(sszvalue)) + request.Header.Set("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + server.PublishBlockV2(writer, request) + assert.Equal(t, http.StatusOK, writer.Code) + }) + t.Run("invalid block", func(t *testing.T) { + server := &Server{ + SyncChecker: &mockSync.Sync{IsSyncing: false}, + } + + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader([]byte(blindedBellatrixBlock))) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + server.PublishBlockV2(writer, request) + assert.Equal(t, http.StatusBadRequest, writer.Code) + assert.Equal(t, true, strings.Contains(writer.Body.String(), "Body does not represent a valid block type")) + }) +} + +func TestPublishBlindedBlockV2(t *testing.T) { + ctrl := gomock.NewController(t) t.Run("Phase 0", func(t *testing.T) { v1alpha1Server := mock2.NewMockBeaconNodeValidatorServer(ctrl) v1alpha1Server.EXPECT().ProposeBeaconBlock(gomock.Any(), mock.MatchedBy(func(req *eth.GenericSignedBeaconBlock) bool { @@ -131,7 +198,7 @@ func TestPublishBlindedBlockV2(t *testing.T) { SyncChecker: &mockSync.Sync{IsSyncing: false}, } - request := httptest.NewRequest("GET", "http://foo.example", bytes.NewReader([]byte(phase0Block))) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader([]byte(phase0Block))) writer := httptest.NewRecorder() writer.Body = &bytes.Buffer{} server.PublishBlindedBlockV2(writer, request) @@ -148,7 +215,7 @@ func TestPublishBlindedBlockV2(t *testing.T) { SyncChecker: &mockSync.Sync{IsSyncing: false}, } - request := httptest.NewRequest("GET", "http://foo.example", bytes.NewReader([]byte(altairBlock))) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader([]byte(altairBlock))) writer := httptest.NewRecorder() writer.Body = &bytes.Buffer{} server.PublishBlindedBlockV2(writer, request) @@ -165,7 +232,7 @@ func TestPublishBlindedBlockV2(t *testing.T) { SyncChecker: &mockSync.Sync{IsSyncing: false}, } - request := httptest.NewRequest("GET", "http://foo.example", bytes.NewReader([]byte(blindedBellatrixBlock))) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader([]byte(blindedBellatrixBlock))) writer := httptest.NewRecorder() writer.Body = &bytes.Buffer{} server.PublishBlindedBlockV2(writer, request) @@ -182,7 +249,7 @@ func TestPublishBlindedBlockV2(t *testing.T) { SyncChecker: &mockSync.Sync{IsSyncing: false}, } - request := httptest.NewRequest("GET", "http://foo.example", bytes.NewReader([]byte(blindedCapellaBlock))) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader([]byte(blindedCapellaBlock))) writer := httptest.NewRecorder() writer.Body = &bytes.Buffer{} server.PublishBlindedBlockV2(writer, request) @@ -193,7 +260,7 @@ func TestPublishBlindedBlockV2(t *testing.T) { SyncChecker: &mockSync.Sync{IsSyncing: false}, } - request := httptest.NewRequest("GET", "http://foo.example", bytes.NewReader([]byte(bellatrixBlock))) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader([]byte(bellatrixBlock))) writer := httptest.NewRecorder() writer.Body = &bytes.Buffer{} server.PublishBlindedBlockV2(writer, request) @@ -209,7 +276,7 @@ func TestPublishBlindedBlockV2(t *testing.T) { OptimisticModeFetcher: chainService, } - request := httptest.NewRequest("GET", "http://foo.example", bytes.NewReader([]byte("foo"))) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader([]byte("foo"))) writer := httptest.NewRecorder() writer.Body = &bytes.Buffer{} server.PublishBlindedBlockV2(writer, request) @@ -218,6 +285,72 @@ func TestPublishBlindedBlockV2(t *testing.T) { }) } +func TestPublishBlindedBlockV2SSZ(t *testing.T) { + ctrl := gomock.NewController(t) + t.Run("Bellatrix", func(t *testing.T) { + v1alpha1Server := mock2.NewMockBeaconNodeValidatorServer(ctrl) + v1alpha1Server.EXPECT().ProposeBeaconBlock(gomock.Any(), mock.MatchedBy(func(req *eth.GenericSignedBeaconBlock) bool { + _, ok := req.Block.(*eth.GenericSignedBeaconBlock_BlindedBellatrix) + return ok + })) + server := &Server{ + V1Alpha1ValidatorServer: v1alpha1Server, + SyncChecker: &mockSync.Sync{IsSyncing: false}, + } + + var bellablock SignedBlindedBeaconBlockBellatrix + err := json.Unmarshal([]byte(blindedBellatrixBlock), &bellablock) + require.NoError(t, err) + genericBlock, err := bellablock.ToGeneric() + require.NoError(t, err) + sszvalue, err := genericBlock.GetBlindedBellatrix().MarshalSSZ() + require.NoError(t, err) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader(sszvalue)) + request.Header.Set("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + server.PublishBlindedBlockV2(writer, request) + assert.Equal(t, http.StatusOK, writer.Code) + }) + t.Run("Capella", func(t *testing.T) { + v1alpha1Server := mock2.NewMockBeaconNodeValidatorServer(ctrl) + v1alpha1Server.EXPECT().ProposeBeaconBlock(gomock.Any(), mock.MatchedBy(func(req *eth.GenericSignedBeaconBlock) bool { + _, ok := req.Block.(*eth.GenericSignedBeaconBlock_BlindedCapella) + return ok + })) + server := &Server{ + V1Alpha1ValidatorServer: v1alpha1Server, + SyncChecker: &mockSync.Sync{IsSyncing: false}, + } + + var cblock SignedBlindedBeaconBlockCapella + err := json.Unmarshal([]byte(blindedCapellaBlock), &cblock) + require.NoError(t, err) + genericBlock, err := cblock.ToGeneric() + require.NoError(t, err) + sszvalue, err := genericBlock.GetBlindedCapella().MarshalSSZ() + require.NoError(t, err) + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader(sszvalue)) + request.Header.Set("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + server.PublishBlindedBlockV2(writer, request) + assert.Equal(t, http.StatusOK, writer.Code) + }) + t.Run("invalid block", func(t *testing.T) { + server := &Server{ + SyncChecker: &mockSync.Sync{IsSyncing: false}, + } + + request := httptest.NewRequest(http.MethodPost, "http://foo.example", bytes.NewReader([]byte(bellatrixBlock))) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + server.PublishBlindedBlockV2(writer, request) + assert.Equal(t, http.StatusBadRequest, writer.Code) + assert.Equal(t, true, strings.Contains(writer.Body.String(), "Body does not represent a valid block type")) + }) +} + const ( phase0Block = `{ "message": { @@ -668,7 +801,8 @@ const ( "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" ], "data": { "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", @@ -688,7 +822,7 @@ const ( } ], "sync_aggregate": { - "sync_committee_bits": "0x01", + "sync_committee_bits": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "sync_committee_signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" }, "execution_payload": { @@ -846,7 +980,8 @@ const ( "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" ], "data": { "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", @@ -866,7 +1001,7 @@ const ( } ], "sync_aggregate": { - "sync_committee_bits": "0x01", + "sync_committee_bits": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "sync_committee_signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" }, "execution_payload_header": { @@ -1022,7 +1157,8 @@ const ( "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" ], "data": { "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", @@ -1042,7 +1178,7 @@ const ( } ], "sync_aggregate": { - "sync_committee_bits": "0x01", + "sync_committee_bits": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "sync_committee_signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" }, "execution_payload": { @@ -1218,7 +1354,8 @@ const ( "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" ], "data": { "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", @@ -1238,7 +1375,7 @@ const ( } ], "sync_aggregate": { - "sync_committee_bits": "0x01", + "sync_committee_bits": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "sync_committee_signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" }, "execution_payload_header": { diff --git a/beacon-chain/rpc/service.go b/beacon-chain/rpc/service.go index e235365b9d..d6b83eaa28 100644 --- a/beacon-chain/rpc/service.go +++ b/beacon-chain/rpc/service.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "net" + "net/http" "sync" "github.com/gorilla/mux" @@ -218,16 +219,17 @@ func (s *Service) Start() { Stater: stater, HeadFetcher: s.cfg.HeadFetcher, } - s.cfg.Router.HandleFunc("/eth/v1/beacon/rewards/blocks/{block_id}", rewardsServer.BlockRewards) - s.cfg.Router.HandleFunc("/eth/v1/beacon/rewards/attestations/{epoch}", rewardsServer.AttestationRewards) - s.cfg.Router.HandleFunc("/eth/v1/beacon/rewards/sync_committee/{block_id}", rewardsServer.SyncCommitteeRewards) + + s.cfg.Router.HandleFunc("/eth/v1/beacon/rewards/blocks/{block_id}", rewardsServer.BlockRewards).Methods(http.MethodGet) + s.cfg.Router.HandleFunc("/eth/v1/beacon/rewards/attestations/{epoch}", rewardsServer.AttestationRewards).Methods(http.MethodPost) + s.cfg.Router.HandleFunc("/eth/v1/beacon/rewards/sync_committee/{block_id}", rewardsServer.SyncCommitteeRewards).Methods(http.MethodPost) builderServer := &rpcBuilder.Server{ FinalizationFetcher: s.cfg.FinalizationFetcher, OptimisticModeFetcher: s.cfg.OptimisticModeFetcher, Stater: stater, } - s.cfg.Router.HandleFunc("/eth/v1/builder/states/{state_id}/expected_withdrawals", builderServer.ExpectedWithdrawals) + s.cfg.Router.HandleFunc("/eth/v1/builder/states/{state_id}/expected_withdrawals", builderServer.ExpectedWithdrawals).Methods(http.MethodGet) validatorServer := &validatorv1alpha1.Server{ Ctx: s.ctx, @@ -321,9 +323,9 @@ func (s *Service) Start() { ExecutionChainInfoFetcher: s.cfg.ExecutionChainInfoFetcher, } - s.cfg.Router.HandleFunc("/prysm/node/trusted_peers", nodeServerPrysm.ListTrustedPeer).Methods("GET") - s.cfg.Router.HandleFunc("/prysm/node/trusted_peers", nodeServerPrysm.AddTrustedPeer).Methods("POST") - s.cfg.Router.HandleFunc("/prysm/node/trusted_peers/{peer_id}", nodeServerPrysm.RemoveTrustedPeer).Methods("Delete") + s.cfg.Router.HandleFunc("/prysm/node/trusted_peers", nodeServerPrysm.ListTrustedPeer).Methods(http.MethodGet) + s.cfg.Router.HandleFunc("/prysm/node/trusted_peers", nodeServerPrysm.AddTrustedPeer).Methods(http.MethodPost) + s.cfg.Router.HandleFunc("/prysm/node/trusted_peers/{peer_id}", nodeServerPrysm.RemoveTrustedPeer).Methods(http.MethodDelete) beaconChainServer := &beaconv1alpha1.Server{ Ctx: s.ctx, @@ -378,9 +380,9 @@ func (s *Service) Start() { HeadFetcher: s.cfg.HeadFetcher, SyncChecker: s.cfg.SyncService, } - s.cfg.Router.HandleFunc("/prysm/validators/performance", httpServer.GetValidatorPerformance) - s.cfg.Router.HandleFunc("/eth/v2/beacon/blocks", beaconChainServerV1.PublishBlockV2) - s.cfg.Router.HandleFunc("/eth/v2/beacon/blinded_blocks", beaconChainServerV1.PublishBlindedBlockV2) + s.cfg.Router.HandleFunc("/prysm/validators/performance", httpServer.GetValidatorPerformance).Methods(http.MethodGet) + s.cfg.Router.HandleFunc("/eth/v2/beacon/blocks", beaconChainServerV1.PublishBlockV2).Methods(http.MethodPost) + s.cfg.Router.HandleFunc("/eth/v2/beacon/blinded_blocks", beaconChainServerV1.PublishBlindedBlockV2).Methods(http.MethodPost) ethpbv1alpha1.RegisterNodeServer(s.grpcServer, nodeServer) ethpbservice.RegisterBeaconNodeServer(s.grpcServer, nodeServerV1) ethpbv1alpha1.RegisterHealthServer(s.grpcServer, nodeServer) diff --git a/network/BUILD.bazel b/network/BUILD.bazel index c0b9bb9916..c7659f52ee 100644 --- a/network/BUILD.bazel +++ b/network/BUILD.bazel @@ -6,6 +6,7 @@ go_library( "auth.go", "endpoint.go", "external_ip.go", + "reader.go", "writer.go", ], importpath = "github.com/prysmaticlabs/prysm/v4/network", @@ -25,6 +26,7 @@ go_test( "auth_test.go", "endpoint_test.go", "external_ip_test.go", + "reader_test.go", ], embed = [":go_default_library"], deps = [ diff --git a/network/reader.go b/network/reader.go new file mode 100644 index 0000000000..99a24cecc4 --- /dev/null +++ b/network/reader.go @@ -0,0 +1,51 @@ +package network + +import ( + "net/http" + "regexp" + "strconv" + "strings" +) + +// match a number with optional decimals +var priorityRegex = regexp.MustCompile(`q=(\d+(?:\.\d+)?)`) + +// SszRequested takes a http request and checks to see if it should be requesting a ssz response. +func SszRequested(req *http.Request) (bool, error) { + accept := req.Header.Values("Accept") + if len(accept) == 0 { + return false, nil + } + types := strings.Split(accept[0], ",") + currentType, currentPriority := "", 0.0 + for _, t := range types { + values := strings.Split(t, ";") + name := values[0] + if name != jsonMediaType && name != octetStreamMediaType { + continue + } + // no params specified + if len(values) == 1 { + priority := 1.0 + if priority > currentPriority { + currentType, currentPriority = name, priority + } + continue + } + params := values[1] + + match := priorityRegex.FindAllStringSubmatch(params, 1) + if len(match) != 1 { + continue + } + priority, err := strconv.ParseFloat(match[0][1], 32) + if err != nil { + return false, err + } + if priority > currentPriority { + currentType, currentPriority = name, priority + } + } + + return currentType == octetStreamMediaType, nil +} diff --git a/network/reader_test.go b/network/reader_test.go new file mode 100644 index 0000000000..b454addcad --- /dev/null +++ b/network/reader_test.go @@ -0,0 +1,99 @@ +package network + +import ( + "fmt" + "net/http/httptest" + "testing" + + "github.com/prysmaticlabs/prysm/v4/testing/assert" + "github.com/prysmaticlabs/prysm/v4/testing/require" +) + +func TestSSZRequested(t *testing.T) { + t.Run("ssz_requested", func(t *testing.T) { + request := httptest.NewRequest("GET", "http://foo.example", nil) + request.Header["Accept"] = []string{octetStreamMediaType} + result, err := SszRequested(request) + require.NoError(t, err) + assert.Equal(t, true, result) + }) + + t.Run("ssz_content_type_first", func(t *testing.T) { + request := httptest.NewRequest("GET", "http://foo.example", nil) + request.Header["Accept"] = []string{fmt.Sprintf("%s,%s", octetStreamMediaType, jsonMediaType)} + result, err := SszRequested(request) + require.NoError(t, err) + assert.Equal(t, true, result) + }) + + t.Run("ssz_content_type_preferred_1", func(t *testing.T) { + request := httptest.NewRequest("GET", "http://foo.example", nil) + request.Header["Accept"] = []string{fmt.Sprintf("%s;q=0.9,%s", jsonMediaType, octetStreamMediaType)} + result, err := SszRequested(request) + require.NoError(t, err) + assert.Equal(t, true, result) + }) + + t.Run("ssz_content_type_preferred_2", func(t *testing.T) { + request := httptest.NewRequest("GET", "http://foo.example", nil) + request.Header["Accept"] = []string{fmt.Sprintf("%s;q=0.95,%s;q=0.9", octetStreamMediaType, jsonMediaType)} + result, err := SszRequested(request) + require.NoError(t, err) + assert.Equal(t, true, result) + }) + + t.Run("other_content_type_preferred", func(t *testing.T) { + request := httptest.NewRequest("GET", "http://foo.example", nil) + request.Header["Accept"] = []string{fmt.Sprintf("%s,%s;q=0.9", jsonMediaType, octetStreamMediaType)} + result, err := SszRequested(request) + require.NoError(t, err) + assert.Equal(t, false, result) + }) + + t.Run("other_params", func(t *testing.T) { + request := httptest.NewRequest("GET", "http://foo.example", nil) + request.Header["Accept"] = []string{fmt.Sprintf("%s,%s;q=0.9,otherparam=xyz", jsonMediaType, octetStreamMediaType)} + result, err := SszRequested(request) + require.NoError(t, err) + assert.Equal(t, false, result) + }) + + t.Run("no_header", func(t *testing.T) { + request := httptest.NewRequest("GET", "http://foo.example", nil) + result, err := SszRequested(request) + require.NoError(t, err) + assert.Equal(t, false, result) + }) + + t.Run("empty_header", func(t *testing.T) { + request := httptest.NewRequest("GET", "http://foo.example", nil) + request.Header["Accept"] = []string{} + result, err := SszRequested(request) + require.NoError(t, err) + assert.Equal(t, false, result) + }) + + t.Run("empty_header_value", func(t *testing.T) { + request := httptest.NewRequest("GET", "http://foo.example", nil) + request.Header["Accept"] = []string{""} + result, err := SszRequested(request) + require.NoError(t, err) + assert.Equal(t, false, result) + }) + + t.Run("other_content_type", func(t *testing.T) { + request := httptest.NewRequest("GET", "http://foo.example", nil) + request.Header["Accept"] = []string{"application/other"} + result, err := SszRequested(request) + require.NoError(t, err) + assert.Equal(t, false, result) + }) + + t.Run("garbage", func(t *testing.T) { + request := httptest.NewRequest("GET", "http://foo.example", nil) + request.Header["Accept"] = []string{"This is Sparta!!!"} + result, err := SszRequested(request) + require.NoError(t, err) + assert.Equal(t, false, result) + }) +} diff --git a/network/writer.go b/network/writer.go index f7b886a162..988d364ca4 100644 --- a/network/writer.go +++ b/network/writer.go @@ -10,6 +10,11 @@ import ( log "github.com/sirupsen/logrus" ) +const ( + jsonMediaType = "application/json" + octetStreamMediaType = "application/octet-stream" +) + // DefaultErrorJson is a JSON representation of a simple error value, containing only a message and an error code. type DefaultErrorJson struct { Message string `json:"message"` @@ -18,13 +23,23 @@ type DefaultErrorJson struct { // WriteJson writes the response message in JSON format. func WriteJson(w http.ResponseWriter, v any) { - w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Type", jsonMediaType) w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(v); err != nil { log.WithError(err).Error("Could not write response message") } } +// WriteSsz writes the response message in ssz format +func WriteSsz(w http.ResponseWriter, respSsz []byte, fileName string) { + w.Header().Set("Content-Length", strconv.Itoa(len(respSsz))) + w.Header().Set("Content-Type", octetStreamMediaType) + w.Header().Set("Content-Disposition", "attachment; filename="+fileName) + if _, err := io.Copy(w, io.NopCloser(bytes.NewReader(respSsz))); err != nil { + log.WithError(err).Error("could not write response message") + } +} + // WriteError writes the error by manipulating headers and the body of the final response. func WriteError(w http.ResponseWriter, errJson *DefaultErrorJson) { j, err := json.Marshal(errJson)