Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions beacon/preconf/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,29 @@ type PayloadProvider interface {
GetPayloadBySlot(ctx context.Context, slot math.Slot, parentBlockRoot common.Root) (ctypes.BuiltExecutionPayloadEnv, error)
}

// SyncChecker exposes the node's sync status for health checks.
type SyncChecker interface {
// IsAppReady returns nil if the node has committed at least one block.
IsAppReady() error
// GetSyncData returns the latest committed height and the target height being synced to.
GetSyncData() (latestHeight int64, syncToHeight int64)
}

// ELChecker exposes the execution-layer client's connectivity status.
type ELChecker interface {
// IsConnected returns true if the execution client is reachable.
IsConnected() bool
}

// Server is the preconf API server that serves GetPayload requests from validators.
type Server struct {
logger log.Logger
validatorJWTs ValidatorJWTs
whitelist Whitelist
preconfProposerTracker ProposerTracker
payloadProvider PayloadProvider
syncChecker SyncChecker
elChecker ELChecker
port int

mu sync.RWMutex
Expand All @@ -79,6 +95,8 @@ func NewServer(
whitelist Whitelist,
preconfProposerTracker ProposerTracker,
payloadProvider PayloadProvider,
syncChecker SyncChecker,
elChecker ELChecker,
port int,
) *Server {
return &Server{
Expand All @@ -87,6 +105,8 @@ func NewServer(
whitelist: whitelist,
preconfProposerTracker: preconfProposerTracker,
payloadProvider: payloadProvider,
syncChecker: syncChecker,
elChecker: elChecker,
port: port,
}
}
Expand Down Expand Up @@ -157,13 +177,49 @@ func (s *Server) Stop() error {
return server.Shutdown(ctx)
}

// handleHealth just sends 200 OK to the health check endpoint.
// handleHealth checks sync status and returns 200 when the sequencer is synced
// and ready to produce blocks, or 503 when it is not.
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
w.WriteHeader(http.StatusOK)

resp := s.buildHealthResponse()

w.Header().Set("Content-Type", "application/json")
if !resp.IsReady || resp.IsSyncing || !resp.ELConnected {
w.WriteHeader(http.StatusServiceUnavailable)
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
s.logger.Error("Failed to encode health response", "error", err)
}
}

// buildHealthResponse inspects the node's sync state and EL connectivity
// and produces a HealthResponse.
func (s *Server) buildHealthResponse() *HealthResponse {
resp := &HealthResponse{
IsReady: true,
ELConnected: true,
}

if s.syncChecker != nil {
resp.IsReady = s.syncChecker.IsAppReady() == nil
latestHeight, syncToHeight := s.syncChecker.GetSyncData()
resp.HeadSlot = latestHeight
resp.SyncDistance = syncToHeight - latestHeight
if resp.SyncDistance < 0 {
resp.SyncDistance = 0
}
resp.IsSyncing = resp.SyncDistance > 0
}

if s.elChecker != nil {
resp.ELConnected = s.elChecker.IsConnected()
}
Comment thread
bar-bera marked this conversation as resolved.

return resp
}

// handleGetPayload handles the GetPayload endpoint.
Expand Down
127 changes: 125 additions & 2 deletions beacon/preconf/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

"github.com/berachain/beacon-kit/beacon/preconf"
"github.com/berachain/beacon-kit/cli/utils/parser"
"github.com/berachain/beacon-kit/errors"

Check failure on line 35 in beacon/preconf/server_test.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gci)
ctypes "github.com/berachain/beacon-kit/consensus-types/types"
engineprimitives "github.com/berachain/beacon-kit/engine-primitives/engine-primitives"
"github.com/berachain/beacon-kit/log/noop"
Expand Down Expand Up @@ -120,6 +121,8 @@
newTestWhitelist(t, pubkeyAHex, pubkeyBHex),
tracker,
provider,
&mockSyncChecker{ready: true},
&mockELChecker{connected: true},
0,
)

Expand Down Expand Up @@ -198,6 +201,8 @@
newTestWhitelist(t, pubkeyAHex, pubkeyBHex),
tt.setupTracker(),
&mockPayloadProvider{hasPayload: true},
&mockSyncChecker{ready: true},
&mockELChecker{connected: true},
0,
)

