diff --git a/benchmarks/indexer-latency.yml b/benchmarks/indexer-latency.yml deleted file mode 100644 index 5a44c1d1..00000000 --- a/benchmarks/indexer-latency.yml +++ /dev/null @@ -1,128 +0,0 @@ -# OpenChainBench. Bench № 084 - -slug: indexer-latency -number: "084" -title: Fastest blockchain indexer. Envio HyperSync, The Graph, Mobula -seo_title: "Fastest blockchain indexer 2026: HyperSync vs The Graph vs Mobula" -seo_description: "How fast do indexing pipelines make new onchain events queryable? Envio HyperSync, The Graph and Mobula measured live on organic Ethereum USDC transfers, second by second." -subtitle: Seconds between a USDC transfer confirming on Ethereum and the moment each indexing pipeline first makes that block's data queryable, measured continuously on organic transfers. -category: Aggregators -status: live -metric: Time to queryable -unit: sec -higher_is_better: false - -seo_intro: | - Every dapp backend, analytics product and trading system sits on an - indexing pipeline, and in 2026 the choice of that pipeline is a live - question again. Dune is sunsetting Sim, teams are re-evaluating - their data stack, and Envio markets HyperSync as "10x faster" than - alternatives without publishing comparable numbers. This benchmark - measures the claim the only way that cannot be gamed. We pick a - random, organic USDC transfer from the newest Ethereum block, then - poll each indexing pipeline until that block's data is queryable. - Because the ground truth is a random real transaction there is - nothing a provider could special case, and every measurement is - publicly re-verifiable from the tx hash. Cohort. Envio HyperSync - (low level log query engine), The Graph (decentralized subgraph - network, probed through the canonical Uniswap V3 subgraph) and - Mobula (wallet-history pipeline), all probed on the identical - schedule from the same host. This cohort is indexing - infrastructure; wallet data APIs (Zerion, Moralis, Mobula and - friends) are raced separately in bench 070, with zero provider - overlap between the two. - -abstract: | - We measure time to queryable for indexing infrastructure. The delay - between an organic USDC transfer confirming on Ethereum (T0 = the - instant our own keyless reference RPC first shows the containing - block) and the first poll at which each provider can return that - block's data. One probe event per 5 minutes; each event uses a fresh - random transfer from a fresh block. The poll schedule is front - loaded (1s to 240s, identical for every provider) and detection is - parser free where the API returns the tx hash verbatim. Because the - three pipelines expose different query surfaces, the probed entity - differs per provider and is disclosed. HyperSync is asked for the - exact Transfer log, Mobula for the exact transfer via its indexed - transfers API, The Graph for a block pinned query on the canonical - Uniswap V3 subgraph that only succeeds once the subgraph has indexed - the reference block. The measured question is identical in all three - cases. is block N data queryable at time T? Events not queryable - within 240 seconds count as timeouts and feed the reliability - column. Per provider monthly quota guards keep the probe inside - every free tier. - -methodology: - - "Ground truth: one probe event per 5 minutes. The harness watches new Ethereum blocks through its own keyless reference RPC (publicnode, never a cohort member's endpoint) and picks one random USDC Transfer log from the newest block. T0 is the instant the harness first observes the containing block; the same T0 is used for every provider." - - "Anti-gaming by construction: the probe target is a random organic USDC transfer, different on every event, so no provider can pre-warm a known benchmark entity. Every measurement is re-verifiable by anyone from the public tx hash. No canary contract is deployed; the benchmark piggybacks on the natural high frequency USDC stream at zero onchain cost." - - "Poll schedule: each provider is polled at 1, 2, 3, 5, 8, 12, 20, 30, 45, 60, 90, 120, 180 and 240 seconds after T0. Reported lag is the first poll at which block N's data is queryable, an upper bound with resolution equal to the gap between consecutive polls." - - "Probed entity per provider, disclosed because the pipelines expose different query surfaces: HyperSync is queried via POST /query for the exact Transfer log in block N; Mobula via its wallet-transactions endpoint for the transfer recipient captured from the log topics; The Graph via a block pinned GraphQL query on the canonical Uniswap V3 Ethereum subgraph, which graph-node rejects until the subgraph has indexed block N. The measured question is identical: is block N data queryable at time T." - - "The Graph number therefore includes the full subgraph pipeline (firehose, indexer sync, gateway routing) rather than a raw data lookup. That is the honest comparison: it is the path every subgraph consumer actually waits on, but readers should know it is a pipeline measurement, not a single database read." - - "Detection is parser free where possible: HyperSync and Mobula responses are scanned for the tx hash substring, so no provider gains or loses from response schema differences. The Graph probe succeeds when the block pinned query returns data without a block range error." - - "Classification: found (lag recorded), timeout (not queryable within 240s), error (provider errored on every poll of the event), quota_paused (monthly guard tripped), disabled (credential not configured). Percentiles are computed from the histogram of found lags over 24h; the timeout rate is published alongside because latency without reliability is a misleading ranking signal." - - "Quota fairness: per provider monthly call budgets sized to each free tier with headroom, enforced by a guard that pauses probing at 90 percent. The Graph joins every second event (100k gateway queries/month free plan); HyperSync and Mobula join every event. Sample sizes per provider are published." - - "Disclosure: Mobula operates OpenChainBench. Its poller runs the identical probe schedule, quota guard and tx hash detection as every other cohort member, and every measurement is re-verifiable from the public tx hash. Alchemy was measured during development but is excluded from the live cohort because probing it rides a paid plan, and this bench only races pipelines that cost nothing to probe; a dedicated free tier key would re-qualify it." - - "Excluded from v1 with reasons: Goldsky (requires deploying and paying for your own subgraph, no neutral shared surface to probe), Dune (2500 free credits/month cannot sustain continuous probing), Bitquery (free tier is a one month trial), Moralis (already raced in bench 070 and its free key is saturated there), SubQuery and Ponder (no measurable hosted free tier)." - - "Cohort positioning: this bench races indexing infrastructure. Wallet data APIs (Zerion, Moralis, Allium, GoldRush, Mobula) are a different product layer and are raced separately in bench 070 with an identical T0 and poll design; the two cohorts share zero providers by construction." - - "Scope: Ethereum mainnet, single probe region. Time to queryable is measured in seconds while cross region network deltas are milliseconds, so multi region probing would add cost without changing the ranking." - -findings: - - "{{best_name}} currently leads at {{best_p50}} (p50 of found lags, 24h) on organic Ethereum USDC transfers." - - "{{name:hypersync}} sits at {{p50:hypersync}}. Envio's marketing claims 10x faster retrieval than alternatives; this page is the first continuously measured public number behind that claim." - - "{{name:thegraph}} runs at {{p50:thegraph}} through the canonical Uniswap V3 subgraph. This is a full pipeline measurement (firehose, indexer, gateway), the path every subgraph consumer actually waits on." - - "{{name:mobula}} sits at {{p50:mobula}} on its wallet-history pipeline, the same surface portfolio and activity feeds built on Mobula consume." - - "Read the reliability column before the latency one: a pipeline that occasionally never surfaces an event within 240 seconds breaks downstream consumers in a way a slow median does not." - -faq: - - q: "Which blockchain indexer makes new events queryable fastest?" - a: "Per the live leaderboard above: {{best_name}} at {{best_p50}} (p50 over 24h of organic Ethereum USDC transfers). The ranking re-sorts continuously as probe events land every 5 minutes. Check the reliability column too; a pipeline that occasionally never surfaces an event within 240 seconds is worse for most consumers than one that is a second slower on median." - - q: "How is indexer latency measured here?" - a: "The harness picks a random organic USDC transfer from the newest Ethereum block, records T0 when its own keyless reference RPC first shows the block, then polls each provider on an identical front loaded schedule (1s to 240s) until that block's data is queryable. HyperSync and Mobula are asked for the exact transfer; The Graph is asked a block pinned query on the canonical Uniswap V3 subgraph that only succeeds once block N is indexed. The full harness is open source and each measurement can be re-verified from the public tx hash." - - q: "Is comparing HyperSync, The Graph and Mobula apples to apples?" - a: "Not perfectly, and the asymmetry is disclosed rather than hidden. HyperSync serves raw data and Mobula a wallet-scoped view of it; The Graph serves subgraph entities, so its number includes the whole firehose plus indexer plus gateway pipeline. All three answer the same operational question a builder has: after an event confirms onchain, how long until my query layer can see it. That is the number this page publishes, per pipeline, with the probed entity documented." - - q: "Why are Goldsky, Dune, SubQuery and Ponder missing?" - a: "Each lacks a neutral, continuously probeable free surface in v1. Goldsky requires deploying your own subgraph, so any probe would measure our deployment, not the platform baseline. Dune's free tier (2500 credits/month) cannot sustain a 5 minute cadence. Bitquery's free tier is a one month trial. SubQuery and Ponder have no measurable hosted free tier. Moralis is already raced in the wallet data cohort of bench 070. Providers change tiers often; the cohort is revisited when a fair probe becomes possible." - - q: "Why does indexer latency matter in 2026?" - a: "Because the indexing market is consolidating and re-forming at once. Dune is sunsetting Sim, pushing teams to re-evaluate their data stack, while Envio advertises HyperSync as 10x faster with no public comparable numbers. For trading systems, alerting and activity feeds, the seconds between confirmation and queryability are the product. This page is the only continuously measured, provider neutral comparison of that number across indexing infrastructure." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/indexer-latency - -prometheus: - window: 24h - freshness_metric: idx_health - expected_freshness_seconds: 1800 - -providers: - - slug: hypersync - name: Envio HyperSync - tag: Low level log query engine, claims 10x faster retrieval - formula: "Median (p50) over 24h of seconds between our reference RPC first showing an Ethereum block and HyperSync /query returning that block's probe USDC Transfer log, from the shared histogram estimator." - queries: - p50: histogram_quantile(0.5, sum by (le) (increase(idx_latency_seconds_bucket{provider="hypersync"}[24h]))) - p90: histogram_quantile(0.9, sum by (le) (increase(idx_latency_seconds_bucket{provider="hypersync"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (increase(idx_latency_seconds_bucket{provider="hypersync"}[24h]))) - success: sum(increase(idx_probe_total{provider="hypersync", result="found"}[24h])) / sum(increase(idx_probe_total{provider="hypersync", result=~"found|timeout"}[24h])) - sample_size: sum(increase(idx_probe_total{provider="hypersync", result=~"found|timeout"}[24h])) - series: avg_over_time(idx_latency_last_seconds{provider="hypersync"}[1h]) - - slug: thegraph - name: The Graph - tag: Decentralized subgraph network, probed via Uniswap V3 subgraph - formula: "Median (p50) over 24h of seconds until a block pinned query on the canonical Uniswap V3 subgraph succeeds for the reference block. Includes the full firehose, indexer and gateway pipeline; joins every second event." - queries: - p50: histogram_quantile(0.5, sum by (le) (increase(idx_latency_seconds_bucket{provider="thegraph"}[24h]))) - p90: histogram_quantile(0.9, sum by (le) (increase(idx_latency_seconds_bucket{provider="thegraph"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (increase(idx_latency_seconds_bucket{provider="thegraph"}[24h]))) - success: sum(increase(idx_probe_total{provider="thegraph", result="found"}[24h])) / sum(increase(idx_probe_total{provider="thegraph", result=~"found|timeout"}[24h])) - sample_size: sum(increase(idx_probe_total{provider="thegraph", result=~"found|timeout"}[24h])) - series: avg_over_time(idx_latency_last_seconds{provider="thegraph"}[1h]) - - slug: mobula - name: Mobula - tag: Wallet-history indexing pipeline (Octopus engine) - formula: "Median (p50) over 24h of seconds between our reference RPC first showing a block and Mobula's wallet-transactions endpoint returning that block's probe USDC transfer for the recipient wallet, from the shared histogram estimator." - queries: - p50: histogram_quantile(0.5, sum by (le) (increase(idx_latency_seconds_bucket{provider="mobula"}[24h]))) - p90: histogram_quantile(0.9, sum by (le) (increase(idx_latency_seconds_bucket{provider="mobula"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (increase(idx_latency_seconds_bucket{provider="mobula"}[24h]))) - success: sum(increase(idx_probe_total{provider="mobula", result="found"}[24h])) / sum(increase(idx_probe_total{provider="mobula", result=~"found|timeout"}[24h])) - sample_size: sum(increase(idx_probe_total{provider="mobula", result=~"found|timeout"}[24h])) - series: avg_over_time(idx_latency_last_seconds{provider="mobula"}[1h]) diff --git a/harnesses/indexer-latency/Dockerfile b/harnesses/indexer-latency/Dockerfile deleted file mode 100644 index 63108cfc..00000000 --- a/harnesses/indexer-latency/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM golang:1.24-alpine AS builder - -WORKDIR /app -RUN apk add --no-cache git - -COPY go.mod go.sum ./ -RUN go mod download - -COPY . . - -RUN CGO_ENABLED=0 GOOS=linux go build -o /app/monitor ./cmd/script - -FROM debian:bookworm-slim - -WORKDIR /app -RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /app/monitor /app/monitor - -EXPOSE 2112 - -CMD ["/app/monitor"] diff --git a/harnesses/indexer-latency/cmd/script/alchemy.go b/harnesses/indexer-latency/cmd/script/alchemy.go deleted file mode 100644 index 9e46a8a7..00000000 --- a/harnesses/indexer-latency/cmd/script/alchemy.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "strings" -) - -// Alchemy poller. -// -// Probed entity: the exact USDC Transfer (matched by tx hash) via -// alchemy_getAssetTransfers with fromBlock=toBlock=N and the USDC -// contract filter. This is Alchemy's indexed transfers pipeline (not a -// raw eth_getLogs proxy), which is what apps built on Alchemy's data -// APIs actually consume. ~150 CU/call on the 30M CU/month free tier. -// -// The key rides in the URL path per Alchemy convention; sanitize() -// strips URLs from any logged error so it can never leak. - -func checkAlchemy(ev probeEvent) (bool, error) { - bn, txHash := ev.Block, ev.TxHash - apiCalls.WithLabelValues("alchemy").Inc() - blk := fmt.Sprintf("0x%x", bn) - body := fmt.Sprintf( - `{"jsonrpc":"2.0","id":1,"method":"alchemy_getAssetTransfers","params":[{"fromBlock":"%s","toBlock":"%s","contractAddresses":["%s"],"category":["erc20"],"maxCount":"0x3e8","withMetadata":false,"excludeZeroValue":false}]}`, - blk, blk, usdcContract, - ) - u := "https://eth-mainnet.g.alchemy.com/v2/" + alchemyAPIKey() - req, err := http.NewRequest("POST", u, strings.NewReader(body)) - if err != nil { - return false, err - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", "OpenChainBench/1.0 (+https://openchainbench.com)") - resp, err := httpClient.Do(req) - if err != nil { - return false, err - } - return bodyContains(resp, txHash) -} diff --git a/harnesses/indexer-latency/cmd/script/config.go b/harnesses/indexer-latency/cmd/script/config.go deleted file mode 100644 index fc34331b..00000000 --- a/harnesses/indexer-latency/cmd/script/config.go +++ /dev/null @@ -1,109 +0,0 @@ -package main - -import ( - "os" - "strconv" - "strings" - "time" -) - -// indexer-latency — bench №084. -// -// Cohort: indexing pipelines reachable at $0 recurring cost (HyperSync, -// The Graph, Mobula). Bench 070 (indexing-freshness) races wallet-data -// APIs on Base native transfers; Mobula appears in both cohorts but on -// a different chain and event class (Ethereum ERC-20 event -// queryability here), so the two benches stay complementary. Alchemy -// was dropped from the cohort: the team's Alchemy account is a paid -// plan, and this bench only runs providers that cost nothing to probe. -// (checkAlchemy stays in the tree for a future dedicated free-tier -// key.) - -// Provider is one indexing pipeline we race. Enabled reports whether -// the required credential is present; a provider without one runs in -// the "disabled" state (counted, health=0) so the harness degrades -// gracefully instead of exiting. -// -// EveryN throttles participation for quota-tight free tiers: the -// provider only joins every Nth probe event. Budget is the monthly -// API-call budget for the guard (90% cutoff, calendar-month reset). -type Provider struct { - Slug string - EveryN int - Budget int64 - Enabled func() bool - // Check asks the provider whether the probe event (USDC Transfer - // tx in block bn) is queryable yet. The probed entity differs per - // provider (raw log vs subgraph block head vs wallet history) — - // disclosed in the bench methodology; the question is identical: - // "is block N data queryable at time T?". - Check func(ev probeEvent) (bool, error) -} - -// probeEvent is one sampled USDC Transfer: block, tx hash, and the -// recipient wallet (topics[2] of the Transfer log), which -// wallet-scoped pipelines like Mobula need to form their query. -type probeEvent struct { - Block uint64 - TxHash string - Wallet string -} - -var cohort = []Provider{ - // HyperSync free tier (token required since Nov 2025) is sized in - // the hundreds of millions of rows/month; our row volume is tiny, - // so the guard is request-count based with wide headroom. - {Slug: "hypersync", EveryN: 1, Budget: 400_000, Enabled: func() bool { return hypersyncToken() != "" }, Check: checkHyperSync}, - // The Graph free plan: 100k gateway queries/month. EveryN=2 on the - // 5-min event cadence = 1 probe event / 10 min → 144 events/day × - // 14 polls ≈ 60k queries/month worst case, inside the 90k budget. - {Slug: "thegraph", EveryN: 2, Budget: 90_000, Enabled: func() bool { return graphAPIKey() != "" }, Check: checkTheGraph}, - // Mobula: operated by the same team as OpenChainBench (disclosed in - // the spec); free for this project. Budget mirrors bench 070's - // Mobula guard so the two harnesses stay inside one key's comfort. - {Slug: "mobula", EveryN: 1, Budget: 250_000, Enabled: func() bool { return mobulaAPIKey() != "" }, Check: checkMobula}, -} - -const chainSlug = "ethereum" - -// USDC on Ethereum mainnet + the ERC-20 Transfer topic — the natural -// high-frequency event stream we piggyback on (no canary contract, $0). -const ( - usdcContract = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - transferTopic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" -) - -func hypersyncToken() string { return strings.TrimSpace(os.Getenv("HYPERSYNC_TOKEN")) } -func graphAPIKey() string { return strings.TrimSpace(os.Getenv("GRAPH_API_KEY")) } -func alchemyAPIKey() string { return strings.TrimSpace(os.Getenv("ALCHEMY_API_KEY")) } -func mobulaAPIKey() string { return strings.TrimSpace(os.Getenv("MOBULA_API_KEY")) } - -// rpcHTTP: OUR keyless reference RPC. T0 = the instant it first shows -// the block containing the probe event. publicnode by default; never a -// cohort member's RPC (that would gift them a head start). -func rpcHTTP() string { - if v := strings.TrimSpace(os.Getenv("INDEXER_RPC_HTTP")); v != "" { - return v - } - return "https://ethereum-rpc.publicnode.com" -} - -// eventInterval: one probe event (fresh USDC Transfer picked from a new -// block) per interval. 5 min default → 288 events/day, which keeps -// every provider inside its monthly free quota given the poll schedule -// (The Graph additionally joins only every 2nd event). -func eventInterval() time.Duration { - if v := strings.TrimSpace(os.Getenv("IDX_EVENT_SECONDS")); v != "" { - if n, err := strconv.Atoi(v); err == nil && n >= 30 { - return time.Duration(n) * time.Second - } - } - return 5 * time.Minute -} - -// pollSchedule: seconds after T0 at which each provider is polled. -// Front-loaded because HyperSync claims sub-second freshness while -// subgraph pipelines can lag minutes; capped at 240s (< event interval) -// after which the event counts as "timeout". Measured lag is an upper -// bound with resolution equal to the gap between consecutive polls. -var pollSchedule = []int{1, 2, 3, 5, 8, 12, 20, 30, 45, 60, 90, 120, 180, 240} diff --git a/harnesses/indexer-latency/cmd/script/hypersync.go b/harnesses/indexer-latency/cmd/script/hypersync.go deleted file mode 100644 index 6116ec9c..00000000 --- a/harnesses/indexer-latency/cmd/script/hypersync.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "strings" -) - -// Envio HyperSync poller. -// -// Probed entity: the exact USDC Transfer log (matched by tx hash) via -// POST /query scoped to [bn, bn+1). While HyperSync's archive height is -// below bn the query legitimately returns no logs (next_block <= bn), -// so substring detection on the tx hash is a correct "queryable yet?" -// signal. /query requires a free bearer token since Nov 2025 -// (HYPERSYNC_TOKEN); without it the provider runs disabled. - -const hypersyncBase = "https://eth.hypersync.xyz" - -func checkHyperSync(ev probeEvent) (bool, error) { - bn, txHash := ev.Block, ev.TxHash - apiCalls.WithLabelValues("hypersync").Inc() - body := fmt.Sprintf( - `{"from_block":%d,"to_block":%d,"logs":[{"address":["%s"],"topics":[["%s"]]}],"field_selection":{"log":["transaction_hash","block_number","log_index"]}}`, - bn, bn+1, usdcContract, transferTopic, - ) - req, err := http.NewRequest("POST", hypersyncBase+"/query", strings.NewReader(body)) - if err != nil { - return false, err - } - req.Header.Set("Authorization", "Bearer "+hypersyncToken()) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", "OpenChainBench/1.0 (+https://openchainbench.com)") - resp, err := httpClient.Do(req) - if err != nil { - return false, err - } - return bodyContains(resp, txHash) -} - -// hypersyncHeight hits the keyless /height endpoint. Used at startup as -// a reachability proof (and logged) even when HYPERSYNC_TOKEN is unset. -func hypersyncHeight() (uint64, error) { - req, err := http.NewRequest("GET", hypersyncBase+"/height", nil) - if err != nil { - return 0, err - } - req.Header.Set("User-Agent", "OpenChainBench/1.0 (+https://openchainbench.com)") - resp, err := httpClient.Do(req) - if err != nil { - return 0, err - } - defer resp.Body.Close() - raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) - if err != nil { - return 0, err - } - if resp.StatusCode != 200 { - return 0, fmt.Errorf("status %d", resp.StatusCode) - } - var out struct { - Height uint64 `json:"height"` - } - if err := json.Unmarshal(raw, &out); err != nil { - return 0, err - } - return out.Height, nil -} diff --git a/harnesses/indexer-latency/cmd/script/loghub.go b/harnesses/indexer-latency/cmd/script/loghub.go deleted file mode 100644 index 9dd74488..00000000 --- a/harnesses/indexer-latency/cmd/script/loghub.go +++ /dev/null @@ -1,114 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "io" - "net/http" - "os" - "strconv" - "sync" - "time" -) - -// Auto-generated by the loghub inline pattern. Captures stdout/stderr into a -// bounded ring buffer and exposes GET /logs?tail=N protected by X-Logs-Token -// matching the LOGS_TOKEN env var. -// -// Keep in sync across miniapps (was previously the shared/loghub package; we -// inline because Railway's per-harness Docker build context can't reach a -// sibling shared module via go.mod replace). - -const logRingMax = 5000 - -type logRing struct { - mu sync.Mutex - lines []string - max int -} - -var globalLogRing = &logRing{max: logRingMax} - -func (b *logRing) push(line string) { - entry := time.Now().UTC().Format("2006-01-02T15:04:05.000Z") + " " + line - b.mu.Lock() - if len(b.lines) >= b.max { - b.lines = append(b.lines[1:], entry) - } else { - b.lines = append(b.lines, entry) - } - b.mu.Unlock() -} - -func (b *logRing) snapshot(tail int) []string { - b.mu.Lock() - defer b.mu.Unlock() - if tail <= 0 || tail >= len(b.lines) { - out := make([]string, len(b.lines)) - copy(out, b.lines) - return out - } - start := len(b.lines) - tail - out := make([]string, tail) - copy(out, b.lines[start:]) - return out -} - -var logSetupOnce sync.Once - -// installLogCapture replaces os.Stdout (and os.Stderr) with the write-end of a -// pipe, then spawns a goroutine that fan-outs every line to the original -// stdout AND the in-memory ring buffer. Call exactly once, very early in -// main(). -func installLogCapture() { logSetupOnce.Do(doInstallLogCapture) } - -func doInstallLogCapture() { - originalStdout := os.Stdout - originalStderr := os.Stderr - r, w, err := os.Pipe() - if err != nil { - fmt.Fprintf(originalStdout, "[loghub] pipe failed: %v (/logs will be empty)\n", err) - return - } - os.Stdout = w - os.Stderr = w - - go func() { - scanner := bufio.NewScanner(r) - buf := make([]byte, 0, 1024*1024) - scanner.Buffer(buf, 1024*1024) - for scanner.Scan() { - line := scanner.Text() - fmt.Fprintln(originalStdout, line) - globalLogRing.push(line) - } - _, _ = io.Copy(originalStdout, r) - _ = originalStderr - }() -} - -// logsHandler returns an http.Handler for GET /logs?tail=N. Requires header -// X-Logs-Token to match the LOGS_TOKEN env var. Returns 403 if env unset. -func logsHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - expected := os.Getenv("LOGS_TOKEN") - if expected == "" { - http.Error(w, "logs disabled: LOGS_TOKEN unset", http.StatusForbidden) - return - } - if r.Header.Get("X-Logs-Token") != expected { - http.Error(w, "forbidden", http.StatusForbidden) - return - } - tail := 500 - if t := r.URL.Query().Get("tail"); t != "" { - if n, err := strconv.Atoi(t); err == nil && n > 0 { - tail = n - } - } - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - for _, l := range globalLogRing.snapshot(tail) { - fmt.Fprintln(w, l) - } - }) -} diff --git a/harnesses/indexer-latency/cmd/script/main.go b/harnesses/indexer-latency/cmd/script/main.go deleted file mode 100644 index 7f72c892..00000000 --- a/harnesses/indexer-latency/cmd/script/main.go +++ /dev/null @@ -1,361 +0,0 @@ -package main - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "math/rand" - "net/http" - "os" - "os/signal" - "strconv" - "strings" - "sync" - "syscall" - "time" -) - -// indexer-latency — bench №084. -// -// How fast does indexing infrastructure make new onchain events -// queryable? One probe event per interval: pick a fresh USDC Transfer -// from the newest Ethereum block via our keyless reference RPC -// (T0 = the instant WE first see the block), then poll each indexing -// pipeline on a front-loaded schedule until block N's data is -// queryable (lag = poll time − T0) or 240s passes (timeout). -// -// $0 methodology: no canary contract, we piggyback on the natural -// high-frequency USDC stream. Ground truth is a random organic -// transfer, different every event, publicly re-verifiable by tx hash. - -func main() { - installLogCapture() // capture stdout into /logs ring buffer - - fmt.Println("=== Indexer Latency Harness (bench 084) ===") - fmt.Println("OpenChainBench — onchain event → indexer time-to-queryable.") - fmt.Printf("Reference RPC: %s\n", rpcHTTP()) - - // Keyless reachability proof: HyperSync /height vs our RPC head. - if h, err := hypersyncHeight(); err == nil { - fmt.Printf("HyperSync /height (keyless): %d\n", h) - } else { - fmt.Printf("HyperSync /height (keyless): unreachable (%s)\n", sanitize(err)) - } - - enabled := 0 - fmt.Printf("Chain: %s | event interval: %s | poll cap: %ds\n", chainSlug, eventInterval(), pollSchedule[len(pollSchedule)-1]) - for _, p := range cohort { - state := "disabled (credential missing)" - if p.Enabled() { - state = fmt.Sprintf("enabled everyN=%d budget=%d calls/mo", p.EveryN, p.Budget) - enabled++ - } - fmt.Printf(" - %-10s %s\n", p.Slug, state) - } - if enabled == 0 { - fmt.Println("[warn] no provider credentials set (HYPERSYNC_TOKEN / GRAPH_API_KEY / MOBULA_API_KEY)") - fmt.Println("[warn] running in degraded mode: events sampled, all providers report result=disabled") - } - - addr := ":2112" - if v := strings.TrimSpace(os.Getenv("METRICS_ADDR")); v != "" { - addr = v - } - fmt.Printf("Metrics server: %s/metrics\n\n", addr) - go func() { - if err := StartMetricsServer(addr); err != nil { - fmt.Printf("[fatal] metrics server: %v\n", err) - os.Exit(1) - } - }() - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - go runEvents(ctx) - - sig := make(chan os.Signal, 1) - signal.Notify(sig, os.Interrupt, syscall.SIGTERM) - s := <-sig - fmt.Printf("\n[shutdown] %v\n", s) - cancel() -} - -func runEvents(ctx context.Context) { - t := time.NewTicker(eventInterval()) - defer t.Stop() - var lastSeen uint64 - eventN := 0 - for { - eventN++ - ev, t0 := waitFreshEvent(ctx, lastSeen) - if ev.Block == 0 { - return // ctx cancelled - } - lastSeen = ev.Block - fmt.Printf("[event %d] block=%d usdc-transfer tx=%s\n", eventN, ev.Block, ev.TxHash[:14]+"…") - - var wg sync.WaitGroup - for _, p := range cohort { - if eventN%p.EveryN != 0 { - continue - } - if !p.Enabled() { - probeTotal.WithLabelValues(p.Slug, "disabled").Inc() - health.WithLabelValues(p.Slug).Set(0) - fmt.Printf(" %-10s disabled (credential missing)\n", p.Slug) - continue - } - if !quota.allow(p) { - probeTotal.WithLabelValues(p.Slug, "quota_paused").Inc() - fmt.Printf(" %-10s quota guard tripped — paused until month rollover\n", p.Slug) - continue - } - wg.Add(1) - go func(p Provider) { - defer wg.Done() - raceProvider(ctx, p, ev, t0) - }(p) - } - wg.Wait() - - select { - case <-ctx.Done(): - return - case <-t.C: - } - } -} - -// raceProvider polls one indexing pipeline on the shared front-loaded -// schedule until block bn's probe event is queryable, or the schedule -// is exhausted (timeout), or every poll errored (error). -func raceProvider(ctx context.Context, p Provider, ev probeEvent, t0 time.Time) { - var lastErr error - polls, errs := 0, 0 - for _, after := range pollSchedule { - wait := time.Until(t0.Add(time.Duration(after) * time.Second)) - if wait > 0 { - select { - case <-ctx.Done(): - return - case <-time.After(wait): - } - } - polls++ - found, err := p.Check(ev) - if err != nil { - errs++ - lastErr = err - continue - } - if found { - lag := time.Since(t0).Seconds() - latencyHist.WithLabelValues(p.Slug).Observe(lag) - latencyLast.WithLabelValues(p.Slug).Set(lag) - probeTotal.WithLabelValues(p.Slug, "found").Inc() - health.WithLabelValues(p.Slug).Set(1) - fmt.Printf(" %-10s queryable in %.1fs\n", p.Slug, lag) - return - } - } - health.WithLabelValues(p.Slug).Set(0) - // "error" only when EVERY poll of the event errored (matches the spec's - // classification). An event with at least one clean "not yet" answer is a - // timeout: reclassifying it as error would drop it from the success-rate - // denominator and let transient blips inflate a provider's score. - if errs == polls && lastErr != nil { - probeTotal.WithLabelValues(p.Slug, "error").Inc() - fmt.Printf(" %-10s error: %s\n", p.Slug, sanitize(lastErr)) - return - } - probeTotal.WithLabelValues(p.Slug, "timeout").Inc() - if lastErr != nil { - fmt.Printf(" %-10s TIMEOUT (not queryable within %ds; %d/%d polls errored, last: %s)\n", p.Slug, pollSchedule[len(pollSchedule)-1], errs, polls, sanitize(lastErr)) - return - } - fmt.Printf(" %-10s TIMEOUT (not queryable within %ds)\n", p.Slug, pollSchedule[len(pollSchedule)-1]) -} - -// --------------------------------------------------------------------------- -// Reference sampler: T0 from OUR keyless RPC (publicnode), never a -// cohort member's endpoint. 500ms head polling bounds the T0 error at -// +500ms, identical for every provider. -// --------------------------------------------------------------------------- - -var httpClient = &http.Client{Timeout: 8 * time.Second} - -func rpcCall(method, params string) (json.RawMessage, error) { - body := fmt.Sprintf(`{"jsonrpc":"2.0","method":"%s","params":%s,"id":%d}`, method, params, time.Now().UnixNano()) - req, err := http.NewRequest("POST", rpcHTTP(), strings.NewReader(body)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", "OpenChainBench/1.0 (+https://openchainbench.com)") - resp, err := httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - raw, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20)) - if err != nil { - return nil, err - } - var env struct { - Result json.RawMessage `json:"result"` - Error *struct { - Message string `json:"message"` - } `json:"error"` - } - if err := json.Unmarshal(raw, &env); err != nil { - return nil, err - } - if env.Error != nil { - return nil, fmt.Errorf("rpc: %s", env.Error.Message) - } - return env.Result, nil -} - -func headBlock() (uint64, error) { - res, err := rpcCall("eth_blockNumber", "[]") - if err != nil { - return 0, err - } - var hexN string - if err := json.Unmarshal(res, &hexN); err != nil { - return 0, err - } - return strconv.ParseUint(strings.TrimPrefix(hexN, "0x"), 16, 64) -} - -type rpcLog struct { - TransactionHash string `json:"transactionHash"` - Topics []string `json:"topics"` -} - -// recipient extracts the ERC-20 Transfer `to` address from topics[2] -// (32-byte left-padded). Empty string when the log shape is unexpected. -func recipient(l rpcLog) string { - if len(l.Topics) < 3 || len(l.Topics[2]) != 66 { - return "" - } - return "0x" + l.Topics[2][26:] -} - -// usdcTransfersInBlock returns the tx hashes of USDC Transfer logs in -// block bn, via eth_getLogs on the reference RPC. -func usdcTransfersInBlock(bn uint64) ([]rpcLog, error) { - params := fmt.Sprintf(`[{"fromBlock":"0x%x","toBlock":"0x%x","address":"%s","topics":["%s"]}]`, bn, bn, usdcContract, transferTopic) - res, err := rpcCall("eth_getLogs", params) - if err != nil { - return nil, err - } - var logs []rpcLog - if err := json.Unmarshal(res, &logs); err != nil { - return nil, err - } - return logs, nil -} - -// waitFreshEvent polls the reference RPC head until a block newer than -// lastSeen containing at least one USDC Transfer shows up. T0 is the -// instant we first observed the new head. Returns (0, "", zero) only -// when ctx is cancelled. -func waitFreshEvent(ctx context.Context, lastSeen uint64) (probeEvent, time.Time) { - for { - select { - case <-ctx.Done(): - return probeEvent{}, time.Time{} - default: - } - bn, err := headBlock() - if err != nil { - fmt.Printf("[sampler] head poll error: %s\n", sanitize(err)) - time.Sleep(2 * time.Second) - continue - } - if bn > lastSeen { - t0 := time.Now() - logs, err := usdcTransfersInBlock(bn) - if err != nil { - fmt.Printf("[sampler] getLogs error: %s\n", sanitize(err)) - time.Sleep(time.Second) - continue - } - if len(logs) > 0 { - pick := logs[rand.Intn(len(logs))] - if strings.HasPrefix(pick.TransactionHash, "0x") && len(pick.TransactionHash) == 66 && recipient(pick) != "" { - return probeEvent{Block: bn, TxHash: strings.ToLower(pick.TransactionHash), Wallet: strings.ToLower(recipient(pick))}, t0 - } - } - // Rare on ETH mainnet: block without a USDC transfer. Skip it. - lastSeen = bn - } - time.Sleep(500 * time.Millisecond) - } -} - -// --------------------------------------------------------------------------- -// Shared helpers. -// --------------------------------------------------------------------------- - -// bodyContains — deliberately parser-free detection: lowercase the raw -// response body and look for the tx hash substring. Immune to -// per-provider schema churn; no response shape is favoured. -func bodyContains(resp *http.Response, txHash string) (bool, error) { - defer resp.Body.Close() - raw, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) - if err != nil { - return false, err - } - if resp.StatusCode != 200 { - return false, fmt.Errorf("status %d", resp.StatusCode) - } - return bytes.Contains(bytes.ToLower(raw), []byte(strings.ToLower(txHash))), nil -} - -// sanitize strips anything URL-ish from provider errors before logging -// (the Alchemy key rides in its URL; never let it reach stdout/loghub). -func sanitize(err error) string { - if err == nil { - return "" - } - s := err.Error() - if i := strings.Index(s, "http"); i >= 0 { - return s[:i] + "" - } - return s -} - -// --------------------------------------------------------------------------- -// Monthly quota guard (same design as indexing-freshness / rpc-keyed-latency). -// --------------------------------------------------------------------------- - -type quotaGuard struct { - mu sync.Mutex - month string - counts map[string]int64 -} - -var quota = "aGuard{counts: make(map[string]int64)} - -func (q *quotaGuard) allow(p Provider) bool { - q.mu.Lock() - defer q.mu.Unlock() - m := time.Now().UTC().Format("2006-01") - if m != q.month { - q.month = m - q.counts = make(map[string]int64) - } - used := q.counts[p.Slug] - ratio := float64(used) / float64(p.Budget) - quotaUsedRatio.WithLabelValues(p.Slug).Set(ratio) - if ratio >= 0.90 { - return false - } - // Reserve the worst case for one event (full poll schedule). - q.counts[p.Slug] = used + int64(len(pollSchedule)) - return true -} diff --git a/harnesses/indexer-latency/cmd/script/metrics.go b/harnesses/indexer-latency/cmd/script/metrics.go deleted file mode 100644 index 4ee048d7..00000000 --- a/harnesses/indexer-latency/cmd/script/metrics.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -var ( - latencyHist = promauto.NewHistogramVec( - prometheus.HistogramOpts{ - Name: "idx_latency_seconds", - Help: "Histogram of time-to-queryable lags: seconds between our reference RPC first showing block N and the provider first returning block N's probe event.", - Buckets: []float64{1, 2, 3, 5, 8, 12, 20, 30, 45, 60, 90, 120, 180, 240, 360, 600}, - }, - []string{"provider"}, - ) - - latencyLast = promauto.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "idx_latency_last_seconds", - Help: "Latest observed time-to-queryable lag per provider (drives the bench page time series).", - }, - []string{"provider"}, - ) - - probeTotal = promauto.NewCounterVec( - prometheus.CounterOpts{ - Name: "idx_probe_total", - Help: "Probe outcomes per provider: found | timeout (not queryable within 240s) | error | quota_paused | disabled (credential missing).", - }, - []string{"provider", "result"}, - ) - - quotaUsedRatio = promauto.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "idx_quota_used_ratio", - Help: "Fraction of the provider's monthly call budget consumed (probing pauses at 0.90, calendar-month reset).", - }, - []string{"provider"}, - ) - - health = promauto.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "idx_health", - Help: "1 when the provider's last probe found the event, 0 on timeout/error/disabled. Doubles as the harness freshness signal.", - }, - []string{"provider"}, - ) - - apiCalls = promauto.NewCounterVec( - prometheus.CounterOpts{ - Name: "idx_api_calls_total", - Help: "API calls issued per provider (feeds the monthly quota guard).", - }, - []string{"provider"}, - ) -) - -func StartMetricsServer(addr string) error { - mux := http.NewServeMux() - mux.Handle("/metrics", promhttp.Handler()) - mux.Handle("/logs", logsHandler()) - mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("ok")) - }) - return http.ListenAndServe(addr, mux) -} diff --git a/harnesses/indexer-latency/cmd/script/mobula.go b/harnesses/indexer-latency/cmd/script/mobula.go deleted file mode 100644 index 6eb83fad..00000000 --- a/harnesses/indexer-latency/cmd/script/mobula.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "fmt" - "net/http" -) - -// Mobula poller. -// -// Probed entity: the exact USDC Transfer (matched by tx hash) via the -// wallet-transactions endpoint of the RECIPIENT address captured from -// the Transfer log's topics. This is Mobula's indexed wallet-history -// pipeline, the same product surface bench 070 races on Base native -// transfers; here it answers the 084 question (Ethereum ERC-20 event -// time-to-queryable) so the two benches stay complementary, not -// duplicated. -// -// Disclosure (also in the spec methodology): Mobula operates -// OpenChainBench. The poller runs the exact probe/quota rules as every -// other cohort member and the query is keyless-reproducible modulo the -// free API key. -// -// limit=100: probe recipients are arbitrary USDC receivers, sometimes -// exchange hot wallets busy enough to bury a minutes-old tx below a -// short first page. 100 rows keeps burial (a false "not yet") rare; -// residual risk is disclosed as upper-bound semantics. - -func checkMobula(ev probeEvent) (bool, error) { - apiCalls.WithLabelValues("mobula").Inc() - u := fmt.Sprintf("https://api.mobula.io/api/1/wallet/transactions?wallet=%s&limit=100", ev.Wallet) - req, err := http.NewRequest("GET", u, nil) - if err != nil { - return false, err - } - req.Header.Set("Authorization", mobulaAPIKey()) - req.Header.Set("User-Agent", "OpenChainBench/1.0 (+https://openchainbench.com)") - resp, err := httpClient.Do(req) - if err != nil { - return false, err - } - return bodyContains(resp, ev.TxHash) -} diff --git a/harnesses/indexer-latency/cmd/script/thegraph.go b/harnesses/indexer-latency/cmd/script/thegraph.go deleted file mode 100644 index e5a1efb9..00000000 --- a/harnesses/indexer-latency/cmd/script/thegraph.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "strings" -) - -// The Graph poller. -// -// The Graph only serves what subgraphs index, so we cannot ask it for a -// raw USDC Transfer log. Probed entity instead: the canonical Uniswap -// V3 Ethereum subgraph queried WITH a block constraint pinned to the -// reference block N. graph-node rejects block-constrained queries until -// the subgraph has indexed block N ("only indexed up to block ..."), so -// a successful data response is exactly "block N data queryable". This -// per-provider entity asymmetry is disclosed in the bench methodology; -// the measured question (block N queryable at T?) is identical. -// -// Gateway free plan (100k queries/month) requires GRAPH_API_KEY; the -// key travels in the Authorization header, never in the URL. - -const uniswapV3SubgraphID = "5zvR82QoaXYFyDEKLZ9t6v9adgnptxYpKpSbxtgVENFV" - -func checkTheGraph(ev probeEvent) (bool, error) { - bn := ev.Block - apiCalls.WithLabelValues("thegraph").Inc() - q := fmt.Sprintf(`{"query":"{ pools(block: {number: %d}, first: 1) { id } }"}`, bn) - u := "https://gateway.thegraph.com/api/subgraphs/id/" + uniswapV3SubgraphID - req, err := http.NewRequest("POST", u, strings.NewReader(q)) - if err != nil { - return false, err - } - req.Header.Set("Authorization", "Bearer "+graphAPIKey()) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", "OpenChainBench/1.0 (+https://openchainbench.com)") - resp, err := httpClient.Do(req) - if err != nil { - return false, err - } - defer resp.Body.Close() - raw, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) - if err != nil { - return false, err - } - if resp.StatusCode != 200 { - return false, fmt.Errorf("status %d", resp.StatusCode) - } - var out struct { - Data json.RawMessage `json:"data"` - Errors []struct { - Message string `json:"message"` - } `json:"errors"` - } - if err := json.Unmarshal(raw, &out); err != nil { - return false, err - } - if len(out.Errors) > 0 { - msg := strings.ToLower(out.Errors[0].Message) - // graph-node's "block N not yet indexed" family of errors is the - // expected pre-sync answer, not an API failure. - if strings.Contains(msg, "indexed up to") || strings.Contains(msg, "has only indexed") || strings.Contains(msg, "missing block") || strings.Contains(msg, "not yet") { - return false, nil - } - return false, fmt.Errorf("graphql: %s", out.Errors[0].Message) - } - return len(out.Data) > 0 && string(out.Data) != "null", nil -} diff --git a/harnesses/indexer-latency/go.mod b/harnesses/indexer-latency/go.mod deleted file mode 100644 index c07b9b31..00000000 --- a/harnesses/indexer-latency/go.mod +++ /dev/null @@ -1,18 +0,0 @@ -module indexer-latency - -go 1.24.0 - -require github.com/prometheus/client_golang v1.23.2 - -require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/sys v0.35.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect -) diff --git a/harnesses/indexer-latency/go.sum b/harnesses/indexer-latency/go.sum deleted file mode 100644 index d6b8ca98..00000000 --- a/harnesses/indexer-latency/go.sum +++ /dev/null @@ -1,46 +0,0 @@ -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/lib/removed-benches.ts b/src/lib/removed-benches.ts index 49b665af..5c4690f2 100644 --- a/src/lib/removed-benches.ts +++ b/src/lib/removed-benches.ts @@ -32,6 +32,12 @@ export const REMOVED_BENCH_SLUGS = new Set([ // duplicate of solana-tx-landing (bench 016); the 027 active-probe // variant never got data on prod and shows an empty placeholder "solana-tx-landing-latency", + // indexer-latency (084) dropped entirely 2026-07-16: HyperSync + The + // Graph providers require paid credentials for the sustained cadence + // (Mobula alone left the leaderboard single-provider). Spec + harness + // removed; kept in the 410 list so any indexed URL returns Gone + // instead of 404. + "indexer-latency", // staging pipeline, held back until validated / announced "indexing-freshness", "rpc-keyed-latency", @@ -46,6 +52,5 @@ export const REMOVED_BENCH_SLUGS = new Set([ // follows suit and is not gated. "oracle-freshness", "rpc-reliability", - "indexer-latency", "evm-block-builders", ]); diff --git a/src/lib/spec.ts b/src/lib/spec.ts index 41b95334..009f2a80 100644 --- a/src/lib/spec.ts +++ b/src/lib/spec.ts @@ -289,7 +289,10 @@ const loadBenchmarkUnfilteredCached = unstable_cache( // parity with the base + solana siblings + ungated on prod. Bench SET // changed (old slug gone, new slug live). Bump to purge cached v29 that // still keys the old slug. - ["bench-unfiltered-v30", process.env.VERCEL_ENV === "production" ? "prod" : "all"], + // v31: 084 indexer-latency dropped entirely (spec + harness deleted; + // HyperSync + The Graph required paid credentials for sustained + // cadence). Bench SET shrank. + ["bench-unfiltered-v31", process.env.VERCEL_ENV === "production" ? "prod" : "all"], { revalidate: 300, tags: ["benchmarks"] }, ); @@ -447,7 +450,8 @@ const loadAllBenchmarksCached = unstable_cache( // v31: bumped with bench-unfiltered-v28 (bench vague 2, 081-085). // v32: bumped with bench-unfiltered-v30 (081 slug rename ws-head-latency // -> ws-head-latency-ethereum + ungate on prod). - ["all-benchmarks-v33", process.env.VERCEL_ENV === "production" ? "prod" : "all"], + // v34: bumped with bench-unfiltered-v31 (084 indexer-latency dropped). + ["all-benchmarks-v34", process.env.VERCEL_ENV === "production" ? "prod" : "all"], { revalidate: 300, tags: ["benchmarks"] }, ); export const loadAllBenchmarks = cache(loadAllBenchmarksCached);