Skip to content

fix(001): measure head lag against a node we hold, not the provider's clock - #2267

Merged
Flotapponnier merged 1 commit into
devfrom
fix/head-lag-node-reference
Sep 5, 2026
Merged

fix(001): measure head lag against a node we hold, not the provider's clock#2267
Flotapponnier merged 1 commit into
devfrom
fix/head-lag-node-reference

Conversation

@Flotapponnier

Copy link
Copy Markdown
Collaborator

The defect

benchmarks/aggregator-head-lag.yml promises, in three places, that head lag is measured against archive nodes:

methodology[7]: "Reference: archive nodes per chain, validated against block hashes."
FAQ: "The harness holds a live WebSocket subscription to canonical-tip archive nodes on each chain."

The harness never did that. Both provider paths compute receiveTime - <the timestamp the provider itself sent us>:

// head_lag_monitor.go:211  Mobula
onChainTime := time.UnixMilli(trade.Date)
// head_lag_monitor.go:707  Codex
onChainTime := time.Unix(event.Timestamp, 0)

grep -rl "archive|eth_getBlockByNumber|getBlockTime|blockTimestamp" over the harness returns nothing.

Measured on trades matched by hash: the same swap carries timestamps 707 ms apart on Solana and 1,000 ms apart on Base depending on which provider you ask. The published leaderboard partly ranks clock placement, not pipeline speed.

The fix

One WebSocket subscription per monitored pool, straight to a node (eth_subscribe logs on EVM, logsSubscribe with a mentions filter on Solana), timestamping every swap on receipt. Provider emissions are matched by transaction hash against that single clock, so every provider is measured with the same ruler.

Published as head_lag_ref_seconds beside head_lag_seconds, not instead of it, so the leaderboard and its history keep working while the two are compared.

Validated before shipping, and it changed the design twice

Check Result
Hash match rate, Base 27/27 (100%)
Hash match rate, Solana 4/4 (100%)
Legacy method error vs this one, Base 1,946 ms on the median
Legacy method error vs this one, Solana 331 ms on the median

Two things the validation caught that would have shipped broken otherwise:

  1. publicnode acknowledges logsSubscribe on Solana and then delivers nothing (0 events in 60 s while the EVM equivalents streamed), and drpc rejects the method on the free plan (code 35). Default switched to api.mainnet-beta.solana.com, which answers.
  2. Against public endpoints the reference node is slower than the providers. Mobula delivers the trade before our subscription sees it: p50 −1.20 s on Base, −0.32 s on Solana.

That second point is the binding constraint and it is documented at the call site rather than hidden. The node's own latency sits in every sample as a roughly constant offset, so the absolute value is not a head lag; the relative comparison between providers is sound, because they are all measured against the same clock on the same transaction, which is precisely what the legacy series cannot claim. Negative values are therefore kept and counted as ahead_of_reference rather than silently dropped, unlike the legacy path which discards every negative.

Point REF_WS_URL_<CHAIN> at a paid or colocated node to collapse the offset and make the absolute number meaningful. Robinhood Chain has no configured endpoint and simply leaves the series empty rather than measuring against something arbitrary.

Also: the reference dialer deliberately does not use getProxyDialer. The scraping proxy is currently quota-saturated and returns 429; routing the reference clock through it would add the proxy's own latency to the ruler.

Auditability

head_lag_ref_matches_total{outcome} counts matched, ahead_of_reference, unmatched and out_of_range, so a degraded reference subscription is visible in the metrics instead of quietly skewing the series. head_lag_ref_clock_entries publishes the window occupancy.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CpArutAtXuBb1BVNUDXoYA

… clock

The spec promises "Reference: archive nodes per chain, validated against
block hashes" in three places. The harness never did that: both provider
paths compute receiveTime minus the timestamp the provider itself sent.
grep for archive/getBlockByNumber/blockTimestamp over the harness returns
nothing.

Measured consequence: on the same transaction hash, Serialized and Mobula
disagree about when it happened by 707 ms on Solana and 1,000 ms on Base,
so the leaderboard partly ranks where each vendor puts its clock.

Adds one WebSocket subscription per monitored pool straight to a node,
timestamping every swap on receipt, matched to provider emissions by
transaction hash. Published as head_lag_ref_seconds beside the legacy
series so the old one keeps its history while the two are compared.

Validated before shipping at a 100% hash match rate on Base and Solana.
That validation also surfaced the binding constraint: against public
endpoints the reference node is SLOWER than the providers, so the series
carries the node's own latency as an offset and must be read as a
relative comparison until REF_WS_URL_<CHAIN> points at a paid node. All
of this is documented at the call site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpArutAtXuBb1BVNUDXoYA
@Flotapponnier
Flotapponnier merged commit 2c05e5d into dev Sep 5, 2026
1 check failed
@Flotapponnier
Flotapponnier deleted the fix/head-lag-node-reference branch September 5, 2026 19:58
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.

1 participant