-
Notifications
You must be signed in to change notification settings - Fork 274
Add timing feature for bid optimization #839
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
fbacd4c
22cdda3
57fbab0
00fd8f8
8a477b1
bd833b7
1570efc
7a381b0
c66acfd
0f78e8d
e03871b
a1b5f9d
5886daa
5115d3f
5a266b0
5390fe7
acbb049
fcc765c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -317,6 +317,80 @@ 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. | ||
|
|
||
| ### Enabling 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 right before the proposal deadline. | ||
|
|
||
| To enable timing games, you must provide a YAML configuration file using the `-config` flag: | ||
|
|
||
| ```bash | ||
| ./mev-boost -config config.yaml | ||
| ``` | ||
| **Notice:** This feature is strictly meant for advanced users and extra care should be taken when setting up timing game associated parameters | ||
|
|
||
| #### 1. Global Timeouts | ||
| These settings apply to all relays and define the hard boundaries for the `getHeader` operation. | ||
|
|
||
| * **`timeout_get_header_ms`** (optional, default: 900ms) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How did we choose defaults 900 and 1000? And could we suggest a general practice to the users for picking different numbers? |
||
| * It is the maximum timeout in milliseconds for get_header requests to relays. | ||
|
|
||
| * **`late_in_slot_time_ms`** (optional, default: 1000ms) | ||
| * It is a safety threshold in milliseconds that marks when in a slot we consider it "too late" to fetch headers from relays. If the request arrives after the threshold, it skips all relay requests and forces local block building. | ||
| ``` | ||
|
|
||
| #### 2. Per-Relay Configuration | ||
| These parameters are configured individually for each relay in `config.yaml`. | ||
|
|
||
| * **`enable_timing_games`** (optional, default: false) | ||
| * Enables the timing games logic for this specific relay. If `false`, standard behavior is used (single request, immediate execution). | ||
|
|
||
| * **`target_first_request_ms`** (`uint64`) | ||
| * It is the target time in milliseconds into the slot when the first get_header request should be sent to a relay. It's only used when `enable_timing_games = true` | ||
|
|
||
| * **`frequency_get_header_ms`** (`uint64`) | ||
| * The interval in milliseconds between subsequent requests to the same relay. After the first request is sent, `mev-boost` will keep sending new requests every `frequency_get_header_ms` until the global timeout budget is exhausted. | ||
|
|
||
| #### 3. Visual Representation | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| participant CL as Consensus Client | ||
| participant MB as Mev-Boost | ||
| participant R as Relay (Timing Games Enabled) | ||
|
|
||
| Note over CL, R: Slot Start (t=0) | ||
|
|
||
| CL->>MB: getHeader | ||
| activate MB | ||
|
|
||
| Note right of MB: Calculate maxTimeout<br/>min(timeout_get_header_ms,<br/>late_in_slot - ms_into_slot) | ||
|
|
||
| opt target_first_request_ms - ms_into_slot > 0 | ||
| Note right of MB: Sleep until target_first_request_ms - ms_into_slot | ||
| end | ||
|
|
||
| par Request 1 | ||
| MB->>R: GET /get_header (Request 1) | ||
| R-->>MB: Bid A (Value: 1.0 ETH) | ||
| and Request 2 (after Frequency interval) | ||
| Note right of MB: Sleep FrequencyMs | ||
| MB->>R: GET /get_header (Request 2) | ||
| R-->>MB: Bid B (Value: 1.1 ETH) | ||
| and Request 3 (after Frequency interval) | ||
| Note right of MB: Sleep FrequencyMs | ||
| MB->>R: GET /get_header (Request 3) | ||
| R-->>MB: Bid C (Value: 1.2 ETH) | ||
| end | ||
|
|
||
| Note right of MB: Timeout Reached or<br/>Replies Finished | ||
|
|
||
| Note right of MB: 1. Select best from Relay:<br/>Bid C (Latest Received) | ||
|
|
||
| Note right of MB: 2. Compare with other relays<br/>(Highest Value Wins) | ||
|
|
||
| MB-->>CL: Return Winning Bid (Bid C) | ||
| deactivate MB | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| # API | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| 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"` | ||
| 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)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We seem to introduce a new watcher along with the config file, which is very nice and is super useful when experimenting with the timing games logic quickly. But when enabled by default, this can cause unwanted side effects since the user might save a few times while making changes. I think I'd want to make some changes and then do a restart with the new values, in production. Maybe we could introduce a simple |
||
| 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 --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 { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = 900 | ||
| } | ||
|
|
||
| lateInSlotTimeMs := config.LateInSlotTimeMs | ||
| if lateInSlotTimeMs == 0 { | ||
| lateInSlotTimeMs = 1000 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
| 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 | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for this documentation! π Can we keep on mentioning "Enable timing games" here but below the "Notice:" sentence, share a link to the rest of the details (config, explanations, diagram etc.). The link could point to a new file
docs/timing-games.mdand the main README would look less bloated without the advanced feature details.