From a32a1bf6f2fae4e379ca2b54e62813035a1ef492 Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Fri, 31 Jan 2025 15:43:20 +0100 Subject: [PATCH] Minor cleanup (#715) * server: refactor bidRespKey * server: split http handling and functionality --- server/functionality.go | 176 ++++++++++++++++++++++++++++++++++ server/service.go | 202 ++++++---------------------------------- server/utils.go | 6 -- 3 files changed, 204 insertions(+), 180 deletions(-) create mode 100644 server/functionality.go diff --git a/server/functionality.go b/server/functionality.go new file mode 100644 index 00000000..7aff0a8c --- /dev/null +++ b/server/functionality.go @@ -0,0 +1,176 @@ +package server + +import ( + "context" + "fmt" + "net/http" + "sync" + "time" + + builderSpec "github.com/attestantio/go-builder-client/spec" + "github.com/flashbots/mev-boost/config" + "github.com/flashbots/mev-boost/server/types" + "github.com/google/uuid" + "github.com/sirupsen/logrus" +) + +func (m *BoostService) getHeader(log *logrus.Entry, ua UserAgent, _slot uint64, pubkey, parentHashHex string) (bidResp, error) { + if len(pubkey) != 98 { + return bidResp{}, errInvalidPubkey + } + + if len(parentHashHex) != 66 { + return bidResp{}, errInvalidHash + } + + // Make sure we have a uid for this slot + m.slotUIDLock.Lock() + if m.slotUID.slot < _slot { + m.slotUID.slot = _slot + m.slotUID.uid = uuid.New() + } + slotUID := m.slotUID.uid + m.slotUIDLock.Unlock() + log = log.WithField("slotUID", slotUID) + + // Log how late into the slot the request starts + slotStartTimestamp := m.genesisTime + _slot*config.SlotTimeSec + msIntoSlot := uint64(time.Now().UTC().UnixMilli()) - slotStartTimestamp*1000 + log.WithFields(logrus.Fields{ + "genesisTime": m.genesisTime, + "slotTimeSec": config.SlotTimeSec, + "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()), + } + // Prepare relay responses + var ( + result = bidResp{} // the final response, containing the highest bid (if any) + relays = make(map[BlockHashHex][]types.RelayEntry) // relays that sent the bid for a specific blockHash + + mu sync.Mutex + wg sync.WaitGroup + ) + + // Call the relays + for _, relay := range m.relays { + wg.Add(1) + go func(relay types.RelayEntry) { + defer wg.Done() + path := fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", _slot, parentHashHex, pubkey) + url := relay.GetURI(path) + log := log.WithField("url", url) + responsePayload := new(builderSpec.VersionedSignedBuilderBid) + code, err := SendHTTPRequest(context.Background(), m.httpClientGetHeader, http.MethodGet, url, ua, headers, nil, responsePayload) + if err != nil { + log.WithError(err).Warn("error making request to relay") + return + } + + if code == http.StatusNoContent { + log.Debug("no-content response") + return + } + + // Skip if payload is empty + if responsePayload.IsEmpty() { + return + } + + // Getting the bid info will check if there are missing fields in the response + bidInfo, err := parseBidInfo(responsePayload) + if err != nil { + log.WithError(err).Warn("error parsing bid info") + return + } + + if bidInfo.blockHash == nilHash { + log.Warn("relay responded with empty block hash") + return + } + + valueEth := weiBigIntToEthBigFloat(bidInfo.value.ToBig()) + log = log.WithFields(logrus.Fields{ + "blockNumber": bidInfo.blockNumber, + "blockHash": bidInfo.blockHash.String(), + "txRoot": bidInfo.txRoot.String(), + "value": valueEth.Text('f', 18), + }) + + if relay.PublicKey.String() != bidInfo.pubkey.String() { + log.Errorf("bid pubkey mismatch. expected: %s - got: %s", relay.PublicKey.String(), bidInfo.pubkey.String()) + return + } + + // Verify the relay signature in the relay response + if !config.SkipRelaySignatureCheck { + ok, err := checkRelaySignature(responsePayload, m.builderSigningDomain, relay.PublicKey) + if err != nil { + log.WithError(err).Error("error verifying relay signature") + return + } + if !ok { + log.Error("failed to verify relay signature") + return + } + } + + // Verify response coherence with proposer's input data + if bidInfo.parentHash.String() != parentHashHex { + log.WithFields(logrus.Fields{ + "originalParentHash": parentHashHex, + "responseParentHash": bidInfo.parentHash.String(), + }).Error("proposer and relay parent hashes are not the same") + return + } + + isZeroValue := bidInfo.value.IsZero() + isEmptyListTxRoot := bidInfo.txRoot.String() == "0x7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1" + if isZeroValue || isEmptyListTxRoot { + log.Warn("ignoring bid with 0 value") + return + } + log.Debug("bid received") + + // Skip if value (fee) is lower than the minimum bid + if bidInfo.value.CmpBig(m.relayMinBid.BigInt()) == -1 { + log.Debug("ignoring bid below min-bid value") + return + } + + mu.Lock() + defer mu.Unlock() + + // 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) + + // Compare the bid with already known top bid (if any) + if !result.response.IsEmpty() { + valueDiff := bidInfo.value.Cmp(result.bidInfo.value) + if valueDiff == -1 { // current bid is less profitable than already known one + return + } else if valueDiff == 0 { // current bid is equally profitable as already known one. Use hash as tiebreaker + previousBidBlockHash := result.bidInfo.blockHash + if bidInfo.blockHash.String() >= previousBidBlockHash.String() { + return + } + } + } + + // Use this relay's response as mev-boost response because it's most profitable + log.Debug("new best bid") + result.response = *responsePayload + result.bidInfo = bidInfo + result.t = time.Now() + }(relay) + } + // Wait for all requests to complete... + wg.Wait() + + // Set the winning relay before returning + result.relays = relays[BlockHashHex(result.bidInfo.blockHash.String())] + return result, nil +} diff --git a/server/service.go b/server/service.go index 67eebe8e..bb5d46db 100644 --- a/server/service.go +++ b/server/service.go @@ -17,7 +17,6 @@ import ( builderApi "github.com/attestantio/go-builder-client/api" builderApiV1 "github.com/attestantio/go-builder-client/api/v1" - builderSpec "github.com/attestantio/go-builder-client/spec" 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" @@ -90,7 +89,7 @@ type BoostService struct { httpClientRegVal http.Client requestMaxRetries int - bids map[bidRespKey]bidResp // keeping track of bids, to log the originating relay on withholding + bids map[string]bidResp // keeping track of bids, to log the originating relay on withholding bidsLock sync.Mutex slotUID *slotUID @@ -116,7 +115,7 @@ func NewBoostService(opts BoostServiceOpts) (*BoostService, error) { relayCheck: opts.RelayCheck, relayMinBid: opts.RelayMinBid, genesisTime: opts.GenesisTime, - bids: make(map[bidRespKey]bidResp), + bids: make(map[string]bidResp), slotUID: &slotUID{}, builderSigningDomain: builderSigningDomain, @@ -292,12 +291,19 @@ func (m *BoostService) handleRegisterValidator(w http.ResponseWriter, req *http. // handleGetHeader requests bids from the relays func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) - slot := vars["slot"] - parentHashHex := vars["parent_hash"] - pubkey := vars["pubkey"] + var ( + vars = mux.Vars(req) + parentHashHex = vars["parent_hash"] + pubkey = vars["pubkey"] + ua = UserAgent(req.Header.Get("User-Agent")) + ) + + slot, err := strconv.ParseUint(vars["slot"], 10, 64) + if err != nil { + m.respondError(w, http.StatusBadRequest, errInvalidSlot.Error()) + return + } - ua := UserAgent(req.Header.Get("User-Agent")) log := m.log.WithFields(logrus.Fields{ "method": "getHeader", "slot": slot, @@ -307,174 +313,26 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) }) log.Debug("getHeader") - _slot, err := strconv.ParseUint(slot, 10, 64) + // Query the relays for the header + result, err := m.getHeader(log, ua, slot, pubkey, parentHashHex) if err != nil { - m.respondError(w, http.StatusBadRequest, errInvalidSlot.Error()) - return - } - - if len(pubkey) != 98 { - m.respondError(w, http.StatusBadRequest, errInvalidPubkey.Error()) - return - } - - if len(parentHashHex) != 66 { - m.respondError(w, http.StatusBadRequest, errInvalidHash.Error()) + m.respondError(w, http.StatusBadRequest, err.Error()) return } - // Make sure we have a uid for this slot - m.slotUIDLock.Lock() - if m.slotUID.slot < _slot { - m.slotUID.slot = _slot - m.slotUID.uid = uuid.New() - } - slotUID := m.slotUID.uid - m.slotUIDLock.Unlock() - log = log.WithField("slotUID", slotUID) - - // Log how late into the slot the request starts - slotStartTimestamp := m.genesisTime + _slot*config.SlotTimeSec - msIntoSlot := uint64(time.Now().UTC().UnixMilli()) - slotStartTimestamp*1000 - log.WithFields(logrus.Fields{ - "genesisTime": m.genesisTime, - "slotTimeSec": config.SlotTimeSec, - "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()), - } - // Prepare relay responses - result := bidResp{} // the final response, containing the highest bid (if any) - relays := make(map[BlockHashHex][]types.RelayEntry) // relays that sent the bid for a specific blockHash - // Call the relays - var mu sync.Mutex - var wg sync.WaitGroup - for _, relay := range m.relays { - wg.Add(1) - go func(relay types.RelayEntry) { - defer wg.Done() - path := fmt.Sprintf("/eth/v1/builder/header/%s/%s/%s", slot, parentHashHex, pubkey) - url := relay.GetURI(path) - log := log.WithField("url", url) - responsePayload := new(builderSpec.VersionedSignedBuilderBid) - code, err := SendHTTPRequest(context.Background(), m.httpClientGetHeader, http.MethodGet, url, ua, headers, nil, responsePayload) - if err != nil { - log.WithError(err).Warn("error making request to relay") - return - } - - if code == http.StatusNoContent { - log.Debug("no-content response") - return - } - - // Skip if payload is empty - if responsePayload.IsEmpty() { - return - } - - // Getting the bid info will check if there are missing fields in the response - bidInfo, err := parseBidInfo(responsePayload) - if err != nil { - log.WithError(err).Warn("error parsing bid info") - return - } - - if bidInfo.blockHash == nilHash { - log.Warn("relay responded with empty block hash") - return - } - - valueEth := weiBigIntToEthBigFloat(bidInfo.value.ToBig()) - log = log.WithFields(logrus.Fields{ - "blockNumber": bidInfo.blockNumber, - "blockHash": bidInfo.blockHash.String(), - "txRoot": bidInfo.txRoot.String(), - "value": valueEth.Text('f', 18), - }) - - if relay.PublicKey.String() != bidInfo.pubkey.String() { - log.Errorf("bid pubkey mismatch. expected: %s - got: %s", relay.PublicKey.String(), bidInfo.pubkey.String()) - return - } - - // Verify the relay signature in the relay response - if !config.SkipRelaySignatureCheck { - ok, err := checkRelaySignature(responsePayload, m.builderSigningDomain, relay.PublicKey) - if err != nil { - log.WithError(err).Error("error verifying relay signature") - return - } - if !ok { - log.Error("failed to verify relay signature") - return - } - } - - // Verify response coherence with proposer's input data - if bidInfo.parentHash.String() != parentHashHex { - log.WithFields(logrus.Fields{ - "originalParentHash": parentHashHex, - "responseParentHash": bidInfo.parentHash.String(), - }).Error("proposer and relay parent hashes are not the same") - return - } - - isZeroValue := bidInfo.value.IsZero() - isEmptyListTxRoot := bidInfo.txRoot.String() == "0x7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1" - if isZeroValue || isEmptyListTxRoot { - log.Warn("ignoring bid with 0 value") - return - } - log.Debug("bid received") - - // Skip if value (fee) is lower than the minimum bid - if bidInfo.value.CmpBig(m.relayMinBid.BigInt()) == -1 { - log.Debug("ignoring bid below min-bid value") - return - } - - mu.Lock() - defer mu.Unlock() - - // 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) - - // Compare the bid with already known top bid (if any) - if !result.response.IsEmpty() { - valueDiff := bidInfo.value.Cmp(result.bidInfo.value) - if valueDiff == -1 { // current bid is less profitable than already known one - return - } else if valueDiff == 0 { // current bid is equally profitable as already known one. Use hash as tiebreaker - previousBidBlockHash := result.bidInfo.blockHash - if bidInfo.blockHash.String() >= previousBidBlockHash.String() { - return - } - } - } - - // Use this relay's response as mev-boost response because it's most profitable - log.Debug("new best bid") - result.response = *responsePayload - result.bidInfo = bidInfo - result.t = time.Now() - }(relay) - } - // Wait for all requests to complete... - wg.Wait() - if result.response.IsEmpty() { log.Info("no bid received") w.WriteHeader(http.StatusNoContent) return } + // Remember the bid, for future logging in case of withholding + m.bidsLock.Lock() + m.bids[bidKey(slot, result.bidInfo.blockHash)] = result + m.bidsLock.Unlock() + // Log result valueEth := weiBigIntToEthBigFloat(result.bidInfo.value.ToBig()) - result.relays = relays[BlockHashHex(result.bidInfo.blockHash.String())] log.WithFields(logrus.Fields{ "blockHash": result.bidInfo.blockHash.String(), "blockNumber": result.bidInfo.blockNumber, @@ -483,16 +341,14 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) "relays": strings.Join(types.RelayEntriesToStrings(result.relays), ", "), }).Info("best bid") - // Remember the bid, for future logging in case of withholding - bidKey := bidRespKey{slot: _slot, blockHash: result.bidInfo.blockHash.String()} - m.bidsLock.Lock() - m.bids[bidKey] = result - m.bidsLock.Unlock() - // Return the bid m.respondOK(w, &result.response) } +func bidKey(slot uint64, blockHash phase0.Hash32) string { + return fmt.Sprintf("%v%v", slot, blockHash) +} + func (m *BoostService) processDenebPayload(w http.ResponseWriter, req *http.Request, log *logrus.Entry, blindedBlock *eth2ApiV1Deneb.SignedBlindedBeaconBlock) { // Get the currentSlotUID for this slot currentSlotUID := "" @@ -524,9 +380,8 @@ func (m *BoostService) processDenebPayload(w http.ResponseWriter, req *http.Requ }).Infof("submitBlindedBlock request start - %d milliseconds into slot %d", msIntoSlot, blindedBlock.Message.Slot) // Get the bid! - bidKey := bidRespKey{slot: uint64(blindedBlock.Message.Slot), blockHash: blindedBlock.Message.Body.ExecutionPayloadHeader.BlockHash.String()} m.bidsLock.Lock() - originalBid := m.bids[bidKey] + originalBid := m.bids[bidKey(uint64(blindedBlock.Message.Slot), blindedBlock.Message.Body.ExecutionPayloadHeader.BlockHash)] m.bidsLock.Unlock() if originalBid.response.IsEmpty() { log.Error("no bid for this getPayload payload found, was getHeader called before?") @@ -670,9 +525,8 @@ func (m *BoostService) processElectraPayload(w http.ResponseWriter, req *http.Re }).Infof("submitBlindedBlock request start - %d milliseconds into slot %d", msIntoSlot, blindedBlock.Message.Slot) // Get the bid! - bidKey := bidRespKey{slot: uint64(blindedBlock.Message.Slot), blockHash: blindedBlock.Message.Body.ExecutionPayloadHeader.BlockHash.String()} m.bidsLock.Lock() - originalBid := m.bids[bidKey] + originalBid := m.bids[bidKey(uint64(blindedBlock.Message.Slot), blindedBlock.Message.Body.ExecutionPayloadHeader.BlockHash)] m.bidsLock.Unlock() if originalBid.response.IsEmpty() { log.Error("no bid for this getPayload payload found, was getHeader called before?") diff --git a/server/utils.go b/server/utils.go index e9b5d0bf..55392525 100644 --- a/server/utils.go +++ b/server/utils.go @@ -164,12 +164,6 @@ type bidResp struct { relays []types.RelayEntry } -// bidRespKey is used as key for the bids cache -type bidRespKey struct { - slot uint64 - blockHash string -} - // bidInfo is used to store bid response fields for logging and validation type bidInfo struct { blockHash phase0.Hash32