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 committed Sep 9, 2024
1 parent 0b1c49f commit 919fb74
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 70 deletions.
7 changes: 7 additions & 0 deletions cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var flags = []cli.Flag{
timeoutGetPayloadFlag,
timeoutRegValFlag,
maxRetriesFlag,
privilegedBuildersFlag,
}

var (
Expand Down Expand Up @@ -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,
}
)
30 changes: 22 additions & 8 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,17 @@ 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{
Log: log,
ListenAddr: listenAddr,
Relays: relays,
RelayMonitors: monitors,
PrivilegedBuilders: privileged,
GenesisForkVersionHex: genesisForkVersion,
GenesisTime: genesisTime,
RelayCheck: relayCheck,
Expand All @@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -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) {
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/types"
)

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/mock_relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,6 @@ import (
"github.com/stretchr/testify/require"
)

const (
mockRelaySecretKeyHex = "0x4e343a647c5a5c44d76c2c58b63f02cdf3a9a0ec40f102ebc26363b4b1b95033"
)

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

// Relay 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 @@ -72,15 +62,19 @@ type Relay struct {
// A secret key must be provided to sign default and custom response messages
func NewRelay(t *testing.T) *Relay {
t.Helper()
relay := &Relay{t: t, secretKey: mockRelaySecretKey, publicKey: mockRelayPublicKey, requestCount: make(map[string]int)}

relay := &Relay{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 = types.NewRelayEntry(urlWithKey)
require.NoError(t, err)
return relay
Expand Down Expand Up @@ -224,7 +218,7 @@ func (m *Relay) defaultHandleGetHeader(w http.ResponseWriter) {
12345,
"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
m.RelayEntry.PublicKey.String(),
spec.DataVersionDeneb,
)
if m.GetHeaderResponse != nil {
Expand Down
118 changes: 82 additions & 36 deletions server/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type BoostServiceOpts struct {
ListenAddr string
Relays []types.RelayEntry
RelayMonitors []*url.URL
PrivilegedBuilders []phase0.BLSPubKey
GenesisForkVersionHex string
GenesisTime uint64
RelayCheck bool
Expand All @@ -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
Expand All @@ -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{
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())]
Expand All @@ -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
Expand All @@ -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 := ""
Expand Down Expand Up @@ -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
}
Loading

0 comments on commit 919fb74

Please sign in to comment.