diff --git a/api/client/beacon/BUILD.bazel b/api/client/beacon/BUILD.bazel index 60691a029..11e8ecb2e 100644 --- a/api/client/beacon/BUILD.bazel +++ b/api/client/beacon/BUILD.bazel @@ -6,12 +6,14 @@ go_library( "checkpoint.go", "client.go", "doc.go", + "health.go", "log.go", ], importpath = "github.com/prysmaticlabs/prysm/v5/api/client/beacon", visibility = ["//visibility:public"], deps = [ "//api/client:go_default_library", + "//api/client/beacon/iface:go_default_library", "//api/server:go_default_library", "//api/server/structs:go_default_library", "//beacon-chain/core/helpers:go_default_library", @@ -37,10 +39,12 @@ go_test( srcs = [ "checkpoint_test.go", "client_test.go", + "health_test.go", ], embed = [":go_default_library"], deps = [ "//api/client:go_default_library", + "//api/client/beacon/testing:go_default_library", "//beacon-chain/state:go_default_library", "//config/params:go_default_library", "//consensus-types/blocks:go_default_library", @@ -54,5 +58,6 @@ go_test( "//testing/util:go_default_library", "//time/slots:go_default_library", "@com_github_pkg_errors//:go_default_library", + "@org_uber_go_mock//gomock:go_default_library", ], ) diff --git a/api/client/beacon/health.go b/api/client/beacon/health.go new file mode 100644 index 000000000..ae3c60ef5 --- /dev/null +++ b/api/client/beacon/health.go @@ -0,0 +1,55 @@ +package beacon + +import ( + "context" + "sync" + + "github.com/prysmaticlabs/prysm/v5/api/client/beacon/iface" +) + +type NodeHealthTracker struct { + isHealthy *bool + healthChan chan bool + node iface.HealthNode + sync.RWMutex +} + +func NewNodeHealthTracker(node iface.HealthNode) *NodeHealthTracker { + return &NodeHealthTracker{ + node: node, + healthChan: make(chan bool, 1), + } +} + +// HealthUpdates provides a read-only channel for health updates. +func (n *NodeHealthTracker) HealthUpdates() <-chan bool { + return n.healthChan +} + +func (n *NodeHealthTracker) IsHealthy() bool { + n.RLock() + defer n.RUnlock() + if n.isHealthy == nil { + return false + } + return *n.isHealthy +} + +func (n *NodeHealthTracker) CheckHealth(ctx context.Context) bool { + n.RLock() + newStatus := n.node.IsHealthy(ctx) + if n.isHealthy == nil { + n.isHealthy = &newStatus + } + isStatusChanged := newStatus != *n.isHealthy + n.RUnlock() + + if isStatusChanged { + n.Lock() + // Double-check the condition to ensure it hasn't changed since the first check. + n.isHealthy = &newStatus + n.Unlock() // It's better to unlock as soon as the protected section is over. + n.healthChan <- newStatus + } + return newStatus +} diff --git a/api/client/beacon/health_test.go b/api/client/beacon/health_test.go new file mode 100644 index 000000000..3118a4427 --- /dev/null +++ b/api/client/beacon/health_test.go @@ -0,0 +1,118 @@ +package beacon + +import ( + "context" + "sync" + "testing" + + healthTesting "github.com/prysmaticlabs/prysm/v5/api/client/beacon/testing" + "go.uber.org/mock/gomock" +) + +func TestNodeHealth_IsHealthy(t *testing.T) { + tests := []struct { + name string + isHealthy bool + want bool + }{ + {"initially healthy", true, true}, + {"initially unhealthy", false, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := &NodeHealthTracker{ + isHealthy: &tt.isHealthy, + healthChan: make(chan bool, 1), + } + if got := n.IsHealthy(); got != tt.want { + t.Errorf("IsHealthy() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNodeHealth_UpdateNodeHealth(t *testing.T) { + tests := []struct { + name string + initial bool // Initial health status + newStatus bool // Status to update to + shouldSend bool // Should a message be sent through the channel + }{ + {"healthy to unhealthy", true, false, true}, + {"unhealthy to healthy", false, true, true}, + {"remain healthy", true, true, false}, + {"remain unhealthy", false, false, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := healthTesting.NewMockHealthClient(ctrl) + client.EXPECT().IsHealthy(gomock.Any()).Return(tt.newStatus) + n := &NodeHealthTracker{ + isHealthy: &tt.initial, + node: client, + healthChan: make(chan bool, 1), + } + + s := n.CheckHealth(context.Background()) + // Check if health status was updated + if s != tt.newStatus { + t.Errorf("UpdateNodeHealth() failed to update isHealthy from %v to %v", tt.initial, tt.newStatus) + } + + select { + case status := <-n.HealthUpdates(): + if !tt.shouldSend { + t.Errorf("UpdateNodeHealth() unexpectedly sent status %v to HealthCh", status) + } else if status != tt.newStatus { + t.Errorf("UpdateNodeHealth() sent wrong status %v, want %v", status, tt.newStatus) + } + default: + if tt.shouldSend { + t.Error("UpdateNodeHealth() did not send any status to HealthCh when expected") + } + } + }) + } +} + +func TestNodeHealth_Concurrency(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := healthTesting.NewMockHealthClient(ctrl) + n := NewNodeHealthTracker(client) + var wg sync.WaitGroup + + // Number of goroutines to spawn for both reading and writing + numGoroutines := 6 + + go func() { + for range n.HealthUpdates() { + // Consume values to avoid blocking on channel send. + } + }() + + wg.Add(numGoroutines * 2) // for readers and writers + + // Concurrently update health status + for i := 0; i < numGoroutines; i++ { + go func() { + defer wg.Done() + client.EXPECT().IsHealthy(gomock.Any()).Return(false) + n.CheckHealth(context.Background()) + client.EXPECT().IsHealthy(gomock.Any()).Return(true) + n.CheckHealth(context.Background()) + }() + } + + // Concurrently read health status + for i := 0; i < numGoroutines; i++ { + go func() { + defer wg.Done() + _ = n.IsHealthy() // Just read the value + }() + } + + wg.Wait() // Wait for all goroutines to finish +} diff --git a/api/client/beacon/iface/BUILD.bazel b/api/client/beacon/iface/BUILD.bazel new file mode 100644 index 000000000..c41efa3d0 --- /dev/null +++ b/api/client/beacon/iface/BUILD.bazel @@ -0,0 +1,8 @@ +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["health.go"], + importpath = "github.com/prysmaticlabs/prysm/v5/api/client/beacon/iface", + visibility = ["//visibility:public"], +) diff --git a/api/client/beacon/iface/health.go b/api/client/beacon/iface/health.go new file mode 100644 index 000000000..ac0ad4211 --- /dev/null +++ b/api/client/beacon/iface/health.go @@ -0,0 +1,13 @@ +package iface + +import "context" + +type HealthTracker interface { + HealthUpdates() <-chan bool + IsHealthy() bool + CheckHealth(ctx context.Context) bool +} + +type HealthNode interface { + IsHealthy(ctx context.Context) bool +} diff --git a/api/client/beacon/testing/BUILD.bazel b/api/client/beacon/testing/BUILD.bazel new file mode 100644 index 000000000..5f21aa5df --- /dev/null +++ b/api/client/beacon/testing/BUILD.bazel @@ -0,0 +1,12 @@ +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["mock.go"], + importpath = "github.com/prysmaticlabs/prysm/v5/api/client/beacon/testing", + visibility = ["//visibility:public"], + deps = [ + "//api/client/beacon/iface:go_default_library", + "@org_uber_go_mock//gomock:go_default_library", + ], +) diff --git a/api/client/beacon/testing/mock.go b/api/client/beacon/testing/mock.go new file mode 100644 index 000000000..25fe4bcf3 --- /dev/null +++ b/api/client/beacon/testing/mock.go @@ -0,0 +1,53 @@ +package testing + +import ( + "context" + "reflect" + + "github.com/prysmaticlabs/prysm/v5/api/client/beacon/iface" + "go.uber.org/mock/gomock" +) + +var ( + _ = iface.HealthNode(&MockHealthClient{}) +) + +// MockHealthClient is a mock of HealthClient interface. +type MockHealthClient struct { + ctrl *gomock.Controller + recorder *MockHealthClientMockRecorder +} + +// MockHealthClientMockRecorder is the mock recorder for MockHealthClient. +type MockHealthClientMockRecorder struct { + mock *MockHealthClient +} + +// IsHealthy mocks base method. +func (m *MockHealthClient) IsHealthy(arg0 context.Context) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsHealthy", arg0) + ret0, ok := ret[0].(bool) + if !ok { + return false + } + return ret0 +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHealthClient) EXPECT() *MockHealthClientMockRecorder { + return m.recorder +} + +// IsHealthy indicates an expected call of IsHealthy. +func (mr *MockHealthClientMockRecorder) IsHealthy(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsHealthy", reflect.TypeOf((*MockHealthClient)(nil).IsHealthy), arg0) +} + +// NewMockHealthClient creates a new mock instance. +func NewMockHealthClient(ctrl *gomock.Controller) *MockHealthClient { + mock := &MockHealthClient{ctrl: ctrl} + mock.recorder = &MockHealthClientMockRecorder{mock} + return mock +} diff --git a/api/client/errors.go b/api/client/errors.go index fb4ef3ae0..f3cf4f09a 100644 --- a/api/client/errors.go +++ b/api/client/errors.go @@ -21,6 +21,9 @@ var ErrNotFound = errors.Wrap(ErrNotOK, "recv 404 NotFound response from API") // ErrInvalidNodeVersion indicates that the /eth/v1/node/version API response format was not recognized. var ErrInvalidNodeVersion = errors.New("invalid node version response") +// ErrConnectionIssue represents a connection problem. +var ErrConnectionIssue = errors.New("could not connect") + // Non200Err is a function that parses an HTTP response to handle responses that are not 200 with a formatted error. func Non200Err(response *http.Response) error { bodyBytes, err := io.ReadAll(response.Body) diff --git a/api/client/event/BUILD.bazel b/api/client/event/BUILD.bazel new file mode 100644 index 000000000..75135de1a --- /dev/null +++ b/api/client/event/BUILD.bazel @@ -0,0 +1,24 @@ +load("@prysm//tools/go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["event_stream.go"], + importpath = "github.com/prysmaticlabs/prysm/v5/api/client/event", + visibility = ["//visibility:public"], + deps = [ + "//api:go_default_library", + "//api/client:go_default_library", + "@com_github_pkg_errors//:go_default_library", + "@com_github_sirupsen_logrus//:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["event_stream_test.go"], + embed = [":go_default_library"], + deps = [ + "//testing/require:go_default_library", + "@com_github_sirupsen_logrus//:go_default_library", + ], +) diff --git a/api/client/event/event_stream.go b/api/client/event/event_stream.go new file mode 100644 index 000000000..48a1951b2 --- /dev/null +++ b/api/client/event/event_stream.go @@ -0,0 +1,148 @@ +package event + +import ( + "bufio" + "context" + "net/http" + "net/url" + "strings" + + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/api" + "github.com/prysmaticlabs/prysm/v5/api/client" + log "github.com/sirupsen/logrus" +) + +const ( + EventHead = "head" + EventBlock = "block" + EventAttestation = "attestation" + EventVoluntaryExit = "voluntary_exit" + EventBlsToExecutionChange = "bls_to_execution_change" + EventProposerSlashing = "proposer_slashing" + EventAttesterSlashing = "attester_slashing" + EventFinalizedCheckpoint = "finalized_checkpoint" + EventChainReorg = "chain_reorg" + EventContributionAndProof = "contribution_and_proof" + EventLightClientFinalityUpdate = "light_client_finality_update" + EventLightClientOptimisticUpdate = "light_client_optimistic_update" + EventPayloadAttributes = "payload_attributes" + EventBlobSidecar = "blob_sidecar" + EventError = "error" + EventConnectionError = "connection_error" +) + +var ( + _ = EventStreamClient(&EventStream{}) +) + +var DefaultEventTopics = []string{EventHead} + +type EventStreamClient interface { + Subscribe(eventsChannel chan<- *Event) +} + +type Event struct { + EventType string + Data []byte +} + +// EventStream is responsible for subscribing to the Beacon API events endpoint +// and dispatching received events to subscribers. +type EventStream struct { + ctx context.Context + httpClient *http.Client + host string + topics []string +} + +func NewEventStream(ctx context.Context, httpClient *http.Client, host string, topics []string) (*EventStream, error) { + // Check if the host is a valid URL + _, err := url.ParseRequestURI(host) + if err != nil { + return nil, err + } + if len(topics) == 0 { + return nil, errors.New("no topics provided") + } + + return &EventStream{ + ctx: ctx, + httpClient: httpClient, + host: host, + topics: topics, + }, nil +} + +func (h *EventStream) Subscribe(eventsChannel chan<- *Event) { + allTopics := strings.Join(h.topics, ",") + log.WithField("topics", allTopics).Info("Listening to Beacon API events") + fullUrl := h.host + "/eth/v1/events?topics=" + allTopics + req, err := http.NewRequestWithContext(h.ctx, http.MethodGet, fullUrl, nil) + if err != nil { + eventsChannel <- &Event{ + EventType: EventConnectionError, + Data: []byte(errors.Wrap(err, "failed to create HTTP request").Error()), + } + } + req.Header.Set("Accept", api.EventStreamMediaType) + req.Header.Set("Connection", api.KeepAlive) + resp, err := h.httpClient.Do(req) + if err != nil { + eventsChannel <- &Event{ + EventType: EventConnectionError, + Data: []byte(errors.Wrap(err, client.ErrConnectionIssue.Error()).Error()), + } + } + + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + log.WithError(closeErr).Error("Failed to close events response body") + } + }() + // Create a new scanner to read lines from the response body + scanner := bufio.NewScanner(resp.Body) + + var eventType, data string // Variables to store event type and data + + // Iterate over lines of the event stream + for scanner.Scan() { + select { + case <-h.ctx.Done(): + log.Info("Context canceled, stopping event stream") + close(eventsChannel) + return + default: + line := scanner.Text() // TODO(13730): scanner does not handle /r and does not fully adhere to https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface + // Handle the event based on your specific format + if line == "" { + // Empty line indicates the end of an event + if eventType != "" && data != "" { + // Process the event when both eventType and data are set + eventsChannel <- &Event{EventType: eventType, Data: []byte(data)} + } + + // Reset eventType and data for the next event + eventType, data = "", "" + continue + } + et, ok := strings.CutPrefix(line, "event: ") + if ok { + // Extract event type from the "event" field + eventType = et + } + d, ok := strings.CutPrefix(line, "data: ") + if ok { + // Extract data from the "data" field + data = d + } + } + } + + if err := scanner.Err(); err != nil { + eventsChannel <- &Event{ + EventType: EventConnectionError, + Data: []byte(errors.Wrap(err, errors.Wrap(client.ErrConnectionIssue, "scanner failed").Error()).Error()), + } + } +} diff --git a/api/client/event/event_stream_test.go b/api/client/event/event_stream_test.go new file mode 100644 index 000000000..28b6a7f87 --- /dev/null +++ b/api/client/event/event_stream_test.go @@ -0,0 +1,80 @@ +package event + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/prysmaticlabs/prysm/v5/testing/require" + log "github.com/sirupsen/logrus" +) + +func TestNewEventStream(t *testing.T) { + validURL := "http://localhost:8080" + invalidURL := "://invalid" + topics := []string{"topic1", "topic2"} + + tests := []struct { + name string + host string + topics []string + wantErr bool + }{ + {"Valid input", validURL, topics, false}, + {"Invalid URL", invalidURL, topics, true}, + {"No topics", validURL, []string{}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewEventStream(context.Background(), &http.Client{}, tt.host, tt.topics) + if (err != nil) != tt.wantErr { + t.Errorf("NewEventStream() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestEventStream(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/eth/v1/events", func(w http.ResponseWriter, r *http.Request) { + flusher, ok := w.(http.Flusher) + require.Equal(t, true, ok) + for i := 1; i <= 2; i++ { + _, err := fmt.Fprintf(w, "event: head\ndata: data%d\n\n", i) + require.NoError(t, err) + flusher.Flush() // Trigger flush to simulate streaming data + time.Sleep(100 * time.Millisecond) // Simulate delay between events + } + }) + server := httptest.NewServer(mux) + defer server.Close() + + topics := []string{"head"} + eventsChannel := make(chan *Event, 1) + stream, err := NewEventStream(context.Background(), http.DefaultClient, server.URL, topics) + require.NoError(t, err) + go stream.Subscribe(eventsChannel) + + // Collect events + var events []*Event + + for len(events) != 2 { + select { + case event := <-eventsChannel: + log.Info(event) + events = append(events, event) + } + } + + // Assertions to verify the events content + expectedData := []string{"data1", "data2"} + for i, event := range events { + if string(event.Data) != expectedData[i] { + t.Errorf("Expected event data %q, got %q", expectedData[i], string(event.Data)) + } + } +} diff --git a/beacon-chain/rpc/prysm/v1alpha1/node/BUILD.bazel b/beacon-chain/rpc/prysm/v1alpha1/node/BUILD.bazel index 864d9a09c..428753ab1 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/node/BUILD.bazel +++ b/beacon-chain/rpc/prysm/v1alpha1/node/BUILD.bazel @@ -18,8 +18,10 @@ go_library( "@com_github_golang_protobuf//ptypes/timestamp", "@com_github_libp2p_go_libp2p//core/network:go_default_library", "@com_github_libp2p_go_libp2p//core/peer:go_default_library", + "@io_opencensus_go//trace:go_default_library", "@org_golang_google_grpc//:go_default_library", "@org_golang_google_grpc//codes:go_default_library", + "@org_golang_google_grpc//metadata:go_default_library", "@org_golang_google_grpc//status:go_default_library", "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", ], @@ -45,7 +47,9 @@ go_test( "@com_github_ethereum_go_ethereum//common:go_default_library", "@com_github_ethereum_go_ethereum//crypto:go_default_library", "@com_github_ethereum_go_ethereum//p2p/enode:go_default_library", + "@com_github_grpc_ecosystem_grpc_gateway_v2//runtime:go_default_library", "@org_golang_google_grpc//:go_default_library", + "@org_golang_google_grpc//metadata:go_default_library", "@org_golang_google_grpc//reflection:go_default_library", "@org_golang_google_protobuf//types/known/emptypb:go_default_library", "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", diff --git a/beacon-chain/rpc/prysm/v1alpha1/node/server.go b/beacon-chain/rpc/prysm/v1alpha1/node/server.go index 1ca1d99dc..67c3b863f 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/node/server.go +++ b/beacon-chain/rpc/prysm/v1alpha1/node/server.go @@ -6,7 +6,9 @@ package node import ( "context" "fmt" + "net/http" "sort" + "strconv" "time" "github.com/golang/protobuf/ptypes/empty" @@ -21,8 +23,10 @@ import ( "github.com/prysmaticlabs/prysm/v5/io/logs" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/runtime/version" + "go.opencensus.io/trace" "google.golang.org/grpc" "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -45,6 +49,35 @@ type Server struct { BeaconMonitoringPort int } +// GetHealth checks the health of the node +func (ns *Server) GetHealth(ctx context.Context, request *ethpb.HealthRequest) (*empty.Empty, error) { + ctx, span := trace.StartSpan(ctx, "node.GetHealth") + defer span.End() + + // Set a timeout for the health check operation + timeoutDuration := 10 * time.Second + ctx, cancel := context.WithTimeout(ctx, timeoutDuration) + defer cancel() // Important to avoid a context leak + + if ns.SyncChecker.Synced() { + return &empty.Empty{}, nil + } + if ns.SyncChecker.Syncing() || ns.SyncChecker.Initialized() { + if request.SyncingStatus != 0 { + // override the 200 success with the provided request status + if err := grpc.SetHeader(ctx, metadata.Pairs("x-http-code", strconv.FormatUint(request.SyncingStatus, 10))); err != nil { + return &empty.Empty{}, status.Errorf(codes.Internal, "Could not set custom success code header: %v", err) + } + return &empty.Empty{}, nil + } + if err := grpc.SetHeader(ctx, metadata.Pairs("x-http-code", strconv.FormatUint(http.StatusPartialContent, 10))); err != nil { + return &empty.Empty{}, status.Errorf(codes.Internal, "Could not set custom success code header: %v", err) + } + return &empty.Empty{}, nil + } + return &empty.Empty{}, status.Errorf(codes.Unavailable, "service unavailable") +} + // GetSyncStatus checks the current network sync status of the node. func (ns *Server) GetSyncStatus(_ context.Context, _ *empty.Empty) (*ethpb.SyncStatus, error) { return ðpb.SyncStatus{ diff --git a/beacon-chain/rpc/prysm/v1alpha1/node/server_test.go b/beacon-chain/rpc/prysm/v1alpha1/node/server_test.go index 0ee4f64f0..1d99a2e7b 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/node/server_test.go +++ b/beacon-chain/rpc/prysm/v1alpha1/node/server_test.go @@ -3,12 +3,14 @@ package node import ( "context" "errors" + "fmt" "testing" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" mock "github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain/testing" dbutil "github.com/prysmaticlabs/prysm/v5/beacon-chain/db/testing" "github.com/prysmaticlabs/prysm/v5/beacon-chain/p2p" @@ -22,6 +24,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/testing/require" "github.com/prysmaticlabs/prysm/v5/testing/util" "google.golang.org/grpc" + "google.golang.org/grpc/metadata" "google.golang.org/grpc/reflection" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" @@ -170,3 +173,53 @@ func TestNodeServer_GetETH1ConnectionStatus(t *testing.T) { assert.Equal(t, ep, res.CurrentAddress) assert.Equal(t, errStr, res.CurrentConnectionError) } + +func TestNodeServer_GetHealth(t *testing.T) { + tests := []struct { + name string + input *mockSync.Sync + customStatus uint64 + wantedErr string + }{ + { + name: "happy path", + input: &mockSync.Sync{IsSyncing: false, IsSynced: true}, + }, + { + name: "syncing", + input: &mockSync.Sync{IsSyncing: false}, + wantedErr: "service unavailable", + }, + { + name: "custom sync status", + input: &mockSync.Sync{IsSyncing: true}, + customStatus: 206, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := grpc.NewServer() + ns := &Server{ + SyncChecker: tt.input, + } + ethpb.RegisterNodeServer(server, ns) + reflection.Register(server) + ctx := grpc.NewContextWithServerTransportStream(context.Background(), &runtime.ServerTransportStream{}) + _, err := ns.GetHealth(ctx, ðpb.HealthRequest{SyncingStatus: tt.customStatus}) + if tt.wantedErr == "" { + require.NoError(t, err) + return + } + if tt.customStatus != 0 { + // Assuming the call was successful, now extract the headers + headers, _ := metadata.FromIncomingContext(ctx) + // Check for the specific header + values, ok := headers["x-http-code"] + require.Equal(t, true, ok && len(values) > 0) + require.Equal(t, fmt.Sprintf("%d", tt.customStatus), values[0]) + + } + require.ErrorContains(t, tt.wantedErr, err) + }) + } +} diff --git a/proto/prysm/v1alpha1/node.pb.go b/proto/prysm/v1alpha1/node.pb.go index 1ca2852b2..ac175b832 100755 --- a/proto/prysm/v1alpha1/node.pb.go +++ b/proto/prysm/v1alpha1/node.pb.go @@ -130,6 +130,53 @@ func (ConnectionState) EnumDescriptor() ([]byte, []int) { return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{1} } +type HealthRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SyncingStatus uint64 `protobuf:"varint,1,opt,name=syncing_status,json=syncingStatus,proto3" json:"syncing_status,omitempty"` +} + +func (x *HealthRequest) Reset() { + *x = HealthRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HealthRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthRequest) ProtoMessage() {} + +func (x *HealthRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthRequest.ProtoReflect.Descriptor instead. +func (*HealthRequest) Descriptor() ([]byte, []int) { + return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{0} +} + +func (x *HealthRequest) GetSyncingStatus() uint64 { + if x != nil { + return x.SyncingStatus + } + return 0 +} + type SyncStatus struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -141,7 +188,7 @@ type SyncStatus struct { func (x *SyncStatus) Reset() { *x = SyncStatus{} if protoimpl.UnsafeEnabled { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[0] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -154,7 +201,7 @@ func (x *SyncStatus) String() string { func (*SyncStatus) ProtoMessage() {} func (x *SyncStatus) ProtoReflect() protoreflect.Message { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[0] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -167,7 +214,7 @@ func (x *SyncStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use SyncStatus.ProtoReflect.Descriptor instead. func (*SyncStatus) Descriptor() ([]byte, []int) { - return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{0} + return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{1} } func (x *SyncStatus) GetSyncing() bool { @@ -190,7 +237,7 @@ type Genesis struct { func (x *Genesis) Reset() { *x = Genesis{} if protoimpl.UnsafeEnabled { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[1] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -203,7 +250,7 @@ func (x *Genesis) String() string { func (*Genesis) ProtoMessage() {} func (x *Genesis) ProtoReflect() protoreflect.Message { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[1] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -216,7 +263,7 @@ func (x *Genesis) ProtoReflect() protoreflect.Message { // Deprecated: Use Genesis.ProtoReflect.Descriptor instead. func (*Genesis) Descriptor() ([]byte, []int) { - return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{1} + return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{2} } func (x *Genesis) GetGenesisTime() *timestamppb.Timestamp { @@ -252,7 +299,7 @@ type Version struct { func (x *Version) Reset() { *x = Version{} if protoimpl.UnsafeEnabled { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[2] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -265,7 +312,7 @@ func (x *Version) String() string { func (*Version) ProtoMessage() {} func (x *Version) ProtoReflect() protoreflect.Message { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[2] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -278,7 +325,7 @@ func (x *Version) ProtoReflect() protoreflect.Message { // Deprecated: Use Version.ProtoReflect.Descriptor instead. func (*Version) Descriptor() ([]byte, []int) { - return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{2} + return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{3} } func (x *Version) GetVersion() string { @@ -306,7 +353,7 @@ type ImplementedServices struct { func (x *ImplementedServices) Reset() { *x = ImplementedServices{} if protoimpl.UnsafeEnabled { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[3] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -319,7 +366,7 @@ func (x *ImplementedServices) String() string { func (*ImplementedServices) ProtoMessage() {} func (x *ImplementedServices) ProtoReflect() protoreflect.Message { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[3] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -332,7 +379,7 @@ func (x *ImplementedServices) ProtoReflect() protoreflect.Message { // Deprecated: Use ImplementedServices.ProtoReflect.Descriptor instead. func (*ImplementedServices) Descriptor() ([]byte, []int) { - return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{3} + return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{4} } func (x *ImplementedServices) GetServices() []string { @@ -353,7 +400,7 @@ type PeerRequest struct { func (x *PeerRequest) Reset() { *x = PeerRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[4] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -366,7 +413,7 @@ func (x *PeerRequest) String() string { func (*PeerRequest) ProtoMessage() {} func (x *PeerRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[4] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -379,7 +426,7 @@ func (x *PeerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerRequest.ProtoReflect.Descriptor instead. func (*PeerRequest) Descriptor() ([]byte, []int) { - return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{4} + return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{5} } func (x *PeerRequest) GetPeerId() string { @@ -400,7 +447,7 @@ type Peers struct { func (x *Peers) Reset() { *x = Peers{} if protoimpl.UnsafeEnabled { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[5] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -413,7 +460,7 @@ func (x *Peers) String() string { func (*Peers) ProtoMessage() {} func (x *Peers) ProtoReflect() protoreflect.Message { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[5] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -426,7 +473,7 @@ func (x *Peers) ProtoReflect() protoreflect.Message { // Deprecated: Use Peers.ProtoReflect.Descriptor instead. func (*Peers) Descriptor() ([]byte, []int) { - return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{5} + return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{6} } func (x *Peers) GetPeers() []*Peer { @@ -451,7 +498,7 @@ type Peer struct { func (x *Peer) Reset() { *x = Peer{} if protoimpl.UnsafeEnabled { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[6] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -464,7 +511,7 @@ func (x *Peer) String() string { func (*Peer) ProtoMessage() {} func (x *Peer) ProtoReflect() protoreflect.Message { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[6] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -477,7 +524,7 @@ func (x *Peer) ProtoReflect() protoreflect.Message { // Deprecated: Use Peer.ProtoReflect.Descriptor instead. func (*Peer) Descriptor() ([]byte, []int) { - return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{6} + return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{7} } func (x *Peer) GetAddress() string { @@ -528,7 +575,7 @@ type HostData struct { func (x *HostData) Reset() { *x = HostData{} if protoimpl.UnsafeEnabled { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[7] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -541,7 +588,7 @@ func (x *HostData) String() string { func (*HostData) ProtoMessage() {} func (x *HostData) ProtoReflect() protoreflect.Message { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[7] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -554,7 +601,7 @@ func (x *HostData) ProtoReflect() protoreflect.Message { // Deprecated: Use HostData.ProtoReflect.Descriptor instead. func (*HostData) Descriptor() ([]byte, []int) { - return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{7} + return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{8} } func (x *HostData) GetAddresses() []string { @@ -592,7 +639,7 @@ type ETH1ConnectionStatus struct { func (x *ETH1ConnectionStatus) Reset() { *x = ETH1ConnectionStatus{} if protoimpl.UnsafeEnabled { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[8] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -605,7 +652,7 @@ func (x *ETH1ConnectionStatus) String() string { func (*ETH1ConnectionStatus) ProtoMessage() {} func (x *ETH1ConnectionStatus) ProtoReflect() protoreflect.Message { - mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[8] + mi := &file_proto_prysm_v1alpha1_node_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -618,7 +665,7 @@ func (x *ETH1ConnectionStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use ETH1ConnectionStatus.ProtoReflect.Descriptor instead. func (*ETH1ConnectionStatus) Descriptor() ([]byte, []int) { - return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{8} + return file_proto_prysm_v1alpha1_node_proto_rawDescGZIP(), []int{9} } func (x *ETH1ConnectionStatus) GetCurrentAddress() string { @@ -663,143 +710,154 @@ var file_proto_prysm_v1alpha1_node_proto_rawDesc = []byte{ 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x65, 0x78, 0x74, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0x26, 0x0a, 0x0a, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x18, 0x0a, 0x07, 0x73, 0x79, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x07, 0x73, 0x79, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x22, 0xc2, 0x01, 0x0a, 0x07, 0x47, 0x65, - 0x6e, 0x65, 0x73, 0x69, 0x73, 0x12, 0x3d, 0x0a, 0x0c, 0x67, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, - 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x67, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, - 0x54, 0x69, 0x6d, 0x65, 0x12, 0x38, 0x0a, 0x18, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x5f, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x16, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x43, - 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3e, - 0x0a, 0x17, 0x67, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x6f, 0x72, 0x73, 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x42, - 0x06, 0x8a, 0xb5, 0x18, 0x02, 0x33, 0x32, 0x52, 0x15, 0x67, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x52, 0x6f, 0x6f, 0x74, 0x22, 0x3f, - 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, - 0x31, 0x0a, 0x13, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x65, 0x64, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x22, 0x26, 0x0a, 0x0b, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x22, 0x3a, 0x0a, 0x05, 0x50, 0x65, - 0x65, 0x72, 0x73, 0x12, 0x31, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, - 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x52, - 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x22, 0xe2, 0x01, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, - 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x42, 0x0a, 0x09, 0x64, 0x69, 0x72, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x65, - 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, - 0x70, 0x68, 0x61, 0x31, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x51, 0x0a, - 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, - 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, - 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, - 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x12, 0x17, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x72, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x72, 0x22, 0x53, 0x0a, 0x08, 0x48, - 0x6f, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x10, - 0x0a, 0x03, 0x65, 0x6e, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x72, - 0x22, 0xc4, 0x01, 0x0a, 0x14, 0x45, 0x54, 0x48, 0x31, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x75, 0x72, - 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x12, 0x38, 0x0a, 0x18, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1c, 0x0a, 0x09, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2a, 0x37, 0x0a, 0x0d, 0x50, 0x65, 0x65, 0x72, 0x44, - 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, - 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x42, 0x4f, 0x55, 0x4e, 0x44, - 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x4f, 0x55, 0x54, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x02, - 0x2a, 0x55, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x0c, 0x44, 0x49, 0x53, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, - 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x44, 0x49, 0x53, 0x43, 0x4f, 0x4e, 0x4e, - 0x45, 0x43, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4e, 0x4e, - 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x43, 0x4f, 0x4e, 0x4e, 0x45, - 0x43, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x32, 0x93, 0x07, 0x0a, 0x04, 0x4e, 0x6f, 0x64, 0x65, - 0x12, 0x6e, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x21, 0x2e, 0x65, 0x74, 0x68, 0x65, - 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, - 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x22, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x1c, 0x12, 0x1a, 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, - 0x68, 0x61, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x73, 0x79, 0x6e, 0x63, 0x69, 0x6e, 0x67, - 0x12, 0x68, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x47, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x12, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1e, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, - 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x47, - 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x22, 0x22, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x12, 0x1a, - 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6e, 0x6f, - 0x64, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x12, 0x68, 0x0a, 0x0a, 0x47, 0x65, - 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x1a, 0x1e, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, - 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x22, 0x22, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x12, 0x1a, 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x76, - 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x82, 0x01, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6d, 0x70, - 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x65, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, - 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x2a, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, - 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, - 0x2e, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x65, 0x64, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x22, 0x23, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x12, 0x1b, 0x2f, 0x65, + 0x6f, 0x22, 0x36, 0x0a, 0x0d, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x79, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x73, 0x79, 0x6e, 0x63, + 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x26, 0x0a, 0x0a, 0x53, 0x79, 0x6e, + 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x79, 0x6e, 0x63, 0x69, + 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x79, 0x6e, 0x63, 0x69, 0x6e, + 0x67, 0x22, 0xc2, 0x01, 0x0a, 0x07, 0x47, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x12, 0x3d, 0x0a, + 0x0c, 0x67, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0b, 0x67, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x38, 0x0a, 0x18, + 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, + 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x16, + 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x41, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3e, 0x0a, 0x17, 0x67, 0x65, 0x6e, 0x65, 0x73, 0x69, + 0x73, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x5f, 0x72, 0x6f, 0x6f, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x06, 0x8a, 0xb5, 0x18, 0x02, 0x33, 0x32, 0x52, + 0x15, 0x67, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, + 0x72, 0x73, 0x52, 0x6f, 0x6f, 0x74, 0x22, 0x3f, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x31, 0x0a, 0x13, 0x49, 0x6d, 0x70, 0x6c, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x65, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x1a, + 0x0a, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x22, 0x26, 0x0a, 0x0b, 0x50, 0x65, + 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x65, 0x65, + 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, + 0x49, 0x64, 0x22, 0x3a, 0x0a, 0x05, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x31, 0x0a, 0x05, 0x70, + 0x65, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x65, 0x74, 0x68, + 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, + 0x61, 0x31, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x22, 0xe2, + 0x01, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x12, 0x42, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, + 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x50, 0x65, 0x65, + 0x72, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x51, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x26, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, + 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, + 0x64, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x65, 0x6e, 0x72, 0x22, 0x53, 0x0a, 0x08, 0x48, 0x6f, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, + 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x17, 0x0a, + 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x72, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x72, 0x22, 0xc4, 0x01, 0x0a, 0x14, 0x45, 0x54, 0x48, + 0x31, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x75, 0x72, 0x72, + 0x65, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x38, 0x0a, 0x18, 0x63, 0x75, + 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x75, + 0x72, 0x72, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x63, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2a, + 0x37, 0x0a, 0x0d, 0x50, 0x65, 0x65, 0x72, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, + 0x07, 0x49, 0x4e, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x4f, 0x55, + 0x54, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x02, 0x2a, 0x55, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x0c, 0x44, + 0x49, 0x53, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x11, 0x0a, + 0x0d, 0x44, 0x49, 0x53, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x01, + 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, + 0x0e, 0x0a, 0x0a, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x32, + 0x81, 0x08, 0x0a, 0x04, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x6e, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x53, + 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x1a, 0x21, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, + 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x22, 0x22, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x12, 0x1a, 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, - 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x62, 0x0a, 0x07, 0x47, 0x65, 0x74, - 0x48, 0x6f, 0x73, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1f, 0x2e, 0x65, - 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, - 0x70, 0x68, 0x61, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x22, 0x1e, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x12, 0x16, 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x76, 0x31, 0x61, 0x6c, - 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70, 0x32, 0x70, 0x12, 0x6b, 0x0a, - 0x07, 0x47, 0x65, 0x74, 0x50, 0x65, 0x65, 0x72, 0x12, 0x22, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, + 0x2f, 0x73, 0x79, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x12, 0x68, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x47, + 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1e, + 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, + 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x22, 0x22, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x12, 0x1a, 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x76, 0x31, 0x61, + 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x73, + 0x69, 0x73, 0x12, 0x68, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1e, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, - 0x2e, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x65, - 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, - 0x70, 0x68, 0x61, 0x31, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x19, 0x12, 0x17, 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, - 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70, 0x65, 0x65, 0x72, 0x12, 0x63, 0x0a, 0x09, 0x4c, 0x69, - 0x73, 0x74, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, - 0x1c, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, - 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x73, 0x22, 0x20, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x12, 0x18, 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x76, 0x31, 0x61, 0x6c, - 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, - 0x8b, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x45, 0x54, 0x48, 0x31, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x1a, 0x2b, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, - 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45, 0x54, 0x48, 0x31, - 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x12, 0x23, 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x76, - 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x65, 0x74, 0x68, - 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x94, 0x01, - 0x0a, 0x19, 0x6f, 0x72, 0x67, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, - 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x42, 0x09, 0x4e, 0x6f, 0x64, - 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x61, 0x74, 0x69, 0x63, 0x6c, 0x61, - 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x35, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, - 0x3b, 0x65, 0x74, 0x68, 0xaa, 0x02, 0x15, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, - 0x45, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xca, 0x02, 0x15, 0x45, - 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x5c, 0x45, 0x74, 0x68, 0x5c, 0x76, 0x31, 0x61, 0x6c, - 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x22, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, + 0x12, 0x1a, 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, + 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x6c, 0x0a, 0x09, + 0x47, 0x65, 0x74, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x24, 0x2e, 0x65, 0x74, 0x68, 0x65, + 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, + 0x31, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x21, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, 0x12, + 0x19, 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6e, + 0x6f, 0x64, 0x65, 0x2f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x82, 0x01, 0x0a, 0x17, 0x4c, + 0x69, 0x73, 0x74, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x65, 0x64, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x2a, + 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, + 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x65, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x22, 0x23, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x1d, 0x12, 0x1b, 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, + 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, + 0x62, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x1f, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, + 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x44, + 0x61, 0x74, 0x61, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x12, 0x16, 0x2f, 0x65, 0x74, + 0x68, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, + 0x70, 0x32, 0x70, 0x12, 0x6b, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x50, 0x65, 0x65, 0x72, 0x12, 0x22, + 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, + 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, + 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x22, + 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x12, 0x17, 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x76, 0x31, + 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70, 0x65, 0x65, 0x72, + 0x12, 0x63, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, + 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x50, 0x65, + 0x65, 0x72, 0x73, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x12, 0x18, 0x2f, 0x65, 0x74, + 0x68, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, + 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x8b, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x45, 0x54, 0x48, + 0x31, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x2b, 0x2e, 0x65, 0x74, 0x68, 0x65, + 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, + 0x31, 0x2e, 0x45, 0x54, 0x48, 0x31, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x12, 0x23, + 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6e, 0x6f, + 0x64, 0x65, 0x2f, 0x65, 0x74, 0x68, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x42, 0x94, 0x01, 0x0a, 0x19, 0x6f, 0x72, 0x67, 0x2e, 0x65, 0x74, 0x68, 0x65, + 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, + 0x31, 0x42, 0x09, 0x4e, 0x6f, 0x64, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3a, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, + 0x61, 0x74, 0x69, 0x63, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, + 0x35, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x31, + 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x3b, 0x65, 0x74, 0x68, 0xaa, 0x02, 0x15, 0x45, 0x74, 0x68, + 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x45, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, + 0x61, 0x31, 0xca, 0x02, 0x15, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x5c, 0x45, 0x74, + 0x68, 0x5c, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -815,45 +873,48 @@ func file_proto_prysm_v1alpha1_node_proto_rawDescGZIP() []byte { } var file_proto_prysm_v1alpha1_node_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_proto_prysm_v1alpha1_node_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_proto_prysm_v1alpha1_node_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_proto_prysm_v1alpha1_node_proto_goTypes = []interface{}{ (PeerDirection)(0), // 0: ethereum.eth.v1alpha1.PeerDirection (ConnectionState)(0), // 1: ethereum.eth.v1alpha1.ConnectionState - (*SyncStatus)(nil), // 2: ethereum.eth.v1alpha1.SyncStatus - (*Genesis)(nil), // 3: ethereum.eth.v1alpha1.Genesis - (*Version)(nil), // 4: ethereum.eth.v1alpha1.Version - (*ImplementedServices)(nil), // 5: ethereum.eth.v1alpha1.ImplementedServices - (*PeerRequest)(nil), // 6: ethereum.eth.v1alpha1.PeerRequest - (*Peers)(nil), // 7: ethereum.eth.v1alpha1.Peers - (*Peer)(nil), // 8: ethereum.eth.v1alpha1.Peer - (*HostData)(nil), // 9: ethereum.eth.v1alpha1.HostData - (*ETH1ConnectionStatus)(nil), // 10: ethereum.eth.v1alpha1.ETH1ConnectionStatus - (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp - (*emptypb.Empty)(nil), // 12: google.protobuf.Empty + (*HealthRequest)(nil), // 2: ethereum.eth.v1alpha1.HealthRequest + (*SyncStatus)(nil), // 3: ethereum.eth.v1alpha1.SyncStatus + (*Genesis)(nil), // 4: ethereum.eth.v1alpha1.Genesis + (*Version)(nil), // 5: ethereum.eth.v1alpha1.Version + (*ImplementedServices)(nil), // 6: ethereum.eth.v1alpha1.ImplementedServices + (*PeerRequest)(nil), // 7: ethereum.eth.v1alpha1.PeerRequest + (*Peers)(nil), // 8: ethereum.eth.v1alpha1.Peers + (*Peer)(nil), // 9: ethereum.eth.v1alpha1.Peer + (*HostData)(nil), // 10: ethereum.eth.v1alpha1.HostData + (*ETH1ConnectionStatus)(nil), // 11: ethereum.eth.v1alpha1.ETH1ConnectionStatus + (*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 13: google.protobuf.Empty } var file_proto_prysm_v1alpha1_node_proto_depIdxs = []int32{ - 11, // 0: ethereum.eth.v1alpha1.Genesis.genesis_time:type_name -> google.protobuf.Timestamp - 8, // 1: ethereum.eth.v1alpha1.Peers.peers:type_name -> ethereum.eth.v1alpha1.Peer + 12, // 0: ethereum.eth.v1alpha1.Genesis.genesis_time:type_name -> google.protobuf.Timestamp + 9, // 1: ethereum.eth.v1alpha1.Peers.peers:type_name -> ethereum.eth.v1alpha1.Peer 0, // 2: ethereum.eth.v1alpha1.Peer.direction:type_name -> ethereum.eth.v1alpha1.PeerDirection 1, // 3: ethereum.eth.v1alpha1.Peer.connection_state:type_name -> ethereum.eth.v1alpha1.ConnectionState - 12, // 4: ethereum.eth.v1alpha1.Node.GetSyncStatus:input_type -> google.protobuf.Empty - 12, // 5: ethereum.eth.v1alpha1.Node.GetGenesis:input_type -> google.protobuf.Empty - 12, // 6: ethereum.eth.v1alpha1.Node.GetVersion:input_type -> google.protobuf.Empty - 12, // 7: ethereum.eth.v1alpha1.Node.ListImplementedServices:input_type -> google.protobuf.Empty - 12, // 8: ethereum.eth.v1alpha1.Node.GetHost:input_type -> google.protobuf.Empty - 6, // 9: ethereum.eth.v1alpha1.Node.GetPeer:input_type -> ethereum.eth.v1alpha1.PeerRequest - 12, // 10: ethereum.eth.v1alpha1.Node.ListPeers:input_type -> google.protobuf.Empty - 12, // 11: ethereum.eth.v1alpha1.Node.GetETH1ConnectionStatus:input_type -> google.protobuf.Empty - 2, // 12: ethereum.eth.v1alpha1.Node.GetSyncStatus:output_type -> ethereum.eth.v1alpha1.SyncStatus - 3, // 13: ethereum.eth.v1alpha1.Node.GetGenesis:output_type -> ethereum.eth.v1alpha1.Genesis - 4, // 14: ethereum.eth.v1alpha1.Node.GetVersion:output_type -> ethereum.eth.v1alpha1.Version - 5, // 15: ethereum.eth.v1alpha1.Node.ListImplementedServices:output_type -> ethereum.eth.v1alpha1.ImplementedServices - 9, // 16: ethereum.eth.v1alpha1.Node.GetHost:output_type -> ethereum.eth.v1alpha1.HostData - 8, // 17: ethereum.eth.v1alpha1.Node.GetPeer:output_type -> ethereum.eth.v1alpha1.Peer - 7, // 18: ethereum.eth.v1alpha1.Node.ListPeers:output_type -> ethereum.eth.v1alpha1.Peers - 10, // 19: ethereum.eth.v1alpha1.Node.GetETH1ConnectionStatus:output_type -> ethereum.eth.v1alpha1.ETH1ConnectionStatus - 12, // [12:20] is the sub-list for method output_type - 4, // [4:12] is the sub-list for method input_type + 13, // 4: ethereum.eth.v1alpha1.Node.GetSyncStatus:input_type -> google.protobuf.Empty + 13, // 5: ethereum.eth.v1alpha1.Node.GetGenesis:input_type -> google.protobuf.Empty + 13, // 6: ethereum.eth.v1alpha1.Node.GetVersion:input_type -> google.protobuf.Empty + 2, // 7: ethereum.eth.v1alpha1.Node.GetHealth:input_type -> ethereum.eth.v1alpha1.HealthRequest + 13, // 8: ethereum.eth.v1alpha1.Node.ListImplementedServices:input_type -> google.protobuf.Empty + 13, // 9: ethereum.eth.v1alpha1.Node.GetHost:input_type -> google.protobuf.Empty + 7, // 10: ethereum.eth.v1alpha1.Node.GetPeer:input_type -> ethereum.eth.v1alpha1.PeerRequest + 13, // 11: ethereum.eth.v1alpha1.Node.ListPeers:input_type -> google.protobuf.Empty + 13, // 12: ethereum.eth.v1alpha1.Node.GetETH1ConnectionStatus:input_type -> google.protobuf.Empty + 3, // 13: ethereum.eth.v1alpha1.Node.GetSyncStatus:output_type -> ethereum.eth.v1alpha1.SyncStatus + 4, // 14: ethereum.eth.v1alpha1.Node.GetGenesis:output_type -> ethereum.eth.v1alpha1.Genesis + 5, // 15: ethereum.eth.v1alpha1.Node.GetVersion:output_type -> ethereum.eth.v1alpha1.Version + 13, // 16: ethereum.eth.v1alpha1.Node.GetHealth:output_type -> google.protobuf.Empty + 6, // 17: ethereum.eth.v1alpha1.Node.ListImplementedServices:output_type -> ethereum.eth.v1alpha1.ImplementedServices + 10, // 18: ethereum.eth.v1alpha1.Node.GetHost:output_type -> ethereum.eth.v1alpha1.HostData + 9, // 19: ethereum.eth.v1alpha1.Node.GetPeer:output_type -> ethereum.eth.v1alpha1.Peer + 8, // 20: ethereum.eth.v1alpha1.Node.ListPeers:output_type -> ethereum.eth.v1alpha1.Peers + 11, // 21: ethereum.eth.v1alpha1.Node.GetETH1ConnectionStatus:output_type -> ethereum.eth.v1alpha1.ETH1ConnectionStatus + 13, // [13:22] is the sub-list for method output_type + 4, // [4:13] is the sub-list for method input_type 4, // [4:4] is the sub-list for extension type_name 4, // [4:4] is the sub-list for extension extendee 0, // [0:4] is the sub-list for field type_name @@ -866,7 +927,7 @@ func file_proto_prysm_v1alpha1_node_proto_init() { } if !protoimpl.UnsafeEnabled { file_proto_prysm_v1alpha1_node_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SyncStatus); i { + switch v := v.(*HealthRequest); i { case 0: return &v.state case 1: @@ -878,7 +939,7 @@ func file_proto_prysm_v1alpha1_node_proto_init() { } } file_proto_prysm_v1alpha1_node_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Genesis); i { + switch v := v.(*SyncStatus); i { case 0: return &v.state case 1: @@ -890,7 +951,7 @@ func file_proto_prysm_v1alpha1_node_proto_init() { } } file_proto_prysm_v1alpha1_node_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Version); i { + switch v := v.(*Genesis); i { case 0: return &v.state case 1: @@ -902,7 +963,7 @@ func file_proto_prysm_v1alpha1_node_proto_init() { } } file_proto_prysm_v1alpha1_node_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ImplementedServices); i { + switch v := v.(*Version); i { case 0: return &v.state case 1: @@ -914,7 +975,7 @@ func file_proto_prysm_v1alpha1_node_proto_init() { } } file_proto_prysm_v1alpha1_node_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerRequest); i { + switch v := v.(*ImplementedServices); i { case 0: return &v.state case 1: @@ -926,7 +987,7 @@ func file_proto_prysm_v1alpha1_node_proto_init() { } } file_proto_prysm_v1alpha1_node_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Peers); i { + switch v := v.(*PeerRequest); i { case 0: return &v.state case 1: @@ -938,7 +999,7 @@ func file_proto_prysm_v1alpha1_node_proto_init() { } } file_proto_prysm_v1alpha1_node_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Peer); i { + switch v := v.(*Peers); i { case 0: return &v.state case 1: @@ -950,7 +1011,7 @@ func file_proto_prysm_v1alpha1_node_proto_init() { } } file_proto_prysm_v1alpha1_node_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*HostData); i { + switch v := v.(*Peer); i { case 0: return &v.state case 1: @@ -962,6 +1023,18 @@ func file_proto_prysm_v1alpha1_node_proto_init() { } } file_proto_prysm_v1alpha1_node_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HostData); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_prysm_v1alpha1_node_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ETH1ConnectionStatus); i { case 0: return &v.state @@ -980,7 +1053,7 @@ func file_proto_prysm_v1alpha1_node_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_prysm_v1alpha1_node_proto_rawDesc, NumEnums: 2, - NumMessages: 9, + NumMessages: 10, NumExtensions: 0, NumServices: 1, }, @@ -1010,6 +1083,7 @@ type NodeClient interface { GetSyncStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SyncStatus, error) GetGenesis(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Genesis, error) GetVersion(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Version, error) + GetHealth(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) ListImplementedServices(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ImplementedServices, error) GetHost(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*HostData, error) GetPeer(ctx context.Context, in *PeerRequest, opts ...grpc.CallOption) (*Peer, error) @@ -1052,6 +1126,15 @@ func (c *nodeClient) GetVersion(ctx context.Context, in *emptypb.Empty, opts ... return out, nil } +func (c *nodeClient) GetHealth(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/ethereum.eth.v1alpha1.Node/GetHealth", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *nodeClient) ListImplementedServices(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ImplementedServices, error) { out := new(ImplementedServices) err := c.cc.Invoke(ctx, "/ethereum.eth.v1alpha1.Node/ListImplementedServices", in, out, opts...) @@ -1102,6 +1185,7 @@ type NodeServer interface { GetSyncStatus(context.Context, *emptypb.Empty) (*SyncStatus, error) GetGenesis(context.Context, *emptypb.Empty) (*Genesis, error) GetVersion(context.Context, *emptypb.Empty) (*Version, error) + GetHealth(context.Context, *HealthRequest) (*emptypb.Empty, error) ListImplementedServices(context.Context, *emptypb.Empty) (*ImplementedServices, error) GetHost(context.Context, *emptypb.Empty) (*HostData, error) GetPeer(context.Context, *PeerRequest) (*Peer, error) @@ -1122,6 +1206,9 @@ func (*UnimplementedNodeServer) GetGenesis(context.Context, *emptypb.Empty) (*Ge func (*UnimplementedNodeServer) GetVersion(context.Context, *emptypb.Empty) (*Version, error) { return nil, status.Errorf(codes.Unimplemented, "method GetVersion not implemented") } +func (*UnimplementedNodeServer) GetHealth(context.Context, *HealthRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetHealth not implemented") +} func (*UnimplementedNodeServer) ListImplementedServices(context.Context, *emptypb.Empty) (*ImplementedServices, error) { return nil, status.Errorf(codes.Unimplemented, "method ListImplementedServices not implemented") } @@ -1196,6 +1283,24 @@ func _Node_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(int return interceptor(ctx, in, info, handler) } +func _Node_GetHealth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HealthRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NodeServer).GetHealth(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/ethereum.eth.v1alpha1.Node/GetHealth", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NodeServer).GetHealth(ctx, req.(*HealthRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Node_ListImplementedServices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { @@ -1302,6 +1407,10 @@ var _Node_serviceDesc = grpc.ServiceDesc{ MethodName: "GetVersion", Handler: _Node_GetVersion_Handler, }, + { + MethodName: "GetHealth", + Handler: _Node_GetHealth_Handler, + }, { MethodName: "ListImplementedServices", Handler: _Node_ListImplementedServices_Handler, diff --git a/proto/prysm/v1alpha1/node.pb.gw.go b/proto/prysm/v1alpha1/node.pb.gw.go index a057bf173..104633765 100755 --- a/proto/prysm/v1alpha1/node.pb.gw.go +++ b/proto/prysm/v1alpha1/node.pb.gw.go @@ -89,6 +89,42 @@ func local_request_Node_GetVersion_0(ctx context.Context, marshaler runtime.Mars } +var ( + filter_Node_GetHealth_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_Node_GetHealth_0(ctx context.Context, marshaler runtime.Marshaler, client NodeClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq HealthRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Node_GetHealth_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetHealth(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Node_GetHealth_0(ctx context.Context, marshaler runtime.Marshaler, server NodeServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq HealthRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Node_GetHealth_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetHealth(ctx, &protoReq) + return msg, metadata, err + +} + func request_Node_ListImplementedServices_0(ctx context.Context, marshaler runtime.Marshaler, client NodeClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata @@ -272,6 +308,29 @@ func RegisterNodeHandlerServer(ctx context.Context, mux *runtime.ServeMux, serve }) + mux.Handle("GET", pattern_Node_GetHealth_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/ethereum.eth.v1alpha1.Node/GetHealth") + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Node_GetHealth_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Node_GetHealth_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_Node_ListImplementedServices_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -488,6 +547,26 @@ func RegisterNodeHandlerClient(ctx context.Context, mux *runtime.ServeMux, clien }) + mux.Handle("GET", pattern_Node_GetHealth_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/ethereum.eth.v1alpha1.Node/GetHealth") + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Node_GetHealth_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Node_GetHealth_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_Node_ListImplementedServices_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -598,6 +677,8 @@ var ( pattern_Node_GetVersion_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"eth", "v1alpha1", "node", "version"}, "")) + pattern_Node_GetHealth_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"eth", "v1alpha1", "node", "health"}, "")) + pattern_Node_ListImplementedServices_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"eth", "v1alpha1", "node", "services"}, "")) pattern_Node_GetHost_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"eth", "v1alpha1", "node", "p2p"}, "")) @@ -616,6 +697,8 @@ var ( forward_Node_GetVersion_0 = runtime.ForwardResponseMessage + forward_Node_GetHealth_0 = runtime.ForwardResponseMessage + forward_Node_ListImplementedServices_0 = runtime.ForwardResponseMessage forward_Node_GetHost_0 = runtime.ForwardResponseMessage diff --git a/proto/prysm/v1alpha1/node.proto b/proto/prysm/v1alpha1/node.proto index e27999e6c..a0f8a510b 100644 --- a/proto/prysm/v1alpha1/node.proto +++ b/proto/prysm/v1alpha1/node.proto @@ -54,6 +54,13 @@ service Node { }; } + // Retrieve the current health of the node. + rpc GetHealth(HealthRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + get: "/eth/v1alpha1/node/health" + }; + } + // Retrieve the list of services implemented and enabled by this node. // // Any service not present in this list may return UNIMPLEMENTED or @@ -94,6 +101,10 @@ service Node { } } +message HealthRequest { + uint64 syncing_status = 1; +} + // Information about the current network sync status of the node. message SyncStatus { // Whether or not the node is currently syncing. diff --git a/testing/validator-mock/BUILD.bazel b/testing/validator-mock/BUILD.bazel index c63e24bf2..8a32955b7 100644 --- a/testing/validator-mock/BUILD.bazel +++ b/testing/validator-mock/BUILD.bazel @@ -13,6 +13,8 @@ go_library( importpath = "github.com/prysmaticlabs/prysm/v5/testing/validator-mock", visibility = ["//visibility:public"], deps = [ + "//api/client/beacon:go_default_library", + "//api/client/event:go_default_library", "//consensus-types/validator:go_default_library", "//proto/prysm/v1alpha1:go_default_library", "//validator/client/iface:go_default_library", diff --git a/testing/validator-mock/node_client_mock.go b/testing/validator-mock/node_client_mock.go index 6e5790c6b..8f6250cd8 100644 --- a/testing/validator-mock/node_client_mock.go +++ b/testing/validator-mock/node_client_mock.go @@ -13,6 +13,7 @@ import ( context "context" reflect "reflect" + "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" @@ -22,6 +23,7 @@ import ( type MockNodeClient struct { ctrl *gomock.Controller recorder *MockNodeClientMockRecorder + healthTracker *beacon.NodeHealthTracker } // MockNodeClientMockRecorder is the mock recorder for MockNodeClient. @@ -33,6 +35,7 @@ type MockNodeClientMockRecorder struct { func NewMockNodeClient(ctrl *gomock.Controller) *MockNodeClient { mock := &MockNodeClient{ctrl: ctrl} mock.recorder = &MockNodeClientMockRecorder{mock} + mock.healthTracker = beacon.NewNodeHealthTracker(mock) return mock } @@ -114,3 +117,7 @@ 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 e4a578f26..a63834d10 100644 --- a/testing/validator-mock/validator_client_mock.go +++ b/testing/validator-mock/validator_client_mock.go @@ -13,6 +13,8 @@ import ( context "context" reflect "reflect" + "github.com/prysmaticlabs/prysm/v5/api/client/beacon" + "github.com/prysmaticlabs/prysm/v5/api/client/event" eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" iface "github.com/prysmaticlabs/prysm/v5/validator/client/iface" gomock "go.uber.org/mock/gomock" @@ -297,32 +299,57 @@ func (mr *MockValidatorClientMockRecorder) ProposeExit(arg0, arg1 any) *gomock.C } // StartEventStream mocks base method. -func (m *MockValidatorClient) StartEventStream(arg0 context.Context) error { +func (m *MockValidatorClient) StartEventStream(arg0 context.Context, arg1 []string, arg2 chan<- *event.Event){ m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StartEventStream", arg0) + _ = m.ctrl.Call(m, "StartEventStream", arg0,arg1,arg2) +} + +// StartEventStream indicates an expected call of StartEventStream. +func (mr *MockValidatorClientMockRecorder) StartEventStream(arg0,arg1,arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartEventStream", reflect.TypeOf((*MockValidatorClient)(nil).StartEventStream), arg0, arg1, arg2) +} + +// ProcessEvent mocks base method. +func (m *MockValidatorClient) ProcessEvent(arg0 *event.Event) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProcessEvent", arg0) ret0, _ := ret[0].(error) return ret0 } -// StartEventStream indicates an expected call of StartEventStream. -func (mr *MockValidatorClientMockRecorder) StartEventStream(arg0 any) *gomock.Call { +// ProcessEvent indicates an expected call of ProcessEvent. +func (mr *MockValidatorClientMockRecorder) ProcessEvent(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartEventStream", reflect.TypeOf((*MockValidatorClient)(nil).StartEventStream), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessEvent", reflect.TypeOf((*MockValidatorClient)(nil).ProcessEvent), arg0) } -// StreamSlots mocks base method. -func (m *MockValidatorClient) StreamSlots(arg0 context.Context, arg1 *eth.StreamSlotsRequest) (eth.BeaconNodeValidator_StreamSlotsClient, error) { +// NodeIsHealthy mocks base method. +func (m *MockValidatorClient) NodeIsHealthy(arg0 context.Context) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StreamSlots", arg0, arg1) - ret0, _ := ret[0].(eth.BeaconNodeValidator_StreamSlotsClient) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "NodeIsHealthy",arg0) + ret0, _ := ret[0].(bool) + return ret0 } -// StreamSlots indicates an expected call of StreamSlots. -func (mr *MockValidatorClientMockRecorder) StreamSlots(arg0, arg1 any) *gomock.Call { +// NodeIsHealthy indicates an expected call of NodeIsHealthy. +func (mr *MockValidatorClientMockRecorder) NodeIsHealthy(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StreamSlots", reflect.TypeOf((*MockValidatorClient)(nil).StreamSlots), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeIsHealthy", reflect.TypeOf((*MockValidatorClient)(nil).NodeIsHealthy), arg0) +} + +// NodeHealthTracker mocks base method. +func (m *MockValidatorClient) NodeHealthTracker() *beacon.NodeHealthTracker { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NodeHealthTracker") + ret0, _ := ret[0].(*beacon.NodeHealthTracker) + return ret0 +} + +// NodeHealthTracker indicates an expected call of NodeHealthTracker. +func (mr *MockValidatorClientMockRecorder) NodeHealthTracker() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeHealthTracker", reflect.TypeOf((*MockValidatorClient)(nil).NodeHealthTracker)) } // SubmitAggregateSelectionProof mocks base method. diff --git a/validator/accounts/cli_manager.go b/validator/accounts/cli_manager.go index 4d7bc81db..e897292c6 100644 --- a/validator/accounts/cli_manager.go +++ b/validator/accounts/cli_manager.go @@ -87,10 +87,7 @@ func (acm *CLIManager) prepareBeaconClients(ctx context.Context) (*iface.Validat acm.beaconApiTimeout, ) - restHandler := &beaconApi.BeaconApiJsonRestHandler{ - HttpClient: http.Client{Timeout: acm.beaconApiTimeout}, - Host: 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/BUILD.bazel b/validator/accounts/testing/BUILD.bazel index c5beb1367..e824b32e2 100644 --- a/validator/accounts/testing/BUILD.bazel +++ b/validator/accounts/testing/BUILD.bazel @@ -10,6 +10,8 @@ go_library( "//validator:__subpackages__", ], deps = [ + "//api/client/beacon:go_default_library", + "//api/client/event:go_default_library", "//config/proposer:go_default_library", "//consensus-types/primitives:go_default_library", "//proto/prysm/v1alpha1:go_default_library", diff --git a/validator/accounts/testing/mock.go b/validator/accounts/testing/mock.go index d1e511611..f520a1909 100644 --- a/validator/accounts/testing/mock.go +++ b/validator/accounts/testing/mock.go @@ -7,6 +7,8 @@ import ( "sync" "time" + "github.com/prysmaticlabs/prysm/v5/api/client/beacon" + "github.com/prysmaticlabs/prysm/v5/api/client/event" "github.com/prysmaticlabs/prysm/v5/config/proposer" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" @@ -213,14 +215,18 @@ func (m *Validator) SetProposerSettings(_ context.Context, settings *proposer.Se return nil } -func (_ *Validator) StartEventStream(_ context.Context) error { +func (*Validator) StartEventStream(_ context.Context, _ []string, _ chan<- *event.Event) { panic("implement me") } -func (_ *Validator) EventStreamIsRunning() bool { +func (*Validator) ProcessEvent(event *event.Event) { panic("implement me") } -func (_ *Validator) NodeIsHealthy(ctx context.Context) bool { +func (*Validator) EventStreamIsRunning() bool { + panic("implement me") +} + +func (*Validator) HealthTracker() *beacon.NodeHealthTracker { panic("implement me") } diff --git a/validator/client/BUILD.bazel b/validator/client/BUILD.bazel index b0f8beed8..bbcab6f56 100644 --- a/validator/client/BUILD.bazel +++ b/validator/client/BUILD.bazel @@ -23,7 +23,11 @@ go_library( "//validator:__subpackages__", ], deps = [ + "//api/client:go_default_library", + "//api/client/beacon:go_default_library", + "//api/client/event:go_default_library", "//api/grpc:go_default_library", + "//api/server/structs:go_default_library", "//async:go_default_library", "//async/event:go_default_library", "//beacon-chain/builder:go_default_library", @@ -115,6 +119,8 @@ go_test( ], embed = [":go_default_library"], deps = [ + "//api/client/beacon:go_default_library", + "//api/client/beacon/testing:go_default_library", "//async/event:go_default_library", "//beacon-chain/core/signing:go_default_library", "//cache/lru:go_default_library", diff --git a/validator/client/beacon-api/BUILD.bazel b/validator/client/beacon-api/BUILD.bazel index 346a48b2d..f42373d18 100644 --- a/validator/client/beacon-api/BUILD.bazel +++ b/validator/client/beacon-api/BUILD.bazel @@ -16,7 +16,6 @@ go_library( "domain_data.go", "doppelganger.go", "duties.go", - "event_handler.go", "genesis.go", "get_beacon_block.go", "index.go", @@ -43,10 +42,11 @@ go_library( visibility = ["//validator:__subpackages__"], deps = [ "//api:go_default_library", + "//api/client/beacon:go_default_library", + "//api/client/event:go_default_library", "//api/server/structs:go_default_library", "//beacon-chain/core/helpers:go_default_library", "//beacon-chain/core/signing:go_default_library", - "//beacon-chain/rpc/eth/events:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", "//consensus-types/validator:go_default_library", @@ -86,7 +86,6 @@ go_test( "domain_data_test.go", "doppelganger_test.go", "duties_test.go", - "event_handler_test.go", "genesis_test.go", "get_beacon_block_test.go", "index_test.go", @@ -138,7 +137,6 @@ go_test( "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_golang_protobuf//ptypes/empty", "@com_github_pkg_errors//:go_default_library", - "@com_github_sirupsen_logrus//hooks/test:go_default_library", "@org_golang_google_protobuf//types/known/emptypb:go_default_library", "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", "@org_uber_go_mock//gomock:go_default_library", diff --git a/validator/client/beacon-api/beacon_api_node_client.go b/validator/client/beacon-api/beacon_api_node_client.go index e1e39f9d4..f995806e2 100644 --- a/validator/client/beacon-api/beacon_api_node_client.go +++ b/validator/client/beacon-api/beacon_api_node_client.go @@ -7,16 +7,22 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/golang/protobuf/ptypes/empty" "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/api/client/beacon" "github.com/prysmaticlabs/prysm/v5/api/server/structs" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/validator/client/iface" "google.golang.org/protobuf/types/known/timestamppb" ) +var ( + _ = iface.NodeClient(&beaconApiNodeClient{}) +) + type beaconApiNodeClient struct { fallbackClient iface.NodeClient jsonRestHandler JsonRestHandler genesisProvider GenesisProvider + healthTracker *beacon.NodeHealthTracker } func (c *beaconApiNodeClient) GetSyncStatus(ctx context.Context, _ *empty.Empty) (*ethpb.SyncStatus, error) { @@ -101,10 +107,16 @@ func (c *beaconApiNodeClient) IsHealthy(ctx context.Context) bool { return c.jsonRestHandler.Get(ctx, "/eth/v1/node/health", nil) == nil } +func (c *beaconApiNodeClient) HealthTracker() *beacon.NodeHealthTracker { + return c.healthTracker +} + func NewNodeClientWithFallback(jsonRestHandler JsonRestHandler, fallbackClient iface.NodeClient) iface.NodeClient { - return &beaconApiNodeClient{ + b := &beaconApiNodeClient{ jsonRestHandler: jsonRestHandler, fallbackClient: fallbackClient, genesisProvider: beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler}, } + b.healthTracker = beacon.NewNodeHealthTracker(b) + return b } diff --git a/validator/client/beacon-api/beacon_api_validator_client.go b/validator/client/beacon-api/beacon_api_validator_client.go index 0f0f5fa87..12463e05e 100644 --- a/validator/client/beacon-api/beacon_api_validator_client.go +++ b/validator/client/beacon-api/beacon_api_validator_client.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/golang/protobuf/ptypes/empty" "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/api/client/event" "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/validator/client/iface" @@ -14,20 +15,14 @@ import ( type ValidatorClientOpt func(*beaconApiValidatorClient) -func WithEventHandler(h *EventHandler) ValidatorClientOpt { - return func(c *beaconApiValidatorClient) { - c.eventHandler = h - } -} - type beaconApiValidatorClient struct { genesisProvider GenesisProvider dutiesProvider dutiesProvider stateValidatorsProvider StateValidatorsProvider jsonRestHandler JsonRestHandler - eventHandler *EventHandler beaconBlockConverter BeaconBlockConverter prysmBeaconChainCLient iface.PrysmBeaconChainClient + isEventStreamRunning bool } func NewBeaconApiValidatorClient(jsonRestHandler JsonRestHandler, opts ...ValidatorClientOpt) iface.ValidatorClient { @@ -41,6 +36,7 @@ func NewBeaconApiValidatorClient(jsonRestHandler JsonRestHandler, opts ...Valida nodeClient: &beaconApiNodeClient{jsonRestHandler: jsonRestHandler}, jsonRestHandler: jsonRestHandler, }, + isEventStreamRunning: false, } for _, o := range opts { o(c) @@ -135,10 +131,6 @@ func (c *beaconApiValidatorClient) ProposeExit(ctx context.Context, in *ethpb.Si }) } -func (c *beaconApiValidatorClient) StreamSlots(ctx context.Context, in *ethpb.StreamSlotsRequest) (ethpb.BeaconNodeValidator_StreamSlotsClient, error) { - return c.streamSlots(ctx, in, time.Second), nil -} - func (c *beaconApiValidatorClient) StreamBlocksAltair(ctx context.Context, in *ethpb.StreamBlocksRequest) (ethpb.BeaconNodeValidator_StreamBlocksAltairClient, error) { return c.streamBlocks(ctx, in, time.Second), nil } @@ -198,17 +190,22 @@ func (c *beaconApiValidatorClient) WaitForChainStart(ctx context.Context, _ *emp return c.waitForChainStart(ctx) } -func (c *beaconApiValidatorClient) StartEventStream(ctx context.Context) error { - if c.eventHandler != nil { - if err := c.eventHandler.get(ctx, []string{"head"}); err != nil { - return errors.Wrapf(err, "could not invoke event handler") +func (c *beaconApiValidatorClient) StartEventStream(ctx context.Context, topics []string, eventsChannel chan<- *event.Event) { + eventStream, err := event.NewEventStream(ctx, c.jsonRestHandler.HttpClient(), c.jsonRestHandler.Host(), topics) + if err != nil { + eventsChannel <- &event.Event{ + EventType: event.EventError, + Data: []byte(errors.Wrap(err, "failed to start event stream").Error()), } + return } - return nil + c.isEventStreamRunning = true + eventStream.Subscribe(eventsChannel) + c.isEventStreamRunning = false } func (c *beaconApiValidatorClient) EventStreamIsRunning() bool { - return c.eventHandler.running + return c.isEventStreamRunning } func (c *beaconApiValidatorClient) GetAggregatedSelections(ctx context.Context, selections []iface.BeaconCommitteeSelection) ([]iface.BeaconCommitteeSelection, error) { diff --git a/validator/client/beacon-api/event_handler.go b/validator/client/beacon-api/event_handler.go deleted file mode 100644 index 1f5b789a1..000000000 --- a/validator/client/beacon-api/event_handler.go +++ /dev/null @@ -1,134 +0,0 @@ -package beacon_api - -import ( - "context" - "net/http" - "strings" - "sync" - - "github.com/pkg/errors" - "github.com/prysmaticlabs/prysm/v5/api" -) - -// Currently set to the first power of 2 bigger than the size of the `head` event -// which is 446 bytes -const eventByteLimit = 512 - -// EventHandler is responsible for subscribing to the Beacon API events endpoint -// and dispatching received events to subscribers. -type EventHandler struct { - httpClient *http.Client - host string - running bool - subs []eventSub - sync.Mutex -} - -type eventSub struct { - name string - ch chan<- event -} - -type event struct { - eventType string - data string -} - -// NewEventHandler returns a new handler. -func NewEventHandler(httpClient *http.Client, host string) *EventHandler { - return &EventHandler{ - httpClient: httpClient, - host: host, - running: false, - subs: make([]eventSub, 0), - } -} - -func (h *EventHandler) subscribe(sub eventSub) { - h.Lock() - h.subs = append(h.subs, sub) - h.Unlock() -} - -func (h *EventHandler) get(ctx context.Context, topics []string) error { - if len(topics) == 0 { - return errors.New("no topics provided") - } - if h.running { - log.Warn("Event listener is already running, ignoring function call") - } - - go func() { - h.running = true - defer func() { h.running = false }() - - allTopics := strings.Join(topics, ",") - log.Info("Starting listening to Beacon API events on topics: " + allTopics) - url := h.host + "/eth/v1/events?topics=" + allTopics - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - log.WithError(err).Error("Failed to create HTTP request") - return - } - req.Header.Set("Accept", api.EventStreamMediaType) - req.Header.Set("Connection", api.KeepAlive) - resp, err := h.httpClient.Do(req) - if err != nil { - log.WithError(err).Error("Failed to perform HTTP request") - return - } - - defer func() { - if closeErr := resp.Body.Close(); closeErr != nil { - log.WithError(closeErr).Error("Failed to close events response body") - } - }() - - // We signal an EOF error in a special way. When we get this error while reading the response body, - // there might still be an event received in the body that we should handle. - eof := false - for { - if ctx.Err() != nil { - log.WithError(ctx.Err()).Error("Stopping listening to Beacon API events") - return - } - - rawData := make([]byte, eventByteLimit) - _, err = resp.Body.Read(rawData) - if err != nil { - if strings.Contains(err.Error(), "EOF") { - log.Error("Received EOF while reading events response body. Stopping listening to Beacon API events") - eof = true - } else { - log.WithError(err).Error("Stopping listening to Beacon API events") - return - } - } - - e := strings.Split(string(rawData), "\n") - // We expect that the event format will contain event type and data separated with a newline - if len(e) < 2 { - // We reached EOF and there is no event to send - if eof { - return - } - continue - } - - for _, sub := range h.subs { - select { - case sub.ch <- event{eventType: e[0], data: e[1]}: - // Event sent successfully. - default: - log.Warn("Subscriber '" + sub.name + "' not ready to receive events") - } - } - // We reached EOF and sent the last event - if eof { - return - } - } - }() - - return nil -} diff --git a/validator/client/beacon-api/event_handler_test.go b/validator/client/beacon-api/event_handler_test.go deleted file mode 100644 index 4338aac9a..000000000 --- a/validator/client/beacon-api/event_handler_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package beacon_api - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/prysmaticlabs/prysm/v5/testing/assert" - "github.com/prysmaticlabs/prysm/v5/testing/require" - logtest "github.com/sirupsen/logrus/hooks/test" -) - -func TestEventHandler(t *testing.T) { - logHook := logtest.NewGlobal() - - mux := http.NewServeMux() - mux.HandleFunc("/eth/v1/events", func(w http.ResponseWriter, r *http.Request) { - flusher, ok := w.(http.Flusher) - require.Equal(t, true, ok) - _, err := fmt.Fprint(w, "head\ndata\n\n") - require.NoError(t, err) - flusher.Flush() - }) - server := httptest.NewServer(mux) - defer server.Close() - - handler := NewEventHandler(http.DefaultClient, server.URL) - ch1 := make(chan event, 1) - sub1 := eventSub{ch: ch1} - ch2 := make(chan event, 1) - sub2 := eventSub{ch: ch2} - ch3 := make(chan event, 1) - sub3 := eventSub{name: "sub3", ch: ch3} - // fill up the channel so that it can't receive more events - ch3 <- event{} - handler.subscribe(sub1) - handler.subscribe(sub2) - handler.subscribe(sub3) - - require.NoError(t, handler.get(context.Background(), []string{"head"})) - // make sure the goroutine inside handler.get is invoked - time.Sleep(500 * time.Millisecond) - - e := <-ch1 - assert.Equal(t, "head", e.eventType) - assert.Equal(t, "data", e.data) - e = <-ch2 - assert.Equal(t, "head", e.eventType) - assert.Equal(t, "data", e.data) - - assert.LogsContain(t, logHook, "Subscriber 'sub3' not ready to receive events") -} diff --git a/validator/client/beacon-api/json_rest_handler.go b/validator/client/beacon-api/json_rest_handler.go index 923d4842b..57f74aaf4 100644 --- a/validator/client/beacon-api/json_rest_handler.go +++ b/validator/client/beacon-api/json_rest_handler.go @@ -16,23 +16,43 @@ import ( type JsonRestHandler interface { Get(ctx context.Context, endpoint string, resp interface{}) error Post(ctx context.Context, endpoint string, headers map[string]string, data *bytes.Buffer, resp interface{}) error + HttpClient() *http.Client + Host() string } type BeaconApiJsonRestHandler struct { - HttpClient http.Client - Host string + client http.Client + host string +} + +// NewBeaconApiJsonRestHandler returns a JsonRestHandler +func NewBeaconApiJsonRestHandler(client http.Client, host string) JsonRestHandler { + return &BeaconApiJsonRestHandler{ + client: client, + host: host, + } +} + +// GetHttpClient 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 { + 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 { - url := c.Host + endpoint + url := c.host + endpoint req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return errors.Wrapf(err, "failed to create request for endpoint %s", url) } - httpResp, err := c.HttpClient.Do(req) + httpResp, err := c.client.Do(req) if err != nil { return errors.Wrapf(err, "failed to perform request for endpoint %s", url) } @@ -58,7 +78,7 @@ func (c BeaconApiJsonRestHandler) Post( return errors.New("data is nil") } - url := c.Host + apiEndpoint + url := c.host + apiEndpoint req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, data) if err != nil { return errors.Wrapf(err, "failed to create request for endpoint %s", url) @@ -69,7 +89,7 @@ func (c BeaconApiJsonRestHandler) Post( } req.Header.Set("Content-Type", api.JsonMediaType) - httpResp, err := c.HttpClient.Do(req) + httpResp, err := c.client.Do(req) if err != nil { return errors.Wrapf(err, "failed to perform request for endpoint %s", url) } diff --git a/validator/client/beacon-api/json_rest_handler_test.go b/validator/client/beacon-api/json_rest_handler_test.go index 5c858f105..d28729eee 100644 --- a/validator/client/beacon-api/json_rest_handler_test.go +++ b/validator/client/beacon-api/json_rest_handler_test.go @@ -41,8 +41,8 @@ func TestGet(t *testing.T) { defer server.Close() jsonRestHandler := BeaconApiJsonRestHandler{ - HttpClient: http.Client{Timeout: time.Second * 5}, - Host: server.URL, + client: http.Client{Timeout: time.Second * 5}, + host: server.URL, } resp := &structs.GetGenesisResponse{} require.NoError(t, jsonRestHandler.Get(ctx, endpoint+"?arg1=abc&arg2=def", resp)) @@ -87,8 +87,8 @@ func TestPost(t *testing.T) { defer server.Close() jsonRestHandler := BeaconApiJsonRestHandler{ - HttpClient: http.Client{Timeout: time.Second * 5}, - Host: server.URL, + client: http.Client{Timeout: time.Second * 5}, + host: server.URL, } resp := &structs.GetGenesisResponse{} require.NoError(t, jsonRestHandler.Post(ctx, endpoint, headers, bytes.NewBuffer(dataBytes), resp)) 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 0c0967573..ca58f39d2 100644 --- a/validator/client/beacon-api/mock/json_rest_handler_mock.go +++ b/validator/client/beacon-api/mock/json_rest_handler_mock.go @@ -12,6 +12,7 @@ package mock import ( bytes "bytes" context "context" + "net/http" reflect "reflect" gomock "go.uber.org/mock/gomock" @@ -35,6 +36,14 @@ 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 @@ -67,3 +76,4 @@ 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) } + diff --git a/validator/client/beacon-api/stream_blocks.go b/validator/client/beacon-api/stream_blocks.go index 5aa52afc0..19e3f1c3a 100644 --- a/validator/client/beacon-api/stream_blocks.go +++ b/validator/client/beacon-api/stream_blocks.go @@ -4,13 +4,11 @@ import ( "bytes" "context" "encoding/json" - "strconv" "time" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/api/server/structs" - "github.com/prysmaticlabs/prysm/v5/beacon-chain/rpc/eth/events" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "google.golang.org/grpc" @@ -23,15 +21,6 @@ type abstractSignedBlockResponseJson struct { Data json.RawMessage `json:"data"` } -type streamSlotsClient struct { - grpc.ClientStream - ctx context.Context - beaconApiClient beaconApiValidatorClient - streamSlotsRequest *ethpb.StreamSlotsRequest - pingDelay time.Duration - ch chan event -} - type streamBlocksAltairClient struct { grpc.ClientStream ctx context.Context @@ -47,18 +36,6 @@ type headSignedBeaconBlockResult struct { slot primitives.Slot } -func (c beaconApiValidatorClient) streamSlots(ctx context.Context, in *ethpb.StreamSlotsRequest, pingDelay time.Duration) ethpb.BeaconNodeValidator_StreamSlotsClient { - ch := make(chan event, 1) - c.eventHandler.subscribe(eventSub{name: "stream slots", ch: ch}) - return &streamSlotsClient{ - ctx: ctx, - beaconApiClient: c, - streamSlotsRequest: in, - pingDelay: pingDelay, - ch: ch, - } -} - func (c beaconApiValidatorClient) streamBlocks(ctx context.Context, in *ethpb.StreamBlocksRequest, pingDelay time.Duration) ethpb.BeaconNodeValidator_StreamBlocksAltairClient { return &streamBlocksAltairClient{ ctx: ctx, @@ -68,30 +45,6 @@ func (c beaconApiValidatorClient) streamBlocks(ctx context.Context, in *ethpb.St } } -func (c *streamSlotsClient) Recv() (*ethpb.StreamSlotsResponse, error) { - for { - select { - case rawEvent := <-c.ch: - if rawEvent.eventType != events.HeadTopic { - continue - } - e := &structs.HeadEvent{} - if err := json.Unmarshal([]byte(rawEvent.data), e); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal head event into JSON") - } - uintSlot, err := strconv.ParseUint(e.Slot, 10, 64) - if err != nil { - return nil, errors.Wrap(err, "failed to parse slot") - } - return ðpb.StreamSlotsResponse{ - Slot: primitives.Slot(uintSlot), - }, nil - case <-c.ctx.Done(): - return nil, errors.New("context canceled") - } - } -} - func (c *streamBlocksAltairClient) Recv() (*ethpb.StreamBlocksResponse, error) { result, err := c.beaconApiClient.getHeadSignedBeaconBlock(c.ctx) if err != nil { diff --git a/validator/client/grpc-api/BUILD.bazel b/validator/client/grpc-api/BUILD.bazel index 254a49ace..3fdd64979 100644 --- a/validator/client/grpc-api/BUILD.bazel +++ b/validator/client/grpc-api/BUILD.bazel @@ -11,6 +11,10 @@ go_library( importpath = "github.com/prysmaticlabs/prysm/v5/validator/client/grpc-api", visibility = ["//validator:__subpackages__"], deps = [ + "//api/client:go_default_library", + "//api/client/beacon:go_default_library", + "//api/client/event:go_default_library", + "//api/server/structs:go_default_library", "//beacon-chain/rpc/eth/helpers:go_default_library", "//beacon-chain/state/state-native:go_default_library", "//consensus-types/primitives:go_default_library", @@ -20,6 +24,8 @@ go_library( "//validator/client/iface:go_default_library", "@com_github_golang_protobuf//ptypes/empty", "@com_github_pkg_errors//:go_default_library", + "@com_github_sirupsen_logrus//:go_default_library", + "@io_opencensus_go//trace:go_default_library", "@org_golang_google_grpc//:go_default_library", ], ) @@ -33,6 +39,8 @@ go_test( ], embed = [":go_default_library"], deps = [ + "//api/client/event:go_default_library", + "//api/server/structs:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", "//consensus-types/validator:go_default_library", @@ -43,6 +51,7 @@ go_test( "//testing/util:go_default_library", "//testing/validator-mock:go_default_library", "//validator/client/iface:go_default_library", + "@com_github_sirupsen_logrus//hooks/test:go_default_library", "@org_golang_google_protobuf//types/known/emptypb:go_default_library", "@org_uber_go_mock//gomock:go_default_library", ], diff --git a/validator/client/grpc-api/grpc_node_client.go b/validator/client/grpc-api/grpc_node_client.go index 2dfe68dfd..c19dd4663 100644 --- a/validator/client/grpc-api/grpc_node_client.go +++ b/validator/client/grpc-api/grpc_node_client.go @@ -4,13 +4,20 @@ import ( "context" "github.com/golang/protobuf/ptypes/empty" + "github.com/prysmaticlabs/prysm/v5/api/client/beacon" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/validator/client/iface" + log "github.com/sirupsen/logrus" "google.golang.org/grpc" ) +var ( + _ = iface.NodeClient(&grpcNodeClient{}) +) + type grpcNodeClient struct { - nodeClient ethpb.NodeClient + nodeClient ethpb.NodeClient + healthTracker *beacon.NodeHealthTracker } func (c *grpcNodeClient) GetSyncStatus(ctx context.Context, in *empty.Empty) (*ethpb.SyncStatus, error) { @@ -29,10 +36,21 @@ func (c *grpcNodeClient) ListPeers(ctx context.Context, in *empty.Empty) (*ethpb return c.nodeClient.ListPeers(ctx, in) } -func (c *grpcNodeClient) IsHealthy(context.Context) bool { - panic("function not supported for gRPC client") +func (c *grpcNodeClient) IsHealthy(ctx context.Context) bool { + _, err := c.nodeClient.GetHealth(ctx, ðpb.HealthRequest{}) + if err != nil { + log.WithError(err).Debug("failed to get health of node") + return false + } + return true +} + +func (c *grpcNodeClient) HealthTracker() *beacon.NodeHealthTracker { + return c.healthTracker } func NewNodeClient(cc grpc.ClientConnInterface) iface.NodeClient { - return &grpcNodeClient{ethpb.NewNodeClient(cc)} + g := &grpcNodeClient{nodeClient: ethpb.NewNodeClient(cc)} + g.healthTracker = beacon.NewNodeHealthTracker(g) + return g } diff --git a/validator/client/grpc-api/grpc_validator_client.go b/validator/client/grpc-api/grpc_validator_client.go index 0aa0390b7..639bd8e39 100644 --- a/validator/client/grpc-api/grpc_validator_client.go +++ b/validator/client/grpc-api/grpc_validator_client.go @@ -2,16 +2,24 @@ package grpc_api import ( "context" + "encoding/json" + "strconv" "github.com/golang/protobuf/ptypes/empty" "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/api/client" + eventClient "github.com/prysmaticlabs/prysm/v5/api/client/event" + "github.com/prysmaticlabs/prysm/v5/api/server/structs" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/validator/client/iface" + log "github.com/sirupsen/logrus" + "go.opencensus.io/trace" "google.golang.org/grpc" ) type grpcValidatorClient struct { beaconNodeValidatorClient ethpb.BeaconNodeValidatorClient + isEventStreamRunning bool } func (c *grpcValidatorClient) GetDuties(ctx context.Context, in *ethpb.DutiesRequest) (*ethpb.DutiesResponse, error) { @@ -70,10 +78,6 @@ func (c *grpcValidatorClient) ProposeExit(ctx context.Context, in *ethpb.SignedV return c.beaconNodeValidatorClient.ProposeExit(ctx, in) } -func (c *grpcValidatorClient) StreamSlots(ctx context.Context, in *ethpb.StreamSlotsRequest) (ethpb.BeaconNodeValidator_StreamSlotsClient, error) { - return c.beaconNodeValidatorClient.StreamSlots(ctx, in) -} - func (c *grpcValidatorClient) StreamBlocksAltair(ctx context.Context, in *ethpb.StreamBlocksRequest) (ethpb.BeaconNodeValidator_StreamBlocksAltairClient, error) { return c.beaconNodeValidatorClient.StreamBlocksAltair(ctx, in) } @@ -119,7 +123,7 @@ func (c *grpcValidatorClient) WaitForChainStart(ctx context.Context, in *empty.E stream, err := c.beaconNodeValidatorClient.WaitForChainStart(ctx, in) if err != nil { return nil, errors.Wrap( - iface.ErrConnectionIssue, + client.ErrConnectionIssue, errors.Wrap(err, "could not setup beacon chain ChainStart streaming client").Error(), ) } @@ -146,13 +150,97 @@ func (grpcValidatorClient) GetAggregatedSyncSelections(context.Context, []iface. } func NewGrpcValidatorClient(cc grpc.ClientConnInterface) iface.ValidatorClient { - return &grpcValidatorClient{ethpb.NewBeaconNodeValidatorClient(cc)} + return &grpcValidatorClient{ethpb.NewBeaconNodeValidatorClient(cc), false} } -func (c *grpcValidatorClient) StartEventStream(context.Context) error { - panic("function not supported for gRPC client") +func (c *grpcValidatorClient) StartEventStream(ctx context.Context, topics []string, eventsChannel chan<- *eventClient.Event) { + ctx, span := trace.StartSpan(ctx, "validator.gRPCClient.StartEventStream") + defer span.End() + if len(topics) == 0 { + eventsChannel <- &eventClient.Event{ + EventType: eventClient.EventError, + Data: []byte(errors.New("no topics were added").Error()), + } + return + } + // TODO(13563): ONLY WORKS WITH HEAD TOPIC RIGHT NOW/ONLY PROVIDES THE SLOT + containsHead := false + for i := range topics { + if topics[i] == eventClient.EventHead { + containsHead = true + } + } + if !containsHead { + eventsChannel <- &eventClient.Event{ + EventType: eventClient.EventConnectionError, + Data: []byte(errors.Wrap(client.ErrConnectionIssue, "gRPC only supports the head topic, and head topic was not passed").Error()), + } + } + if containsHead && len(topics) > 1 { + log.Warn("gRPC only supports the head topic, other topics will be ignored") + } + + stream, err := c.beaconNodeValidatorClient.StreamSlots(ctx, ðpb.StreamSlotsRequest{VerifiedOnly: true}) + if err != nil { + eventsChannel <- &eventClient.Event{ + EventType: eventClient.EventConnectionError, + Data: []byte(errors.Wrap(client.ErrConnectionIssue, err.Error()).Error()), + } + return + } + c.isEventStreamRunning = true + for { + select { + case <-ctx.Done(): + log.Info("Context canceled, stopping event stream") + close(eventsChannel) + c.isEventStreamRunning = false + return + default: + if ctx.Err() != nil { + c.isEventStreamRunning = false + if errors.Is(ctx.Err(), context.Canceled) { + eventsChannel <- &eventClient.Event{ + EventType: eventClient.EventConnectionError, + Data: []byte(errors.Wrap(client.ErrConnectionIssue, ctx.Err().Error()).Error()), + } + return + } + eventsChannel <- &eventClient.Event{ + EventType: eventClient.EventError, + Data: []byte(ctx.Err().Error()), + } + return + } + res, err := stream.Recv() + if err != nil { + c.isEventStreamRunning = false + eventsChannel <- &eventClient.Event{ + EventType: eventClient.EventConnectionError, + Data: []byte(errors.Wrap(client.ErrConnectionIssue, err.Error()).Error()), + } + return + } + if res == nil { + continue + } + b, err := json.Marshal(structs.HeadEvent{ + Slot: strconv.FormatUint(uint64(res.Slot), 10), + }) + if err != nil { + eventsChannel <- &eventClient.Event{ + EventType: eventClient.EventError, + Data: []byte(errors.Wrap(err, "failed to marshal Head Event").Error()), + } + } + eventsChannel <- &eventClient.Event{ + EventType: eventClient.EventHead, + Data: b, + } + } + } } func (c *grpcValidatorClient) EventStreamIsRunning() bool { - panic("function not supported for gRPC client") + return c.isEventStreamRunning } diff --git a/validator/client/grpc-api/grpc_validator_client_test.go b/validator/client/grpc-api/grpc_validator_client_test.go index d8710180f..cae411ee1 100644 --- a/validator/client/grpc-api/grpc_validator_client_test.go +++ b/validator/client/grpc-api/grpc_validator_client_test.go @@ -2,11 +2,18 @@ package grpc_api import ( "context" + "encoding/json" "errors" "testing" + "time" + eventClient "github.com/prysmaticlabs/prysm/v5/api/client/event" + "github.com/prysmaticlabs/prysm/v5/api/server/structs" + eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/testing/assert" mock2 "github.com/prysmaticlabs/prysm/v5/testing/mock" + "github.com/prysmaticlabs/prysm/v5/testing/require" + logTest "github.com/sirupsen/logrus/hooks/test" "go.uber.org/mock/gomock" "google.golang.org/protobuf/types/known/emptypb" ) @@ -21,8 +28,105 @@ func TestWaitForChainStart_StreamSetupFails(t *testing.T) { gomock.Any(), ).Return(nil, errors.New("failed stream")) - validatorClient := &grpcValidatorClient{beaconNodeValidatorClient} + validatorClient := &grpcValidatorClient{beaconNodeValidatorClient, true} _, err := validatorClient.WaitForChainStart(context.Background(), &emptypb.Empty{}) want := "could not setup beacon chain ChainStart streaming client" assert.ErrorContains(t, want, err) } + +func TestStartEventStream(t *testing.T) { + hook := logTest.NewGlobal() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + beaconNodeValidatorClient := mock2.NewMockBeaconNodeValidatorClient(ctrl) + grpcClient := &grpcValidatorClient{beaconNodeValidatorClient, true} + tests := []struct { + name string + topics []string + prepare func() + verify func(t *testing.T, event *eventClient.Event) + }{ + { + name: "Happy path Head topic", + topics: []string{"head"}, + prepare: func() { + stream := mock2.NewMockBeaconNodeValidator_StreamSlotsClient(ctrl) + beaconNodeValidatorClient.EXPECT().StreamSlots(gomock.Any(), + ð.StreamSlotsRequest{VerifiedOnly: true}).Return(stream, nil) + stream.EXPECT().Context().Return(ctx).AnyTimes() + stream.EXPECT().Recv().Return( + ð.StreamSlotsResponse{Slot: 123}, + nil, + ).AnyTimes() + }, + verify: func(t *testing.T, event *eventClient.Event) { + require.Equal(t, event.EventType, eventClient.EventHead) + head := structs.HeadEvent{} + require.NoError(t, json.Unmarshal(event.Data, &head)) + require.Equal(t, head.Slot, "123") + }, + }, + { + name: "no head produces error", + topics: []string{"unsupportedTopic"}, + prepare: func() { + stream := mock2.NewMockBeaconNodeValidator_StreamSlotsClient(ctrl) + beaconNodeValidatorClient.EXPECT().StreamSlots(gomock.Any(), + ð.StreamSlotsRequest{VerifiedOnly: true}).Return(stream, nil) + stream.EXPECT().Context().Return(ctx).AnyTimes() + stream.EXPECT().Recv().Return( + ð.StreamSlotsResponse{Slot: 123}, + nil, + ).AnyTimes() + }, + verify: func(t *testing.T, event *eventClient.Event) { + require.Equal(t, event.EventType, eventClient.EventConnectionError) + }, + }, + { + name: "Unsupported topics warning", + topics: []string{"head", "unsupportedTopic"}, + prepare: func() { + stream := mock2.NewMockBeaconNodeValidator_StreamSlotsClient(ctrl) + beaconNodeValidatorClient.EXPECT().StreamSlots(gomock.Any(), + ð.StreamSlotsRequest{VerifiedOnly: true}).Return(stream, nil) + stream.EXPECT().Context().Return(ctx).AnyTimes() + stream.EXPECT().Recv().Return( + ð.StreamSlotsResponse{Slot: 123}, + nil, + ).AnyTimes() + }, + verify: func(t *testing.T, event *eventClient.Event) { + require.Equal(t, event.EventType, eventClient.EventHead) + head := structs.HeadEvent{} + require.NoError(t, json.Unmarshal(event.Data, &head)) + require.Equal(t, head.Slot, "123") + assert.LogsContain(t, hook, "gRPC only supports the head topic") + }, + }, + { + name: "No topics error", + topics: []string{}, + prepare: func() {}, + verify: func(t *testing.T, event *eventClient.Event) { + require.Equal(t, event.EventType, eventClient.EventError) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + eventsChannel := make(chan *eventClient.Event, 1) // Buffer to prevent blocking + tc.prepare() // Setup mock expectations + + go grpcClient.StartEventStream(ctx, tc.topics, eventsChannel) + + event := <-eventsChannel + // Depending on what you're testing, you may need a timeout or a specific number of events to read + time.AfterFunc(1*time.Second, cancel) // Prevents hanging forever + tc.verify(t, event) + }) + } +} diff --git a/validator/client/iface/BUILD.bazel b/validator/client/iface/BUILD.bazel index 283d7bc4f..8c58f7d9d 100644 --- a/validator/client/iface/BUILD.bazel +++ b/validator/client/iface/BUILD.bazel @@ -12,6 +12,8 @@ go_library( importpath = "github.com/prysmaticlabs/prysm/v5/validator/client/iface", visibility = ["//visibility:public"], deps = [ + "//api/client/beacon:go_default_library", + "//api/client/event:go_default_library", "//config/fieldparams:go_default_library", "//config/proposer:go_default_library", "//consensus-types/primitives:go_default_library", diff --git a/validator/client/iface/node_client.go b/validator/client/iface/node_client.go index 71dfee1e3..710c74bf2 100644 --- a/validator/client/iface/node_client.go +++ b/validator/client/iface/node_client.go @@ -4,6 +4,7 @@ import ( "context" "github.com/golang/protobuf/ptypes/empty" + "github.com/prysmaticlabs/prysm/v5/api/client/beacon" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" ) @@ -12,5 +13,5 @@ type NodeClient interface { GetGenesis(ctx context.Context, in *empty.Empty) (*ethpb.Genesis, error) GetVersion(ctx context.Context, in *empty.Empty) (*ethpb.Version, error) ListPeers(ctx context.Context, in *empty.Empty) (*ethpb.Peers, error) - IsHealthy(ctx context.Context) bool + HealthTracker() *beacon.NodeHealthTracker } diff --git a/validator/client/iface/validator.go b/validator/client/iface/validator.go index b51fb76f0..761628dbb 100644 --- a/validator/client/iface/validator.go +++ b/validator/client/iface/validator.go @@ -2,9 +2,10 @@ package iface import ( "context" - "errors" "time" + "github.com/prysmaticlabs/prysm/v5/api/client/beacon" + "github.com/prysmaticlabs/prysm/v5/api/client/event" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/config/proposer" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" @@ -14,9 +15,6 @@ import ( "github.com/prysmaticlabs/prysm/v5/validator/keymanager" ) -// ErrConnectionIssue represents a connection problem. -var ErrConnectionIssue = errors.New("could not connect") - // ValidatorRole defines the validator role. type ValidatorRole int8 @@ -57,16 +55,16 @@ type Validator interface { UpdateDomainDataCaches(ctx context.Context, slot primitives.Slot) WaitForKeymanagerInitialization(ctx context.Context) error Keymanager() (keymanager.IKeymanager, error) - ReceiveSlots(ctx context.Context, connectionErrorChannel chan<- error) HandleKeyReload(ctx context.Context, currentKeys [][fieldparams.BLSPubkeyLength]byte) (bool, error) CheckDoppelGanger(ctx context.Context) error PushProposerSettings(ctx context.Context, km keymanager.IKeymanager, slot primitives.Slot, deadline time.Time) error SignValidatorRegistrationRequest(ctx context.Context, signer SigningFunc, newValidatorRegistration *ethpb.ValidatorRegistrationV1) (*ethpb.SignedValidatorRegistrationV1, error) + StartEventStream(ctx context.Context, topics []string, eventsChan chan<- *event.Event) + ProcessEvent(event *event.Event) ProposerSettings() *proposer.Settings SetProposerSettings(context.Context, *proposer.Settings) error - StartEventStream(ctx context.Context) error EventStreamIsRunning() bool - NodeIsHealthy(ctx context.Context) bool + HealthTracker() *beacon.NodeHealthTracker } // SigningFunc interface defines a type for the a function that signs a message diff --git a/validator/client/iface/validator_client.go b/validator/client/iface/validator_client.go index dc3d4f504..13cf67c4d 100644 --- a/validator/client/iface/validator_client.go +++ b/validator/client/iface/validator_client.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/golang/protobuf/ptypes/empty" + "github.com/prysmaticlabs/prysm/v5/api/client/event" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" ) @@ -144,9 +145,8 @@ type ValidatorClient interface { GetSyncSubcommitteeIndex(ctx context.Context, in *ethpb.SyncSubcommitteeIndexRequest) (*ethpb.SyncSubcommitteeIndexResponse, error) GetSyncCommitteeContribution(ctx context.Context, in *ethpb.SyncCommitteeContributionRequest) (*ethpb.SyncCommitteeContribution, error) SubmitSignedContributionAndProof(ctx context.Context, in *ethpb.SignedContributionAndProof) (*empty.Empty, error) - StreamSlots(ctx context.Context, in *ethpb.StreamSlotsRequest) (ethpb.BeaconNodeValidator_StreamSlotsClient, error) SubmitValidatorRegistrations(ctx context.Context, in *ethpb.SignedValidatorRegistrationsV1) (*empty.Empty, error) - StartEventStream(ctx context.Context) error + StartEventStream(ctx context.Context, topics []string, eventsChannel chan<- *event.Event) EventStreamIsRunning() bool GetAggregatedSelections(ctx context.Context, selections []BeaconCommitteeSelection) ([]BeaconCommitteeSelection, error) GetAggregatedSyncSelections(ctx context.Context, selections []SyncCommitteeSelection) ([]SyncCommitteeSelection, error) diff --git a/validator/client/runner.go b/validator/client/runner.go index c561c1411..71a7a4729 100644 --- a/validator/client/runner.go +++ b/validator/client/runner.go @@ -7,7 +7,8 @@ import ( "time" "github.com/pkg/errors" - "github.com/prysmaticlabs/prysm/v5/config/features" + "github.com/prysmaticlabs/prysm/v5/api/client" + "github.com/prysmaticlabs/prysm/v5/api/client/event" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" @@ -40,12 +41,12 @@ func run(ctx context.Context, v iface.Validator) { if err != nil { return // Exit if context is canceled. } - - connectionErrorChannel := make(chan error, 1) - go v.ReceiveSlots(ctx, connectionErrorChannel) if err := v.UpdateDuties(ctx, headSlot); err != nil { handleAssignmentError(err, headSlot) } + eventsChan := make(chan *event.Event, 1) + healthTracker := v.HealthTracker() + runHealthCheckRoutine(ctx, v, eventsChan) accountsChangedChan := make(chan [][fieldparams.BLSPubkeyLength]byte, 1) km, err := v.Keymanager() @@ -76,15 +77,10 @@ func run(ctx context.Context, v iface.Validator) { sub.Unsubscribe() close(accountsChangedChan) return // Exit if context is canceled. - case slotsError := <-connectionErrorChannel: - if slotsError != nil { - log.WithError(slotsError).Warn("slots stream interrupted") - go v.ReceiveSlots(ctx, connectionErrorChannel) + case slot := <-v.NextSlot(): + if !healthTracker.IsHealthy() { continue } - case currentKeys := <-accountsChangedChan: - onAccountsChanged(ctx, v, currentKeys, accountsChangedChan) - case slot := <-v.NextSlot(): span.AddAttributes(trace.Int64Attribute("slot", int64(slot))) // lint:ignore uintcast -- This conversion is OK for tracing. deadline := v.SlotDeadline(slot) @@ -128,6 +124,22 @@ func run(ctx context.Context, v iface.Validator) { continue } performRoles(slotCtx, allRoles, v, slot, &wg, span) + case isHealthyAgain := <-healthTracker.HealthUpdates(): + if isHealthyAgain { + headSlot, err = initializeValidatorAndGetHeadSlot(ctx, v) + if err != nil { + log.WithError(err).Error("Failed to re initialize validator and get head slot") + continue + } + if err := v.UpdateDuties(ctx, headSlot); err != nil { + handleAssignmentError(err, headSlot) + continue + } + } + case e := <-eventsChan: + v.ProcessEvent(e) + case currentKeys := <-accountsChangedChan: // should be less of a priority than next slot + onAccountsChanged(ctx, v, currentKeys, accountsChangedChan) } } } @@ -196,13 +208,6 @@ func initializeValidatorAndGetHeadSlot(ctx context.Context, v iface.Validator) ( log.WithError(err).Fatal("Could not wait for validator activation") } - if features.Get().EnableBeaconRESTApi { - if err = v.StartEventStream(ctx); err != nil { - log.WithError(err).Fatal("Could not start API event stream") - } - runHealthCheckRoutine(ctx, v) - } - headSlot, err = v.CanonicalHeadSlot(ctx) if isConnectionError(err) { log.WithError(err).Warn("Could not get current canonical head slot") @@ -273,7 +278,7 @@ func performRoles(slotCtx context.Context, allRoles map[[48]byte][]iface.Validat } func isConnectionError(err error) bool { - return err != nil && errors.Is(err, iface.ErrConnectionIssue) + return err != nil && errors.Is(err, client.ErrConnectionIssue) } func handleAssignmentError(err error, slot primitives.Slot) { @@ -288,24 +293,23 @@ func handleAssignmentError(err error, slot primitives.Slot) { } } -func runHealthCheckRoutine(ctx context.Context, v iface.Validator) { +func runHealthCheckRoutine(ctx context.Context, v iface.Validator, eventsChan chan<- *event.Event) { + log.Info("Starting health check routine for beacon node apis") healthCheckTicker := time.NewTicker(time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Second) + tracker := v.HealthTracker() go func() { - for { - select { - case <-healthCheckTicker.C: - if v.NodeIsHealthy(ctx) && !v.EventStreamIsRunning() { - if err := v.StartEventStream(ctx); err != nil { - log.WithError(err).Error("Could not start API event stream") - } - } - case <-ctx.Done(): - if ctx.Err() != nil { - log.WithError(ctx.Err()).Error("Context cancelled") - } - log.Error("Context cancelled") + // trigger the healthcheck immediately the first time + for ; true; <-healthCheckTicker.C { + if ctx.Err() != nil { + log.WithError(ctx.Err()).Error("Context cancelled") return } + isHealthy := tracker.CheckHealth(ctx) + // in case of node returning healthy but event stream died + if isHealthy && !v.EventStreamIsRunning() { + log.Info("Event stream reconnecting...") + go v.StartEventStream(ctx, event.DefaultEventTopics, eventsChan) + } } }() } diff --git a/validator/client/runner_test.go b/validator/client/runner_test.go index d4faced80..b617cb033 100644 --- a/validator/client/runner_test.go +++ b/validator/client/runner_test.go @@ -8,6 +8,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/api/client/beacon" + healthTesting "github.com/prysmaticlabs/prysm/v5/api/client/beacon/testing" "github.com/prysmaticlabs/prysm/v5/async/event" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/config/params" @@ -18,6 +20,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/validator/client/iface" "github.com/prysmaticlabs/prysm/v5/validator/client/testutil" logTest "github.com/sirupsen/logrus/hooks/test" + "go.uber.org/mock/gomock" ) func cancelledContext() context.Context { @@ -27,21 +30,41 @@ func cancelledContext() context.Context { } func TestCancelledContext_CleansUpValidator(t *testing.T) { - v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}} + ctrl := gomock.NewController(t) + defer ctrl.Finish() + node := healthTesting.NewMockHealthClient(ctrl) + tracker := beacon.NewNodeHealthTracker(node) + v := &testutil.FakeValidator{ + Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}, + Tracker: tracker, + } run(cancelledContext(), v) assert.Equal(t, true, v.DoneCalled, "Expected Done() to be called") } func TestCancelledContext_WaitsForChainStart(t *testing.T) { - v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}} + ctrl := gomock.NewController(t) + defer ctrl.Finish() + node := healthTesting.NewMockHealthClient(ctrl) + tracker := beacon.NewNodeHealthTracker(node) + v := &testutil.FakeValidator{ + Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}, + Tracker: tracker, + } run(cancelledContext(), v) assert.Equal(t, 1, v.WaitForChainStartCalled, "Expected WaitForChainStart() to be called") } func TestRetry_On_ConnectionError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + node := healthTesting.NewMockHealthClient(ctrl) + tracker := beacon.NewNodeHealthTracker(node) retry := 10 + node.EXPECT().IsHealthy(gomock.Any()).Return(true) v := &testutil.FakeValidator{ Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}, + Tracker: tracker, RetryTillSuccess: retry, } backOffPeriod = 10 * time.Millisecond @@ -55,18 +78,31 @@ func TestRetry_On_ConnectionError(t *testing.T) { assert.Equal(t, retry*3, v.WaitForChainStartCalled, "Expected WaitForChainStart() to be called") assert.Equal(t, retry*2, v.WaitForSyncCalled, "Expected WaitForSync() to be called") assert.Equal(t, retry, v.WaitForActivationCalled, "Expected WaitForActivation() to be called") - assert.Equal(t, retry, v.CanonicalHeadSlotCalled, "Expected WaitForActivation() to be called") - assert.Equal(t, retry, v.ReceiveBlocksCalled, "Expected WaitForActivation() to be called") + assert.Equal(t, retry, v.CanonicalHeadSlotCalled, "Expected CanonicalHeadSlotCalled() to be called") } func TestCancelledContext_WaitsForActivation(t *testing.T) { - v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}} + ctrl := gomock.NewController(t) + defer ctrl.Finish() + node := healthTesting.NewMockHealthClient(ctrl) + tracker := beacon.NewNodeHealthTracker(node) + v := &testutil.FakeValidator{ + Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}, + Tracker: tracker, + } run(cancelledContext(), v) assert.Equal(t, 1, v.WaitForActivationCalled, "Expected WaitForActivation() to be called") } func TestUpdateDuties_NextSlot(t *testing.T) { - v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}} + ctrl := gomock.NewController(t) + defer ctrl.Finish() + node := healthTesting.NewMockHealthClient(ctrl) + tracker := beacon.NewNodeHealthTracker(node) + node.EXPECT().IsHealthy(gomock.Any()).Return(true).AnyTimes() + // avoid race condition between the cancellation of the context in the go stream from slot and the setting of IsHealthy + _ = tracker.CheckHealth(context.Background()) + v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}, Tracker: tracker} ctx, cancel := context.WithCancel(context.Background()) slot := primitives.Slot(55) @@ -86,7 +122,14 @@ func TestUpdateDuties_NextSlot(t *testing.T) { func TestUpdateDuties_HandlesError(t *testing.T) { hook := logTest.NewGlobal() - v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}} + ctrl := gomock.NewController(t) + defer ctrl.Finish() + node := healthTesting.NewMockHealthClient(ctrl) + tracker := beacon.NewNodeHealthTracker(node) + node.EXPECT().IsHealthy(gomock.Any()).Return(true).AnyTimes() + // avoid race condition between the cancellation of the context in the go stream from slot and the setting of IsHealthy + _ = tracker.CheckHealth(context.Background()) + v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}, Tracker: tracker} ctx, cancel := context.WithCancel(context.Background()) slot := primitives.Slot(55) @@ -105,7 +148,14 @@ func TestUpdateDuties_HandlesError(t *testing.T) { } func TestRoleAt_NextSlot(t *testing.T) { - v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}} + ctrl := gomock.NewController(t) + defer ctrl.Finish() + node := healthTesting.NewMockHealthClient(ctrl) + tracker := beacon.NewNodeHealthTracker(node) + node.EXPECT().IsHealthy(gomock.Any()).Return(true).AnyTimes() + // avoid race condition between the cancellation of the context in the go stream from slot and the setting of IsHealthy + _ = tracker.CheckHealth(context.Background()) + v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}, Tracker: tracker} ctx, cancel := context.WithCancel(context.Background()) slot := primitives.Slot(55) @@ -124,7 +174,14 @@ func TestRoleAt_NextSlot(t *testing.T) { } func TestAttests_NextSlot(t *testing.T) { - v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}} + ctrl := gomock.NewController(t) + defer ctrl.Finish() + node := healthTesting.NewMockHealthClient(ctrl) + tracker := beacon.NewNodeHealthTracker(node) + node.EXPECT().IsHealthy(gomock.Any()).Return(true).AnyTimes() + // avoid race condition between the cancellation of the context in the go stream from slot and the setting of IsHealthy + _ = tracker.CheckHealth(context.Background()) + v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}, Tracker: tracker} ctx, cancel := context.WithCancel(context.Background()) slot := primitives.Slot(55) @@ -144,7 +201,14 @@ func TestAttests_NextSlot(t *testing.T) { } func TestProposes_NextSlot(t *testing.T) { - v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}} + ctrl := gomock.NewController(t) + defer ctrl.Finish() + node := healthTesting.NewMockHealthClient(ctrl) + tracker := beacon.NewNodeHealthTracker(node) + node.EXPECT().IsHealthy(gomock.Any()).Return(true).AnyTimes() + // avoid race condition between the cancellation of the context in the go stream from slot and the setting of IsHealthy + _ = tracker.CheckHealth(context.Background()) + v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}, Tracker: tracker} ctx, cancel := context.WithCancel(context.Background()) slot := primitives.Slot(55) @@ -164,7 +228,14 @@ func TestProposes_NextSlot(t *testing.T) { } func TestBothProposesAndAttests_NextSlot(t *testing.T) { - v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}} + ctrl := gomock.NewController(t) + defer ctrl.Finish() + node := healthTesting.NewMockHealthClient(ctrl) + tracker := beacon.NewNodeHealthTracker(node) + node.EXPECT().IsHealthy(gomock.Any()).Return(true).AnyTimes() + // avoid race condition between the cancellation of the context in the go stream from slot and the setting of IsHealthy + _ = tracker.CheckHealth(context.Background()) + v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}, Tracker: tracker} ctx, cancel := context.WithCancel(context.Background()) slot := primitives.Slot(55) @@ -188,7 +259,12 @@ func TestBothProposesAndAttests_NextSlot(t *testing.T) { func TestKeyReload_ActiveKey(t *testing.T) { ctx := context.Background() km := &mockKeymanager{} - v := &testutil.FakeValidator{Km: km} + ctrl := gomock.NewController(t) + defer ctrl.Finish() + node := healthTesting.NewMockHealthClient(ctrl) + tracker := beacon.NewNodeHealthTracker(node) + node.EXPECT().IsHealthy(gomock.Any()).Return(true).AnyTimes() + v := &testutil.FakeValidator{Km: km, Tracker: tracker} ac := make(chan [][fieldparams.BLSPubkeyLength]byte) current := [][fieldparams.BLSPubkeyLength]byte{testutil.ActiveKey} onAccountsChanged(ctx, v, current, ac) @@ -202,7 +278,12 @@ func TestKeyReload_NoActiveKey(t *testing.T) { na := notActive(t) ctx := context.Background() km := &mockKeymanager{} - v := &testutil.FakeValidator{Km: km} + ctrl := gomock.NewController(t) + defer ctrl.Finish() + node := healthTesting.NewMockHealthClient(ctrl) + tracker := beacon.NewNodeHealthTracker(node) + node.EXPECT().IsHealthy(gomock.Any()).Return(true).AnyTimes() + v := &testutil.FakeValidator{Km: km, Tracker: tracker} ac := make(chan [][fieldparams.BLSPubkeyLength]byte) current := [][fieldparams.BLSPubkeyLength]byte{na} onAccountsChanged(ctx, v, current, ac) @@ -224,7 +305,12 @@ func notActive(t *testing.T) [fieldparams.BLSPubkeyLength]byte { } func TestUpdateProposerSettingsAt_EpochStart(t *testing.T) { - v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}} + ctrl := gomock.NewController(t) + defer ctrl.Finish() + node := healthTesting.NewMockHealthClient(ctrl) + tracker := beacon.NewNodeHealthTracker(node) + node.EXPECT().IsHealthy(gomock.Any()).Return(true).AnyTimes() + v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}, Tracker: tracker} err := v.SetProposerSettings(context.Background(), &proposer.Settings{ DefaultConfig: &proposer.Option{ FeeRecipientConfig: &proposer.FeeRecipientConfig{ @@ -249,7 +335,16 @@ func TestUpdateProposerSettingsAt_EpochStart(t *testing.T) { } func TestUpdateProposerSettingsAt_EpochEndOk(t *testing.T) { - v := &testutil.FakeValidator{Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}, ProposerSettingWait: time.Duration(params.BeaconConfig().SecondsPerSlot-1) * time.Second} + ctrl := gomock.NewController(t) + defer ctrl.Finish() + node := healthTesting.NewMockHealthClient(ctrl) + tracker := beacon.NewNodeHealthTracker(node) + node.EXPECT().IsHealthy(gomock.Any()).Return(true).AnyTimes() + v := &testutil.FakeValidator{ + Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}, + ProposerSettingWait: time.Duration(params.BeaconConfig().SecondsPerSlot-1) * time.Second, + Tracker: tracker, + } err := v.SetProposerSettings(context.Background(), &proposer.Settings{ DefaultConfig: &proposer.Option{ FeeRecipientConfig: &proposer.FeeRecipientConfig{ @@ -275,9 +370,15 @@ func TestUpdateProposerSettingsAt_EpochEndOk(t *testing.T) { func TestUpdateProposerSettings_ContinuesAfterValidatorRegistrationFails(t *testing.T) { errSomeotherError := errors.New("some internal error") + ctrl := gomock.NewController(t) + defer ctrl.Finish() + node := healthTesting.NewMockHealthClient(ctrl) + tracker := beacon.NewNodeHealthTracker(node) + node.EXPECT().IsHealthy(gomock.Any()).Return(true).AnyTimes() v := &testutil.FakeValidator{ ProposerSettingsErr: errors.Wrap(ErrBuilderValidatorRegistration, errSomeotherError.Error()), Km: &mockKeymanager{accountsChangedFeed: &event.Feed{}}, + Tracker: tracker, } err := v.SetProposerSettings(context.Background(), &proposer.Settings{ DefaultConfig: &proposer.Option{ diff --git a/validator/client/service.go b/validator/client/service.go index 9c8540c70..abc69949a 100644 --- a/validator/client/service.go +++ b/validator/client/service.go @@ -194,14 +194,12 @@ func (v *ValidatorService) Start() { return } - restHandler := &beaconApi.BeaconApiJsonRestHandler{ - HttpClient: http.Client{Timeout: v.conn.GetBeaconApiTimeout()}, - Host: v.conn.GetBeaconApiUrl(), - } + restHandler := beaconApi.NewBeaconApiJsonRestHandler( + http.Client{Timeout: v.conn.GetBeaconApiTimeout()}, + v.conn.GetBeaconApiUrl(), + ) - evHandler := beaconApi.NewEventHandler(http.DefaultClient, v.conn.GetBeaconApiUrl()) - opts := []beaconApi.ValidatorClientOpt{beaconApi.WithEventHandler(evHandler)} - validatorClient := validatorClientFactory.NewValidatorClient(v.conn, restHandler, opts...) + validatorClient := validatorClientFactory.NewValidatorClient(v.conn, restHandler) valStruct := &validator{ validatorClient: validatorClient, diff --git a/validator/client/testutil/BUILD.bazel b/validator/client/testutil/BUILD.bazel index a565abb7a..633d7c976 100644 --- a/validator/client/testutil/BUILD.bazel +++ b/validator/client/testutil/BUILD.bazel @@ -10,6 +10,9 @@ go_library( importpath = "github.com/prysmaticlabs/prysm/v5/validator/client/testutil", visibility = ["//validator:__subpackages__"], deps = [ + "//api/client:go_default_library", + "//api/client/beacon:go_default_library", + "//api/client/event:go_default_library", "//config/fieldparams:go_default_library", "//config/proposer:go_default_library", "//consensus-types/primitives:go_default_library", diff --git a/validator/client/testutil/mock_validator.go b/validator/client/testutil/mock_validator.go index ce397db98..25c90cc93 100644 --- a/validator/client/testutil/mock_validator.go +++ b/validator/client/testutil/mock_validator.go @@ -5,6 +5,9 @@ import ( "context" "time" + api "github.com/prysmaticlabs/prysm/v5/api/client" + "github.com/prysmaticlabs/prysm/v5/api/client/beacon" + "github.com/prysmaticlabs/prysm/v5/api/client/event" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/config/proposer" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" @@ -55,6 +58,7 @@ type FakeValidator struct { proposerSettings *proposer.Settings ProposerSettingWait time.Duration Km keymanager.IKeymanager + Tracker *beacon.NodeHealthTracker } // Done for mocking. @@ -75,7 +79,7 @@ func (fv *FakeValidator) LogSubmittedSyncCommitteeMessages() {} func (fv *FakeValidator) WaitForChainStart(_ context.Context) error { fv.WaitForChainStartCalled++ if fv.RetryTillSuccess >= fv.WaitForChainStartCalled { - return iface.ErrConnectionIssue + return api.ErrConnectionIssue } return nil } @@ -87,7 +91,7 @@ func (fv *FakeValidator) WaitForActivation(_ context.Context, accountChan chan [ return nil } if fv.RetryTillSuccess >= fv.WaitForActivationCalled { - return iface.ErrConnectionIssue + return api.ErrConnectionIssue } return nil } @@ -96,7 +100,7 @@ func (fv *FakeValidator) WaitForActivation(_ context.Context, accountChan chan [ func (fv *FakeValidator) WaitForSync(_ context.Context) error { fv.WaitForSyncCalled++ if fv.RetryTillSuccess >= fv.WaitForSyncCalled { - return iface.ErrConnectionIssue + return api.ErrConnectionIssue } return nil } @@ -111,7 +115,7 @@ func (fv *FakeValidator) SlasherReady(_ context.Context) error { func (fv *FakeValidator) CanonicalHeadSlot(_ context.Context) (primitives.Slot, error) { fv.CanonicalHeadSlotCalled++ if fv.RetryTillSuccess > fv.CanonicalHeadSlotCalled { - return 0, iface.ErrConnectionIssue + return 0, api.ErrConnectionIssue } return 0, nil } @@ -217,14 +221,6 @@ func (*FakeValidator) CheckDoppelGanger(_ context.Context) error { return nil } -// ReceiveSlots for mocking -func (fv *FakeValidator) ReceiveSlots(_ context.Context, connectionErrorChannel chan<- error) { - fv.ReceiveBlocksCalled++ - if fv.RetryTillSuccess > fv.ReceiveBlocksCalled { - connectionErrorChannel <- iface.ErrConnectionIssue - } -} - // HandleKeyReload for mocking func (fv *FakeValidator) HandleKeyReload(_ context.Context, newKeys [][fieldparams.BLSPubkeyLength]byte) (anyActive bool, err error) { fv.HandleKeyReloadCalled = true @@ -286,14 +282,15 @@ func (fv *FakeValidator) SetProposerSettings(_ context.Context, settings *propos return nil } -func (fv *FakeValidator) StartEventStream(_ context.Context) error { - return nil +func (*FakeValidator) StartEventStream(_ context.Context, _ []string, _ chan<- *event.Event) { } -func (fv *FakeValidator) EventStreamIsRunning() bool { +func (*FakeValidator) ProcessEvent(_ *event.Event) {} + +func (*FakeValidator) EventStreamIsRunning() bool { return true } -func (fv *FakeValidator) NodeIsHealthy(context.Context) bool { - return true +func (fv *FakeValidator) HealthTracker() *beacon.NodeHealthTracker { + return fv.Tracker } diff --git a/validator/client/validator.go b/validator/client/validator.go index 144cafede..83a0c431c 100644 --- a/validator/client/validator.go +++ b/validator/client/validator.go @@ -7,6 +7,7 @@ import ( "context" "encoding/binary" "encoding/hex" + "encoding/json" "fmt" "io" "math" @@ -20,6 +21,10 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" lru "github.com/hashicorp/golang-lru" "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/api/client" + "github.com/prysmaticlabs/prysm/v5/api/client/beacon" + eventClient "github.com/prysmaticlabs/prysm/v5/api/client/event" + "github.com/prysmaticlabs/prysm/v5/api/server/structs" "github.com/prysmaticlabs/prysm/v5/async/event" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/altair" "github.com/prysmaticlabs/prysm/v5/cmd" @@ -248,7 +253,7 @@ func (v *validator) WaitForChainStart(ctx context.Context) error { chainStartRes, err := v.validatorClient.WaitForChainStart(ctx, &emptypb.Empty{}) if err == io.EOF { - return iface.ErrConnectionIssue + return client.ErrConnectionIssue } if ctx.Err() == context.Canceled { @@ -257,7 +262,7 @@ func (v *validator) WaitForChainStart(ctx context.Context) error { if err != nil { return errors.Wrap( - iface.ErrConnectionIssue, + client.ErrConnectionIssue, errors.Wrap(err, "could not receive ChainStart from stream").Error(), ) } @@ -310,7 +315,7 @@ func (v *validator) WaitForSync(ctx context.Context) error { s, err := v.nodeClient.GetSyncStatus(ctx, &emptypb.Empty{}) if err != nil { - return errors.Wrap(iface.ErrConnectionIssue, errors.Wrap(err, "could not get sync status").Error()) + return errors.Wrap(client.ErrConnectionIssue, errors.Wrap(err, "could not get sync status").Error()) } if !s.Syncing { return nil @@ -322,7 +327,7 @@ func (v *validator) WaitForSync(ctx context.Context) error { case <-time.After(slots.DivideSlotBy(2 /* twice per slot */)): s, err := v.nodeClient.GetSyncStatus(ctx, &emptypb.Empty{}) if err != nil { - return errors.Wrap(iface.ErrConnectionIssue, errors.Wrap(err, "could not get sync status").Error()) + return errors.Wrap(client.ErrConnectionIssue, errors.Wrap(err, "could not get sync status").Error()) } if !s.Syncing { return nil @@ -334,35 +339,6 @@ func (v *validator) WaitForSync(ctx context.Context) error { } } -// ReceiveSlots starts a stream listener to obtain -// slots from the beacon node when it imports a block. Upon receiving a slot, the service -// broadcasts it to a feed for other usages to subscribe to. -func (v *validator) ReceiveSlots(ctx context.Context, connectionErrorChannel chan<- error) { - stream, err := v.validatorClient.StreamSlots(ctx, ðpb.StreamSlotsRequest{VerifiedOnly: true}) - if err != nil { - log.WithError(err).Error("Failed to retrieve slots stream, " + iface.ErrConnectionIssue.Error()) - connectionErrorChannel <- errors.Wrap(iface.ErrConnectionIssue, err.Error()) - return - } - - for { - if ctx.Err() == context.Canceled { - log.WithError(ctx.Err()).Error("Context canceled - shutting down slots receiver") - return - } - res, err := stream.Recv() - if err != nil { - log.WithError(err).Error("Could not receive slots from beacon node: " + iface.ErrConnectionIssue.Error()) - connectionErrorChannel <- errors.Wrap(iface.ErrConnectionIssue, err.Error()) - return - } - if res == nil { - continue - } - v.setHighestSlot(res.Slot) - } -} - func (v *validator) checkAndLogValidatorStatus(statuses []*validatorStatus, activeValCount int64) bool { nonexistentIndex := primitives.ValidatorIndex(^uint64(0)) var validatorActivated bool @@ -429,7 +405,7 @@ func (v *validator) CanonicalHeadSlot(ctx context.Context) (primitives.Slot, err defer span.End() head, err := v.beaconClient.GetChainHead(ctx, &emptypb.Empty{}) if err != nil { - return 0, errors.Wrap(iface.ErrConnectionIssue, err.Error()) + return 0, errors.Wrap(client.ErrConnectionIssue, err.Error()) } return head.HeadSlot, nil } @@ -1092,16 +1068,43 @@ func (v *validator) PushProposerSettings(ctx context.Context, km keymanager.IKey return nil } -func (v *validator) StartEventStream(ctx context.Context) error { - return v.validatorClient.StartEventStream(ctx) +func (v *validator) StartEventStream(ctx context.Context, topics []string, eventsChannel chan<- *eventClient.Event) { + log.WithField("topics", topics).Info("Starting event stream") + v.validatorClient.StartEventStream(ctx, topics, eventsChannel) +} + +func (v *validator) ProcessEvent(event *eventClient.Event) { + if event == nil || event.Data == nil { + log.Warn("Received empty event") + } + switch event.EventType { + case eventClient.EventError: + log.Error(string(event.Data)) + case eventClient.EventConnectionError: + log.WithError(errors.New(string(event.Data))).Error("Event stream interrupted") + case eventClient.EventHead: + log.Debug("Received head event") + head := &structs.HeadEvent{} + if err := json.Unmarshal(event.Data, head); err != nil { + log.WithError(err).Error("Failed to unmarshal head Event into JSON") + } + uintSlot, err := strconv.ParseUint(head.Slot, 10, 64) + if err != nil { + log.WithError(err).Error("Failed to parse slot") + } + v.setHighestSlot(primitives.Slot(uintSlot)) + default: + // just keep going and log the error + log.WithField("type", event.EventType).WithField("data", string(event.Data)).Warn("Received an unknown event") + } } func (v *validator) EventStreamIsRunning() bool { return v.validatorClient.EventStreamIsRunning() } -func (v *validator) NodeIsHealthy(ctx context.Context) bool { - return v.nodeClient.IsHealthy(ctx) +func (v *validator) HealthTracker() *beacon.NodeHealthTracker { + return v.nodeClient.HealthTracker() } func (v *validator) filterAndCacheActiveKeys(ctx context.Context, pubkeys [][fieldparams.BLSPubkeyLength]byte, slot primitives.Slot) ([][fieldparams.BLSPubkeyLength]byte, error) { diff --git a/validator/client/validator_test.go b/validator/client/validator_test.go index 6659fc8d3..3bdb48003 100644 --- a/validator/client/validator_test.go +++ b/validator/client/validator_test.go @@ -943,33 +943,6 @@ func TestCheckAndLogValidatorStatus_OK(t *testing.T) { } } -func TestService_ReceiveSlots_SetHighest(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - client := validatormock.NewMockValidatorClient(ctrl) - - v := validator{ - validatorClient: client, - slotFeed: new(event.Feed), - } - stream := mock2.NewMockBeaconNodeValidator_StreamSlotsClient(ctrl) - ctx, cancel := context.WithCancel(context.Background()) - client.EXPECT().StreamSlots( - gomock.Any(), - ðpb.StreamSlotsRequest{VerifiedOnly: true}, - ).Return(stream, nil) - stream.EXPECT().Context().Return(ctx).AnyTimes() - stream.EXPECT().Recv().Return( - ðpb.StreamSlotsResponse{Slot: 123}, - nil, - ).Do(func() { - cancel() - }) - connectionErrorChannel := make(chan error) - v.ReceiveSlots(ctx, connectionErrorChannel) - require.Equal(t, primitives.Slot(123), v.highestValidSlot) -} - type doppelGangerRequestMatcher struct { req *ethpb.DoppelGangerRequest } diff --git a/validator/rpc/beacon.go b/validator/rpc/beacon.go index c165e1a92..7355c93bd 100644 --- a/validator/rpc/beacon.go +++ b/validator/rpc/beacon.go @@ -54,10 +54,8 @@ func (s *Server) registerBeaconClient() error { s.beaconApiTimeout, ) - restHandler := &beaconApi.BeaconApiJsonRestHandler{ - HttpClient: http.Client{Timeout: s.beaconApiTimeout}, - Host: s.beaconApiEndpoint, - } + restHandler := beaconApi.NewBeaconApiJsonRestHandler(http.Client{Timeout: s.beaconApiTimeout}, s.beaconApiEndpoint) + s.beaconChainClient = beaconChainClientFactory.NewBeaconChainClient(conn, restHandler) s.beaconNodeClient = nodeClientFactory.NewNodeClient(conn, restHandler) s.beaconNodeValidatorClient = validatorClientFactory.NewValidatorClient(conn, restHandler)