Expand All @@ -220,7 +225,7 @@
func TestServer_RejectsNonPostMethods(t *testing.T) {
t.Parallel()

server := preconf.NewServer(noop.NewLogger[any](), nil, nil, nil, nil, 0)
server := preconf.NewServer(noop.NewLogger[any](), nil, nil, nil, nil, nil, nil, 0)

for _, method := range []string{http.MethodGet, http.MethodPut, http.MethodDelete} {
req := httptest.NewRequest(method, preconf.PayloadEndpoint, nil)
Expand Down Expand Up @@ -263,7 +268,7 @@
wl, err := preconf.NewWhitelist(tmpFile)
require.NoError(t, err)

server := preconf.NewServer(noop.NewLogger[any](), nil, wl, nil, nil, 0)
server := preconf.NewServer(noop.NewLogger[any](), nil, wl, nil, nil, nil, nil, 0)

require.True(t, wl.IsWhitelisted(pkA))
require.False(t, wl.IsWhitelisted(pkB))
Expand Down Expand Up @@ -314,3 +319,121 @@
}

func (m *mockPayloadEnvelope) ShouldOverrideBuilder() bool { return false }

// mockSyncChecker implements preconf.SyncChecker for tests.
type mockSyncChecker struct {
ready bool
latestHeight int64
syncToHeight int64
}

func (m *mockSyncChecker) IsAppReady() error {
if !m.ready {
return errors.New("app not ready")
}
return nil
}

func (m *mockSyncChecker) GetSyncData() (int64, int64) {
return m.latestHeight, m.syncToHeight
}

// mockELChecker implements preconf.ELChecker for tests.
type mockELChecker struct {
connected bool
}

func (m *mockELChecker) IsConnected() bool {
return m.connected
}

func TestServer_HealthEndpoint(t *testing.T) {
t.Parallel()

tests := []struct {
name string
syncChecker *mockSyncChecker
elChecker *mockELChecker
wantStatus int
wantReady bool
wantSync bool
wantELConn bool
}{
{
name: "healthy - synced, ready, EL connected",
syncChecker: &mockSyncChecker{ready: true, latestHeight: 100, syncToHeight: 100},
elChecker: &mockELChecker{connected: true},
wantStatus: http.StatusOK,
wantReady: true,
wantSync: false,
wantELConn: true,
},
{
name: "unhealthy - still syncing",
syncChecker: &mockSyncChecker{ready: true, latestHeight: 50, syncToHeight: 100},
elChecker: &mockELChecker{connected: true},
wantStatus: http.StatusServiceUnavailable,
wantReady: true,
wantSync: true,
wantELConn: true,
},
{
name: "unhealthy - app not ready",
syncChecker: &mockSyncChecker{ready: false, latestHeight: 0, syncToHeight: 0},
elChecker: &mockELChecker{connected: true},
wantStatus: http.StatusServiceUnavailable,
wantReady: false,
wantSync: false,
wantELConn: true,
},
{
name: "unhealthy - EL disconnected",
syncChecker: &mockSyncChecker{ready: true, latestHeight: 100, syncToHeight: 100},
elChecker: &mockELChecker{connected: false},
wantStatus: http.StatusServiceUnavailable,
wantReady: true,
wantSync: false,
wantELConn: false,
},
{
name: "healthy - nil checkers",
wantStatus: http.StatusOK,
wantReady: true,
wantSync: false,
wantELConn: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

var syncChecker preconf.SyncChecker
if tt.syncChecker != nil {
syncChecker = tt.syncChecker
}
var elChecker preconf.ELChecker
if tt.elChecker != nil {
elChecker = tt.elChecker
}

server := preconf.NewServer(
noop.NewLogger[any](), nil, nil, nil, nil,
syncChecker, elChecker, 0,
)

req := httptest.NewRequest(http.MethodGet, preconf.HealthEndpoint, nil)
rec := httptest.NewRecorder()
server.Handler().ServeHTTP(rec, req)

require.Equal(t, tt.wantStatus, rec.Code)

var resp preconf.HealthResponse
err := json.NewDecoder(rec.Body).Decode(&resp)
require.NoError(t, err)
require.Equal(t, tt.wantReady, resp.IsReady)
require.Equal(t, tt.wantSync, resp.IsSyncing)
require.Equal(t, tt.wantELConn, resp.ELConnected)
})
Comment thread
bar-bera marked this conversation as resolved.
}
}
18 changes: 18 additions & 0 deletions beacon/preconf/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,24 @@ func NewGetPayloadResponseFromEnvelope(env ctypes.BuiltExecutionPayloadEnv) *Get
}
}

// HealthResponse is the response body for the health endpoint.
type HealthResponse struct {
// IsReady indicates whether the node has committed at least one block.
IsReady bool `json:"is_ready"`

// IsSyncing indicates whether the node is still catching up with the chain.
IsSyncing bool `json:"is_syncing"`

// ELConnected indicates whether the execution-layer client is reachable.
ELConnected bool `json:"el_connected"`

// HeadSlot is the latest committed block height.
HeadSlot int64 `json:"head_slot"`

// SyncDistance is the number of slots remaining until the node is synced.
SyncDistance int64 `json:"sync_distance"`
}

// ErrorResponse is the error response body.
type ErrorResponse struct {
// Code is the error code.
Expand Down
6 changes: 6 additions & 0 deletions node-core/components/preconf_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import (
"github.com/berachain/beacon-kit/beacon/preconf"
"github.com/berachain/beacon-kit/config"
"github.com/berachain/beacon-kit/errors"
"github.com/berachain/beacon-kit/execution/client"
"github.com/berachain/beacon-kit/log/phuslu"
"github.com/berachain/beacon-kit/node-core/types"
payloadbuilder "github.com/berachain/beacon-kit/payload/builder"
)

Expand All @@ -38,6 +40,8 @@ type PreconfServerInput struct {
Whitelist preconf.Whitelist
PreconfProposerTracker preconf.ProposerTracker
LocalBuilder *payloadbuilder.PayloadBuilder
ConsensusService types.ConsensusService
EngineClient *client.EngineClient
}

// ProvidePreconfServer provides the preconf API server for sequencer mode.
Expand Down Expand Up @@ -84,6 +88,8 @@ func ProvidePreconfServer(in PreconfServerInput) (*preconf.Server, error) {
in.Whitelist,
in.PreconfProposerTracker,
in.LocalBuilder,
in.ConsensusService,
in.EngineClient,
cfg.APIPort,
), nil
}
Loading