diff --git a/.golangci.yml b/.golangci.yml index ed23268a..1a7d0aff 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -30,6 +30,7 @@ linters: - goconst - gosec - ireturn + - maintidx - noctx - tagliatelle - perfsprint diff --git a/go.mod b/go.mod index e1fc3845..a30ef991 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 + github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09 github.com/urfave/cli/v3 v3.0.0-beta1 ) diff --git a/go.sum b/go.sum index 413c7dd5..29f3997a 100644 --- a/go.sum +++ b/go.sum @@ -132,6 +132,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09 h1:QVxbx5l/0pzciWYOynixQMtUhPYC3YKD6EcUlOsgGqw= +github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09/go.mod h1:Uy/Rnv5WKuOO+PuDhuYLEpUiiKIZtss3z519uk67aF0= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= diff --git a/server/constants.go b/server/constants.go new file mode 100644 index 00000000..c49dd637 --- /dev/null +++ b/server/constants.go @@ -0,0 +1,19 @@ +package server + +const ( + HeaderAccept = "Accept" + HeaderContentType = "Content-Type" + HeaderEthConsensusVersion = "Eth-Consensus-Version" + HeaderKeySlotUID = "X-MEVBoost-SlotID" + HeaderKeyVersion = "X-MEVBoost-Version" + HeaderStartTimeUnixMS = "X-MEVBoost-StartTimeUnixMS" + HeaderUserAgent = "User-Agent" + + MediaTypeJSON = "application/json" + MediaTypeOctetStream = "application/octet-stream" + + EthConsensusVersionBellatrix = "bellatrix" + EthConsensusVersionCapella = "capella" + EthConsensusVersionDeneb = "deneb" + EthConsensusVersionElectra = "electra" +) diff --git a/server/get_header.go b/server/get_header.go index 4bd9fb5c..a5cba4e8 100644 --- a/server/get_header.go +++ b/server/get_header.go @@ -2,12 +2,20 @@ package server import ( "context" + "encoding/json" "fmt" + "io" + "mime" "net/http" "sync" "time" + builderApiBellatrix "github.com/attestantio/go-builder-client/api/bellatrix" + builderApiCapella "github.com/attestantio/go-builder-client/api/capella" + builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb" + builderApiElectra "github.com/attestantio/go-builder-client/api/electra" builderSpec "github.com/attestantio/go-builder-client/spec" + "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/flashbots/mev-boost/config" "github.com/flashbots/mev-boost/server/types" @@ -16,7 +24,7 @@ import ( ) // getHeader requests a bid from each relay and returns the most profitable one -func (m *BoostService) getHeader(log *logrus.Entry, ua UserAgent, slot phase0.Slot, pubkey, parentHashHex string) (bidResp, error) { +func (m *BoostService) getHeader(log *logrus.Entry, slot phase0.Slot, pubkey, parentHashHex string, ua UserAgent, proposerAcceptContentTypes string) (bidResp, error) { // Ensure arguments are valid if len(pubkey) != 98 { return bidResp{}, errInvalidPubkey @@ -35,6 +43,10 @@ func (m *BoostService) getHeader(log *logrus.Entry, ua UserAgent, slot phase0.Sl m.slotUIDLock.Unlock() log = log.WithField("slotUID", slotUID) + // Compute these once, instead of for each relay + userAgent := wrapUserAgent(ua) + startTime := fmt.Sprintf("%d", time.Now().UTC().UnixMilli()) + // Log how late into the slot the request starts slotStartTimestamp := m.genesisTime + uint64(slot)*config.SlotTimeSec msIntoSlot := uint64(time.Now().UTC().UnixMilli()) - slotStartTimestamp*1000 @@ -44,12 +56,6 @@ func (m *BoostService) getHeader(log *logrus.Entry, ua UserAgent, slot phase0.Sl "msIntoSlot": msIntoSlot, }).Infof("getHeader request start - %d milliseconds into slot %d", msIntoSlot, slot) - // Add request headers - headers := map[string]string{ - HeaderKeySlotUID: slotUID.String(), - HeaderStartTimeUnixMS: fmt.Sprintf("%d", time.Now().UTC().UnixMilli()), - } - var ( mu sync.Mutex wg sync.WaitGroup @@ -71,20 +77,71 @@ func (m *BoostService) getHeader(log *logrus.Entry, ua UserAgent, slot phase0.Sl url := relay.GetURI(fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", slot, parentHashHex, pubkey)) log := log.WithField("url", url) - // Send the get bid request to the relay - bid := new(builderSpec.VersionedSignedBuilderBid) - code, err := SendHTTPRequest(context.Background(), m.httpClientGetHeader, http.MethodGet, url, ua, headers, nil, bid) + // Make a new request + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) if err != nil { - log.WithError(err).Warn("error making request to relay") + log.WithError(err).Warn("error creating new request") return } - if code == http.StatusNoContent { + + // Add header fields to this request + req.Header.Set(HeaderAccept, proposerAcceptContentTypes) + req.Header.Set(HeaderKeySlotUID, slotUID.String()) + req.Header.Set(HeaderStartTimeUnixMS, startTime) + req.Header.Set(HeaderUserAgent, userAgent) + + // Send the request + log.Debug("requesting header") + resp, err := m.httpClientGetHeader.Do(req) + if err != nil { + log.WithError(err).Warn("error calling getHeader on relay") + return + } + defer resp.Body.Close() + + // Check if no header is available + if resp.StatusCode == http.StatusNoContent { log.Debug("no-content response") return } + // Check that the response was successful + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf("%w: %d", errHTTPErrorResponse, resp.StatusCode) + log.WithError(err).Warn("error status code") + return + } + + // Get the resp body content + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + log.WithError(err).Warn("error reading response body") + return + } + + // Get the response's content type, default to JSON + respContentType, _, err := mime.ParseMediaType(resp.Header.Get(HeaderContentType)) + if err != nil { + log.WithError(err).Warn("error parsing response content type") + respContentType = MediaTypeJSON + } + log = log.WithField("respContentType", respContentType) + + // Get the optional version, used with SSZ decoding + respEthConsensusVersion := resp.Header.Get(HeaderEthConsensusVersion) + log = log.WithField("respEthConsensusVersion", respEthConsensusVersion) + + // Decode bid + bid := new(builderSpec.VersionedSignedBuilderBid) + err = decodeBid(respBytes, respContentType, respEthConsensusVersion, bid) + if err != nil { + log.WithError(err).Warn("error decoding bid") + return + } + // Skip if bid is empty if bid.IsEmpty() { + log.Debug("skipping empty bid") return } @@ -157,20 +214,28 @@ func (m *BoostService) getHeader(log *logrus.Entry, ua UserAgent, slot phase0.Sl mu.Lock() defer mu.Unlock() + // Create a copy of the relay instance with its encoding preference. If we request SSZ and the relay + // responds with JSON, we know that it does not support SSZ yet. This preference will be used in getPayload, + // because we must encode the blinded block in the request in such a way that the relay can decode it. + relayWithEncodingPreference := relay.Copy() + relayWithEncodingPreference.SupportsSSZ = respContentType == MediaTypeOctetStream + // Remember which relays delivered which bids (multiple relays might deliver the top bid) - relays[BlockHashHex(bidInfo.blockHash.String())] = append(relays[BlockHashHex(bidInfo.blockHash.String())], relay) + relays[BlockHashHex(bidInfo.blockHash.String())] = append(relays[BlockHashHex(bidInfo.blockHash.String())], relayWithEncodingPreference) // Compare the bid with already known top bid (if any) if !result.response.IsEmpty() { valueDiff := bidInfo.value.Cmp(result.bidInfo.value) if valueDiff == -1 { // The current bid is less profitable than already known one + log.Debug("ignoring less profitable bid") return } else if valueDiff == 0 { // The current bid is equally profitable as already known one // Use hash as tiebreaker previousBidBlockHash := result.bidInfo.blockHash if bidInfo.blockHash.String() >= previousBidBlockHash.String() { + log.Debug("equally profitable bid lost tiebreaker") return } } @@ -189,3 +254,89 @@ func (m *BoostService) getHeader(log *logrus.Entry, ua UserAgent, slot phase0.Sl result.relays = relays[BlockHashHex(result.bidInfo.blockHash.String())] return result, nil } + +// decodeBid decodes a bid by SSZ or JSON, depending on the provided respContentType +func decodeBid(respBytes []byte, respContentType, ethConsensusVersion string, bid *builderSpec.VersionedSignedBuilderBid) error { + switch respContentType { + case MediaTypeOctetStream: + if ethConsensusVersion != "" { + // Do SSZ decoding + switch ethConsensusVersion { + case EthConsensusVersionBellatrix: + bid.Version = spec.DataVersionBellatrix + bid.Bellatrix = new(builderApiBellatrix.SignedBuilderBid) + return bid.Bellatrix.UnmarshalSSZ(respBytes) + case EthConsensusVersionCapella: + bid.Version = spec.DataVersionCapella + bid.Capella = new(builderApiCapella.SignedBuilderBid) + return bid.Capella.UnmarshalSSZ(respBytes) + case EthConsensusVersionDeneb: + bid.Version = spec.DataVersionDeneb + bid.Deneb = new(builderApiDeneb.SignedBuilderBid) + return bid.Deneb.UnmarshalSSZ(respBytes) + case EthConsensusVersionElectra: + bid.Version = spec.DataVersionElectra + bid.Electra = new(builderApiElectra.SignedBuilderBid) + return bid.Electra.UnmarshalSSZ(respBytes) + default: + return errInvalidForkVersion + } + } else { + return types.ErrMissingEthConsensusVersion + } + case MediaTypeJSON: + // Do JSON decoding + return json.Unmarshal(respBytes, bid) + } + return types.ErrInvalidContentType +} + +// respondGetHeaderJSON responds to the proposer in JSON +func (m *BoostService) respondGetHeaderJSON(w http.ResponseWriter, result *bidResp) { + w.Header().Set(HeaderContentType, MediaTypeJSON) + w.WriteHeader(http.StatusOK) + + // Serialize and write the data + if err := json.NewEncoder(w).Encode(&result.response); err != nil { + m.log.WithField("response", result.response).WithError(err).Error("could not write OK response") + http.Error(w, "", http.StatusInternalServerError) + } +} + +// respondGetHeaderSSZ responds to the proposer in SSZ +func (m *BoostService) respondGetHeaderSSZ(w http.ResponseWriter, result *bidResp) { + // Serialize the response + var err error + var sszData []byte + switch result.response.Version { + case spec.DataVersionBellatrix: + w.Header().Set(HeaderEthConsensusVersion, EthConsensusVersionBellatrix) + sszData, err = result.response.Bellatrix.MarshalSSZ() + case spec.DataVersionCapella: + w.Header().Set(HeaderEthConsensusVersion, EthConsensusVersionCapella) + sszData, err = result.response.Capella.MarshalSSZ() + case spec.DataVersionDeneb: + w.Header().Set(HeaderEthConsensusVersion, EthConsensusVersionDeneb) + sszData, err = result.response.Deneb.MarshalSSZ() + case spec.DataVersionElectra: + w.Header().Set(HeaderEthConsensusVersion, EthConsensusVersionElectra) + sszData, err = result.response.Electra.MarshalSSZ() + case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair: + err = errInvalidForkVersion + } + if err != nil { + m.log.WithError(err).Error("error serializing response as SSZ") + http.Error(w, "failed to serialize response", http.StatusInternalServerError) + return + } + + // Write the header + w.Header().Set(HeaderContentType, MediaTypeOctetStream) + w.WriteHeader(http.StatusOK) + + // Write SSZ data + if _, err := w.Write(sszData); err != nil { + m.log.WithError(err).Error("error writing SSZ response") + http.Error(w, "failed to write response", http.StatusInternalServerError) + } +} diff --git a/server/mock/mock_relay.go b/server/mock/mock_relay.go index dd1761ff..b73fd87a 100644 --- a/server/mock/mock_relay.go +++ b/server/mock/mock_relay.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "strings" "sync" "testing" "time" @@ -71,6 +72,10 @@ type Relay struct { // Server section Server *httptest.Server ResponseDelay time.Duration + + // Force response encodings + ForceJSON bool + ForceSSZ bool } // NewRelay creates a mocked relay which implements the backend.BoostBackend interface @@ -221,6 +226,7 @@ func (m *Relay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, publi ParentHash: HexToHash(parentHash), WithdrawalsRoot: phase0.Root{}, BaseFeePerGas: uint256.NewInt(0), + ExtraData: make([]byte, 0), }, BlobKZGCommitments: make([]deneb.KZGCommitment, 0), Value: uint256.NewInt(value), @@ -278,15 +284,11 @@ func (m *Relay) handleGetHeader(w http.ResponseWriter, req *http.Request) { m.handlerOverrideGetHeader(w, req) return } - m.defaultHandleGetHeader(w) + m.defaultHandleGetHeader(w, req) } // defaultHandleGetHeader returns the default handler for handleGetHeader -func (m *Relay) defaultHandleGetHeader(w http.ResponseWriter) { - // By default, everything will be ok. - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - +func (m *Relay) defaultHandleGetHeader(w http.ResponseWriter, req *http.Request) { // Build the default response. response := m.MakeGetHeaderResponse( 12345, @@ -299,9 +301,42 @@ func (m *Relay) defaultHandleGetHeader(w http.ResponseWriter) { response = m.GetHeaderResponse } - if err := json.NewEncoder(w).Encode(response); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + respondJSON := func() { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + respondSSZ := func() { + w.Header().Set("Eth-Consensus-Version", "deneb") + w.Header().Set("Content-Type", "application/octet-stream") + w.WriteHeader(http.StatusOK) + sszData, err := response.Deneb.MarshalSSZ() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = w.Write(sszData) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + // We cannot use code in server, so this is a simplistic + // negotiation which should only be used in testing. + switch { + case m.ForceJSON: + respondJSON() + case m.ForceSSZ: + respondSSZ() + case strings.Contains(req.Header.Get("Accept"), "application/octet-stream"): + respondSSZ() + default: + respondJSON() } } diff --git a/server/service.go b/server/service.go index 8ac83247..5c24f0e9 100644 --- a/server/service.go +++ b/server/service.go @@ -28,6 +28,7 @@ import ( "github.com/google/uuid" "github.com/gorilla/mux" "github.com/sirupsen/logrus" + goacceptheaders "github.com/timewasted/go-accept-headers" ) var ( @@ -132,7 +133,7 @@ func NewBoostService(opts BoostServiceOpts) (*BoostService, error) { } func (m *BoostService) respondError(w http.ResponseWriter, code int, message string) { - w.Header().Set("Content-Type", "application/json") + w.Header().Set(HeaderContentType, MediaTypeJSON) w.WriteHeader(code) resp := httpErrorResp{code, message} if err := json.NewEncoder(w).Encode(resp); err != nil { @@ -142,7 +143,7 @@ func (m *BoostService) respondError(w http.ResponseWriter, code int, message str } func (m *BoostService) respondOK(w http.ResponseWriter, response any) { - w.Header().Set("Content-Type", "application/json") + w.Header().Set(HeaderContentType, MediaTypeJSON) w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(response); err != nil { m.log.WithField("response", response).WithError(err).Error("could not write OK response") @@ -227,12 +228,12 @@ func (m *BoostService) handleRegisterValidator(w http.ResponseWriter, req *http. log.Debug("handling request") // Get the user agent - ua := UserAgent(req.Header.Get("User-Agent")) + ua := UserAgent(req.Header.Get(HeaderUserAgent)) log = log.WithFields(logrus.Fields{"ua": ua}) // Additional header fields header := req.Header - header.Set("User-Agent", wrapUserAgent(ua)) + header.Set(HeaderUserAgent, wrapUserAgent(ua)) header.Set(HeaderStartTimeUnixMS, fmt.Sprintf("%d", time.Now().UTC().UnixMilli())) // Read the validator registrations @@ -260,9 +261,13 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) vars = mux.Vars(req) parentHashHex = vars["parent_hash"] pubkey = vars["pubkey"] - ua = UserAgent(req.Header.Get("User-Agent")) + ua = UserAgent(req.Header.Get(HeaderUserAgent)) + + rawProposerAcceptContentTypes = req.Header.Get(HeaderAccept) + parsedProposerAcceptContentTypes = goacceptheaders.Parse(rawProposerAcceptContentTypes) ) + // Parse the slot slotValue, err := strconv.ParseUint(vars["slot"], 10, 64) if err != nil { m.respondError(w, http.StatusBadRequest, errInvalidSlot.Error()) @@ -270,22 +275,25 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) } slot := phase0.Slot(slotValue) + // Add relevant fields to the logger log := m.log.WithFields(logrus.Fields{ - "method": "getHeader", - "slot": slot, - "parentHash": parentHashHex, - "pubkey": pubkey, - "ua": ua, + "method": "getHeader", + "slot": slot, + "parentHash": parentHashHex, + "pubkey": pubkey, + "ua": ua, + "rawProposerAcceptContentTypes": rawProposerAcceptContentTypes, }) - log.Debug("getHeader") + log.Debug("handling request") // Query the relays for the header - result, err := m.getHeader(log, ua, slot, pubkey, parentHashHex) + result, err := m.getHeader(log, slot, pubkey, parentHashHex, ua, rawProposerAcceptContentTypes) if err != nil { m.respondError(w, http.StatusBadRequest, err.Error()) return } + // Bail if none of the relays returned a bid if result.response.IsEmpty() { log.Info("no bid received") w.WriteHeader(http.StatusNoContent) @@ -307,8 +315,31 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) "relays": strings.Join(types.RelayEntriesToStrings(result.relays), ", "), }).Info("best bid") - // Return the bid - m.respondOK(w, &result.response) + // Default to JSON if the proposer provides nothing + if len(parsedProposerAcceptContentTypes) == 0 { + log.Info("no proposer accepts, defaulting to JSON") + parsedProposerAcceptContentTypes = goacceptheaders.AcceptSlice{{Type: MediaTypeJSON}} + } + + // Get the proposer's highest quality acceptable media type + proposerPreferredContentType, err := parsedProposerAcceptContentTypes.Negotiate(MediaTypeJSON, MediaTypeOctetStream) + if err != nil { + log.WithError(err).Warn("failed to negotiate preferred content-type") + proposerPreferredContentType = MediaTypeJSON + } + + // Respond appropriately + if proposerPreferredContentType == MediaTypeJSON { + log.Debug("responding with JSON") + m.respondGetHeaderJSON(w, &result) + } else if proposerPreferredContentType == MediaTypeOctetStream { + log.Debug("responding with SSZ") + m.respondGetHeaderSSZ(w, &result) + } else { + message := fmt.Sprintf("unsupported media type: %s", proposerPreferredContentType) + log.Error(message) + m.respondError(w, http.StatusNotAcceptable, message) + } } // respondPayload responds to the proposer with the payload @@ -337,7 +368,7 @@ func (m *BoostService) handleGetPayload(w http.ResponseWriter, req *http.Request } // Read user agent for logging - userAgent := UserAgent(req.Header.Get("User-Agent")) + userAgent := UserAgent(req.Header.Get(HeaderUserAgent)) // New forks need to be added at the front of this array. // The ordering of the array conveys precedence of the decoders. diff --git a/server/service_test.go b/server/service_test.go index 19066074..db2e6cf3 100644 --- a/server/service_test.go +++ b/server/service_test.go @@ -75,7 +75,7 @@ func newTestBackend(t *testing.T, numRelays int, relayTimeout time.Duration) *te return &backend } -func (be *testBackend) request(t *testing.T, method, path string, payload any) *httptest.ResponseRecorder { +func (be *testBackend) request(t *testing.T, method, path string, header http.Header, payload any) *httptest.ResponseRecorder { t.Helper() var req *http.Request var err error @@ -87,8 +87,11 @@ func (be *testBackend) request(t *testing.T, method, path string, payload any) * require.NoError(t, err2) req, err = http.NewRequest(method, path, bytes.NewReader(payloadBytes)) } - require.NoError(t, err) + + // Set header + req.Header = header + rr := httptest.NewRecorder() be.boost.getRouter().ServeHTTP(rr, req) return rr @@ -168,10 +171,13 @@ func TestWebserverMaxHeaderSize(t *testing.T) { func TestStatus(t *testing.T) { t.Run("At least one relay is available", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + backend := newTestBackend(t, 1, time.Second) time.Sleep(time.Millisecond * 20) path := "/eth/v1/builder/status" - rr := backend.request(t, http.MethodGet, path, nil) + rr := backend.request(t, http.MethodGet, path, header, nil) require.Equal(t, http.StatusOK, rr.Code) require.NotEmpty(t, rr.Header().Get("X-MEVBoost-Version")) @@ -179,11 +185,14 @@ func TestStatus(t *testing.T) { }) t.Run("No relays available", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + backend := newTestBackend(t, 1, time.Second) backend.relays[0].Server.Close() // makes the relay unavailable path := "/eth/v1/builder/status" - rr := backend.request(t, http.MethodGet, path, nil) + rr := backend.request(t, http.MethodGet, path, header, nil) require.Equal(t, http.StatusServiceUnavailable, rr.Code) require.NotEmpty(t, rr.Header().Get("X-MEVBoost-Version")) @@ -206,19 +215,24 @@ func TestRegisterValidator(t *testing.T) { payload := []builderApiV1.SignedValidatorRegistration{reg} t.Run("Normal function", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + backend := newTestBackend(t, 1, time.Second) - rr := backend.request(t, http.MethodPost, path, payload) + rr := backend.request(t, http.MethodPost, path, header, payload) require.Equal(t, http.StatusOK, rr.Code) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) }) t.Run("Relay error response", func(t *testing.T) { - backend := newTestBackend(t, 2, time.Second) + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + backend := newTestBackend(t, 2, time.Second) backend.relays[0].ResponseDelay = 5 * time.Millisecond backend.relays[1].ResponseDelay = 5 * time.Millisecond - rr := backend.request(t, http.MethodPost, path, payload) + rr := backend.request(t, http.MethodPost, path, header, payload) require.Equal(t, http.StatusOK, rr.Code) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) require.Equal(t, 1, backend.relays[1].GetRequestCount(path)) @@ -227,7 +241,8 @@ func TestRegisterValidator(t *testing.T) { backend.relays[0].OverrideHandleRegisterValidator(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) }) - rr = backend.request(t, http.MethodPost, path, payload) + + rr = backend.request(t, http.MethodPost, path, header, payload) require.Equal(t, http.StatusOK, rr.Code) require.Equal(t, 2, backend.relays[0].GetRequestCount(path)) require.Equal(t, 2, backend.relays[1].GetRequestCount(path)) @@ -236,7 +251,7 @@ func TestRegisterValidator(t *testing.T) { backend.relays[1].OverrideHandleRegisterValidator(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) }) - rr = backend.request(t, http.MethodPost, path, payload) + rr = backend.request(t, http.MethodPost, path, header, payload) require.JSONEq(t, `{"code":502,"message":"no successful relay response"}`+"\n", rr.Body.String()) require.Equal(t, http.StatusBadGateway, rr.Code) require.Equal(t, 3, backend.relays[0].GetRequestCount(path)) @@ -244,13 +259,16 @@ func TestRegisterValidator(t *testing.T) { }) t.Run("mev-boost relay timeout works with slow relay", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + backend := newTestBackend(t, 1, 150*time.Millisecond) // 10ms max - rr := backend.request(t, http.MethodPost, path, payload) + rr := backend.request(t, http.MethodPost, path, header, payload) require.Equal(t, http.StatusOK, rr.Code) // Now make the relay return slowly, mev-boost should return an error backend.relays[0].ResponseDelay = 180 * time.Millisecond - rr = backend.request(t, http.MethodPost, path, payload) + rr = backend.request(t, http.MethodPost, path, header, payload) require.JSONEq(t, `{"code":502,"message":"no successful relay response"}`+"\n", rr.Body.String()) require.Equal(t, http.StatusBadGateway, rr.Code) require.Equal(t, 2, backend.relays[0].GetRequestCount(path)) @@ -269,13 +287,30 @@ func TestGetHeader(t *testing.T) { require.Equal(t, "/eth/v1/builder/header/1/0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7/0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", path) t.Run("Okay response from relay", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + backend := newTestBackend(t, 1, time.Second) - rr := backend.request(t, http.MethodGet, path, nil) + rr := backend.request(t, http.MethodGet, path, header, nil) require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) }) t.Run("Okay response from relay deneb", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + + backend := newTestBackend(t, 1, time.Second) + rr := backend.request(t, http.MethodGet, path, header, nil) + require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) + }) + + t.Run("Okay response from relay deneb in ssz", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderEthConsensusVersion, "deneb") + header.Set(HeaderAccept, MediaTypeOctetStream) + backend := newTestBackend(t, 1, time.Second) resp := backend.relays[0].MakeGetHeaderResponse( 12345, @@ -285,12 +320,125 @@ func TestGetHeader(t *testing.T) { spec.DataVersionDeneb, ) backend.relays[0].GetHeaderResponse = resp - rr := backend.request(t, http.MethodGet, path, nil) + rr := backend.request(t, http.MethodGet, path, header, nil) require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) + require.Equal(t, MediaTypeOctetStream, rr.Header().Get(HeaderContentType)) + + // Ensure the response was SSZ + bid := new(builderApiDeneb.SignedBuilderBid) + err := bid.UnmarshalSSZ(rr.Body.Bytes()) + require.NoError(t, err) + require.EqualValues(t, *resp.Deneb, *bid) + }) + + t.Run("Relay returns SSZ, mev-boost returns SSZ", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderEthConsensusVersion, "deneb") + header.Set(HeaderAccept, "application/octet-stream;q=1.0,application/json;q=0.9") + + backend := newTestBackend(t, 1, time.Second) + backend.relays[0].ForceSSZ = true + rr := backend.request(t, http.MethodGet, path, header, nil) + require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) + require.Equal(t, MediaTypeOctetStream, rr.Header().Get(HeaderContentType)) + }) + + t.Run("One relay returns SSZ, another relay returns JSON, mev-boost returns SSZ", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderEthConsensusVersion, "deneb") + header.Set(HeaderAccept, "application/octet-stream;q=1.0,application/json;q=0.9") + + backend := newTestBackend(t, 2, time.Second) + backend.relays[0].ForceSSZ = true + backend.relays[1].ForceJSON = true + rr := backend.request(t, http.MethodGet, path, header, nil) + require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) + require.Equal(t, 1, backend.relays[1].GetRequestCount(path)) + require.Equal(t, MediaTypeOctetStream, rr.Header().Get(HeaderContentType)) + }) + + t.Run("One relay returns JSON, mev-boost returns preferred SSZ", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderEthConsensusVersion, "deneb") + header.Set(HeaderAccept, "application/octet-stream;q=1.0,application/json;q=0.9") + + backend := newTestBackend(t, 1, time.Second) + backend.relays[0].ForceJSON = true + rr := backend.request(t, http.MethodGet, path, header, nil) + require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) + require.Equal(t, MediaTypeOctetStream, rr.Header().Get(HeaderContentType)) + }) + + t.Run("Two relays return JSON, mev-boost returns preferred SSZ", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderEthConsensusVersion, "deneb") + header.Set(HeaderAccept, "application/octet-stream;q=1.0,application/json;q=0.9") + + backend := newTestBackend(t, 2, time.Second) + backend.relays[0].ForceJSON = true + backend.relays[1].ForceJSON = true + rr := backend.request(t, http.MethodGet, path, header, nil) + require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) + require.Equal(t, 1, backend.relays[1].GetRequestCount(path)) + require.Equal(t, MediaTypeOctetStream, rr.Header().Get(HeaderContentType)) + }) + + t.Run("Accepts both with Q values", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderEthConsensusVersion, "deneb") + header.Set(HeaderAccept, "application/octet-stream;q=1.0,application/json;q=0.9") + + backend := newTestBackend(t, 1, time.Second) + rr := backend.request(t, http.MethodGet, path, header, nil) + require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) + require.Equal(t, MediaTypeOctetStream, rr.Header().Get(HeaderContentType)) + }) + + t.Run("No accept value", func(t *testing.T) { + // This should default to JSON + header := make(http.Header) + header.Set(HeaderEthConsensusVersion, "deneb") + header.Del(HeaderAccept) + + backend := newTestBackend(t, 1, time.Second) + rr := backend.request(t, http.MethodGet, path, header, nil) + require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) + require.Equal(t, MediaTypeJSON, rr.Header().Get(HeaderContentType)) //nolint:testifylint + }) + + t.Run("Accepts both but prefers JSON", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderEthConsensusVersion, "deneb") + header.Set(HeaderAccept, "application/octet-stream;q=0.9,application/json;q=1.0") + + backend := newTestBackend(t, 1, time.Second) + rr := backend.request(t, http.MethodGet, path, header, nil) + require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) + require.Equal(t, MediaTypeJSON, rr.Header().Get(HeaderContentType)) //nolint:testifylint + }) + + t.Run("Only accepts unsupported media type", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderEthConsensusVersion, "deneb") + header.Set(HeaderAccept, "plain/text") + + backend := newTestBackend(t, 1, time.Second) + rr := backend.request(t, http.MethodGet, path, header, nil) + require.Equal(t, http.StatusNotAcceptable, rr.Code, rr.Body.String()) }) t.Run("Bad response from relays", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + backend := newTestBackend(t, 2, time.Second) resp := backend.relays[0].MakeGetHeaderResponse( 12345, @@ -303,20 +451,23 @@ func TestGetHeader(t *testing.T) { // 1/2 failing responses are okay backend.relays[0].GetHeaderResponse = resp - rr := backend.request(t, http.MethodGet, path, nil) + rr := backend.request(t, http.MethodGet, path, header, nil) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) require.Equal(t, 1, backend.relays[1].GetRequestCount(path)) require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) // 2/2 failing responses are okay backend.relays[1].GetHeaderResponse = resp - rr = backend.request(t, http.MethodGet, path, nil) + rr = backend.request(t, http.MethodGet, path, header, nil) require.Equal(t, 2, backend.relays[0].GetRequestCount(path)) require.Equal(t, 2, backend.relays[1].GetRequestCount(path)) require.Equal(t, http.StatusNoContent, rr.Code) }) t.Run("Invalid relay public key", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + backend := newTestBackend(t, 1, time.Second) backend.relays[0].GetHeaderResponse = backend.relays[0].MakeGetHeaderResponse( @@ -331,7 +482,7 @@ func TestGetHeader(t *testing.T) { pk := phase0.BLSPubKey{} backend.boost.relays[0].PublicKey = pk - rr := backend.request(t, http.MethodGet, path, nil) + rr := backend.request(t, http.MethodGet, path, header, nil) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) // Request should have no content @@ -339,6 +490,9 @@ func TestGetHeader(t *testing.T) { }) t.Run("Invalid relay signature", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + backend := newTestBackend(t, 1, time.Second) backend.relays[0].GetHeaderResponse = backend.relays[0].MakeGetHeaderResponse( @@ -352,7 +506,7 @@ func TestGetHeader(t *testing.T) { // Scramble the signature backend.relays[0].GetHeaderResponse.Deneb.Signature = phase0.BLSSignature{} - rr := backend.request(t, http.MethodGet, path, nil) + rr := backend.request(t, http.MethodGet, path, header, nil) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) // Request should have no content @@ -360,42 +514,54 @@ func TestGetHeader(t *testing.T) { }) t.Run("Invalid slot number", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + // Number larger than uint64 creates parsing error slot := fmt.Sprintf("%d0", uint64(math.MaxUint64)) invalidSlotPath := fmt.Sprintf("/eth/v1/builder/header/%s/%s/%s", slot, hash.String(), pubkey.String()) backend := newTestBackend(t, 1, time.Second) - rr := backend.request(t, http.MethodGet, invalidSlotPath, nil) + rr := backend.request(t, http.MethodGet, invalidSlotPath, header, nil) require.JSONEq(t, `{"code":400,"message":"invalid slot"}`+"\n", rr.Body.String()) require.Equal(t, http.StatusBadRequest, rr.Code, rr.Body.String()) require.Equal(t, 0, backend.relays[0].GetRequestCount(path)) }) t.Run("Invalid pubkey length", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + invalidPubkeyPath := fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", 1, hash.String(), "0x1") backend := newTestBackend(t, 1, time.Second) - rr := backend.request(t, http.MethodGet, invalidPubkeyPath, nil) + rr := backend.request(t, http.MethodGet, invalidPubkeyPath, header, nil) require.JSONEq(t, `{"code":400,"message":"invalid pubkey"}`+"\n", rr.Body.String()) require.Equal(t, http.StatusBadRequest, rr.Code, rr.Body.String()) require.Equal(t, 0, backend.relays[0].GetRequestCount(path)) }) t.Run("Invalid hash length", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + invalidSlotPath := fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", 1, "0x1", pubkey.String()) backend := newTestBackend(t, 1, time.Second) - rr := backend.request(t, http.MethodGet, invalidSlotPath, nil) + rr := backend.request(t, http.MethodGet, invalidSlotPath, header, nil) require.JSONEq(t, `{"code":400,"message":"invalid hash"}`+"\n", rr.Body.String()) require.Equal(t, http.StatusBadRequest, rr.Code, rr.Body.String()) require.Equal(t, 0, backend.relays[0].GetRequestCount(path)) }) t.Run("Invalid parent hash", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + backend := newTestBackend(t, 1, time.Second) invalidParentHashPath := getHeaderPath(1, phase0.Hash32{}, pubkey) - rr := backend.request(t, http.MethodGet, invalidParentHashPath, nil) + rr := backend.request(t, http.MethodGet, invalidParentHashPath, header, nil) require.Equal(t, http.StatusNoContent, rr.Code) require.Equal(t, 0, backend.relays[0].GetRequestCount(path)) }) @@ -409,6 +575,9 @@ func TestGetHeaderBids(t *testing.T) { require.Equal(t, "/eth/v1/builder/header/2/0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7/0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", path) t.Run("Use header with highest value", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + // Create backend and register 3 relays. backend := newTestBackend(t, 3, time.Second) @@ -440,7 +609,7 @@ func TestGetHeaderBids(t *testing.T) { ) // Run the request. - rr := backend.request(t, http.MethodGet, path, nil) + rr := backend.request(t, http.MethodGet, path, header, nil) // Each relay must have received the request. require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) @@ -459,6 +628,9 @@ func TestGetHeaderBids(t *testing.T) { }) t.Run("Use header with lowest blockhash if same value", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + // Create backend and register 3 relays. backend := newTestBackend(t, 3, time.Second) @@ -487,7 +659,7 @@ func TestGetHeaderBids(t *testing.T) { ) // Run the request. - rr := backend.request(t, http.MethodGet, path, nil) + rr := backend.request(t, http.MethodGet, path, header, nil) // Each relay must have received the request. require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) @@ -510,6 +682,9 @@ func TestGetHeaderBids(t *testing.T) { }) t.Run("Respect minimum bid cutoff", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + // Create backend and register relay. backend := newTestBackend(t, 1, time.Second) @@ -523,7 +698,7 @@ func TestGetHeaderBids(t *testing.T) { ) // Run the request. - rr := backend.request(t, http.MethodGet, path, nil) + rr := backend.request(t, http.MethodGet, path, header, nil) // Each relay must have received the request. require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) @@ -533,6 +708,9 @@ func TestGetHeaderBids(t *testing.T) { }) t.Run("Allow bids which meet minimum bid cutoff", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + // Create backend and register relay. backend := newTestBackend(t, 1, time.Second) @@ -546,7 +724,7 @@ func TestGetHeaderBids(t *testing.T) { ) // Run the request. - rr := backend.request(t, http.MethodGet, path, nil) + rr := backend.request(t, http.MethodGet, path, header, nil) // Each relay must have received the request. require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) @@ -598,8 +776,11 @@ func TestGetPayload(t *testing.T) { } t.Run("Okay response from relay", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + backend := newTestBackend(t, 1, time.Second) - rr := backend.request(t, http.MethodPost, path, payload) + rr := backend.request(t, http.MethodPost, path, header, payload) require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) @@ -610,6 +791,9 @@ func TestGetPayload(t *testing.T) { }) t.Run("Bad response from relays", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + backend := newTestBackend(t, 2, time.Second) resp := &builderApi.VersionedSubmitBlindedBlockResponse{ Version: spec.DataVersionDeneb, @@ -623,7 +807,7 @@ func TestGetPayload(t *testing.T) { // 1/2 failing responses are okay backend.relays[0].GetPayloadResponse = resp - rr := backend.request(t, http.MethodPost, path, payload) + rr := backend.request(t, http.MethodPost, path, header, payload) require.GreaterOrEqual(t, backend.relays[1].GetRequestCount(path)+backend.relays[0].GetRequestCount(path), 1) require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) @@ -631,7 +815,7 @@ func TestGetPayload(t *testing.T) { backend = newTestBackend(t, 2, time.Second) backend.relays[0].GetPayloadResponse = resp backend.relays[1].GetPayloadResponse = resp - rr = backend.request(t, http.MethodPost, path, payload) + rr = backend.request(t, http.MethodPost, path, header, payload) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) require.Equal(t, 1, backend.relays[1].GetRequestCount(path)) require.JSONEq(t, `{"code":502,"message":"no successful relay response"}`+"\n", rr.Body.String()) @@ -639,6 +823,9 @@ func TestGetPayload(t *testing.T) { }) t.Run("Retries on error from relay", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + backend := newTestBackend(t, 1, 2*time.Second) count := 0 @@ -653,11 +840,14 @@ func TestGetPayload(t *testing.T) { } count++ }) - rr := backend.request(t, http.MethodPost, path, payload) + rr := backend.request(t, http.MethodPost, path, header, payload) require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) }) t.Run("Error after max retries are reached", func(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + backend := newTestBackend(t, 1, time.Second) count := 0 @@ -674,7 +864,7 @@ func TestGetPayload(t *testing.T) { require.NoError(t, err, "failed to write error response") //nolint:testifylint // if we fail here the test is compromised } }) - rr := backend.request(t, http.MethodPost, path, payload) + rr := backend.request(t, http.MethodPost, path, header, payload) require.Equal(t, 5, backend.relays[0].GetRequestCount(path)) require.JSONEq(t, `{"code":502,"message":"no successful relay response"}`+"\n", rr.Body.String()) require.Equal(t, http.StatusBadGateway, rr.Code, rr.Body.String()) @@ -825,6 +1015,9 @@ func denebExecutionPayloadAndBlobsBundle(header *deneb.ExecutionPayloadHeader, k } func TestGetPayloadForks(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + //nolint: forcetypeassert,thelper tests := []struct { fork string @@ -878,7 +1071,7 @@ func TestGetPayloadForks(t *testing.T) { // Prepare getPayload response backend.relays[0].GetPayloadResponse = blindedBlockToBlockResponse(signedBlindedBeaconBlock) // call getPayload, ensure it's only called on relay 0 (origin of the bid) - rr := backend.request(t, http.MethodPost, params.PathGetPayload, signedBlindedBeaconBlock) + rr := backend.request(t, http.MethodPost, params.PathGetPayload, header, signedBlindedBeaconBlock) require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) require.Equal(t, 1, backend.relays[0].GetRequestCount(params.PathGetPayload)) resp := new(builderApi.VersionedSubmitBlindedBlockResponse) @@ -891,6 +1084,9 @@ func TestGetPayloadForks(t *testing.T) { } func TestGetPayloadToAllRelays(t *testing.T) { + header := make(http.Header) + header.Set(HeaderAccept, MediaTypeJSON) + // Load the signed blinded beacon block used for getPayload jsonFile, err := os.Open("../testdata/signed-blinded-beacon-block-deneb.json") require.NoError(t, err) @@ -910,7 +1106,7 @@ func TestGetPayloadToAllRelays(t *testing.T) { "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", spec.DataVersionDeneb, ) - rr := backend.request(t, http.MethodGet, getHeaderPath, nil) + rr := backend.request(t, http.MethodGet, getHeaderPath, header, nil) require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) require.Equal(t, 1, backend.relays[0].GetRequestCount(getHeaderPath)) require.Equal(t, 1, backend.relays[1].GetRequestCount(getHeaderPath)) @@ -919,7 +1115,7 @@ func TestGetPayloadToAllRelays(t *testing.T) { backend.relays[0].GetPayloadResponse = blindedBlockToBlockResponse(signedBlindedBeaconBlock) // call getPayload, ensure it's called to all relays - rr = backend.request(t, http.MethodPost, params.PathGetPayload, signedBlindedBeaconBlock) + rr = backend.request(t, http.MethodPost, params.PathGetPayload, header, signedBlindedBeaconBlock) require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) require.Equal(t, 1, backend.relays[0].GetRequestCount(params.PathGetPayload)) require.Equal(t, 1, backend.relays[1].GetRequestCount(params.PathGetPayload)) diff --git a/server/types/errors.go b/server/types/errors.go index 54fdcd99..478cdd68 100644 --- a/server/types/errors.go +++ b/server/types/errors.go @@ -7,3 +7,9 @@ var ErrMissingRelayPubkey = errors.New("missing relay public key") // ErrPointAtInfinityPubkey is returned if a new RelayEntry URL has point-at-infinity public key. var ErrPointAtInfinityPubkey = errors.New("relay public key cannot be the point-at-infinity") + +// ErrInvalidContentType is returned when the response content type is invalid. +var ErrInvalidContentType = errors.New("invalid content type") + +// ErrMissingEthConsensusVersion is returned when the response is octet-stream but there is no "Eth-Consensus-Version" header. +var ErrMissingEthConsensusVersion = errors.New("missing eth-consensus-version") diff --git a/server/types/relay_entry.go b/server/types/relay_entry.go index 3ad7ac93..ec97f09f 100644 --- a/server/types/relay_entry.go +++ b/server/types/relay_entry.go @@ -10,8 +10,9 @@ import ( // RelayEntry represents a relay that mev-boost connects to. type RelayEntry struct { - PublicKey phase0.BLSPubKey - URL *url.URL + PublicKey phase0.BLSPubKey + URL *url.URL + SupportsSSZ bool } func (r *RelayEntry) String() string { @@ -72,3 +73,14 @@ func RelayEntriesToStrings(relays []RelayEntry) []string { } return ret } + +// Copy returns a deep copy of the relay entry. +func (r *RelayEntry) Copy() (ret RelayEntry) { + ret.PublicKey = r.PublicKey + ret.SupportsSSZ = r.SupportsSSZ + if r.URL != nil { + urlCopy := *r.URL + ret.URL = &urlCopy + } + return +} diff --git a/server/utils.go b/server/utils.go index 70ca747c..a4c546ce 100644 --- a/server/utils.go +++ b/server/utils.go @@ -26,12 +26,6 @@ import ( "github.com/sirupsen/logrus" ) -const ( - HeaderKeySlotUID = "X-MEVBoost-SlotID" - HeaderKeyVersion = "X-MEVBoost-Version" - HeaderStartTimeUnixMS = "X-MEVBoost-StartTimeUnixMS" -) - var ( errHTTPErrorResponse = errors.New("HTTP error response") errInvalidForkVersion = errors.New("invalid fork version")