Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

4 watching

Forks

Repository files navigation

client-nodes-reporter

A small Go CLI that:

  1. Scrapes Ethereum execution-layer client distribution data (currently from ethernodes.org),
  2. Records one row per run in a Notion database,
  3. Posts a Slack message summarising today's count and a 35-day trend chart (rendered via QuickChart).

It is designed to be run as a one-shot job — locally, in a container, or via any scheduler (GitHub Actions, Kubernetes CronJob, systemd timer, plain cron, etc.).

Architecture

  • cmd/ — Cobra root command, flag parsing, wiring.
  • datasources/ — implementations of the DataSource interface that scrape upstream sites.
  • database/ — Notion read/write (AddClientData, GetLatestData).
  • notifier/ — Slack message + QuickChart line graph.
  • configs/ — client-type enum (Nethermind, Geth, Besu, Erigon, Reth).

Configuration

All settings can be provided as a flag or as an environment variable.

Flag Env var Default Notes
--source, -s — ethernodes ethernodes or ethernets
--client, -c — nethermind nethermind, geth, besu, erigon, reth
--debug, -d — false sets log level to debug
--log-format, -f REPORTER_LOG_FORMAT json json or text
--skip-update — false skip scraping and Notion write; only read history and post to Slack
--notion-db REPORTER_NOTION_DB — required — Notion database ID
--notion-token REPORTER_NOTION_TOKEN — required — Notion integration token
--slack-app-token REPORTER_SLACK_APP_TOKEN — required — Slack bot token
--slack-channel REPORTER_SLACK_CHANNEL — required — channel name or ID
--flaresolverr-url REPORTER_FLARESOLVERR_URL — optional — FlareSolverr v1 endpoint (e.g. http://localhost:8191/v1). When set, all ethernodes fetches are proxied through it. See Cloudflare workaround.
--max-retries — 3 maximum retry attempts per fetch
--retry-delay — 1s initial backoff between retries (doubled on each attempt)

Running locally — from source

Requires Go 1.25+.

export $(grep -v '^#' .env | xargs)
go run main.go --debug --client nethermind --source ethernodes

Use --skip-update to avoid writing to Notion while still posting the Slack report (useful for chart-only re-runs).

Running locally — Docker

docker build -t client-nodes-reporter:local .
docker run --rm --env-file .env client-nodes-reporter:local \
  --debug --client nethermind --source ethernodes

Or, with the published image:

docker run --rm --env-file .env \
  ghcr.io/nethermindeth/client-nodes-reporter:latest \
  --client nethermind --source ethernodes

Cloudflare workaround (FlareSolverr)

ethernodes.org sits behind Cloudflare, which can return HTTP 403 to direct requests from datacenter IPs or any client whose TLS fingerprint and header order don't look like a real browser. The scraper detects this and fails loudly rather than parsing garbage.

To work around it, the tool can route fetches through a FlareSolverr instance — an HTTP proxy that drives a real headless Chromium, which fixes both the TLS fingerprint and any JS challenges Cloudflare wants solved. Set --flaresolverr-url (or REPORTER_FLARESOLVERR_URL) to its /v1 endpoint and all ethernodes fetches go through it; leave it empty and the tool does a direct fetch with browser-shaped headers (plus a colly fallback).

Caveat: FlareSolverr beats challenge-layer blocks (JS check, fingerprint, header order). It does not beat pure IP-reputation blocks — the FlareSolverr-issued request still leaves from the same egress IP as the host running it. If Cloudflare keeps serving 403 even with FlareSolverr in front, the IP is the problem and the host needs to be on an egress Cloudflare considers benign.

Running locally with FlareSolverr

docker run -d --rm --name flaresolverr -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest
# wait ~20s for Chromium to boot
export $(grep -v '^#' .env | xargs)
REPORTER_FLARESOLVERR_URL=http://localhost:8191/v1 \
  go run main.go --debug --client nethermind --source ethernodes
docker stop flaresolverr

Running on a schedule

The binary is a one-shot, so any scheduler that can run a container or a binary once a day works. The repo includes one example: .github/workflows/ethernodes-scrape.yml, a GitHub Actions workflow that brings up a FlareSolverr service container next to the scraper and triggers the published image on a daily cron (also triggerable manually with a skip-update toggle).

For other schedulers (Kubernetes CronJob, systemd timer, plain cron) the recipe is the same:

  1. Make sure a FlareSolverr instance is reachable from wherever the binary runs (sidecar container, peer pod, separate host).
  2. Provide the four REPORTER_* secrets via env or .env.
  3. Set REPORTER_FLARESOLVERR_URL to the FlareSolverr /v1 endpoint.
  4. Invoke the binary or the published image once per day.

Data sources

ethernodes (default)

  • Total + per-client counts come from the main page at https://ethernodes.org.
  • Per-client synced count comes from https://ethernodes.org/client/el/<client>?synced=1.
  • Overall execution-layer synced count comes from https://ethernodes.org/sync.
  • The site sits behind Cloudflare; see Cloudflare workaround above for how to route through FlareSolverr. If a fetch returns a Cloudflare challenge or block page, the run fails loudly (no silent fallbacks).

ethernets

  • Synced totals (network + per-client) come from https://www.ethernets.io/?synced=yes.
  • Unsynced totals come from https://www.ethernets.io/?synced=no.
  • The two are summed to produce the overall network total and per-client total; the synced page directly yields the synced counts.
  • No Cloudflare in front of the site, so FlareSolverr is not needed.

Nethermind state-scheme report

reporter nethermind-state-scheme is a separate, Nethermind-specific subcommand that tracks how the Nethermind execution-layer fleet is moving from pre-v2 releases to v2, and on v2, how many nodes run the halfpath vs the flat state scheme. It reads node data from the public enrscout API, stores one row per run in its own Notion database, and posts a Slack summary table with a stacked bar chart of node counts per bucket over time.

Nodes are grouped by client_version:

Bucket Rule Example
Pre V2 major version < 2 (or unparseable) v1.39.3+28cbe2a0
V2 Halfpath v2+, ends in -hp v2.0.0+bec830cd-hp
V2 Flat v2+, ends in -f v2.0.0+bec830cd-f
V2 Other v2+, any other suffix v2.0.0+bec830cd-fit

Every node enrscout lists is counted. Nodes whose fingerprint is not fresh (fp_status != ok) are also counted separately as Stale. enrscout's methodology version is stored with each row, and the Slack message warns when it changes between runs.

REPORTER_STATE_SCHEME_NOTION_DB=<database id> \
  go run main.go nethermind-state-scheme --debug --log-format text

The subcommand shares --notion-token, --slack-app-token, --slack-channel, --max-retries, --retry-delay, --skip-update, --debug and --log-format with the root command, and adds:

Flag Env var Default Notes
--notion-db REPORTER_STATE_SCHEME_NOTION_DB — required — must be a different database from REPORTER_NOTION_DB
--network — mainnet mainnet, hoodi or sepolia

The Notion database needs these properties (names and types must match), and it must be shared with the integration:

Property Type
Name Title
Network Select
Total, Pre V2, V2 Halfpath, V2 Flat, V2 Other, Stale, Network EL Total Number
Methodology Text
Snapshot At Date
Created time Created time

Charts are uploaded via QuickChart's /chart/create endpoint, because the inline URL for this many series is longer than Slack's image URL limit.

Adding a new client

  1. Add a new ClientType constant in configs/configs.go.
  2. Extend ClientTypeFromString and ClientType.String to handle the new value.
  3. In datasources/ethernodes.go, extend getClientURLName (URL slug) and matchesClientName (display-name → enum match) for the new client.

Releasing

Pushing to main or pushing a tag publishes to GHCR via .github/workflows/docker-publish.yml:

  • Push to main → :latest + :main + :sha-<short>
  • Push a tag vX.Y.Z → :X.Y.Z + :X.Y (plus latest/main/sha- if on default branch)

For the workflow to push to GHCR, the repo must have Settings → Actions → General → Workflow permissions → Read and write permissions enabled.

Known limitations / future work

  • No automated tests for the scrapers. Selector drift on either source will only surface as a failed run; if the Slack message stops appearing, run the binary locally with --debug to see which step failed.
  • Cloudflare can 403 direct requests from datacenter IPs against ethernodes.org. Route through FlareSolverr, or run from an egress Cloudflare considers benign. If FlareSolverr alone is not enough (IP-reputation block), the only remedy is to move the egress.
  • The ethernodes scraper only requests gzip+deflate (not brotli) to avoid pulling in a brotli decoder. Cloudflare currently honours this; if it ever stops, add github.com/andybalholm/brotli and teach readMaybeGzip about Content-Encoding: br.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages