From 6fddd13cb2af17c9c4dc36a155fd32fa3c3a223b Mon Sep 17 00:00:00 2001 From: Sammy Rosso <15244892+saolyn@users.noreply.github.com> Date: Tue, 28 May 2024 18:43:06 -0700 Subject: [PATCH] Multiple BN HTTP resolver (#13433) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * http resolver * Redo * Revert "Redo" This reverts commit 5437c44ac26e48c041f5c16f399446c5f72c31a2. * Revert "http resolver" This reverts commit 206207b530c30aca13d705db259c6cf738cae533. * Add host change to ValidatorClient + Validator * Update mockgen * Tidy * Add mock validator * Update gomock * Gaz * Solve interface issues * Fix host * Fix test * Add tests * Add endpoint change log * Fix log * Gen mock * Fix test * Fix deepsource * Lint + deepsource * Move to healthCheckRoutine * Fix build errors * Switch host to string * Forgot a couple * Radek' review * Add PushProposerSettings to goroutine * Radek' review * James' review + test fix * Radek' suggestion Co-authored-by: Radosław Kapka * Check if new node is healthy * Fix linter errors * Add host switch logic to ChangeHost * Lint + comment * Fix messy merge * rename ChangeHost to SetHost * improve log * remove log * switch one node * rename param --------- Co-authored-by: Radosław Kapka Co-authored-by: rkapka --- testing/mock/node_service_mock.go | 20 ++++++++ testing/validator-mock/node_client_mock.go | 26 ++++------ .../validator-mock/validator_client_mock.go | 26 ++++++++++ validator/accounts/cli_manager.go | 5 +- validator/accounts/testing/mock.go | 8 +++ .../beacon-api/beacon_api_validator_client.go | 8 +++ .../beacon_api_validator_client_test.go | 29 +++++++++++ .../client/beacon-api/json_rest_handler.go | 17 ++++--- .../beacon-api/mock/json_rest_handler_mock.go | 50 +++++++++++++++---- .../client/grpc-api/grpc_validator_client.go | 13 ++++- validator/client/iface/validator.go | 4 +- validator/client/iface/validator_client.go | 2 + validator/client/runner.go | 23 +++++++++ validator/client/service.go | 11 +++- validator/client/testutil/mock_validator.go | 8 +++ validator/client/validator.go | 17 +++++++ validator/client/validator_test.go | 32 ++++++++++++ validator/rpc/beacon.go | 5 +- 18 files changed, 267 insertions(+), 37 deletions(-) diff --git a/testing/mock/node_service_mock.go b/testing/mock/node_service_mock.go index 57a0cde054f6..72f268f582b4 100644 --- a/testing/mock/node_service_mock.go +++ b/testing/mock/node_service_mock.go @@ -82,6 +82,26 @@ func (mr *MockNodeClientMockRecorder) GetGenesis(arg0, arg1 any, arg2 ...any) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGenesis", reflect.TypeOf((*MockNodeClient)(nil).GetGenesis), varargs...) } +// GetHealth mocks base method. +func (m *MockNodeClient) GetHealth(arg0 context.Context, arg1 *eth.HealthRequest, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetHealth", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHealth indicates an expected call of GetHealth. +func (mr *MockNodeClientMockRecorder) GetHealth(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHealth", reflect.TypeOf((*MockNodeClient)(nil).GetHealth), varargs...) +} + // GetHost mocks base method. func (m *MockNodeClient) GetHost(arg0 context.Context, arg1 *emptypb.Empty, arg2 ...grpc.CallOption) (*eth.HostData, error) { m.ctrl.T.Helper() diff --git a/testing/validator-mock/node_client_mock.go b/testing/validator-mock/node_client_mock.go index 337b8e22465e..fa8eb1b3d9fb 100644 --- a/testing/validator-mock/node_client_mock.go +++ b/testing/validator-mock/node_client_mock.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - "github.com/prysmaticlabs/prysm/v5/api/client/beacon" + beacon "github.com/prysmaticlabs/prysm/v5/api/client/beacon" eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" gomock "go.uber.org/mock/gomock" emptypb "google.golang.org/protobuf/types/known/emptypb" @@ -21,9 +21,8 @@ import ( // MockNodeClient is a mock of NodeClient interface. type MockNodeClient struct { - ctrl *gomock.Controller - recorder *MockNodeClientMockRecorder - healthTracker *beacon.NodeHealthTracker + ctrl *gomock.Controller + recorder *MockNodeClientMockRecorder } // MockNodeClientMockRecorder is the mock recorder for MockNodeClient. @@ -35,7 +34,6 @@ type MockNodeClientMockRecorder struct { func NewMockNodeClient(ctrl *gomock.Controller) *MockNodeClient { mock := &MockNodeClient{ctrl: ctrl} mock.recorder = &MockNodeClientMockRecorder{mock} - mock.healthTracker = beacon.NewNodeHealthTracker(mock) return mock } @@ -89,18 +87,18 @@ func (mr *MockNodeClientMockRecorder) GetVersion(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVersion", reflect.TypeOf((*MockNodeClient)(nil).GetVersion), arg0, arg1) } -// IsHealthy mocks base method. -func (m *MockNodeClient) IsHealthy(arg0 context.Context) bool { +// HealthTracker mocks base method. +func (m *MockNodeClient) HealthTracker() *beacon.NodeHealthTracker { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsHealthy", arg0) - ret0, _ := ret[0].(bool) + ret := m.ctrl.Call(m, "HealthTracker") + ret0, _ := ret[0].(*beacon.NodeHealthTracker) return ret0 } -// IsHealthy indicates an expected call of IsHealthy. -func (mr *MockNodeClientMockRecorder) IsHealthy(arg0 any) *gomock.Call { +// HealthTracker indicates an expected call of HealthTracker. +func (mr *MockNodeClientMockRecorder) HealthTracker() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsHealthy", reflect.TypeOf((*MockNodeClient)(nil).IsHealthy), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthTracker", reflect.TypeOf((*MockNodeClient)(nil).HealthTracker)) } // ListPeers mocks base method. @@ -117,7 +115,3 @@ func (mr *MockNodeClientMockRecorder) ListPeers(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPeers", reflect.TypeOf((*MockNodeClient)(nil).ListPeers), arg0, arg1) } - -func (m *MockNodeClient) HealthTracker() *beacon.NodeHealthTracker { - return m.healthTracker -} diff --git a/testing/validator-mock/validator_client_mock.go b/testing/validator-mock/validator_client_mock.go index 7af1aee9a0de..bcb815d331d6 100644 --- a/testing/validator-mock/validator_client_mock.go +++ b/testing/validator-mock/validator_client_mock.go @@ -223,6 +223,20 @@ func (mr *MockValidatorClientMockRecorder) GetSyncSubcommitteeIndex(arg0, arg1 a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSyncSubcommitteeIndex", reflect.TypeOf((*MockValidatorClient)(nil).GetSyncSubcommitteeIndex), arg0, arg1) } +// Host mocks base method. +func (m *MockValidatorClient) Host() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Host") + ret0, _ := ret[0].(string) + return ret0 +} + +// Host indicates an expected call of Host. +func (mr *MockValidatorClientMockRecorder) Host() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Host", reflect.TypeOf((*MockValidatorClient)(nil).Host)) +} + // MultipleValidatorStatus mocks base method. func (m *MockValidatorClient) MultipleValidatorStatus(arg0 context.Context, arg1 *eth.MultipleValidatorStatusRequest) (*eth.MultipleValidatorStatusResponse, error) { m.ctrl.T.Helper() @@ -298,6 +312,18 @@ func (mr *MockValidatorClientMockRecorder) ProposeExit(arg0, arg1 any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProposeExit", reflect.TypeOf((*MockValidatorClient)(nil).ProposeExit), arg0, arg1) } +// SetHost mocks base method. +func (m *MockValidatorClient) SetHost(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetHost", arg0) +} + +// SetHost indicates an expected call of SetHost. +func (mr *MockValidatorClientMockRecorder) SetHost(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetHost", reflect.TypeOf((*MockValidatorClient)(nil).SetHost), arg0) +} + // StartEventStream mocks base method. func (m *MockValidatorClient) StartEventStream(arg0 context.Context, arg1 []string, arg2 chan<- *event.Event) { m.ctrl.T.Helper() diff --git a/validator/accounts/cli_manager.go b/validator/accounts/cli_manager.go index e897292c6b04..3f0116e3c65a 100644 --- a/validator/accounts/cli_manager.go +++ b/validator/accounts/cli_manager.go @@ -87,7 +87,10 @@ func (acm *CLIManager) prepareBeaconClients(ctx context.Context) (*iface.Validat acm.beaconApiTimeout, ) - restHandler := beaconApi.NewBeaconApiJsonRestHandler(http.Client{Timeout: acm.beaconApiTimeout}, acm.beaconApiEndpoint) + restHandler := beaconApi.NewBeaconApiJsonRestHandler( + http.Client{Timeout: acm.beaconApiTimeout}, + acm.beaconApiEndpoint, + ) validatorClient := validatorClientFactory.NewValidatorClient(conn, restHandler) nodeClient := nodeClientFactory.NewNodeClient(conn, restHandler) diff --git a/validator/accounts/testing/mock.go b/validator/accounts/testing/mock.go index 3d5fccd86f74..f8f53f78f240 100644 --- a/validator/accounts/testing/mock.go +++ b/validator/accounts/testing/mock.go @@ -249,3 +249,11 @@ func (*Validator) EventStreamIsRunning() bool { func (*Validator) HealthTracker() *beacon.NodeHealthTracker { panic("implement me") } + +func (*Validator) Host() string { + panic("implement me") +} + +func (*Validator) ChangeHost() { + panic("implement me") +} diff --git a/validator/client/beacon-api/beacon_api_validator_client.go b/validator/client/beacon-api/beacon_api_validator_client.go index 43eb64d3726d..667ac9a4710f 100644 --- a/validator/client/beacon-api/beacon_api_validator_client.go +++ b/validator/client/beacon-api/beacon_api_validator_client.go @@ -230,3 +230,11 @@ func wrapInMetrics[Resp any](action string, f func() (Resp, error)) (Resp, error } return resp, err } + +func (c *beaconApiValidatorClient) Host() string { + return c.jsonRestHandler.Host() +} + +func (c *beaconApiValidatorClient) SetHost(host string) { + c.jsonRestHandler.SetHost(host) +} diff --git a/validator/client/beacon-api/beacon_api_validator_client_test.go b/validator/client/beacon-api/beacon_api_validator_client_test.go index f416ae498fce..7e5872124a91 100644 --- a/validator/client/beacon-api/beacon_api_validator_client_test.go +++ b/validator/client/beacon-api/beacon_api_validator_client_test.go @@ -202,3 +202,32 @@ func TestBeaconApiValidatorClient_ProposeBeaconBlockError(t *testing.T) { assert.ErrorContains(t, expectedErr.Error(), err) assert.DeepEqual(t, expectedResp, resp) } + +func TestBeaconApiValidatorClient_Host(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + hosts := []string{"http://localhost:8080", "http://localhost:8081"} + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().SetHost( + hosts[0], + ).Times(1) + jsonRestHandler.EXPECT().Host().Return( + hosts[0], + ).Times(1) + + validatorClient := beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + validatorClient.SetHost(hosts[0]) + host := validatorClient.Host() + require.Equal(t, hosts[0], host) + + jsonRestHandler.EXPECT().SetHost( + hosts[1], + ).Times(1) + jsonRestHandler.EXPECT().Host().Return( + hosts[1], + ).Times(1) + validatorClient.SetHost(hosts[1]) + host = validatorClient.Host() + require.Equal(t, hosts[1], host) +} diff --git a/validator/client/beacon-api/json_rest_handler.go b/validator/client/beacon-api/json_rest_handler.go index 57f74aaf4d14..890a10e26be6 100644 --- a/validator/client/beacon-api/json_rest_handler.go +++ b/validator/client/beacon-api/json_rest_handler.go @@ -18,6 +18,7 @@ type JsonRestHandler interface { Post(ctx context.Context, endpoint string, headers map[string]string, data *bytes.Buffer, resp interface{}) error HttpClient() *http.Client Host() string + SetHost(host string) } type BeaconApiJsonRestHandler struct { @@ -33,19 +34,19 @@ func NewBeaconApiJsonRestHandler(client http.Client, host string) JsonRestHandle } } -// GetHttpClient returns the underlying HTTP client of the handler -func (c BeaconApiJsonRestHandler) HttpClient() *http.Client { +// HttpClient returns the underlying HTTP client of the handler +func (c *BeaconApiJsonRestHandler) HttpClient() *http.Client { return &c.client } -// GetHost returns the underlying HTTP host -func (c BeaconApiJsonRestHandler) Host() string { +// Host returns the underlying HTTP host +func (c *BeaconApiJsonRestHandler) Host() string { return c.host } // Get sends a GET request and decodes the response body as a JSON object into the passed in object. // If an HTTP error is returned, the body is decoded as a DefaultJsonError JSON object and returned as the first return value. -func (c BeaconApiJsonRestHandler) Get(ctx context.Context, endpoint string, resp interface{}) error { +func (c *BeaconApiJsonRestHandler) Get(ctx context.Context, endpoint string, resp interface{}) error { url := c.host + endpoint req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { @@ -67,7 +68,7 @@ func (c BeaconApiJsonRestHandler) Get(ctx context.Context, endpoint string, resp // Post sends a POST request and decodes the response body as a JSON object into the passed in object. // If an HTTP error is returned, the body is decoded as a DefaultJsonError JSON object and returned as the first return value. -func (c BeaconApiJsonRestHandler) Post( +func (c *BeaconApiJsonRestHandler) Post( ctx context.Context, apiEndpoint string, headers map[string]string, @@ -134,3 +135,7 @@ func decodeResp(httpResp *http.Response, resp interface{}) error { return nil } + +func (c *BeaconApiJsonRestHandler) SetHost(host string) { + c.host = host +} diff --git a/validator/client/beacon-api/mock/json_rest_handler_mock.go b/validator/client/beacon-api/mock/json_rest_handler_mock.go index b79df47828b9..1e2e98499cd3 100644 --- a/validator/client/beacon-api/mock/json_rest_handler_mock.go +++ b/validator/client/beacon-api/mock/json_rest_handler_mock.go @@ -12,7 +12,7 @@ package mock import ( bytes "bytes" context "context" - "net/http" + http "net/http" reflect "reflect" gomock "go.uber.org/mock/gomock" @@ -36,14 +36,6 @@ func NewMockJsonRestHandler(ctrl *gomock.Controller) *MockJsonRestHandler { return mock } -func (mr *MockJsonRestHandler) HttpClient() *http.Client { - return nil -} - -func (mr *MockJsonRestHandler) Host() string { - return "" -} - // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockJsonRestHandler) EXPECT() *MockJsonRestHandlerMockRecorder { return m.recorder @@ -63,6 +55,34 @@ func (mr *MockJsonRestHandlerMockRecorder) Get(ctx, endpoint, resp any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockJsonRestHandler)(nil).Get), ctx, endpoint, resp) } +// Host mocks base method. +func (m *MockJsonRestHandler) Host() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Host") + ret0, _ := ret[0].(string) + return ret0 +} + +// Host indicates an expected call of Host. +func (mr *MockJsonRestHandlerMockRecorder) Host() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Host", reflect.TypeOf((*MockJsonRestHandler)(nil).Host)) +} + +// HttpClient mocks base method. +func (m *MockJsonRestHandler) HttpClient() *http.Client { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HttpClient") + ret0, _ := ret[0].(*http.Client) + return ret0 +} + +// HttpClient indicates an expected call of HttpClient. +func (mr *MockJsonRestHandlerMockRecorder) HttpClient() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HttpClient", reflect.TypeOf((*MockJsonRestHandler)(nil).HttpClient)) +} + // Post mocks base method. func (m *MockJsonRestHandler) Post(ctx context.Context, endpoint string, headers map[string]string, data *bytes.Buffer, resp any) error { m.ctrl.T.Helper() @@ -76,3 +96,15 @@ func (mr *MockJsonRestHandlerMockRecorder) Post(ctx, endpoint, headers, data, re mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MockJsonRestHandler)(nil).Post), ctx, endpoint, headers, data, resp) } + +// SetHost mocks base method. +func (m *MockJsonRestHandler) SetHost(host string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetHost", host) +} + +// SetHost indicates an expected call of SetHost. +func (mr *MockJsonRestHandlerMockRecorder) SetHost(host any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetHost", reflect.TypeOf((*MockJsonRestHandler)(nil).SetHost), host) +} diff --git a/validator/client/grpc-api/grpc_validator_client.go b/validator/client/grpc-api/grpc_validator_client.go index cb87cc174756..11ca01b0c2ef 100644 --- a/validator/client/grpc-api/grpc_validator_client.go +++ b/validator/client/grpc-api/grpc_validator_client.go @@ -142,11 +142,11 @@ func (c *grpcValidatorClient) AggregatedSigAndAggregationBits( return c.beaconNodeValidatorClient.AggregatedSigAndAggregationBits(ctx, in) } -func (grpcValidatorClient) GetAggregatedSelections(context.Context, []iface.BeaconCommitteeSelection) ([]iface.BeaconCommitteeSelection, error) { +func (*grpcValidatorClient) GetAggregatedSelections(context.Context, []iface.BeaconCommitteeSelection) ([]iface.BeaconCommitteeSelection, error) { return nil, iface.ErrNotSupported } -func (grpcValidatorClient) GetAggregatedSyncSelections(context.Context, []iface.SyncCommitteeSelection) ([]iface.SyncCommitteeSelection, error) { +func (*grpcValidatorClient) GetAggregatedSyncSelections(context.Context, []iface.SyncCommitteeSelection) ([]iface.SyncCommitteeSelection, error) { return nil, iface.ErrNotSupported } @@ -245,3 +245,12 @@ func (c *grpcValidatorClient) StartEventStream(ctx context.Context, topics []str func (c *grpcValidatorClient) EventStreamIsRunning() bool { return c.isEventStreamRunning } + +func (*grpcValidatorClient) Host() string { + log.Warn(iface.ErrNotSupported) + return "" +} + +func (*grpcValidatorClient) SetHost(_ string) { + log.Warn(iface.ErrNotSupported) +} diff --git a/validator/client/iface/validator.go b/validator/client/iface/validator.go index 3163b8dd8a6a..5d3cb0110453 100644 --- a/validator/client/iface/validator.go +++ b/validator/client/iface/validator.go @@ -68,7 +68,9 @@ type Validator interface { SetGraffiti(ctx context.Context, pubKey [fieldparams.BLSPubkeyLength]byte, graffiti []byte) error DeleteGraffiti(ctx context.Context, pubKey [fieldparams.BLSPubkeyLength]byte) error HealthTracker() *beacon.NodeHealthTracker + Host() string + ChangeHost() } -// SigningFunc interface defines a type for the a function that signs a message +// SigningFunc interface defines a type for the function that signs a message type SigningFunc func(context.Context, *validatorpb.SignRequest) (bls.Signature, error) diff --git a/validator/client/iface/validator_client.go b/validator/client/iface/validator_client.go index 3f9cbe8fd22f..2e59a686f489 100644 --- a/validator/client/iface/validator_client.go +++ b/validator/client/iface/validator_client.go @@ -150,4 +150,6 @@ type ValidatorClient interface { EventStreamIsRunning() bool GetAggregatedSelections(ctx context.Context, selections []BeaconCommitteeSelection) ([]BeaconCommitteeSelection, error) GetAggregatedSyncSelections(ctx context.Context, selections []SyncCommitteeSelection) ([]SyncCommitteeSelection, error) + Host() string + SetHost(host string) } diff --git a/validator/client/runner.go b/validator/client/runner.go index 71a7a4729f35..83c9fec2489b 100644 --- a/validator/client/runner.go +++ b/validator/client/runner.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/api/client" "github.com/prysmaticlabs/prysm/v5/api/client/event" + "github.com/prysmaticlabs/prysm/v5/config/features" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" @@ -305,6 +306,28 @@ func runHealthCheckRoutine(ctx context.Context, v iface.Validator, eventsChan ch return } isHealthy := tracker.CheckHealth(ctx) + if !isHealthy && features.Get().EnableBeaconRESTApi { + v.ChangeHost() + if !tracker.CheckHealth(ctx) { + continue // Skip to the next ticker + } + + km, err := v.Keymanager() + if err != nil { + log.WithError(err).Error("Could not get keymanager") + return + } + slot, err := v.CanonicalHeadSlot(ctx) + if err != nil { + log.WithError(err).Error("Could not get canonical head slot") + return + } + deadline := time.Now().Add(5 * time.Minute) // Should consider changing to a constant + if err := v.PushProposerSettings(ctx, km, slot, deadline); err != nil { + log.WithError(err).Warn("Failed to update proposer settings") + } + } + // in case of node returning healthy but event stream died if isHealthy && !v.EventStreamIsRunning() { log.Info("Event stream reconnecting...") diff --git a/validator/client/service.go b/validator/client/service.go index 6e5a4cd6900a..a7bd4408c202 100644 --- a/validator/client/service.go +++ b/validator/client/service.go @@ -3,6 +3,7 @@ package client import ( "context" "net/http" + "strings" "time" "github.com/dgraph-io/ristretto" @@ -165,9 +166,15 @@ func (v *ValidatorService) Start() { return } + u := strings.ReplaceAll(v.conn.GetBeaconApiUrl(), " ", "") + hosts := strings.Split(u, ",") + if len(hosts) == 0 { + log.WithError(err).Error("No API hosts provided") + return + } restHandler := beaconApi.NewBeaconApiJsonRestHandler( http.Client{Timeout: v.conn.GetBeaconApiTimeout()}, - v.conn.GetBeaconApiUrl(), + hosts[0], ) validatorClient := validatorClientFactory.NewValidatorClient(v.conn, restHandler) @@ -184,6 +191,8 @@ func (v *ValidatorService) Start() { graffiti: v.graffiti, graffitiStruct: v.graffitiStruct, graffitiOrderedIndex: graffitiOrderedIndex, + beaconNodeHosts: hosts, + currentHostIndex: 0, validatorClient: validatorClient, chainClient: beaconChainClientFactory.NewChainClient(v.conn, restHandler), nodeClient: nodeClientFactory.NewNodeClient(v.conn, restHandler), diff --git a/validator/client/testutil/mock_validator.go b/validator/client/testutil/mock_validator.go index 15c4d17a537f..93f015bfd473 100644 --- a/validator/client/testutil/mock_validator.go +++ b/validator/client/testutil/mock_validator.go @@ -324,3 +324,11 @@ func (*FakeValidator) EventStreamIsRunning() bool { func (fv *FakeValidator) HealthTracker() *beacon.NodeHealthTracker { return fv.Tracker } + +func (*FakeValidator) Host() string { + return "127.0.0.1:0" +} + +func (fv *FakeValidator) ChangeHost() { + fv.Host() +} diff --git a/validator/client/validator.go b/validator/client/validator.go index 142ec9f32f1b..4bb0d1b4ab0a 100644 --- a/validator/client/validator.go +++ b/validator/client/validator.go @@ -84,6 +84,8 @@ type validator struct { graffiti []byte graffitiStruct *graffiti.Graffiti graffitiOrderedIndex uint64 + beaconNodeHosts []string + currentHostIndex uint64 validatorClient iface.ValidatorClient chainClient iface.ChainClient nodeClient iface.NodeClient @@ -1114,6 +1116,21 @@ func (v *validator) HealthTracker() *beacon.NodeHealthTracker { return v.nodeClient.HealthTracker() } +func (v *validator) Host() string { + return v.validatorClient.Host() +} + +func (v *validator) ChangeHost() { + if len(v.beaconNodeHosts) == 1 { + log.Infof("Beacon node at %s is not responding, no backup node configured", v.Host()) + return + } + next := (v.currentHostIndex + 1) % uint64(len(v.beaconNodeHosts)) + log.Infof("Beacon node at %s is not responding, switching to %s...", v.beaconNodeHosts[v.currentHostIndex], v.beaconNodeHosts[next]) + v.validatorClient.SetHost(v.beaconNodeHosts[next]) + v.currentHostIndex = next +} + func (v *validator) filterAndCacheActiveKeys(ctx context.Context, pubkeys [][fieldparams.BLSPubkeyLength]byte, slot primitives.Slot) ([][fieldparams.BLSPubkeyLength]byte, error) { filteredKeys := make([][fieldparams.BLSPubkeyLength]byte, 0) statusRequestKeys := make([][]byte, 0) diff --git a/validator/client/validator_test.go b/validator/client/validator_test.go index 23d26e18aff6..884606bd1d52 100644 --- a/validator/client/validator_test.go +++ b/validator/client/validator_test.go @@ -2590,3 +2590,35 @@ func TestValidator_buildSignedRegReqs_TimestampBeforeGenesis(t *testing.T) { actual := v.buildSignedRegReqs(ctx, pubkeys, signer) assert.Equal(t, 0, len(actual)) } + +func TestValidator_Host(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + client := validatormock.NewMockValidatorClient(ctrl) + v := validator{ + validatorClient: client, + } + + client.EXPECT().Host().Return("host").Times(1) + require.Equal(t, "host", v.Host()) +} + +func TestValidator_ChangeHost(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + client := validatormock.NewMockValidatorClient(ctrl) + v := validator{ + validatorClient: client, + beaconNodeHosts: []string{"http://localhost:8080", "http://localhost:8081"}, + currentHostIndex: 0, + } + + client.EXPECT().SetHost(v.beaconNodeHosts[1]) + client.EXPECT().SetHost(v.beaconNodeHosts[0]) + v.ChangeHost() + assert.Equal(t, uint64(1), v.currentHostIndex) + v.ChangeHost() + assert.Equal(t, uint64(0), v.currentHostIndex) +} diff --git a/validator/rpc/beacon.go b/validator/rpc/beacon.go index e86dd2a7ce6e..5ae024d3011a 100644 --- a/validator/rpc/beacon.go +++ b/validator/rpc/beacon.go @@ -54,7 +54,10 @@ func (s *Server) registerBeaconClient() error { s.beaconApiTimeout, ) - restHandler := beaconApi.NewBeaconApiJsonRestHandler(http.Client{Timeout: s.beaconApiTimeout}, s.beaconApiEndpoint) + restHandler := beaconApi.NewBeaconApiJsonRestHandler( + http.Client{Timeout: s.beaconApiTimeout}, + s.beaconApiEndpoint, + ) s.chainClient = beaconChainClientFactory.NewChainClient(conn, restHandler) s.nodeClient = nodeClientFactory.NewNodeClient(conn, restHandler)