Skip to content

Real-Time Slashing Event Feed Duplicate Display Under WebSocket Reconnection #39

Description

@JamesEjembi

Real-Time Slashing Event Feed Duplicate Display Under WebSocket Reconnection

Problem Statement

The slashing event feed in src/components/slashing/SlashingEventFeed.tsx displays real-time slashing events as they occur. The useSlashingStream() hook at src/hooks/useSlashingStream.ts:55 subscribes to a WebSocket channel that emits SlashingEvent messages. When the WebSocket reconnects (after a brief network interruption), the server re-sends the last 5 events as a "catch-up" burst (to ensure the client doesn't miss events during disconnection). The hook's onMessage handler at line 70 pushes each event into a useState<SlashingEvent[]> list. If the client was disconnected for 2 seconds and 3 slashing events occurred during that time, the catch-up burst includes those 3 events. However, the client may have already received some of these events before disconnection (partial overlap). The client processes the catch-up events without deduplication, adding the same 3 events again. The feed shows 6 events instead of 3. Duplicate slashing events in the feed cause the NOC operator to believe more nodes are being slashed than actually are, potentially triggering an unnecessary emergency response.

State Invariants & Parameters

  • WebSocket reconnection: brief (50ms-5s)
  • Catch-up burst size: 5 events
  • Max events during disconnection: up to 5 (covered by catch-up)
  • Partial overlap probability: high (events received before disconnect)
  • Feed display: chronological list with event.id as key
  • Invariant: ∀ event_id: count(feed_events[event_id]) <= 1

Affected Code Paths

  • src/hooks/useSlashingStream.ts:50-90onMessage handler with no dedup
  • src/hooks/useWebSocketReconnect.ts:40-65 — Reconnection logic with catch-up
  • src/components/slashing/SlashingEventFeed.tsx:45-75 — Feed rendering
  • src/hooks/tests/useSlashingStream.test.ts — No reconnection dedup test

Resolution Blueprint

  1. Add a received event ID set in useSlashingStream.ts: maintain a Set<eventId> of recently received event IDs (with TTL of 5 minutes via Map<eventId, timestamp> and periodic cleanup). Before adding an event to the feed, check if eventId ∈ receivedIds. If yes, skip. Add the eventId to the set.
  2. Implement server-side dedup headers: the catch-up burst includes a x-catchup-from header with the last event ID the server has. The client sends its last received event ID in the x-last-event-id header on reconnect. The server only sends events with id > client_last_event_id. This eliminates the partial overlap entirely.
  3. Use a keyed React render with event.id as the key prop: React's reconciliation will not re-render duplicate keys. However, the event will still be in the array (just not rendered visually). To remove it, filter the array on insertion: setEvents(prev => prev.some(e => e.id === event.id) ? prev : [...prev, event]).
  4. In the feed component, add an invisible dedup layer: a useRef<Set<string>> tracks displayed event IDs. Before rendering, filter out any event whose ID is already displayed. The dedup layer is reset only on full page navigation.
  5. Add a WebSocket reconnection e2e test that simulates a 2-second disconnect during which 3 slashing events are emitted, then reconnects, and verifies exactly 3 unique events appear in the feed (no duplicates).

Labels

  • Complexity: Hardcore
  • Layer: Core-Engine
  • Type: Race-Condition

Metadata

Metadata

Assignees

Labels

Complexity: HardcoreExtremely difficult, high-complexity engineering taskGrantFox OSSIssue tracked in GrantFox OSSLayer: Core-EngineCore engine layerMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial CampaignCampaign: Official CampaignType: Race-ConditionConcurrency and race condition related issues

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions