diff --git a/cli/flags.go b/cli/flags.go index c37cfcac..9b166cbe 100644 --- a/cli/flags.go +++ b/cli/flags.go @@ -34,6 +34,7 @@ var flags = []cli.Flag{ timeoutGetPayloadFlag, timeoutRegValFlag, maxRetriesFlag, + privilegedBuildersFlag, } var ( @@ -171,4 +172,10 @@ var ( Value: 5, Category: RelayCategory, } + privilegedBuildersFlag = &cli.StringSliceFlag{ + Name: "privileged-builder", + Sources: cli.EnvVars("PRIVILEGED_BUILDER"), + Usage: "relay username/pubkey - single entry or comma-separated list", + Category: RelayCategory, + } ) diff --git a/cli/main.go b/cli/main.go index eaaa5b29..df49c023 100644 --- a/cli/main.go +++ b/cli/main.go @@ -65,9 +65,9 @@ func start(_ context.Context, cmd *cli.Command) error { } var ( - genesisForkVersion, genesisTime = setupGenesis(cmd) - relays, monitors, minBid, relayCheck = setupRelays(cmd) - listenAddr = cmd.String(addrFlag.Name) + genesisForkVersion, genesisTime = setupGenesis(cmd) + relays, monitors, privileged, minBid, relayCheck = setupRelays(cmd) + listenAddr = cmd.String(addrFlag.Name) ) opts := server.BoostServiceOpts{ @@ -75,6 +75,7 @@ func start(_ context.Context, cmd *cli.Command) error { ListenAddr: listenAddr, Relays: relays, RelayMonitors: monitors, + PrivilegedBuilders: privileged, GenesisForkVersionHex: genesisForkVersion, GenesisTime: genesisTime, RelayCheck: relayCheck, @@ -97,11 +98,12 @@ func start(_ context.Context, cmd *cli.Command) error { return service.StartHTTPServer() } -func setupRelays(cmd *cli.Command) (relayList, relayMonitorList, types.U256Str, bool) { +func setupRelays(cmd *cli.Command) (relayList, relayMonitorList, privilegedBuilderList, types.U256Str, bool) { // For backwards compatibility with the -relays flag. var ( - relays relayList - monitors relayMonitorList + relays relayList + monitors relayMonitorList + privileged privilegedBuilderList ) if cmd.IsSet(relaysFlag.Name) { relayURLs := cmd.StringSlice(relaysFlag.Name) @@ -117,9 +119,20 @@ func setupRelays(cmd *cli.Command) (relayList, relayMonitorList, types.U256Str, if len(relays) == 0 { log.Fatal("no relays specified") } + + if cmd.IsSet(privilegedBuildersFlag.Name) { + privilegedBuilders := cmd.StringSlice(privilegedBuildersFlag.Name) + for _, builder := range privilegedBuilders { + if err := privileged.Set(builder); err != nil { + log.WithError(err).WithField("privilegedBuilder", builder).Fatal("Invalid privileged builder") + } + } + } + log.Infof("using %d relays", len(relays)) for index, relay := range relays { - log.Infof("relay #%d: %s", index+1, relay.String()) + isPrivileged := privileged.Contains(relay.PublicKey) + log.Infof("relay #%d: %s, privileged %t", index+1, relay.String(), isPrivileged) } // For backwards compatibility with the -relay-monitors flag. @@ -148,7 +161,8 @@ func setupRelays(cmd *cli.Command) (relayList, relayMonitorList, types.U256Str, if relayMinBidWei.BigInt().Sign() > 0 { log.Infof("Min bid set to %v eth (%v wei)", cmd.Float(minBidFlag.Name), relayMinBidWei) } - return relays, monitors, *relayMinBidWei, cmd.Bool(relayCheckFlag.Name) + + return relays, monitors, privileged, *relayMinBidWei, cmd.Bool(relayCheckFlag.Name) } func setupGenesis(cmd *cli.Command) (string, uint64) { diff --git a/cli/types.go b/cli/types.go index 28c99b39..1b6cde8b 100644 --- a/cli/types.go +++ b/cli/types.go @@ -1,10 +1,13 @@ package cli import ( + "bytes" "errors" "net/url" "strings" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/flashbots/go-boost-utils/utils" "github.com/flashbots/mev-boost/server/types" ) @@ -67,3 +70,34 @@ func (rm *relayMonitorList) Set(value string) error { *rm = append(*rm, relayMonitor) return nil } + +type privilegedBuilderList []phase0.BLSPubKey + +func (pb *privilegedBuilderList) String() string { + privilegedBuilders := []string{} + for _, privilegedBuilder := range *pb { + privilegedBuilders = append(privilegedBuilders, privilegedBuilder.String()) + } + return strings.Join(privilegedBuilders, ",") +} + +func (pb *privilegedBuilderList) Contains(privilegedBuilder phase0.BLSPubKey) bool { + for _, entry := range *pb { + if bytes.Equal(entry[:], privilegedBuilder[:]) { + return true + } + } + return false +} + +func (pb *privilegedBuilderList) Set(value string) error { + privilegedBuilder, err := utils.HexToPubkey(value) + if err != nil { + return err + } + if pb.Contains(privilegedBuilder) { + return errDuplicateEntry + } + *pb = append(*pb, privilegedBuilder) + return nil +} diff --git a/server/service.go b/server/service.go index bdfc929e..f32a76b2 100644 --- a/server/service.go +++ b/server/service.go @@ -60,6 +60,7 @@ type BoostServiceOpts struct { ListenAddr string Relays []types.RelayEntry RelayMonitors []*url.URL + PrivilegedBuilders []phase0.BLSPubKey GenesisForkVersionHex string GenesisTime uint64 RelayCheck bool @@ -73,14 +74,15 @@ type BoostServiceOpts struct { // BoostService - the mev-boost service type BoostService struct { - listenAddr string - relays []types.RelayEntry - relayMonitors []*url.URL - log *logrus.Entry - srv *http.Server - relayCheck bool - relayMinBid types.U256Str - genesisTime uint64 + listenAddr string + relays []types.RelayEntry + relayMonitors []*url.URL + privilegedBuilders []phase0.BLSPubKey + log *logrus.Entry + srv *http.Server + relayCheck bool + relayMinBid types.U256Str + genesisTime uint64 builderSigningDomain phase0.Domain httpClientGetHeader http.Client @@ -107,15 +109,16 @@ func NewBoostService(opts BoostServiceOpts) (*BoostService, error) { } return &BoostService{ - listenAddr: opts.ListenAddr, - relays: opts.Relays, - relayMonitors: opts.RelayMonitors, - log: opts.Log, - relayCheck: opts.RelayCheck, - relayMinBid: opts.RelayMinBid, - genesisTime: opts.GenesisTime, - bids: make(map[bidRespKey]bidResp), - slotUID: &slotUID{}, + listenAddr: opts.ListenAddr, + relays: opts.Relays, + relayMonitors: opts.RelayMonitors, + log: opts.Log, + relayCheck: opts.RelayCheck, + relayMinBid: opts.RelayMinBid, + privilegedBuilders: opts.PrivilegedBuilders, + genesisTime: opts.GenesisTime, + bids: make(map[bidRespKey]bidResp), + slotUID: &slotUID{}, builderSigningDomain: builderSigningDomain, httpClientGetHeader: http.Client{ @@ -289,7 +292,7 @@ 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) { +func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) { //nolint:maintidx vars := mux.Vars(req) slot := vars["slot"] parentHashHex := vars["parent_hash"] @@ -346,6 +349,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) } // Prepare relay responses result := bidResp{} // the final response, containing the highest bid (if any) + resultPrivileged := bidResp{} // the final response, containing the highest bid (if any) for privileged relays relays := make(map[BlockHashHex][]types.RelayEntry) // relays that sent the bid for a specific blockHash // Call the relays var mu sync.Mutex @@ -441,35 +445,46 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) // 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 - } - } + if m.isPrivilegedRelay(relay.PublicKey) { + m.setBestBid(&resultPrivileged, bidInfo, responsePayload, log) + } else { + m.setBestBid(&result, bidInfo, responsePayload, log) } - - // 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() { + if resultPrivileged.response.IsEmpty() && result.response.IsEmpty() { log.Info("no bid received") w.WriteHeader(http.StatusNoContent) return } + if !resultPrivileged.response.IsEmpty() { + // Log result privileged + valueEth := weiBigIntToEthBigFloat(resultPrivileged.bidInfo.value.ToBig()) + resultPrivileged.relays = relays[BlockHashHex(resultPrivileged.bidInfo.blockHash.String())] + log.WithFields(logrus.Fields{ + "blockHash": resultPrivileged.bidInfo.blockHash.String(), + "blockNumber": resultPrivileged.bidInfo.blockNumber, + "txRoot": resultPrivileged.bidInfo.txRoot.String(), + "value": valueEth.Text('f', 18), + "relays": strings.Join(types.RelayEntriesToStrings(resultPrivileged.relays), ", "), + "privileged": true, + }).Info("best privileged bid") + + // Remember the bid, for future logging in case of withholding + bidKey := bidRespKey{slot: _slot, blockHash: resultPrivileged.bidInfo.blockHash.String()} + m.bidsLock.Lock() + m.bids[bidKey] = resultPrivileged + m.bidsLock.Unlock() + + // Return the bid + m.respondOK(w, &resultPrivileged.response) + return + } + // Log result valueEth := weiBigIntToEthBigFloat(result.bidInfo.value.ToBig()) result.relays = relays[BlockHashHex(result.bidInfo.blockHash.String())] @@ -479,6 +494,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) "txRoot": result.bidInfo.txRoot.String(), "value": valueEth.Text('f', 18), "relays": strings.Join(types.RelayEntriesToStrings(result.relays), ", "), + "privileged": false, }).Info("best bid") // Remember the bid, for future logging in case of withholding @@ -491,6 +507,27 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) m.respondOK(w, &result.response) } +func (m *BoostService) setBestBid(result *bidResp, bidInfo bidInfo, responsePayload *builderSpec.VersionedSignedBuilderBid, log *logrus.Entry) { + // 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() +} + func (m *BoostService) processDenebPayload(w http.ResponseWriter, req *http.Request, log *logrus.Entry, blindedBlock *eth2ApiV1Deneb.SignedBlindedBeaconBlock) { // Get the currentSlotUID for this slot currentSlotUID := "" @@ -688,3 +725,12 @@ func (m *BoostService) CheckRelays() int { wg.Wait() return int(numSuccessRequestsToRelay) } + +func (m *BoostService) isPrivilegedRelay(pubkey phase0.BLSPubKey) bool { + for _, builder := range m.privilegedBuilders { + if bytes.Equal(builder[:], pubkey[:]) { + return true + } + } + return false +}