Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
15 changes: 13 additions & 2 deletions pkg/rpc/server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ func RegisterCustomHTTPEndpoints(mux *http.ServeMux, s store.Store, pm p2p.P2PRP
// 2. Block production/sync status:
// - Confirms node state is accessible
// - Verifies at least one block has been produced/synced
// 3. Aggregator-specific checks (for aggregator nodes only):
// - Validates blocks are being produced at expected rate (within 5x block_time)
// 3. Block execution liveness:
// - Aggregator nodes: validates blocks are being produced at expected rate (within 5x block_time)
// - Non-aggregator nodes: validates a block has been executed within readiness_window_seconds
// 4. Sync status (for all nodes):
// - Compares local height with best known network height
// - Ensures node is not falling behind by more than readiness_max_blocks_behind
Expand Down Expand Up @@ -134,6 +135,16 @@ func RegisterCustomHTTPEndpoints(mux *http.ServeMux, s store.Store, pm p2p.P2PRP
http.Error(w, "UNREADY: aggregator not producing blocks at expected rate", http.StatusServiceUnavailable)
return
}
} else {
readinessWindowSeconds := cfg.Node.ReadinessWindowSeconds
if readinessWindowSeconds == 0 {
readinessWindowSeconds = 15 // fallback to default 15s window
}
maxAllowedDelay := time.Duration(readinessWindowSeconds) * time.Second
if time.Since(state.LastBlockTime) > maxAllowedDelay {
http.Error(w, "UNREADY: node not executing blocks", http.StatusServiceUnavailable)
return
}
Comment on lines +138 to +147

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard against uint64→Duration overflow edge case.

time.Duration(readinessWindowSeconds) * time.Second can wrap to a negative maxAllowedDelay if ReadinessWindowSeconds exceeds int64 nanosecond capacity (~292 years), causing permanent UNREADY. Add an upper-bound check or saturate before conversion.

 			readinessWindowSeconds := cfg.Node.ReadinessWindowSeconds
 			if readinessWindowSeconds == 0 {
 				readinessWindowSeconds = 15 // fallback to default 15s window
 			}
+			const maxReadinessWindowSeconds = uint64(292 * 365 * 24 * 60 * 60) // ~292 years
+			if readinessWindowSeconds > maxReadinessWindowSeconds {
+				readinessWindowSeconds = maxReadinessWindowSeconds
+			}
 			maxAllowedDelay := time.Duration(readinessWindowSeconds) * time.Second
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else {
readinessWindowSeconds := cfg.Node.ReadinessWindowSeconds
if readinessWindowSeconds == 0 {
readinessWindowSeconds = 15 // fallback to default 15s window
}
maxAllowedDelay := time.Duration(readinessWindowSeconds) * time.Second
if time.Since(state.LastBlockTime) > maxAllowedDelay {
http.Error(w, "UNREADY: node not executing blocks", http.StatusServiceUnavailable)
return
}
readinessWindowSeconds := cfg.Node.ReadinessWindowSeconds
if readinessWindowSeconds == 0 {
readinessWindowSeconds = 15 // fallback to default 15s window
}
const maxReadinessWindowSeconds = uint64(292 * 365 * 24 * 60 * 60) // ~292 years
if readinessWindowSeconds > maxReadinessWindowSeconds {
readinessWindowSeconds = maxReadinessWindowSeconds
}
maxAllowedDelay := time.Duration(readinessWindowSeconds) * time.Second
if time.Since(state.LastBlockTime) > maxAllowedDelay {
http.Error(w, "UNREADY: node not executing blocks", http.StatusServiceUnavailable)
return
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/rpc/server/http.go` around lines 138 - 147, Guard the readiness timeout
calculation against uint64-to-time.Duration overflow in the HTTP readiness path.
In the readiness check inside the server logic, before computing maxAllowedDelay
from cfg.Node.ReadinessWindowSeconds, cap the value or saturate it to a safe
maximum so the conversion cannot wrap negative; keep the existing 0 fallback and
preserve the subsequent time.Since(state.LastBlockTime) comparison.

}

if bestKnownHeightProvider == nil {
Expand Down
68 changes: 68 additions & 0 deletions pkg/rpc/server/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,71 @@ func TestHealthReady_aggregatorBlockDelay(t *testing.T) {
})
}
}

func TestHealthReady_nonAggregatorBlockDelay(t *testing.T) {
logger := zerolog.Nop()

type spec struct {
readinessWindowSeconds uint64
delay time.Duration
expStatusCode int
expBody string
}

specs := map[string]spec{
"within readiness window": {
readinessWindowSeconds: 10,
delay: 5 * time.Second,
expStatusCode: http.StatusOK,
expBody: "READY\n",
},
"exceeds readiness window": {
readinessWindowSeconds: 10,
delay: 15 * time.Second,
expStatusCode: http.StatusServiceUnavailable,
expBody: "UNREADY: node not executing blocks\n",
},
"zero readiness window falls back to default 15s": {
readinessWindowSeconds: 0,
delay: 10 * time.Second,
expStatusCode: http.StatusOK,
expBody: "READY\n",
},
}

for name, tc := range specs {
t.Run(name, func(t *testing.T) {
mux := http.NewServeMux()

cfg := config.DefaultConfig()
cfg.Node.Aggregator = false
cfg.Node.ReadinessWindowSeconds = tc.readinessWindowSeconds

mockStore := mocks.NewMockStore(t)
state := types.State{
LastBlockHeight: 10,
LastBlockTime: time.Now().Add(-tc.delay),
}
mockStore.On("GetState", mock.Anything).Return(state, nil)

bestKnownHeightProvider := func() uint64 { return state.LastBlockHeight }

RegisterCustomHTTPEndpoints(mux, mockStore, nil, cfg, bestKnownHeightProvider, logger, nil)

ts := httptest.NewServer(mux)
t.Cleanup(ts.Close)

req, err := http.NewRequest(http.MethodGet, ts.URL+"/health/ready", nil)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req) //nolint:gosec // ok to use default client in tests
require.NoError(t, err)
t.Cleanup(func() { _ = resp.Body.Close() })

body, err := io.ReadAll(resp.Body)
require.NoError(t, err)

assert.Equal(t, tc.expStatusCode, resp.StatusCode)
assert.Equal(t, tc.expBody, string(body))
})
}
}
Loading