Skip to content
Merged
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,10 @@ Usage of mev-boost:
check relay status on startup and on the status API call
-relays string
relay urls - single entry or comma-separated list (scheme://pubkey@host)
-config string
path to YAML configuration file for enabling advanced features
-watch-config
enable hot reloading of config file (requires -config)
-request-timeout-getheader int
timeout for getHeader requests to the relay [ms] (default 950)
-request-timeout-getpayload int
Expand Down Expand Up @@ -317,6 +321,14 @@ Example for setting a minimum bid value of 0.06 ETH:

Optionally, the `-metrics` flag can be provided to expose a prometheus metrics server. The metrics server address/port can be changed with the `-metrics-addr` (e.g., `-metrics-addr localhost:9009`) flag.

### Enable timing games

The **Timing Games** feature allows `mev-boost` to optimize block proposal by strategically timing `getHeader` requests to relays. Instead of sending a single request immediately, it can delay the initial request and send multiple follow-up requests to capture the latest, most valuable bids before the proposal deadline.

**Notice:** This feature is strictly meant for advanced users and extra care should be taken when setting up timing game associated parameters.

For detailed configuration options, parameters, and visual diagrams, see [docs/timing-games.md](docs/timing-games.md).

---

# API
Expand Down
173 changes: 173 additions & 0 deletions cli/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package cli

import (
"errors"
"fmt"
"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"
)

var errRelayConfiguredTwice = errors.New("relay is specified in both cli flags and config file")

type RelayConfigYAML struct {
URL string `yaml:"url"`
EnableTimingGames bool `yaml:"enable_timing_games"`
TargetFirstRequestMs uint64 `yaml:"target_first_request_ms"`
FrequencyGetHeaderMs uint64 `yaml:"frequency_get_header_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)) {
Comment thread
faheelsattar marked this conversation as resolved.
cw.onConfigChange = onConfigChange

cw.v.OnConfigChange(func(_ fsnotify.Event) {
cw.log.Info("config file changed, reloading...")

// explicitly read the file to get the latest content since viper
// may cache the old value and not read the file immediately.
data, err := os.ReadFile(cw.configPath)
if err != nil {
cw.log.WithError(err).Error("failed to read new config file, keeping old config")
return
}

var config Config
if err := yaml.Unmarshal(data, &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 with %d relays from config file", len(newConfig.RelayConfigs))

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

cw.v.WatchConfig()
}

// MergeRelayConfigs merges relays passed via --relay with relays from config file.
// Returns an error if the same relay appears in both places.
// Users should specify each relay in exactly one place either cli or config file.
func MergeRelayConfigs(relays []types.RelayEntry, configMap map[string]types.RelayConfig) ([]types.RelayConfig, error) {
configs := make([]types.RelayConfig, 0)

for _, entry := range relays {
urlStr := entry.String()
if _, exists := configMap[urlStr]; exists {
return nil, fmt.Errorf("%w: %s", errRelayConfiguredTwice, urlStr)
}
configs = append(configs, types.NewRelayConfig(entry))
}

for _, config := range configMap {
configs = append(configs, config)
}

return configs, nil
}

func parseConfig(config Config) (*ConfigResult, error) {
timeoutGetHeaderMs := config.TimeoutGetHeaderMs
if timeoutGetHeaderMs == 0 {
Copy link
Copy Markdown
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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

we already have doc and default values in the config file. Do you propose some different value?

timeoutGetHeaderMs = 950
}

lateInSlotTimeMs := config.LateInSlotTimeMs
if lateInSlotTimeMs == 0 {
lateInSlotTimeMs = 2000
}

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
}
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
}
Loading
Loading