From 7604511101975738d278cd357b6a3b202677742c Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:04:20 -0300 Subject: [PATCH 01/10] ePBS settings --- .../service/config/settings-builder.go | 52 +++++ .../service/config/settings-commit-boost.go | 12 +- .../service/config/settings-epbs.go | 81 ++++++++ .../service/config/settings-home.go | 17 +- .../service/config/settings-mev-boost.go | 12 +- .../service/config/standard-layout.go | 2 +- shared/services/config/epbs-config.go | 189 +++++++++++++++++ .../config/migration/migration-manager.go | 10 + .../migration/migration-manager_test.go | 69 +++++++ .../config/migration/v1245-manager.go | 68 +++++++ .../config/migration/v1245-manager_test.go | 190 ++++++++++++++++++ shared/services/config/rocket-pool-config.go | 14 ++ 12 files changed, 691 insertions(+), 25 deletions(-) create mode 100644 rocketpool-cli/service/config/settings-builder.go create mode 100644 rocketpool-cli/service/config/settings-epbs.go create mode 100644 shared/services/config/epbs-config.go create mode 100644 shared/services/config/migration/migration-manager_test.go create mode 100644 shared/services/config/migration/v1245-manager.go create mode 100644 shared/services/config/migration/v1245-manager_test.go diff --git a/rocketpool-cli/service/config/settings-builder.go b/rocketpool-cli/service/config/settings-builder.go new file mode 100644 index 000000000..2b4ba02b6 --- /dev/null +++ b/rocketpool-cli/service/config/settings-builder.go @@ -0,0 +1,52 @@ +package config + +import ( + cfgtypes "github.com/rocket-pool/smartnode/shared/types/config" +) + +type BuilderConfigPage struct { + epbsPage *EpbsPage + page *page + layout *standardLayout + items map[*cfgtypes.Parameter]*parameterizedFormItem +} + +func NewBuilderConfigPage(epbsPage *EpbsPage) *BuilderConfigPage { + configPage := &BuilderConfigPage{ + epbsPage: epbsPage, + layout: newStandardLayout(), + items: map[*cfgtypes.Parameter]*parameterizedFormItem{}, + } + cfg := epbsPage.home.md.Config + configPage.layout.createForm(&cfg.Smartnode.Network, "Builder Settings") + configPage.layout.setupEscapeReturnHomeHandler(epbsPage.home.md, epbsPage.page) + items := createParameterizedFormItems(cfg.Epbs.GetParameters(), configPage.layout) + configPage.layout.mapParameterizedFormItems(items...) + for _, item := range items { + configPage.items[item.parameter] = item + } + configPage.page = newPage( + epbsPage.page, + "settings-builder", + "Builder Settings", + "Set the minimum bid and maximum execution payment in ETH, and select relay operators for the ePBS builder list. These settings are shared by all consensus clients.", + configPage.layout.grid, + ) + configPage.handleLayoutChanged() + return configPage +} + +func (configPage *BuilderConfigPage) getPage() *page { + return configPage.page +} + +func (configPage *BuilderConfigPage) handleLayoutChanged() { + cfg := configPage.epbsPage.home.md.Config.Epbs + configPage.layout.form.Clear(true) + configPage.layout.form.AddFormItem(configPage.items[&cfg.MinBid].item) + configPage.layout.form.AddFormItem(configPage.items[&cfg.MaxExecutionPayment].item) + for _, param := range cfg.GetAvailableRelayParameters() { + configPage.layout.form.AddFormItem(configPage.items[param].item) + } + configPage.layout.refresh() +} diff --git a/rocketpool-cli/service/config/settings-commit-boost.go b/rocketpool-cli/service/config/settings-commit-boost.go index 47b202177..e6fbbfc08 100644 --- a/rocketpool-cli/service/config/settings-commit-boost.go +++ b/rocketpool-cli/service/config/settings-commit-boost.go @@ -9,7 +9,7 @@ import ( // The page wrapper for the Commit-boost config type CommitBoostConfigPage struct { - home *settingsHome + epbsPage *EpbsPage page *page layout *standardLayout masterConfig *config.RocketPoolConfig @@ -26,16 +26,16 @@ type CommitBoostConfigPage struct { } // Creates a new page for the Commit-Boost settings -func NewCommitBoostConfigPage(home *settingsHome) *CommitBoostConfigPage { +func NewCommitBoostConfigPage(epbsPage *EpbsPage) *CommitBoostConfigPage { configPage := &CommitBoostConfigPage{ - home: home, - masterConfig: home.md.Config, + epbsPage: epbsPage, + masterConfig: epbsPage.home.md.Config, } configPage.createContent() configPage.page = newPage( - home.homePage, + epbsPage.page, "settings-commit-boost", "Commit-Boost", "Select this to configure the settings for the Commit-Boost PBS client, the source of blocks with MEV rewards for your validators.\n\nCommit-Boost is a powerful PBS client that offers detailed reporting and transparency around block building, very low latency, and a responsive development team.\n\n", @@ -57,7 +57,7 @@ func (configPage *CommitBoostConfigPage) createContent() { // Create the layout configPage.layout = newStandardLayout() configPage.layout.createForm(&configPage.masterConfig.Smartnode.Network, "Commit-Boost Settings") - configPage.layout.setupEscapeReturnHomeHandler(configPage.home.md, configPage.home.homePage) + configPage.layout.setupEscapeReturnHomeHandler(configPage.epbsPage.home.md, configPage.epbsPage.page) // Set up the form items configPage.enableBox = createParameterizedCheckbox(&configPage.masterConfig.EnableCommitBoost) diff --git a/rocketpool-cli/service/config/settings-epbs.go b/rocketpool-cli/service/config/settings-epbs.go new file mode 100644 index 000000000..077655b63 --- /dev/null +++ b/rocketpool-cli/service/config/settings-epbs.go @@ -0,0 +1,81 @@ +package config + +import ( + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +// EpbsPage groups shared builder settings and the existing PBS clients. +type EpbsPage struct { + home *settingsHome + page *page + layout *standardLayout + categoryList *tview.List + builderPage *BuilderConfigPage + mevBoostPage *MevBoostConfigPage + commitBoostPage *CommitBoostConfigPage + subpages []settingsPage +} + +func NewEpbsPage(home *settingsHome) *EpbsPage { + epbsPage := &EpbsPage{home: home} + epbsPage.page = newPage( + home.homePage, + "settings-epbs", + "ePBS", + "Configure builder settings shared by all consensus clients, and manage MEV-Boost and Commit-Boost.", + nil, + ) + epbsPage.builderPage = NewBuilderConfigPage(epbsPage) + epbsPage.mevBoostPage = NewMevBoostConfigPage(epbsPage) + epbsPage.commitBoostPage = NewCommitBoostConfigPage(epbsPage) + epbsPage.subpages = []settingsPage{ + epbsPage.builderPage, + epbsPage.mevBoostPage, + epbsPage.commitBoostPage, + } + for _, subpage := range epbsPage.subpages { + home.md.pages.AddPage(subpage.getPage().id, subpage.getPage().content, true, false) + } + epbsPage.createContent() + epbsPage.page.content = epbsPage.layout.grid + return epbsPage +} + +func (epbsPage *EpbsPage) getPage() *page { + return epbsPage.page +} + +func (epbsPage *EpbsPage) createContent() { + epbsPage.layout = newStandardLayout() + epbsPage.layout.createSettingFooter() + list := tview.NewList().SetChangedFunc(func(index int, mainText, secondaryText string, shortcut rune) { + epbsPage.layout.descriptionBox.SetText(epbsPage.subpages[index].getPage().description) + }) + list.SetBackgroundColor(tview.Styles.ContrastBackgroundColor) + list.SetBorderPadding(0, 0, 1, 1) + list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == tcell.KeyEsc { + epbsPage.home.md.setPage(epbsPage.home.homePage) + return nil + } + return event + }) + for _, subpage := range epbsPage.subpages { + list.AddItem(subpage.getPage().title, "", 0, nil) + } + list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) { + subpage := epbsPage.subpages[index] + subpage.handleLayoutChanged() + epbsPage.home.md.setPage(subpage.getPage()) + }) + epbsPage.categoryList = list + epbsPage.layout.setContent(list, list.Box, "ePBS Settings") + epbsPage.layout.descriptionBox.SetText(epbsPage.subpages[0].getPage().description) +} + +func (epbsPage *EpbsPage) handleLayoutChanged() { + for _, subpage := range epbsPage.subpages { + subpage.handleLayoutChanged() + } +} diff --git a/rocketpool-cli/service/config/settings-home.go b/rocketpool-cli/service/config/settings-home.go index 4d8a81049..e47b16cd7 100644 --- a/rocketpool-cli/service/config/settings-home.go +++ b/rocketpool-cli/service/config/settings-home.go @@ -19,8 +19,7 @@ type settingsHome struct { ecPage *ExecutionConfigPage fallbackPage *FallbackConfigPage ccPage *ConsensusConfigPage - mevBoostPage *MevBoostConfigPage - commitBoostPage *CommitBoostConfigPage + epbsPage *EpbsPage metricsPage *MetricsConfigPage alertingPage *AlertingConfigPage addonsPage *AddonsPage @@ -47,8 +46,7 @@ func newSettingsHome(md *MainDisplay) *settingsHome { home.ecPage = NewExecutionConfigPage(home) home.ccPage = NewConsensusConfigPage(home) home.fallbackPage = NewFallbackConfigPage(home) - home.mevBoostPage = NewMevBoostConfigPage(home) - home.commitBoostPage = NewCommitBoostConfigPage(home) + home.epbsPage = NewEpbsPage(home) home.metricsPage = NewMetricsConfigPage(home) home.alertingPage = NewAlertingConfigPage(home) home.addonsPage = NewAddonsPage(home) @@ -58,8 +56,7 @@ func newSettingsHome(md *MainDisplay) *settingsHome { home.ecPage, home.ccPage, home.fallbackPage, - home.mevBoostPage, - home.commitBoostPage, + home.epbsPage, home.metricsPage, home.alertingPage, home.addonsPage, @@ -246,12 +243,8 @@ func (home *settingsHome) refresh() { home.fallbackPage.layout.refresh() } - if home.mevBoostPage != nil { - home.mevBoostPage.layout.refresh() - } - - if home.commitBoostPage != nil { - home.commitBoostPage.layout.refresh() + if home.epbsPage != nil { + home.epbsPage.handleLayoutChanged() } if home.metricsPage != nil { diff --git a/rocketpool-cli/service/config/settings-mev-boost.go b/rocketpool-cli/service/config/settings-mev-boost.go index 085ea9519..e249af8a6 100644 --- a/rocketpool-cli/service/config/settings-mev-boost.go +++ b/rocketpool-cli/service/config/settings-mev-boost.go @@ -9,7 +9,7 @@ import ( // The page wrapper for the MEV-boost config type MevBoostConfigPage struct { - home *settingsHome + epbsPage *EpbsPage page *page layout *standardLayout masterConfig *config.RocketPoolConfig @@ -31,16 +31,16 @@ type MevBoostConfigPage struct { } // Creates a new page for the MEV-Boost settings -func NewMevBoostConfigPage(home *settingsHome) *MevBoostConfigPage { +func NewMevBoostConfigPage(epbsPage *EpbsPage) *MevBoostConfigPage { configPage := &MevBoostConfigPage{ - home: home, - masterConfig: home.md.Config, + epbsPage: epbsPage, + masterConfig: epbsPage.home.md.Config, } configPage.createContent() configPage.page = newPage( - home.homePage, + epbsPage.page, "settings-mev-boost", "MEV-Boost", "Select this to configure the settings for the Flashbots MEV-Boost client, the source of blocks with MEV rewards for your minipools.\n\nFor more information on Flashbots, MEV, and MEV-Boost, please see https://writings.flashbots.net/writings/why-run-mevboost/", @@ -62,7 +62,7 @@ func (configPage *MevBoostConfigPage) createContent() { // Create the layout configPage.layout = newStandardLayout() configPage.layout.createForm(&configPage.masterConfig.Smartnode.Network, "MEV-Boost Settings") - configPage.layout.setupEscapeReturnHomeHandler(configPage.home.md, configPage.home.homePage) + configPage.layout.setupEscapeReturnHomeHandler(configPage.epbsPage.home.md, configPage.epbsPage.page) // Set up the form items configPage.enableBox = createParameterizedCheckbox(&configPage.masterConfig.EnableMevBoost) diff --git a/rocketpool-cli/service/config/standard-layout.go b/rocketpool-cli/service/config/standard-layout.go index 4a63e6629..2e8d5e653 100644 --- a/rocketpool-cli/service/config/standard-layout.go +++ b/rocketpool-cli/service/config/standard-layout.go @@ -239,7 +239,7 @@ func (layout *standardLayout) createSettingFooter() { panic(fmt.Errorf("error writing nav string 1: %w", err)) } - navString2 := "Esc: Go Back to Categories (saves changes)" + navString2 := "Esc: Go Back (saves changes)" navTextView2 := tview.NewTextView(). SetDynamicColors(false). SetRegions(false). diff --git a/shared/services/config/epbs-config.go b/shared/services/config/epbs-config.go new file mode 100644 index 000000000..c3f19e2cd --- /dev/null +++ b/shared/services/config/epbs-config.go @@ -0,0 +1,189 @@ +package config + +import ( + "fmt" + "net/url" + "regexp" + "strconv" + "strings" + + "github.com/rocket-pool/smartnode/shared/types/config" +) + +var builderAmountPattern = regexp.MustCompile(`^[0-9]+(\.[0-9]+)?$`) + +// EpbsConfig stores builder settings shared by all consensus clients. +// Client startup integration is separate; saving these settings does not yet +// require restarting any containers. +type EpbsConfig struct { + Title string `yaml:"-"` + + MinBid config.Parameter `yaml:"minBid,omitempty"` + MaxExecutionPayment config.Parameter `yaml:"maxExecutionPayment,omitempty"` + FlashbotsRelay config.Parameter `yaml:"flashbotsEnabled,omitempty"` + BloxRouteRegulatedRelay config.Parameter `yaml:"bloxRouteRegulatedEnabled,omitempty"` + UltrasoundRelay config.Parameter `yaml:"ultrasoundEnabled,omitempty"` + UltrasoundFilteredRelay config.Parameter `yaml:"ultrasoundFilteredEnabled,omitempty"` + AestusRelay config.Parameter `yaml:"aestusEnabled,omitempty"` + TitanGlobalRelay config.Parameter `yaml:"titanGlobalEnabled,omitempty"` + TitanRegionalRelay config.Parameter `yaml:"titanRegionalEnabled,omitempty"` + BtcsOfacRelay config.Parameter `yaml:"btcsOfacEnabled,omitempty"` + + parentConfig *RocketPoolConfig `yaml:"-"` + relays []config.MevRelay `yaml:"-"` +} + +func NewEpbsConfig(cfg *RocketPoolConfig) *EpbsConfig { + epbs := &EpbsConfig{ + Title: "Builder Settings", + parentConfig: cfg, + relays: createDefaultRelays(cfg.networks), + MinBid: config.Parameter{ + ID: "minBid", + Name: "Minimum Bid (ETH)", + Description: "The minimum payment to accept from an ePBS builder, in ETH. For example, enter 0.05 for 0.05 ETH. Leave blank to use the consensus client's default.", + Type: config.ParameterType_String, + Default: map[config.Network]interface{}{config.Network_All: ""}, + CanBeBlank: true, + OverwriteOnUpgrade: false, + }, + MaxExecutionPayment: config.Parameter{ + ID: "maxExecutionPayment", + Name: "Max Execution Payment (ETH)", + Description: "The maximum execution-layer payment to count toward an ePBS builder bid, in ETH. These payments rely on the builder's promise and are not backed by staked collateral. Set to 0 to count only trustless payments. For example, enter 0.05 to count up to 0.05 ETH of promised execution-layer payments.", + Type: config.ParameterType_String, + Default: map[config.Network]interface{}{config.Network_All: "0"}, + CanBeBlank: false, + OverwriteOnUpgrade: false, + }, + } + for _, option := range epbs.relayOptions() { + for _, relay := range epbs.relays { + if relay.ID != option.id { + continue + } + *option.parameter = config.Parameter{ + ID: option.parameterID, + Name: fmt.Sprintf("Enable %s", relay.Name), + Description: fmt.Sprintf("Select %s for the ePBS builder list. You can select multiple operators.\n\nUses this operator's existing relay endpoint for the selected network. ePBS availability at that endpoint has not yet been verified.", relay.Name), + Type: config.ParameterType_Bool, + Default: map[config.Network]interface{}{config.Network_All: false}, + CanBeBlank: false, + OverwriteOnUpgrade: false, + } + } + } + return epbs +} + +type epbsRelayOption struct { + id config.MevRelayID + parameterID string + parameter *config.Parameter +} + +func (cfg *EpbsConfig) relayOptions() []epbsRelayOption { + return []epbsRelayOption{ + {config.MevRelayID_Flashbots, "flashbotsEnabled", &cfg.FlashbotsRelay}, + {config.MevRelayID_BloxrouteRegulated, "bloxRouteRegulatedEnabled", &cfg.BloxRouteRegulatedRelay}, + {config.MevRelayID_Ultrasound, "ultrasoundEnabled", &cfg.UltrasoundRelay}, + {config.MevRelayID_UltrasoundFiltered, "ultrasoundFilteredEnabled", &cfg.UltrasoundFilteredRelay}, + {config.MevRelayID_Aestus, "aestusEnabled", &cfg.AestusRelay}, + {config.MevRelayID_TitanGlobal, "titanGlobalEnabled", &cfg.TitanGlobalRelay}, + {config.MevRelayID_TitanRegional, "titanRegionalEnabled", &cfg.TitanRegionalRelay}, + {config.MevRelayID_BTCSOfac, "btcsOfacEnabled", &cfg.BtcsOfacRelay}, + } +} + +func (cfg *EpbsConfig) GetParameters() []*config.Parameter { + params := []*config.Parameter{&cfg.MinBid, &cfg.MaxExecutionPayment} + for _, option := range cfg.relayOptions() { + params = append(params, option.parameter) + } + return params +} + +func (cfg *EpbsConfig) GetConfigTitle() string { + return cfg.Title +} + +// GetAvailableRelayParameters uses the same operator catalog and network +// availability as MEV-Boost, with independent selections for ePBS. +func (cfg *EpbsConfig) GetAvailableRelayParameters() []*config.Parameter { + params := []*config.Parameter{} + network := cfg.parentConfig.GetNetwork() + for _, option := range cfg.relayOptions() { + for _, relay := range cfg.relays { + if relay.ID == option.id && relay.Urls.UrlExists(network) { + params = append(params, option.parameter) + } + } + } + return params +} + +// GetBuilderUrls returns the selected network's endpoints in catalog order. +// These are seeded from the relay catalog, not verified ePBS endpoints. Relay +// signing pubkeys in URL userinfo are not builder authentication credentials. +func (cfg *EpbsConfig) GetBuilderUrls() ([]string, error) { + urls := []string{} + seen := map[string]bool{} + network := cfg.parentConfig.GetNetwork() + for _, option := range cfg.relayOptions() { + if option.parameter.Value != true { + continue + } + for _, relay := range cfg.relays { + if relay.ID != option.id || !relay.Urls.UrlExists(network) { + continue + } + endpoint, err := url.Parse(relay.Urls[network]) + if err != nil || (endpoint.Scheme != "https" && endpoint.Scheme != "http") || endpoint.Hostname() == "" { + return nil, fmt.Errorf("ePBS builder endpoint for %s must be an HTTP or HTTPS URL", relay.Name) + } + endpoint.User = nil + builderURL := endpoint.String() + if !seen[builderURL] { + urls = append(urls, builderURL) + seen[builderURL] = true + } + } + } + return urls, nil +} + +// GetMinBidGwei converts the ETH setting exactly, without floating point or +// rounding. A blank value lets client integrations retain their own defaults. +func (cfg *EpbsConfig) GetMinBidGwei() (string, error) { + return builderAmountToGwei(cfg.MinBid.Value.(string), cfg.MinBid.Name) +} + +// GetMaxExecutionPaymentGwei converts the ETH cap exactly. Blank input uses +// the default of zero, so only trustless payments count toward a builder bid. +func (cfg *EpbsConfig) GetMaxExecutionPaymentGwei() (string, error) { + gwei, err := builderAmountToGwei(cfg.MaxExecutionPayment.Value.(string), cfg.MaxExecutionPayment.Name) + if err == nil && gwei == "" { + return "0", nil + } + return gwei, err +} + +func builderAmountToGwei(value, name string) (string, error) { + value = strings.TrimSpace(value) + if value == "" { + return "", nil + } + if !builderAmountPattern.MatchString(value) { + return "", fmt.Errorf("ePBS %s must be a non-negative decimal amount, for example 0.05", name) + } + whole, fraction, _ := strings.Cut(value, ".") + fraction = strings.TrimRight(fraction, "0") + if len(fraction) > 9 { + return "", fmt.Errorf("ePBS %s", name) + } + gwei, err := strconv.ParseUint(whole+fraction+strings.Repeat("0", 9-len(fraction)), 10, 64) + if err != nil { + return "", fmt.Errorf("ePBS %s must not exceed 18446744073.709551615 ETH", name) + } + return strconv.FormatUint(gwei, 10), nil +} diff --git a/shared/services/config/migration/migration-manager.go b/shared/services/config/migration/migration-manager.go index 13789d1cf..dda9dec47 100644 --- a/shared/services/config/migration/migration-manager.go +++ b/shared/services/config/migration/migration-manager.go @@ -53,6 +53,11 @@ func UpdateConfig(serializedConfig map[string]map[string]string) error { if err != nil { return err } + // Include development configs saved by this release before ePBS was added. + v1245, err := parseVersion("1.24.5") + if err != nil { + return err + } // Create the collection of upgraders upgraders := []ConfigUpgrader{ @@ -85,6 +90,10 @@ func UpdateConfig(serializedConfig map[string]map[string]string) error { Version: v1210, UpgradeFunc: upgradeFromV1210, }, + { + Version: v1245, + UpgradeFunc: upgradeFromV1245, + }, } // Find the index of the provided config's version @@ -92,6 +101,7 @@ func UpdateConfig(serializedConfig map[string]map[string]string) error { for i, upgrader := range upgraders { if configVersion.LessThanOrEqual(upgrader.Version) { targetIndex = i + break } } diff --git a/shared/services/config/migration/migration-manager_test.go b/shared/services/config/migration/migration-manager_test.go new file mode 100644 index 000000000..9914d0c58 --- /dev/null +++ b/shared/services/config/migration/migration-manager_test.go @@ -0,0 +1,69 @@ +package migration + +import ( + "slices" + "testing" +) + +func TestUpdateConfigRunsAllApplicableMigrations(t *testing.T) { + serialized := map[string]map[string]string{ + "root": { + "version": "v1.3.1", "executionClient": "geth", + "enableMevBoost": "true", "enableCommitBoost": "false", + }, + "geth": { + "p2pPort": "30305", "ethstatsLabel": "node", "ethstatsLogin": "login", "archiveMode": "true", + }, + "nimbus": {"additionalFlags": "--log-level=debug"}, + "executionCommon": { + "openRpcPorts": "true", "suggestedBlockGasLimit": "36000000", + }, + "consensusCommon": { + "openApiPort": "false", "suggestedBlockGasLimit": "36000000", "checkpointSyncUrl": "https://beaconstate.info", + }, + "nethermind": {"pruneMemSize": "512", "fullPruneMemoryBudget": "512"}, + "smartnode": { + "network": "mainnet", "priorityFee": "2", "minipoolStakeGasThreshold": "150", "apiPort": "9001", + }, + "mevBoost": {"selectionMode": "relay", "bloxRouteMaxProfitEnabled": "true"}, + } + if err := UpdateConfig(serialized); err != nil { + t.Fatal(err) + } + for _, check := range []struct { + section string + key string + want string + }{ + {"executionCommon", "p2pPort", "30305"}, + {"executionCommon", "ethstatsLabel", "node"}, + {"executionCommon", "ethstatsLogin", "login"}, + {"nimbus", "additionalBnFlags", "--log-level=debug"}, + {"executionCommon", "openRpcPorts", "localhost"}, + {"consensusCommon", "openApiPort", "closed"}, + {"executionCommon", "suggestedBlockGasLimit", ""}, + {"consensusCommon", "suggestedBlockGasLimit", ""}, + {"executionCommon", "pruningMode", "archive"}, + {"nethermind", "pruneMemSize", ""}, + {"nethermind", "fullPruneMemoryBudget", ""}, + {"smartnode", "priorityFee", "0.01"}, + {"smartnode", "minipoolStakeGasThreshold", "20"}, + {"api", "apiPort", "9001"}, + {"mevBoost", "bloxRouteRegulatedEnabled", "true"}, + {"epbs", "bloxRouteRegulatedEnabled", "true"}, + {"epbs", "flashbotsEnabled", "false"}, + } { + if got := serialized[check.section][check.key]; got != check.want { + t.Errorf("%s.%s = %q, want %q", check.section, check.key, got, check.want) + } + } + if !slices.Contains(checkpointSyncUrlReplacements, serialized["consensusCommon"]["checkpointSyncUrl"]) { + t.Fatal("checkpoint URL migration was skipped") + } + if _, exists := serialized["smartnode"]["apiPort"]; exists { + t.Fatal("legacy API port was not removed") + } + if _, exists := serialized["mevBoost"]["bloxRouteMaxProfitEnabled"]; exists { + t.Fatal("retired relay was not migrated before initializing ePBS settings") + } +} diff --git a/shared/services/config/migration/v1245-manager.go b/shared/services/config/migration/v1245-manager.go new file mode 100644 index 000000000..f5f4cc201 --- /dev/null +++ b/shared/services/config/migration/v1245-manager.go @@ -0,0 +1,68 @@ +package migration + +import "strconv" + +// Preserve the relay catalog and profile membership at the time ePBS settings +// were introduced, independently of future changes to the service defaults. +var epbsRelayMigrations = []struct { + builderKey string + mevProfileKey string + commitBoostKey string +}{ + {"flashbotsEnabled", "enableRegulatedAllMev", "cbFlashbotsEnabled"}, + {"bloxRouteRegulatedEnabled", "enableRegulatedAllMev", "cbBloxRouteRegulatedEnabled"}, + {"ultrasoundEnabled", "enableUnregulatedAllMev", ""}, + {"ultrasoundFilteredEnabled", "enableRegulatedAllMev", "cbUltrasoundFilteredEnabled"}, + {"aestusEnabled", "enableUnregulatedAllMev", ""}, + {"titanGlobalEnabled", "enableUnregulatedAllMev", ""}, + {"titanRegionalEnabled", "enableRegulatedAllMev", "cbTitanRegionalEnabled"}, + {"btcsOfacEnabled", "enableRegulatedAllMev", "cbBtcsOfacEnabled"}, +} + +func upgradeFromV1245(serializedConfig map[string]map[string]string) error { + // An existing section belongs to the user, including blank bids and disabled + // builders. Never re-seed it from settings they may have since changed. + if serializedConfig["epbs"] != nil { + return nil + } + + root := serializedConfig["root"] + mev := serializedConfig["mevBoost"] + cb := serializedConfig["commitBoostConfig"] + + // Missing settings use the same defaults as the legacy configuration. Match + // the existing PBS URL precedence if both services were enabled. + mevEnabled, exists := root["enableMevBoost"] + useMev := !exists || isTrue(mevEnabled) + useCb := !useMev && isTrue(root["enableCommitBoost"]) + useMev = useMev && (mev["mode"] == "" || mev["mode"] == "local") + useCb = useCb && (cb["mode"] == "" || cb["mode"] == "local") + + // There were no shared bid limits to migrate. Default to counting only + // trustless payments. External service URLs and custom relays remain in their + // original sections; they are not entries in the built-in builder catalog. + epbs := map[string]string{"minBid": "", "maxExecutionPayment": "0"} + for _, relay := range epbsRelayMigrations { + enabled := false + if useMev { + switch mev["selectionMode"] { + case "", "profile": + enabled = isTrue(mev[relay.mevProfileKey]) + case "relay": + enabled = isTrue(mev[relay.builderKey]) + } + } else if useCb && relay.commitBoostKey != "" { + switch cb["relaySelectionMode"] { + case "", "all": + enabled = true + case "manual": + enabled = isTrue(cb[relay.commitBoostKey]) + } + } + // Keep selections across networks, just as manual relay selection does. + // The ePBS config filters unavailable endpoints for the active network. + epbs[relay.builderKey] = strconv.FormatBool(enabled) + } + serializedConfig["epbs"] = epbs + return nil +} diff --git a/shared/services/config/migration/v1245-manager_test.go b/shared/services/config/migration/v1245-manager_test.go new file mode 100644 index 000000000..e712d921f --- /dev/null +++ b/shared/services/config/migration/v1245-manager_test.go @@ -0,0 +1,190 @@ +package migration + +import ( + "maps" + "reflect" + "slices" + "strconv" + "testing" +) + +func TestUpdateConfigEpbsMigration(t *testing.T) { + regulated := []string{"flashbotsEnabled", "bloxRouteRegulatedEnabled", "ultrasoundFilteredEnabled", "titanRegionalEnabled", "btcsOfacEnabled"} + unregulated := []string{"ultrasoundEnabled", "aestusEnabled", "titanGlobalEnabled"} + all := slices.Concat(regulated, unregulated) + for _, test := range []struct { + name string + root map[string]string + mev map[string]string + cb map[string]string + want []string + }{ + { + name: "MEV manual ignores profiles", + mev: map[string]string{ + "selectionMode": "relay", "aestusEnabled": "true", "btcsOfacEnabled": "true", + "enableRegulatedAllMev": "true", "enableUnregulatedAllMev": "true", + }, + want: []string{"aestusEnabled", "btcsOfacEnabled"}, + }, + { + name: "MEV regulated profile ignores manual choices", + mev: map[string]string{"selectionMode": "profile", "enableRegulatedAllMev": "true", "aestusEnabled": "true"}, + want: regulated, + }, + { + name: "MEV unregulated profile", + mev: map[string]string{"selectionMode": "profile", "enableUnregulatedAllMev": "true"}, + want: unregulated, + }, + { + name: "MEV both profiles", + mev: map[string]string{"selectionMode": "profile", "enableRegulatedAllMev": "true", "enableUnregulatedAllMev": "true"}, + want: all, + }, + { + name: "MEV missing settings use legacy defaults", + mev: map[string]string{"enableUnregulatedAllMev": "true"}, + want: unregulated, + }, + { + name: "MEV empty profile ignores stale manual choices", + mev: map[string]string{"selectionMode": "profile", "flashbotsEnabled": "true"}, + }, + { + name: "Commit-Boost manual", + root: map[string]string{"enableMevBoost": "false", "enableCommitBoost": "true"}, + cb: map[string]string{"relaySelectionMode": "manual", "cbTitanRegionalEnabled": "true", "cbBtcsOfacEnabled": "true"}, + want: []string{"titanRegionalEnabled", "btcsOfacEnabled"}, + }, + { + name: "Commit-Boost all ignores disabled manual choices", + root: map[string]string{"enableMevBoost": "false", "enableCommitBoost": "true"}, + cb: map[string]string{"relaySelectionMode": "all", "cbFlashbotsEnabled": "false"}, + want: regulated, + }, + { + name: "Commit-Boost missing settings use legacy defaults", + root: map[string]string{"enableMevBoost": "false", "enableCommitBoost": "true"}, + want: regulated, + }, + { + name: "disabled services retain their settings without selecting builders", + root: map[string]string{"enableMevBoost": "false", "enableCommitBoost": "false"}, + mev: map[string]string{"selectionMode": "profile", "enableRegulatedAllMev": "true"}, + cb: map[string]string{"relaySelectionMode": "all"}, + }, + { + name: "external MEV service has no known relay selection", + mev: map[string]string{"mode": "external", "externalUrl": "http://mev.example:18550", "enableRegulatedAllMev": "true"}, + }, + { + name: "external Commit-Boost service has no known relay selection", + root: map[string]string{"enableMevBoost": "false", "enableCommitBoost": "true"}, + cb: map[string]string{"mode": "external", "externalUrl": "http://cb.example:18550", "relaySelectionMode": "all"}, + }, + { + name: "custom relays remain in Commit-Boost", + root: map[string]string{"enableMevBoost": "false", "enableCommitBoost": "true"}, + cb: map[string]string{"relaySelectionMode": "manual", "customRelays": "https://0x123@custom.example", "cbFlashbotsEnabled": "true"}, + want: []string{"flashbotsEnabled"}, + }, + { + name: "both enabled retains existing MEV precedence", + root: map[string]string{"enableMevBoost": "true", "enableCommitBoost": "true"}, + mev: map[string]string{"selectionMode": "relay", "aestusEnabled": "true"}, + cb: map[string]string{"relaySelectionMode": "all"}, + want: []string{"aestusEnabled"}, + }, + { + name: "external MEV does not fall back to inactive local Commit-Boost", + root: map[string]string{"enableMevBoost": "true", "enableCommitBoost": "true"}, + mev: map[string]string{"mode": "external"}, + cb: map[string]string{"relaySelectionMode": "all"}, + }, + } { + t.Run(test.name, func(t *testing.T) { + root := map[string]string{"version": "v1.24.4"} + maps.Copy(root, test.root) + serialized := map[string]map[string]string{ + "root": root, "mevBoost": test.mev, "commitBoostConfig": test.cb, + } + before := make(map[string]map[string]string) + for section, params := range serialized { + before[section] = maps.Clone(params) + } + if err := UpdateConfig(serialized); err != nil { + t.Fatal(err) + } + epbs := serialized["epbs"] + if len(epbs) != 10 || epbs["minBid"] != "" || epbs["maxExecutionPayment"] != "0" { + t.Fatalf("unexpected builder settings: %v", epbs) + } + for _, key := range all { + want := strconv.FormatBool(slices.Contains(test.want, key)) + if epbs[key] != want { + t.Errorf("%s = %q, want %q", key, epbs[key], want) + } + } + for section, params := range before { + if !reflect.DeepEqual(serialized[section], params) { + t.Errorf("migration changed existing section %s", section) + } + } + epbs = maps.Clone(epbs) + if err := UpdateConfig(serialized); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(serialized["epbs"], epbs) { + t.Fatal("repeated migration changed builder settings") + } + }) + } +} + +func TestUpdateConfigPreservesExistingEpbsSettings(t *testing.T) { + for _, epbs := range []map[string]string{ + {}, + {"minBid": ""}, + {"minBid": "0.05", "flashbotsEnabled": "false", "aestusEnabled": "true"}, + {"minBid": "0.05", "maxExecutionPayment": "0"}, + {"minBid": "0.05", "maxExecutionPayment": "0.02"}, + } { + serialized := map[string]map[string]string{ + "root": {"version": "v1.24.4", "enableMevBoost": "true"}, + "mevBoost": {"selectionMode": "profile", "enableRegulatedAllMev": "true"}, + "epbs": maps.Clone(epbs), + } + if err := UpdateConfig(serialized); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(serialized["epbs"], epbs) { + t.Fatalf("overwrote existing settings: got %v, want %v", serialized["epbs"], epbs) + } + } +} + +func TestUpdateConfigEpbsVersionBoundary(t *testing.T) { + for _, test := range []struct { + version string + migrate bool + }{ + {"v1.24.3", true}, + {"1.24.4", true}, + {"v1.24.4-dev", true}, + {"v1.24.5-dev", true}, + {"v1.24.5", true}, + {"v1.24.6", false}, + {"v1.25.0", false}, + } { + t.Run(test.version, func(t *testing.T) { + serialized := map[string]map[string]string{"root": {"version": test.version}} + if err := UpdateConfig(serialized); err != nil { + t.Fatal(err) + } + if _, migrated := serialized["epbs"]; migrated != test.migrate { + t.Fatalf("migrated = %v, want %v", migrated, test.migrate) + } + }) + } +} diff --git a/shared/services/config/rocket-pool-config.go b/shared/services/config/rocket-pool-config.go index 9c993eda5..9fc80f695 100644 --- a/shared/services/config/rocket-pool-config.go +++ b/shared/services/config/rocket-pool-config.go @@ -142,6 +142,9 @@ type RocketPoolConfig struct { // Native mode Native *NativeConfig `yaml:"native,omitempty"` + // Builder settings shared by consensus clients + Epbs *EpbsConfig `yaml:"epbs,omitempty"` + // MEV-Boost EnableMevBoost config.Parameter `yaml:"enableMevBoost,omitempty"` MevBoost *MevBoostConfig `yaml:"mevBoost,omitempty"` @@ -620,6 +623,7 @@ func newRocketPoolConfig(rpDir string, isNativeMode bool, networks *NetworksConf cfg.Exporter = NewExporterConfig(cfg) cfg.BitflyNodeMetrics = NewBitflyNodeMetricsConfig(cfg) cfg.Native = NewNativeConfig(cfg) + cfg.Epbs = NewEpbsConfig(cfg) cfg.MevBoost = NewMevBoostConfig(cfg) cfg.CommitBoost = NewCommitBoostConfig(cfg) // Addons @@ -746,6 +750,7 @@ func (cfg *RocketPoolConfig) GetSubconfigs() map[string]config.Config { "exporter": cfg.Exporter, "bitflyNodeMetrics": cfg.BitflyNodeMetrics, "native": cfg.Native, + "epbs": cfg.Epbs, "mevBoost": cfg.MevBoost, "commitBoostConfig": cfg.CommitBoost, "addons-gww": cfg.GraffitiWallWriter.GetConfig(), @@ -1807,6 +1812,15 @@ func (cfg *RocketPoolConfig) GetChanges(oldConfig *RocketPoolConfig) (map[string // Checks to see if the current configuration is valid; if not, returns a list of errors func (cfg *RocketPoolConfig) Validate() []string { errors := []string{} + if _, err := cfg.Epbs.GetMinBidGwei(); err != nil { + errors = append(errors, err.Error()) + } + if _, err := cfg.Epbs.GetMaxExecutionPaymentGwei(); err != nil { + errors = append(errors, err.Error()) + } + if _, err := cfg.Epbs.GetBuilderUrls(); err != nil { + errors = append(errors, err.Error()) + } if err := cfg.ValidateNetworkClients(); err != nil { errors = append(errors, err.Error()) } From 9da94930924bb4141958dec70c5f5cb9a6984f1c Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:11:05 -0300 Subject: [PATCH 02/10] Lodestar epbs flags --- shared/services/config/epbs-config.go | 14 ++++++++++++-- .../rocketpool/assets/install/scripts/start-vc.sh | 15 +++++++++++++++ .../assets/install/templates/validator.tmpl | 3 +++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/shared/services/config/epbs-config.go b/shared/services/config/epbs-config.go index c3f19e2cd..ddfb12850 100644 --- a/shared/services/config/epbs-config.go +++ b/shared/services/config/epbs-config.go @@ -13,8 +13,6 @@ import ( var builderAmountPattern = regexp.MustCompile(`^[0-9]+(\.[0-9]+)?$`) // EpbsConfig stores builder settings shared by all consensus clients. -// Client startup integration is separate; saving these settings does not yet -// require restarting any containers. type EpbsConfig struct { Title string `yaml:"-"` @@ -44,6 +42,7 @@ func NewEpbsConfig(cfg *RocketPoolConfig) *EpbsConfig { Description: "The minimum payment to accept from an ePBS builder, in ETH. For example, enter 0.05 for 0.05 ETH. Leave blank to use the consensus client's default.", Type: config.ParameterType_String, Default: map[config.Network]interface{}{config.Network_All: ""}, + AffectsContainers: []config.ContainerID{config.ContainerID_Validator}, CanBeBlank: true, OverwriteOnUpgrade: false, }, @@ -53,6 +52,7 @@ func NewEpbsConfig(cfg *RocketPoolConfig) *EpbsConfig { Description: "The maximum execution-layer payment to count toward an ePBS builder bid, in ETH. These payments rely on the builder's promise and are not backed by staked collateral. Set to 0 to count only trustless payments. For example, enter 0.05 to count up to 0.05 ETH of promised execution-layer payments.", Type: config.ParameterType_String, Default: map[config.Network]interface{}{config.Network_All: "0"}, + AffectsContainers: []config.ContainerID{config.ContainerID_Validator}, CanBeBlank: false, OverwriteOnUpgrade: false, }, @@ -68,6 +68,7 @@ func NewEpbsConfig(cfg *RocketPoolConfig) *EpbsConfig { Description: fmt.Sprintf("Select %s for the ePBS builder list. You can select multiple operators.\n\nUses this operator's existing relay endpoint for the selected network. ePBS availability at that endpoint has not yet been verified.", relay.Name), Type: config.ParameterType_Bool, Default: map[config.Network]interface{}{config.Network_All: false}, + AffectsContainers: []config.ContainerID{config.ContainerID_Validator}, CanBeBlank: false, OverwriteOnUpgrade: false, } @@ -152,6 +153,15 @@ func (cfg *EpbsConfig) GetBuilderUrls() ([]string, error) { return urls, nil } +// GetBuilderUrlString formats the selected endpoints for client CLI arguments. +func (cfg *EpbsConfig) GetBuilderUrlString() (string, error) { + urls, err := cfg.GetBuilderUrls() + if err != nil { + return "", err + } + return strings.Join(urls, ","), nil +} + // GetMinBidGwei converts the ETH setting exactly, without floating point or // rounding. A blank value lets client integrations retain their own defaults. func (cfg *EpbsConfig) GetMinBidGwei() (string, error) { diff --git a/shared/services/rocketpool/assets/install/scripts/start-vc.sh b/shared/services/rocketpool/assets/install/scripts/start-vc.sh index c162757b8..8a4e659d4 100755 --- a/shared/services/rocketpool/assets/install/scripts/start-vc.sh +++ b/shared/services/rocketpool/assets/install/scripts/start-vc.sh @@ -132,6 +132,21 @@ if [ ! -z "$VC_SUGGESTED_BLOCK_GAS_LIMIT" ]; then CMD="$CMD --monitoring.endpoint $BITFLY_NODE_METRICS_ENDPOINT?apikey=$BITFLY_NODE_METRICS_SECRET&machine=$BITFLY_NODE_METRICS_MACHINE_NAME" fi + # Set up the ePBS builder flags (amounts are in integer Gwei) + if [ ! -z "$EPBS_BUILDER_URLS" ]; then + CMD="$CMD --builder.urls $EPBS_BUILDER_URLS" + fi + if [ ! -z "$EPBS_MIN_BID_GWEI" ]; then + CMD="$CMD --builder.minBid $EPBS_MIN_BID_GWEI" + fi + if [ ! -z "$EPBS_MAX_EXECUTION_PAYMENT_GWEI" ]; then + CMD="$CMD --builder.maxExecutionPayment $EPBS_MAX_EXECUTION_PAYMENT_GWEI" + # Compare as text: a uint64 Gwei amount can exceed shell integer limits. + if [ "$EPBS_MAX_EXECUTION_PAYMENT_GWEI" != "0" ]; then + CMD="$CMD --allowDangerousTrustedPayments" + fi + fi + exec ${CMD} --graffiti "$GRAFFITI" fi diff --git a/shared/services/rocketpool/assets/install/templates/validator.tmpl b/shared/services/rocketpool/assets/install/templates/validator.tmpl index 1e7ceaa3a..6101c6c41 100644 --- a/shared/services/rocketpool/assets/install/templates/validator.tmpl +++ b/shared/services/rocketpool/assets/install/templates/validator.tmpl @@ -56,6 +56,9 @@ services: - ADDON_GWW_ENABLED={{.GraffitiWallWriter.GetEnabledParameter}} - PBS_URL={{.PbsUrl}} - ENABLE_PBS={{.IsPbsEnabled}} + - EPBS_BUILDER_URLS={{.Epbs.GetBuilderUrlString}} + - EPBS_MIN_BID_GWEI={{.Epbs.GetMinBidGwei}} + - EPBS_MAX_EXECUTION_PAYMENT_GWEI={{.Epbs.GetMaxExecutionPaymentGwei}} {{- if eq .ConsensusClient.String "teku"}} - TEKU_USE_SLASHING_PROTECTION={{.Teku.UseSlashingProtection}} {{- end}} From 0ce43c84b2122e0f02e133548aeedddf445fe03d Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:35:57 -0300 Subject: [PATCH 03/10] Teku epbs flags --- .../rocketpool/assets/install/scripts/start-vc.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/shared/services/rocketpool/assets/install/scripts/start-vc.sh b/shared/services/rocketpool/assets/install/scripts/start-vc.sh index 8a4e659d4..d1b144f8f 100755 --- a/shared/services/rocketpool/assets/install/scripts/start-vc.sh +++ b/shared/services/rocketpool/assets/install/scripts/start-vc.sh @@ -309,6 +309,17 @@ if [ "$CC_CLIENT" = "teku" ]; then CMD="$CMD --metrics-publish-endpoint=$BITFLY_NODE_METRICS_ENDPOINT?apikey=$BITFLY_NODE_METRICS_SECRET&machine=$BITFLY_NODE_METRICS_MACHINE_NAME" fi + # Set up the ePBS builder flags (amounts are in integer Gwei) + if [ ! -z "$EPBS_BUILDER_URLS" ]; then + CMD="$CMD --Xbuilder-urls=$EPBS_BUILDER_URLS" + fi + if [ ! -z "$EPBS_MIN_BID_GWEI" ]; then + CMD="$CMD --Xbuilder-min-bid=$EPBS_MIN_BID_GWEI" + fi + if [ ! -z "$EPBS_MAX_EXECUTION_PAYMENT_GWEI" ]; then + CMD="$CMD --Xbuilder-max-execution-payment=$EPBS_MAX_EXECUTION_PAYMENT_GWEI" + fi + if [ "$ADDON_GWW_ENABLED" = "true" ]; then echo "$GRAFFITI" > $GWW_GRAFFITI_FILE # Default graffiti value for Teku exec ${CMD} --validators-graffiti-file=$GWW_GRAFFITI_FILE From f3932216fd6bc49bd807e392eb31f8aca81ce2d7 Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:10:30 -0300 Subject: [PATCH 04/10] Prysm epbs flags --- .../rocketpool/assets/install/scripts/start-vc.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/shared/services/rocketpool/assets/install/scripts/start-vc.sh b/shared/services/rocketpool/assets/install/scripts/start-vc.sh index d1b144f8f..590784923 100755 --- a/shared/services/rocketpool/assets/install/scripts/start-vc.sh +++ b/shared/services/rocketpool/assets/install/scripts/start-vc.sh @@ -243,6 +243,16 @@ if [ "$CC_CLIENT" = "prysm" ]; then CMD="$CMD --disable-monitoring --disable-account-metrics" fi + if [ ! -z "$EPBS_BUILDER_URLS" ]; then + CMD="$CMD --builder-urls=$EPBS_BUILDER_URLS" + fi + if [ ! -z "$EPBS_MIN_BID_GWEI" ]; then + CMD="$CMD --builder-min-bid=$EPBS_MIN_BID_GWEI" + fi + if [ ! -z "$EPBS_MAX_EXECUTION_PAYMENT_GWEI" ]; then + CMD="$CMD --builder-max-execution-payment=$EPBS_MAX_EXECUTION_PAYMENT_GWEI" + fi + if [ "$ADDON_GWW_ENABLED" = "true" ]; then echo "ordered:\n - $GRAFFITI" > $GWW_GRAFFITI_FILE # Default graffiti value for Prysm exec ${CMD} --graffiti-file=$GWW_GRAFFITI_FILE From e8ed8a51d902c84a4f90c8f05692dc81d9e530b4 Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:24:13 -0300 Subject: [PATCH 05/10] Nimbus builder settings --- .../rocketpool/assets/install/scripts/start-vc.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/shared/services/rocketpool/assets/install/scripts/start-vc.sh b/shared/services/rocketpool/assets/install/scripts/start-vc.sh index 590784923..9279d7520 100755 --- a/shared/services/rocketpool/assets/install/scripts/start-vc.sh +++ b/shared/services/rocketpool/assets/install/scripts/start-vc.sh @@ -187,6 +187,14 @@ if [ "$CC_CLIENT" = "nimbus" ]; then CMD="$CMD --metrics --metrics-address=0.0.0.0 --metrics-port=$VC_METRICS_PORT" fi + # Set up the ePBS builder flags (amounts are in integer Gwei) + if [ ! -z "$EPBS_MIN_BID_GWEI" ]; then + CMD="$CMD --builder-min-bid=$EPBS_MIN_BID_GWEI" + fi + if [ ! -z "$EPBS_MAX_EXECUTION_PAYMENT_GWEI" ]; then + CMD="$CMD --builder-max-execution-payment=$EPBS_MAX_EXECUTION_PAYMENT_GWEI" + fi + # Graffiti breaks if it's in the CMD string instead of here because of spaces exec ${CMD} --graffiti="$GRAFFITI" From 0149735ff519efcd8fc80da6a9480ad85d7cded3 Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:07:06 -0300 Subject: [PATCH 06/10] Set Nimbus payload-builder-url --- .../rocketpool/assets/install/scripts/start-vc.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/shared/services/rocketpool/assets/install/scripts/start-vc.sh b/shared/services/rocketpool/assets/install/scripts/start-vc.sh index 9279d7520..24233e679 100755 --- a/shared/services/rocketpool/assets/install/scripts/start-vc.sh +++ b/shared/services/rocketpool/assets/install/scripts/start-vc.sh @@ -179,7 +179,7 @@ if [ "$CC_CLIENT" = "nimbus" ]; then --block-monitor-type=event \ $VC_ADDITIONAL_FLAGS" - if [ "$ENABLE_PBS" = "true" ]; then + if [ "$ENABLE_PBS" = "true" ] || [ ! -z "$EPBS_BUILDER_URLS" ]; then CMD="$CMD --payload-builder" fi @@ -188,6 +188,15 @@ if [ "$CC_CLIENT" = "nimbus" ]; then fi # Set up the ePBS builder flags (amounts are in integer Gwei) + if [ ! -z "$EPBS_BUILDER_URLS" ]; then + BUILDER_URLS=$(printf '%s' "$EPBS_BUILDER_URLS" | tr ',' ' ') + for BUILDER_URL in $BUILDER_URLS; do + CMD="$CMD --payload-builder-url=$BUILDER_URL" + done + case "$EPBS_BUILDER_URLS" in + *,*) echo "WARNING: Nimbus v26.10.1 uses only the last --payload-builder-url when multiple builders are configured." ;; + esac + fi if [ ! -z "$EPBS_MIN_BID_GWEI" ]; then CMD="$CMD --builder-min-bid=$EPBS_MIN_BID_GWEI" fi From ee968656a0816dafa165d31b6ccebbc99d14925b Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:07:23 -0300 Subject: [PATCH 07/10] commit-boost-rc1 --- shared/services/rocketpool/assets/install/testnet.env | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shared/services/rocketpool/assets/install/testnet.env b/shared/services/rocketpool/assets/install/testnet.env index 2e76cee9b..132d6f6ef 100644 --- a/shared/services/rocketpool/assets/install/testnet.env +++ b/shared/services/rocketpool/assets/install/testnet.env @@ -3,4 +3,5 @@ # Keys not listed here inherit the mainnet pin. # # Example: -# RP_IMAGE_GETH=ethereum/client-go:v1.17.4@sha256:... \ No newline at end of file +# RP_IMAGE_GETH=ethereum/client-go:v1.17.4@sha256:... +RP_IMAGE_COMMIT_BOOST=ghcr.io/commit-boost/commit-boost:v0.12.0-rc1@sha256:096000933cd18f8c98cc843e7cdcf7f3a14b0390cc2890bc815d13a35183ba50 \ No newline at end of file From 235069b32f5ab6bdca8189ad4f381599e41a097b Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:09:38 -0300 Subject: [PATCH 08/10] Improve error message --- shared/services/config/epbs-config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/services/config/epbs-config.go b/shared/services/config/epbs-config.go index ddfb12850..09323ab0a 100644 --- a/shared/services/config/epbs-config.go +++ b/shared/services/config/epbs-config.go @@ -189,7 +189,7 @@ func builderAmountToGwei(value, name string) (string, error) { whole, fraction, _ := strings.Cut(value, ".") fraction = strings.TrimRight(fraction, "0") if len(fraction) > 9 { - return "", fmt.Errorf("ePBS %s", name) + return "", fmt.Errorf("ePBS %s must not have more than 9 decimal places", name) } gwei, err := strconv.ParseUint(whole+fraction+strings.Repeat("0", 9-len(fraction)), 10, 64) if err != nil { From 0c1a7535999b4e69a66cb7d1ff21e9df728fe215 Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:12:49 -0300 Subject: [PATCH 09/10] Fix GetBuilderUrls --- shared/services/config/epbs-config.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/shared/services/config/epbs-config.go b/shared/services/config/epbs-config.go index 09323ab0a..741d24560 100644 --- a/shared/services/config/epbs-config.go +++ b/shared/services/config/epbs-config.go @@ -123,9 +123,10 @@ func (cfg *EpbsConfig) GetAvailableRelayParameters() []*config.Parameter { return params } -// GetBuilderUrls returns the selected network's endpoints in catalog order. -// These are seeded from the relay catalog, not verified ePBS endpoints. Relay -// signing pubkeys in URL userinfo are not builder authentication credentials. +// GetBuilderUrls returns the selected network's endpoints in catalog order as +// scheme://host. These are seeded from the relay catalog, not verified ePBS +// endpoints. Relay userinfo and query strings are MEV-Boost metadata, not +// ePBS builder credentials or paths. func (cfg *EpbsConfig) GetBuilderUrls() ([]string, error) { urls := []string{} seen := map[string]bool{} @@ -142,8 +143,10 @@ func (cfg *EpbsConfig) GetBuilderUrls() ([]string, error) { if err != nil || (endpoint.Scheme != "https" && endpoint.Scheme != "http") || endpoint.Hostname() == "" { return nil, fmt.Errorf("ePBS builder endpoint for %s must be an HTTP or HTTPS URL", relay.Name) } - endpoint.User = nil - builderURL := endpoint.String() + builderURL := (&url.URL{ + Scheme: endpoint.Scheme, + Host: endpoint.Host, + }).String() if !seen[builderURL] { urls = append(urls, builderURL) seen[builderURL] = true From 0aab8b85d692512a80952d0655604d8444361fbf Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:31:25 -0300 Subject: [PATCH 10/10] Adjust builder urls for Hoodi --- .../service/config/settings-builder.go | 4 +- shared/services/config/epbs-config.go | 134 ++++++------ .../migration/migration-manager_test.go | 69 ------- .../config/migration/v1245-manager.go | 49 +++-- .../config/migration/v1245-manager_test.go | 190 ------------------ 5 files changed, 89 insertions(+), 357 deletions(-) delete mode 100644 shared/services/config/migration/migration-manager_test.go delete mode 100644 shared/services/config/migration/v1245-manager_test.go diff --git a/rocketpool-cli/service/config/settings-builder.go b/rocketpool-cli/service/config/settings-builder.go index 2b4ba02b6..b3a281ae8 100644 --- a/rocketpool-cli/service/config/settings-builder.go +++ b/rocketpool-cli/service/config/settings-builder.go @@ -29,7 +29,7 @@ func NewBuilderConfigPage(epbsPage *EpbsPage) *BuilderConfigPage { epbsPage.page, "settings-builder", "Builder Settings", - "Set the minimum bid and maximum execution payment in ETH, and select relay operators for the ePBS builder list. These settings are shared by all consensus clients.", + "Set the minimum bid and maximum execution payment in ETH, and select builders for the ePBS builder list. These settings are shared by all consensus clients.", configPage.layout.grid, ) configPage.handleLayoutChanged() @@ -45,7 +45,7 @@ func (configPage *BuilderConfigPage) handleLayoutChanged() { configPage.layout.form.Clear(true) configPage.layout.form.AddFormItem(configPage.items[&cfg.MinBid].item) configPage.layout.form.AddFormItem(configPage.items[&cfg.MaxExecutionPayment].item) - for _, param := range cfg.GetAvailableRelayParameters() { + for _, param := range cfg.GetAvailableBuilderParameters() { configPage.layout.form.AddFormItem(configPage.items[param].item) } configPage.layout.refresh() diff --git a/shared/services/config/epbs-config.go b/shared/services/config/epbs-config.go index 741d24560..9c98ea02c 100644 --- a/shared/services/config/epbs-config.go +++ b/shared/services/config/epbs-config.go @@ -16,26 +16,22 @@ var builderAmountPattern = regexp.MustCompile(`^[0-9]+(\.[0-9]+)?$`) type EpbsConfig struct { Title string `yaml:"-"` - MinBid config.Parameter `yaml:"minBid,omitempty"` - MaxExecutionPayment config.Parameter `yaml:"maxExecutionPayment,omitempty"` - FlashbotsRelay config.Parameter `yaml:"flashbotsEnabled,omitempty"` - BloxRouteRegulatedRelay config.Parameter `yaml:"bloxRouteRegulatedEnabled,omitempty"` - UltrasoundRelay config.Parameter `yaml:"ultrasoundEnabled,omitempty"` - UltrasoundFilteredRelay config.Parameter `yaml:"ultrasoundFilteredEnabled,omitempty"` - AestusRelay config.Parameter `yaml:"aestusEnabled,omitempty"` - TitanGlobalRelay config.Parameter `yaml:"titanGlobalEnabled,omitempty"` - TitanRegionalRelay config.Parameter `yaml:"titanRegionalEnabled,omitempty"` - BtcsOfacRelay config.Parameter `yaml:"btcsOfacEnabled,omitempty"` + MinBid config.Parameter `yaml:"minBid,omitempty"` + MaxExecutionPayment config.Parameter `yaml:"maxExecutionPayment,omitempty"` + Aestus config.Parameter `yaml:"aestusEnabled,omitempty"` + Flashbots config.Parameter `yaml:"flashbotsEnabled,omitempty"` + Nflaig config.Parameter `yaml:"nflaigEnabled,omitempty"` + Titan config.Parameter `yaml:"titanEnabled,omitempty"` + Ultrasound config.Parameter `yaml:"ultrasoundEnabled,omitempty"` + Pk910 config.Parameter `yaml:"pk910Enabled,omitempty"` parentConfig *RocketPoolConfig `yaml:"-"` - relays []config.MevRelay `yaml:"-"` } func NewEpbsConfig(cfg *RocketPoolConfig) *EpbsConfig { epbs := &EpbsConfig{ Title: "Builder Settings", parentConfig: cfg, - relays: createDefaultRelays(cfg.networks), MinBid: config.Parameter{ ID: "minBid", Name: "Minimum Bid (ETH)", @@ -57,48 +53,42 @@ func NewEpbsConfig(cfg *RocketPoolConfig) *EpbsConfig { OverwriteOnUpgrade: false, }, } - for _, option := range epbs.relayOptions() { - for _, relay := range epbs.relays { - if relay.ID != option.id { - continue - } - *option.parameter = config.Parameter{ - ID: option.parameterID, - Name: fmt.Sprintf("Enable %s", relay.Name), - Description: fmt.Sprintf("Select %s for the ePBS builder list. You can select multiple operators.\n\nUses this operator's existing relay endpoint for the selected network. ePBS availability at that endpoint has not yet been verified.", relay.Name), - Type: config.ParameterType_Bool, - Default: map[config.Network]interface{}{config.Network_All: false}, - AffectsContainers: []config.ContainerID{config.ContainerID_Validator}, - CanBeBlank: false, - OverwriteOnUpgrade: false, - } + for _, option := range epbs.builderOptions() { + *option.parameter = config.Parameter{ + ID: option.id, + Name: fmt.Sprintf("Enable %s", option.name), + Description: fmt.Sprintf("Select %s for the ePBS builder list. You can select multiple operators.\n\n%s", option.name, option.url), + Type: config.ParameterType_Bool, + Default: map[config.Network]interface{}{config.Network_All: false}, + AffectsContainers: []config.ContainerID{config.ContainerID_Validator}, + CanBeBlank: false, + OverwriteOnUpgrade: false, } } return epbs } -type epbsRelayOption struct { - id config.MevRelayID - parameterID string - parameter *config.Parameter +type epbsBuilderOption struct { + id string + name string + url string + parameter *config.Parameter } -func (cfg *EpbsConfig) relayOptions() []epbsRelayOption { - return []epbsRelayOption{ - {config.MevRelayID_Flashbots, "flashbotsEnabled", &cfg.FlashbotsRelay}, - {config.MevRelayID_BloxrouteRegulated, "bloxRouteRegulatedEnabled", &cfg.BloxRouteRegulatedRelay}, - {config.MevRelayID_Ultrasound, "ultrasoundEnabled", &cfg.UltrasoundRelay}, - {config.MevRelayID_UltrasoundFiltered, "ultrasoundFilteredEnabled", &cfg.UltrasoundFilteredRelay}, - {config.MevRelayID_Aestus, "aestusEnabled", &cfg.AestusRelay}, - {config.MevRelayID_TitanGlobal, "titanGlobalEnabled", &cfg.TitanGlobalRelay}, - {config.MevRelayID_TitanRegional, "titanRegionalEnabled", &cfg.TitanRegionalRelay}, - {config.MevRelayID_BTCSOfac, "btcsOfacEnabled", &cfg.BtcsOfacRelay}, +func (cfg *EpbsConfig) builderOptions() []epbsBuilderOption { + return []epbsBuilderOption{ + {"aestusEnabled", "Aestus", "https://hoodi.aestus.live", &cfg.Aestus}, + {"flashbotsEnabled", "Flashbots", "https://boost-relay-hoodi.flashbots.net", &cfg.Flashbots}, + {"nflaigEnabled", "nflaig", "https://builder-hoodi.nflaig.dev", &cfg.Nflaig}, + {"titanEnabled", "Titan", "https://hoodi.titanrelay.xyz", &cfg.Titan}, + {"ultrasoundEnabled", "Ultra Sound", "https://relay-hoodi.ultrasound.money", &cfg.Ultrasound}, + {"pk910Enabled", "pk910", "https://builder-hoodi.pk910.de", &cfg.Pk910}, } } func (cfg *EpbsConfig) GetParameters() []*config.Parameter { params := []*config.Parameter{&cfg.MinBid, &cfg.MaxExecutionPayment} - for _, option := range cfg.relayOptions() { + for _, option := range cfg.builderOptions() { params = append(params, option.parameter) } return params @@ -108,49 +98,45 @@ func (cfg *EpbsConfig) GetConfigTitle() string { return cfg.Title } -// GetAvailableRelayParameters uses the same operator catalog and network -// availability as MEV-Boost, with independent selections for ePBS. -func (cfg *EpbsConfig) GetAvailableRelayParameters() []*config.Parameter { +func (cfg *EpbsConfig) hoodiBuildersAvailable() bool { + info := cfg.parentConfig.GetNetworkInfo() + return info != nil && info.BeaconNetwork == "hoodi" +} + +// GetAvailableBuilderParameters returns the Hoodi ePBS builder catalog. +func (cfg *EpbsConfig) GetAvailableBuilderParameters() []*config.Parameter { + if !cfg.hoodiBuildersAvailable() { + return nil + } params := []*config.Parameter{} - network := cfg.parentConfig.GetNetwork() - for _, option := range cfg.relayOptions() { - for _, relay := range cfg.relays { - if relay.ID == option.id && relay.Urls.UrlExists(network) { - params = append(params, option.parameter) - } - } + for _, option := range cfg.builderOptions() { + params = append(params, option.parameter) } return params } -// GetBuilderUrls returns the selected network's endpoints in catalog order as -// scheme://host. These are seeded from the relay catalog, not verified ePBS -// endpoints. Relay userinfo and query strings are MEV-Boost metadata, not -// ePBS builder credentials or paths. +// GetBuilderUrls returns the selected endpoints in catalog order as scheme://host. func (cfg *EpbsConfig) GetBuilderUrls() ([]string, error) { + if !cfg.hoodiBuildersAvailable() { + return nil, nil + } urls := []string{} seen := map[string]bool{} - network := cfg.parentConfig.GetNetwork() - for _, option := range cfg.relayOptions() { + for _, option := range cfg.builderOptions() { if option.parameter.Value != true { continue } - for _, relay := range cfg.relays { - if relay.ID != option.id || !relay.Urls.UrlExists(network) { - continue - } - endpoint, err := url.Parse(relay.Urls[network]) - if err != nil || (endpoint.Scheme != "https" && endpoint.Scheme != "http") || endpoint.Hostname() == "" { - return nil, fmt.Errorf("ePBS builder endpoint for %s must be an HTTP or HTTPS URL", relay.Name) - } - builderURL := (&url.URL{ - Scheme: endpoint.Scheme, - Host: endpoint.Host, - }).String() - if !seen[builderURL] { - urls = append(urls, builderURL) - seen[builderURL] = true - } + endpoint, err := url.Parse(option.url) + if err != nil || (endpoint.Scheme != "https" && endpoint.Scheme != "http") || endpoint.Hostname() == "" { + return nil, fmt.Errorf("ePBS builder endpoint for %s must be an HTTP or HTTPS URL", option.name) + } + builderURL := (&url.URL{ + Scheme: endpoint.Scheme, + Host: endpoint.Host, + }).String() + if !seen[builderURL] { + urls = append(urls, builderURL) + seen[builderURL] = true } } return urls, nil diff --git a/shared/services/config/migration/migration-manager_test.go b/shared/services/config/migration/migration-manager_test.go deleted file mode 100644 index 9914d0c58..000000000 --- a/shared/services/config/migration/migration-manager_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package migration - -import ( - "slices" - "testing" -) - -func TestUpdateConfigRunsAllApplicableMigrations(t *testing.T) { - serialized := map[string]map[string]string{ - "root": { - "version": "v1.3.1", "executionClient": "geth", - "enableMevBoost": "true", "enableCommitBoost": "false", - }, - "geth": { - "p2pPort": "30305", "ethstatsLabel": "node", "ethstatsLogin": "login", "archiveMode": "true", - }, - "nimbus": {"additionalFlags": "--log-level=debug"}, - "executionCommon": { - "openRpcPorts": "true", "suggestedBlockGasLimit": "36000000", - }, - "consensusCommon": { - "openApiPort": "false", "suggestedBlockGasLimit": "36000000", "checkpointSyncUrl": "https://beaconstate.info", - }, - "nethermind": {"pruneMemSize": "512", "fullPruneMemoryBudget": "512"}, - "smartnode": { - "network": "mainnet", "priorityFee": "2", "minipoolStakeGasThreshold": "150", "apiPort": "9001", - }, - "mevBoost": {"selectionMode": "relay", "bloxRouteMaxProfitEnabled": "true"}, - } - if err := UpdateConfig(serialized); err != nil { - t.Fatal(err) - } - for _, check := range []struct { - section string - key string - want string - }{ - {"executionCommon", "p2pPort", "30305"}, - {"executionCommon", "ethstatsLabel", "node"}, - {"executionCommon", "ethstatsLogin", "login"}, - {"nimbus", "additionalBnFlags", "--log-level=debug"}, - {"executionCommon", "openRpcPorts", "localhost"}, - {"consensusCommon", "openApiPort", "closed"}, - {"executionCommon", "suggestedBlockGasLimit", ""}, - {"consensusCommon", "suggestedBlockGasLimit", ""}, - {"executionCommon", "pruningMode", "archive"}, - {"nethermind", "pruneMemSize", ""}, - {"nethermind", "fullPruneMemoryBudget", ""}, - {"smartnode", "priorityFee", "0.01"}, - {"smartnode", "minipoolStakeGasThreshold", "20"}, - {"api", "apiPort", "9001"}, - {"mevBoost", "bloxRouteRegulatedEnabled", "true"}, - {"epbs", "bloxRouteRegulatedEnabled", "true"}, - {"epbs", "flashbotsEnabled", "false"}, - } { - if got := serialized[check.section][check.key]; got != check.want { - t.Errorf("%s.%s = %q, want %q", check.section, check.key, got, check.want) - } - } - if !slices.Contains(checkpointSyncUrlReplacements, serialized["consensusCommon"]["checkpointSyncUrl"]) { - t.Fatal("checkpoint URL migration was skipped") - } - if _, exists := serialized["smartnode"]["apiPort"]; exists { - t.Fatal("legacy API port was not removed") - } - if _, exists := serialized["mevBoost"]["bloxRouteMaxProfitEnabled"]; exists { - t.Fatal("retired relay was not migrated before initializing ePBS settings") - } -} diff --git a/shared/services/config/migration/v1245-manager.go b/shared/services/config/migration/v1245-manager.go index f5f4cc201..f655c68ee 100644 --- a/shared/services/config/migration/v1245-manager.go +++ b/shared/services/config/migration/v1245-manager.go @@ -2,21 +2,20 @@ package migration import "strconv" -// Preserve the relay catalog and profile membership at the time ePBS settings -// were introduced, independently of future changes to the service defaults. -var epbsRelayMigrations = []struct { - builderKey string - mevProfileKey string - commitBoostKey string +// Preserve overlapping MEV-Boost / Commit-Boost operator selections at the time +// ePBS settings were introduced. +var epbsBuilderMigrations = []struct { + builderKey string + mevRelayKeys []string + mevProfileKeys []string + commitBoostKeys []string }{ - {"flashbotsEnabled", "enableRegulatedAllMev", "cbFlashbotsEnabled"}, - {"bloxRouteRegulatedEnabled", "enableRegulatedAllMev", "cbBloxRouteRegulatedEnabled"}, - {"ultrasoundEnabled", "enableUnregulatedAllMev", ""}, - {"ultrasoundFilteredEnabled", "enableRegulatedAllMev", "cbUltrasoundFilteredEnabled"}, - {"aestusEnabled", "enableUnregulatedAllMev", ""}, - {"titanGlobalEnabled", "enableUnregulatedAllMev", ""}, - {"titanRegionalEnabled", "enableRegulatedAllMev", "cbTitanRegionalEnabled"}, - {"btcsOfacEnabled", "enableRegulatedAllMev", "cbBtcsOfacEnabled"}, + {"aestusEnabled", []string{"aestusEnabled"}, []string{"enableUnregulatedAllMev"}, nil}, + {"flashbotsEnabled", []string{"flashbotsEnabled"}, []string{"enableRegulatedAllMev"}, []string{"cbFlashbotsEnabled"}}, + {"nflaigEnabled", nil, nil, nil}, + {"titanEnabled", []string{"titanGlobalEnabled", "titanRegionalEnabled"}, []string{"enableRegulatedAllMev", "enableUnregulatedAllMev"}, []string{"cbTitanRegionalEnabled"}}, + {"ultrasoundEnabled", []string{"ultrasoundEnabled"}, []string{"enableUnregulatedAllMev"}, nil}, + {"pk910Enabled", nil, nil, nil}, } func upgradeFromV1245(serializedConfig map[string]map[string]string) error { @@ -42,26 +41,32 @@ func upgradeFromV1245(serializedConfig map[string]map[string]string) error { // trustless payments. External service URLs and custom relays remain in their // original sections; they are not entries in the built-in builder catalog. epbs := map[string]string{"minBid": "", "maxExecutionPayment": "0"} - for _, relay := range epbsRelayMigrations { + for _, builder := range epbsBuilderMigrations { enabled := false if useMev { switch mev["selectionMode"] { case "", "profile": - enabled = isTrue(mev[relay.mevProfileKey]) + for _, key := range builder.mevProfileKeys { + enabled = enabled || isTrue(mev[key]) + } case "relay": - enabled = isTrue(mev[relay.builderKey]) + for _, key := range builder.mevRelayKeys { + enabled = enabled || isTrue(mev[key]) + } } - } else if useCb && relay.commitBoostKey != "" { + } else if useCb && len(builder.commitBoostKeys) > 0 { switch cb["relaySelectionMode"] { case "", "all": enabled = true case "manual": - enabled = isTrue(cb[relay.commitBoostKey]) + for _, key := range builder.commitBoostKeys { + enabled = enabled || isTrue(cb[key]) + } } } - // Keep selections across networks, just as manual relay selection does. - // The ePBS config filters unavailable endpoints for the active network. - epbs[relay.builderKey] = strconv.FormatBool(enabled) + // Keep selections across networks. The ePBS config only exposes Hoodi + // endpoints for the active network. + epbs[builder.builderKey] = strconv.FormatBool(enabled) } serializedConfig["epbs"] = epbs return nil diff --git a/shared/services/config/migration/v1245-manager_test.go b/shared/services/config/migration/v1245-manager_test.go deleted file mode 100644 index e712d921f..000000000 --- a/shared/services/config/migration/v1245-manager_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package migration - -import ( - "maps" - "reflect" - "slices" - "strconv" - "testing" -) - -func TestUpdateConfigEpbsMigration(t *testing.T) { - regulated := []string{"flashbotsEnabled", "bloxRouteRegulatedEnabled", "ultrasoundFilteredEnabled", "titanRegionalEnabled", "btcsOfacEnabled"} - unregulated := []string{"ultrasoundEnabled", "aestusEnabled", "titanGlobalEnabled"} - all := slices.Concat(regulated, unregulated) - for _, test := range []struct { - name string - root map[string]string - mev map[string]string - cb map[string]string - want []string - }{ - { - name: "MEV manual ignores profiles", - mev: map[string]string{ - "selectionMode": "relay", "aestusEnabled": "true", "btcsOfacEnabled": "true", - "enableRegulatedAllMev": "true", "enableUnregulatedAllMev": "true", - }, - want: []string{"aestusEnabled", "btcsOfacEnabled"}, - }, - { - name: "MEV regulated profile ignores manual choices", - mev: map[string]string{"selectionMode": "profile", "enableRegulatedAllMev": "true", "aestusEnabled": "true"}, - want: regulated, - }, - { - name: "MEV unregulated profile", - mev: map[string]string{"selectionMode": "profile", "enableUnregulatedAllMev": "true"}, - want: unregulated, - }, - { - name: "MEV both profiles", - mev: map[string]string{"selectionMode": "profile", "enableRegulatedAllMev": "true", "enableUnregulatedAllMev": "true"}, - want: all, - }, - { - name: "MEV missing settings use legacy defaults", - mev: map[string]string{"enableUnregulatedAllMev": "true"}, - want: unregulated, - }, - { - name: "MEV empty profile ignores stale manual choices", - mev: map[string]string{"selectionMode": "profile", "flashbotsEnabled": "true"}, - }, - { - name: "Commit-Boost manual", - root: map[string]string{"enableMevBoost": "false", "enableCommitBoost": "true"}, - cb: map[string]string{"relaySelectionMode": "manual", "cbTitanRegionalEnabled": "true", "cbBtcsOfacEnabled": "true"}, - want: []string{"titanRegionalEnabled", "btcsOfacEnabled"}, - }, - { - name: "Commit-Boost all ignores disabled manual choices", - root: map[string]string{"enableMevBoost": "false", "enableCommitBoost": "true"}, - cb: map[string]string{"relaySelectionMode": "all", "cbFlashbotsEnabled": "false"}, - want: regulated, - }, - { - name: "Commit-Boost missing settings use legacy defaults", - root: map[string]string{"enableMevBoost": "false", "enableCommitBoost": "true"}, - want: regulated, - }, - { - name: "disabled services retain their settings without selecting builders", - root: map[string]string{"enableMevBoost": "false", "enableCommitBoost": "false"}, - mev: map[string]string{"selectionMode": "profile", "enableRegulatedAllMev": "true"}, - cb: map[string]string{"relaySelectionMode": "all"}, - }, - { - name: "external MEV service has no known relay selection", - mev: map[string]string{"mode": "external", "externalUrl": "http://mev.example:18550", "enableRegulatedAllMev": "true"}, - }, - { - name: "external Commit-Boost service has no known relay selection", - root: map[string]string{"enableMevBoost": "false", "enableCommitBoost": "true"}, - cb: map[string]string{"mode": "external", "externalUrl": "http://cb.example:18550", "relaySelectionMode": "all"}, - }, - { - name: "custom relays remain in Commit-Boost", - root: map[string]string{"enableMevBoost": "false", "enableCommitBoost": "true"}, - cb: map[string]string{"relaySelectionMode": "manual", "customRelays": "https://0x123@custom.example", "cbFlashbotsEnabled": "true"}, - want: []string{"flashbotsEnabled"}, - }, - { - name: "both enabled retains existing MEV precedence", - root: map[string]string{"enableMevBoost": "true", "enableCommitBoost": "true"}, - mev: map[string]string{"selectionMode": "relay", "aestusEnabled": "true"}, - cb: map[string]string{"relaySelectionMode": "all"}, - want: []string{"aestusEnabled"}, - }, - { - name: "external MEV does not fall back to inactive local Commit-Boost", - root: map[string]string{"enableMevBoost": "true", "enableCommitBoost": "true"}, - mev: map[string]string{"mode": "external"}, - cb: map[string]string{"relaySelectionMode": "all"}, - }, - } { - t.Run(test.name, func(t *testing.T) { - root := map[string]string{"version": "v1.24.4"} - maps.Copy(root, test.root) - serialized := map[string]map[string]string{ - "root": root, "mevBoost": test.mev, "commitBoostConfig": test.cb, - } - before := make(map[string]map[string]string) - for section, params := range serialized { - before[section] = maps.Clone(params) - } - if err := UpdateConfig(serialized); err != nil { - t.Fatal(err) - } - epbs := serialized["epbs"] - if len(epbs) != 10 || epbs["minBid"] != "" || epbs["maxExecutionPayment"] != "0" { - t.Fatalf("unexpected builder settings: %v", epbs) - } - for _, key := range all { - want := strconv.FormatBool(slices.Contains(test.want, key)) - if epbs[key] != want { - t.Errorf("%s = %q, want %q", key, epbs[key], want) - } - } - for section, params := range before { - if !reflect.DeepEqual(serialized[section], params) { - t.Errorf("migration changed existing section %s", section) - } - } - epbs = maps.Clone(epbs) - if err := UpdateConfig(serialized); err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(serialized["epbs"], epbs) { - t.Fatal("repeated migration changed builder settings") - } - }) - } -} - -func TestUpdateConfigPreservesExistingEpbsSettings(t *testing.T) { - for _, epbs := range []map[string]string{ - {}, - {"minBid": ""}, - {"minBid": "0.05", "flashbotsEnabled": "false", "aestusEnabled": "true"}, - {"minBid": "0.05", "maxExecutionPayment": "0"}, - {"minBid": "0.05", "maxExecutionPayment": "0.02"}, - } { - serialized := map[string]map[string]string{ - "root": {"version": "v1.24.4", "enableMevBoost": "true"}, - "mevBoost": {"selectionMode": "profile", "enableRegulatedAllMev": "true"}, - "epbs": maps.Clone(epbs), - } - if err := UpdateConfig(serialized); err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(serialized["epbs"], epbs) { - t.Fatalf("overwrote existing settings: got %v, want %v", serialized["epbs"], epbs) - } - } -} - -func TestUpdateConfigEpbsVersionBoundary(t *testing.T) { - for _, test := range []struct { - version string - migrate bool - }{ - {"v1.24.3", true}, - {"1.24.4", true}, - {"v1.24.4-dev", true}, - {"v1.24.5-dev", true}, - {"v1.24.5", true}, - {"v1.24.6", false}, - {"v1.25.0", false}, - } { - t.Run(test.version, func(t *testing.T) { - serialized := map[string]map[string]string{"root": {"version": test.version}} - if err := UpdateConfig(serialized); err != nil { - t.Fatal(err) - } - if _, migrated := serialized["epbs"]; migrated != test.migrate { - t.Fatalf("migrated = %v, want %v", migrated, test.migrate) - } - }) - } -}