Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
8ed5f68
Add ssz support for getHeader
jtraglia Feb 5, 2025
498d8fc
Request SSZ then JSON
jtraglia Feb 5, 2025
a0cc8a0
Read response body after checking for success
jtraglia Feb 5, 2025
3005de8
Request what the client asks for first
jtraglia Feb 5, 2025
3afd060
Decode based on resp content type
jtraglia Feb 5, 2025
f65e83d
Return the best bid in the request encoding
jtraglia Feb 5, 2025
2073a41
Fix some nits
jtraglia Feb 5, 2025
b4b9088
Fix debug message
jtraglia Feb 5, 2025
b1cda56
Always request header from relay in SSZ first
jtraglia Feb 5, 2025
097c625
Merge branch 'develop' into ssz-get-header
jtraglia Feb 5, 2025
db3f598
Revert "Always request header from relay in SSZ first"
jtraglia Feb 5, 2025
6545540
Update about request (SSZ and JSON) order
jtraglia Feb 5, 2025
735bcbd
Update mock_relay.defaultHandleGetHeader to handle SSZ
jtraglia Feb 5, 2025
881cff0
Add a test & fix a couple bugs
jtraglia Feb 5, 2025
8d1073f
Fix lint
jtraglia Feb 5, 2025
5342231
Address review feedback
jtraglia Feb 5, 2025
db84842
Fix double close
jtraglia Feb 5, 2025
1e5ff29
Use consistent log message
jtraglia Feb 5, 2025
357212a
Fix body close
jtraglia Feb 5, 2025
8e5b644
One request per relay
jtraglia Feb 6, 2025
5014440
Respond with client's favorite except
jtraglia Feb 6, 2025
6171a13
Add accept file
jtraglia Feb 6, 2025
a860bf2
Move content types to new file
jtraglia Feb 6, 2025
5761355
Default respond with JSON
jtraglia Feb 6, 2025
e7d51de
Fix nits
jtraglia Feb 6, 2025
25462d6
Add SupportsSSZ field to RelayEntry
jtraglia Feb 6, 2025
3f50e91
Improve media type handling
jtraglia Feb 6, 2025
0cde21a
Return NotAcceptable if the only accept type is unsupported
jtraglia Feb 6, 2025
4c9f49b
Fix lint
jtraglia Feb 6, 2025
7c27374
Remove now incorrect comment
jtraglia Feb 6, 2025
78463c4
Return JSON in getHeader under some circumstances
jtraglia Feb 11, 2025
17810c2
Include ethConsensusVersion in getHeader response
jtraglia Feb 11, 2025
461ceab
Decode header with ethConsensusVersion from response
jtraglia Feb 11, 2025
a405b92
Fix & improve tests
jtraglia Feb 11, 2025
f607ce3
Update comment
jtraglia Feb 13, 2025
160eb66
Merge branch 'develop' into ssz-get-header
jtraglia Feb 13, 2025
1ff636c
Do away with function map
jtraglia Feb 13, 2025
fd18b82
Pass header fields, not the whole header
jtraglia Feb 13, 2025
0e38a22
Add back HeaderKeySlotUID
jtraglia Feb 13, 2025
8cd3b0e
Use timewasted/go-accept-headers
jtraglia Feb 13, 2025
5b6caba
Clean up header fields
jtraglia Feb 14, 2025
3fbeafd
Fix some nits & add more debug prints
jtraglia Feb 14, 2025
dc7aaaa
Address review feedback
jtraglia Feb 14, 2025
fbfc3bd
Merge branch 'develop' into ssz-get-header
jtraglia Feb 14, 2025
dc70b85
Replace "client" with "proposer" in comments
jtraglia Feb 14, 2025
d3123bf
Remove unnecessary debug print
jtraglia Feb 14, 2025
744273a
Rename some variables & fix a nit
jtraglia Feb 14, 2025
5a919ff
Merge branch 'develop' into ssz-get-header
jtraglia Feb 14, 2025
de43986
Add another debug print
jtraglia Feb 14, 2025
3c85273
Use more header constants
jtraglia Feb 17, 2025
6f8b538
Rename media_type.go to constants.go & move header constants
jtraglia Feb 17, 2025
bdf4c6a
Fix nits
jtraglia Feb 17, 2025
93a4755
Add EthConsensusVersion constants
jtraglia Feb 17, 2025
14442d1
Fix linter
jtraglia Feb 17, 2025
1fcd5ff
Remove allBidsWereJSON logic
jtraglia Feb 17, 2025
a745a43
Fix lint
jtraglia Feb 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ linters:
- goconst
- gosec
- ireturn
- maintidx
- noctx
- tagliatelle
- perfsprint
Expand Down
135 changes: 124 additions & 11 deletions server/get_header.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ package server

import (
"context"
"encoding/json"
"fmt"
"io"
"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"
Expand All @@ -16,7 +23,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, header http.Header) (bidResp, error) {
// Ensure arguments are valid
if len(pubkey) != 98 {
return bidResp{}, errInvalidPubkey
Expand Down Expand Up @@ -44,11 +51,9 @@ 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()),
}
// Get the optional version, used with SSZ decoding
ethConsensusVersion := header.Get("Eth-Consensus-Version")
log = log.WithField("ethConsensusVersion", ethConsensusVersion)

var (
mu sync.Mutex
Expand All @@ -71,18 +76,90 @@ 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)
// A method for sending the request with a specific accept header value
doRequest := func(accept string) (*http.Response, error) {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if err != nil {
log.WithError(err).Warn("error creating new request")
return nil, err
}
for key, values := range header {
req.Header[key] = values
}
req.Header.Set("Accept", accept)
return m.httpClientGetHeader.Do(req)
}

