Skip to content

fix(ws): refuse stale latest below TipHW (FOOS)#11

Merged
snowkide merged 11 commits into
feat/websocket-supportfrom
fix/ws-tip-http-floor-from-cached-head
Jul 24, 2026
Merged

fix(ws): refuse stale latest below TipHW (FOOS)#11
snowkide merged 11 commits into
feat/websocket-supportfrom
fix/ws-tip-http-floor-from-cached-head

Conversation

@jleeh

@jleeh jleeh commented Jul 22, 2026

Copy link
Copy Markdown

Summary

  • Stop fail-open to stale latest: EnforceHighestBlock no longer uses pickHighestBlock against the lagging response when the concrete TipHW re-fetch misses. That fail-open is the MultiNode FOOS trigger after WS newHeads already delivered a higher head.
  • Tip re-fetch path: pin to EvmLeaderUpstream (force-poll if needed), then retry unconstrained; accept only responses that meet the tip floor; otherwise return an error.
  • Availability gate: fail-open when the local poller lags network TipHW (WS/Redis already observed the block).
  • Cache: only skip enforcement for cached latest that already meets TipHW.

Test plan

  • TestHttpServer_GetBlockByNumberLatest_RefetchPinsEvmLeaderUpstream
  • TestHttpServer_GetBlockByNumberLatest_RefusesStaleFailOpen
  • TestHttpServer_EvmGetBlockByNumber (incl. FailFast cases updated for refuse-stale)
  • Build/roll snowkide/erpc:ws-tip-http-floor-test4 over prod DS (currently on test3)
  • Confirm OCR/TGO FOOS / No live RPC nodes drops; bounce CL only if still stuck after eRPC is healthy

TipHW alone still allowed eth_getBlockByNumber("latest") to fail-open to a
stale upstream block when re-fetch of the WS tip missed. Cache the newHeads
header before fan-out and prefer it in EnforceHighestBlock for header-only
requests so HTTP cannot regress below a tip already delivered on the pod.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
File Lines Key changes Risk
🟠 eth_getBlockByNumber.go +211/-106 tipRefresher, refreshHighestLatestBlockNumber, forwardGetBlockByNumber, ... ⚠ ErrEndpointMissingData, ErrJsonRpcExceptionInternal
🟠 networks.go +91/-14 EvmRefreshHighestLatestBlockNumber ⚠ ErrEndpointMissingData, ErrUpstreamBlockUnavailable
🔵 shared_state_variable.go +98/-9 TryUpdateAndPublish, RefreshFromRemote, publishRemoteState
🔵 evm_state_poller.go +13/-2 PollLatestBlockNumberNow, pollLatestBlockNumber
🔵 subscription_manager.go +10/-3 SuggestLatestBlock
🔵 upstream_fake.go +4/-0 PollLatestBlockNumberNow
12 test files +847

xray — see through AI slop with deterministic architecture PR diff reviews

Record the tip-source upstream id with the cached newHeads head, partition
eth_getBlockByNumber("latest") using TipHW so WS upstreams are tried first,
and pin EnforceHighestBlock re-fetch to that tip source instead of only
excluding the stale HTTP responder.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jleeh jleeh changed the title fix(ws): serve cached newHeads header when HTTP latest lags tip fix(ws): floor HTTP latest on cached tip and route to tip-source upstreams Jul 22, 2026
Tip ownership already lives on per-upstream SuggestLatestBlock/LatestBlock
and partitionUpstreamsByLatestBlock. Keep TipHW partitioning for "latest"
plus the header cache floor; remove the redundant upstreamId pin registry.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jleeh jleeh changed the title fix(ws): floor HTTP latest on cached tip and route to tip-source upstreams fix(ws): floor HTTP latest on cached tip; partition latest via TipHW Jul 22, 2026
Drop the newHeads header-cache / tip-source registry overbuild. Tip
ownership already lives on per-upstream pollers (SuggestLatestBlock) and
EvmLeaderUpstream; EnforceHighestBlock now UseUpstream-pins the concrete
tip re-fetch to that leader when its LatestBlock covers TipHW.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jleeh jleeh changed the title fix(ws): floor HTTP latest on cached tip; partition latest via TipHW fix(ws): pin EnforceHighestBlock tip re-fetch to EvmLeaderUpstream Jul 22, 2026
Availability checks and EnforceHighestBlock were calling PollLatestBlockNumber,
which can reuse a debounced tip behind network TipHW (WS/Redis), falsely
rejecting eth_call and fail-opening stale latest. Add PollLatestBlockNumberNow
and use it on those paths; force-poll the leader before pinning the tip re-fetch.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jleeh jleeh changed the title fix(ws): pin EnforceHighestBlock tip re-fetch to EvmLeaderUpstream fix(ws): pin tip re-fetch to leader + bypass poller debounce on tip gate Jul 23, 2026
EnforceHighestBlock used pickHighestBlock, which fail-opened to a lagging
"latest" when the concrete TipHW fetch returned null/error. That is the
MultiNode FOOS trigger after WS newHeads already delivered the higher head.

Re-fetch tip (leader pin, then unconstrained), accept only responses that
meet the tip floor, and return an error instead of stale. Also fail-open
the per-upstream availability gate when poller lags TipHW, and only skip
enforcement for cached latest that already meets the tip.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jleeh jleeh changed the title fix(ws): pin tip re-fetch to leader + bypass poller debounce on tip gate fix(ws): refuse stale latest below TipHW (FOOS) Jul 23, 2026
jleeh and others added 5 commits July 23, 2026 14:03
Cross-pod TipHW Redis push was async, so sibling pods could still serve
a lower HTTP latest after WS newHeads advanced highestUserObservations,
silently demoting MultiNode via FOOS. Publish TipHW before fan-out and
refresh from Redis when local TipHW would skip EnforceHighestBlock.

Co-authored-by: Cursor <cursoragent@cursor.com>
If TipHW advanced from a WS newHeads observation on this pod, HTTP
eth_getBlockByNumber("latest", false) must return that header instead of
hard-failing when concrete tip re-fetch cannot reach TipHW yet.

Co-authored-by: Cursor <cursoragent@cursor.com>
Fallback newHeads must not advance TipHW while primaries are up (same
rule as poller aggregation). Also let the per-request fallback escape
hatch fire when primaries return null/emptyish for a concrete tip block,
so tip re-fetch can use public fallbacks instead of hard-failing.

Co-authored-by: Cursor <cursoragent@cursor.com>
That path masked TipHW inflation / missing failover. Keep TipHW floor
from primary WS only, refuse-stale when tip re-fetch misses, and escape
to fallbacks on emptyish primary misses.

Co-authored-by: Cursor <cursoragent@cursor.com>
Skipping TipHW for fallback WS while Ingest still fans those heads out
caused MultiNode FOOS on matic. TipHW must cover every delivered head;
tip re-fetch of a fallback-advanced TipHW relies on the emptyish escape
hatch to reach public upstreams when primaries miss.

Co-authored-by: Cursor <cursoragent@cursor.com>
@snowkide
snowkide merged commit 08dd03b into feat/websocket-support Jul 24, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants