Skip to content

Commit 7a60459

Browse files
authored
Add timing feature for bid optimization (#839)
* enable timing games * fix lint * add example * remove quotations * upd config value * upd config * add tests * add id to relay config * merge relays with config relays * config hot reloading * fix lint * add example config in root * remove id field * add config tests * enforce timout on reqs * add docs * fix spelling * update example config * fix * address comments * remove dups when merging relays * fix linter * fix tests * restructure conditionals * update docs * adjust test * update config docs * improve doc
1 parent 36bc01f commit 7a60459

17 files changed

+1933
-241
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,10 @@ Usage of mev-boost:
270270
check relay status on startup and on the status API call
271271
-relays string
272272
relay urls - single entry or comma-separated list (scheme://pubkey@host)
273+
-config string
274+
path to YAML configuration file for enabling advanced features
275+
-watch-config
276+
enable hot reloading of config file (requires -config)
273277
-request-timeout-getheader int
274278
timeout for getHeader requests to the relay [ms] (default 950)
275279
-request-timeout-getpayload int
@@ -327,6 +331,14 @@ Example for setting a minimum bid value of 0.06 ETH:
327331

328332
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.
329333

334+
### Enable timing games
335+
336+
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.
337+
338+
**Notice:** This feature is strictly meant for advanced users and extra care should be taken when setting up timing game associated parameters.
339+
340+
For detailed configuration options, parameters, and visual diagrams, see [docs/timing-games.md](docs/timing-games.md).
341+
330342
---
331343

332344
# API

cli/config.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package cli
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/flashbots/mev-boost/server/types"
11+
"github.com/fsnotify/fsnotify"
12+
"github.com/sirupsen/logrus"
13+
"github.com/spf13/viper"
14+
"gopkg.in/yaml.v3"
15+
)
16+
17+
var errRelayConfiguredTwice = errors.New("relay is specified in both cli flags and config file")
18+
19+
type RelayConfigYAML struct {
20+
URL string `yaml:"url"`
21+
EnableTimingGames bool `yaml:"enable_timing_games"`
22+
TargetFirstRequestMs uint64 `yaml:"target_first_request_ms"`
23+
FrequencyGetHeaderMs uint64 `yaml:"frequency_get_header_ms"`
24+
}
25+
26+
// Config holds all configuration settings from the config file
27+
type Config struct {
28+
TimeoutGetHeaderMs uint64 `yaml:"timeout_get_header_ms"`
29+
LateInSlotTimeMs uint64 `yaml:"late_in_slot_time_ms"`
30+
Relays []RelayConfigYAML `yaml:"relays"`
31+
}
32+
33+
type ConfigResult struct {
34+
RelayConfigs map[string]types.RelayConfig
35+
TimeoutGetHeaderMs uint64
36+
LateInSlotTimeMs uint64
37+
}
38+
39+
// ConfigWatcher provides hot reloading of config files
40+
type ConfigWatcher struct {
41+
v *viper.Viper
42+
configPath string
43+
cliRelays []types.RelayEntry
44+
onConfigChange func(*ConfigResult)
45+
log *logrus.Entry
46+
}
47+
48+
// LoadConfigFile loads configurations from a YAML file
49+
func LoadConfigFile(configPath string) (*ConfigResult, error) {
50+
data, err := os.ReadFile(configPath)
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
var config Config
56+
if err := yaml.Unmarshal(data, &config); err != nil {
57+
return nil, err
58+
}
59+
return parseConfig(config)
60+
}
61+
62+
// NewConfigWatcher creates a new config file watcher
63+
func NewConfigWatcher(configPath string, cliRelays []types.RelayEntry, log *logrus.Entry) (*ConfigWatcher, error) {
64+
v := viper.New()
65+
absPath, err := filepath.Abs(configPath)
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
v.SetConfigFile(absPath)
71+
v.SetConfigType("yaml")
72+
73+
if err := v.ReadInConfig(); err != nil {
74+
return nil, err
75+
}
76+
77+
return &ConfigWatcher{
78+
v: v,
79+
configPath: absPath,
80+
cliRelays: cliRelays,
81+
log: log,
82+
}, nil
83+
}
84+
85+
// Watch starts watching the config file for changes
86+
func (cw *ConfigWatcher) Watch(onConfigChange func(*ConfigResult)) {
87+
cw.onConfigChange = onConfigChange
88+
89+
cw.v.OnConfigChange(func(_ fsnotify.Event) {
90+
cw.log.Info("config file changed, reloading...")
91+
92+
// explicitly read the file to get the latest content since viper
93+
// may cache the old value and not read the file immediately.
94+
data, err := os.ReadFile(cw.configPath)
95+
if err != nil {
96+
cw.log.WithError(err).Error("failed to read new config file, keeping old config")
97+
return
98+
}
99+
100+
var config Config
101+
if err := yaml.Unmarshal(data, &config); err != nil {
102+
cw.log.WithError(err).Error("failed to unmarshal new config, keeping old config")
103+
return
104+
}
105+
newConfig, err := parseConfig(config)
106+
if err != nil {
107+
cw.log.WithError(err).Error("failed to parse new config, keeping old config")
108+
return
109+
}
110+
111+
cw.log.Infof("successfully loaded new config with %d relays from config file", len(newConfig.RelayConfigs))
112+
113+
if cw.onConfigChange != nil {
114+
cw.onConfigChange(newConfig)
115+
}
116+
})
117+
118+
cw.v.WatchConfig()
119+
}
120+
121+
// MergeRelayConfigs merges relays passed via --relay with relays from config file.
122+
// Returns an error if the same relay appears in both places.
123+
// Users should specify each relay in exactly one place either cli or config file.
124+
func MergeRelayConfigs(relays []types.RelayEntry, configMap map[string]types.RelayConfig) ([]types.RelayConfig, error) {
125+
configs := make([]types.RelayConfig, 0)
126+
127+
for _, entry := range relays {
128+
urlStr := entry.String()
129+
if _, exists := configMap[urlStr]; exists {
130+
return nil, fmt.Errorf("%w: %s", errRelayConfiguredTwice, urlStr)
131+
}
132+
configs = append(configs, types.NewRelayConfig(entry))
133+
}
134+
135+
for _, config := range configMap {
136+
configs = append(configs, config)
137+
}
138+
139+
return configs, nil
140+
}
141+
142+
func parseConfig(config Config) (*ConfigResult, error) {
143+
timeoutGetHeaderMs := config.TimeoutGetHeaderMs
144+
if timeoutGetHeaderMs == 0 {
145+
timeoutGetHeaderMs = 950
146+
}
147+
148+
lateInSlotTimeMs := config.LateInSlotTimeMs
149+
if lateInSlotTimeMs == 0 {
150+
lateInSlotTimeMs = 2000
151+
}
152+
153+
configMap := make(map[string]types.RelayConfig)
154+
for _, relay := range config.Relays {
155+
relayEntry, err := types.NewRelayEntry(strings.TrimSpace(relay.URL))
156+
if err != nil {
157+
return nil, err
158+
}
159+
relayConfig := types.RelayConfig{
160+
RelayEntry: relayEntry,
161+
EnableTimingGames: relay.EnableTimingGames,
162+
TargetFirstRequestMs: relay.TargetFirstRequestMs,
163+
FrequencyGetHeaderMs: relay.FrequencyGetHeaderMs,
164+
}
165+
configMap[relayEntry.String()] = relayConfig
166+
}
167+
168+
return &ConfigResult{
169+
RelayConfigs: configMap,
170+
TimeoutGetHeaderMs: timeoutGetHeaderMs,
171+
LateInSlotTimeMs: lateInSlotTimeMs,
172+
}, nil
173+
}

0 commit comments

Comments
 (0)