mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-20 18:58:11 -05:00
Compare commits
43 Commits
use-delay
...
gRPC-fallb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bfea69aed | ||
|
|
7cc6ded31a | ||
|
|
5fd3300fdb | ||
|
|
f74a9cb3ec | ||
|
|
3d903d5d75 | ||
|
|
5f335b1b58 | ||
|
|
1f7f7c6833 | ||
|
|
4e952354d1 | ||
|
|
5268da43f1 | ||
|
|
a6fc327cfb | ||
|
|
cf04b457a6 | ||
|
|
4586b0accf | ||
|
|
3a71ad2ec1 | ||
|
|
588766e520 | ||
|
|
ecc19bc6ed | ||
|
|
164d2d50fd | ||
|
|
a135a336c3 | ||
|
|
5f189f002e | ||
|
|
1355a9ff4d | ||
|
|
34478f30c8 | ||
|
|
214b4428e6 | ||
|
|
bca6166e82 | ||
|
|
b6818853b4 | ||
|
|
5a56bfcf98 | ||
|
|
db82f3cc9d | ||
|
|
9c874037d1 | ||
|
|
9bb231fb3b | ||
|
|
6432140603 | ||
|
|
5e0a9ff992 | ||
|
|
0bfd661baf | ||
|
|
b21acc0bbb | ||
|
|
f6f65987c6 | ||
|
|
d26cdd74ee | ||
|
|
d1905cb018 | ||
|
|
9f828bdd88 | ||
|
|
17413b52ed | ||
|
|
a651e7f0ac | ||
|
|
3e1cb45e92 | ||
|
|
fc2dcb0e88 | ||
|
|
888db581dd | ||
|
|
f1d2ee72e2 | ||
|
|
31f18b9f60 | ||
|
|
6462c997e9 |
@@ -3,13 +3,16 @@ load("@prysm//tools/go:def.bzl", "go_library", "go_test")
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"grpc_connection_provider.go",
|
||||
"grpcutils.go",
|
||||
"log.go",
|
||||
"mock_grpc_provider.go",
|
||||
"parameters.go",
|
||||
],
|
||||
importpath = "github.com/OffchainLabs/prysm/v7/api/grpc",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"@com_github_pkg_errors//:go_default_library",
|
||||
"@com_github_sirupsen_logrus//:go_default_library",
|
||||
"@org_golang_google_grpc//:go_default_library",
|
||||
"@org_golang_google_grpc//metadata:go_default_library",
|
||||
@@ -18,12 +21,17 @@ go_library(
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["grpcutils_test.go"],
|
||||
srcs = [
|
||||
"grpc_connection_provider_test.go",
|
||||
"grpcutils_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//testing/assert:go_default_library",
|
||||
"//testing/require:go_default_library",
|
||||
"@com_github_sirupsen_logrus//hooks/test:go_default_library",
|
||||
"@org_golang_google_grpc//:go_default_library",
|
||||
"@org_golang_google_grpc//credentials/insecure:go_default_library",
|
||||
"@org_golang_google_grpc//metadata:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
172
api/grpc/grpc_connection_provider.go
Normal file
172
api/grpc/grpc_connection_provider.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
pkgErrors "github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// GrpcConnectionProvider manages gRPC connections for failover support.
|
||||
// It allows switching between different beacon node endpoints when the current one becomes unavailable.
|
||||
// Only one connection is maintained at a time - when switching hosts, the old connection is closed.
|
||||
type GrpcConnectionProvider interface {
|
||||
// CurrentConn returns the currently active gRPC connection.
|
||||
// The connection is created lazily on first call.
|
||||
// Returns nil if the provider has been closed.
|
||||
CurrentConn() *grpc.ClientConn
|
||||
// CurrentHost returns the address of the currently active endpoint.
|
||||
CurrentHost() string
|
||||
// Hosts returns all configured endpoint addresses.
|
||||
Hosts() []string
|
||||
// SetHost switches to the endpoint at the given index.
|
||||
// The new connection is created lazily on next CurrentConn() call.
|
||||
SetHost(index int) error
|
||||
// Close closes the current connection.
|
||||
Close() error
|
||||
}
|
||||
|
||||
type grpcConnectionProvider struct {
|
||||
// Immutable after construction - no lock needed for reads
|
||||
endpoints []string
|
||||
ctx context.Context
|
||||
dialOpts []grpc.DialOption
|
||||
|
||||
// Current connection state (protected by mu)
|
||||
currentIndex uint64
|
||||
conn *grpc.ClientConn
|
||||
|
||||
mu sync.Mutex
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
// NewGrpcConnectionProvider creates a new connection provider that manages gRPC connections.
|
||||
// The endpoint parameter can be a comma-separated list of addresses (e.g., "host1:4000,host2:4000").
|
||||
// Only one connection is maintained at a time, created lazily on first use.
|
||||
func NewGrpcConnectionProvider(
|
||||
ctx context.Context,
|
||||
endpoint string,
|
||||
dialOpts []grpc.DialOption,
|
||||
) (GrpcConnectionProvider, error) {
|
||||
endpoints := parseEndpoints(endpoint)
|
||||
if len(endpoints) == 0 {
|
||||
return nil, pkgErrors.New("no gRPC endpoints provided")
|
||||
}
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"endpoints": endpoints,
|
||||
"count": len(endpoints),
|
||||
}).Info("Initialized gRPC connection provider with multiple endpoints")
|
||||
|
||||
return &grpcConnectionProvider{
|
||||
endpoints: endpoints,
|
||||
ctx: ctx,
|
||||
dialOpts: dialOpts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseEndpoints splits a comma-separated endpoint string into individual endpoints.
|
||||
func parseEndpoints(endpoint string) []string {
|
||||
if endpoint == "" {
|
||||
return nil
|
||||
}
|
||||
var endpoints []string
|
||||
for p := range strings.SplitSeq(endpoint, ",") {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
endpoints = append(endpoints, p)
|
||||
}
|
||||
}
|
||||
return endpoints
|
||||
}
|
||||
|
||||
func (p *grpcConnectionProvider) CurrentConn() *grpc.ClientConn {
|
||||
if p.closed.Load() {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// Return existing connection if available
|
||||
if p.conn != nil {
|
||||
return p.conn
|
||||
}
|
||||
|
||||
// Create connection lazily
|
||||
ep := p.endpoints[p.currentIndex]
|
||||
conn, err := grpc.DialContext(p.ctx, ep, p.dialOpts...)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("endpoint", ep).Error("Failed to create gRPC connection")
|
||||
return nil
|
||||
}
|
||||
|
||||
p.conn = conn
|
||||
log.WithField("endpoint", ep).Debug("Created gRPC connection")
|
||||
return conn
|
||||
}
|
||||
|
||||
func (p *grpcConnectionProvider) CurrentHost() string {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
return p.endpoints[p.currentIndex]
|
||||
}
|
||||
|
||||
func (p *grpcConnectionProvider) Hosts() []string {
|
||||
// Return a copy to maintain immutability
|
||||
hosts := make([]string, len(p.endpoints))
|
||||
copy(hosts, p.endpoints)
|
||||
return hosts
|
||||
}
|
||||
|
||||
func (p *grpcConnectionProvider) SetHost(index int) error {
|
||||
if index < 0 || index >= len(p.endpoints) {
|
||||
return pkgErrors.Errorf("invalid host index %d, must be between 0 and %d", index, len(p.endpoints)-1)
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if uint64(index) == p.currentIndex {
|
||||
return nil // Already on this host
|
||||
}
|
||||
|
||||
oldHost := p.endpoints[p.currentIndex]
|
||||
|
||||
// Close existing connection if any
|
||||
if p.conn != nil {
|
||||
if err := p.conn.Close(); err != nil {
|
||||
log.WithError(err).WithField("endpoint", oldHost).Debug("Failed to close previous connection")
|
||||
}
|
||||
p.conn = nil
|
||||
}
|
||||
|
||||
p.currentIndex = uint64(index)
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"previousHost": oldHost,
|
||||
"newHost": p.endpoints[index],
|
||||
}).Debug("Switched gRPC endpoint")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *grpcConnectionProvider) Close() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.closed.Load() {
|
||||
return nil
|
||||
}
|
||||
p.closed.Store(true)
|
||||
|
||||
if p.conn != nil {
|
||||
if err := p.conn.Close(); err != nil {
|
||||
return pkgErrors.Wrapf(err, "failed to close connection to %s", p.endpoints[p.currentIndex])
|
||||
}
|
||||
p.conn = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
203
api/grpc/grpc_connection_provider_test.go
Normal file
203
api/grpc/grpc_connection_provider_test.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/testing/assert"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
func TestParseEndpoints(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []string
|
||||
}{
|
||||
{"single endpoint", "localhost:4000", []string{"localhost:4000"}},
|
||||
{"multiple endpoints", "host1:4000,host2:4000,host3:4000", []string{"host1:4000", "host2:4000", "host3:4000"}},
|
||||
{"endpoints with spaces", "host1:4000, host2:4000 , host3:4000", []string{"host1:4000", "host2:4000", "host3:4000"}},
|
||||
{"empty string", "", nil},
|
||||
{"only commas", ",,,", nil},
|
||||
{"trailing comma", "host1:4000,host2:4000,", []string{"host1:4000", "host2:4000"}},
|
||||
{"leading comma", ",host1:4000,host2:4000", []string{"host1:4000", "host2:4000"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.DeepEqual(t, tt.expected, parseEndpoints(tt.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewGrpcConnectionProvider_Errors(t *testing.T) {
|
||||
t.Run("no endpoints", func(t *testing.T) {
|
||||
dialOpts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
|
||||
_, err := NewGrpcConnectionProvider(context.Background(), "", dialOpts)
|
||||
require.ErrorContains(t, "no gRPC endpoints provided", err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGrpcConnectionProvider_LazyConnection(t *testing.T) {
|
||||
// Start only one server but configure provider with two endpoints
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
server := grpc.NewServer()
|
||||
go func() { _ = server.Serve(lis) }()
|
||||
defer server.Stop()
|
||||
|
||||
validAddr := lis.Addr().String()
|
||||
invalidAddr := "127.0.0.1:1" // Port 1 is unlikely to be listening
|
||||
|
||||
// Provider should succeed even though second endpoint is invalid (lazy connections)
|
||||
endpoint := validAddr + "," + invalidAddr
|
||||
ctx := context.Background()
|
||||
dialOpts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
|
||||
provider, err := NewGrpcConnectionProvider(ctx, endpoint, dialOpts)
|
||||
require.NoError(t, err, "Provider creation should succeed with lazy connections")
|
||||
defer func() { _ = provider.Close() }()
|
||||
|
||||
// First endpoint should work
|
||||
conn := provider.CurrentConn()
|
||||
assert.NotNil(t, conn, "First connection should be created lazily")
|
||||
}
|
||||
|
||||
func TestGrpcConnectionProvider_SingleConnectionModel(t *testing.T) {
|
||||
// Create provider with 3 endpoints
|
||||
var addrs []string
|
||||
var servers []*grpc.Server
|
||||
|
||||
for range 3 {
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
server := grpc.NewServer()
|
||||
go func() { _ = server.Serve(lis) }()
|
||||
addrs = append(addrs, lis.Addr().String())
|
||||
servers = append(servers, server)
|
||||
}
|
||||
defer func() {
|
||||
for _, s := range servers {
|
||||
s.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
endpoint := strings.Join(addrs, ",")
|
||||
ctx := context.Background()
|
||||
dialOpts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
|
||||
provider, err := NewGrpcConnectionProvider(ctx, endpoint, dialOpts)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = provider.Close() }()
|
||||
|
||||
// Access the internal state to verify single connection behavior
|
||||
p := provider.(*grpcConnectionProvider)
|
||||
|
||||
// Initially no connection
|
||||
p.mu.Lock()
|
||||
assert.Equal(t, (*grpc.ClientConn)(nil), p.conn, "Connection should be nil before access")
|
||||
p.mu.Unlock()
|
||||
|
||||
// Access connection - should create one
|
||||
conn0 := provider.CurrentConn()
|
||||
assert.NotNil(t, conn0)
|
||||
|
||||
p.mu.Lock()
|
||||
assert.NotNil(t, p.conn, "Connection should be created after CurrentConn()")
|
||||
firstConn := p.conn
|
||||
p.mu.Unlock()
|
||||
|
||||
// Call CurrentConn again - should return same connection
|
||||
conn0Again := provider.CurrentConn()
|
||||
assert.Equal(t, conn0, conn0Again, "Should return same connection")
|
||||
|
||||
// Switch to different host - old connection should be closed, new one created lazily
|
||||
require.NoError(t, provider.SetHost(1))
|
||||
|
||||
p.mu.Lock()
|
||||
assert.Equal(t, (*grpc.ClientConn)(nil), p.conn, "Connection should be nil after SetHost (lazy)")
|
||||
p.mu.Unlock()
|
||||
|
||||
// Get new connection
|
||||
conn1 := provider.CurrentConn()
|
||||
assert.NotNil(t, conn1)
|
||||
assert.NotEqual(t, firstConn, conn1, "Should be a different connection after switching hosts")
|
||||
}
|
||||
|
||||
// testProvider creates a provider with n test servers and returns cleanup function.
|
||||
func testProvider(t *testing.T, n int) (GrpcConnectionProvider, []string, func()) {
|
||||
var addrs []string
|
||||
var cleanups []func()
|
||||
|
||||
for range n {
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
server := grpc.NewServer()
|
||||
go func() { _ = server.Serve(lis) }()
|
||||
addrs = append(addrs, lis.Addr().String())
|
||||
cleanups = append(cleanups, server.Stop)
|
||||
}
|
||||
|
||||
endpoint := strings.Join(addrs, ",")
|
||||
|
||||
ctx := context.Background()
|
||||
dialOpts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
|
||||
provider, err := NewGrpcConnectionProvider(ctx, endpoint, dialOpts)
|
||||
require.NoError(t, err)
|
||||
|
||||
cleanup := func() {
|
||||
_ = provider.Close()
|
||||
for _, c := range cleanups {
|
||||
c()
|
||||
}
|
||||
}
|
||||
return provider, addrs, cleanup
|
||||
}
|
||||
|
||||
func TestGrpcConnectionProvider(t *testing.T) {
|
||||
provider, addrs, cleanup := testProvider(t, 3)
|
||||
defer cleanup()
|
||||
|
||||
t.Run("initial state", func(t *testing.T) {
|
||||
assert.Equal(t, 3, len(provider.Hosts()))
|
||||
assert.Equal(t, addrs[0], provider.CurrentHost())
|
||||
assert.NotNil(t, provider.CurrentConn())
|
||||
})
|
||||
|
||||
t.Run("SetHost", func(t *testing.T) {
|
||||
require.NoError(t, provider.SetHost(1))
|
||||
assert.Equal(t, addrs[1], provider.CurrentHost())
|
||||
assert.NotNil(t, provider.CurrentConn()) // New connection created lazily
|
||||
require.NoError(t, provider.SetHost(0))
|
||||
assert.Equal(t, addrs[0], provider.CurrentHost())
|
||||
require.ErrorContains(t, "invalid host index", provider.SetHost(-1))
|
||||
require.ErrorContains(t, "invalid host index", provider.SetHost(3))
|
||||
})
|
||||
|
||||
t.Run("SetHost circular", func(t *testing.T) {
|
||||
// Test round-robin style switching using SetHost with manual index
|
||||
indices := []int{1, 2, 0, 1} // Simulate circular switching
|
||||
for i, idx := range indices {
|
||||
require.NoError(t, provider.SetHost(idx))
|
||||
assert.Equal(t, addrs[idx], provider.CurrentHost(), "iteration %d", i)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Hosts returns copy", func(t *testing.T) {
|
||||
hosts := provider.Hosts()
|
||||
original := hosts[0]
|
||||
hosts[0] = "modified"
|
||||
assert.Equal(t, original, provider.Hosts()[0])
|
||||
})
|
||||
}
|
||||
|
||||
func TestGrpcConnectionProvider_Close(t *testing.T) {
|
||||
provider, _, cleanup := testProvider(t, 1)
|
||||
defer cleanup()
|
||||
|
||||
assert.NotNil(t, provider.CurrentConn())
|
||||
require.NoError(t, provider.Close())
|
||||
assert.Equal(t, (*grpc.ClientConn)(nil), provider.CurrentConn())
|
||||
require.NoError(t, provider.Close()) // Double close is safe
|
||||
}
|
||||
20
api/grpc/mock_grpc_provider.go
Normal file
20
api/grpc/mock_grpc_provider.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package grpc
|
||||
|
||||
import "google.golang.org/grpc"
|
||||
|
||||
// MockGrpcProvider implements GrpcConnectionProvider for testing.
|
||||
type MockGrpcProvider struct {
|
||||
MockConn *grpc.ClientConn
|
||||
MockHosts []string
|
||||
}
|
||||
|
||||
func (m *MockGrpcProvider) CurrentConn() *grpc.ClientConn { return m.MockConn }
|
||||
func (m *MockGrpcProvider) CurrentHost() string {
|
||||
if len(m.MockHosts) > 0 {
|
||||
return m.MockHosts[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (m *MockGrpcProvider) Hosts() []string { return m.MockHosts }
|
||||
func (m *MockGrpcProvider) SetHost(int) error { return nil }
|
||||
func (m *MockGrpcProvider) Close() error { return nil }
|
||||
33
api/rest/BUILD.bazel
Normal file
33
api/rest/BUILD.bazel
Normal file
@@ -0,0 +1,33 @@
|
||||
load("@prysm//tools/go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"mock_rest_provider.go",
|
||||
"rest_connection_provider.go",
|
||||
"rest_handler.go",
|
||||
],
|
||||
importpath = "github.com/OffchainLabs/prysm/v7/api/rest",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//api:go_default_library",
|
||||
"//api/apiutil:go_default_library",
|
||||
"//api/client:go_default_library",
|
||||
"//config/params:go_default_library",
|
||||
"//network/httputil:go_default_library",
|
||||
"//runtime/version:go_default_library",
|
||||
"@com_github_pkg_errors//:go_default_library",
|
||||
"@com_github_sirupsen_logrus//:go_default_library",
|
||||
"@io_opentelemetry_go_contrib_instrumentation_net_http_otelhttp//:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["rest_connection_provider_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//testing/assert:go_default_library",
|
||||
"//testing/require:go_default_library",
|
||||
],
|
||||
)
|
||||
23
api/rest/mock_rest_provider.go
Normal file
23
api/rest/mock_rest_provider.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package rest
|
||||
|
||||
import "net/http"
|
||||
|
||||
// MockRestProvider implements RestConnectionProvider for testing.
|
||||
type MockRestProvider struct {
|
||||
MockClient *http.Client
|
||||
MockHandler RestHandler
|
||||
MockHosts []string
|
||||
HostIndex int
|
||||
}
|
||||
|
||||
func (m *MockRestProvider) HttpClient() *http.Client { return m.MockClient }
|
||||
func (m *MockRestProvider) RestHandler() RestHandler { return m.MockHandler }
|
||||
func (m *MockRestProvider) CurrentHost() string {
|
||||
if len(m.MockHosts) > 0 {
|
||||
return m.MockHosts[m.HostIndex%len(m.MockHosts)]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (m *MockRestProvider) Hosts() []string { return m.MockHosts }
|
||||
func (m *MockRestProvider) SetHost(index int) error { m.HostIndex = index; return nil }
|
||||
func (m *MockRestProvider) NextHost() { m.HostIndex = (m.HostIndex + 1) % len(m.MockHosts) }
|
||||
177
api/rest/rest_connection_provider.go
Normal file
177
api/rest/rest_connection_provider.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/api/client"
|
||||
pkgErrors "github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
)
|
||||
|
||||
var log = logrus.WithField("prefix", "rest")
|
||||
|
||||
// RestConnectionProvider manages HTTP client configuration for REST API with failover support.
|
||||
// It allows switching between different beacon node REST endpoints when the current one becomes unavailable.
|
||||
type RestConnectionProvider interface {
|
||||
// HttpClient returns the configured HTTP client with headers, timeout, and optional tracing.
|
||||
HttpClient() *http.Client
|
||||
// RestHandler returns the REST handler for making API requests.
|
||||
RestHandler() RestHandler
|
||||
// CurrentHost returns the current REST API endpoint URL.
|
||||
CurrentHost() string
|
||||
// Hosts returns all configured REST API endpoint URLs.
|
||||
Hosts() []string
|
||||
// SetHost switches to the endpoint at the given index.
|
||||
SetHost(index int) error
|
||||
// NextHost switches to the next endpoint in round-robin fashion.
|
||||
NextHost()
|
||||
}
|
||||
|
||||
// RestConnectionProviderOption is a functional option for configuring the REST connection provider.
|
||||
type RestConnectionProviderOption func(*restConnectionProvider)
|
||||
|
||||
// WithHttpTimeout sets the HTTP client timeout.
|
||||
func WithHttpTimeout(timeout time.Duration) RestConnectionProviderOption {
|
||||
return func(p *restConnectionProvider) {
|
||||
p.timeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
// WithHttpHeaders sets custom HTTP headers to include in all requests.
|
||||
func WithHttpHeaders(headers map[string][]string) RestConnectionProviderOption {
|
||||
return func(p *restConnectionProvider) {
|
||||
p.headers = headers
|
||||
}
|
||||
}
|
||||
|
||||
// WithTracing enables OpenTelemetry tracing for HTTP requests.
|
||||
func WithTracing() RestConnectionProviderOption {
|
||||
return func(p *restConnectionProvider) {
|
||||
p.enableTracing = true
|
||||
}
|
||||
}
|
||||
|
||||
type restConnectionProvider struct {
|
||||
endpoints []string
|
||||
httpClient *http.Client
|
||||
restHandler RestHandler
|
||||
currentIndex atomic.Uint64
|
||||
timeout time.Duration
|
||||
headers map[string][]string
|
||||
enableTracing bool
|
||||
}
|
||||
|
||||
// NewRestConnectionProvider creates a new REST connection provider that manages HTTP client configuration.
|
||||
// The endpoint parameter can be a comma-separated list of URLs (e.g., "http://host1:3500,http://host2:3500").
|
||||
func NewRestConnectionProvider(endpoint string, opts ...RestConnectionProviderOption) (RestConnectionProvider, error) {
|
||||
endpoints := parseEndpoints(endpoint)
|
||||
if len(endpoints) == 0 {
|
||||
return nil, pkgErrors.New("no REST API endpoints provided")
|
||||
}
|
||||
|
||||
p := &restConnectionProvider{
|
||||
endpoints: endpoints,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(p)
|
||||
}
|
||||
|
||||
// Build the HTTP transport chain
|
||||
var transport http.RoundTripper = http.DefaultTransport
|
||||
|
||||
// Add custom headers if configured
|
||||
if len(p.headers) > 0 {
|
||||
transport = client.NewCustomHeadersTransport(transport, p.headers)
|
||||
}
|
||||
|
||||
// Add tracing if enabled
|
||||
if p.enableTracing {
|
||||
transport = otelhttp.NewTransport(transport)
|
||||
}
|
||||
|
||||
p.httpClient = &http.Client{
|
||||
Timeout: p.timeout,
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
// Create the REST handler with the HTTP client and initial host
|
||||
p.restHandler = newRestHandler(*p.httpClient, endpoints[0])
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"endpoints": endpoints,
|
||||
"count": len(endpoints),
|
||||
}).Info("Initialized REST connection provider with endpoints")
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// parseEndpoints splits a comma-separated endpoint string into individual endpoints.
|
||||
func parseEndpoints(endpoint string) []string {
|
||||
if endpoint == "" {
|
||||
return nil
|
||||
}
|
||||
var endpoints []string
|
||||
for p := range strings.SplitSeq(endpoint, ",") {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
endpoints = append(endpoints, p)
|
||||
}
|
||||
}
|
||||
return endpoints
|
||||
}
|
||||
|
||||
func (p *restConnectionProvider) HttpClient() *http.Client {
|
||||
return p.httpClient
|
||||
}
|
||||
|
||||
func (p *restConnectionProvider) RestHandler() RestHandler {
|
||||
return p.restHandler
|
||||
}
|
||||
|
||||
func (p *restConnectionProvider) CurrentHost() string {
|
||||
idx := p.currentIndex.Load() % uint64(len(p.endpoints))
|
||||
return p.endpoints[idx]
|
||||
}
|
||||
|
||||
func (p *restConnectionProvider) Hosts() []string {
|
||||
// Return a copy to maintain immutability
|
||||
hosts := make([]string, len(p.endpoints))
|
||||
copy(hosts, p.endpoints)
|
||||
return hosts
|
||||
}
|
||||
|
||||
func (p *restConnectionProvider) SetHost(index int) error {
|
||||
if index < 0 || index >= len(p.endpoints) {
|
||||
return pkgErrors.Errorf("invalid host index %d, must be between 0 and %d", index, len(p.endpoints)-1)
|
||||
}
|
||||
|
||||
oldIdx := p.currentIndex.Load()
|
||||
p.currentIndex.Store(uint64(index))
|
||||
|
||||
// Update the rest handler's host
|
||||
p.restHandler.SetHost(p.endpoints[index])
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"previousHost": p.endpoints[oldIdx%uint64(len(p.endpoints))],
|
||||
"newHost": p.endpoints[index],
|
||||
}).Debug("Trying REST endpoint")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *restConnectionProvider) NextHost() {
|
||||
oldIdx := p.currentIndex.Load()
|
||||
newIdx := (oldIdx + 1) % uint64(len(p.endpoints))
|
||||
p.currentIndex.Store(newIdx)
|
||||
|
||||
// Update the rest handler's host
|
||||
p.restHandler.SetHost(p.endpoints[newIdx])
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"previousHost": p.endpoints[oldIdx],
|
||||
"newHost": p.endpoints[newIdx],
|
||||
}).Debug("Switched to next REST endpoint")
|
||||
}
|
||||
85
api/rest/rest_connection_provider_test.go
Normal file
85
api/rest/rest_connection_provider_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/testing/assert"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/require"
|
||||
)
|
||||
|
||||
func TestParseEndpoints(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []string
|
||||
}{
|
||||
{"single endpoint", "http://localhost:3500", []string{"http://localhost:3500"}},
|
||||
{"multiple endpoints", "http://host1:3500,http://host2:3500,http://host3:3500", []string{"http://host1:3500", "http://host2:3500", "http://host3:3500"}},
|
||||
{"endpoints with spaces", "http://host1:3500, http://host2:3500 , http://host3:3500", []string{"http://host1:3500", "http://host2:3500", "http://host3:3500"}},
|
||||
{"empty string", "", nil},
|
||||
{"only commas", ",,,", nil},
|
||||
{"trailing comma", "http://host1:3500,http://host2:3500,", []string{"http://host1:3500", "http://host2:3500"}},
|
||||
{"leading comma", ",http://host1:3500,http://host2:3500", []string{"http://host1:3500", "http://host2:3500"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.DeepEqual(t, tt.expected, parseEndpoints(tt.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRestConnectionProvider_Errors(t *testing.T) {
|
||||
t.Run("no endpoints", func(t *testing.T) {
|
||||
_, err := NewRestConnectionProvider("")
|
||||
require.ErrorContains(t, "no REST API endpoints provided", err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRestConnectionProvider(t *testing.T) {
|
||||
provider, err := NewRestConnectionProvider("http://host1:3500,http://host2:3500,http://host3:3500")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("initial state", func(t *testing.T) {
|
||||
assert.Equal(t, 3, len(provider.Hosts()))
|
||||
assert.Equal(t, "http://host1:3500", provider.CurrentHost())
|
||||
assert.NotNil(t, provider.HttpClient())
|
||||
})
|
||||
|
||||
t.Run("SetHost", func(t *testing.T) {
|
||||
require.NoError(t, provider.SetHost(1))
|
||||
assert.Equal(t, "http://host2:3500", provider.CurrentHost())
|
||||
require.NoError(t, provider.SetHost(0))
|
||||
assert.Equal(t, "http://host1:3500", provider.CurrentHost())
|
||||
require.ErrorContains(t, "invalid host index", provider.SetHost(-1))
|
||||
require.ErrorContains(t, "invalid host index", provider.SetHost(3))
|
||||
})
|
||||
|
||||
t.Run("NextHost circular", func(t *testing.T) {
|
||||
require.NoError(t, provider.SetHost(0)) // Reset to start
|
||||
expected := []string{"http://host2:3500", "http://host3:3500", "http://host1:3500", "http://host2:3500"}
|
||||
for i, exp := range expected {
|
||||
provider.NextHost()
|
||||
assert.Equal(t, exp, provider.CurrentHost(), "iteration %d", i)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Hosts returns copy", func(t *testing.T) {
|
||||
hosts := provider.Hosts()
|
||||
original := hosts[0]
|
||||
hosts[0] = "modified"
|
||||
assert.Equal(t, original, provider.Hosts()[0])
|
||||
})
|
||||
}
|
||||
|
||||
func TestRestConnectionProvider_WithOptions(t *testing.T) {
|
||||
headers := map[string][]string{"Authorization": {"Bearer token"}}
|
||||
provider, err := NewRestConnectionProvider(
|
||||
"http://localhost:3500",
|
||||
WithHttpHeaders(headers),
|
||||
WithHttpTimeout(30000000000), // 30 seconds in nanoseconds
|
||||
WithTracing(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, provider.HttpClient())
|
||||
assert.Equal(t, "http://localhost:3500", provider.CurrentHost())
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package beacon_api
|
||||
package rest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
type reqOption func(*http.Request)
|
||||
|
||||
// RestHandler defines the interface for making REST API requests.
|
||||
type RestHandler interface {
|
||||
Get(ctx context.Context, endpoint string, resp any) error
|
||||
GetStatusCode(ctx context.Context, endpoint string) (int, error)
|
||||
@@ -32,26 +33,31 @@ type RestHandler interface {
|
||||
SetHost(host string)
|
||||
}
|
||||
|
||||
type BeaconApiRestHandler struct {
|
||||
type restHandler struct {
|
||||
client http.Client
|
||||
host string
|
||||
reqOverrides []reqOption
|
||||
}
|
||||
|
||||
// NewBeaconApiRestHandler returns a RestHandler
|
||||
func NewBeaconApiRestHandler(client http.Client, host string) RestHandler {
|
||||
brh := &BeaconApiRestHandler{
|
||||
// newRestHandler returns a RestHandler (internal use)
|
||||
func newRestHandler(client http.Client, host string) RestHandler {
|
||||
return NewRestHandler(client, host)
|
||||
}
|
||||
|
||||
// NewRestHandler returns a RestHandler
|
||||
func NewRestHandler(client http.Client, host string) RestHandler {
|
||||
rh := &restHandler{
|
||||
client: client,
|
||||
host: host,
|
||||
}
|
||||
brh.appendAcceptOverride()
|
||||
return brh
|
||||
rh.appendAcceptOverride()
|
||||
return rh
|
||||
}
|
||||
|
||||
// appendAcceptOverride enables the Accept header to be customized at runtime via an environment variable.
|
||||
// This is specified as an env var because it is a niche option that prysm may use for performance testing or debugging
|
||||
// bug which users are unlikely to need. Using an env var keeps the set of user-facing flags cleaner.
|
||||
func (c *BeaconApiRestHandler) appendAcceptOverride() {
|
||||
func (c *restHandler) appendAcceptOverride() {
|
||||
if accept := os.Getenv(params.EnvNameOverrideAccept); accept != "" {
|
||||
c.reqOverrides = append(c.reqOverrides, func(req *http.Request) {
|
||||
req.Header.Set("Accept", accept)
|
||||
@@ -60,18 +66,18 @@ func (c *BeaconApiRestHandler) appendAcceptOverride() {
|
||||
}
|
||||
|
||||
// HttpClient returns the underlying HTTP client of the handler
|
||||
func (c *BeaconApiRestHandler) HttpClient() *http.Client {
|
||||
func (c *restHandler) HttpClient() *http.Client {
|
||||
return &c.client
|
||||
}
|
||||
|
||||
// Host returns the underlying HTTP host
|
||||
func (c *BeaconApiRestHandler) Host() string {
|
||||
func (c *restHandler) 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 *BeaconApiRestHandler) Get(ctx context.Context, endpoint string, resp any) error {
|
||||
func (c *restHandler) Get(ctx context.Context, endpoint string, resp any) error {
|
||||
url := c.host + endpoint
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
@@ -94,7 +100,7 @@ func (c *BeaconApiRestHandler) Get(ctx context.Context, endpoint string, resp an
|
||||
// GetStatusCode sends a GET request and returns only the HTTP status code.
|
||||
// This is useful for endpoints like /eth/v1/node/health that communicate status via HTTP codes
|
||||
// (200 = ready, 206 = syncing, 503 = unavailable) rather than response bodies.
|
||||
func (c *BeaconApiRestHandler) GetStatusCode(ctx context.Context, endpoint string) (int, error) {
|
||||
func (c *restHandler) GetStatusCode(ctx context.Context, endpoint string) (int, error) {
|
||||
url := c.host + endpoint
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
@@ -113,7 +119,7 @@ func (c *BeaconApiRestHandler) GetStatusCode(ctx context.Context, endpoint strin
|
||||
return httpResp.StatusCode, nil
|
||||
}
|
||||
|
||||
func (c *BeaconApiRestHandler) GetSSZ(ctx context.Context, endpoint string) ([]byte, http.Header, error) {
|
||||
func (c *restHandler) GetSSZ(ctx context.Context, endpoint string) ([]byte, http.Header, error) {
|
||||
url := c.host + endpoint
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
@@ -168,7 +174,7 @@ func (c *BeaconApiRestHandler) GetSSZ(ctx context.Context, endpoint string) ([]b
|
||||
|
||||
// Post sends a POST request and decodes the response body as a JSON object into the passed in object.
|
||||
// If an HTTP error is returned, the body is decoded as a DefaultJsonError JSON object and returned as the first return value.
|
||||
func (c *BeaconApiRestHandler) Post(
|
||||
func (c *restHandler) Post(
|
||||
ctx context.Context,
|
||||
apiEndpoint string,
|
||||
headers map[string]string,
|
||||
@@ -204,7 +210,7 @@ func (c *BeaconApiRestHandler) Post(
|
||||
}
|
||||
|
||||
// PostSSZ sends a POST request and prefers an SSZ (application/octet-stream) response body.
|
||||
func (c *BeaconApiRestHandler) PostSSZ(
|
||||
func (c *restHandler) PostSSZ(
|
||||
ctx context.Context,
|
||||
apiEndpoint string,
|
||||
headers map[string]string,
|
||||
@@ -305,6 +311,6 @@ func decodeResp(httpResp *http.Response, resp any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BeaconApiRestHandler) SetHost(host string) {
|
||||
func (c *restHandler) SetHost(host string) {
|
||||
c.host = host
|
||||
}
|
||||
@@ -1053,6 +1053,10 @@ func (s *Store) getStateUsingStateDiff(ctx context.Context, blockRoot [32]byte)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if uint64(slot) < s.getOffset() {
|
||||
return nil, ErrSlotBeforeOffset
|
||||
}
|
||||
|
||||
st, err := s.stateByDiff(ctx, slot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1070,6 +1074,10 @@ func (s *Store) hasStateUsingStateDiff(ctx context.Context, blockRoot [32]byte)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if uint64(slot) < s.getOffset() {
|
||||
return false, ErrSlotBeforeOffset
|
||||
}
|
||||
|
||||
stateLvl := computeLevel(s.getOffset(), slot)
|
||||
return stateLvl != -1, nil
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ const (
|
||||
*/
|
||||
|
||||
// SlotInDiffTree returns whether the given slot is a saving point in the diff tree.
|
||||
// It it is, it also returns the offset and level in the tree.
|
||||
// If it is, it also returns the offset and level in the tree.
|
||||
func (s *Store) SlotInDiffTree(slot primitives.Slot) (uint64, int, error) {
|
||||
offset := s.getOffset()
|
||||
if uint64(slot) < offset {
|
||||
|
||||
@@ -25,7 +25,7 @@ func newStateDiffCache(s *Store) (*stateDiffCache, error) {
|
||||
return bbolt.ErrBucketNotFound
|
||||
}
|
||||
|
||||
offsetBytes := bucket.Get([]byte("offset"))
|
||||
offsetBytes := bucket.Get(offsetKey)
|
||||
if offsetBytes == nil {
|
||||
return errors.New("state diff cache: offset not found")
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
var (
|
||||
offsetKey = []byte("offset")
|
||||
ErrSlotBeforeOffset = errors.New("slot is before root offset")
|
||||
ErrSlotBeforeOffset = errors.New("slot is before state-diff root offset")
|
||||
)
|
||||
|
||||
func makeKeyForStateDiffTree(level int, slot uint64) []byte {
|
||||
@@ -73,6 +73,9 @@ func (s *Store) getAnchorState(offset uint64, lvl int, slot primitives.Slot) (an
|
||||
|
||||
// computeLevel computes the level in the diff tree. Returns -1 in case slot should not be in tree.
|
||||
func computeLevel(offset uint64, slot primitives.Slot) int {
|
||||
if uint64(slot) < offset {
|
||||
return -1
|
||||
}
|
||||
rel := uint64(slot) - offset
|
||||
for i, exp := range flags.Get().StateDiffExponents {
|
||||
if exp < 2 || exp >= 64 {
|
||||
|
||||
@@ -43,8 +43,12 @@ func TestStateDiff_ComputeLevel(t *testing.T) {
|
||||
|
||||
offset := db.getOffset()
|
||||
|
||||
// should be -1. slot < offset
|
||||
lvl := computeLevel(10, primitives.Slot(9))
|
||||
require.Equal(t, -1, lvl)
|
||||
|
||||
// 2 ** 21
|
||||
lvl := computeLevel(offset, primitives.Slot(math.PowerOf2(21)))
|
||||
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(21)))
|
||||
require.Equal(t, 0, lvl)
|
||||
|
||||
// 2 ** 21 * 3
|
||||
|
||||
@@ -1395,6 +1395,23 @@ func TestStore_CanSaveRetrieveStateUsingStateDiff(t *testing.T) {
|
||||
require.IsNil(t, readSt)
|
||||
})
|
||||
|
||||
t.Run("slot before offset", func(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
setDefaultStateDiffExponents()
|
||||
|
||||
err := setOffsetInDB(db, 10)
|
||||
require.NoError(t, err)
|
||||
|
||||
r := bytesutil.ToBytes32([]byte{'A'})
|
||||
ss := ðpb.StateSummary{Slot: 9, Root: r[:]}
|
||||
err = db.SaveStateSummary(t.Context(), ss)
|
||||
require.NoError(t, err)
|
||||
|
||||
st, err := db.getStateUsingStateDiff(t.Context(), r)
|
||||
require.ErrorIs(t, err, ErrSlotBeforeOffset)
|
||||
require.IsNil(t, st)
|
||||
})
|
||||
|
||||
t.Run("Full state snapshot", func(t *testing.T) {
|
||||
t.Run("using state summary", func(t *testing.T) {
|
||||
for v := range version.All() {
|
||||
@@ -1627,4 +1644,21 @@ func TestStore_HasStateUsingStateDiff(t *testing.T) {
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
t.Run("slot before offset", func(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
setDefaultStateDiffExponents()
|
||||
|
||||
err := setOffsetInDB(db, 10)
|
||||
require.NoError(t, err)
|
||||
|
||||
r := bytesutil.ToBytes32([]byte{'B'})
|
||||
ss := ðpb.StateSummary{Slot: 0, Root: r[:]}
|
||||
err = db.SaveStateSummary(t.Context(), ss)
|
||||
require.NoError(t, err)
|
||||
|
||||
hasState, err := db.hasStateUsingStateDiff(t.Context(), r)
|
||||
require.ErrorIs(t, err, ErrSlotBeforeOffset)
|
||||
require.Equal(t, false, hasState)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ func (b batch) ensureParent(expected [32]byte) error {
|
||||
func (b batch) blockRequest() *eth.BeaconBlocksByRangeRequest {
|
||||
return ð.BeaconBlocksByRangeRequest{
|
||||
StartSlot: b.begin,
|
||||
Count: uint64(b.end - b.begin),
|
||||
Count: uint64(b.end.FlooredSubSlot(b.begin)),
|
||||
Step: 1,
|
||||
}
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func (b batch) blockRequest() *eth.BeaconBlocksByRangeRequest {
|
||||
func (b batch) blobRequest() *eth.BlobSidecarsByRangeRequest {
|
||||
return ð.BlobSidecarsByRangeRequest{
|
||||
StartSlot: b.begin,
|
||||
Count: uint64(b.end - b.begin),
|
||||
Count: uint64(b.end.FlooredSubSlot(b.begin)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,93 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestBlockRequest(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
begin primitives.Slot
|
||||
end primitives.Slot
|
||||
expectedCount uint64
|
||||
}{
|
||||
{
|
||||
name: "normal case",
|
||||
begin: 100,
|
||||
end: 200,
|
||||
expectedCount: 100,
|
||||
},
|
||||
{
|
||||
name: "end equals begin",
|
||||
begin: 100,
|
||||
end: 100,
|
||||
expectedCount: 0,
|
||||
},
|
||||
{
|
||||
name: "end less than begin (would underflow without check)",
|
||||
begin: 200,
|
||||
end: 100,
|
||||
expectedCount: 0,
|
||||
},
|
||||
{
|
||||
name: "zero values",
|
||||
begin: 0,
|
||||
end: 0,
|
||||
expectedCount: 0,
|
||||
},
|
||||
{
|
||||
name: "single slot",
|
||||
begin: 0,
|
||||
end: 1,
|
||||
expectedCount: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
b := batch{begin: tc.begin, end: tc.end}
|
||||
req := b.blockRequest()
|
||||
require.Equal(t, tc.expectedCount, req.Count)
|
||||
require.Equal(t, tc.begin, req.StartSlot)
|
||||
require.Equal(t, uint64(1), req.Step)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlobRequest(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
begin primitives.Slot
|
||||
end primitives.Slot
|
||||
expectedCount uint64
|
||||
}{
|
||||
{
|
||||
name: "normal case",
|
||||
begin: 100,
|
||||
end: 200,
|
||||
expectedCount: 100,
|
||||
},
|
||||
{
|
||||
name: "end equals begin",
|
||||
begin: 100,
|
||||
end: 100,
|
||||
expectedCount: 0,
|
||||
},
|
||||
{
|
||||
name: "end less than begin (would underflow without check)",
|
||||
begin: 200,
|
||||
end: 100,
|
||||
expectedCount: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
b := batch{begin: tc.begin, end: tc.end}
|
||||
req := b.blobRequest()
|
||||
require.Equal(t, tc.expectedCount, req.Count)
|
||||
require.Equal(t, tc.begin, req.StartSlot)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortBatchDesc(t *testing.T) {
|
||||
orderIn := []primitives.Slot{100, 10000, 1}
|
||||
orderOut := []primitives.Slot{10000, 100, 1}
|
||||
|
||||
@@ -668,7 +668,7 @@ func populateBlock(bw *blocks.BlockWithROSidecars, blobs []blocks.ROBlob, req *p
|
||||
|
||||
func missingCommitError(root [32]byte, slot primitives.Slot, missing [][]byte) error {
|
||||
missStr := make([]string, 0, len(missing))
|
||||
for k := range missing {
|
||||
for _, k := range missing {
|
||||
missStr = append(missStr, fmt.Sprintf("%#x", k))
|
||||
}
|
||||
return errors.Wrapf(errMissingBlobsForBlockCommitments,
|
||||
|
||||
@@ -226,8 +226,6 @@ func (s *Service) Start() {
|
||||
|
||||
// fetchOriginSidecars fetches origin sidecars
|
||||
func (s *Service) fetchOriginSidecars(peers []peer.ID) error {
|
||||
const delay = 10 * time.Second // The delay between each attempt to fetch origin data column sidecars
|
||||
|
||||
blockRoot, err := s.cfg.DB.OriginCheckpointBlockRoot(s.ctx)
|
||||
if errors.Is(err, db.ErrNotFoundOriginBlockRoot) {
|
||||
return nil
|
||||
@@ -260,7 +258,7 @@ func (s *Service) fetchOriginSidecars(peers []peer.ID) error {
|
||||
blockVersion := roBlock.Version()
|
||||
|
||||
if blockVersion >= version.Fulu {
|
||||
if err := s.fetchOriginDataColumnSidecars(roBlock, delay); err != nil {
|
||||
if err := s.fetchOriginDataColumnSidecars(roBlock); err != nil {
|
||||
return errors.Wrap(err, "fetch origin columns")
|
||||
}
|
||||
return nil
|
||||
@@ -414,7 +412,7 @@ func (s *Service) fetchOriginBlobSidecars(pids []peer.ID, rob blocks.ROBlock) er
|
||||
return fmt.Errorf("no connected peer able to provide blobs for checkpoint sync block %#x", r)
|
||||
}
|
||||
|
||||
func (s *Service) fetchOriginDataColumnSidecars(roBlock blocks.ROBlock, delay time.Duration) error {
|
||||
func (s *Service) fetchOriginDataColumnSidecars(roBlock blocks.ROBlock) error {
|
||||
const (
|
||||
errorMessage = "Failed to fetch origin data column sidecars"
|
||||
warningIteration = 10
|
||||
@@ -501,7 +499,6 @@ func (s *Service) fetchOriginDataColumnSidecars(roBlock blocks.ROBlock, delay ti
|
||||
log := log.WithFields(logrus.Fields{
|
||||
"attempt": attempt,
|
||||
"missingIndices": helpers.SortedPrettySliceFromMap(missingIndicesByRoot[root]),
|
||||
"delay": delay,
|
||||
})
|
||||
|
||||
logFunc := log.Debug
|
||||
@@ -510,13 +507,6 @@ func (s *Service) fetchOriginDataColumnSidecars(roBlock blocks.ROBlock, delay ti
|
||||
}
|
||||
|
||||
logFunc("Failed to fetch some origin data column sidecars, retrying later")
|
||||
|
||||
// Wait before retrying, respecting context cancellation.
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return s.ctx.Err()
|
||||
case <-time.After(delay):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -687,10 +687,7 @@ func TestFetchOriginColumns(t *testing.T) {
|
||||
cfg.BlobSchedule = []params.BlobScheduleEntry{{Epoch: 0, MaxBlobsPerBlock: 10}}
|
||||
params.OverrideBeaconConfig(cfg)
|
||||
|
||||
const (
|
||||
delay = 0
|
||||
blobCount = 1
|
||||
)
|
||||
const blobCount = 1
|
||||
|
||||
t.Run("block has no commitments", func(t *testing.T) {
|
||||
service := new(Service)
|
||||
@@ -702,7 +699,7 @@ func TestFetchOriginColumns(t *testing.T) {
|
||||
roBlock, err := blocks.NewROBlock(signedBlock)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.fetchOriginDataColumnSidecars(roBlock, delay)
|
||||
err = service.fetchOriginDataColumnSidecars(roBlock)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@@ -724,7 +721,7 @@ func TestFetchOriginColumns(t *testing.T) {
|
||||
err := storage.Save(verifiedSidecars)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.fetchOriginDataColumnSidecars(roBlock, delay)
|
||||
err = service.fetchOriginDataColumnSidecars(roBlock)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@@ -829,7 +826,7 @@ func TestFetchOriginColumns(t *testing.T) {
|
||||
attempt++
|
||||
})
|
||||
|
||||
err = service.fetchOriginDataColumnSidecars(roBlock, delay)
|
||||
err = service.fetchOriginDataColumnSidecars(roBlock)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check all corresponding sidecars are saved in the store.
|
||||
|
||||
@@ -77,8 +77,13 @@ func SendBeaconBlocksByRangeRequest(
|
||||
}
|
||||
defer closeStream(stream, log)
|
||||
|
||||
// Cap the slice capacity to MaxRequestBlock to prevent panic from invalid Count values.
|
||||
// This guards against upstream bugs that may produce astronomically large Count values
|
||||
// (e.g., due to unsigned integer underflow).
|
||||
sliceCap := min(req.Count, params.MaxRequestBlock(slots.ToEpoch(tor.CurrentSlot())))
|
||||
|
||||
// Augment block processing function, if non-nil block processor is provided.
|
||||
blocks := make([]interfaces.ReadOnlySignedBeaconBlock, 0, req.Count)
|
||||
blocks := make([]interfaces.ReadOnlySignedBeaconBlock, 0, sliceCap)
|
||||
process := func(blk interfaces.ReadOnlySignedBeaconBlock) error {
|
||||
blocks = append(blocks, blk)
|
||||
if blockProcessor != nil {
|
||||
|
||||
3
changelog/bastin_statediff-touch-up.md
Normal file
3
changelog/bastin_statediff-touch-up.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Ignored
|
||||
|
||||
- small touch ups on state diff code.
|
||||
7
changelog/james-prysm_grpc-fallback.md
Normal file
7
changelog/james-prysm_grpc-fallback.md
Normal file
@@ -0,0 +1,7 @@
|
||||
### Changed
|
||||
|
||||
- gRPC fallback now matches rest api implementation and will also check and connect to only synced nodes.
|
||||
|
||||
### Removed
|
||||
|
||||
- gRPC resolver for load balancing, the new implementation matches rest api's so we should remove the resolver so it's handled the same way for consistency.
|
||||
3
changelog/manu_remove_unused_delay.md
Normal file
3
changelog/manu_remove_unused_delay.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Removed
|
||||
|
||||
- Remove unused `delay` parameter from `fetchOriginDataColumnSidecars` function.
|
||||
3
changelog/pvl-fix-16223.md
Normal file
3
changelog/pvl-fix-16223.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Changed
|
||||
|
||||
- Added some defensive checks to prevent overflows in block batch requests.
|
||||
@@ -1,3 +0,0 @@
|
||||
### Changed
|
||||
|
||||
- Use delay in fetchOriginDataColumnSidecars. Currently it is simply logged.
|
||||
3
changelog/satushh-log.md
Normal file
3
changelog/satushh-log.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Changed
|
||||
|
||||
- Log commitments instead of indices in missingCommitError
|
||||
3
changelog/willianpaixao_shell-autocompletion.md
Normal file
3
changelog/willianpaixao_shell-autocompletion.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Added
|
||||
|
||||
- Added shell completion support for `beacon-chain` and `validator` CLI tools.
|
||||
@@ -3,6 +3,8 @@ load("@prysm//tools/go:def.bzl", "go_library", "go_test")
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"completion.go",
|
||||
"completion_scripts.go",
|
||||
"config.go",
|
||||
"defaults.go",
|
||||
"flags.go",
|
||||
@@ -28,6 +30,7 @@ go_test(
|
||||
name = "go_default_test",
|
||||
size = "small",
|
||||
srcs = [
|
||||
"completion_test.go",
|
||||
"config_test.go",
|
||||
"flags_test.go",
|
||||
"helpers_test.go",
|
||||
|
||||
@@ -271,9 +271,11 @@ func main() {
|
||||
Commands: []*cli.Command{
|
||||
dbcommands.Commands,
|
||||
jwtcommands.Commands,
|
||||
cmd.CompletionCommand("beacon-chain"),
|
||||
},
|
||||
Flags: appFlags,
|
||||
Before: before,
|
||||
Flags: appFlags,
|
||||
Before: before,
|
||||
EnableBashCompletion: true,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
|
||||
63
cmd/completion.go
Normal file
63
cmd/completion.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CompletionCommand returns the completion command for the given binary name.
|
||||
// The binaryName parameter should be "beacon-chain" or "validator".
|
||||
func CompletionCommand(binaryName string) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "completion",
|
||||
Category: "completion",
|
||||
Usage: "Generate shell completion scripts",
|
||||
Description: fmt.Sprintf(`Generate shell completion scripts for bash, zsh, or fish.
|
||||
|
||||
To load completions:
|
||||
|
||||
Bash:
|
||||
$ source <(%[1]s completion bash)
|
||||
# To load completions for each session, execute once:
|
||||
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
|
||||
|
||||
Zsh:
|
||||
# To load completions for each session, execute once:
|
||||
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
|
||||
|
||||
# You may need to start a new shell for completions to take effect.
|
||||
|
||||
Fish:
|
||||
$ %[1]s completion fish | source
|
||||
# To load completions for each session, execute once:
|
||||
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
|
||||
`, binaryName),
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "bash",
|
||||
Usage: "Generate bash completion script",
|
||||
Action: func(_ *cli.Context) error {
|
||||
fmt.Println(bashCompletionScript(binaryName))
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "zsh",
|
||||
Usage: "Generate zsh completion script",
|
||||
Action: func(_ *cli.Context) error {
|
||||
fmt.Println(zshCompletionScript(binaryName))
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "fish",
|
||||
Usage: "Generate fish completion script",
|
||||
Action: func(_ *cli.Context) error {
|
||||
fmt.Println(fishCompletionScript(binaryName))
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
99
cmd/completion_scripts.go
Normal file
99
cmd/completion_scripts.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// bashCompletionScript returns the bash completion script for the given binary.
|
||||
func bashCompletionScript(binaryName string) string {
|
||||
// Convert hyphens to underscores for bash function names
|
||||
funcName := strings.ReplaceAll(binaryName, "-", "_")
|
||||
return fmt.Sprintf(`#!/bin/bash
|
||||
|
||||
_%[1]s_completions() {
|
||||
local cur prev words cword opts
|
||||
COMPREPLY=()
|
||||
|
||||
# Use bash-completion if available, otherwise set variables directly
|
||||
if declare -F _init_completion >/dev/null 2>&1; then
|
||||
_init_completion -n "=:" || return
|
||||
else
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
words=("${COMP_WORDS[@]}")
|
||||
cword=$COMP_CWORD
|
||||
fi
|
||||
|
||||
# Build command array for completion - flag must be at the END
|
||||
local -a requestComp
|
||||
if [[ "$cur" == "-"* ]]; then
|
||||
requestComp=("${COMP_WORDS[@]:0:COMP_CWORD}" "$cur" --generate-bash-completion)
|
||||
else
|
||||
requestComp=("${COMP_WORDS[@]:0:COMP_CWORD}" --generate-bash-completion)
|
||||
fi
|
||||
|
||||
opts=$("${requestComp[@]}" 2>/dev/null)
|
||||
COMPREPLY=($(compgen -W "${opts}" -- "${cur}"))
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -o bashdefault -o default -o nospace -F _%[1]s_completions %[2]s
|
||||
`, funcName, binaryName)
|
||||
}
|
||||
|
||||
// zshCompletionScript returns the zsh completion script for the given binary.
|
||||
func zshCompletionScript(binaryName string) string {
|
||||
// Convert hyphens to underscores for zsh function names
|
||||
funcName := strings.ReplaceAll(binaryName, "-", "_")
|
||||
return fmt.Sprintf(`#compdef %[2]s
|
||||
|
||||
_%[1]s() {
|
||||
local curcontext="$curcontext" ret=1
|
||||
local -a completions
|
||||
|
||||
# Build command array with --generate-bash-completion at the END
|
||||
local -a requestComp
|
||||
if [[ "${words[CURRENT]}" == -* ]]; then
|
||||
requestComp=(${words[1,CURRENT]} --generate-bash-completion)
|
||||
else
|
||||
requestComp=(${words[1,CURRENT-1]} --generate-bash-completion)
|
||||
fi
|
||||
|
||||
completions=($("${requestComp[@]}" 2>/dev/null))
|
||||
|
||||
if [[ ${#completions[@]} -gt 0 ]]; then
|
||||
_describe -t commands '%[2]s' completions && ret=0
|
||||
fi
|
||||
|
||||
# Fallback to file completion
|
||||
_files && ret=0
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
compdef _%[1]s %[2]s
|
||||
`, funcName, binaryName)
|
||||
}
|
||||
|
||||
// fishCompletionScript returns the fish completion script for the given binary.
|
||||
func fishCompletionScript(binaryName string) string {
|
||||
// Convert hyphens to underscores for fish function names
|
||||
funcName := strings.ReplaceAll(binaryName, "-", "_")
|
||||
return fmt.Sprintf(`# Fish completion for %[2]s
|
||||
|
||||
function __fish_%[1]s_complete
|
||||
set -l args (commandline -opc)
|
||||
set -l cur (commandline -ct)
|
||||
|
||||
# Build command with --generate-bash-completion at the END
|
||||
if string match -q -- '-*' "$cur"
|
||||
%[2]s $args $cur --generate-bash-completion 2>/dev/null
|
||||
else
|
||||
%[2]s $args --generate-bash-completion 2>/dev/null
|
||||
end
|
||||
end
|
||||
|
||||
complete -c %[2]s -f -a "(__fish_%[1]s_complete)"
|
||||
`, funcName, binaryName)
|
||||
}
|
||||
105
cmd/completion_test.go
Normal file
105
cmd/completion_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/testing/assert"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCompletionCommand(t *testing.T) {
|
||||
t.Run("creates command with correct name", func(t *testing.T) {
|
||||
cmd := CompletionCommand("beacon-chain")
|
||||
require.Equal(t, "completion", cmd.Name)
|
||||
})
|
||||
|
||||
t.Run("has three subcommands", func(t *testing.T) {
|
||||
cmd := CompletionCommand("beacon-chain")
|
||||
require.Equal(t, 3, len(cmd.Subcommands))
|
||||
|
||||
names := make([]string, len(cmd.Subcommands))
|
||||
for i, sub := range cmd.Subcommands {
|
||||
names[i] = sub.Name
|
||||
}
|
||||
assert.DeepEqual(t, []string{"bash", "zsh", "fish"}, names)
|
||||
})
|
||||
|
||||
t.Run("description contains binary name", func(t *testing.T) {
|
||||
cmd := CompletionCommand("validator")
|
||||
assert.Equal(t, true, strings.Contains(cmd.Description, "validator"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBashCompletionScript(t *testing.T) {
|
||||
script := bashCompletionScript("beacon-chain")
|
||||
|
||||
assert.Equal(t, true, strings.Contains(script, "beacon-chain"), "script should contain binary name")
|
||||
assert.Equal(t, true, strings.Contains(script, "_beacon_chain_completions"), "script should contain function name with underscores")
|
||||
assert.Equal(t, true, strings.Contains(script, "complete -o bashdefault"), "script should contain complete command")
|
||||
assert.Equal(t, true, strings.Contains(script, "--generate-bash-completion"), "script should use generate-bash-completion flag")
|
||||
}
|
||||
|
||||
func TestZshCompletionScript(t *testing.T) {
|
||||
script := zshCompletionScript("validator")
|
||||
|
||||
assert.Equal(t, true, strings.Contains(script, "#compdef validator"), "script should contain compdef directive")
|
||||
assert.Equal(t, true, strings.Contains(script, "_validator"), "script should contain function name")
|
||||
assert.Equal(t, true, strings.Contains(script, "--generate-bash-completion"), "script should use generate-bash-completion flag")
|
||||
}
|
||||
|
||||
func TestFishCompletionScript(t *testing.T) {
|
||||
script := fishCompletionScript("beacon-chain")
|
||||
|
||||
assert.Equal(t, true, strings.Contains(script, "complete -c beacon-chain"), "script should contain complete command")
|
||||
assert.Equal(t, true, strings.Contains(script, "__fish_beacon_chain_complete"), "script should contain function name with underscores")
|
||||
assert.Equal(t, true, strings.Contains(script, "--generate-bash-completion"), "script should use generate-bash-completion flag")
|
||||
}
|
||||
|
||||
func TestScriptFunctionNames(t *testing.T) {
|
||||
// Test that hyphens are converted to underscores in function names
|
||||
bashScript := bashCompletionScript("beacon-chain")
|
||||
assert.Equal(t, true, strings.Contains(bashScript, "_beacon_chain_completions"))
|
||||
assert.Equal(t, false, strings.Contains(bashScript, "_beacon-chain_completions"))
|
||||
|
||||
zshScript := zshCompletionScript("beacon-chain")
|
||||
assert.Equal(t, true, strings.Contains(zshScript, "_beacon_chain"))
|
||||
|
||||
fishScript := fishCompletionScript("beacon-chain")
|
||||
assert.Equal(t, true, strings.Contains(fishScript, "__fish_beacon_chain_complete"))
|
||||
}
|
||||
|
||||
func TestCompletionSubcommandActions(t *testing.T) {
|
||||
// Test that Action functions execute without errors
|
||||
cmd := CompletionCommand("beacon-chain")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
subcommand string
|
||||
}{
|
||||
{"bash action executes", "bash"},
|
||||
{"zsh action executes", "zsh"},
|
||||
{"fish action executes", "fish"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var subCmd *cli.Command
|
||||
for _, sub := range cmd.Subcommands {
|
||||
if sub.Name == tt.subcommand {
|
||||
subCmd = sub
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, subCmd, "subcommand should exist")
|
||||
require.NotNil(t, subCmd.Action, "subcommand should have an action")
|
||||
|
||||
// Action should not return an error; use a real cli.Context
|
||||
app := &cli.App{}
|
||||
ctx := cli.NewContext(app, nil, nil)
|
||||
err := subCmd.Action(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -140,8 +140,10 @@ func main() {
|
||||
slashingprotectioncommands.Commands,
|
||||
dbcommands.Commands,
|
||||
web.Commands,
|
||||
cmd.CompletionCommand("validator"),
|
||||
},
|
||||
Flags: appFlags,
|
||||
Flags: appFlags,
|
||||
EnableBashCompletion: true,
|
||||
Before: func(ctx *cli.Context) error {
|
||||
// Load flags from config file, if specified.
|
||||
if err := cmd.LoadFlagsFromConfig(ctx, appFlags); err != nil {
|
||||
|
||||
@@ -25,6 +25,7 @@ go_library(
|
||||
],
|
||||
deps = [
|
||||
"//api/grpc:go_default_library",
|
||||
"//api/rest:go_default_library",
|
||||
"//beacon-chain/core/blocks:go_default_library",
|
||||
"//cmd/validator/flags:go_default_library",
|
||||
"//config/fieldparams:go_default_library",
|
||||
|
||||
@@ -3,14 +3,13 @@ package accounts
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
grpcutil "github.com/OffchainLabs/prysm/v7/api/grpc"
|
||||
"github.com/OffchainLabs/prysm/v7/api/rest"
|
||||
"github.com/OffchainLabs/prysm/v7/crypto/bls"
|
||||
"github.com/OffchainLabs/prysm/v7/validator/accounts/wallet"
|
||||
beaconApi "github.com/OffchainLabs/prysm/v7/validator/client/beacon-api"
|
||||
iface "github.com/OffchainLabs/prysm/v7/validator/client/iface"
|
||||
nodeClientFactory "github.com/OffchainLabs/prysm/v7/validator/client/node-client-factory"
|
||||
validatorClientFactory "github.com/OffchainLabs/prysm/v7/validator/client/validator-client-factory"
|
||||
@@ -77,22 +76,17 @@ func (acm *CLIManager) prepareBeaconClients(ctx context.Context) (*iface.Validat
|
||||
}
|
||||
|
||||
ctx = grpcutil.AppendHeaders(ctx, acm.grpcHeaders)
|
||||
grpcConn, err := grpc.DialContext(ctx, acm.beaconRPCProvider, acm.dialOpts...)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "could not dial endpoint %s", acm.beaconRPCProvider)
|
||||
}
|
||||
conn := validatorHelpers.NewNodeConnection(
|
||||
grpcConn,
|
||||
acm.beaconApiEndpoint,
|
||||
validatorHelpers.WithBeaconApiTimeout(acm.beaconApiTimeout),
|
||||
)
|
||||
|
||||
restHandler := beaconApi.NewBeaconApiRestHandler(
|
||||
http.Client{Timeout: acm.beaconApiTimeout},
|
||||
acm.beaconApiEndpoint,
|
||||
conn, err := validatorHelpers.NewNodeConnection(
|
||||
validatorHelpers.WithGrpc(ctx, acm.beaconRPCProvider, acm.dialOpts),
|
||||
validatorHelpers.WithREST(acm.beaconApiEndpoint, rest.WithHttpTimeout(acm.beaconApiTimeout)),
|
||||
)
|
||||
validatorClient := validatorClientFactory.NewValidatorClient(conn, restHandler)
|
||||
nodeClient := nodeClientFactory.NewNodeClient(conn, restHandler)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
validatorClient := validatorClientFactory.NewValidatorClient(conn)
|
||||
nodeClient := nodeClientFactory.NewNodeClient(conn)
|
||||
|
||||
return &validatorClient, &nodeClient, nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ go_library(
|
||||
"log.go",
|
||||
"log_helpers.go",
|
||||
"metrics.go",
|
||||
"multiple_endpoints_grpc_resolver.go",
|
||||
"propose.go",
|
||||
"registration.go",
|
||||
"runner.go",
|
||||
@@ -29,6 +28,7 @@ go_library(
|
||||
"//api/client:go_default_library",
|
||||
"//api/client/event:go_default_library",
|
||||
"//api/grpc:go_default_library",
|
||||
"//api/rest:go_default_library",
|
||||
"//api/server/structs:go_default_library",
|
||||
"//async:go_default_library",
|
||||
"//async/event:go_default_library",
|
||||
@@ -58,7 +58,6 @@ go_library(
|
||||
"//time/slots:go_default_library",
|
||||
"//validator/accounts/iface:go_default_library",
|
||||
"//validator/accounts/wallet:go_default_library",
|
||||
"//validator/client/beacon-api:go_default_library",
|
||||
"//validator/client/beacon-chain-client-factory:go_default_library",
|
||||
"//validator/client/iface:go_default_library",
|
||||
"//validator/client/node-client-factory:go_default_library",
|
||||
@@ -86,13 +85,11 @@ go_library(
|
||||
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
|
||||
"@com_github_sirupsen_logrus//:go_default_library",
|
||||
"@io_opentelemetry_go_contrib_instrumentation_google_golang_org_grpc_otelgrpc//:go_default_library",
|
||||
"@io_opentelemetry_go_contrib_instrumentation_net_http_otelhttp//:go_default_library",
|
||||
"@io_opentelemetry_go_otel_trace//:go_default_library",
|
||||
"@org_golang_google_grpc//:go_default_library",
|
||||
"@org_golang_google_grpc//codes:go_default_library",
|
||||
"@org_golang_google_grpc//credentials:go_default_library",
|
||||
"@org_golang_google_grpc//metadata:go_default_library",
|
||||
"@org_golang_google_grpc//resolver:go_default_library",
|
||||
"@org_golang_google_grpc//status:go_default_library",
|
||||
"@org_golang_google_protobuf//proto:go_default_library",
|
||||
"@org_golang_google_protobuf//types/known/emptypb:go_default_library",
|
||||
@@ -124,6 +121,8 @@ go_test(
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//api/grpc:go_default_library",
|
||||
"//api/rest:go_default_library",
|
||||
"//api/server/structs:go_default_library",
|
||||
"//async/event:go_default_library",
|
||||
"//beacon-chain/core/signing:go_default_library",
|
||||
|
||||
@@ -26,7 +26,6 @@ go_library(
|
||||
"propose_exit.go",
|
||||
"prysm_beacon_chain_client.go",
|
||||
"registration.go",
|
||||
"rest_handler_client.go",
|
||||
"state_validators.go",
|
||||
"status.go",
|
||||
"stream_blocks.go",
|
||||
@@ -43,6 +42,7 @@ go_library(
|
||||
"//api:go_default_library",
|
||||
"//api/apiutil:go_default_library",
|
||||
"//api/client/event:go_default_library",
|
||||
"//api/rest:go_default_library",
|
||||
"//api/server/structs:go_default_library",
|
||||
"//beacon-chain/core/helpers:go_default_library",
|
||||
"//beacon-chain/core/signing:go_default_library",
|
||||
@@ -111,6 +111,7 @@ go_test(
|
||||
deps = [
|
||||
"//api:go_default_library",
|
||||
"//api/apiutil:go_default_library",
|
||||
"//api/rest:go_default_library",
|
||||
"//api/server/structs:go_default_library",
|
||||
"//beacon-chain/core/helpers:go_default_library",
|
||||
"//beacon-chain/rpc/eth/shared/testing:go_default_library",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package beacon_api
|
||||
|
||||
import (
|
||||
"github.com/OffchainLabs/prysm/v7/api/rest"
|
||||
"context"
|
||||
"reflect"
|
||||
"strconv"
|
||||
@@ -17,7 +18,7 @@ import (
|
||||
|
||||
type beaconApiChainClient struct {
|
||||
fallbackClient iface.ChainClient
|
||||
jsonRestHandler RestHandler
|
||||
jsonRestHandler rest.RestHandler
|
||||
stateValidatorsProvider StateValidatorsProvider
|
||||
}
|
||||
|
||||
@@ -327,7 +328,7 @@ func (c beaconApiChainClient) ValidatorParticipation(ctx context.Context, in *et
|
||||
return nil, errors.New("beaconApiChainClient.ValidatorParticipation is not implemented. To use a fallback client, pass a fallback client as the last argument of NewBeaconApiChainClientWithFallback.")
|
||||
}
|
||||
|
||||
func NewBeaconApiChainClientWithFallback(jsonRestHandler RestHandler, fallbackClient iface.ChainClient) iface.ChainClient {
|
||||
func NewBeaconApiChainClientWithFallback(jsonRestHandler rest.RestHandler, fallbackClient iface.ChainClient) iface.ChainClient {
|
||||
return &beaconApiChainClient{
|
||||
jsonRestHandler: jsonRestHandler,
|
||||
fallbackClient: fallbackClient,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package beacon_api
|
||||
|
||||
import (
|
||||
"github.com/OffchainLabs/prysm/v7/api/rest"
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -20,7 +21,7 @@ var (
|
||||
|
||||
type beaconApiNodeClient struct {
|
||||
fallbackClient iface.NodeClient
|
||||
jsonRestHandler RestHandler
|
||||
jsonRestHandler rest.RestHandler
|
||||
genesisProvider GenesisProvider
|
||||
}
|
||||
|
||||
@@ -115,7 +116,7 @@ func (c *beaconApiNodeClient) IsReady(ctx context.Context) bool {
|
||||
return statusCode == http.StatusOK
|
||||
}
|
||||
|
||||
func NewNodeClientWithFallback(jsonRestHandler RestHandler, fallbackClient iface.NodeClient) iface.NodeClient {
|
||||
func NewNodeClientWithFallback(jsonRestHandler rest.RestHandler, fallbackClient iface.NodeClient) iface.NodeClient {
|
||||
b := &beaconApiNodeClient{
|
||||
jsonRestHandler: jsonRestHandler,
|
||||
fallbackClient: fallbackClient,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package beacon_api
|
||||
|
||||
import (
|
||||
"github.com/OffchainLabs/prysm/v7/api/rest"
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -22,13 +23,13 @@ type beaconApiValidatorClient struct {
|
||||
genesisProvider GenesisProvider
|
||||
dutiesProvider dutiesProvider
|
||||
stateValidatorsProvider StateValidatorsProvider
|
||||
jsonRestHandler RestHandler
|
||||
jsonRestHandler rest.RestHandler
|
||||
beaconBlockConverter BeaconBlockConverter
|
||||
prysmChainClient iface.PrysmChainClient
|
||||
isEventStreamRunning bool
|
||||
}
|
||||
|
||||
func NewBeaconApiValidatorClient(jsonRestHandler RestHandler, opts ...ValidatorClientOpt) iface.ValidatorClient {
|
||||
func NewBeaconApiValidatorClient(jsonRestHandler rest.RestHandler, opts ...ValidatorClientOpt) iface.ValidatorClient {
|
||||
c := &beaconApiValidatorClient{
|
||||
genesisProvider: &beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler},
|
||||
dutiesProvider: beaconApiDutiesProvider{jsonRestHandler: jsonRestHandler},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package beacon_api
|
||||
|
||||
import (
|
||||
"github.com/OffchainLabs/prysm/v7/api/rest"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -27,7 +28,7 @@ type dutiesProvider interface {
|
||||
}
|
||||
|
||||
type beaconApiDutiesProvider struct {
|
||||
jsonRestHandler RestHandler
|
||||
jsonRestHandler rest.RestHandler
|
||||
}
|
||||
|
||||
type attesterDuty struct {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package beacon_api
|
||||
|
||||
import (
|
||||
"github.com/OffchainLabs/prysm/v7/api/rest"
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -20,7 +21,7 @@ type GenesisProvider interface {
|
||||
}
|
||||
|
||||
type beaconApiGenesisProvider struct {
|
||||
jsonRestHandler RestHandler
|
||||
jsonRestHandler rest.RestHandler
|
||||
genesis *structs.Genesis
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package beacon_api
|
||||
|
||||
import (
|
||||
"github.com/OffchainLabs/prysm/v7/api/rest"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
// NewPrysmChainClient returns implementation of iface.PrysmChainClient.
|
||||
func NewPrysmChainClient(jsonRestHandler RestHandler, nodeClient iface.NodeClient) iface.PrysmChainClient {
|
||||
func NewPrysmChainClient(jsonRestHandler rest.RestHandler, nodeClient iface.NodeClient) iface.PrysmChainClient {
|
||||
return prysmChainClient{
|
||||
jsonRestHandler: jsonRestHandler,
|
||||
nodeClient: nodeClient,
|
||||
@@ -26,7 +27,7 @@ func NewPrysmChainClient(jsonRestHandler RestHandler, nodeClient iface.NodeClien
|
||||
}
|
||||
|
||||
type prysmChainClient struct {
|
||||
jsonRestHandler RestHandler
|
||||
jsonRestHandler rest.RestHandler
|
||||
nodeClient iface.NodeClient
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/api"
|
||||
"github.com/OffchainLabs/prysm/v7/api/rest"
|
||||
"github.com/OffchainLabs/prysm/v7/api/server/structs"
|
||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||
"github.com/OffchainLabs/prysm/v7/network/httputil"
|
||||
"github.com/OffchainLabs/prysm/v7/runtime/version"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/assert"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/require"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
)
|
||||
@@ -45,10 +44,7 @@ func TestGet(t *testing.T) {
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
jsonRestHandler := BeaconApiRestHandler{
|
||||
client: http.Client{Timeout: time.Second * 5},
|
||||
host: server.URL,
|
||||
}
|
||||
jsonRestHandler := rest.NewRestHandler(http.Client{Timeout: time.Second * 5}, server.URL)
|
||||
resp := &structs.GetGenesisResponse{}
|
||||
require.NoError(t, jsonRestHandler.Get(ctx, endpoint+"?arg1=abc&arg2=def", resp))
|
||||
assert.DeepEqual(t, genesisJson, resp)
|
||||
@@ -79,10 +75,7 @@ func TestGetSSZ(t *testing.T) {
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
jsonRestHandler := BeaconApiRestHandler{
|
||||
client: http.Client{Timeout: time.Second * 5},
|
||||
host: server.URL,
|
||||
}
|
||||
jsonRestHandler := rest.NewRestHandler(http.Client{Timeout: time.Second * 5}, server.URL)
|
||||
|
||||
body, header, err := jsonRestHandler.GetSSZ(ctx, endpoint)
|
||||
require.NoError(t, err)
|
||||
@@ -108,10 +101,7 @@ func TestGetSSZ(t *testing.T) {
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
jsonRestHandler := BeaconApiRestHandler{
|
||||
client: http.Client{Timeout: time.Second * 5},
|
||||
host: server.URL,
|
||||
}
|
||||
jsonRestHandler := rest.NewRestHandler(http.Client{Timeout: time.Second * 5}, server.URL)
|
||||
|
||||
body, header, err := jsonRestHandler.GetSSZ(ctx, endpoint)
|
||||
require.NoError(t, err)
|
||||
@@ -136,10 +126,7 @@ func TestGetSSZ(t *testing.T) {
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
jsonRestHandler := BeaconApiRestHandler{
|
||||
client: http.Client{Timeout: time.Second * 5},
|
||||
host: server.URL,
|
||||
}
|
||||
jsonRestHandler := rest.NewRestHandler(http.Client{Timeout: time.Second * 5}, server.URL)
|
||||
|
||||
_, _, err := jsonRestHandler.GetSSZ(ctx, endpoint)
|
||||
require.NoError(t, err)
|
||||
@@ -161,7 +148,7 @@ func TestAcceptOverrideSSZ(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := NewBeaconApiRestHandler(http.Client{Timeout: time.Second * 5}, srv.URL)
|
||||
c := rest.NewRestHandler(http.Client{Timeout: time.Second * 5}, srv.URL)
|
||||
_, _, err := c.GetSSZ(t.Context(), "/test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -204,162 +191,12 @@ func TestPost(t *testing.T) {
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
jsonRestHandler := BeaconApiRestHandler{
|
||||
client: http.Client{Timeout: time.Second * 5},
|
||||
host: server.URL,
|
||||
}
|
||||
jsonRestHandler := rest.NewRestHandler(http.Client{Timeout: time.Second * 5}, server.URL)
|
||||
resp := &structs.GetGenesisResponse{}
|
||||
require.NoError(t, jsonRestHandler.Post(ctx, endpoint, headers, bytes.NewBuffer(dataBytes), resp))
|
||||
assert.DeepEqual(t, genesisJson, resp)
|
||||
}
|
||||
|
||||
func Test_decodeResp(t *testing.T) {
|
||||
type j struct {
|
||||
Foo string `json:"foo"`
|
||||
}
|
||||
t.Run("200 JSON with charset", func(t *testing.T) {
|
||||
body := bytes.Buffer{}
|
||||
r := &http.Response{
|
||||
Status: "200",
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(&body),
|
||||
Header: map[string][]string{"Content-Type": {"application/json; charset=utf-8"}},
|
||||
}
|
||||
require.NoError(t, decodeResp(r, nil))
|
||||
})
|
||||
t.Run("200 non-JSON", func(t *testing.T) {
|
||||
body := bytes.Buffer{}
|
||||
r := &http.Response{
|
||||
Status: "200",
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(&body),
|
||||
Header: map[string][]string{"Content-Type": {api.OctetStreamMediaType}},
|
||||
}
|
||||
require.NoError(t, decodeResp(r, nil))
|
||||
})
|
||||
t.Run("204 non-JSON", func(t *testing.T) {
|
||||
body := bytes.Buffer{}
|
||||
r := &http.Response{
|
||||
Status: "204",
|
||||
StatusCode: http.StatusNoContent,
|
||||
Body: io.NopCloser(&body),
|
||||
Header: map[string][]string{"Content-Type": {api.OctetStreamMediaType}},
|
||||
}
|
||||
require.NoError(t, decodeResp(r, nil))
|
||||
})
|
||||
t.Run("500 non-JSON", func(t *testing.T) {
|
||||
body := bytes.Buffer{}
|
||||
_, err := body.WriteString("foo")
|
||||
require.NoError(t, err)
|
||||
r := &http.Response{
|
||||
Status: "500",
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Body: io.NopCloser(&body),
|
||||
Header: map[string][]string{"Content-Type": {api.OctetStreamMediaType}},
|
||||
}
|
||||
err = decodeResp(r, nil)
|
||||
errJson := &httputil.DefaultJsonError{}
|
||||
require.Equal(t, true, errors.As(err, &errJson))
|
||||
assert.Equal(t, http.StatusInternalServerError, errJson.Code)
|
||||
assert.Equal(t, "foo", errJson.Message)
|
||||
})
|
||||
t.Run("200 JSON with resp", func(t *testing.T) {
|
||||
body := bytes.Buffer{}
|
||||
b, err := json.Marshal(&j{Foo: "foo"})
|
||||
require.NoError(t, err)
|
||||
body.Write(b)
|
||||
r := &http.Response{
|
||||
Status: "200",
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(&body),
|
||||
Header: map[string][]string{"Content-Type": {api.JsonMediaType}},
|
||||
}
|
||||
resp := &j{}
|
||||
require.NoError(t, decodeResp(r, resp))
|
||||
assert.Equal(t, "foo", resp.Foo)
|
||||
})
|
||||
t.Run("200 JSON without resp", func(t *testing.T) {
|
||||
body := bytes.Buffer{}
|
||||
r := &http.Response{
|
||||
Status: "200",
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(&body),
|
||||
Header: map[string][]string{"Content-Type": {api.JsonMediaType}},
|
||||
}
|
||||
require.NoError(t, decodeResp(r, nil))
|
||||
})
|
||||
t.Run("204 JSON", func(t *testing.T) {
|
||||
body := bytes.Buffer{}
|
||||
r := &http.Response{
|
||||
Status: "204",
|
||||
StatusCode: http.StatusNoContent,
|
||||
Body: io.NopCloser(&body),
|
||||
Header: map[string][]string{"Content-Type": {api.JsonMediaType}},
|
||||
}
|
||||
require.NoError(t, decodeResp(r, nil))
|
||||
})
|
||||
t.Run("500 JSON", func(t *testing.T) {
|
||||
body := bytes.Buffer{}
|
||||
b, err := json.Marshal(&httputil.DefaultJsonError{Code: http.StatusInternalServerError, Message: "error"})
|
||||
require.NoError(t, err)
|
||||
body.Write(b)
|
||||
r := &http.Response{
|
||||
Status: "500",
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Body: io.NopCloser(&body),
|
||||
Header: map[string][]string{"Content-Type": {api.JsonMediaType}},
|
||||
}
|
||||
err = decodeResp(r, nil)
|
||||
errJson := &httputil.DefaultJsonError{}
|
||||
require.Equal(t, true, errors.As(err, &errJson))
|
||||
assert.Equal(t, http.StatusInternalServerError, errJson.Code)
|
||||
assert.Equal(t, "error", errJson.Message)
|
||||
})
|
||||
t.Run("200 JSON cannot decode", func(t *testing.T) {
|
||||
body := bytes.Buffer{}
|
||||
_, err := body.WriteString("foo")
|
||||
require.NoError(t, err)
|
||||
r := &http.Response{
|
||||
Status: "200",
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(&body),
|
||||
Header: map[string][]string{"Content-Type": {api.JsonMediaType}},
|
||||
Request: &http.Request{},
|
||||
}
|
||||
resp := &j{}
|
||||
err = decodeResp(r, resp)
|
||||
assert.ErrorContains(t, "failed to decode response body into json", err)
|
||||
})
|
||||
t.Run("500 JSON cannot decode", func(t *testing.T) {
|
||||
body := bytes.Buffer{}
|
||||
_, err := body.WriteString("foo")
|
||||
require.NoError(t, err)
|
||||
r := &http.Response{
|
||||
Status: "500",
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Body: io.NopCloser(&body),
|
||||
Header: map[string][]string{"Content-Type": {api.JsonMediaType}},
|
||||
Request: &http.Request{},
|
||||
}
|
||||
err = decodeResp(r, nil)
|
||||
assert.ErrorContains(t, "failed to decode response body into error json", err)
|
||||
})
|
||||
t.Run("500 not JSON", func(t *testing.T) {
|
||||
body := bytes.Buffer{}
|
||||
_, err := body.WriteString("foo")
|
||||
require.NoError(t, err)
|
||||
r := &http.Response{
|
||||
Status: "500",
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Body: io.NopCloser(&body),
|
||||
Header: map[string][]string{"Content-Type": {"text/plain"}},
|
||||
Request: &http.Request{},
|
||||
}
|
||||
err = decodeResp(r, nil)
|
||||
assert.ErrorContains(t, "HTTP request unsuccessful (500: foo)", err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetStatusCode(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
const endpoint = "/eth/v1/node/health"
|
||||
@@ -401,10 +238,7 @@ func TestGetStatusCode(t *testing.T) {
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
jsonRestHandler := BeaconApiRestHandler{
|
||||
client: http.Client{Timeout: time.Second * 5},
|
||||
host: server.URL,
|
||||
}
|
||||
jsonRestHandler := rest.NewRestHandler(http.Client{Timeout: time.Second * 5}, server.URL)
|
||||
|
||||
statusCode, err := jsonRestHandler.GetStatusCode(ctx, endpoint)
|
||||
require.NoError(t, err)
|
||||
@@ -413,10 +247,7 @@ func TestGetStatusCode(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("returns error on connection failure", func(t *testing.T) {
|
||||
jsonRestHandler := BeaconApiRestHandler{
|
||||
client: http.Client{Timeout: time.Millisecond * 100},
|
||||
host: "http://localhost:99999", // Invalid port
|
||||
}
|
||||
jsonRestHandler := rest.NewRestHandler(http.Client{Timeout: time.Millisecond * 100}, "http://localhost:99999")
|
||||
|
||||
_, err := jsonRestHandler.GetStatusCode(ctx, endpoint)
|
||||
require.ErrorContains(t, "failed to perform request", err)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package beacon_api
|
||||
|
||||
import (
|
||||
"github.com/OffchainLabs/prysm/v7/api/rest"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -21,7 +22,7 @@ type StateValidatorsProvider interface {
|
||||
}
|
||||
|
||||
type beaconApiStateValidatorsProvider struct {
|
||||
jsonRestHandler RestHandler
|
||||
jsonRestHandler rest.RestHandler
|
||||
}
|
||||
|
||||
func (c beaconApiStateValidatorsProvider) StateValidators(
|
||||
|
||||
@@ -9,19 +9,17 @@ import (
|
||||
validatorHelpers "github.com/OffchainLabs/prysm/v7/validator/helpers"
|
||||
)
|
||||
|
||||
func NewChainClient(validatorConn validatorHelpers.NodeConnection, jsonRestHandler beaconApi.RestHandler) iface.ChainClient {
|
||||
grpcClient := grpcApi.NewGrpcChainClient(validatorConn.GetGrpcClientConn())
|
||||
func NewChainClient(validatorConn validatorHelpers.NodeConnection) iface.ChainClient {
|
||||
grpcClient := grpcApi.NewGrpcChainClient(validatorConn)
|
||||
if features.Get().EnableBeaconRESTApi {
|
||||
return beaconApi.NewBeaconApiChainClientWithFallback(jsonRestHandler, grpcClient)
|
||||
} else {
|
||||
return grpcClient
|
||||
return beaconApi.NewBeaconApiChainClientWithFallback(validatorConn.GetRestHandler(), grpcClient)
|
||||
}
|
||||
return grpcClient
|
||||
}
|
||||
|
||||
func NewPrysmChainClient(validatorConn validatorHelpers.NodeConnection, jsonRestHandler beaconApi.RestHandler) iface.PrysmChainClient {
|
||||
func NewPrysmChainClient(validatorConn validatorHelpers.NodeConnection) iface.PrysmChainClient {
|
||||
if features.Get().EnableBeaconRESTApi {
|
||||
return beaconApi.NewPrysmChainClient(jsonRestHandler, nodeClientFactory.NewNodeClient(validatorConn, jsonRestHandler))
|
||||
} else {
|
||||
return grpcApi.NewGrpcPrysmChainClient(validatorConn.GetGrpcClientConn())
|
||||
return beaconApi.NewPrysmChainClient(validatorConn.GetRestHandler(), nodeClientFactory.NewNodeClient(validatorConn))
|
||||
}
|
||||
return grpcApi.NewGrpcPrysmChainClient(validatorConn)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"grpc_beacon_chain_client.go",
|
||||
"grpc_client_manager.go",
|
||||
"grpc_node_client.go",
|
||||
"grpc_prysm_beacon_chain_client.go",
|
||||
"grpc_validator_client.go",
|
||||
@@ -25,6 +26,7 @@ go_library(
|
||||
"//proto/eth/v1:go_default_library",
|
||||
"//proto/prysm/v1alpha1:go_default_library",
|
||||
"//validator/client/iface:go_default_library",
|
||||
"//validator/helpers:go_default_library",
|
||||
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
|
||||
"@com_github_golang_protobuf//ptypes/empty",
|
||||
"@com_github_pkg_errors//:go_default_library",
|
||||
@@ -39,6 +41,7 @@ go_test(
|
||||
name = "go_default_test",
|
||||
size = "small",
|
||||
srcs = [
|
||||
"grpc_client_manager_test.go",
|
||||
"grpc_prysm_beacon_chain_client_test.go",
|
||||
"grpc_validator_client_test.go",
|
||||
],
|
||||
@@ -56,7 +59,9 @@ go_test(
|
||||
"//testing/util:go_default_library",
|
||||
"//testing/validator-mock:go_default_library",
|
||||
"//validator/client/iface:go_default_library",
|
||||
"//validator/helpers:go_default_library",
|
||||
"@com_github_sirupsen_logrus//hooks/test:go_default_library",
|
||||
"@org_golang_google_grpc//:go_default_library",
|
||||
"@org_golang_google_protobuf//types/known/emptypb:go_default_library",
|
||||
"@org_uber_go_mock//gomock:go_default_library",
|
||||
],
|
||||
|
||||
@@ -5,38 +5,42 @@ import (
|
||||
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/OffchainLabs/prysm/v7/validator/client/iface"
|
||||
validatorHelpers "github.com/OffchainLabs/prysm/v7/validator/helpers"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type grpcChainClient struct {
|
||||
beaconChainClient ethpb.BeaconChainClient
|
||||
*grpcClientManager[ethpb.BeaconChainClient]
|
||||
}
|
||||
|
||||
func (c *grpcChainClient) ChainHead(ctx context.Context, in *empty.Empty) (*ethpb.ChainHead, error) {
|
||||
return c.beaconChainClient.GetChainHead(ctx, in)
|
||||
return c.getClient().GetChainHead(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcChainClient) ValidatorBalances(ctx context.Context, in *ethpb.ListValidatorBalancesRequest) (*ethpb.ValidatorBalances, error) {
|
||||
return c.beaconChainClient.ListValidatorBalances(ctx, in)
|
||||
return c.getClient().ListValidatorBalances(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcChainClient) Validators(ctx context.Context, in *ethpb.ListValidatorsRequest) (*ethpb.Validators, error) {
|
||||
return c.beaconChainClient.ListValidators(ctx, in)
|
||||
return c.getClient().ListValidators(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcChainClient) ValidatorQueue(ctx context.Context, in *empty.Empty) (*ethpb.ValidatorQueue, error) {
|
||||
return c.beaconChainClient.GetValidatorQueue(ctx, in)
|
||||
return c.getClient().GetValidatorQueue(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcChainClient) ValidatorPerformance(ctx context.Context, in *ethpb.ValidatorPerformanceRequest) (*ethpb.ValidatorPerformanceResponse, error) {
|
||||
return c.beaconChainClient.GetValidatorPerformance(ctx, in)
|
||||
return c.getClient().GetValidatorPerformance(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcChainClient) ValidatorParticipation(ctx context.Context, in *ethpb.GetValidatorParticipationRequest) (*ethpb.ValidatorParticipationResponse, error) {
|
||||
return c.beaconChainClient.GetValidatorParticipation(ctx, in)
|
||||
return c.getClient().GetValidatorParticipation(ctx, in)
|
||||
}
|
||||
|
||||
func NewGrpcChainClient(cc grpc.ClientConnInterface) iface.ChainClient {
|
||||
return &grpcChainClient{ethpb.NewBeaconChainClient(cc)}
|
||||
// NewGrpcChainClient creates a new gRPC chain client that supports
|
||||
// dynamic connection switching via the NodeConnection's GrpcConnectionProvider.
|
||||
func NewGrpcChainClient(conn validatorHelpers.NodeConnection) iface.ChainClient {
|
||||
return &grpcChainClient{
|
||||
grpcClientManager: newGrpcClientManager(conn, ethpb.NewBeaconChainClient),
|
||||
}
|
||||
}
|
||||
|
||||
58
validator/client/grpc-api/grpc_client_manager.go
Normal file
58
validator/client/grpc-api/grpc_client_manager.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package grpc_api
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
validatorHelpers "github.com/OffchainLabs/prysm/v7/validator/helpers"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// grpcClientManager handles dynamic gRPC client recreation when the connection changes.
|
||||
// It uses generics to work with any gRPC client type.
|
||||
type grpcClientManager[T any] struct {
|
||||
sync.RWMutex
|
||||
conn validatorHelpers.NodeConnection
|
||||
client T
|
||||
lastHost string
|
||||
newClient func(grpc.ClientConnInterface) T
|
||||
}
|
||||
|
||||
// newGrpcClientManager creates a new client manager with the given connection and client constructor.
|
||||
func newGrpcClientManager[T any](
|
||||
conn validatorHelpers.NodeConnection,
|
||||
newClient func(grpc.ClientConnInterface) T,
|
||||
) *grpcClientManager[T] {
|
||||
return &grpcClientManager[T]{
|
||||
conn: conn,
|
||||
newClient: newClient,
|
||||
client: newClient(conn.GetGrpcClientConn()),
|
||||
lastHost: conn.GetGrpcConnectionProvider().CurrentHost(),
|
||||
}
|
||||
}
|
||||
|
||||
// getClient returns the current client, recreating it if the connection has changed.
|
||||
func (m *grpcClientManager[T]) getClient() T {
|
||||
// Safety check for tests that create manager directly without connection
|
||||
if m.conn == nil || m.conn.GetGrpcConnectionProvider() == nil {
|
||||
return m.client
|
||||
}
|
||||
currentHost := m.conn.GetGrpcConnectionProvider().CurrentHost()
|
||||
m.RLock()
|
||||
if m.lastHost == currentHost {
|
||||
client := m.client
|
||||
m.RUnlock()
|
||||
return client
|
||||
}
|
||||
m.RUnlock()
|
||||
|
||||
// Connection changed, need to recreate client
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
// Double-check after acquiring write lock
|
||||
if m.lastHost == currentHost {
|
||||
return m.client
|
||||
}
|
||||
m.client = m.newClient(m.conn.GetGrpcClientConn())
|
||||
m.lastHost = currentHost
|
||||
return m.client
|
||||
}
|
||||
168
validator/client/grpc-api/grpc_client_manager_test.go
Normal file
168
validator/client/grpc-api/grpc_client_manager_test.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package grpc_api
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/testing/assert"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/require"
|
||||
validatorHelpers "github.com/OffchainLabs/prysm/v7/validator/helpers"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// mockProvider implements grpcutil.GrpcConnectionProvider for testing.
|
||||
type mockProvider struct {
|
||||
hosts []string
|
||||
currentIndex int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (m *mockProvider) CurrentConn() *grpc.ClientConn { return nil }
|
||||
func (m *mockProvider) Hosts() []string { return m.hosts }
|
||||
func (m *mockProvider) Close() error { return nil }
|
||||
|
||||
func (m *mockProvider) CurrentHost() string {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.hosts[m.currentIndex]
|
||||
}
|
||||
|
||||
func (m *mockProvider) SetHost(index int) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.currentIndex = index
|
||||
return nil
|
||||
}
|
||||
|
||||
// nextHost is a test helper for round-robin simulation (not part of the interface).
|
||||
func (m *mockProvider) nextHost() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.currentIndex = (m.currentIndex + 1) % len(m.hosts)
|
||||
}
|
||||
|
||||
// testClient is a simple type for testing the generic client manager.
|
||||
type testClient struct{ id int }
|
||||
|
||||
// testManager creates a manager with client creation counting.
|
||||
func testManager(t *testing.T, provider *mockProvider) (*grpcClientManager[*testClient], *int) {
|
||||
conn, err := validatorHelpers.NewNodeConnection(validatorHelpers.WithGrpcProvider(provider))
|
||||
require.NoError(t, err)
|
||||
|
||||
clientCount := new(int)
|
||||
newClient := func(grpc.ClientConnInterface) *testClient {
|
||||
*clientCount++
|
||||
return &testClient{id: *clientCount}
|
||||
}
|
||||
|
||||
manager := newGrpcClientManager(conn, newClient)
|
||||
require.NotNil(t, manager)
|
||||
return manager, clientCount
|
||||
}
|
||||
|
||||
func TestGrpcClientManager(t *testing.T) {
|
||||
t.Run("tracks host", func(t *testing.T) {
|
||||
provider := &mockProvider{hosts: []string{"host1:4000", "host2:4000"}}
|
||||
manager, count := testManager(t, provider)
|
||||
assert.Equal(t, 1, *count)
|
||||
assert.Equal(t, "host1:4000", manager.lastHost)
|
||||
})
|
||||
|
||||
t.Run("same host returns same client", func(t *testing.T) {
|
||||
provider := &mockProvider{hosts: []string{"host1:4000", "host2:4000"}}
|
||||
manager, count := testManager(t, provider)
|
||||
|
||||
c1, c2, c3 := manager.getClient(), manager.getClient(), manager.getClient()
|
||||
assert.Equal(t, 1, *count)
|
||||
assert.Equal(t, c1, c2)
|
||||
assert.Equal(t, c2, c3)
|
||||
})
|
||||
|
||||
t.Run("host change recreates client", func(t *testing.T) {
|
||||
provider := &mockProvider{hosts: []string{"host1:4000", "host2:4000"}}
|
||||
manager, count := testManager(t, provider)
|
||||
|
||||
c1 := manager.getClient()
|
||||
assert.Equal(t, 1, c1.id)
|
||||
|
||||
provider.nextHost()
|
||||
c2 := manager.getClient()
|
||||
assert.Equal(t, 2, *count)
|
||||
assert.Equal(t, 2, c2.id)
|
||||
|
||||
// Same host again - no recreation
|
||||
c3 := manager.getClient()
|
||||
assert.Equal(t, 2, *count)
|
||||
assert.Equal(t, c2, c3)
|
||||
})
|
||||
|
||||
t.Run("multiple host switches", func(t *testing.T) {
|
||||
provider := &mockProvider{hosts: []string{"host1:4000", "host2:4000", "host3:4000"}}
|
||||
manager, count := testManager(t, provider)
|
||||
assert.Equal(t, 1, *count)
|
||||
|
||||
for expected := 2; expected <= 4; expected++ {
|
||||
provider.nextHost()
|
||||
_ = manager.getClient()
|
||||
assert.Equal(t, expected, *count)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGrpcClientManager_Concurrent(t *testing.T) {
|
||||
t.Run("concurrent access same host", func(t *testing.T) {
|
||||
provider := &mockProvider{hosts: []string{"host1:4000", "host2:4000"}}
|
||||
manager, _ := testManager(t, provider)
|
||||
|
||||
var clientCount int
|
||||
var countMu sync.Mutex
|
||||
// Override with thread-safe counter
|
||||
manager.newClient = func(grpc.ClientConnInterface) *testClient {
|
||||
countMu.Lock()
|
||||
clientCount++
|
||||
id := clientCount
|
||||
countMu.Unlock()
|
||||
return &testClient{id: id}
|
||||
}
|
||||
manager.client = manager.newClient(nil)
|
||||
clientCount = 1
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for range 100 {
|
||||
wg.Go(func() { _ = manager.getClient() })
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
countMu.Lock()
|
||||
assert.Equal(t, 1, clientCount)
|
||||
countMu.Unlock()
|
||||
})
|
||||
|
||||
t.Run("concurrent with host changes", func(t *testing.T) {
|
||||
provider := &mockProvider{hosts: []string{"host1:4000", "host2:4000"}}
|
||||
manager, _ := testManager(t, provider)
|
||||
|
||||
var clientCount int
|
||||
var countMu sync.Mutex
|
||||
manager.newClient = func(grpc.ClientConnInterface) *testClient {
|
||||
countMu.Lock()
|
||||
clientCount++
|
||||
id := clientCount
|
||||
countMu.Unlock()
|
||||
return &testClient{id: id}
|
||||
}
|
||||
manager.client = manager.newClient(nil)
|
||||
clientCount = 1
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for range 50 {
|
||||
wg.Go(func() { _ = manager.getClient() })
|
||||
wg.Go(func() { provider.nextHost() })
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
countMu.Lock()
|
||||
assert.NotEqual(t, 0, clientCount, "Should have created at least one client")
|
||||
countMu.Unlock()
|
||||
})
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/OffchainLabs/prysm/v7/validator/client/iface"
|
||||
validatorHelpers "github.com/OffchainLabs/prysm/v7/validator/helpers"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -14,35 +14,48 @@ var (
|
||||
)
|
||||
|
||||
type grpcNodeClient struct {
|
||||
nodeClient ethpb.NodeClient
|
||||
*grpcClientManager[ethpb.NodeClient]
|
||||
}
|
||||
|
||||
func (c *grpcNodeClient) SyncStatus(ctx context.Context, in *empty.Empty) (*ethpb.SyncStatus, error) {
|
||||
return c.nodeClient.GetSyncStatus(ctx, in)
|
||||
return c.getClient().GetSyncStatus(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcNodeClient) Genesis(ctx context.Context, in *empty.Empty) (*ethpb.Genesis, error) {
|
||||
return c.nodeClient.GetGenesis(ctx, in)
|
||||
return c.getClient().GetGenesis(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcNodeClient) Version(ctx context.Context, in *empty.Empty) (*ethpb.Version, error) {
|
||||
return c.nodeClient.GetVersion(ctx, in)
|
||||
return c.getClient().GetVersion(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcNodeClient) Peers(ctx context.Context, in *empty.Empty) (*ethpb.Peers, error) {
|
||||
return c.nodeClient.ListPeers(ctx, in)
|
||||
return c.getClient().ListPeers(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcNodeClient) IsReady(ctx context.Context) bool {
|
||||
_, err := c.nodeClient.GetHealth(ctx, ðpb.HealthRequest{})
|
||||
_, err := c.getClient().GetHealth(ctx, ðpb.HealthRequest{})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to get health of node")
|
||||
log.WithError(err).Debug("Failed to get health of node")
|
||||
return false
|
||||
}
|
||||
// Then check sync status - we only want fully synced nodes
|
||||
syncStatus, err := c.getClient().GetSyncStatus(ctx, &empty.Empty{})
|
||||
if err != nil {
|
||||
log.WithError(err).Debug("Failed to get sync status of node")
|
||||
return false
|
||||
}
|
||||
if syncStatus.Syncing {
|
||||
log.Debug("Node is syncing, not fully synced")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func NewNodeClient(cc grpc.ClientConnInterface) iface.NodeClient {
|
||||
g := &grpcNodeClient{nodeClient: ethpb.NewNodeClient(cc)}
|
||||
return g
|
||||
// NewNodeClient creates a new gRPC node client that supports
|
||||
// dynamic connection switching via the NodeConnection's GrpcConnectionProvider.
|
||||
func NewNodeClient(conn validatorHelpers.NodeConnection) iface.NodeClient {
|
||||
return &grpcNodeClient{
|
||||
grpcClientManager: newGrpcClientManager(conn, ethpb.NewNodeClient),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
eth "github.com/OffchainLabs/prysm/v7/proto/eth/v1"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/OffchainLabs/prysm/v7/validator/client/iface"
|
||||
validatorHelpers "github.com/OffchainLabs/prysm/v7/validator/helpers"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type grpcPrysmChainClient struct {
|
||||
@@ -95,6 +95,8 @@ func (c *grpcPrysmChainClient) ValidatorPerformance(ctx context.Context, in *eth
|
||||
return c.chainClient.ValidatorPerformance(ctx, in)
|
||||
}
|
||||
|
||||
func NewGrpcPrysmChainClient(cc grpc.ClientConnInterface) iface.PrysmChainClient {
|
||||
return &grpcPrysmChainClient{chainClient: &grpcChainClient{ethpb.NewBeaconChainClient(cc)}}
|
||||
// NewGrpcPrysmChainClient creates a new gRPC Prysm chain client that supports
|
||||
// dynamic connection switching via the NodeConnection's GrpcConnectionProvider.
|
||||
func NewGrpcPrysmChainClient(conn validatorHelpers.NodeConnection) iface.PrysmChainClient {
|
||||
return &grpcPrysmChainClient{chainClient: NewGrpcChainClient(conn)}
|
||||
}
|
||||
|
||||
@@ -14,24 +14,24 @@ import (
|
||||
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/OffchainLabs/prysm/v7/validator/client/iface"
|
||||
validatorHelpers "github.com/OffchainLabs/prysm/v7/validator/helpers"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type grpcValidatorClient struct {
|
||||
beaconNodeValidatorClient ethpb.BeaconNodeValidatorClient
|
||||
isEventStreamRunning bool
|
||||
*grpcClientManager[ethpb.BeaconNodeValidatorClient]
|
||||
isEventStreamRunning bool
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) Duties(ctx context.Context, in *ethpb.DutiesRequest) (*ethpb.ValidatorDutiesContainer, error) {
|
||||
if features.Get().DisableDutiesV2 {
|
||||
return c.getDuties(ctx, in)
|
||||
}
|
||||
dutiesResponse, err := c.beaconNodeValidatorClient.GetDutiesV2(ctx, in)
|
||||
dutiesResponse, err := c.getClient().GetDutiesV2(ctx, in)
|
||||
if err != nil {
|
||||
if status.Code(err) == codes.Unimplemented {
|
||||
log.Warn("GetDutiesV2 returned status code unavailable, falling back to GetDuties")
|
||||
@@ -47,7 +47,7 @@ func (c *grpcValidatorClient) Duties(ctx context.Context, in *ethpb.DutiesReques
|
||||
|
||||
// getDuties is calling the v1 of get duties
|
||||
func (c *grpcValidatorClient) getDuties(ctx context.Context, in *ethpb.DutiesRequest) (*ethpb.ValidatorDutiesContainer, error) {
|
||||
dutiesResponse, err := c.beaconNodeValidatorClient.GetDuties(ctx, in)
|
||||
dutiesResponse, err := c.getClient().GetDuties(ctx, in)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(
|
||||
client.ErrConnectionIssue,
|
||||
@@ -147,108 +147,108 @@ func toValidatorDutyV2(duty *ethpb.DutiesV2Response_Duty) (*ethpb.ValidatorDuty,
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) CheckDoppelGanger(ctx context.Context, in *ethpb.DoppelGangerRequest) (*ethpb.DoppelGangerResponse, error) {
|
||||
return c.beaconNodeValidatorClient.CheckDoppelGanger(ctx, in)
|
||||
return c.getClient().CheckDoppelGanger(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) DomainData(ctx context.Context, in *ethpb.DomainRequest) (*ethpb.DomainResponse, error) {
|
||||
return c.beaconNodeValidatorClient.DomainData(ctx, in)
|
||||
return c.getClient().DomainData(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) AttestationData(ctx context.Context, in *ethpb.AttestationDataRequest) (*ethpb.AttestationData, error) {
|
||||
return c.beaconNodeValidatorClient.GetAttestationData(ctx, in)
|
||||
return c.getClient().GetAttestationData(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) BeaconBlock(ctx context.Context, in *ethpb.BlockRequest) (*ethpb.GenericBeaconBlock, error) {
|
||||
return c.beaconNodeValidatorClient.GetBeaconBlock(ctx, in)
|
||||
return c.getClient().GetBeaconBlock(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) FeeRecipientByPubKey(ctx context.Context, in *ethpb.FeeRecipientByPubKeyRequest) (*ethpb.FeeRecipientByPubKeyResponse, error) {
|
||||
return c.beaconNodeValidatorClient.GetFeeRecipientByPubKey(ctx, in)
|
||||
return c.getClient().GetFeeRecipientByPubKey(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) SyncCommitteeContribution(ctx context.Context, in *ethpb.SyncCommitteeContributionRequest) (*ethpb.SyncCommitteeContribution, error) {
|
||||
return c.beaconNodeValidatorClient.GetSyncCommitteeContribution(ctx, in)
|
||||
return c.getClient().GetSyncCommitteeContribution(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) SyncMessageBlockRoot(ctx context.Context, in *empty.Empty) (*ethpb.SyncMessageBlockRootResponse, error) {
|
||||
return c.beaconNodeValidatorClient.GetSyncMessageBlockRoot(ctx, in)
|
||||
return c.getClient().GetSyncMessageBlockRoot(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) SyncSubcommitteeIndex(ctx context.Context, in *ethpb.SyncSubcommitteeIndexRequest) (*ethpb.SyncSubcommitteeIndexResponse, error) {
|
||||
return c.beaconNodeValidatorClient.GetSyncSubcommitteeIndex(ctx, in)
|
||||
return c.getClient().GetSyncSubcommitteeIndex(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) MultipleValidatorStatus(ctx context.Context, in *ethpb.MultipleValidatorStatusRequest) (*ethpb.MultipleValidatorStatusResponse, error) {
|
||||
return c.beaconNodeValidatorClient.MultipleValidatorStatus(ctx, in)
|
||||
return c.getClient().MultipleValidatorStatus(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) PrepareBeaconProposer(ctx context.Context, in *ethpb.PrepareBeaconProposerRequest) (*empty.Empty, error) {
|
||||
return c.beaconNodeValidatorClient.PrepareBeaconProposer(ctx, in)
|
||||
return c.getClient().PrepareBeaconProposer(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) ProposeAttestation(ctx context.Context, in *ethpb.Attestation) (*ethpb.AttestResponse, error) {
|
||||
return c.beaconNodeValidatorClient.ProposeAttestation(ctx, in)
|
||||
return c.getClient().ProposeAttestation(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) ProposeAttestationElectra(ctx context.Context, in *ethpb.SingleAttestation) (*ethpb.AttestResponse, error) {
|
||||
return c.beaconNodeValidatorClient.ProposeAttestationElectra(ctx, in)
|
||||
return c.getClient().ProposeAttestationElectra(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) ProposeBeaconBlock(ctx context.Context, in *ethpb.GenericSignedBeaconBlock) (*ethpb.ProposeResponse, error) {
|
||||
return c.beaconNodeValidatorClient.ProposeBeaconBlock(ctx, in)
|
||||
return c.getClient().ProposeBeaconBlock(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) ProposeExit(ctx context.Context, in *ethpb.SignedVoluntaryExit) (*ethpb.ProposeExitResponse, error) {
|
||||
return c.beaconNodeValidatorClient.ProposeExit(ctx, in)
|
||||
return c.getClient().ProposeExit(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) StreamBlocksAltair(ctx context.Context, in *ethpb.StreamBlocksRequest) (ethpb.BeaconNodeValidator_StreamBlocksAltairClient, error) {
|
||||
return c.beaconNodeValidatorClient.StreamBlocksAltair(ctx, in)
|
||||
return c.getClient().StreamBlocksAltair(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) SubmitAggregateSelectionProof(ctx context.Context, in *ethpb.AggregateSelectionRequest, _ primitives.ValidatorIndex, _ uint64) (*ethpb.AggregateSelectionResponse, error) {
|
||||
return c.beaconNodeValidatorClient.SubmitAggregateSelectionProof(ctx, in)
|
||||
return c.getClient().SubmitAggregateSelectionProof(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) SubmitAggregateSelectionProofElectra(ctx context.Context, in *ethpb.AggregateSelectionRequest, _ primitives.ValidatorIndex, _ uint64) (*ethpb.AggregateSelectionElectraResponse, error) {
|
||||
return c.beaconNodeValidatorClient.SubmitAggregateSelectionProofElectra(ctx, in)
|
||||
return c.getClient().SubmitAggregateSelectionProofElectra(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) SubmitSignedAggregateSelectionProof(ctx context.Context, in *ethpb.SignedAggregateSubmitRequest) (*ethpb.SignedAggregateSubmitResponse, error) {
|
||||
return c.beaconNodeValidatorClient.SubmitSignedAggregateSelectionProof(ctx, in)
|
||||
return c.getClient().SubmitSignedAggregateSelectionProof(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) SubmitSignedAggregateSelectionProofElectra(ctx context.Context, in *ethpb.SignedAggregateSubmitElectraRequest) (*ethpb.SignedAggregateSubmitResponse, error) {
|
||||
return c.beaconNodeValidatorClient.SubmitSignedAggregateSelectionProofElectra(ctx, in)
|
||||
return c.getClient().SubmitSignedAggregateSelectionProofElectra(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) SubmitSignedContributionAndProof(ctx context.Context, in *ethpb.SignedContributionAndProof) (*empty.Empty, error) {
|
||||
return c.beaconNodeValidatorClient.SubmitSignedContributionAndProof(ctx, in)
|
||||
return c.getClient().SubmitSignedContributionAndProof(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) SubmitSyncMessage(ctx context.Context, in *ethpb.SyncCommitteeMessage) (*empty.Empty, error) {
|
||||
return c.beaconNodeValidatorClient.SubmitSyncMessage(ctx, in)
|
||||
return c.getClient().SubmitSyncMessage(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) SubmitValidatorRegistrations(ctx context.Context, in *ethpb.SignedValidatorRegistrationsV1) (*empty.Empty, error) {
|
||||
return c.beaconNodeValidatorClient.SubmitValidatorRegistrations(ctx, in)
|
||||
return c.getClient().SubmitValidatorRegistrations(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) SubscribeCommitteeSubnets(ctx context.Context, in *ethpb.CommitteeSubnetsSubscribeRequest, _ []*ethpb.ValidatorDuty) (*empty.Empty, error) {
|
||||
return c.beaconNodeValidatorClient.SubscribeCommitteeSubnets(ctx, in)
|
||||
return c.getClient().SubscribeCommitteeSubnets(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) ValidatorIndex(ctx context.Context, in *ethpb.ValidatorIndexRequest) (*ethpb.ValidatorIndexResponse, error) {
|
||||
return c.beaconNodeValidatorClient.ValidatorIndex(ctx, in)
|
||||
return c.getClient().ValidatorIndex(ctx, in)
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) ValidatorStatus(ctx context.Context, in *ethpb.ValidatorStatusRequest) (*ethpb.ValidatorStatusResponse, error) {
|
||||
return c.beaconNodeValidatorClient.ValidatorStatus(ctx, in)
|
||||
return c.getClient().ValidatorStatus(ctx, in)
|
||||
}
|
||||
|
||||
// Deprecated: Do not use.
|
||||
func (c *grpcValidatorClient) WaitForChainStart(ctx context.Context, in *empty.Empty) (*ethpb.ChainStartResponse, error) {
|
||||
stream, err := c.beaconNodeValidatorClient.WaitForChainStart(ctx, in)
|
||||
stream, err := c.getClient().WaitForChainStart(ctx, in)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(
|
||||
client.ErrConnectionIssue,
|
||||
@@ -260,13 +260,13 @@ func (c *grpcValidatorClient) WaitForChainStart(ctx context.Context, in *empty.E
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) AssignValidatorToSubnet(ctx context.Context, in *ethpb.AssignValidatorToSubnetRequest) (*empty.Empty, error) {
|
||||
return c.beaconNodeValidatorClient.AssignValidatorToSubnet(ctx, in)
|
||||
return c.getClient().AssignValidatorToSubnet(ctx, in)
|
||||
}
|
||||
func (c *grpcValidatorClient) AggregatedSigAndAggregationBits(
|
||||
ctx context.Context,
|
||||
in *ethpb.AggregatedSigAndAggregationBitsRequest,
|
||||
) (*ethpb.AggregatedSigAndAggregationBitsResponse, error) {
|
||||
return c.beaconNodeValidatorClient.AggregatedSigAndAggregationBits(ctx, in)
|
||||
return c.getClient().AggregatedSigAndAggregationBits(ctx, in)
|
||||
}
|
||||
|
||||
func (*grpcValidatorClient) AggregatedSelections(context.Context, []iface.BeaconCommitteeSelection) ([]iface.BeaconCommitteeSelection, error) {
|
||||
@@ -277,8 +277,12 @@ func (*grpcValidatorClient) AggregatedSyncSelections(context.Context, []iface.Sy
|
||||
return nil, iface.ErrNotSupported
|
||||
}
|
||||
|
||||
func NewGrpcValidatorClient(cc grpc.ClientConnInterface) iface.ValidatorClient {
|
||||
return &grpcValidatorClient{ethpb.NewBeaconNodeValidatorClient(cc), false}
|
||||
// NewGrpcValidatorClient creates a new gRPC validator client that supports
|
||||
// dynamic connection switching via the NodeConnection's GrpcConnectionProvider.
|
||||
func NewGrpcValidatorClient(conn validatorHelpers.NodeConnection) iface.ValidatorClient {
|
||||
return &grpcValidatorClient{
|
||||
grpcClientManager: newGrpcClientManager(conn, ethpb.NewBeaconNodeValidatorClient),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *grpcValidatorClient) StartEventStream(ctx context.Context, topics []string, eventsChannel chan<- *eventClient.Event) {
|
||||
@@ -308,7 +312,7 @@ func (c *grpcValidatorClient) StartEventStream(ctx context.Context, topics []str
|
||||
log.Warn("gRPC only supports the head topic, other topics will be ignored")
|
||||
}
|
||||
|
||||
stream, err := c.beaconNodeValidatorClient.StreamSlots(ctx, ðpb.StreamSlotsRequest{VerifiedOnly: true})
|
||||
stream, err := c.getClient().StreamSlots(ctx, ðpb.StreamSlotsRequest{VerifiedOnly: true})
|
||||
if err != nil {
|
||||
eventsChannel <- &eventClient.Event{
|
||||
EventType: eventClient.EventConnectionError,
|
||||
@@ -374,11 +378,20 @@ func (c *grpcValidatorClient) EventStreamIsRunning() bool {
|
||||
return c.isEventStreamRunning
|
||||
}
|
||||
|
||||
func (*grpcValidatorClient) Host() string {
|
||||
log.Warn(iface.ErrNotSupported)
|
||||
return ""
|
||||
func (c *grpcValidatorClient) Host() string {
|
||||
return c.grpcClientManager.conn.GetGrpcConnectionProvider().CurrentHost()
|
||||
}
|
||||
|
||||
func (*grpcValidatorClient) SetHost(_ string) {
|
||||
log.Warn(iface.ErrNotSupported)
|
||||
func (c *grpcValidatorClient) SetHost(host string) {
|
||||
provider := c.grpcClientManager.conn.GetGrpcConnectionProvider()
|
||||
// Find the index of the requested host and switch to it
|
||||
for i, h := range provider.Hosts() {
|
||||
if h == host {
|
||||
if err := provider.SetHost(i); err != nil {
|
||||
log.WithError(err).WithField("host", host).Error("Failed to set gRPC host")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
log.WithField("host", host).Warn("Requested gRPC host not found in configured endpoints")
|
||||
}
|
||||
|
||||
@@ -133,7 +133,12 @@ func TestWaitForChainStart_StreamSetupFails(t *testing.T) {
|
||||
gomock.Any(),
|
||||
).Return(nil, errors.New("failed stream"))
|
||||
|
||||
validatorClient := &grpcValidatorClient{beaconNodeValidatorClient, true}
|
||||
validatorClient := &grpcValidatorClient{
|
||||
grpcClientManager: &grpcClientManager[eth.BeaconNodeValidatorClient]{
|
||||
client: beaconNodeValidatorClient,
|
||||
},
|
||||
isEventStreamRunning: true,
|
||||
}
|
||||
_, err := validatorClient.WaitForChainStart(t.Context(), &emptypb.Empty{})
|
||||
want := "could not setup beacon chain ChainStart streaming client"
|
||||
assert.ErrorContains(t, want, err)
|
||||
@@ -146,7 +151,12 @@ func TestStartEventStream(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
beaconNodeValidatorClient := mock2.NewMockBeaconNodeValidatorClient(ctrl)
|
||||
grpcClient := &grpcValidatorClient{beaconNodeValidatorClient, true}
|
||||
grpcClient := &grpcValidatorClient{
|
||||
grpcClientManager: &grpcClientManager[eth.BeaconNodeValidatorClient]{
|
||||
client: beaconNodeValidatorClient,
|
||||
},
|
||||
isEventStreamRunning: true,
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
topics []string
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
// Modification of a default grpc passthrough resolver (google.golang.org/grpc/resolver/passthrough) allowing to use multiple addresses
|
||||
// in grpc endpoint. Example:
|
||||
// conn, err := grpc.DialContext(ctx, "127.0.0.1:4000,127.0.0.1:4001", grpc.WithInsecure(), grpc.WithResolvers(&multipleEndpointsGrpcResolverBuilder{}))
|
||||
// It can be used with any grpc load balancer (pick_first, round_robin). Default is pick_first.
|
||||
// Round robin can be used by adding the following option:
|
||||
// grpc.WithDefaultServiceConfig("{\"loadBalancingConfig\":[{\"round_robin\":{}}]}")
|
||||
type multipleEndpointsGrpcResolverBuilder struct{}
|
||||
|
||||
// Build creates and starts multiple endpoints resolver.
|
||||
func (*multipleEndpointsGrpcResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, _ resolver.BuildOptions) (resolver.Resolver, error) {
|
||||
r := &multipleEndpointsGrpcResolver{
|
||||
target: target,
|
||||
cc: cc,
|
||||
}
|
||||
r.start()
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Scheme returns default scheme.
|
||||
func (*multipleEndpointsGrpcResolverBuilder) Scheme() string {
|
||||
return resolver.GetDefaultScheme()
|
||||
}
|
||||
|
||||
type multipleEndpointsGrpcResolver struct {
|
||||
target resolver.Target
|
||||
cc resolver.ClientConn
|
||||
}
|
||||
|
||||
func (r *multipleEndpointsGrpcResolver) start() {
|
||||
ep := r.target.Endpoint()
|
||||
endpoints := strings.Split(ep, ",")
|
||||
var addrs []resolver.Address
|
||||
for _, endpoint := range endpoints {
|
||||
addrs = append(addrs, resolver.Address{Addr: endpoint, ServerName: endpoint})
|
||||
}
|
||||
if err := r.cc.UpdateState(resolver.State{Addresses: addrs}); err != nil {
|
||||
log.WithError(err).Error("Failed to update grpc connection state")
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveNow --
|
||||
func (*multipleEndpointsGrpcResolver) ResolveNow(_ resolver.ResolveNowOptions) {}
|
||||
|
||||
// Close --
|
||||
func (*multipleEndpointsGrpcResolver) Close() {}
|
||||
@@ -8,11 +8,10 @@ import (
|
||||
validatorHelpers "github.com/OffchainLabs/prysm/v7/validator/helpers"
|
||||
)
|
||||
|
||||
func NewNodeClient(validatorConn validatorHelpers.NodeConnection, jsonRestHandler beaconApi.RestHandler) iface.NodeClient {
|
||||
grpcClient := grpcApi.NewNodeClient(validatorConn.GetGrpcClientConn())
|
||||
func NewNodeClient(validatorConn validatorHelpers.NodeConnection) iface.NodeClient {
|
||||
grpcClient := grpcApi.NewNodeClient(validatorConn)
|
||||
if features.Get().EnableBeaconRESTApi {
|
||||
return beaconApi.NewNodeClientWithFallback(jsonRestHandler, grpcClient)
|
||||
} else {
|
||||
return grpcClient
|
||||
return beaconApi.NewNodeClientWithFallback(validatorConn.GetRestHandler(), grpcClient)
|
||||
}
|
||||
return grpcClient
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@ package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
api "github.com/OffchainLabs/prysm/v7/api/client"
|
||||
eventClient "github.com/OffchainLabs/prysm/v7/api/client/event"
|
||||
grpcutil "github.com/OffchainLabs/prysm/v7/api/grpc"
|
||||
"github.com/OffchainLabs/prysm/v7/api/rest"
|
||||
"github.com/OffchainLabs/prysm/v7/async/event"
|
||||
lruwrpr "github.com/OffchainLabs/prysm/v7/cache/lru"
|
||||
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
|
||||
@@ -17,7 +15,6 @@ import (
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/OffchainLabs/prysm/v7/validator/accounts/wallet"
|
||||
beaconApi "github.com/OffchainLabs/prysm/v7/validator/client/beacon-api"
|
||||
beaconChainClientFactory "github.com/OffchainLabs/prysm/v7/validator/client/beacon-chain-client-factory"
|
||||
"github.com/OffchainLabs/prysm/v7/validator/client/iface"
|
||||
nodeclientfactory "github.com/OffchainLabs/prysm/v7/validator/client/node-client-factory"
|
||||
@@ -35,7 +32,6 @@ import (
|
||||
grpcprometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
|
||||
"github.com/pkg/errors"
|
||||
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -134,19 +130,21 @@ func NewValidatorService(ctx context.Context, cfg *Config) (*ValidatorService, e
|
||||
|
||||
s.ctx = grpcutil.AppendHeaders(ctx, cfg.GRPCHeaders)
|
||||
|
||||
grpcConn, err := grpc.DialContext(ctx, cfg.BeaconNodeGRPCEndpoint, dialOpts...)
|
||||
conn, err := validatorHelpers.NewNodeConnection(
|
||||
validatorHelpers.WithGrpc(s.ctx, cfg.BeaconNodeGRPCEndpoint, dialOpts),
|
||||
validatorHelpers.WithREST(cfg.BeaconApiEndpoint,
|
||||
rest.WithHttpHeaders(cfg.BeaconApiHeaders),
|
||||
rest.WithHttpTimeout(cfg.BeaconApiTimeout),
|
||||
rest.WithTracing(),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
if cfg.BeaconNodeCert != "" {
|
||||
if cfg.BeaconNodeCert != "" && cfg.BeaconNodeGRPCEndpoint != "" {
|
||||
log.Info("Established secure gRPC connection")
|
||||
}
|
||||
s.conn = validatorHelpers.NewNodeConnection(
|
||||
grpcConn,
|
||||
cfg.BeaconApiEndpoint,
|
||||
validatorHelpers.WithBeaconApiHeaders(cfg.BeaconApiHeaders),
|
||||
validatorHelpers.WithBeaconApiTimeout(cfg.BeaconApiTimeout),
|
||||
)
|
||||
s.conn = conn
|
||||
|
||||
return s, nil
|
||||
}
|
||||
@@ -181,20 +179,13 @@ func (v *ValidatorService) Start() {
|
||||
return
|
||||
}
|
||||
|
||||
u := strings.ReplaceAll(v.conn.GetBeaconApiUrl(), " ", "")
|
||||
hosts := strings.Split(u, ",")
|
||||
if len(hosts) == 0 {
|
||||
log.WithError(err).Error("No API hosts provided")
|
||||
restProvider := v.conn.GetRestConnectionProvider()
|
||||
if restProvider == nil || len(restProvider.Hosts()) == 0 {
|
||||
log.Error("No REST API hosts provided")
|
||||
return
|
||||
}
|
||||
|
||||
headersTransport := api.NewCustomHeadersTransport(http.DefaultTransport, v.conn.GetBeaconApiHeaders())
|
||||
restHandler := beaconApi.NewBeaconApiRestHandler(
|
||||
http.Client{Timeout: v.conn.GetBeaconApiTimeout(), Transport: otelhttp.NewTransport(headersTransport)},
|
||||
hosts[0],
|
||||
)
|
||||
|
||||
validatorClient := validatorclientfactory.NewValidatorClient(v.conn, restHandler)
|
||||
validatorClient := validatorclientfactory.NewValidatorClient(v.conn)
|
||||
|
||||
v.validator = &validator{
|
||||
slotFeed: new(event.Feed),
|
||||
@@ -208,12 +199,12 @@ func (v *ValidatorService) Start() {
|
||||
graffiti: v.graffiti,
|
||||
graffitiStruct: v.graffitiStruct,
|
||||
graffitiOrderedIndex: graffitiOrderedIndex,
|
||||
beaconNodeHosts: hosts,
|
||||
conn: v.conn,
|
||||
currentHostIndex: 0,
|
||||
validatorClient: validatorClient,
|
||||
chainClient: beaconChainClientFactory.NewChainClient(v.conn, restHandler),
|
||||
nodeClient: nodeclientfactory.NewNodeClient(v.conn, restHandler),
|
||||
prysmChainClient: beaconChainClientFactory.NewPrysmChainClient(v.conn, restHandler),
|
||||
chainClient: beaconChainClientFactory.NewChainClient(v.conn),
|
||||
nodeClient: nodeclientfactory.NewNodeClient(v.conn),
|
||||
prysmChainClient: beaconChainClientFactory.NewPrysmChainClient(v.conn),
|
||||
db: v.db,
|
||||
km: nil,
|
||||
web3SignerConfig: v.web3SignerConfig,
|
||||
@@ -369,7 +360,6 @@ func ConstructDialOptions(
|
||||
grpcprometheus.StreamClientInterceptor,
|
||||
grpcretry.StreamClientInterceptor(),
|
||||
),
|
||||
grpc.WithResolvers(&multipleEndpointsGrpcResolverBuilder{}),
|
||||
}
|
||||
|
||||
dialOpts = append(dialOpts, extraOpts...)
|
||||
|
||||
@@ -33,7 +33,10 @@ func TestStop_CancelsContext(t *testing.T) {
|
||||
|
||||
func TestNew_Insecure(t *testing.T) {
|
||||
hook := logTest.NewGlobal()
|
||||
_, err := NewValidatorService(t.Context(), &Config{})
|
||||
_, err := NewValidatorService(t.Context(), &Config{
|
||||
BeaconNodeGRPCEndpoint: "localhost:4000",
|
||||
BeaconApiEndpoint: "http://localhost:3500",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.LogsContain(t, hook, "You are using an insecure gRPC connection")
|
||||
}
|
||||
@@ -58,7 +61,11 @@ func TestStart_GrpcHeaders(t *testing.T) {
|
||||
"Authorization", "this is a valid value",
|
||||
},
|
||||
} {
|
||||
cfg := &Config{GRPCHeaders: strings.Split(input, ",")}
|
||||
cfg := &Config{
|
||||
BeaconNodeGRPCEndpoint: "localhost:4000",
|
||||
BeaconApiEndpoint: "http://localhost:3500",
|
||||
GRPCHeaders: strings.Split(input, ","),
|
||||
}
|
||||
validatorService, err := NewValidatorService(ctx, cfg)
|
||||
require.NoError(t, err)
|
||||
md, _ := metadata.FromOutgoingContext(validatorService.ctx)
|
||||
|
||||
@@ -10,12 +10,10 @@ import (
|
||||
|
||||
func NewValidatorClient(
|
||||
validatorConn validatorHelpers.NodeConnection,
|
||||
jsonRestHandler beaconApi.RestHandler,
|
||||
opt ...beaconApi.ValidatorClientOpt,
|
||||
) iface.ValidatorClient {
|
||||
if features.Get().EnableBeaconRESTApi {
|
||||
return beaconApi.NewBeaconApiValidatorClient(jsonRestHandler, opt...)
|
||||
} else {
|
||||
return grpcApi.NewGrpcValidatorClient(validatorConn.GetGrpcClientConn())
|
||||
return beaconApi.NewBeaconApiValidatorClient(validatorConn.GetRestHandler(), opt...)
|
||||
}
|
||||
return grpcApi.NewGrpcValidatorClient(validatorConn)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
"github.com/OffchainLabs/prysm/v7/validator/db"
|
||||
dbCommon "github.com/OffchainLabs/prysm/v7/validator/db/common"
|
||||
"github.com/OffchainLabs/prysm/v7/validator/graffiti"
|
||||
validatorHelpers "github.com/OffchainLabs/prysm/v7/validator/helpers"
|
||||
"github.com/OffchainLabs/prysm/v7/validator/keymanager"
|
||||
"github.com/OffchainLabs/prysm/v7/validator/keymanager/local"
|
||||
remoteweb3signer "github.com/OffchainLabs/prysm/v7/validator/keymanager/remote-web3signer"
|
||||
@@ -101,9 +102,9 @@ type validator struct {
|
||||
pubkeyToStatus map[[fieldparams.BLSPubkeyLength]byte]*validatorStatus
|
||||
wallet *wallet.Wallet
|
||||
walletInitializedChan chan *wallet.Wallet
|
||||
currentHostIndex uint64
|
||||
walletInitializedFeed *event.Feed
|
||||
graffitiOrderedIndex uint64
|
||||
conn validatorHelpers.NodeConnection
|
||||
submittedAtts map[submittedAttKey]*submittedAtt
|
||||
validatorsRegBatchSize int
|
||||
validatorClient iface.ValidatorClient
|
||||
@@ -114,7 +115,7 @@ type validator struct {
|
||||
km keymanager.IKeymanager
|
||||
accountChangedSub event.Subscription
|
||||
ticker slots.Ticker
|
||||
beaconNodeHosts []string
|
||||
currentHostIndex uint64
|
||||
genesisTime time.Time
|
||||
graffiti []byte
|
||||
voteStats voteStats
|
||||
@@ -1311,34 +1312,64 @@ func (v *validator) Host() string {
|
||||
}
|
||||
|
||||
func (v *validator) changeHost() {
|
||||
next := (v.currentHostIndex + 1) % uint64(len(v.beaconNodeHosts))
|
||||
hosts := v.hosts()
|
||||
if len(hosts) <= 1 {
|
||||
return
|
||||
}
|
||||
next := (v.currentHostIndex + 1) % uint64(len(hosts))
|
||||
log.WithFields(logrus.Fields{
|
||||
"currentHost": v.beaconNodeHosts[v.currentHostIndex],
|
||||
"nextHost": v.beaconNodeHosts[next],
|
||||
"currentHost": hosts[v.currentHostIndex],
|
||||
"nextHost": hosts[next],
|
||||
}).Warn("Beacon node is not responding, switching host")
|
||||
v.validatorClient.SetHost(v.beaconNodeHosts[next])
|
||||
v.validatorClient.SetHost(hosts[next])
|
||||
v.currentHostIndex = next
|
||||
}
|
||||
|
||||
// hosts returns the list of configured beacon node hosts.
|
||||
func (v *validator) hosts() []string {
|
||||
if features.Get().EnableBeaconRESTApi {
|
||||
return v.conn.GetRestConnectionProvider().Hosts()
|
||||
}
|
||||
return v.conn.GetGrpcConnectionProvider().Hosts()
|
||||
}
|
||||
|
||||
// numHosts returns the number of configured beacon node hosts.
|
||||
func (v *validator) numHosts() int {
|
||||
return len(v.hosts())
|
||||
}
|
||||
|
||||
func (v *validator) FindHealthyHost(ctx context.Context) bool {
|
||||
// Tail-recursive closure keeps retry count private.
|
||||
var check func(remaining int) bool
|
||||
check = func(remaining int) bool {
|
||||
if v.nodeClient.IsReady(ctx) { // ready → done
|
||||
numHosts := v.numHosts()
|
||||
startingHost := v.Host()
|
||||
attemptedHosts := []string{}
|
||||
|
||||
// Check all hosts for a fully synced node
|
||||
for i := range numHosts {
|
||||
if v.nodeClient.IsReady(ctx) {
|
||||
if len(attemptedHosts) > 0 {
|
||||
log.WithFields(logrus.Fields{
|
||||
"previousHost": startingHost,
|
||||
"newHost": v.Host(),
|
||||
"failedAttempts": attemptedHosts,
|
||||
}).Info("Failover succeeded: connected to healthy beacon node")
|
||||
}
|
||||
return true
|
||||
}
|
||||
if len(v.beaconNodeHosts) == 1 && features.Get().EnableBeaconRESTApi {
|
||||
log.WithField("host", v.Host()).Warn("Beacon node is not responding, no backup node configured")
|
||||
return false
|
||||
log.WithField("host", v.Host()).Debug("Beacon node not fully synced")
|
||||
attemptedHosts = append(attemptedHosts, v.Host())
|
||||
|
||||
// Try next host if not the last iteration
|
||||
if i < numHosts-1 {
|
||||
v.changeHost()
|
||||
}
|
||||
if remaining == 0 || !features.Get().EnableBeaconRESTApi {
|
||||
return false // exhausted or REST disabled
|
||||
}
|
||||
v.changeHost()
|
||||
return check(remaining - 1) // recurse
|
||||
}
|
||||
|
||||
return check(len(v.beaconNodeHosts))
|
||||
if numHosts == 1 {
|
||||
log.WithField("host", v.Host()).Warn("Beacon node is not fully synced, no backup node configured")
|
||||
} else {
|
||||
log.Warn("No fully synced beacon node found")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (v *validator) filterAndCacheActiveKeys(ctx context.Context, pubkeys [][fieldparams.BLSPubkeyLength]byte, slot primitives.Slot) ([][fieldparams.BLSPubkeyLength]byte, error) {
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
grpcutil "github.com/OffchainLabs/prysm/v7/api/grpc"
|
||||
"github.com/OffchainLabs/prysm/v7/api/rest"
|
||||
"github.com/OffchainLabs/prysm/v7/api/server/structs"
|
||||
"github.com/OffchainLabs/prysm/v7/async/event"
|
||||
"github.com/OffchainLabs/prysm/v7/cmd/validator/flags"
|
||||
@@ -37,6 +39,7 @@ import (
|
||||
"github.com/OffchainLabs/prysm/v7/validator/accounts/wallet"
|
||||
"github.com/OffchainLabs/prysm/v7/validator/client/iface"
|
||||
dbTest "github.com/OffchainLabs/prysm/v7/validator/db/testing"
|
||||
validatorHelpers "github.com/OffchainLabs/prysm/v7/validator/helpers"
|
||||
"github.com/OffchainLabs/prysm/v7/validator/keymanager"
|
||||
"github.com/OffchainLabs/prysm/v7/validator/keymanager/local"
|
||||
remoteweb3signer "github.com/OffchainLabs/prysm/v7/validator/keymanager/remote-web3signer"
|
||||
@@ -2792,18 +2795,27 @@ func TestValidator_Host(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidator_ChangeHost(t *testing.T) {
|
||||
// Enable REST API mode for this test since changeHost only calls SetHost in REST API mode
|
||||
resetCfg := features.InitWithReset(&features.Flags{EnableBeaconRESTApi: true})
|
||||
defer resetCfg()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
hosts := []string{"http://localhost:8080", "http://localhost:8081"}
|
||||
restProvider := &rest.MockRestProvider{MockHosts: hosts}
|
||||
conn, err := validatorHelpers.NewNodeConnection(validatorHelpers.WithRestProvider(restProvider))
|
||||
require.NoError(t, err)
|
||||
|
||||
client := validatormock.NewMockValidatorClient(ctrl)
|
||||
v := validator{
|
||||
validatorClient: client,
|
||||
beaconNodeHosts: []string{"http://localhost:8080", "http://localhost:8081"},
|
||||
conn: conn,
|
||||
currentHostIndex: 0,
|
||||
}
|
||||
|
||||
client.EXPECT().SetHost(v.beaconNodeHosts[1])
|
||||
client.EXPECT().SetHost(v.beaconNodeHosts[0])
|
||||
client.EXPECT().SetHost(hosts[1])
|
||||
client.EXPECT().SetHost(hosts[0])
|
||||
v.changeHost()
|
||||
assert.Equal(t, uint64(1), v.currentHostIndex)
|
||||
v.changeHost()
|
||||
@@ -2838,12 +2850,16 @@ func TestUpdateValidatorStatusCache(t *testing.T) {
|
||||
gomock.Any(),
|
||||
gomock.Any()).Return(mockResponse, nil)
|
||||
|
||||
mockProvider := &grpcutil.MockGrpcProvider{MockHosts: []string{"localhost:4000", "localhost:4001"}}
|
||||
conn, err := validatorHelpers.NewNodeConnection(validatorHelpers.WithGrpcProvider(mockProvider))
|
||||
require.NoError(t, err)
|
||||
|
||||
v := &validator{
|
||||
validatorClient: client,
|
||||
beaconNodeHosts: []string{"http://localhost:8080", "http://localhost:8081"},
|
||||
conn: conn,
|
||||
currentHostIndex: 0,
|
||||
pubkeyToStatus: map[[fieldparams.BLSPubkeyLength]byte]*validatorStatus{
|
||||
[fieldparams.BLSPubkeyLength]byte{0x03}: &validatorStatus{ // add non existent key and status to cache, should be fully removed on update
|
||||
[fieldparams.BLSPubkeyLength]byte{0x03}: { // add non existent key and status to cache, should be fully removed on update
|
||||
publicKey: []byte{0x03},
|
||||
status: ðpb.ValidatorStatusResponse{
|
||||
Status: ethpb.ValidatorStatus_ACTIVE,
|
||||
@@ -2853,7 +2869,7 @@ func TestUpdateValidatorStatusCache(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
err := v.updateValidatorStatusCache(ctx, pubkeys)
|
||||
err = v.updateValidatorStatusCache(ctx, pubkeys)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// make sure the nonexistent key is fully removed
|
||||
|
||||
@@ -10,6 +10,8 @@ go_library(
|
||||
importpath = "github.com/OffchainLabs/prysm/v7/validator/helpers",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//api/grpc:go_default_library",
|
||||
"//api/rest:go_default_library",
|
||||
"//config/fieldparams:go_default_library",
|
||||
"//consensus-types/primitives:go_default_library",
|
||||
"//validator/db/iface:go_default_library",
|
||||
@@ -24,18 +26,23 @@ go_test(
|
||||
srcs = [
|
||||
"converts_test.go",
|
||||
"metadata_test.go",
|
||||
"node_connection_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//api/grpc:go_default_library",
|
||||
"//api/rest:go_default_library",
|
||||
"//config/fieldparams:go_default_library",
|
||||
"//config/proposer:go_default_library",
|
||||
"//consensus-types/interfaces:go_default_library",
|
||||
"//consensus-types/primitives:go_default_library",
|
||||
"//proto/prysm/v1alpha1:go_default_library",
|
||||
"//testing/assert:go_default_library",
|
||||
"//testing/require:go_default_library",
|
||||
"//validator/db/common:go_default_library",
|
||||
"//validator/db/iface:go_default_library",
|
||||
"//validator/slashing-protection-history/format:go_default_library",
|
||||
"@com_github_prometheus_client_golang//prometheus:go_default_library",
|
||||
"@org_golang_google_grpc//:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,78 +1,142 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"time"
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
grpcutil "github.com/OffchainLabs/prysm/v7/api/grpc"
|
||||
"github.com/OffchainLabs/prysm/v7/api/rest"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Use an interface with a private dummy function to force all other packages to call NewNodeConnection
|
||||
// NodeConnection provides access to both gRPC and REST API connections to a beacon node.
|
||||
type NodeConnection interface {
|
||||
// GetGrpcClientConn returns the current gRPC client connection.
|
||||
// Returns nil if no gRPC provider is configured.
|
||||
GetGrpcClientConn() *grpc.ClientConn
|
||||
GetBeaconApiUrl() string
|
||||
GetBeaconApiHeaders() map[string][]string
|
||||
setBeaconApiHeaders(map[string][]string)
|
||||
GetBeaconApiTimeout() time.Duration
|
||||
setBeaconApiTimeout(time.Duration)
|
||||
dummy()
|
||||
// GetGrpcConnectionProvider returns the gRPC connection provider.
|
||||
GetGrpcConnectionProvider() grpcutil.GrpcConnectionProvider
|
||||
// GetRestConnectionProvider returns the REST connection provider.
|
||||
GetRestConnectionProvider() rest.RestConnectionProvider
|
||||
// GetRestHandler returns the REST handler for making API requests.
|
||||
// Returns nil if no REST provider is configured.
|
||||
GetRestHandler() rest.RestHandler
|
||||
// GetHttpClient returns the configured HTTP client for REST API requests.
|
||||
// Returns nil if no REST provider is configured.
|
||||
GetHttpClient() *http.Client
|
||||
}
|
||||
|
||||
type nodeConnection struct {
|
||||
grpcClientConn *grpc.ClientConn
|
||||
beaconApiUrl string
|
||||
beaconApiHeaders map[string][]string
|
||||
beaconApiTimeout time.Duration
|
||||
}
|
||||
|
||||
// NodeConnectionOption is a functional option for configuring the node connection.
|
||||
type NodeConnectionOption func(nc NodeConnection)
|
||||
|
||||
// WithBeaconApiHeaders sets the HTTP headers that should be sent to the server along with each request.
|
||||
func WithBeaconApiHeaders(headers map[string][]string) NodeConnectionOption {
|
||||
return func(nc NodeConnection) {
|
||||
nc.setBeaconApiHeaders(headers)
|
||||
}
|
||||
}
|
||||
|
||||
// WithBeaconApiTimeout sets the HTTP request timeout.
|
||||
func WithBeaconApiTimeout(timeout time.Duration) NodeConnectionOption {
|
||||
return func(nc NodeConnection) {
|
||||
nc.setBeaconApiTimeout(timeout)
|
||||
}
|
||||
grpcConnectionProvider grpcutil.GrpcConnectionProvider
|
||||
restConnectionProvider rest.RestConnectionProvider
|
||||
}
|
||||
|
||||
func (c *nodeConnection) GetGrpcClientConn() *grpc.ClientConn {
|
||||
return c.grpcClientConn
|
||||
}
|
||||
|
||||
func (c *nodeConnection) GetBeaconApiUrl() string {
|
||||
return c.beaconApiUrl
|
||||
}
|
||||
|
||||
func (c *nodeConnection) GetBeaconApiHeaders() map[string][]string {
|
||||
return c.beaconApiHeaders
|
||||
}
|
||||
|
||||
func (c *nodeConnection) setBeaconApiHeaders(headers map[string][]string) {
|
||||
c.beaconApiHeaders = headers
|
||||
}
|
||||
|
||||
func (c *nodeConnection) GetBeaconApiTimeout() time.Duration {
|
||||
return c.beaconApiTimeout
|
||||
}
|
||||
|
||||
func (c *nodeConnection) setBeaconApiTimeout(timeout time.Duration) {
|
||||
c.beaconApiTimeout = timeout
|
||||
}
|
||||
|
||||
func (*nodeConnection) dummy() {}
|
||||
|
||||
func NewNodeConnection(grpcConn *grpc.ClientConn, beaconApiUrl string, opts ...NodeConnectionOption) NodeConnection {
|
||||
conn := &nodeConnection{}
|
||||
conn.grpcClientConn = grpcConn
|
||||
conn.beaconApiUrl = beaconApiUrl
|
||||
for _, opt := range opts {
|
||||
opt(conn)
|
||||
if c.grpcConnectionProvider == nil {
|
||||
return nil
|
||||
}
|
||||
return conn
|
||||
return c.grpcConnectionProvider.CurrentConn()
|
||||
}
|
||||
|
||||
func (c *nodeConnection) GetGrpcConnectionProvider() grpcutil.GrpcConnectionProvider {
|
||||
return c.grpcConnectionProvider
|
||||
}
|
||||
|
||||
func (c *nodeConnection) GetRestConnectionProvider() rest.RestConnectionProvider {
|
||||
return c.restConnectionProvider
|
||||
}
|
||||
|
||||
func (c *nodeConnection) GetRestHandler() rest.RestHandler {
|
||||
if c.restConnectionProvider == nil {
|
||||
return nil
|
||||
}
|
||||
return c.restConnectionProvider.RestHandler()
|
||||
}
|
||||
|
||||
func (c *nodeConnection) GetHttpClient() *http.Client {
|
||||
if c.restConnectionProvider == nil {
|
||||
return nil
|
||||
}
|
||||
return c.restConnectionProvider.HttpClient()
|
||||
}
|
||||
|
||||
// nodeConnectionBuilder is used internally to build a NodeConnection.
|
||||
type nodeConnectionBuilder struct {
|
||||
grpcProvider grpcutil.GrpcConnectionProvider
|
||||
restProvider rest.RestConnectionProvider
|
||||
}
|
||||
|
||||
// NodeConnectionOption is a functional option for configuring a NodeConnection.
|
||||
type NodeConnectionOption func(*nodeConnectionBuilder) error
|
||||
|
||||
// WithGrpc configures a gRPC connection provider for the NodeConnection.
|
||||
// If endpoint is empty, this option is a no-op.
|
||||
func WithGrpc(ctx context.Context, endpoint string, dialOpts []grpc.DialOption) NodeConnectionOption {
|
||||
return func(b *nodeConnectionBuilder) error {
|
||||
if endpoint == "" {
|
||||
return nil
|
||||
}
|
||||
provider, err := grpcutil.NewGrpcConnectionProvider(ctx, endpoint, dialOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create gRPC connection provider")
|
||||
}
|
||||
b.grpcProvider = provider
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithREST configures a REST connection provider for the NodeConnection.
|
||||
// If endpoint is empty, this option is a no-op.
|
||||
func WithREST(endpoint string, opts ...rest.RestConnectionProviderOption) NodeConnectionOption {
|
||||
return func(b *nodeConnectionBuilder) error {
|
||||
if endpoint == "" {
|
||||
return nil
|
||||
}
|
||||
provider, err := rest.NewRestConnectionProvider(endpoint, opts...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create REST connection provider")
|
||||
}
|
||||
b.restProvider = provider
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithGrpcProvider sets a pre-built gRPC connection provider.
|
||||
// This is primarily useful for testing with mock providers.
|
||||
func WithGrpcProvider(provider grpcutil.GrpcConnectionProvider) NodeConnectionOption {
|
||||
return func(b *nodeConnectionBuilder) error {
|
||||
b.grpcProvider = provider
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRestProvider sets a pre-built REST connection provider.
|
||||
// This is primarily useful for testing with mock providers.
|
||||
func WithRestProvider(provider rest.RestConnectionProvider) NodeConnectionOption {
|
||||
return func(b *nodeConnectionBuilder) error {
|
||||
b.restProvider = provider
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewNodeConnection creates a new NodeConnection with the given options.
|
||||
// At least one provider (gRPC or REST) must be configured via options.
|
||||
// Returns an error if no providers are configured.
|
||||
func NewNodeConnection(opts ...NodeConnectionOption) (NodeConnection, error) {
|
||||
b := &nodeConnectionBuilder{}
|
||||
for _, opt := range opts {
|
||||
if err := opt(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if b.grpcProvider == nil && b.restProvider == nil {
|
||||
return nil, errors.New("at least one beacon node endpoint must be provided (--beacon-rpc-provider or --beacon-rest-api-provider)")
|
||||
}
|
||||
|
||||
return &nodeConnection{
|
||||
grpcConnectionProvider: b.grpcProvider,
|
||||
restConnectionProvider: b.restProvider,
|
||||
}, nil
|
||||
}
|
||||
|
||||
102
validator/helpers/node_connection_test.go
Normal file
102
validator/helpers/node_connection_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
grpcutil "github.com/OffchainLabs/prysm/v7/api/grpc"
|
||||
"github.com/OffchainLabs/prysm/v7/api/rest"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/assert"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/require"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestNewNodeConnection(t *testing.T) {
|
||||
t.Run("with both providers", func(t *testing.T) {
|
||||
grpcProvider := &grpcutil.MockGrpcProvider{MockHosts: []string{"localhost:4000"}}
|
||||
restProvider := &rest.MockRestProvider{MockHosts: []string{"http://localhost:3500"}}
|
||||
conn, err := NewNodeConnection(
|
||||
WithGrpcProvider(grpcProvider),
|
||||
WithRestProvider(restProvider),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, grpcProvider, conn.GetGrpcConnectionProvider())
|
||||
assert.Equal(t, restProvider, conn.GetRestConnectionProvider())
|
||||
})
|
||||
|
||||
t.Run("with only rest provider", func(t *testing.T) {
|
||||
restProvider := &rest.MockRestProvider{MockHosts: []string{"http://localhost:3500"}}
|
||||
conn, err := NewNodeConnection(WithRestProvider(restProvider))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, (grpcutil.GrpcConnectionProvider)(nil), conn.GetGrpcConnectionProvider())
|
||||
assert.Equal(t, (*grpc.ClientConn)(nil), conn.GetGrpcClientConn())
|
||||
assert.Equal(t, restProvider, conn.GetRestConnectionProvider())
|
||||
})
|
||||
|
||||
t.Run("with only grpc provider", func(t *testing.T) {
|
||||
grpcProvider := &grpcutil.MockGrpcProvider{MockHosts: []string{"localhost:4000"}}
|
||||
conn, err := NewNodeConnection(WithGrpcProvider(grpcProvider))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, grpcProvider, conn.GetGrpcConnectionProvider())
|
||||
assert.Equal(t, (rest.RestConnectionProvider)(nil), conn.GetRestConnectionProvider())
|
||||
assert.Equal(t, (*http.Client)(nil), conn.GetHttpClient())
|
||||
})
|
||||
|
||||
t.Run("with no providers returns error", func(t *testing.T) {
|
||||
conn, err := NewNodeConnection()
|
||||
require.ErrorContains(t, "at least one beacon node endpoint must be provided", err)
|
||||
assert.Equal(t, (NodeConnection)(nil), conn)
|
||||
})
|
||||
|
||||
t.Run("with empty endpoints is no-op", func(t *testing.T) {
|
||||
// Empty endpoints should be skipped, resulting in no providers
|
||||
conn, err := NewNodeConnection(
|
||||
WithGrpc(context.Background(), "", nil),
|
||||
WithREST(""),
|
||||
)
|
||||
require.ErrorContains(t, "at least one beacon node endpoint must be provided", err)
|
||||
assert.Equal(t, (NodeConnection)(nil), conn)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNodeConnection_GetGrpcClientConn(t *testing.T) {
|
||||
t.Run("delegates to provider", func(t *testing.T) {
|
||||
// We can't easily create a real grpc.ClientConn in tests,
|
||||
// but we can verify the delegation works with nil
|
||||
grpcProvider := &grpcutil.MockGrpcProvider{MockConn: nil, MockHosts: []string{"localhost:4000"}}
|
||||
conn, err := NewNodeConnection(WithGrpcProvider(grpcProvider))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should delegate to provider.CurrentConn()
|
||||
assert.Equal(t, grpcProvider.CurrentConn(), conn.GetGrpcClientConn())
|
||||
})
|
||||
|
||||
t.Run("returns nil when provider is nil", func(t *testing.T) {
|
||||
restProvider := &rest.MockRestProvider{MockHosts: []string{"http://localhost:3500"}}
|
||||
conn, err := NewNodeConnection(WithRestProvider(restProvider))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, (*grpc.ClientConn)(nil), conn.GetGrpcClientConn())
|
||||
})
|
||||
}
|
||||
|
||||
func TestNodeConnection_GetHttpClient(t *testing.T) {
|
||||
t.Run("delegates to provider", func(t *testing.T) {
|
||||
mockClient := &http.Client{}
|
||||
restProvider := &rest.MockRestProvider{MockClient: mockClient, MockHosts: []string{"http://localhost:3500"}}
|
||||
conn, err := NewNodeConnection(WithRestProvider(restProvider))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, mockClient, conn.GetHttpClient())
|
||||
})
|
||||
|
||||
t.Run("returns nil when provider is nil", func(t *testing.T) {
|
||||
grpcProvider := &grpcutil.MockGrpcProvider{MockHosts: []string{"localhost:4000"}}
|
||||
conn, err := NewNodeConnection(WithGrpcProvider(grpcProvider))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, (*http.Client)(nil), conn.GetHttpClient())
|
||||
})
|
||||
}
|
||||
@@ -41,6 +41,8 @@ func TestNode_Builds(t *testing.T) {
|
||||
set.String("wallet-password-file", passwordFile, "path to wallet password")
|
||||
set.String("keymanager-kind", "imported", "keymanager kind")
|
||||
set.String("verbosity", "debug", "log verbosity")
|
||||
set.String("beacon-rpc-provider", "localhost:4000", "beacon node RPC endpoint")
|
||||
set.String("beacon-rest-api-provider", "http://localhost:3500", "beacon node REST API endpoint")
|
||||
require.NoError(t, set.Set(flags.WalletPasswordFileFlag.Name, passwordFile))
|
||||
ctx := cli.NewContext(&app, set, nil)
|
||||
opts := []accounts.Option{
|
||||
|
||||
@@ -23,9 +23,9 @@ go_library(
|
||||
],
|
||||
deps = [
|
||||
"//api:go_default_library",
|
||||
"//api/client:go_default_library",
|
||||
"//api/grpc:go_default_library",
|
||||
"//api/pagination:go_default_library",
|
||||
"//api/rest:go_default_library",
|
||||
"//api/server:go_default_library",
|
||||
"//api/server/httprest:go_default_library",
|
||||
"//api/server/middleware:go_default_library",
|
||||
@@ -55,7 +55,6 @@ go_library(
|
||||
"//validator/accounts/petnames:go_default_library",
|
||||
"//validator/accounts/wallet:go_default_library",
|
||||
"//validator/client:go_default_library",
|
||||
"//validator/client/beacon-api:go_default_library",
|
||||
"//validator/client/beacon-chain-client-factory:go_default_library",
|
||||
"//validator/client/iface:go_default_library",
|
||||
"//validator/client/node-client-factory:go_default_library",
|
||||
@@ -79,7 +78,6 @@ go_library(
|
||||
"@com_github_pkg_errors//:go_default_library",
|
||||
"@com_github_sirupsen_logrus//:go_default_library",
|
||||
"@com_github_wealdtech_go_eth2_wallet_encryptor_keystorev4//:go_default_library",
|
||||
"@io_opentelemetry_go_contrib_instrumentation_net_http_otelhttp//: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",
|
||||
@@ -106,6 +104,7 @@ go_test(
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//api:go_default_library",
|
||||
"//api/grpc:go_default_library",
|
||||
"//async/event:go_default_library",
|
||||
"//cmd/validator/flags:go_default_library",
|
||||
"//config/features:go_default_library",
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
api "github.com/OffchainLabs/prysm/v7/api/client"
|
||||
grpcutil "github.com/OffchainLabs/prysm/v7/api/grpc"
|
||||
"github.com/OffchainLabs/prysm/v7/api/rest"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/OffchainLabs/prysm/v7/validator/client"
|
||||
beaconApi "github.com/OffchainLabs/prysm/v7/validator/client/beacon-api"
|
||||
beaconChainClientFactory "github.com/OffchainLabs/prysm/v7/validator/client/beacon-chain-client-factory"
|
||||
nodeClientFactory "github.com/OffchainLabs/prysm/v7/validator/client/node-client-factory"
|
||||
validatorClientFactory "github.com/OffchainLabs/prysm/v7/validator/client/validator-client-factory"
|
||||
@@ -17,7 +14,6 @@ import (
|
||||
grpcopentracing "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
|
||||
grpcprometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
|
||||
"github.com/pkg/errors"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -41,30 +37,26 @@ func (s *Server) registerBeaconClient() error {
|
||||
|
||||
s.ctx = grpcutil.AppendHeaders(s.ctx, s.grpcHeaders)
|
||||
|
||||
grpcConn, err := grpc.DialContext(s.ctx, s.beaconNodeEndpoint, dialOpts...)
|
||||
conn, err := validatorHelpers.NewNodeConnection(
|
||||
validatorHelpers.WithGrpc(s.ctx, s.beaconNodeEndpoint, dialOpts),
|
||||
validatorHelpers.WithREST(s.beaconApiEndpoint,
|
||||
rest.WithHttpHeaders(s.beaconApiHeaders),
|
||||
rest.WithHttpTimeout(s.beaconApiTimeout),
|
||||
rest.WithTracing(),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not dial endpoint: %s", s.beaconNodeEndpoint)
|
||||
return err
|
||||
}
|
||||
if s.beaconNodeCert != "" {
|
||||
if s.beaconNodeCert != "" && s.beaconNodeEndpoint != "" {
|
||||
log.Info("Established secure gRPC connection")
|
||||
}
|
||||
s.healthClient = ethpb.NewHealthClient(grpcConn)
|
||||
if grpcConn := conn.GetGrpcClientConn(); grpcConn != nil {
|
||||
s.healthClient = ethpb.NewHealthClient(grpcConn)
|
||||
}
|
||||
|
||||
conn := validatorHelpers.NewNodeConnection(
|
||||
grpcConn,
|
||||
s.beaconApiEndpoint,
|
||||
validatorHelpers.WithBeaconApiHeaders(s.beaconApiHeaders),
|
||||
validatorHelpers.WithBeaconApiTimeout(s.beaconApiTimeout),
|
||||
)
|
||||
|
||||
headersTransport := api.NewCustomHeadersTransport(http.DefaultTransport, conn.GetBeaconApiHeaders())
|
||||
restHandler := beaconApi.NewBeaconApiRestHandler(
|
||||
http.Client{Timeout: s.beaconApiTimeout, Transport: otelhttp.NewTransport(headersTransport)},
|
||||
s.beaconApiEndpoint,
|
||||
)
|
||||
|
||||
s.chainClient = beaconChainClientFactory.NewChainClient(conn, restHandler)
|
||||
s.nodeClient = nodeClientFactory.NewNodeClient(conn, restHandler)
|
||||
s.beaconNodeValidatorClient = validatorClientFactory.NewValidatorClient(conn, restHandler)
|
||||
s.chainClient = beaconChainClientFactory.NewChainClient(conn)
|
||||
s.nodeClient = nodeClientFactory.NewNodeClient(conn)
|
||||
s.beaconNodeValidatorClient = validatorClientFactory.NewValidatorClient(conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,19 +3,17 @@ package rpc
|
||||
import (
|
||||
"testing"
|
||||
|
||||
grpcutil "github.com/OffchainLabs/prysm/v7/api/grpc"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/assert"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/require"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestGrpcHeaders(t *testing.T) {
|
||||
s := &Server{
|
||||
ctx: t.Context(),
|
||||
grpcHeaders: []string{"first=value1", "second=value2"},
|
||||
}
|
||||
err := s.registerBeaconClient()
|
||||
require.NoError(t, err)
|
||||
md, _ := metadata.FromOutgoingContext(s.ctx)
|
||||
ctx := t.Context()
|
||||
grpcHeaders := []string{"first=value1", "second=value2"}
|
||||
ctx = grpcutil.AppendHeaders(ctx, grpcHeaders)
|
||||
md, _ := metadata.FromOutgoingContext(ctx)
|
||||
require.Equal(t, 2, md.Len(), "MetadataV0 contains wrong number of values")
|
||||
assert.Equal(t, "value1", md.Get("first")[0])
|
||||
assert.Equal(t, "value2", md.Get("second")[0])
|
||||
|
||||
Reference in New Issue
Block a user