Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
170 changes: 170 additions & 0 deletions cli/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package cli

import (
"os"
"path/filepath"
"strings"

"github.com/flashbots/mev-boost/server/types"
"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
)

type RelayConfigYAML struct {
URL string `yaml:"url"`
ID string `yaml:"id"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do relays need an ID outside the URL?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optionally if you want. URLs can be too long when being read in metrics so you can essentially give an alias

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in other places, just the hostname was a good enough ID and short one. seems better to use only the hostname for metrics too?

EnableTimingGames bool `yaml:"enable_timing_games"`
TargetFirstRequestMs uint64 `yaml:"target_first_request_ms"`
FrequencyGetHeaderMs uint64 `yaml:"frequency_getheader_ms"`
}

// Config holds all configuration settings from the config file
type Config struct {
TimeoutGetHeaderMs uint64 `yaml:"timeout_get_header_ms"`
LateInSlotTimeMs uint64 `yaml:"late_in_slot_time_ms"`
Relays []RelayConfigYAML `yaml:"relays"`
}

type ConfigResult struct {
RelayConfigs map[string]types.RelayConfig
TimeoutGetHeaderMs uint64
LateInSlotTimeMs uint64
}

// ConfigWatcher provides hot reloading of config files
type ConfigWatcher struct {
v *viper.Viper
configPath string
cliRelays []types.RelayEntry
onConfigChange func(*ConfigResult)
log *logrus.Entry
}

// LoadConfigFile loads configurations from a YAML file
func LoadConfigFile(configPath string) (*ConfigResult, error) {
data, err := os.ReadFile(configPath)
if err != nil {
return nil, err
}

var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, err
}
return parseConfig(config)
}

// NewConfigWatcher creates a new config file watcher
func NewConfigWatcher(configPath string, cliRelays []types.RelayEntry, log *logrus.Entry) (*ConfigWatcher, error) {
v := viper.New()
absPath, err := filepath.Abs(configPath)
if err != nil {
return nil, err
}

v.SetConfigFile(absPath)
v.SetConfigType("yaml")

if err := v.ReadInConfig(); err != nil {
return nil, err
}

return &ConfigWatcher{
v: v,
configPath: absPath,
cliRelays: cliRelays,
log: log,
}, nil
}

// Watch starts watching the config file for changes
func (cw *ConfigWatcher) Watch(onConfigChange func(*ConfigResult)) {
cw.onConfigChange = onConfigChange

cw.v.OnConfigChange(func(_ fsnotify.Event) {
cw.log.Info("config file changed, reloading...")
var config Config
if err := cw.v.Unmarshal(&config); err != nil {
cw.log.WithError(err).Error("failed to unmarshal new config, keeping old config")
return
}
newConfig, err := parseConfig(config)
if err != nil {
cw.log.WithError(err).Error("failed to parse new config, keeping old config")
return
}

cw.log.Infof("successfully loaded new config")

if cw.onConfigChange != nil {
cw.onConfigChange(newConfig)
}
})

cw.v.WatchConfig()
}

// MergeRelayConfigs merges relays passed via --relays with config file settings.
// this allows the users to still use --relays if they dont want to provide a config file
func MergeRelayConfigs(relays []types.RelayEntry, configMap map[string]types.RelayConfig) []types.RelayConfig {
configs := make([]types.RelayConfig, 0)
processedURLs := make(map[string]bool)

for _, entry := range relays {
urlStr := entry.String()
if config, exists := configMap[urlStr]; exists {
config.RelayEntry = entry
configs = append(configs, config)
} else {
configs = append(configs, types.NewRelayConfig(entry))
}
processedURLs[urlStr] = true
}

for urlStr, config := range configMap {
if !processedURLs[urlStr] {
configs = append(configs, config)
}
}
return configs
}

func parseConfig(config Config) (*ConfigResult, error) {
timeoutGetHeaderMs := config.TimeoutGetHeaderMs
if timeoutGetHeaderMs == 0 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

neeed to clearly specify this in a documentaton on what the default value is. you can do it in the config.yaml

timeoutGetHeaderMs = 900
}

lateInSlotTimeMs := config.LateInSlotTimeMs
if lateInSlotTimeMs == 0 {
lateInSlotTimeMs = 1000
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for this.

}

configMap := make(map[string]types.RelayConfig)
for _, relay := range config.Relays {
relayEntry, err := types.NewRelayEntry(strings.TrimSpace(relay.URL))
if err != nil {
return nil, err
}
if relay.ID != "" {
relayEntry.ID = relay.ID
} else {
relayEntry.ID = relayEntry.URL.String()
}
relayConfig := types.RelayConfig{
RelayEntry: relayEntry,
EnableTimingGames: relay.EnableTimingGames,
TargetFirstRequestMs: relay.TargetFirstRequestMs,
FrequencyGetHeaderMs: relay.FrequencyGetHeaderMs,
}
configMap[relayEntry.String()] = relayConfig
}

return &ConfigResult{
RelayConfigs: configMap,
TimeoutGetHeaderMs: timeoutGetHeaderMs,
LateInSlotTimeMs: lateInSlotTimeMs,
}, nil
}
7 changes: 7 additions & 0 deletions cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var flags = []cli.Flag{
hoodiFlag,
// relay
relaysFlag,
relayConfigFlag,
deprecatedRelayMonitorFlag,
minBidFlag,
relayCheckFlag,
Expand Down Expand Up @@ -135,6 +136,12 @@ var (
Usage: "relay urls - single entry or comma-separated list (scheme://pubkey@host)",
Category: RelayCategory,
}
relayConfigFlag = &cli.StringFlag{
Name: "config",
Sources: cli.EnvVars("CONFIG_FILE"),
Usage: "path to YAML configuration file",
Category: RelayCategory,
}
deprecatedRelayMonitorFlag = &cli.StringSliceFlag{
Name: "relay-monitors",
Aliases: []string{"relay-monitor"},
Expand Down
77 changes: 67 additions & 10 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/flashbots/mev-boost/common"
"github.com/flashbots/mev-boost/config"
"github.com/flashbots/mev-boost/server"
serverTypes "github.com/flashbots/mev-boost/server/types"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v3"
)
Expand All @@ -29,6 +30,15 @@ const (
genesisTimeHoodi = 1742213400
)

type RelaySetupResult struct {
RelayConfigs []serverTypes.RelayConfig
MinBid types.U256Str
RelayCheck bool
TimeoutGetHeaderMs uint64
LateInSlotTimeMs uint64
CLIRelays []serverTypes.RelayEntry // CLI-provided relays for hot-reload merging
}

var (
// errors
errInvalidLoglevel = errors.New("invalid loglevel")
Expand Down Expand Up @@ -66,7 +76,7 @@ func start(_ context.Context, cmd *cli.Command) error {

var (
genesisForkVersion, genesisTime = setupGenesis(cmd)
relays, minBid, relayCheck = setupRelays(cmd)
relaySetup = setupRelays(cmd)
listenAddr = cmd.String(addrFlag.Name)
metricsEnabled = cmd.Bool(metricsFlag.Name)
metricsAddr = cmd.String(metricsAddrFlag.Name)
Expand All @@ -75,26 +85,43 @@ func start(_ context.Context, cmd *cli.Command) error {
opts := server.BoostServiceOpts{
Log: log,
ListenAddr: listenAddr,
Relays: relays,
RelayConfigs: relaySetup.RelayConfigs,
GenesisForkVersionHex: genesisForkVersion,
GenesisTime: genesisTime,
RelayCheck: relayCheck,
RelayMinBid: minBid,
RelayCheck: relaySetup.RelayCheck,
RelayMinBid: relaySetup.MinBid,
RequestTimeoutGetHeader: time.Duration(cmd.Int(timeoutGetHeaderFlag.Name)) * time.Millisecond,
RequestTimeoutGetPayload: time.Duration(cmd.Int(timeoutGetPayloadFlag.Name)) * time.Millisecond,
RequestTimeoutRegVal: time.Duration(cmd.Int(timeoutRegValFlag.Name)) * time.Millisecond,
RequestMaxRetries: cmd.Int(maxRetriesFlag.Name),
MetricsAddr: metricsAddr,
TimeoutGetHeaderMs: relaySetup.TimeoutGetHeaderMs,
LateInSlotTimeMs: relaySetup.LateInSlotTimeMs,
}
service, err := server.NewBoostService(opts)
if err != nil {
log.WithError(err).Fatal("failed creating the server")
}

if relayCheck && service.CheckRelays() == 0 {
if relaySetup.RelayCheck && service.CheckRelays() == 0 {
log.Error("no relay passed the health-check!")
}

// set up config file watcher if a config file is provided
if cmd.IsSet(relayConfigFlag.Name) {
configPath := cmd.String(relayConfigFlag.Name)
watcher, err := NewConfigWatcher(configPath, relaySetup.CLIRelays, log)
if err != nil {
log.WithError(err).Warn("failed to set up config watcher")
} else {
// register a callback which gets invoked when config file changes
watcher.Watch(func(newConfig *ConfigResult) {
mergedConfigs := MergeRelayConfigs(relaySetup.CLIRelays, newConfig.RelayConfigs)
service.UpdateConfig(mergedConfigs, newConfig.TimeoutGetHeaderMs, newConfig.LateInSlotTimeMs)
})
}
}

if metricsEnabled {
go func() {
log.Infof("metrics server listening on %v", opts.MetricsAddr)
Expand All @@ -108,7 +135,7 @@ func start(_ context.Context, cmd *cli.Command) error {
return service.StartHTTPServer()
}

func setupRelays(cmd *cli.Command) (relayList, types.U256Str, bool) {
func setupRelays(cmd *cli.Command) RelaySetupResult {
// For backwards compatibility with the -relays flag.
var relays relayList
if cmd.IsSet(relaysFlag.Name) {
Expand All @@ -125,9 +152,32 @@ func setupRelays(cmd *cli.Command) (relayList, types.U256Str, bool) {
if len(relays) == 0 {
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())

// load configuration via config file
var configMap map[string]serverTypes.RelayConfig
var timeoutGetHeaderMs uint64 = 900
var lateInSlotTimeMs uint64 = 1000
if cmd.IsSet(relayConfigFlag.Name) {
configPath := cmd.String(relayConfigFlag.Name)
log.Infof("loading config from: %s", configPath)
configResult, err := LoadConfigFile(configPath)
if err != nil {
log.WithError(err).Fatal("failed to load config file")
} else {
configMap = configResult.RelayConfigs
timeoutGetHeaderMs = configResult.TimeoutGetHeaderMs
lateInSlotTimeMs = configResult.LateInSlotTimeMs
}
}
relayConfigs := MergeRelayConfigs(relays, configMap)

log.Infof("using %d relays", len(relayConfigs))
for index, config := range relayConfigs {
if config.EnableTimingGames {
log.Infof("relay #%d: %s timing games: enabled", index+1, config.RelayEntry.String())
} else {
log.Infof("relay #%d: %s", index+1, config.RelayEntry.String())
}
}

relayMinBidWei, err := sanitizeMinBid(cmd.Float(minBidFlag.Name))
Expand All @@ -137,7 +187,14 @@ func setupRelays(cmd *cli.Command) (relayList, types.U256Str, bool) {
if relayMinBidWei.BigInt().Sign() > 0 {
log.Infof("min bid set to %v eth (%v wei)", cmd.Float(minBidFlag.Name), relayMinBidWei)
}
return relays, *relayMinBidWei, cmd.Bool(relayCheckFlag.Name)
return RelaySetupResult{
RelayConfigs: relayConfigs,
MinBid: *relayMinBidWei,
RelayCheck: cmd.Bool(relayCheckFlag.Name),
TimeoutGetHeaderMs: timeoutGetHeaderMs,
LateInSlotTimeMs: lateInSlotTimeMs,
CLIRelays: []serverTypes.RelayEntry(relays),
}
}

func setupGenesis(cmd *cli.Command) (string, uint64) {
Expand Down
24 changes: 24 additions & 0 deletions examples/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Example configuration for mev-boost
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would recommend to create a config.example.yaml


# timeout settings for get_header requests
timeout_get_header_ms: 900 # timeout for get_header request in milliseconds
late_in_slot_time_ms: 1000 # threshold that defines when in a slot is considered "too late"

# Relay configurations
relays:
# relay with timing games enabled
- url: https://0x9000009807ed12c1f08bf4e81c6da3ba8e3fc3d953898ce0102433094e5f22f21102ec057841fcb81978ed1ea0fa8246@relay.relayer1.net
# id for identifying the relay. if not provided, the url will be used as the id
id: relay.relayer1.net
enable_timing_games: true
# time in ms from slot start for the first getHeader request
target_first_request_ms: 200
# time in ms between subsequent getHeader requests
frequency_getheader_ms: 100

# relay with timing games disabled (standard behavior)
- url: https://0x9000009807ed12c1f08bf4e81c6da3ba8e3fc3d953898ce0102433094e5f22f21102ec057841fcb81978ed1ea0fa8246@relay.relayer2.com
id: relay.relayer2.com
enable_timing_games: false
target_first_request_ms: 0
frequency_getheader_ms: 0
Loading
Loading