diff --git a/rocketpool-cli/service/config/settings-fallback.go b/rocketpool-cli/service/config/settings-fallback.go index 5d45dc6dc..41dcf9ec8 100644 --- a/rocketpool-cli/service/config/settings-fallback.go +++ b/rocketpool-cli/service/config/settings-fallback.go @@ -9,14 +9,15 @@ import ( // The page wrapper for the fallback config type FallbackConfigPage struct { - home *settingsHome - page *page - layout *standardLayout - masterConfig *config.RocketPoolConfig - useFallbackBox *parameterizedFormItem - reconnectDelay *parameterizedFormItem - fallbackNormalItems []*parameterizedFormItem - fallbackPrysmItems []*parameterizedFormItem + home *settingsHome + page *page + layout *standardLayout + masterConfig *config.RocketPoolConfig + useFallbackBox *parameterizedFormItem + reconnectDelay *parameterizedFormItem + fallbackNormalItems []*parameterizedFormItem + fallbackPrysmItems []*parameterizedFormItem + prioritizeValidation *parameterizedFormItem } // Creates a new page for the fallback client settings @@ -76,11 +77,13 @@ func (configPage *FallbackConfigPage) createContent() { configPage.reconnectDelay = createParameterizedStringField(&configPage.masterConfig.ReconnectDelay) configPage.fallbackNormalItems = createParameterizedFormItems(configPage.masterConfig.FallbackNormal.GetParameters(), configPage.layout.descriptionBox) configPage.fallbackPrysmItems = createParameterizedFormItems(configPage.masterConfig.FallbackPrysm.GetParameters(), configPage.layout.descriptionBox) + configPage.prioritizeValidation = createParameterizedCheckbox(&configPage.masterConfig.PrioritizeValidation) // Map the parameters to the form items in the layout configPage.layout.mapParameterizedFormItems(configPage.useFallbackBox, configPage.reconnectDelay) configPage.layout.mapParameterizedFormItems(configPage.fallbackNormalItems...) configPage.layout.mapParameterizedFormItems(configPage.fallbackPrysmItems...) + configPage.layout.mapParameterizedFormItems(configPage.prioritizeValidation) // Set up the setting callbacks configPage.useFallbackBox.item.(*tview.Checkbox).SetChangedFunc(func(checked bool) { @@ -113,6 +116,7 @@ func (configPage *FallbackConfigPage) handleUseFallbackChanged() { default: configPage.layout.addFormItems(configPage.fallbackNormalItems) } + configPage.layout.form.AddFormItem(configPage.prioritizeValidation.item) configPage.layout.refresh() } diff --git a/rocketpool-cli/service/config/settings-native-fallback.go b/rocketpool-cli/service/config/settings-native-fallback.go index 206d92051..5db5073cc 100644 --- a/rocketpool-cli/service/config/settings-native-fallback.go +++ b/rocketpool-cli/service/config/settings-native-fallback.go @@ -8,13 +8,14 @@ import ( // The page wrapper for the fallback config type NativeFallbackConfigPage struct { - home *settingsNativeHome - page *page - layout *standardLayout - masterConfig *config.RocketPoolConfig - useFallbackBox *parameterizedFormItem - reconnectDelay *parameterizedFormItem - fallbackItems []*parameterizedFormItem + home *settingsNativeHome + page *page + layout *standardLayout + masterConfig *config.RocketPoolConfig + useFallbackBox *parameterizedFormItem + reconnectDelay *parameterizedFormItem + fallbackItems []*parameterizedFormItem + prioritizeValidation *parameterizedFormItem } // Creates a new page for the fallback client settings @@ -73,10 +74,12 @@ func (configPage *NativeFallbackConfigPage) createContent() { configPage.useFallbackBox = createParameterizedCheckbox(&configPage.masterConfig.UseFallbackClients) configPage.reconnectDelay = createParameterizedStringField(&configPage.masterConfig.ReconnectDelay) configPage.fallbackItems = createParameterizedFormItems(configPage.masterConfig.FallbackNormal.GetParameters(), configPage.layout.descriptionBox) + configPage.fallbackItems = createParameterizedFormItems(configPage.masterConfig.FallbackNormal.GetParameters(), configPage.layout.descriptionBox) // Map the parameters to the form items in the layout configPage.layout.mapParameterizedFormItems(configPage.useFallbackBox, configPage.reconnectDelay) configPage.layout.mapParameterizedFormItems(configPage.fallbackItems...) + configPage.prioritizeValidation = createParameterizedCheckbox(&configPage.masterConfig.PrioritizeValidation) // Set up the setting callbacks configPage.useFallbackBox.item.(*tview.Checkbox).SetChangedFunc(func(checked bool) { @@ -102,6 +105,7 @@ func (configPage *NativeFallbackConfigPage) handleUseFallbackChanged() { } configPage.layout.form.AddFormItem(configPage.reconnectDelay.item) configPage.layout.addFormItems(configPage.fallbackItems) + configPage.layout.form.AddFormItem(configPage.prioritizeValidation.item) configPage.layout.refresh() } diff --git a/shared/services/bc-manager.go b/shared/services/bc-manager.go index 74cf64803..c757f3578 100644 --- a/shared/services/bc-manager.go +++ b/shared/services/bc-manager.go @@ -23,6 +23,7 @@ type BeaconClientManager struct { primaryReady bool fallbackReady bool ignoreSyncCheck bool + preferFallback bool } // This is a signature for a wrapped Beacon client function that only returns an error @@ -36,6 +37,7 @@ type bcFunction2 func(beacon.Client) (interface{}, interface{}, error) // Creates a new BeaconClientManager instance based on the Rocket Pool config func NewBeaconClientManager(cfg *config.RocketPoolConfig) (*BeaconClientManager, error) { + var preferFallback bool // Primary CC var primaryProvider string @@ -62,6 +64,7 @@ func NewBeaconClientManager(cfg *config.RocketPoolConfig) (*BeaconClientManager, if cfg.UseFallbackClients.Value == true { if cfg.IsNativeMode { fallbackProvider = cfg.FallbackNormal.CcHttpUrl.Value.(string) + preferFallback = cfg.PrioritizeValidation.Value.(bool) } else { switch selectedCC { case cfgtypes.ConsensusClient_Prysm: @@ -69,6 +72,7 @@ func NewBeaconClientManager(cfg *config.RocketPoolConfig) (*BeaconClientManager, default: fallbackProvider = cfg.FallbackNormal.CcHttpUrl.Value.(string) } + preferFallback = cfg.PrioritizeValidation.Value.(bool) } } @@ -80,11 +84,12 @@ func NewBeaconClientManager(cfg *config.RocketPoolConfig) (*BeaconClientManager, } return &BeaconClientManager{ - primaryBc: primaryBc, - fallbackBc: fallbackBc, - logger: log.NewColorLogger(color.FgHiBlue), - primaryReady: true, - fallbackReady: fallbackBc != nil, + primaryBc: primaryBc, + fallbackBc: fallbackBc, + logger: log.NewColorLogger(color.FgHiBlue), + primaryReady: true, + fallbackReady: fallbackBc != nil, + preferFallback: preferFallback, }, nil } @@ -403,42 +408,59 @@ func (m *BeaconClientManager) runFunction0(function bcFunction0) error { return fmt.Errorf("no Beacon clients were ready") } -// Attempts to run a function progressively through each client until one succeeds or they all fail. +// Attempts to run a function progressively through each client in the preferred order until one succeeds or they all fail. func (m *BeaconClientManager) runFunction1(function bcFunction1) (interface{}, error) { - - // Check if we can use the primary - if m.primaryReady { - // Try to run the function on the primary - result, err := function(m.primaryBc) - if err != nil { - if m.isDisconnected(err) { - // If it's disconnected, log it and try the fallback - m.logger.Printlnf("WARNING: Primary Beacon client disconnected (%s), using fallback...", err.Error()) - m.primaryReady = false - return m.runFunction1(function) - } - // If it's a different error, just return it - return nil, err - } - // If there's no error, return the result - return result, nil + const ( + primary = iota + fallback = iota + ) + + var order []int + if !m.preferFallback { + order = []int{primary, fallback} + } else { + order = []int{fallback, primary} } - - if m.fallbackReady { - // Try to run the function on the fallback - result, err := function(m.fallbackBc) - if err != nil { - if m.isDisconnected(err) { - // If it's disconnected, log it and try the fallback - m.logger.Printlnf("WARNING: Fallback Beacon client disconnected (%s)", err.Error()) - m.fallbackReady = false - return nil, fmt.Errorf("all Beacon clients failed") + for _, client := range order { + switch client { + case primary: + + // Check if we can use the primary + if m.primaryReady { + // Try to run the function on the primary + result, err := function(m.primaryBc) + if err != nil { + if m.isDisconnected(err) { + // If it's disconnected, log it and try the fallback + m.logger.Printlnf("WARNING: Primary Beacon client disconnected (%s), using fallback...", err.Error()) + m.primaryReady = false + return m.runFunction1(function) + } + // If it's a different error, just return it + return nil, err + } + // If there's no error, return the result + return result, nil + } + case fallback: + + if m.fallbackReady { + // Try to run the function on the fallback + result, err := function(m.fallbackBc) + if err != nil { + if m.isDisconnected(err) { + // If it's disconnected, log it and try the fallback + m.logger.Printlnf("WARNING: Fallback Beacon client disconnected (%s)", err.Error()) + m.fallbackReady = false + return m.runFunction1(function) + } + // If it's a different error, just return it + return nil, err + } + // If there's no error, return the result + return result, nil } - // If it's a different error, just return it - return nil, err } - // If there's no error, return the result - return result, nil } return nil, fmt.Errorf("no Beacon clients were ready") @@ -447,40 +469,57 @@ func (m *BeaconClientManager) runFunction1(function bcFunction1) (interface{}, e // Attempts to run a function progressively through each client until one succeeds or they all fail. func (m *BeaconClientManager) runFunction2(function bcFunction2) (interface{}, interface{}, error) { - - // Check if we can use the primary - if m.primaryReady { - // Try to run the function on the primary - result1, result2, err := function(m.primaryBc) - if err != nil { - if m.isDisconnected(err) { - // If it's disconnected, log it and try the fallback - m.logger.Printlnf("WARNING: Primary Beacon client disconnected (%s), using fallback...", err.Error()) - m.primaryReady = false - return m.runFunction2(function) - } - // If it's a different error, just return it - return nil, nil, err - } - // If there's no error, return the result - return result1, result2, nil + const ( + primary = iota + fallback = iota + ) + + var order []int + if !m.preferFallback { + order = []int{primary, fallback} + } else { + order = []int{fallback, primary} } - - if m.fallbackReady { - // Try to run the function on the fallback - result1, result2, err := function(m.fallbackBc) - if err != nil { - if m.isDisconnected(err) { - // If it's disconnected, log it and try the fallback - m.logger.Printlnf("WARNING: Fallback Beacon client disconnected (%s)", err.Error()) - m.fallbackReady = false - return nil, nil, fmt.Errorf("all Beacon clients failed") + for _, client := range order { + switch client { + case primary: + + // Check if we can use the primary + if m.primaryReady { + // Try to run the function on the primary + result1, result2, err := function(m.primaryBc) + if err != nil { + if m.isDisconnected(err) { + // If it's disconnected, log it and try the fallback + m.logger.Printlnf("WARNING: Primary Beacon client disconnected (%s), using fallback...", err.Error()) + m.primaryReady = false + return m.runFunction2(function) + } + // If it's a different error, just return it + return nil, nil, err + } + // If there's no error, return the result + return result1, result2, nil + } + case fallback: + + if m.fallbackReady { + // Try to run the function on the fallback + result1, result2, err := function(m.fallbackBc) + if err != nil { + if m.isDisconnected(err) { + // If it's disconnected, log it and try the fallback + m.logger.Printlnf("WARNING: Fallback Beacon client disconnected (%s)", err.Error()) + m.fallbackReady = false + return m.runFunction2(function) + } + // If it's a different error, just return it + return nil, nil, err + } + // If there's no error, return the result + return result1, result2, nil } - // If it's a different error, just return it - return nil, nil, err } - // If there's no error, return the result - return result1, result2, nil } return nil, nil, fmt.Errorf("no Beacon clients were ready") diff --git a/shared/services/config/rocket-pool-config.go b/shared/services/config/rocket-pool-config.go index 9212e5e89..be7c9cad5 100644 --- a/shared/services/config/rocket-pool-config.go +++ b/shared/services/config/rocket-pool-config.go @@ -62,8 +62,9 @@ type RocketPoolConfig struct { ExecutionClient config.Parameter `yaml:"executionClient,omitempty"` // Fallback settings - UseFallbackClients config.Parameter `yaml:"useFallbackClients,omitempty"` - ReconnectDelay config.Parameter `yaml:"reconnectDelay,omitempty"` + UseFallbackClients config.Parameter `yaml:"useFallbackClients,omitempty"` + ReconnectDelay config.Parameter `yaml:"reconnectDelay,omitempty"` + PrioritizeValidation config.Parameter `yaml:"prioritizeValidation,omitempty"` // Consensus client settings ConsensusClientMode config.Parameter `yaml:"consensusClientMode,omitempty"` @@ -237,6 +238,17 @@ func NewRocketPoolConfig(rpDir string, isNativeMode bool) *RocketPoolConfig { OverwriteOnUpgrade: false, }, + PrioritizeValidation: config.Parameter{ + ID: "prioritizeValidation", + Name: "Prioritize Validation", + Description: "If enabled, metrics and wallet operations will go through the Fallback to prioritize resources on this node to the validator.", + Type: config.ParameterType_Bool, + Default: map[config.Network]interface{}{config.Network_All: true}, + EnvironmentVariables: []string{}, + CanBeBlank: false, + OverwriteOnUpgrade: false, + }, + ConsensusClientMode: config.Parameter{ ID: "consensusClientMode", Name: "Consensus Client Mode", @@ -525,6 +537,7 @@ func (cfg *RocketPoolConfig) GetParameters() []*config.Parameter { &cfg.ExecutionClient, &cfg.UseFallbackClients, &cfg.ReconnectDelay, + &cfg.PrioritizeValidation, &cfg.ConsensusClientMode, &cfg.ConsensusClient, &cfg.ExternalConsensusClient, diff --git a/shared/services/ec-manager.go b/shared/services/ec-manager.go index d417564f3..e4bae2504 100644 --- a/shared/services/ec-manager.go +++ b/shared/services/ec-manager.go @@ -29,6 +29,7 @@ type ExecutionClientManager struct { primaryReady bool fallbackReady bool ignoreSyncCheck bool + preferFallback bool } // This is a signature for a wrapped ethclient.Client function @@ -39,6 +40,7 @@ func NewExecutionClientManager(cfg *config.RocketPoolConfig) (*ExecutionClientMa var primaryEcUrl string var fallbackEcUrl string + var preferFallback bool // Get the primary EC url if cfg.IsNativeMode { @@ -53,6 +55,7 @@ func NewExecutionClientManager(cfg *config.RocketPoolConfig) (*ExecutionClientMa if cfg.UseFallbackClients.Value == true { if cfg.IsNativeMode { fallbackEcUrl = cfg.FallbackNormal.EcHttpUrl.Value.(string) + preferFallback = cfg.PrioritizeValidation.Value.(bool) } else { cc, _ := cfg.GetSelectedConsensusClient() switch cc { @@ -61,6 +64,7 @@ func NewExecutionClientManager(cfg *config.RocketPoolConfig) (*ExecutionClientMa default: fallbackEcUrl = cfg.FallbackNormal.EcHttpUrl.Value.(string) } + preferFallback = cfg.PrioritizeValidation.Value.(bool) } } @@ -78,13 +82,14 @@ func NewExecutionClientManager(cfg *config.RocketPoolConfig) (*ExecutionClientMa } return &ExecutionClientManager{ - primaryEcUrl: primaryEcUrl, - fallbackEcUrl: fallbackEcUrl, - primaryEc: primaryEc, - fallbackEc: fallbackEc, - logger: log.NewColorLogger(color.FgYellow), - primaryReady: true, - fallbackReady: fallbackEc != nil, + primaryEcUrl: primaryEcUrl, + fallbackEcUrl: fallbackEcUrl, + primaryEc: primaryEc, + fallbackEc: fallbackEc, + logger: log.NewColorLogger(color.FgYellow), + primaryReady: true, + fallbackReady: fallbackEc != nil, + preferFallback: preferFallback, }, nil } @@ -457,46 +462,62 @@ func checkEcStatus(client *ethclient.Client) api.ClientStatus { } -// Attempts to run a function progressively through each client until one succeeds or they all fail. +// Attempts to run a function progressively through each client in the preferred order until one succeeds or they all fail. func (p *ExecutionClientManager) runFunction(function ecFunction) (interface{}, error) { - // Check if we can use the primary - if p.primaryReady { - // Try to run the function on the primary - result, err := function(p.primaryEc) - if err != nil { - if p.isDisconnected(err) { - // If it's disconnected, log it and try the fallback - p.logger.Printlnf("WARNING: Primary Execution client disconnected (%s), using fallback...", err.Error()) - p.primaryReady = false - return p.runFunction(function) - } - - // If it's a different error, just return it - return nil, err - } - - // If there's no error, return the result - return result, nil - } + const ( + primary = iota + fallback = iota + ) - if p.fallbackReady { - // Try to run the function on the fallback - result, err := function(p.fallbackEc) - if err != nil { - if p.isDisconnected(err) { - // If it's disconnected, log it and try the fallback - p.logger.Printlnf("WARNING: Fallback Execution client disconnected (%s)", err.Error()) - p.fallbackReady = false - return nil, fmt.Errorf("all Execution clients failed") + var order []int + if !p.preferFallback { + order = []int{primary, fallback} + } else { + order = []int{fallback, primary} + } + for _, client := range order { + switch client { + case primary: + // Check if we can use the primary + if p.primaryReady { + // Try to run the function on the primary + result, err := function(p.primaryEc) + if err != nil { + if p.isDisconnected(err) { + // If it's disconnected, log it and try the fallback + p.logger.Printlnf("WARNING: Primary Execution client disconnected (%s), using fallback...", err.Error()) + p.primaryReady = false + return p.runFunction(function) + } + + // If it's a different error, just return it + return nil, err + } + + // If there's no error, return the result + return result, nil + } + case fallback: + if p.fallbackReady { + // Try to run the function on the fallback + result, err := function(p.fallbackEc) + if err != nil { + if p.isDisconnected(err) { + // If it's disconnected, log it and try the fallback + p.logger.Printlnf("WARNING: Fallback Execution client disconnected (%s)", err.Error()) + p.fallbackReady = false + return p.runFunction(function) + } + + // If it's a different error, just return it + return nil, err + } + + // If there's no error, return the result + return result, nil } - - // If it's a different error, just return it - return nil, err } - - // If there's no error, return the result - return result, nil } return nil, fmt.Errorf("no Execution clients were ready") diff --git a/shared/services/rocketpool/client.go b/shared/services/rocketpool/client.go index 609ac7681..aac5b6fbd 100644 --- a/shared/services/rocketpool/client.go +++ b/shared/services/rocketpool/client.go @@ -117,6 +117,12 @@ func getClientStatusString(clientStatus api.ClientStatus) string { // Check the status of the Execution and Consensus client(s) and provision the API with them func checkClientStatus(rp *Client) (bool, error) { + // Get the config + cfg, _, err := rp.LoadConfig() + if err != nil { + return false, fmt.Errorf("error loading user settings: %w", err) + } + // Check if the primary clients are up, synced, and able to respond to requests - if not, forces the use of the fallbacks for this command response, err := rp.GetClientStatus() if err != nil { @@ -126,10 +132,36 @@ func checkClientStatus(rp *Client) (bool, error) { ecMgrStatus := response.EcManagerStatus bcMgrStatus := response.BcManagerStatus - // Primary EC and CC are good - if ecMgrStatus.PrimaryClientStatus.IsSynced && bcMgrStatus.PrimaryClientStatus.IsSynced { - rp.SetClientStatusFlags(true, false) - return true, nil + fallbackEnabled := ecMgrStatus.FallbackEnabled && bcMgrStatus.FallbackEnabled + if !fallbackEnabled { + if ecMgrStatus.PrimaryClientStatus.IsSynced && bcMgrStatus.PrimaryClientStatus.IsSynced { + rp.SetClientStatusFlags(true, false) + return true, nil + } + + // Get the status messages + primaryEcStatus := getClientStatusString(ecMgrStatus.PrimaryClientStatus) + primaryBcStatus := getClientStatusString(bcMgrStatus.PrimaryClientStatus) + + // Primary isn't ready and fallback isn't enabled + fmt.Printf("Error: primary client pair isn't ready and fallback clients aren't enabled.\n\tPrimary EC status: %s\n\tPrimary CC status: %s\n", primaryEcStatus, primaryBcStatus) + return false, nil + } + + // Fallbacks are enabled. Check whether they're preferred. + preferFallback := cfg.PrioritizeValidation.Value.(bool) + + // Preferred EC and CC are good + if !preferFallback { + if ecMgrStatus.PrimaryClientStatus.IsSynced && bcMgrStatus.PrimaryClientStatus.IsSynced { + rp.SetClientStatusFlags(true, false) + return true, nil + } + } else { + if ecMgrStatus.FallbackClientStatus.IsSynced && bcMgrStatus.FallbackClientStatus.IsSynced { + rp.SetClientStatusFlags(true, true) + return true, nil + } } // Get the status messages @@ -138,23 +170,25 @@ func checkClientStatus(rp *Client) (bool, error) { fallbackEcStatus := getClientStatusString(ecMgrStatus.FallbackClientStatus) fallbackBcStatus := getClientStatusString(bcMgrStatus.FallbackClientStatus) - // Check the fallbacks if enabled - if ecMgrStatus.FallbackEnabled && bcMgrStatus.FallbackEnabled { - - // Fallback EC and CC are good + // Check the non-preffered clients + if !preferFallback { + // Primary preferred but not synced, check the fallback if ecMgrStatus.FallbackClientStatus.IsSynced && bcMgrStatus.FallbackClientStatus.IsSynced { fmt.Printf("%sNOTE: primary clients are not ready, using fallback clients...\n\tPrimary EC status: %s\n\tPrimary CC status: %s%s\n\n", colorYellow, primaryEcStatus, primaryBcStatus, colorReset) rp.SetClientStatusFlags(true, true) return true, nil } - - // Both pairs aren't ready - fmt.Printf("Error: neither primary nor fallback client pairs are ready.\n\tPrimary EC status: %s\n\tFallback EC status: %s\n\tPrimary CC status: %s\n\tFallback CC status: %s\n", primaryEcStatus, fallbackEcStatus, primaryBcStatus, fallbackBcStatus) - return false, nil + } else { + // Fallback preferred but not synced, check the primary + if ecMgrStatus.PrimaryClientStatus.IsSynced && bcMgrStatus.PrimaryClientStatus.IsSynced { + fmt.Printf("%sNOTE: fallback clients are not ready, using primary clients despite prioritizing validation...\n\tFallback EC status: %s\n\tFallback CC status: %s%s\n\n", colorYellow, fallbackEcStatus, fallbackBcStatus, colorReset) + rp.SetClientStatusFlags(true, false) + return true, nil + } } - // Primary isn't ready and fallback isn't enabled - fmt.Printf("Error: primary client pair isn't ready and fallback clients aren't enabled.\n\tPrimary EC status: %s\n\tPrimary CC status: %s\n", primaryEcStatus, primaryBcStatus) + // Both pairs aren't ready + fmt.Printf("Error: neither primary nor fallback client pairs are ready.\n\tPrimary EC status: %s\n\tFallback EC status: %s\n\tPrimary CC status: %s\n\tFallback CC status: %s\n", primaryEcStatus, fallbackEcStatus, primaryBcStatus, fallbackBcStatus) return false, nil }