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
157 changes: 93 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,99 +1,128 @@
# OpenChainBench

> An open, reproducible benchmark series for crypto infrastructure — aggregators, bridges, price feeds, and more. Live at [openchainbench.xyz](https://openchainbench.xyz).
> Open, reproducible benchmarks for crypto infrastructure — aggregators, bridges, RPCs, price feeds. Same metric, same conditions, every provider. Live at [openchainbench.xyz](https://openchainbench.xyz).

OpenChainBench is a "field journal" — a research-paper-styled site that publishes one benchmark at a time, each one shipping with the script that produced it. The goal is to make performance an observable property of crypto infra, the way uptime is for SaaS.
OpenChainBench publishes one benchmark at a time, each one shipping with the script that produces its data. The goal is to make performance an observable property of crypto infra — measured in the open, by anyone who wants to add a provider or a metric.

The project is **sponsored by [Mobula](https://mobula.io)** and **edited independently**: every provider (sponsor included) goes through the same harness with the same inputs, and corrections are published in the open.
The project is community-run, MIT-licensed, and accepts PRs from any party including the providers it benchmarks.

## What's inside

```
benchmarks/ Spec files — one YAML per report
├── README.md Spec reference + submission guide
└── *.yml A spec wires editorial metadata to PromQL
src/
├── app/ Next.js 16 App Router
│ ├── page.tsx Front page
│ ├── benchmarks/
│ │ ├── page.tsx Index
│ │ └── [slug]/page.tsx One paper-styled report per slug
│ ├── methodology / about / press / opengraph-image / icon
│ └── globals.css Paper theme
├── components/
│ ├── site-header / site-footer
│ ├── byline · big-number · section-rule · figure
│ ├── range-chart Editorial range plot (p50→p99 + median)
│ ├── ledger-table Stock-exchange-style table w/ sparklines
│ ├── region-grid Tufte small multiples per region
│ └── sparkline 24h trend SVG
├── data/
│ ├── benchmarks.ts Mock data + async loaders
│ └── site.ts Site constants
└── lib/
├── prometheus.ts HTTP API client (instant + range queries)
├── spec.ts YAML loader, overlay-live-on-mock policy
└── format.ts / utils.ts
benchmarks/ Spec files — one YAML per published benchmark
├── aggregator-head-lag.yml
├── bridge-quote-latency.yml
├── bridge-fee.yml
└── README.md Spec format reference + submission guide

harnesses/ The runners that produce the metrics
├── aggregator-head-lag/ Go service: WebSocket monitor for indexation lag
├── bridge-quote/ Go service: 4-bridge quote latency + fees
├── bridge-fee/ Go service: cost-percent comparison
└── README.md Contract for new harnesses

src/ Next.js 16 site (App Router, ISR, Tailwind v4)
├── app/ Pages — overview, benchmarks index, [slug] reports
├── components/ time-series-chart, ledger-table, region-grid, …
├── data/ Spec loader (YAML → Prometheus → Benchmark[])
└── lib/ Prometheus client, spec schema (Zod), formatting

scripts/ pnpm validate, pnpm spec:dry-run
docs/ Methodology, ADRs, style guide
```

Tailwind v4 with a paper aesthetic — cream background, Source Serif 4 for body, Inter for UI labels, JetBrains Mono for figures.
## How a benchmark gets data

```
[harness] ──── push metrics ────▶ [Prometheus]
▲ │
│ │ PromQL queries
│ runs 24/7 on Railway │ (defined in YAML)
│ ▼
│ [benchmarks/<slug>.yml]
│ │
│ │ resolved server-side
│ │ at request time
│ ▼
│ [Next.js site] ── ISR 60s
│ │
│ ▼
│ openchainbench.xyz/benchmarks/<slug>
└──── source code lives in harnesses/<slug>/, deployed from this repo
```

The harness is the source of truth: it calls real provider endpoints, measures latency / cost / success, and pushes Prometheus metrics with the labels declared in the spec. The site never fakes numbers — if the harness stops emitting, the affected percentiles disappear from the page rather than fall back to placeholders.

## Architecture

| Layer | Where it runs | Why |
|---|---|---|
| Site (Next.js, ISR) | Vercel | Static pages with 60s revalidate, edge cache |
| Prometheus | Railway | Time-series DB, 24/7 |
| Harnesses (Go) | Railway | Long-running WebSockets, schedulers, on-chain signing |

Vercel and Railway are intentionally split: Vercel can't host long-lived WebSocket connections or sign on-chain transactions; Railway can't serve a globally cached Next.js site at the same cost. They communicate over HTTPS — the site queries Prom URLs declared in each YAML spec.

## Running locally
## Running the site locally

```bash
pnpm install
pnpm dev
pnpm dev # http://localhost:3000
```

Open [localhost:3000](http://localhost:3000).

## How a benchmark gets data
The site reads every `benchmarks/*.yml` at request time. Specs whose Prometheus URL the runtime can't reach render as drafts (no numbers, methodology only).

```bash
pnpm validate # schema-lint every spec in benchmarks/
pnpm spec:dry-run <slug> # query Prometheus and print numbers, no rendering
pnpm build # production build
```
benchmarks/<slug>.yml ── PromQL ──► Prometheus
│ │
│ (fail / empty / no URL) │ (live numbers)
▼ ▼
src/data/benchmarks.ts (mock fallback) ─► Benchmark[]
Paper report at /benchmarks/<slug>

## Running a harness locally

Each harness has its own README with run instructions. They are independent Go programs (one per benchmark) that you can build with `go run ./cmd/...` or via the included Dockerfile.

```bash
cd harnesses/aggregator-head-lag
cp .env.example .env # fill in API keys
docker-compose up -d # local Prom + monitor + Grafana
```

Pages revalidate every minute (Next.js ISR). Live numbers replace mock numbers as soon as the harness starts emitting metrics with the labels the spec expects. The site never half-renders: any missing percentile and the page falls back to the mock so readers can't be misled about what's real.
Set `prom_url` in the corresponding YAML to your local Prom (`http://localhost:9090`) to render the site against your own data.

## Adding a benchmark

1. **Editorial mock.** Append an entry to `MOCK_BENCHMARKS` in `src/data/benchmarks.ts` with title, abstract, methodology, findings and placeholder numbers per provider. This is what readers see until the harness fills Prometheus.
2. **Harness.** Drop the script that emits metrics into `harnesses/<slug>/` (TypeScript, Bun or Python — whatever fits). The harness runs continuously and pushes to Prometheus / writes to a Pushgateway.
3. **Spec.** Create `benchmarks/<slug>.yml` with the Prometheus URL and the PromQL queries (see `benchmarks/aggregator-quote-latency.yml`).
4. **PR.** The build picks the spec up automatically.

The shape of a benchmark report is fixed: abstract → range chart → ledger table → region grid → findings → methodology → citation. Don't drift; readers learn the format and skim by it.
Full guide in [CONTRIBUTING.md](./CONTRIBUTING.md). Short version:

## Data provenance
1. **Open an issue** with the [new-benchmark template](https://github.com/OpenChainBench/OpenChainBench/issues/new?template=new-benchmark.md). Sketch the metric, providers, methodology — get feedback before you build.
2. **Write the spec** at `benchmarks/<slug>.yml`. Format documented in [`benchmarks/README.md`](./benchmarks/README.md), validated by `src/lib/spec-schema.ts`.
3. **Build the harness** in `harnesses/<slug>/`. Any language works as long as it pushes Prometheus metrics with the labels your spec references. See the existing harnesses as reference.
4. **Open a PR.** CI runs schema validation, typecheck, lint, and build. Once green and merged: the site picks up the new spec automatically; a maintainer wires the harness into Railway (one-time setup per benchmark).

Each report links its harness in the source line. Every run stores raw transcripts (request + response, timestamps, region) so any single data point can be audited after the fact. If a number is wrong we publish a dated correction in place; future issues acknowledge it on the masthead.
Hosting trade-off: light harnesses (one HTTP poll loop, no secrets) can be deployed onto the OpenChainBench Railway. Harnesses that hold wallets, sign transactions, or otherwise represent capital must run from infra owned by the contributor — they push metrics to a public Prom endpoint and the site queries it the same way.

## Editorial policy
## Editorial conventions

- Mobula does not see results before publication and cannot block a report.
- The benchmark plan (providers, routes, cadence, timeout) is committed before each run.
- Where Mobula loses, we publish that. Where it wins, we explain why.
- All code is MIT, all reports are CC-BY-4.0.
- **No pre-determined winners.** Specs do not declare a "best" provider. The leader on every page is computed at render time from the lowest p50.
- **Tail before mean.** Headlines use p50 and p99. The arithmetic mean is reported in the table but never used as a takeaway.
- **State the timeout.** Failures are excluded from latency aggregates and counted toward success rate. Both numbers are reported.
- **Methodology first.** A spec without a written methodology is rejected.
- **Corrections in place.** If a number is wrong we publish a dated note on the affected report; future readers see it on the masthead.

## Stack

- Next.js 16 (App Router) on Vercel
- Tailwind v4 (CSS-only theme)
- Source Serif 4 / Inter / JetBrains Mono via `next/font`
- Static rendering — every benchmark page is pre-rendered, with ISR for live runs once the harness is wired in.
- Next.js 16 (App Router, ISR, Turbopack) on Vercel
- Tailwind v4 (CSS-only theme, `@theme` tokens)
- Source Serif 4 / Inter Tight / JetBrains Mono via `next/font`
- Zod for spec validation
- Prometheus HTTP API (instant + range queries)
- Go 1.24 for the existing harnesses (any language is acceptable)

## Social
## Links

- Twitter / X — [@openchainbench](https://twitter.com/openchainbench)
- Reddit — [r/openchainbench](https://reddit.com/r/openchainbench)
- GitHub — [mobula/openchainbench](https://github.com/mobula/openchainbench)
- Site — [openchainbench.xyz](https://openchainbench.xyz)
- Twitter — [@openchainbench](https://twitter.com/openchainbench)
- GitHub — [OpenChainBench/OpenChainBench](https://github.com/OpenChainBench/OpenChainBench)

## License

Expand Down
2 changes: 1 addition & 1 deletion benchmarks/bridge-fee.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ methodology:

findings: []

source: https://github.com/OpenChainBench/OpenChainBench/tree/main/harnesses/bridge-fee
source: https://github.com/OpenChainBench/OpenChainBench/tree/main/harnesses/bridge-monitor

prometheus:
url: https://prometheus-production-9ffe.up.railway.app
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/bridge-quote-latency.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ methodology:

findings: []

source: https://github.com/OpenChainBench/OpenChainBench/tree/main/harnesses/bridge-quote
source: https://github.com/OpenChainBench/OpenChainBench/tree/main/harnesses/bridge-monitor

prometheus:
url: https://prometheus-production-9ffe.up.railway.app
Expand Down
59 changes: 34 additions & 25 deletions harnesses/README.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,58 @@
# Harnesses

Each subdirectory holds the actual benchmark script for one report — the thing that runs continuously, calls providers, and pushes metrics to Prometheus.
Each subdirectory holds the runner for one or more benchmarks — the long-running process that calls providers, measures latency / cost / success, and pushes metrics to Prometheus. The OpenChainBench site queries that same Prometheus and renders the data.

```
harnesses/
├── aggregator-quote/ (Bench № 001)
├── bridge-e2e/ (Bench № 002)
├── feed-freshness/ (Bench № 003)
└── rpc-head-lag/ (Bench № 004)
├── aggregator-head-lag/ Bench № 001 — WebSocket monitor (Go)
└── bridge-monitor/ Bench № 002 + № 003 — quote loop + execution (Go)
```

A single harness can serve multiple benchmarks when the same set of measurements is consumed by more than one spec — `bridge-monitor` is the canonical example, producing both `bridge_quote_latency_ms` (read by `bridge-quote-latency.yml`) and `bridge_cost_percent` (read by `bridge-fee.yml`).

## Contract

A harness is whatever you need it to be — Bun, Node, Python, Rust, a cron-runner shell script. The contract it must satisfy is small:
A harness can be written in any language. The contract it must satisfy is small:

| Concern | Requirement |
| -------------- | ----------------------------------------------------------------------------------------------- |
| Inputs | Read provider API keys from env vars, never commit them |
| Loop | Run continuously (or on a fixed cadence ≥ 24h) and push the same metric set every iteration |
| Metric names | Match the names in the matching `benchmarks/<slug>.yml` exactly |
| Labels | At minimum `provider`, `region`. Additional labels (chain, route) are encouraged |
| Push target | `pushgateway` or remote-write to the project's Prometheus |
| Timeouts | Documented, fail closed (count toward success rate) |
| Reproducibility| README explains how to run it locally with one command |
| License | MIT, same as the rest of the repo |
| Concern | Requirement |
| --- | --- |
| Inputs | Read provider API keys / wallet keys from env vars, never commit them |
| Loop | Run continuously and push the same metric set every iteration |
| Metric names | Match the names referenced in the matching `benchmarks/<slug>.yml` exactly |
| Labels | Include `provider` (or equivalent) and `region` at minimum; chain/route labels encouraged |
| Push target | Scrape endpoint exposed for Prometheus, or push to a remote-write endpoint |
| Timeouts | Documented; failures fail closed (counted toward success rate, excluded from latency aggregates) |
| Reproducibility | README explains how to run it locally with one command |
| License | MIT, same as the rest of the repo |

## Subdirectory layout

A harness is expected to ship at minimum:

```
harnesses/<slug>/
├── README.md What it measures, the providers, the labels, env vars, how to run
├── Dockerfile Container image for the runner
├── Makefile or run.sh `make run` (or `./run.sh`) starts the loop
└── … Source files in whatever language fits
├── README.md What it measures, providers, labels, env vars, how to run
├── Dockerfile Container image for the runner
├── .env.example Every env var the runner reads, with placeholders
└── … Source files in whatever language fits
```

The point isn't uniformity for its own sake; it's that any contributor can `cd` into a folder and figure out what's happening in 30 seconds.
If the harness ships a full local stack (Prometheus + Grafana + Alertmanager) it should include a `docker-compose.yml` and a `Makefile` for the common targets (`make run`, `make logs`, `make stop`). The two existing harnesses do.

## Hosting

Two paths exist for getting a harness into production:

1. **OpenChainBench Railway.** Light harnesses (one HTTP loop, no wallets, no signing) can be deployed onto the project's shared Railway. A maintainer wires the service after the PR is merged.
2. **Contributor-hosted.** Harnesses that hold wallets, sign transactions, or otherwise represent capital must run from infrastructure owned by the contributor. They push metrics to a publicly-reachable Prometheus endpoint and the site queries it the same way as the project-hosted harnesses.

Either way the YAML spec's `prom_url` decides which Prometheus the site reads from — the data path is identical.

## Submitting a new harness

See [`/CONTRIBUTING.md`](../CONTRIBUTING.md) for the full submission flow. tldr:
See [`/CONTRIBUTING.md`](../CONTRIBUTING.md) for the full submission flow. Short version:

1. Add the editorial mock in `src/data/benchmarks.ts`.
2. Add the spec in `benchmarks/<slug>.yml`.
3. Add the harness here.
1. Open an issue with the new-benchmark template (sketch the metric, providers, methodology).
2. Write the spec at `benchmarks/<slug>.yml`.
3. Build the harness here at `harnesses/<slug>/`.
4. Open a PR.
34 changes: 34 additions & 0 deletions harnesses/aggregator-head-lag/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Environment variables (contains secrets)
.env

# Build artifacts
benchmark
*.exe
*.dll
*.so
*.dylib

# Test binaries
*.test
*.out

# Go workspace file
go.work

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Logs
*.log

# Temporary files
tmp/
temp/
36 changes: 36 additions & 0 deletions harnesses/aggregator-head-lag/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Build stage
FROM golang:1.24-alpine AS builder

WORKDIR /app

# Install dependencies
RUN apk add --no-cache git

# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY . .

# Build the binary
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/monitor ./cmd/script

# Runtime stage
FROM debian:bookworm-slim

WORKDIR /app

# Install runtime dependencies
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Copy binary from builder
COPY --from=builder /app/monitor /app/monitor

# Expose metrics port
EXPOSE 2112

# Run the monitor
CMD ["/app/monitor"]
Loading
Loading