Skip to content

Commit

Permalink
feat: privileged-builders
Browse files Browse the repository at this point in the history
Privileged builders are a list of public keys of builders from which
bids will be accepted first, even if the bid is lower. If no bids are
received from the privileged builders, bids from other builders will be
accepted.

This is useful when you have a special contract with specific relays and
you would like to have their blocks used instead of other relays. While
still having other relays as a fallback.
  • Loading branch information
MrKoberman authored and zimbatm committed Jun 28, 2024
1 parent 8d36fc9 commit 84363f2
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 73 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,10 @@ Usage of mev-boost:
relay monitor urls - single entry or comma-separated list (scheme://host)
-relays string
relay urls - single entry or comma-separated list (scheme://pubkey@host)
-privileged-builder
a single privileged builder(relay pubkey), can be specified multiple times
-privileged-builders string
privileged builders(relay pubkey) - single entry or comma-separated list
-request-timeout-getheader int
timeout for getHeader requests to the relay [ms] (default 950)
-request-timeout-getpayload int
Expand Down
54 changes: 35 additions & 19 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,18 @@ var (
errInvalidLoglevel = errors.New("invalid loglevel")

// defaults
defaultLogJSON = os.Getenv("LOG_JSON") != ""
defaultLogLevel = common.GetEnv("LOG_LEVEL", "info")
defaultListenAddr = common.GetEnv("BOOST_LISTEN_ADDR", "localhost:18550")
defaultRelayCheck = os.Getenv("RELAY_STARTUP_CHECK") != ""
defaultRelayMinBidEth = common.GetEnvFloat64("MIN_BID_ETH", 0)
defaultDisableLogVersion = os.Getenv("DISABLE_LOG_VERSION") == "1" // disables adding the version to every log entry
defaultDebug = os.Getenv("DEBUG") != ""
defaultLogServiceTag = os.Getenv("LOG_SERVICE_TAG")
defaultRelays = os.Getenv("RELAYS")
defaultRelayMonitors = os.Getenv("RELAY_MONITORS")
defaultMaxRetries = common.GetEnvInt("REQUEST_MAX_RETRIES", 5)
defaultLogJSON = os.Getenv("LOG_JSON") != ""
defaultLogLevel = common.GetEnv("LOG_LEVEL", "info")
defaultListenAddr = common.GetEnv("BOOST_LISTEN_ADDR", "localhost:18550")
defaultRelayCheck = os.Getenv("RELAY_STARTUP_CHECK") != ""
defaultRelayMinBidEth = common.GetEnvFloat64("MIN_BID_ETH", 0)
defaultDisableLogVersion = os.Getenv("DISABLE_LOG_VERSION") == "1" // disables adding the version to every log entry
defaultDebug = os.Getenv("DEBUG") != ""
defaultLogServiceTag = os.Getenv("LOG_SERVICE_TAG")
defaultRelays = os.Getenv("RELAYS")
defaultRelayMonitors = os.Getenv("RELAY_MONITORS")
defaultMaxRetries = common.GetEnvInt("REQUEST_MAX_RETRIES", 5)
defaultPrivilegedBuilders = os.Getenv("PRIVILEGED_BUILDERS")

defaultGenesisForkVersion = common.GetEnv("GENESIS_FORK_VERSION", "")
defaultGenesisTime = common.GetEnvInt("GENESIS_TIMESTAMP", -1)
Expand All @@ -54,8 +55,9 @@ var (
defaultTimeoutMsGetPayload = common.GetEnvInt("RELAY_TIMEOUT_MS_GETPAYLOAD", 4000) // timeout for getPayload requests
defaultTimeoutMsRegisterValidator = common.GetEnvInt("RELAY_TIMEOUT_MS_REGVAL", 3000) // timeout for registerValidator requests

relays relayList
relayMonitors relayMonitorList
relays relayList
relayMonitors relayMonitorList
privilegedBuilders privilegedBuilderList

// cli flags
printVersion = flag.Bool("version", false, "only print version")
Expand All @@ -65,11 +67,12 @@ var (
logService = flag.String("log-service", defaultLogServiceTag, "add a 'service=...' tag to all log messages")
logNoVersion = flag.Bool("log-no-version", defaultDisableLogVersion, "disables adding the version to every log entry")

listenAddr = flag.String("addr", defaultListenAddr, "listen-address for mev-boost server")
relayURLs = flag.String("relays", defaultRelays, "relay urls - single entry or comma-separated list (scheme://pubkey@host)")
relayCheck = flag.Bool("relay-check", defaultRelayCheck, "check relay status on startup and on the status API call")
relayMinBidEth = flag.Float64("min-bid", defaultRelayMinBidEth, "minimum bid to accept from a relay [eth]")
relayMonitorURLs = flag.String("relay-monitors", defaultRelayMonitors, "relay monitor urls - single entry or comma-separated list (scheme://host)")
listenAddr = flag.String("addr", defaultListenAddr, "listen-address for mev-boost server")
relayURLs = flag.String("relays", defaultRelays, "relay urls - single entry or comma-separated list (scheme://pubkey@host)")
relayCheck = flag.Bool("relay-check", defaultRelayCheck, "check relay status on startup and on the status API call")
relayMinBidEth = flag.Float64("min-bid", defaultRelayMinBidEth, "minimum bid to accept from a relay [eth]")
relayMonitorURLs = flag.String("relay-monitors", defaultRelayMonitors, "relay monitor urls - single entry or comma-separated list (scheme://host)")
privilegedBuilderKeys = flag.String("privileged-builders", defaultPrivilegedBuilders, "single entry or comma-separated list of relay username (pubkey)")

relayTimeoutMsGetHeader = flag.Int("request-timeout-getheader", defaultTimeoutMsGetHeader, "timeout for getHeader requests to the relay [ms]")
relayTimeoutMsGetPayload = flag.Int("request-timeout-getpayload", defaultTimeoutMsGetPayload, "timeout for getPayload requests to the relay [ms]")
Expand All @@ -94,6 +97,7 @@ func Main() {
// process repeatable flags
flag.Var(&relays, "relay", "a single relay, can be specified multiple times")
flag.Var(&relayMonitors, "relay-monitor", "a single relay monitor, can be specified multiple times")
flag.Var(&privilegedBuilders, "privileged-builder", "a single privileged builder, can be specified multiple times")

// parse flags and get started
flag.Parse()
Expand Down Expand Up @@ -148,13 +152,24 @@ func Main() {
}
}

// set relay priorities
if *privilegedBuilderKeys != "" {
for _, builderKey := range strings.Split(*privilegedBuilderKeys, ",") {
err := privilegedBuilders.Set(strings.TrimSpace(builderKey))
if err != nil {
log.WithError(err).WithField("privilegedBuilder", builderKey).Fatal("Invalid privileged builder")
}
}
}

if len(relays) == 0 {
flag.Usage()
log.Fatal("no relays specified")
}
log.Infof("using %d relays", len(relays))
for index, relay := range relays {
log.Infof("relay #%d: %s", index+1, relay.String())
isPrivileged := privilegedBuilders.Contains(relay.PublicKey)
log.Infof("relay #%d: %s, privileged %t", index+1, relay.String(), isPrivileged)
}

// For backwards compatibility with the -relay-monitors flag.
Expand Down Expand Up @@ -196,6 +211,7 @@ func Main() {
ListenAddr: *listenAddr,
Relays: relays,
RelayMonitors: relayMonitors,
PrivilegedBuilders: privilegedBuilders,
GenesisForkVersionHex: genesisForkVersionHex,
GenesisTime: genesisTime,
RelayCheck: *relayCheck,
Expand Down
34 changes: 34 additions & 0 deletions cli/types.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand Down Expand Up @@ -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
}
20 changes: 7 additions & 13 deletions server/mock_relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,6 @@ import (
"github.com/stretchr/testify/require"
)

const (
mockRelaySecretKeyHex = "0x4e343a647c5a5c44d76c2c58b63f02cdf3a9a0ec40f102ebc26363b4b1b95033"
)

var (
skBytes, _ = hexutil.Decode(mockRelaySecretKeyHex)
mockRelaySecretKey, _ = bls.SecretKeyFromBytes(skBytes)
mockRelayPublicKey, _ = bls.PublicKeyFromSecretKey(mockRelaySecretKey)
)

// mockRelay is used to fake a relay's behavior.
// You can override each of its handler by setting the instance's HandlerOverride_METHOD_TO_OVERRIDE to your own
// handler.
Expand Down Expand Up @@ -71,15 +61,19 @@ type mockRelay struct {
// A secret key must be provided to sign default and custom response messages
func newMockRelay(t *testing.T) *mockRelay {
t.Helper()
relay := &mockRelay{t: t, secretKey: mockRelaySecretKey, publicKey: mockRelayPublicKey, requestCount: make(map[string]int)}

relay := &mockRelay{t: t, requestCount: make(map[string]int)}

relay.secretKey, _, _ = bls.GenerateNewKeypair()
relay.publicKey, _ = bls.PublicKeyFromSecretKey(relay.secretKey)

// Initialize server
relay.Server = httptest.NewServer(relay.getRouter())

// Create the RelayEntry with correct pubkey
url, err := url.Parse(relay.Server.URL)
require.NoError(t, err)
urlWithKey := fmt.Sprintf("%s://%s@%s", url.Scheme, hexutil.Encode(bls.PublicKeyToBytes(mockRelayPublicKey)), url.Host)
urlWithKey := fmt.Sprintf("%s://%s@%s", url.Scheme, hexutil.Encode(bls.PublicKeyToBytes(relay.publicKey)), url.Host)
relay.RelayEntry, err = NewRelayEntry(urlWithKey)
require.NoError(t, err)
return relay
Expand Down Expand Up @@ -244,7 +238,7 @@ func (m *mockRelay) defaultHandleGetHeader(w http.ResponseWriter) {
12345,
"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
m.RelayEntry.PublicKey.String(),
spec.DataVersionCapella,
)
if m.GetHeaderResponse != nil {
Expand Down
102 changes: 74 additions & 28 deletions server/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type BoostServiceOpts struct {
ListenAddr string
Relays []RelayEntry
RelayMonitors []*url.URL
PrivilegedBuilders []phase0.BLSPubKey
GenesisForkVersionHex string
GenesisTime uint64
RelayCheck bool
Expand All @@ -84,6 +85,7 @@ type BoostService struct {
listenAddr string
relays []RelayEntry
relayMonitors []*url.URL
privilegedBuilders []phase0.BLSPubKey
log *logrus.Entry
srv *http.Server
relayCheck bool
Expand Down Expand Up @@ -115,15 +117,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,
privilegedBuilders: opts.PrivilegedBuilders,
log: opts.Log,
relayCheck: opts.RelayCheck,
relayMinBid: opts.RelayMinBid,
genesisTime: opts.GenesisTime,
bids: make(map[bidRespKey]bidResp),
slotUID: &slotUID{},

builderSigningDomain: builderSigningDomain,
httpClientGetHeader: http.Client{
Expand Down Expand Up @@ -309,7 +312,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"]
Expand Down Expand Up @@ -367,6 +370,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][]RelayEntry) // relays that sent the bid for a specific blockHash

// Call the relays
Expand Down Expand Up @@ -463,36 +467,47 @@ 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(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())]
Expand All @@ -502,6 +517,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request)
"txRoot": result.bidInfo.txRoot.String(),
"value": valueEth.Text('f', 18),
"relays": strings.Join(RelayEntriesToStrings(result.relays), ", "),
"privileged": false,
}).Info("best bid")

// Remember the bid, for future logging in case of withholding
Expand All @@ -514,6 +530,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) processCapellaPayload(w http.ResponseWriter, req *http.Request, log *logrus.Entry, payload *eth2ApiV1Capella.SignedBlindedBeaconBlock, body []byte) {
if payload.Message == nil || payload.Message.Body == nil || payload.Message.Body.ExecutionPayloadHeader == nil {
log.WithField("body", string(body)).Error("missing parts of the request payload from the beacon-node")
Expand Down Expand Up @@ -854,3 +891,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
}
Loading

0 comments on commit 84363f2

Please sign in to comment.