// Send the get bid request to the relay. Try what the client
// accepts (either SSZ or JSON) first. If no accept type is specified,
// request JSON. We cannot request SSZ if the client does not, because
// the appropriate Eth-Consensus-Version header value is not known.
var err error
var resp *http.Response
switch header.Get("Accept") {
case "application/octet-stream":
log.Debug("requesting header in SSZ")
resp, err = doRequest("application/octet-stream")
if err != nil {
log.WithError(err).Warn("error calling getHeader on relay")
return
}

// Check if the relay supports SSZ requests
if resp.StatusCode != http.StatusNotAcceptable {
// The relay didn't complain about the accept value.
// This means we should try processing the response.
log.Debug("response indicated SSZ is accepted")
break
}

// The response status was NotAcceptable.
// This means we should try again with JSON.
log.Debug("response indicated SSZ is not accepted")
resp.Body.Close()
fallthrough
default:
log.Debug("requesting header in JSON")
resp, err = doRequest("application/json")
}
if err != nil {
log.WithError(err).Warn("error making request to relay")
log.WithError(err).Warn("error calling getHeader on relay")
return
}
if code == http.StatusNoContent {
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
respContentType := resp.Header.Get("Content-Type")
log = log.WithField("respContentType", respContentType)

// Decode bid
bid := new(builderSpec.VersionedSignedBuilderBid)
err = decodeBid(respBytes, respContentType, ethConsensusVersion, bid)
if err != nil {
log.WithError(err).Warn("error decoding bid")
return
}

// Skip if bid is empty
if bid.IsEmpty() {
return
Expand Down Expand Up @@ -189,3 +266,39 @@ 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 "application/octet-stream":
if ethConsensusVersion != "" {
// Do SSZ decoding
switch ethConsensusVersion {
case "bellatrix":
bid.Version = spec.DataVersionBellatrix
bid.Bellatrix = new(builderApiBellatrix.SignedBuilderBid)
return bid.Bellatrix.UnmarshalSSZ(respBytes)
case "capella":
bid.Version = spec.DataVersionCapella
bid.Capella = new(builderApiCapella.SignedBuilderBid)
return bid.Capella.UnmarshalSSZ(respBytes)
case "deneb":
bid.Version = spec.DataVersionDeneb
bid.Deneb = new(builderApiDeneb.SignedBuilderBid)
return bid.Deneb.UnmarshalSSZ(respBytes)
case "electra":
bid.Version = spec.DataVersionElectra
bid.Electra = new(builderApiElectra.SignedBuilderBid)
return bid.Electra.UnmarshalSSZ(respBytes)
default:
return errInvalidForkVersion
}
} else {
return types.ErrMissingEthConsensusVersion
}
case "application/json":
// Do JSON decoding
return json.Unmarshal(respBytes, bid)
}
return types.ErrInvalidContentType
}
32 changes: 23 additions & 9 deletions server/mock/mock_relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,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),
Expand Down Expand Up @@ -259,15 +260,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,
Expand All @@ -280,9 +277,26 @@ 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
if req.Header.Get("Accept") == "application/octet-stream" {
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
}
} else {
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
}
}
}

Expand Down
51 changes: 49 additions & 2 deletions server/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
eth2ApiV1Capella "github.com/attestantio/go-eth2-client/api/v1/capella"
eth2ApiV1Deneb "github.com/attestantio/go-eth2-client/api/v1/deneb"
eth2ApiV1Electra "github.com/attestantio/go-eth2-client/api/v1/electra"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/flashbots/go-boost-utils/ssz"
"github.com/flashbots/go-utils/httplogger"
Expand Down Expand Up @@ -315,8 +316,13 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request)
})
log.Debug("getHeader")

// Additional header fields
header := req.Header
header.Set("User-Agent", wrapUserAgent(ua))
header.Set(HeaderStartTimeUnixMS, fmt.Sprintf("%d", time.Now().UTC().UnixMilli()))

// Query the relays for the header
result, err := m.getHeader(log, ua, slot, pubkey, parentHashHex)
result, err := m.getHeader(log, slot, pubkey, parentHashHex, header)
if err != nil {
m.respondError(w, http.StatusBadRequest, err.Error())
return
Expand All @@ -333,9 +339,13 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request)
m.bids[bidKey(slot, result.bidInfo.blockHash)] = result
m.bidsLock.Unlock()

// How should we respond to the client
acceptFromClient := req.Header.Get("Accept")

// Log result
valueEth := weiBigIntToEthBigFloat(result.bidInfo.value.ToBig())
log.WithFields(logrus.Fields{
"acceptType": acceptFromClient,
"blockHash": result.bidInfo.blockHash.String(),
"blockNumber": result.bidInfo.blockNumber,
"txRoot": result.bidInfo.txRoot.String(),
Expand All @@ -344,7 +354,44 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request)
}).Info("best bid")

// Return the bid
m.respondOK(w, &result.response)
switch acceptFromClient {
case "application/octet-stream":
w.Header().Set("Content-Type", "application/octet-stream")
w.WriteHeader(http.StatusOK)

// Serialize the response
var sszData []byte
switch result.response.Version {
case spec.DataVersionBellatrix:
sszData, err = result.response.Bellatrix.MarshalSSZ()
case spec.DataVersionCapella:
sszData, err = result.response.Capella.MarshalSSZ()
case spec.DataVersionDeneb:
sszData, err = result.response.Deneb.MarshalSSZ()
case spec.DataVersionElectra:
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 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)
}
default:
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
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)
}
}
}

// respondPayload responds to the proposer with the payload
Expand Down
Loading
Loading